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}