Skip to main content

lini/
lib.rs

1mod ast;
2mod desugar;
3mod error;
4mod expr;
5mod fmt;
6mod icon;
7mod layout;
8mod lexer;
9mod lint;
10mod palette;
11mod render;
12mod resolve;
13mod serve;
14mod span;
15mod syntax;
16mod theme;
17
18pub use error::{Diagnostic, Error, Level};
19pub use fmt::format as format_source;
20
21/// Lower a source file's sugar to primitives + `.lini-*` classes and print canonical
22/// `.lini` — what `lini desugar` shows: every typed instance becomes a `|primitive|`
23/// wearing its `.lini-*` chain, defines and templates collapse into generated
24/// `.lini-*` class defs, scene/link defaults fill the global block, and labels /
25/// `along:` become explicit. Comments are dropped. The lowered form re-renders
26/// identically and is a fixed point of desugar.
27pub fn desugar_source(src: &str) -> Result<String, Error> {
28    let tokens = lexer::lex(src)?;
29    let file = syntax::parser::parse(&tokens)?;
30    Ok(fmt::print_file(&desugar::desugar(&file)?))
31}
32pub use layout::{Rule, Severity, Violation};
33pub use serve::{ServeTarget, serve};
34pub use theme::{builtin_css, extract_lini_vars, list_themes, pair_css};
35
36/// Top-level compile options threaded through every phase. Build with
37/// `Options::default()` and override fields with the struct-update syntax —
38/// future versions may add knobs.
39#[derive(Clone, Debug, Default)]
40pub struct Options {
41    /// Emit `var()` values inline as their resolved literal so renderers
42    /// without CSS-variable support (resvg, librsvg, image converters) still
43    /// display the diagram correctly. The structural class rules stay; only the
44    /// `@layer` variable defaults are dropped (their values are inlined).
45    pub bake_vars: bool,
46    /// Output wrapper format.
47    pub format: OutputFormat,
48    /// Raw CSS text whose `--lini-*` declarations override built-in defaults
49    /// before the `defaults {}` block. `extract_lini_vars` does the parse.
50    pub theme_css: Option<String>,
51}
52
53#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
54pub enum OutputFormat {
55    #[default]
56    Svg,
57    Html,
58}
59
60pub fn compile_str(src: &str) -> Result<String, Error> {
61    compile_str_with(src, &Options::default())
62}
63
64pub fn compile_str_with(src: &str, opts: &Options) -> Result<String, Error> {
65    let program = resolve_pipeline(src, opts)?;
66    let mut laid_out = layout::layout(&program)?;
67    render::lower_gradients(&mut laid_out);
68    Ok(finish_svg(&laid_out, opts))
69}
70
71/// Compile to SVG **and** collect the routing diagnostics in a single layout
72/// pass. The CLI's default path needs both (the SVG to emit, the diagnostics
73/// to warn); routing through here runs the link router once instead of twice.
74pub fn compile_str_checked(src: &str, opts: &Options) -> Result<(String, Vec<Diagnostic>), Error> {
75    let program = resolve_pipeline(src, opts)?;
76    let mut laid_out = layout::layout(&program)?;
77    render::lower_gradients(&mut laid_out);
78    let diags = routing_diagnostics_of(layout::validate_routing(&laid_out));
79    Ok((finish_svg(&laid_out, opts), diags))
80}
81
82fn finish_svg(laid_out: &layout::LaidOut, opts: &Options) -> String {
83    let svg = render::render(laid_out, opts);
84    match opts.format {
85        OutputFormat::Svg => svg,
86        OutputFormat::Html => wrap_html(&svg),
87    }
88}
89
90/// Lex and parse only — verifies syntactic correctness without running
91/// resolve/layout/render.
92pub fn check_parse(src: &str) -> Result<(), Error> {
93    let tokens = lexer::lex(src)?;
94    let _file = syntax::parser::parse(&tokens)?;
95    Ok(())
96}
97
98/// Lex, parse, and run the lint pass. Returns warnings (no errors).
99/// Parse errors are surfaced as `Err`; missing lints just return an empty Vec.
100pub fn lint_str(src: &str) -> Result<Vec<Diagnostic>, Error> {
101    let tokens = lexer::lex(src)?;
102    let file = syntax::parser::parse(&tokens)?;
103    Ok(lint::lint(&file))
104}
105
106/// Lex, parse, and resolve. Verifies semantic correctness without running
107/// layout or render. The CLI's `--check` flag goes through here.
108pub fn check(src: &str) -> Result<(), Error> {
109    check_with(src, &Options::default())
110}
111
112pub fn check_with(src: &str, opts: &Options) -> Result<(), Error> {
113    let _ = resolve_pipeline(src, opts)?;
114    Ok(())
115}
116
117/// Lex, parse, resolve, lay out, route, then validate the routing against the
118/// contract in LINKING.md. Returns the violations found (empty = clean). Parse
119/// and resolve errors surface as `Err`.
120pub fn validate_str(src: &str) -> Result<Vec<Violation>, Error> {
121    let program = resolve_pipeline(src, &Options::default())?;
122    let laid_out = layout::layout(&program)?;
123    Ok(layout::validate_routing(&laid_out))
124}
125
126/// Surface routing violations as user-facing diagnostics. Crossings are normal,
127/// counted output (`Info`) and stay silent here; everything else — an impossible
128/// link, or a law breach (which would mean an engine bug) — is flagged, never
129/// silent. The CLI prints these as warnings; `--strict` makes them fail the build.
130fn routing_diagnostics_of(violations: Vec<Violation>) -> Vec<Diagnostic> {
131    violations
132        .into_iter()
133        .filter(|v| v.severity != Severity::Info)
134        .map(|v| {
135            Diagnostic::warn(
136                v.span,
137                format!("{} ({}): {}", v.rule.id(), v.links.join(", "), v.detail),
138            )
139        })
140        .collect()
141}
142
143fn resolve_pipeline(src: &str, opts: &Options) -> Result<resolve::Program, Error> {
144    let tokens = lexer::lex(src)?;
145    let file = syntax::parser::parse(&tokens)?;
146    let lowered = desugar::desugar(&file)?;
147    let theme = match &opts.theme_css {
148        Some(css) => theme::extract_lini_vars(css),
149        None => Vec::new(),
150    };
151    resolve::resolve_with_theme(&lowered, &theme)
152}
153
154fn wrap_html(svg: &str) -> String {
155    format!(
156        "<!doctype html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <title>lini</title>\n</head>\n<body>\n{}</body>\n</html>\n",
157        svg
158    )
159}
160
161/// Test-only hooks for the link-routing parameter sweep (see `tests/linking_sweep.rs`).
162/// Not part of the public API.
163#[doc(hidden)]
164pub mod testing {
165    use crate::Options;
166    use crate::layout;
167    use crate::resolve::ResolvedValue;
168
169    pub use crate::layout::LaidOut;
170
171    /// A node's absolute rect by full dot-path, for geometric assertions.
172    pub fn node_rect(laid: &LaidOut, path: &str) -> Option<(f64, f64, f64, f64)> {
173        layout::node_rect(&laid.nodes, path)
174    }
175
176    /// Compile `src` to a laid-out scene with `clearance` forced on every link,
177    /// overriding whatever the source set. Gap growth runs as in production —
178    /// starved corridors may widen the layout.
179    pub fn route_sample(src: &str, clearance: f64) -> LaidOut {
180        layout::layout(&forced(src, clearance)).expect("layout")
181    }
182
183    /// [`route_sample`] with gap growth disabled: the raw router's result, the
184    /// one the clearance sweep measures. `clearance` does not move nodes here,
185    /// so the node geometry — and hence which links are routable — is
186    /// identical across values.
187    pub fn route_sample_raw(src: &str, clearance: f64) -> LaidOut {
188        layout::layout_raw(&forced(src, clearance)).expect("layout")
189    }
190
191    fn forced(src: &str, clearance: f64) -> crate::resolve::Program {
192        let mut prog = super::resolve_pipeline(src, &Options::default()).expect("resolve");
193        for w in &mut prog.links {
194            w.attrs
195                .insert("clearance", ResolvedValue::Number(clearance));
196        }
197        prog
198    }
199
200    /// The number of routable edges the source declares (fans/chains already expanded
201    /// at resolve into one `ResolvedLink` per edge-chain).
202    pub fn declared_edges(src: &str) -> usize {
203        let prog = super::resolve_pipeline(src, &Options::default()).expect("resolve");
204        prog.links
205            .iter()
206            .map(|w| w.endpoints.len().saturating_sub(1))
207            .sum()
208    }
209
210    /// Judge a laid-out scene against the four laws (the independent validator).
211    pub fn laws(laid: &LaidOut) -> Vec<crate::Violation> {
212        layout::validate_routing(laid)
213    }
214}