lex_babel/lib.rs
1//! Multi-format interoperability for Lex documents
2//!
3//! This crate provides a uniform interface for converting between Lex AST and various document
4//! formats (Markdown, HTML, Pandoc JSON, etc.).
5//!
6//! TLDR: For format authors:
7//! - Babel never parses or serializes any format, but instead relies on the format's libraries
8//! - The conversion should be by converting to the IR, running the common code in common if relevant (it usually is), then to the AST of the target format.
9//! - We should use the testing harness (see lex-core/src/lex/testing.rs) to load documents and process them into ASTs.
10//! - Each element should use the harness above and the available file for isolated element testing with unit tests (load with the lib, assert with AST / IR)
11//! - Each format should have trifecta unit tested in from and to formats to lex.
12//! - Each format should have a kitchensink unit tested in from and to formats to lex
13//! - Read the README.lex for full details
14//!
15//! Architecture
16//!
17//! The goal here is to, as much as possible, split what is the common logic for multiple formats
18//! conversions into a format agnostic layer. This is done by using the IR representation (./ir/mod.rs),
19//! and having the common code in ./common/mod.rs. This allows for the format specific code to be focused on the data format transformations, while having a strong, focused core that can be well tested in isolation.
20//!
21//! This is a pure lib, that is, it powers the lexd CLI but is shell agnostic, that is no code
22//! should be written that supposes a shell environment, be it to std print, env vars etc.
23//!
24//! The file structure:
25//! .
26//! ├── error.rs
27//! ├── format.rs # Format trait definition
28//! ├── registry.rs # FormatRegistry for discovery and selection
29//! ├── formats
30//! │ └── <format>
31//! │ ├── parser.rs # Parser implementation
32//! │ ├── serializer.rs # Serializer implementation
33//! │ └── mod.rs
34//! ├── lib.rs
35//! ├── ir # Intermediate Representation
36//! │ ├── nodes.rs # IR data model (DocNode, InlineContent, etc.)
37//! │ ├── events.rs # Flat event stream representation
38//! │ ├── from_lex.rs # Lex AST → IR conversion
39//! │ └── to_lex.rs # IR → Lex AST conversion
40//! └── common # Common mapping code
41//! ├── flat_to_nested.rs # Events → IR tree (with auto-closing)
42//! ├── nested_to_flat.rs # IR tree → Events
43//! └── verbatim/ # Verbatim handler registry (doc.table, doc.image, etc.)
44//!
45//! Testing
46//! tests
47//! └── <format>
48//! ├── <testname>.rs
49//! └── fixtures
50//! ├── <docname>.<format>
51//! ├── kitchensink.html
52//! ├── kitchensink.lex
53//! └── kitchensink.md
54//!
55//! Note that rust does not by default discover tests in subdirectories, so we need to include these
56//! in the mod.
57//!
58//!
59//! Core Algorithms
60//!
61//! The most complex part of the work is reconstructing a nested representation from a flat document, followed by the reverse operations. For this reason we have a common IR (./ir/mod.rs) that is used for all formats.
62//! Over this representation we implement both algorithms (see ./common/flat_to_nested.rs and ./common/nested_to_flat.rs).
63//! This means that all the heavy lifting is done by a core, well tested and maintained module,
64//! freeing format adaptations to be focused on the simpler data format transformations.
65//!
66//!
67//! Formats
68//!
69//! Format specific capabilities are implemented with the Format trait. Formats should have a
70//! parse() and serialize() method, a name and file extensions. See the trait def [./format.rs]
71//! - Format trait: Uniform interface for all formats (parsing and/or serialization)
72//! - FormatRegistry: Centralized discovery and selection of formats
73//! - Format implementations: Concrete implementations for each supported format
74//!
75//!
76//! The Lex Format
77//!
78//! The Lex format itself is implemented as a format, see ./formats/lex/mod.rs, which allows for
79//! a homogeneous API where all formats have identical interfaces.
80//!
81//! Note that Lex is a more expressive format than most, which means that converting from Lex is
82//! simple, but always lossy. In particular converting to Lex requires some consideration on how
83//! to best represent the author's intent.
84//!
85//! This means that full format interop round tripping is not possible.
86//!
87//! v1 Interop Scope
88//!
89//! The full contributor-facing version of this lives in
90//! `comms/docs/interop-scope.lex` (browse on GitHub:
91//! <https://github.com/lex-fmt/comms/blob/main/docs/interop-scope.lex>).
92//! The short version:
93//!
94//! | Tier | Format | Export | Import | Notes |
95//! |-------------------|------------|--------|--------|------------------------------------------------------|
96//! | Core | Markdown | ✓ | ✓ | Lingua franca. Round-trip is the bar. |
97//! | Core | HTML | ✓ | — | Publishing target; PDF and editor previews consume. |
98//! | Core | PDF | ✓ | — | Headless Chrome over the HTML output. |
99//! | Core | PNG | ✓ | — | Headless Chrome screenshot of the HTML output. |
100//! | Stretch | HTML | — | ✓ | After core lands. |
101//! | Experimental | RFC XML | — | ✓ | Proof-of-concept; no bespoke investment. |
102//! | Planned | Pandoc | — | — | Bridge to DOCX/EPUB/RST/Org/etc. Not started. |
103//! | Planned | LaTeX | ✓ | — | Export only, via Pandoc once Pandoc lands. |
104//! | Category error | PDF import | — | — | **Will not be implemented.** See below. |
105//!
106//! **PDF import is a category error, not a postponed task.** PDF is a
107//! presentation format — paragraphs, headings, and lists are
108//! reconstructed heuristically from layout (font size, indentation,
109//! glyph positions), and no rule-based importer recovers that
110//! reliably. Pandoc punts to `pdftotext` for the same reason. This
111//! will not be implemented, ever, regardless of adoption. ML-based
112//! extraction (Marker, Nougat, Mathpix, Grobid) is a different
113//! product category and out of scope for lex-babel. Do not list,
114//! advertise, or design around the possibility.
115//!
116//! Diagnostic outputs (`tag`, `treeviz`, `linetreeviz`) sit outside
117//! this tiering — they're AST visualizers used by `lexd inspect`,
118//! not interop targets.
119//!
120//! Library Choices
121//!
122//! This, not being Lex's core, means that we will offload as much as possible to better, specialized crates
123//! for each format. The scope here is mainly to adapt the ASTs from Lex to the format or vice
124//! versa. For example we never write the serializer for, say, Markdown, but pass the AST to the
125//! Markdown library. To support a format inbound, we write the format AST → Lex AST adapter.
126//! Likewise, for outbound formats we will do the reverse, converting from the Lex AST to the
127//! format's.
128//!
129//! As much as possible, we will use Rust crates, and avoid shelling out and having outside dependencies.
130//!
131pub mod error;
132pub mod format;
133pub mod formats;
134pub mod publish;
135pub mod registry;
136pub mod render_dispatch;
137pub mod templates;
138pub mod transforms;
139
140pub mod common;
141pub mod ir;
142
143pub use error::FormatError;
144pub use format::{Format, SerializedDocument};
145pub use registry::FormatRegistry;
146
147/// Converts a lex document to the Intermediate Representation (IR).
148///
149/// # Information Loss
150///
151/// The IR is a simplified, semantic representation. The following
152/// Lex information is lost during conversion:
153/// - Blank line grouping (BlankLineGroup nodes)
154/// - Source positions and token information
155/// - Comment annotations at document level
156///
157/// For lossless Lex representation, use the AST directly.
158///
159/// Uses the shared default registry ([`default_registry()`]) for
160/// verbatim-label `on_resolve` dispatch. Callers that need to plug
161/// in third-party namespaces (lex-cli with `boot_registry`, lex-lsp,
162/// embedders) should call [`to_ir_with_registry`] directly.
163pub fn to_ir(doc: &lex_core::lex::ast::elements::Document) -> ir::nodes::Document {
164 to_ir_with_registry(doc, default_registry())
165}
166
167/// Converts a Lex AST to its IR representation, dispatching verbatim
168/// labels through the supplied registry's `on_resolve` hooks.
169///
170/// This is the explicit-registry variant of [`to_ir`] — use it when
171/// the caller has its own registry (with third-party namespaces
172/// registered) and wants those handlers to participate in IR
173/// construction.
174pub fn to_ir_with_registry(
175 doc: &lex_core::lex::ast::elements::Document,
176 registry: &lex_extension_host::registry::Registry,
177) -> ir::nodes::Document {
178 ir::from_lex::from_lex_document(doc, registry)
179}
180
181/// Process-wide registry with the built-in `lex.*` schemas
182/// registered. Used by `to_ir` and `to_lex_document` for callers
183/// that don't supply their own. Constructed lazily on first call.
184///
185/// Filesystem access is plumbed through a no-op loader — this
186/// registry doesn't resolve `lex.include` (the to_ir/to_lex paths
187/// never invoke `on_resolve` for that label; includes are resolved
188/// elsewhere in the pipeline).
189pub fn default_registry() -> &'static lex_extension_host::registry::Registry {
190 use lex_core::lex::includes::{LoadError, LoadedFile, Loader, ResolveConfig};
191 use lex_extension_host::registry::Registry;
192 use std::path::Path;
193 use std::sync::{Arc, OnceLock};
194
195 struct NoopLoader;
196 impl Loader for NoopLoader {
197 fn load(&self, path: &Path) -> Result<LoadedFile, LoadError> {
198 Err(LoadError::NotFound {
199 path: path.to_path_buf(),
200 })
201 }
202 }
203
204 static REGISTRY: OnceLock<Registry> = OnceLock::new();
205 REGISTRY.get_or_init(|| {
206 let registry = Registry::new();
207 lex_core::lex::builtins::register_into(
208 ®istry,
209 Arc::new(NoopLoader),
210 ResolveConfig::with_root(std::path::PathBuf::from("/")),
211 )
212 .expect("registering built-in lex.* handlers must succeed for a fresh registry");
213 registry
214 })
215}
216
217/// Converts an IR document back to Lex AST.
218///
219/// This is useful for round-trip conversions: Format → IR → Lex.
220pub fn from_ir(doc: &ir::nodes::Document) -> lex_core::lex::ast::elements::Document {
221 ir::to_lex::to_lex_document(doc)
222}