Oregami
Repositories/oxedyne/fe2o3

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
13pub mod ast;
14pub mod codefig;
15pub(crate) mod lex;
16pub mod lower;
17pub mod mathparse;
18pub mod parse;
19pub mod rules;
20pub mod set;
21
22use crate::doc::Block;
23use crate::doc::Segment;
24
25pub use parse::Refusal;
26pub use parse::RefusalClass;
27pub use parse::Refusals;
28
29use 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.
35pub 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.
56pub(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.
67pub 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`.
72pub 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`].
80pub 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.
89pub 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.
99pub 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.
107pub 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)]
115mod 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}