fix(fonts): stop CRLF pairing at inline objects

This commit is contained in:
ldm0
2026-10-25 15:44:11 +08:00
parent 4011793fd5
commit 672118c9fa
4 changed files with 108 additions and 0 deletions
+49
View File
@@ -2916,6 +2916,55 @@ fn bidi_controls(bidi: InlineUnicodeBidi, direction: InlineDirection) -> Vec<(ch
mod tests {
use super::*;
#[test]
fn carriage_returns_separate_space_styles_at_atomic_and_float_objects() {
let root = LayoutBoxId::from_index(0);
let prefix = LayoutBoxId::from_index(1);
let object = LayoutBoxId::from_index(2);
let tail = LayoutBoxId::from_index(3);
for (role, expected, tail_spaces) in [
(InlineObjectRole::Atomic, "a b", 1),
(InlineObjectRole::Float, "a b", 1),
(InlineObjectRole::OutOfFlow, "a b", 0),
(InlineObjectRole::StartEdge, "a b", 0),
(InlineObjectRole::EndEdge, "a b", 0),
] {
let mut normalizer = InlineNormalizer::new(root);
normalizer.push_text(
prefix,
"a\r",
InlineWhiteSpaceCollapse::Collapse,
InlineTextTransform::None,
&[root],
);
let kind = match role {
InlineObjectRole::Float => InlineBoxKind::CustomOutOfFlow,
InlineObjectRole::OutOfFlow => InlineBoxKind::OutOfFlow,
_ => InlineBoxKind::InFlow,
};
normalizer.push_object(object, role, kind, &[root], InlineVerticalAlign::default());
normalizer.push_text(
tail,
"\nb",
InlineWhiteSpaceCollapse::Collapse,
InlineTextTransform::None,
&[root],
);
let input = normalizer.finish();
assert_eq!(input.text, expected, "{role:?}");
assert_eq!(
input
.units
.iter()
.filter(|unit| unit.style_box == tail
&& &input.text[unit.output_range.clone()] == " ")
.count(),
tail_spaces,
"{role:?}"
);
}
}
#[test]
fn capitalize_keeps_boundaries_across_segment_breaks_and_text_runs() {
for (runs, expected) in [
@@ -258,6 +258,9 @@ impl TextCollector<'_> {
{
return None;
}
// The CR has already contributed a normalized newline. An object
// ends CRLF pairing before saving the surrounding text context.
self.context.carriage_return = false;
if !out_of_flow {
if floated || display.outside() == DisplayOutside::Inline {
self.flush_space();
@@ -3,6 +3,7 @@ use base64::Engine;
use std::time::Duration;
mod automatic;
mod automatic_boundaries;
mod automatic_controls;
mod automatic_lists;
mod automatic_normalization;
@@ -0,0 +1,55 @@
use super::automatic_controls::font_usage_vm;
use super::selection_order::order_font_data;
use super::*;
#[test]
fn automatic_fonts_end_crlf_pairing_at_rendered_inline_objects() {
let (_, wide) = order_font_data();
for (object, expected) in [
(
"<i style='display:inline-block;width:5px;height:5px'></i>",
"loaded",
),
(
"<i style='display:inline-flex;width:5px;height:5px'></i>",
"loaded",
),
(
"<i style='display:inline-block;width:5px;height:5px'>x</i>",
"loaded",
),
("<img width='5' height='5'>", "loaded"),
("<input value='x'>", "loaded"),
("<select><option>x</option></select>", "loaded"),
("<i style='float:left;width:5px;height:5px'></i>", "loaded"),
// Out-of-flow objects and inline edges keep the first pending space's
// font. Their LF still collapses with the CR's ordinary-font space.
(
"<i style='position:absolute;width:5px;height:5px'></i>",
"unloaded",
),
("<i></i>", "unloaded"),
("<i style='display:contents'></i>", "unloaded"),
("<i style='display:none'></i>", "unloaded"),
] {
let mut vm = font_usage_vm();
let object = serde_json::to_string(object).unwrap();
vm.eval(&format!(r#"
const container=document.createElement('div');
container.append(document.createTextNode('a\r'));
container.insertAdjacentHTML('beforeend',{object});
const tail=document.createElement('span');
tail.style.cssText='display:contents;font-family:SpaceFace';
tail.textContent='\nb';container.append(tail);document.body.append(container);
globalThis.face=new FontFace('SpaceFace','url(data:font/ttf;base64,{wide})',{{unicodeRange:'U+0020'}});
document.fonts.add(face);
"#)).unwrap();
vm.reconcile_document_font_resources();
vm.eval("document.fonts.ready.then(()=>globalThis.ready=true)")
.unwrap();
run_font_tasks(&mut vm);
assert_eq!(vm.eval("face.status").unwrap(), expected, "{object}");
assert_eq!(vm.eval("globalThis.ready===true").unwrap(), "true");
assert_eq!(vm.layout_pass_observability_for_test().1, 0);
}
}