Skip to content

Commit ef1311c

Browse files
authored
Merge pull request #5 from erweixin/fix-above
Fix above nsupseteqq
2 parents e2feac1 + fdc2e4b commit ef1311c

49 files changed

Lines changed: 164 additions & 28 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/ratex-font/src/data/symbols_data.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,15 +1875,15 @@ pub static SYMBOLS: &[SymbolEntry] = &[
18751875
("\\@nshortmid", 0, 1, "rel", Some('\u{E006}')),
18761876
("\\@nshortparallel", 0, 1, "rel", Some('\u{E007}')),
18771877
("\\@nsubseteqq", 0, 1, "rel", Some('\u{E016}')),
1878-
("\\@nsupseteqq", 0, 1, "rel", Some('\u{E017}')),
1879-
("\\@varsubsetneq", 0, 1, "rel", Some('\u{E018}')),
1880-
("\\@varsubsetneqq", 0, 1, "rel", Some('\u{E019}')),
1881-
("\\@varsupsetneq", 0, 1, "rel", Some('\u{E01A}')),
1882-
("\\@varsupsetneqq", 0, 1, "rel", Some('\u{E01B}')),
1878+
("\\@nsupseteqq", 0, 1, "rel", Some('\u{E018}')),
1879+
("\\@varsubsetneq", 0, 1, "rel", Some('\u{E01A}')),
1880+
("\\@varsubsetneqq", 0, 1, "rel", Some('\u{E017}')),
1881+
("\\@varsupsetneq", 0, 1, "rel", Some('\u{E01B}')),
1882+
("\\@varsupsetneqq", 0, 1, "rel", Some('\u{E019}')),
18831883
("\\@imath", 0, 0, "mathord", Some('\u{E131}')),
18841884
("\\@jmath", 0, 0, "mathord", Some('\u{E237}')),
1885-
("\\@ulcorner", 0, 1, "open", Some('\u{E01C}')),
1886-
("\\@urcorner", 0, 1, "close", Some('\u{E01D}')),
1887-
("\\@llcorner", 0, 1, "open", Some('\u{E01E}')),
1888-
("\\@lrcorner", 0, 1, "close", Some('\u{E01F}')),
1885+
("\\@ulcorner", 0, 1, "open", Some('\u{250C}')),
1886+
("\\@urcorner", 0, 1, "close", Some('\u{2510}')),
1887+
("\\@llcorner", 0, 1, "open", Some('\u{2514}')),
1888+
("\\@lrcorner", 0, 1, "close", Some('\u{2518}')),
18891889
];

crates/ratex-layout/src/engine.rs

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ fn layout_node(node: &ParseNode, options: &LayoutOptions) -> LayoutBox {
198198
let bar_thickness = if *has_bar_line {
199199
bar_size
200200
.as_ref()
201-
.map(|m| m.number * options.metrics().default_rule_thickness)
201+
.map(|m| measurement_to_em(m, options))
202202
.unwrap_or(options.metrics().default_rule_thickness)
203203
} else {
204204
0.0
@@ -1427,7 +1427,12 @@ fn layout_accent(
14271427
0.0
14281428
};
14291429

1430-
let gap = if use_arrow_path { 0.12 } else { 0.0 };
1430+
// gap = clearance between body top and bottom of accent SVG.
1431+
// For arrow accents, the SVG path is centered (height=h/2, depth=h/2).
1432+
// The gap prevents the visible arrowhead boundary from overlapping with body top.
1433+
let gap = if use_arrow_path {
1434+
if label == "\\Overrightarrow" { 0.21 } else { 0.12 }
1435+
} else { 0.0 };
14311436

14321437
let clearance = if is_below {
14331438
body_box.height + body_box.depth + accent_box.depth + gap
@@ -2423,12 +2428,16 @@ fn layout_overline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
24232428
let metrics = options.metrics();
24242429
let rule = metrics.default_rule_thickness;
24252430

2431+
// Total height: body height + 2*rule clearance + rule thickness = body.height + 3*rule
24262432
let height = body_box.height + 3.0 * rule;
24272433
LayoutBox {
24282434
width: body_box.width,
24292435
height,
24302436
depth: body_box.depth,
2431-
content: BoxContent::HBox(vec![body_box]),
2437+
content: BoxContent::Overline {
2438+
body: Box::new(body_box),
2439+
rule_thickness: rule,
2440+
},
24322441
color: options.color,
24332442
}
24342443
}
@@ -2438,12 +2447,16 @@ fn layout_underline(body: &ParseNode, options: &LayoutOptions) -> LayoutBox {
24382447
let metrics = options.metrics();
24392448
let rule = metrics.default_rule_thickness;
24402449

2450+
// Total depth: body depth + 2*rule clearance + rule thickness = body.depth + 3*rule
24412451
let depth = body_box.depth + 3.0 * rule;
24422452
LayoutBox {
24432453
width: body_box.width,
24442454
height: body_box.height,
24452455
depth,
2446-
content: BoxContent::HBox(vec![body_box]),
2456+
content: BoxContent::Underline {
2457+
body: Box::new(body_box),
2458+
rule_thickness: rule,
2459+
},
24472460
color: options.color,
24482461
}
24492462
}
@@ -2463,7 +2476,13 @@ fn layout_spacing_command(text: &str, options: &LayoutOptions) -> LayoutBox {
24632476
"\\!" | "\\negthinspace" => -3.0 * mu,
24642477
"\\negmedspace" => -4.0 * mu,
24652478
"\\negthickspace" => -5.0 * mu,
2466-
"~" | "\\nobreakspace" | "\\ " => metrics.space,
2479+
"~" | "\\nobreakspace" | "\\ " | "\\space" => {
2480+
// KaTeX renders these by placing the U+00A0 glyph (char 160) via mathsym.
2481+
// Look up its width from MainRegular; fall back to 0.25em (the font-defined value).
2482+
get_char_metrics(FontId::MainRegular, 160)
2483+
.map(|m| m.width)
2484+
.unwrap_or(0.25)
2485+
}
24672486
"\\quad" => metrics.quad,
24682487
"\\qquad" => 2.0 * metrics.quad,
24692488
"\\enspace" => metrics.quad / 2.0,
@@ -2687,37 +2706,50 @@ fn layout_xarrow(
26872706
color: options.color,
26882707
};
26892708

2709+
// KaTeX positions xarrows centered on the math axis, with a 0.111em (2mu) gap
2710+
// between the arrow and the text above/below (see amsmath.dtx reference).
26902711
let metrics = options.metrics();
2691-
let _sp1 = 0.111; // bigOpSpacing1 — above the superscript
2692-
let _sp2 = 0.166; // bigOpSpacing2 — below the subscript
2693-
let sp3 = 0.2; // bigOpSpacing3 — gap between base and script
2694-
let sp5 = metrics.big_op_spacing5;
2712+
let axis = metrics.axis_height; // 0.25em
2713+
let arrow_half = actual_arrow_h / 2.0;
2714+
let gap = 0.111; // 2mu gap (KaTeX constant)
2715+
2716+
// Center the arrow on the math axis by shifting it up.
2717+
let base_shift = -axis;
2718+
2719+
// sup_kern: gap between arrow top and text bottom.
2720+
// In the OpLimits renderer:
2721+
// sup_y = y - (arrow_half - base_shift) - sup_kern - sup_box.depth * ratio
2722+
// = y - (arrow_half + axis) - sup_kern - sup_box.depth * ratio
2723+
// KaTeX: text_baseline = -(axis + arrow_half + gap)
2724+
// (with extra -= depth when depth > 0.25, but that's rare for typical text)
2725+
// Matching: sup_kern = gap
2726+
let sup_kern = gap;
2727+
let sub_kern = gap;
26952728

2696-
let total_w = arrow_w;
26972729
let sup_h = body_box.height * sup_ratio;
26982730
let sup_d = body_box.depth * sup_ratio;
26992731

2700-
let height = arrow_box.height + sp3 + sup_h + sup_d + sp5;
2701-
let mut depth = arrow_box.depth + sp5;
2732+
// Height: from baseline to top of upper text
2733+
let height = axis + arrow_half + gap + sup_h + sup_d;
2734+
// Depth: arrow bottom below baseline = arrow_half - axis
2735+
let mut depth = (arrow_half - axis).max(0.0);
27022736

27032737
if let Some(ref bel) = below_box {
27042738
let sub_h = bel.height * sub_ratio;
27052739
let sub_d = bel.depth * sub_ratio;
2706-
depth = arrow_box.depth + sp3 + sub_h + sub_d + sp5;
2740+
// Lower text positioned symmetrically below the arrow
2741+
depth = (arrow_half - axis) + gap + sub_h + sub_d;
27072742
}
27082743

2709-
let sup_kern = sp3;
2710-
let sub_kern = sp3;
2711-
27122744
LayoutBox {
2713-
width: total_w,
2745+
width: arrow_w,
27142746
height,
27152747
depth,
27162748
content: BoxContent::OpLimits {
27172749
base: Box::new(arrow_box),
27182750
sup: Some(Box::new(body_box)),
27192751
sub: below_box.map(Box::new),
2720-
base_shift: 0.0,
2752+
base_shift,
27212753
sup_kern,
27222754
sub_kern,
27232755
slant: 0.0,

crates/ratex-layout/src/layout_box.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,20 @@ pub enum BoxContent {
149149
body: Box<LayoutBox>,
150150
},
151151

152+
/// \overline{body}: body with a horizontal rule drawn above it.
153+
/// The rule sits `2 * rule_thickness` above the body's top (clearance), and is `rule_thickness` thick.
154+
Overline {
155+
body: Box<LayoutBox>,
156+
rule_thickness: f64,
157+
},
158+
159+
/// \underline{body}: body with a horizontal rule drawn below it.
160+
/// The rule sits `2 * rule_thickness` below the body's bottom (clearance), and is `rule_thickness` thick.
161+
Underline {
162+
body: Box<LayoutBox>,
163+
rule_thickness: f64,
164+
},
165+
152166
/// Empty placeholder.
153167
Empty,
154168
}

crates/ratex-layout/src/to_display.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,32 @@ fn emit_box(lbox: &LayoutBox, x: f64, y: f64, scale: f64, items: &mut Vec<Displa
419419
emit_box(body, x, y, scale, items);
420420
}
421421

422+
BoxContent::Overline { body, rule_thickness } => {
423+
emit_box(body, x, y, scale, items);
424+
// Rule center is at 2.5 * rule_thickness above the body's top
425+
let rule_center_y = y - (body.height + 2.5 * rule_thickness) * scale;
426+
items.push(DisplayItem::Line {
427+
x,
428+
y: rule_center_y,
429+
width: lbox.width * scale,
430+
thickness: rule_thickness * scale,
431+
color: lbox.color,
432+
});
433+
}
434+
435+
BoxContent::Underline { body, rule_thickness } => {
436+
emit_box(body, x, y, scale, items);
437+
// Rule center is at 2.5 * rule_thickness below the body's bottom
438+
let rule_center_y = y + (body.depth + 2.5 * rule_thickness) * scale;
439+
items.push(DisplayItem::Line {
440+
x,
441+
y: rule_center_y,
442+
width: lbox.width * scale,
443+
thickness: rule_thickness * scale,
444+
color: lbox.color,
445+
});
446+
}
447+
422448
BoxContent::Kern | BoxContent::Empty => {}
423449
}
424450
}

crates/ratex-parser/src/macro_expander.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ impl<'a> MacroExpander<'a> {
348348
("\\bra", "\\mathinner{\\langle{#1}|}"),
349349
("\\ket", "\\mathinner{|{#1}\\rangle}"),
350350
("\\braket", "\\mathinner{\\langle{#1}\\rangle}"),
351-
("\\Braket", "\\mathinner{\\langle{#1}\\rangle}"),
351+
("\\Braket", "\\bra@ket{\\left\\langle}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\right\\rangle}"),
352352
("\\Bra", "\\left\\langle#1\\right|"),
353353
("\\Ket", "\\left|#1\\right\\rangle"),
354354

@@ -676,6 +676,69 @@ impl<'a> MacroExpander<'a> {
676676
);
677677
}
678678

679+
// \bra@ket: like \bra@set but replaces ALL | at depth 0 (for \Braket)
680+
self.macros.set(
681+
"\\bra@ket".to_string(),
682+
MacroDefinition::Function(|me: &mut MacroExpander| -> ParseResult<Vec<Token>> {
683+
let args = me.consume_args(4)?;
684+
let left = args[0].clone();
685+
let middle = args[1].clone();
686+
let middle_double = args[2].clone();
687+
let right = args[3].clone();
688+
689+
let content = me.consume_args(1)?;
690+
let content = content.into_iter().next().unwrap();
691+
692+
// Convert stack-order (reversed) to logical order, replace all | at depth 0,
693+
// then reverse back to stack order.
694+
let logical: Vec<Token> = content.into_iter().rev().collect();
695+
let mut new_logical: Vec<Token> = Vec::new();
696+
let mut depth: i32 = 0;
697+
let mut i = 0;
698+
while i < logical.len() {
699+
let t = &logical[i];
700+
if t.text == "{" {
701+
depth += 1;
702+
new_logical.push(t.clone());
703+
} else if t.text == "}" {
704+
depth -= 1;
705+
new_logical.push(t.clone());
706+
} else if depth == 0 && t.text == "|" {
707+
// Check for || (double pipe) → middleDouble
708+
if !middle_double.is_empty()
709+
&& i + 1 < logical.len()
710+
&& logical[i + 1].text == "|"
711+
{
712+
// middle_double is in stack/reversed order; reverse to logical order
713+
new_logical.extend(middle_double.iter().rev().cloned());
714+
i += 2;
715+
continue;
716+
}
717+
// middle is in stack/reversed order; reverse to logical order
718+
new_logical.extend(middle.iter().rev().cloned());
719+
} else {
720+
new_logical.push(t.clone());
721+
}
722+
i += 1;
723+
}
724+
725+
// Reverse back to stack order
726+
let content_rev: Vec<Token> = new_logical.into_iter().rev().collect();
727+
728+
// Build: right + content + left (reversed for stack)
729+
let mut to_expand = Vec::new();
730+
to_expand.extend(right);
731+
to_expand.extend(content_rev);
732+
to_expand.extend(left);
733+
734+
me.begin_group();
735+
let expanded = me.expand_tokens(to_expand)?;
736+
me.end_group();
737+
738+
Ok(expanded)
739+
}),
740+
);
741+
679742
// \bra@set: braket set notation helper
680743
// Only replaces the FIRST | with middle tokens (one-shot), matching KaTeX
681744
self.macros.set(

crates/ratex-render/tests/golden_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,4 @@ fn golden_test_pass_rate() {
233233
Many failures are from unimplemented node types, not rendering bugs."
234234
);
235235
}
236+

tests/golden/output/0009.png

17 Bytes

tests/golden/output/0019.png

54 Bytes

tests/golden/output/0027.png

279 Bytes

tests/golden/output/0137.png

4.12 KB

0 commit comments

Comments
 (0)