oxedyne/fe2o3/fe2o3_austenite/src/lang/mod.rs
6.5 KiB, 52 runs
created by r1870400018:36171, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The source front end: Austenite reads Typst markup, so an existing Typst document sets without |
| 2 | //! being rewritten in a new language. [`parse::document`] reads a source string into the surface tree |
| 3 | //! of [`ast::Item`], and [`lower::blocks`] maps that tree onto [`doc::Block`](crate::doc::Block), the |
| 4 | //! authoring vocabulary the two-pass driver already sets. The two steps are kept apart so the surface |
| 5 | //! can grow -- richer parse, same lowering seam -- without disturbing the block layer beneath it. |
| 6 | //! |
| 7 | //! The markup implemented so far is Typst's: headings (`=`), paragraphs, `*strong*` and `_emph_`, |
| 8 | //! bullet (`-`) and numbered (`+`) lists, and the `@label` cross-reference, with a heading labelled by a |
| 9 | //! trailing `<name>` and `\` escaping the next character. Typst code statements -- `#import`, `#let`, |
| 10 | //! `#set`, `#show` -- and whole-line calls to template functions are skipped for now: the styling and |
| 11 | //! computation layer, and inline `$maths$`, code and `#figure`/`#image`, are later increments. |
| 12 | |
| 13 | pub mod ast; |
| 14 | pub mod codefig; |
| 15 | pub(crate) mod lex; |
| 16 | pub mod lower; |
| 17 | pub mod mathparse; |
| 18 | pub mod parse; |
| 19 | pub mod rules; |
| 20 | pub mod set; |
| 21 | |
| 22 | use crate::doc::Block; |
| 23 | use crate::doc::Segment; |
| 24 | |
| 25 | pub use parse::Refusal; |
| 26 | pub use parse::RefusalClass; |
| 27 | pub use parse::Refusals; |
| 28 | |
| 29 | use oxedyne_fe2o3_core::prelude::*; |
| 30 | |
| 31 | /// The 1-based line and column a byte offset falls on within `src`, and the full text of that line (its |
| 32 | /// trailing newline trimmed). The column is a byte offset within the line, not a character count, matching |
| 33 | /// [`crate::ir::Span`]'s own byte-based accounting. Shared by the native binary's `--explain` caret and the |
| 34 | /// wasm surface's `file:line:col` diagnostics, so a refused site reads back to the same position on both. |
| 35 | pub fn line_col_of(src: &str, offset: u32) -> (usize, usize, &str) { |
| 36 | let offset = (offset as usize).min(src.len()); |
| 37 | let mut line_no = 1usize; |
| 38 | let mut line_start = 0usize; |
| 39 | for (i, b) in src.bytes().enumerate() { |
| 40 | if i >= offset { |
| 41 | break; |
| 42 | } |
| 43 | if b == b'\n' { |
| 44 | line_no += 1; |
| 45 | line_start = i + 1; |
| 46 | } |
| 47 | } |
| 48 | let line_end = src[line_start..].find('\n').map(|p| line_start + p).unwrap_or(src.len()); |
| 49 | let col = offset.saturating_sub(line_start) + 1; |
| 50 | (line_no, col, &src[line_start..line_end]) |
| 51 | } |
| 52 | |
| 53 | /// The file a local `#import "<rel>"` written in `dir` names, and its source, as Typst resolves one: |
| 54 | /// relative to the importing file's own directory. `None` for a package import (`@preview/...`), a file |
| 55 | /// that cannot be read, or one `depth` imports down past the walk's cap, which stops a cycle. |
| 56 | pub(crate) fn resolve_import(dir: &std::path::Path, rel: &str, depth: u32) -> Option<(std::path::PathBuf, String)> { |
| 57 | if depth > 4 || rel.starts_with('@') { |
| 58 | return None; |
| 59 | } |
| 60 | let path = dir.join(rel); |
| 61 | crate::vfs::read_to_string(&path).ok().map(|src| (path, src)) |
| 62 | } |
| 63 | |
| 64 | /// Reads one run of Typst inline markup -- prose with `*strong*`, `_emph_`, a maths span or a glossary |
| 65 | /// term -- into the [`Segment`]s the block layer sets, without a surrounding block. The book layer uses |
| 66 | /// it to turn a `term-defs` definition (Typst content, `[...]`) into the runs of a glossary table cell. |
| 67 | pub fn inline_segments(text: &str) -> Vec<Segment> { |
| 68 | lower::lower_runs(&parse::parse_inlines(text)) |
| 69 | } |
| 70 | |
| 71 | /// As [`inline_segments`], each run that asks for something answered for at `site`. |
| 72 | pub fn inline_segments_in(text: &str, site: &crate::ir::Site) -> Vec<Segment> { |
| 73 | lower::lower_runs_in(&parse::parse_inlines(text), site) |
| 74 | } |
| 75 | |
| 76 | /// Parses Typst source and lowers it to the block list the driver authors from, in one step. The usual |
| 77 | /// entry point: a caller that wants the surface tree in between reaches for [`parse::document`] and |
| 78 | /// [`lower::blocks`] directly, and one that wants the report of skipped constructs reaches for |
| 79 | /// [`to_blocks_with_refusals`]. |
| 80 | pub fn to_blocks(src: &str) -> Outcome<Vec<Block>> { |
| 81 | let (blocks, _) = res!(to_blocks_with_refusals(src)); |
| 82 | Ok(blocks) |
| 83 | } |
| 84 | |
| 85 | /// Parses and lowers as [`to_blocks`], and alongside the blocks returns the [`Refusals`] naming every |
| 86 | /// construct the reader passed over -- a `#let`/`#set`/`#show`/`#import` line, an unknown standalone or |
| 87 | /// inline `#func` call, a `#columns` wrapper. A caller prints the summary so a dropped construct is a |
| 88 | /// visible report ("skipped 3 unsupported constructs: #show (2), #columns (1)") rather than a silent gap. |
| 89 | pub fn to_blocks_with_refusals(src: &str) -> Outcome<(Vec<Block>, Refusals)> { |
| 90 | let (items, skips) = res!(parse::document_with_refusals(src)); |
| 91 | Ok((lower::blocks(&items), skips)) |
| 92 | } |
| 93 | |
| 94 | /// As [`to_blocks_with_refusals`], with the `#let` bindings in scope: a furniture call (`#pr-note[ ... ]`, |
| 95 | /// `#aside-box(title: [..])[ ... ]`) expands into a padded box, and a content-binding reference |
| 96 | /// (`#greet("world")`, a bare `#intro`) expands into its re-read markup, rather than either being tallied as |
| 97 | /// a skip. The assembler collects the definitions once (see [`crate::book::collect_scope`]) and threads them |
| 98 | /// into every chapter and lone file it reads. |
| 99 | pub fn to_blocks_with_templates(src: &str, binds: rules::Bindings) -> Outcome<(Vec<Block>, Refusals)> { |
| 100 | let (items, skips) = res!(parse::document_with_templates(src, binds)); |
| 101 | Ok((lower::blocks(&items), skips)) |
| 102 | } |
| 103 | |
| 104 | /// As [`to_blocks_with_templates`], for text read from `file` starting at byte `at` of it: every site the |
| 105 | /// parse records, and every construct in the blocks that asks for something, stands at its place in the |
| 106 | /// file. |
| 107 | pub fn to_blocks_in(src: &str, binds: rules::Bindings, file: &str, at: u32) -> Outcome<(Vec<Block>, Refusals)> { |
| 108 | let (items, mut skips) = res!(parse::document_with_templates(src, binds)); |
| 109 | skips.shift(at); |
| 110 | skips.tag_file(file); |
| 111 | Ok((lower::blocks_in(&items, &lower::SiteBase::new(file, at)), skips)) |
| 112 | } |
| 113 | |
| 114 | #[cfg(test)] |
| 115 | mod tests { |
| 116 | use super::line_col_of; |
| 117 | |
| 118 | /// A pure check of the byte-offset-to-line/column arithmetic the `--explain` caret and the wasm |
| 119 | /// diagnostics both depend on, with no file involved: the third line, its fifth byte (the `d` of "third"). |
| 120 | #[test] |
| 121 | fn line_col_of_finds_the_right_line_and_column() { |
| 122 | let src = "first\nsecond\nthird line\n"; |
| 123 | let offset = src.find("d line").expect("fixture text") as u32; |
| 124 | let (line_no, col, text) = line_col_of(src, offset); |
| 125 | assert_eq!(line_no, 3, "wrong line for offset {}", offset); |
| 126 | assert_eq!(col, 5, "wrong column for offset {}", offset); |
| 127 | assert_eq!(text, "third line"); |
| 128 | } |
| 129 | |
| 130 | /// The very first byte reports line 1, column 1 -- the boundary a fencepost error would miss. |
| 131 | #[test] |
| 132 | fn line_col_of_handles_the_first_byte() { |
| 133 | let (line_no, col, text) = line_col_of("hello\nworld\n", 0); |
| 134 | assert_eq!((line_no, col), (1, 1)); |
| 135 | assert_eq!(text, "hello"); |
| 136 | } |
| 137 | } |