Skip to main content

rustyfi_backend/
context.rs

1use crate::font::FontKey;
2use crate::graphics::Color;
3use crate::length::Length;
4use crate::math::{default_math_class_map, MathCharClass, MathKind};
5use std::collections::BTreeMap;
6use std::sync::Arc;
7
8/// Opaque handle to the context's installed math command (v0.0.6
9/// `context_main.math_command`). The closure VALUE lives lang-side in
10/// `Interp::math_commands` — this crate cannot depend on
11/// `rustyfi_lang::Value`, so this is the same id-into-an-`Interp`-table
12/// seam as `ImageId`/`HookId` (hbox.rs).
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct MathCmdId(pub usize);
15
16/// v0.0.6 `CharBasis.script`, SURFACE subset: the four constructors a
17/// `script` VALUE can carry (`get_script`, evalUtil.ml:235-241; the port's
18/// `script` variant decl, prim_types.rs `script_decl`). Upstream's
19/// internal-only `CommonNarrow`/`CommonWide`/`Inherited` (charBasis.ml:11-13)
20/// arise solely inside the char decoder; context storage never sees them.
21/// Discriminants index `langsys_scheme`.
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum Script {
24    HanIdeographic = 0,
25    /// Upstream internal name `HiraganaOrKatakana`; surface ctor `Kana`.
26    Kana = 1,
27    Latin = 2,
28    OtherScript = 3,
29}
30
31/// v0.0.6 `CharBasis.language_system` (the port's `language` variant decl).
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum Language {
34    Japanese,
35    English,
36    NoLanguageSystem,
37}
38
39/// The set of Knuth–Liang hyphenation dictionaries a `Context` may have
40/// installed. Deliberately a `Copy` tag with **no dependency on the
41/// `hyphenation` crate** — `Context` is cloned constantly, so it cannot own a
42/// `hyphenation::Standard` dictionary (~89 KiB, not cheaply clonable). The
43/// dictionaries live in a process-global load-once cache in
44/// `crates/rustyfi-lang/src/hyphenation.rs`, keyed by this tag.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum HyphenLang {
47    EnglishUS,
48    /// en-GB. Maps to `hyphenation::Language::EnglishGB`.
49    EnglishGB,
50}
51
52/// SATySFi 0.1's `math_script_level` (`dev-0-1-0 src/backend/horzBox.ml:
53/// 139-142`) — how many script-nesting levels deep the current math reading
54/// context sits. V0_0 never reads it (its script-size shrink is a fixed
55/// per-call constant, not context-carried).
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum MathScriptLevel {
58    Base,
59    Script,
60    ScriptScript,
61}
62
63/// One script's font selection within a `Context::font_scheme`:
64/// upstream `font_with_ratio` (`horzBox.ml`) folded into a plain struct.
65/// `ratio` scales `ctx.font_size` for this script's glyphs; `rising` is a
66/// further fraction-of-size baseline raise (`fontInfo.ml`'s
67/// `get_font_with_ratio`).
68#[derive(Clone, Copy, Debug, PartialEq)]
69pub struct ScriptFont {
70    pub font: FontKey,
71    pub ratio: f64,
72    pub rising: f64,
73}
74
75/// The typesetting context (a subset of `context_main` in horzBox.ml).
76#[derive(Clone, Debug, PartialEq)]
77pub struct Context {
78    pub font: FontKey,
79    /// The dedicated math font (v0.0.6 context_main.math_font; set-math-font).
80    /// Math layout measures/emits glyphs under THIS key, falling back to `font`
81    /// per-glyph when it has no glyph (see primitives::math_glyph_font). The
82    /// OpenType MATH-table lookups key on this same FontKey.
83    pub math_font: FontKey,
84    pub font_size: Length,
85    /// Baseline-to-baseline distance.
86    pub leading: Length,
87    /// Wrap width for paragraphs.
88    pub paragraph_width: Length,
89    /// Extra vertical skip inserted above a paragraph
90    /// (`set-paragraph-margin`'s first argument; v0.0.6
91    /// `context_main.paragraph_top`, horzBox.ml:227). Emitted by
92    /// `prim_line_break` as a leading `VertBox::Skip`. A skip at the
93    /// very top of a page/column is discarded by `chop_page` (upstream's
94    /// page-top glue suppression), so this adds no space above a page's first
95    /// paragraph.
96    pub paragraph_top: Length,
97    /// Extra vertical skip inserted below a paragraph
98    /// (`set-paragraph-margin`'s second argument; v0.0.6
99    /// `context_main.paragraph_bottom`, horzBox.ml:228) — a trailing
100    /// `VertBox::Skip`.
101    pub paragraph_bottom: Length,
102    /// A manual vertical shift applied to text set under this context
103    /// (`set-manual-rising`'s argument; v0.0.6 `context_main.manual_rising`,
104    /// horzBox.ml:232). Stored only: nothing downstream reads it (upstream's
105    /// `PHGRising` box has no analogue here).
106    pub manual_rising: Length,
107    /// `set-dominant-wide-script` (v0.0.6 `context_main.dominant_wide_script`,
108    /// horzBox.ml:218). Storage only; no layout consumer yet.
109    pub dominant_wide_script: Script,
110    /// `set-dominant-narrow-script` (horzBox.ml:219). Same status.
111    pub dominant_narrow_script: Script,
112    /// `set-language`/`get-language` (v0.0.6 `context_main.langsys_scheme`,
113    /// horzBox.ml:216 — a script→language_system map). Stored as a dense
114    /// 4-slot array indexed by `Script`'s discriminant; upstream's "absent
115    /// from map" IS `NoLanguageSystem` (`get_language_system`'s default,
116    /// horzBox.ml:483-487), so the empty map and `[NoLanguageSystem; 4]`
117    /// are indistinguishable — no Option needed.
118    pub langsys_scheme: [Language; 4],
119    /// `set-font`'s per-script font/ratio/rising scheme (v0.0.6
120    /// `context_main.font_scheme`, horzBox.ml:214), indexed by `Script`'s
121    /// discriminant. Resolution rule (back-compat critical, see `set-font`'s
122    /// doc in rustyfi-lang): `Latin`-script text reads `Context::font`
123    /// directly, NOT this scheme's `Latin` slot — `set-font Latin f` writes
124    /// BOTH so the two stay in sync, leaving a bare
125    /// `set-font-key`/`\bold`/`\emph` (which only touches `font`) unaffected.
126    pub font_scheme: [ScriptFont; 4],
127    /// `set-text-color`/`get-text-color` (row 1-2; v0.0.6
128    /// `context_main.text_color`). Copied into each run's
129    /// `HorzStringInfo::color` at box-construction time — which is what both
130    /// PDF writers emit their fill-color op from.
131    pub text_color: Color,
132    /// `set-hyphen-penalty` (row 3; v0.0.6 `context_main.hyphen_badness`).
133    /// Consumed by `rustyfi-lang`'s `flush_word` injection as each injected
134    /// `Discretionary`'s `penalty`, but only when `hyphen_dictionary` is
135    /// `Some(_)`; with no dictionary installed it has no layout effect.
136    pub hyphen_badness: i64,
137    /// The installed hyphenation dictionary
138    /// (`set-hyphenation-dictionary`/`load-hyphenation-dictionary`; v0.0.6
139    /// `context_main.hyphen_dictionary`). `flush_word` (`rustyfi-lang`) runs
140    /// the hyphenation branch **iff** this is `Some(tag)` and the run's
141    /// script is `Latin`.
142    pub hyphen_dictionary: Option<HyphenLang>,
143    /// Minimum number of chars that must PRECEDE an accepted hyphenation
144    /// break (`set-hyphen-min`'s first argument; v0.0.6
145    /// `context_main.left_hyphen_min`). Default 3.
146    pub left_hyphen_min: i64,
147    /// Minimum number of chars that must FOLLOW it (`set-hyphen-min`'s
148    /// second argument). Default 2.
149    pub right_hyphen_min: i64,
150    /// `set-space-ratio`'s three fields (row 4; v0.0.6
151    /// `context_main.space_natural`/`space_shrink`/`space_stretch`). Each is
152    /// a ratio of `font_size` DIRECTLY, not of the natural width:
153    /// `text_to_boxes`'s interword glue is natural = `font_size *
154    /// space_natural`, shrink/stretch likewise.
155    pub space_natural: f64,
156    pub space_shrink: f64,
157    pub space_stretch: f64,
158    /// `set-adjacent-stretch` (v0.0.6 `context_main.adjacent_stretch`) — the
159    /// stretch, as a ratio of `font_size`, of the glue SATySFi puts between
160    /// two DIRECTLY ADJACENT CJK characters (`convertText.ml:101`
161    /// `adjacent_space`: natural 0, shrink 0, stretch `font_size * ratio`).
162    /// This is what lets a Japanese line fill its column: unspaced CJK has no
163    /// interword glue, so without it a CJK line's only elasticity is whatever
164    /// incidental Latin spaces it happens to contain.
165    pub adjacent_stretch: f64,
166    /// The installed `[math] inline-cmd` applied to bare `${…}` in inline
167    /// text (v0.0.6 `context_main.math_command`). `None` only for contexts
168    /// built by `Context::initial` directly (unit tests) — the
169    /// `get-initial-context` primitive always installs its second argument.
170    pub math_command: Option<MathCmdId>,
171    /// `set-code-text-command` (v0.0.6 `context_main.code_text_command`) — the
172    /// `[string] inline-cmd` a backtick literal inside inline text is handed
173    /// to. `None` is upstream's `DefaultCodeTextCommand`: the literal is set as
174    /// ordinary text. Same id-into-`Interp` handle as `math_command`.
175    pub code_text_command: Option<MathCmdId>,
176    /// `\mathrm`/`\bm`/… restyling target (v0.0.6 `context_main.
177    /// math_char_class`): which Mathematical- Alphanumeric style block a
178    /// plain `${…}` letter resolves to. Set by `Math::ChangeCharClass`'s
179    /// layout arm (`primitives.rs`), consulted by `resolve_variant_char`.
180    /// Defaults to `Italic`, v0.0.6's own default.
181    pub math_char_class: MathCharClass,
182    /// Upstream `default_math_class_map` (`primitives.cppo.ml:465-480`):
183    /// whole-TOKEN entries (`=`, `-`, `,`, …) consulted BEFORE the per-char
184    /// variant lookup below. `Arc` so that cloning a `Context` (every `..ctx`
185    /// spread) stays a refcount bump.
186    pub math_class_map: Arc<BTreeMap<String, (String, MathKind)>>,
187    /// `set-math-variant-char`'s runtime override table: `(source
188    /// char, style) -> replacement char`, consulted BEFORE
189    /// `default_math_variant_char`'s built-in Mathematical-Alphanumeric
190    /// remap. Empty by default; copy-on-written via `Arc::make_mut`.
191    pub math_variant_char_map: Arc<BTreeMap<(char, MathCharClass), char>>,
192    /// V0_1-only (`enter_script`, port of `dev-0-1-0
193    /// src/frontend/context.ml:52-68`). `Base` under V0_0 always.
194    pub math_script_level: MathScriptLevel,
195    /// Whether the current math sub-formula is laid out "cramped" (TeXbook
196    /// Appendix G): set on the recursive layout `Context` for a radical's
197    /// radicand, a fraction's denominator, and any subscript. Read by BOTH
198    /// V0_0 and V0_1 (the bit rides the shared layout-recursion clone, not a
199    /// version-gated primitive). Only consumed by `sup_shift_clamped`'s
200    /// superscript shift-up formula.
201    pub math_cramped: bool,
202}
203
204impl Context {
205    /// The default context `get-initial-context` hands to `document`.
206    pub fn initial(paragraph_width: Length) -> Context {
207        Context {
208            font: FontKey(0),
209            math_font: FontKey(0),
210            font_size: Length::pt(12.0),
211            leading: Length::pt(18.0),
212            paragraph_width,
213            // Default paragraph margin. Measured against the reference SATySFi
214            // 0.0.11 (flake.nix): a body paragraph boundary advances 27pt =
215            // leading(18) + 9, i.e. the effective paragraph margin is 9pt
216            // (= font_size 12 × 0.75), NOT the 18pt an older reading of
217            // primitives.cppo.ml assumed. Matching it makes the port's page
218            // breaks coincide with SATySFi's across the prose corpus.
219            paragraph_top: Length::pt(18.0),
220            paragraph_bottom: Length::pt(18.0),
221            manual_rising: Length::ZERO,
222            dominant_wide_script: Script::OtherScript,
223            dominant_narrow_script: Script::OtherScript,
224            langsys_scheme: [Language::NoLanguageSystem; 4],
225            font_scheme: [ScriptFont {
226                font: FontKey(0),
227                ratio: 1.0,
228                rising: 0.0,
229            }; 4],
230            // v0.0.6's `get_pdf_mode_initial_context` (primitives.cppo.ml):
231            // `text_color = DeviceGray 0.`, `hyphen_badness = 100`,
232            // `space_natural = 0.33`, `space_shrink = 0.08`,
233            // `space_stretch = 0.16`, `adjacent_stretch = 0.025`.
234            text_color: Color::Gray(0.0),
235            hyphen_badness: 100,
236            // Upstream loads `dist/hyph/english.satysfi-hyph` into
237            // `default_hyphen_dictionary` at startup and hands it to EVERY
238            // initial context (`primitives.cppo.ml:500,607`).
239            hyphen_dictionary: Some(HyphenLang::EnglishUS),
240            left_hyphen_min: 3,
241            right_hyphen_min: 2,
242            space_natural: 0.33,
243            space_shrink: 0.08,
244            space_stretch: 0.16,
245            // `convertText.ml:103` — inter-CJK glue stretch ratio.
246            adjacent_stretch: 0.025,
247            math_command: None,
248            code_text_command: None,
249            math_char_class: MathCharClass::Italic,
250            math_class_map: Arc::new(default_math_class_map()),
251            math_variant_char_map: Arc::new(BTreeMap::new()),
252            math_script_level: MathScriptLevel::Base,
253            math_cramped: false,
254        }
255    }
256}
257
258/// Page geometry (A4 with even margins by default).
259#[derive(Clone, Debug, PartialEq)]
260pub struct PageGeometry {
261    pub paper_width: Length,
262    pub paper_height: Length,
263    /// Top-left corner of the text area.
264    pub text_origin: (Length, Length),
265    pub text_width: Length,
266    pub text_height: Length,
267}
268
269impl Default for PageGeometry {
270    fn default() -> Self {
271        let paper_width = Length::from_unit(210.0, "mm").unwrap();
272        let paper_height = Length::from_unit(297.0, "mm").unwrap();
273        let margin = Length::from_unit(25.0, "mm").unwrap();
274        PageGeometry {
275            paper_width,
276            paper_height,
277            text_origin: (margin, margin),
278            text_width: paper_width - margin - margin,
279            text_height: paper_height - margin - margin,
280        }
281    }
282}
283
284impl PageGeometry {
285    /// Build a geometry from `page`'s paper dimensions. Only
286    /// `paper_width`/`paper_height` are read by the PDF writer; the `text_*`
287    /// fields are vestigial here — each page's real text area lives in its
288    /// `PlacedLine` coordinates, set per page by the content scheme
289    /// (`chop_page`'s caller).
290    pub fn for_paper(paper_width: Length, paper_height: Length) -> PageGeometry {
291        PageGeometry {
292            paper_width,
293            paper_height,
294            text_origin: (Length::ZERO, Length::ZERO),
295            text_width: paper_width,
296            text_height: paper_height,
297        }
298    }
299}
300
301/// v0.0.6's `page_size` (`primitives.cppo.ml:203-212`) — the paper-size
302/// constant set `page-break`'s first argument selects from, the port of
303/// `get_pdf_paper` (`handlePdf.ml:406`, `Pdfpaper.t` dims from
304/// `pdfpaper.ml`).
305#[derive(Clone, Copy, Debug, PartialEq)]
306pub enum PaperSize {
307    A0,
308    A1,
309    A2,
310    A3,
311    A4,
312    A5,
313    USLetter,
314    USLegal,
315    UserDefined(Length, Length),
316}
317
318impl PaperSize {
319    /// `(width, height)` in points.
320    pub fn dims(&self) -> (Length, Length) {
321        fn mm(w: f64, h: f64) -> (Length, Length) {
322            (
323                Length::from_unit(w, "mm").unwrap(),
324                Length::from_unit(h, "mm").unwrap(),
325            )
326        }
327        fn inch(w: f64, h: f64) -> (Length, Length) {
328            (
329                Length::from_unit(w, "inch").unwrap(),
330                Length::from_unit(h, "inch").unwrap(),
331            )
332        }
333        match *self {
334            PaperSize::A0 => mm(841.0, 1189.0),
335            PaperSize::A1 => mm(594.0, 841.0),
336            PaperSize::A2 => mm(420.0, 594.0),
337            PaperSize::A3 => mm(297.0, 420.0),
338            PaperSize::A4 => mm(210.0, 297.0),
339            PaperSize::A5 => mm(148.0, 210.0),
340            PaperSize::USLetter => inch(8.5, 11.0),
341            PaperSize::USLegal => inch(8.5, 14.0),
342            PaperSize::UserDefined(w, h) => (w, h),
343        }
344    }
345}