Implement CSS text truncation
This commit is contained in:
1 parent
f8f8d6da51
commit
26a78a91c8
6 files changed
+589
-5
No files matched your search
Vendored
+6
-1
@@ -6024,7 +6024,12 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
const lookup = (rawProp) => {
|
const lookup = (rawProp) => {
|
||||||
if (typeof rawProp !== 'string') return '';
|
if (typeof rawProp !== 'string') return '';
|
||||||
refreshRendered();
|
refreshRendered();
|
||||||
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
let kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||||
|
// CSSOM camelCase vendor properties omit the punctuation from their JS
|
||||||
|
// spelling (`webkitLineClamp`) but computed-property names retain it
|
||||||
|
// (`-webkit-line-clamp`). Normalize the prefix once for every WebKit
|
||||||
|
// property instead of adding per-property aliases to the native snapshot.
|
||||||
|
if (kebab.startsWith('webkit-')) kebab = '-' + kebab;
|
||||||
if (rendered && Object.prototype.hasOwnProperty.call(rendered, kebab))
|
if (rendered && Object.prototype.hasOwnProperty.call(rendered, kebab))
|
||||||
return rendered[kebab];
|
return rendered[kebab];
|
||||||
// Non-render builds and properties outside the renderer snapshot retain
|
// Non-render builds and properties outside the renderer snapshot retain
|
||||||
|
|||||||
@@ -2647,6 +2647,55 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn webkit_truncation_computed_names_use_native_support_and_vendor_prefixes() {
|
||||||
|
let dom = parse_html(
|
||||||
|
r#"<html><head><style>
|
||||||
|
#clamp { display:-webkit-box; -webkit-box-orient:vertical;
|
||||||
|
-webkit-line-clamp:2; overflow:hidden; }
|
||||||
|
#legacy { display:-webkit-inline-box; -webkit-box-orient:horizontal; }
|
||||||
|
</style></head><body>
|
||||||
|
<div id="clamp">one two three four five six seven eight</div>
|
||||||
|
<span id="legacy">legacy</span>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(120.0, 200.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const clamp = getComputedStyle(document.getElementById("clamp"));
|
||||||
|
const legacy = getComputedStyle(document.getElementById("legacy"));
|
||||||
|
return {
|
||||||
|
supports: [
|
||||||
|
CSS.supports("text-overflow", "ellipsis"),
|
||||||
|
CSS.supports("-webkit-line-clamp", "2"),
|
||||||
|
CSS.supports("-webkit-line-clamp", "0"),
|
||||||
|
CSS.supports("display", "-webkit-box"),
|
||||||
|
CSS.supports("-webkit-box-orient", "vertical")
|
||||||
|
],
|
||||||
|
clamp: [clamp.display, clamp.webkitLineClamp,
|
||||||
|
clamp.webkitBoxOrient,
|
||||||
|
clamp.getPropertyValue("-webkit-line-clamp")],
|
||||||
|
legacy: [legacy.display, legacy.webkitBoxOrient]
|
||||||
|
};
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({
|
||||||
|
"supports": [true, true, false, true, true],
|
||||||
|
"clamp": ["flow-root", "2", "vertical", "2"],
|
||||||
|
"legacy": ["-webkit-inline-box", "horizontal"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
#[test]
|
#[test]
|
||||||
fn computed_typography_uses_resolved_renderer_values() {
|
fn computed_typography_uses_resolved_renderer_values() {
|
||||||
|
|||||||
@@ -417,6 +417,25 @@ pub struct InlineItem {
|
|||||||
/// Canonical axis tuples, populated only when this IFC actually contains
|
/// Canonical axis tuples, populated only when this IFC actually contains
|
||||||
/// variable text. Glyph metadata stores a one-based index.
|
/// variable text. Glyph metadata stores a one-based index.
|
||||||
variation_sets: Vec<Arc<FontVariations>>,
|
variation_sets: Vec<Arc<FontVariations>>,
|
||||||
|
/// Direct pure-text legacy clamp. Nested block descendants do not enter
|
||||||
|
/// this IFC and are intentionally left to the future block-line iterator.
|
||||||
|
line_clamp: Option<usize>,
|
||||||
|
/// Ordinary single-value ellipsis is active only when inline-axis
|
||||||
|
/// overflow is non-visible. The full buffer remains intact for natural
|
||||||
|
/// overflow metrics and overflow-visible clamp painting.
|
||||||
|
ellipsis_overflow: bool,
|
||||||
|
/// Separately shaped marker using the same final text attributes. Keeping
|
||||||
|
/// this distinct avoids mutating DOM text or the natural content buffer.
|
||||||
|
marker_buffer: Option<Buffer>,
|
||||||
|
marker: Option<MarkerPlacement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct MarkerPlacement {
|
||||||
|
line_index: usize,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
content_end: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Owns the font set and shaping caches for one render pass, plus every
|
/// Owns the font set and shaping caches for one render pass, plus every
|
||||||
@@ -1138,6 +1157,16 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let line_clamp = (base.webkit_box_display.is_some()
|
||||||
|
&& base.webkit_box_orient_vertical)
|
||||||
|
.then_some(base.webkit_line_clamp)
|
||||||
|
.flatten()
|
||||||
|
.map(|lines| lines as usize);
|
||||||
|
let ellipsis_overflow = base.text_overflow == crate::TextOverflow::Ellipsis
|
||||||
|
&& base.clips_overflow_x();
|
||||||
|
let marker_attrs = (line_clamp.is_some() || ellipsis_overflow)
|
||||||
|
.then(|| spans.last().map(|(_, attrs)| attrs.clone()))
|
||||||
|
.flatten();
|
||||||
let rich = spans.iter().map(|(text, attrs)| {
|
let rich = spans.iter().map(|(text, attrs)| {
|
||||||
let variation_index = attrs
|
let variation_index = attrs
|
||||||
.variations
|
.variations
|
||||||
@@ -1161,6 +1190,31 @@ impl TextEngine {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let marker_buffer = marker_attrs.map(|attrs| {
|
||||||
|
let variation_index = attrs
|
||||||
|
.variations
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|variations| {
|
||||||
|
variation_sets
|
||||||
|
.iter()
|
||||||
|
.position(|existing| **existing == **variations)
|
||||||
|
})
|
||||||
|
.map_or(0, |index| index + 1);
|
||||||
|
let mut marker = Buffer::new(&mut self.font_system, metrics);
|
||||||
|
marker.set_wrap(&mut self.font_system, Wrap::None);
|
||||||
|
let marker_attrs = attrs.to_attrs(variation_index);
|
||||||
|
marker.set_rich_text(
|
||||||
|
&mut self.font_system,
|
||||||
|
[("…", marker_attrs)],
|
||||||
|
&Attrs::new().family(Family::Name(FAMILY)),
|
||||||
|
Shaping::Advanced,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
marker.set_size(&mut self.font_system, None, None);
|
||||||
|
marker.shape_until_scroll(&mut self.font_system, false);
|
||||||
|
marker
|
||||||
|
});
|
||||||
|
|
||||||
let align = match base.text_align {
|
let align = match base.text_align {
|
||||||
Some(taffy::AlignItems::CENTER) => Some(Align::Center),
|
Some(taffy::AlignItems::CENTER) => Some(Align::Center),
|
||||||
Some(taffy::AlignItems::FLEX_END) => Some(Align::End),
|
Some(taffy::AlignItems::FLEX_END) => Some(Align::End),
|
||||||
@@ -1190,6 +1244,10 @@ impl TextEngine {
|
|||||||
clip: None,
|
clip: None,
|
||||||
clip_fills,
|
clip_fills,
|
||||||
variation_sets,
|
variation_sets,
|
||||||
|
line_clamp,
|
||||||
|
ellipsis_overflow,
|
||||||
|
marker_buffer,
|
||||||
|
marker: None,
|
||||||
});
|
});
|
||||||
Some(idx)
|
Some(idx)
|
||||||
}
|
}
|
||||||
@@ -1216,8 +1274,16 @@ impl TextEngine {
|
|||||||
let TextEngine { font_system, items, .. } = self;
|
let TextEngine { font_system, items, .. } = self;
|
||||||
let item = &mut items[idx];
|
let item = &mut items[idx];
|
||||||
shape_with_text_indent(font_system, item, width, wrap);
|
shape_with_text_indent(font_system, item, width, wrap);
|
||||||
let (width, height) = buffer_size(&item.buffer, item.first_line_offset);
|
let (width, height, clamped) =
|
||||||
(width, height.max(item.forced_min_height))
|
buffer_size(&item.buffer, item.first_line_offset, item.line_clamp);
|
||||||
|
(
|
||||||
|
width,
|
||||||
|
if clamped {
|
||||||
|
height
|
||||||
|
} else {
|
||||||
|
height.max(item.forced_min_height)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exact max-content size for one fallback word item. Paragraph IFCs keep
|
/// Exact max-content size for one fallback word item. Paragraph IFCs keep
|
||||||
@@ -1326,6 +1392,68 @@ impl TextEngine {
|
|||||||
Some(Align::End | Align::Right) => 1.0,
|
Some(Align::End | Align::Right) => 1.0,
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
};
|
};
|
||||||
|
item.marker = None;
|
||||||
|
let marker_width = item
|
||||||
|
.marker_buffer
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|buffer| buffer.layout_runs().next().map(|run| run.line_w));
|
||||||
|
if let Some(marker_width) = marker_width {
|
||||||
|
let mut nonempty = 0usize;
|
||||||
|
let mut clamp_target = None;
|
||||||
|
let mut clamp_has_following_line = false;
|
||||||
|
let mut overflow_target = None;
|
||||||
|
for (line_index, run) in item.buffer.layout_runs().enumerate() {
|
||||||
|
if run.glyphs.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
nonempty += 1;
|
||||||
|
let line_offset = if line_index == 0 {
|
||||||
|
item.first_line_offset
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let content_end = run
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.map(|glyph| glyph.x + glyph.w + line_offset)
|
||||||
|
.fold(0.0f32, f32::max);
|
||||||
|
if item.line_clamp == Some(nonempty) {
|
||||||
|
clamp_target = Some((line_index, run.line_y, content_end));
|
||||||
|
} else if item.line_clamp.is_some_and(|limit| nonempty > limit) {
|
||||||
|
clamp_has_following_line = true;
|
||||||
|
}
|
||||||
|
if overflow_target.is_none()
|
||||||
|
&& item.ellipsis_overflow
|
||||||
|
&& content_end > content_width + 0.01
|
||||||
|
{
|
||||||
|
overflow_target = Some((line_index, run.line_y, content_end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let target = if clamp_has_following_line {
|
||||||
|
clamp_target.map(|target| (target, true))
|
||||||
|
} else {
|
||||||
|
overflow_target.map(|target| (target, false))
|
||||||
|
};
|
||||||
|
if let Some(((line_index, line_y, natural_end), is_clamp)) = target {
|
||||||
|
let available_marker_start = (content_width - marker_width).max(0.0);
|
||||||
|
let marker_x = if is_clamp {
|
||||||
|
natural_end.min(available_marker_start)
|
||||||
|
} else {
|
||||||
|
available_marker_start
|
||||||
|
};
|
||||||
|
let marker_line_y = item
|
||||||
|
.marker_buffer
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|buffer| buffer.layout_runs().next().map(|run| run.line_y))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
item.marker = Some(MarkerPlacement {
|
||||||
|
line_index,
|
||||||
|
x: marker_x,
|
||||||
|
y: line_y - marker_line_y,
|
||||||
|
content_end: marker_x,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
item.origin = (content_origin.0 + alignment_inset, content_origin.1);
|
item.origin = (content_origin.0 + alignment_inset, content_origin.1);
|
||||||
item.clip = clip;
|
item.clip = clip;
|
||||||
}
|
}
|
||||||
@@ -1775,9 +1903,15 @@ fn push_text(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Total shaped size of a buffer: widest line, and the bottom of the last line.
|
/// Total shaped size of a buffer: widest line, and the bottom of the last line.
|
||||||
fn buffer_size(buffer: &Buffer, first_line_offset: f32) -> (f32, f32) {
|
fn buffer_size(
|
||||||
|
buffer: &Buffer,
|
||||||
|
first_line_offset: f32,
|
||||||
|
line_clamp: Option<usize>,
|
||||||
|
) -> (f32, f32, bool) {
|
||||||
let mut w = 0.0f32;
|
let mut w = 0.0f32;
|
||||||
let mut h = 0.0f32;
|
let mut h = 0.0f32;
|
||||||
|
let mut nonempty_lines = 0usize;
|
||||||
|
let mut clamp_height = None;
|
||||||
for (line_index, run) in buffer.layout_runs().enumerate() {
|
for (line_index, run) in buffer.layout_runs().enumerate() {
|
||||||
let offset = if line_index == 0 {
|
let offset = if line_index == 0 {
|
||||||
first_line_offset
|
first_line_offset
|
||||||
@@ -1786,8 +1920,19 @@ fn buffer_size(buffer: &Buffer, first_line_offset: f32) -> (f32, f32) {
|
|||||||
};
|
};
|
||||||
w = w.max((run.line_w + offset).max(0.0));
|
w = w.max((run.line_w + offset).max(0.0));
|
||||||
h = h.max(run.line_top + run.line_height);
|
h = h.max(run.line_top + run.line_height);
|
||||||
|
if !run.glyphs.is_empty() {
|
||||||
|
nonempty_lines += 1;
|
||||||
|
if line_clamp == Some(nonempty_lines) {
|
||||||
|
clamp_height = Some(run.line_top + run.line_height);
|
||||||
}
|
}
|
||||||
(w.ceil(), h)
|
}
|
||||||
|
}
|
||||||
|
let clamped = line_clamp.is_some_and(|limit| nonempty_lines > limit);
|
||||||
|
(
|
||||||
|
w.ceil(),
|
||||||
|
if clamped { clamp_height.unwrap_or(h) } else { h },
|
||||||
|
clamped,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn used_text_indent(value: Dimension, width: Option<f32>) -> f32 {
|
fn used_text_indent(value: Dimension, width: Option<f32>) -> f32 {
|
||||||
@@ -2188,6 +2333,16 @@ impl TextEngine {
|
|||||||
let base_y = run.line_y;
|
let base_y = run.line_y;
|
||||||
let mut seg: Option<(f32, f32, f32, [u8; 4])> = None; // x0, x1, font_size, color
|
let mut seg: Option<(f32, f32, f32, [u8; 4])> = None; // x0, x1, font_size, color
|
||||||
for g in run.glyphs {
|
for g in run.glyphs {
|
||||||
|
// Keep decoration and background-clip bounds in lockstep with
|
||||||
|
// glyph painting. A truncated glyph must not leave an
|
||||||
|
// underline tail or expand a gradient's sampling bounds past
|
||||||
|
// the separately painted ellipsis marker.
|
||||||
|
if item.marker.is_some_and(|marker| {
|
||||||
|
marker.line_index == line_index
|
||||||
|
&& g.x + line_offset + g.w > marker.content_end
|
||||||
|
}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let underlined = g.metadata & META_UNDERLINE != 0;
|
let underlined = g.metadata & META_UNDERLINE != 0;
|
||||||
if let Some(fill_index) = metadata_fill(g.metadata) {
|
if let Some(fill_index) = metadata_fill(g.metadata) {
|
||||||
if let Some(bounds) = fill_bounds.get_mut(fill_index) {
|
if let Some(bounds) = fill_bounds.get_mut(fill_index) {
|
||||||
@@ -2253,6 +2408,12 @@ impl TextEngine {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
for glyph in run.glyphs {
|
for glyph in run.glyphs {
|
||||||
|
if item.marker.is_some_and(|marker| {
|
||||||
|
marker.line_index == line_index
|
||||||
|
&& glyph.x + line_offset + glyph.w > marker.content_end
|
||||||
|
}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let physical = glyph.physical((line_offset, 0.0), 1.0);
|
let physical = glyph.physical((line_offset, 0.0), 1.0);
|
||||||
let glyph_color = glyph.color_opt.unwrap_or(default);
|
let glyph_color = glyph.color_opt.unwrap_or(default);
|
||||||
let fill_index = metadata_fill(glyph.metadata);
|
let fill_index = metadata_fill(glyph.metadata);
|
||||||
@@ -2348,6 +2509,50 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The marker owns its own shaped buffer so it uses a real U+2026
|
||||||
|
// glyph from the selected font without changing DOM text or the
|
||||||
|
// natural content buffer. The direct pure-text slice is LTR-only;
|
||||||
|
// logical-start marker placement arrives with the bidi foundation.
|
||||||
|
if let (Some(marker), Some(marker_buffer)) = (item.marker, item.marker_buffer.as_ref()) {
|
||||||
|
marker_buffer.draw(font_system, swash, default, |x, y, _, _, color| {
|
||||||
|
let px = ox as i32 + marker.x.round() as i32 + x;
|
||||||
|
let py = oy as i32 + marker.y.round() as i32 + y;
|
||||||
|
if let Some((cx0, cy0, cx1, cy1)) = clip_bounds {
|
||||||
|
if (px as f32) < cx0
|
||||||
|
|| (px as f32) >= cx1
|
||||||
|
|| (py as f32) < cy0
|
||||||
|
|| (py as f32) >= cy1
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if px < 0 || px >= pw || py < 0 || py >= ph {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let alpha = color.a() as u32;
|
||||||
|
if alpha == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let index = (py * pw + px) as usize;
|
||||||
|
let dst = pixels[index];
|
||||||
|
let inverse = 255 - alpha;
|
||||||
|
let out_alpha = alpha + dst.alpha() as u32 * inverse / 255;
|
||||||
|
let out_red = color.r() as u32 * alpha / 255
|
||||||
|
+ dst.red() as u32 * inverse / 255;
|
||||||
|
let out_green = color.g() as u32 * alpha / 255
|
||||||
|
+ dst.green() as u32 * inverse / 255;
|
||||||
|
let out_blue = color.b() as u32 * alpha / 255
|
||||||
|
+ dst.blue() as u32 * inverse / 255;
|
||||||
|
pixels[index] = tiny_skia::PremultipliedColorU8::from_rgba(
|
||||||
|
out_red as u8,
|
||||||
|
out_green as u8,
|
||||||
|
out_blue as u8,
|
||||||
|
out_alpha as u8,
|
||||||
|
)
|
||||||
|
.unwrap_or(dst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Stroke underline segments with the same authored alpha as their
|
// Stroke underline segments with the same authored alpha as their
|
||||||
// glyphs. Transparent text decorations are transparent too.
|
// glyphs. Transparent text decorations are transparent too.
|
||||||
for (x0, x1, y, thick, col) in underlines {
|
for (x0, x1, y, thick, col) in underlines {
|
||||||
|
|||||||
@@ -452,6 +452,15 @@ pub struct LayoutStyle {
|
|||||||
/// until the parent's computed outer/inner display is known. The DOM
|
/// until the parent's computed outer/inner display is known. The DOM
|
||||||
/// top-down pass copies that provenance and then clears this marker.
|
/// top-down pass copies that provenance and then clears this marker.
|
||||||
pub(crate) display_inherit: bool,
|
pub(crate) display_inherit: bool,
|
||||||
|
/// Original legacy flexbox display provenance. `Some(false)` is
|
||||||
|
/// `-webkit-box`; `Some(true)` is `-webkit-inline-box`. A vertical legacy
|
||||||
|
/// box with an active line clamp computes to flow-root/inline-block, but
|
||||||
|
/// retaining the specified display makes that adjustment independent of
|
||||||
|
/// declaration order and keeps unclamped CSSOM serialization honest.
|
||||||
|
pub(crate) webkit_box_display: Option<bool>,
|
||||||
|
/// Computed legacy box orientation. Only the vertical/block-axis form
|
||||||
|
/// activates the legacy WebKit line-clamp contract.
|
||||||
|
pub(crate) webkit_box_orient_vertical: bool,
|
||||||
/// Computed CSS `container-type` (not inherited).
|
/// Computed CSS `container-type` (not inherited).
|
||||||
pub container_type: ContainerType,
|
pub container_type: ContainerType,
|
||||||
/// The specified value was the CSS-wide `inherit` keyword. Resolved
|
/// The specified value was the CSS-wide `inherit` keyword. Resolved
|
||||||
@@ -934,6 +943,14 @@ pub struct LayoutStyle {
|
|||||||
/// value (or the initial `normal`). The inline shaper uses this to retain
|
/// value (or the initial `normal`). The inline shaper uses this to retain
|
||||||
/// author/source newlines in code blocks and to select wrapping behavior.
|
/// author/source newlines in code blocks and to select wrapping behavior.
|
||||||
pub white_space: Option<WhiteSpace>,
|
pub white_space: Option<WhiteSpace>,
|
||||||
|
/// Non-inherited `text-overflow`. This first implementation deliberately
|
||||||
|
/// models Chromium's single-value `clip|ellipsis` syntax; bidi/two-sided
|
||||||
|
/// markers remain a separate extension.
|
||||||
|
pub text_overflow: TextOverflow,
|
||||||
|
/// Non-inherited legacy line count. It affects direct pure-text inline
|
||||||
|
/// formatting contexts only; nested descendant line counting needs a real
|
||||||
|
/// block-line iterator and must not be approximated by clipping children.
|
||||||
|
pub webkit_line_clamp: Option<u32>,
|
||||||
/// `overflow-wrap` (legacy alias: `word-wrap`). Inherited; `None` means
|
/// `overflow-wrap` (legacy alias: `word-wrap`). Inherited; `None` means
|
||||||
/// inherit the nearest ancestor's value (or the initial `normal`).
|
/// inherit the nearest ancestor's value (or the initial `normal`).
|
||||||
///
|
///
|
||||||
@@ -1168,6 +1185,16 @@ pub enum WhiteSpace {
|
|||||||
BreakSpaces,
|
BreakSpaces,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single-value CSS `text-overflow` behavior supported by Chromium's default
|
||||||
|
/// feature set. The marker is generated by the inline formatter and never
|
||||||
|
/// inserted into DOM text.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum TextOverflow {
|
||||||
|
#[default]
|
||||||
|
Clip,
|
||||||
|
Ellipsis,
|
||||||
|
}
|
||||||
|
|
||||||
/// Emergency wrapping behavior for otherwise-unbreakable text.
|
/// Emergency wrapping behavior for otherwise-unbreakable text.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub enum OverflowWrap {
|
pub enum OverflowWrap {
|
||||||
|
|||||||
@@ -412,10 +412,19 @@ impl PreparedRender {
|
|||||||
let rect = self.layout.rects.get(&id);
|
let rect = self.layout.rects.get(&id);
|
||||||
let mut out = HashMap::new();
|
let mut out = HashMap::new();
|
||||||
|
|
||||||
|
let active_webkit_clamp = style.webkit_box_display.is_some()
|
||||||
|
&& style.webkit_box_orient_vertical
|
||||||
|
&& style.webkit_line_clamp.is_some();
|
||||||
let display = if style.display_contents {
|
let display = if style.display_contents {
|
||||||
"contents"
|
"contents"
|
||||||
} else if style.display == crate::Display::None {
|
} else if style.display == crate::Display::None {
|
||||||
"none"
|
"none"
|
||||||
|
} else if active_webkit_clamp && style.webkit_box_display == Some(false) {
|
||||||
|
"flow-root"
|
||||||
|
} else if style.webkit_box_display == Some(false) && !active_webkit_clamp {
|
||||||
|
"-webkit-box"
|
||||||
|
} else if style.webkit_box_display == Some(true) && !active_webkit_clamp {
|
||||||
|
"-webkit-inline-box"
|
||||||
} else if style.internal_flex_container {
|
} else if style.internal_flex_container {
|
||||||
"block"
|
"block"
|
||||||
} else {
|
} else {
|
||||||
@@ -501,6 +510,27 @@ impl PreparedRender {
|
|||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
out.insert(
|
||||||
|
"text-overflow",
|
||||||
|
match style.text_overflow {
|
||||||
|
crate::TextOverflow::Clip => "clip",
|
||||||
|
crate::TextOverflow::Ellipsis => "ellipsis",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"-webkit-line-clamp",
|
||||||
|
style.webkit_line_clamp.map_or_else(|| "none".to_string(), |lines| lines.to_string()),
|
||||||
|
);
|
||||||
|
out.insert(
|
||||||
|
"-webkit-box-orient",
|
||||||
|
if style.webkit_box_orient_vertical {
|
||||||
|
"vertical"
|
||||||
|
} else {
|
||||||
|
"horizontal"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
let overflow_wrap = match style.overflow_wrap.unwrap_or_default() {
|
let overflow_wrap = match style.overflow_wrap.unwrap_or_default() {
|
||||||
crate::OverflowWrap::Normal => "normal",
|
crate::OverflowWrap::Normal => "normal",
|
||||||
crate::OverflowWrap::BreakWord => "break-word",
|
crate::OverflowWrap::BreakWord => "break-word",
|
||||||
@@ -6938,6 +6968,111 @@ mod tests {
|
|||||||
assert_eq!(prepared.computed_style(id).unwrap()["outline-width"], "0px");
|
assert_eq!(prepared.computed_style(id).unwrap()["outline-width"], "0px");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_pure_text_webkit_clamp_matches_line_geometry_and_computed_values() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<html><body style="margin:0">
|
||||||
|
<div id="clamped" style="width:120px;font:16px/20px Arial;
|
||||||
|
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
|
||||||
|
overflow:hidden">one<br>two<br>three<br>four<br>five</div>
|
||||||
|
<div id="exact" style="width:120px;font:16px/20px Arial;
|
||||||
|
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
|
||||||
|
overflow:hidden">one<br>two</div>
|
||||||
|
<div id="inactive" style="width:120px;font:16px/20px Arial;
|
||||||
|
-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden">
|
||||||
|
one<br>two<br>three<br>four<br>five</div>
|
||||||
|
<div id="nested" style="width:120px;font:16px/20px Arial;
|
||||||
|
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
|
||||||
|
overflow:hidden"><div>one</div><div>two</div><div>three</div></div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let clamped = tree.get_element_by_id("clamped").unwrap();
|
||||||
|
let exact = tree.get_element_by_id("exact").unwrap();
|
||||||
|
let inactive = tree.get_element_by_id("inactive").unwrap();
|
||||||
|
let nested = tree.get_element_by_id("nested").unwrap();
|
||||||
|
let mut resources = RenderResourceCache::default();
|
||||||
|
let prepared = prepare_dom(&tree, (300.0, 300.0), None, &mut resources).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(prepared.document_rect(clamped).unwrap().height, 40.0);
|
||||||
|
assert_eq!(prepared.document_rect(exact).unwrap().height, 40.0);
|
||||||
|
assert_eq!(prepared.document_rect(inactive).unwrap().height, 100.0);
|
||||||
|
assert_eq!(
|
||||||
|
prepared.document_rect(nested).unwrap().height,
|
||||||
|
60.0,
|
||||||
|
"this slice must not fake descendant block-line counting by clipping children"
|
||||||
|
);
|
||||||
|
let computed = prepared.computed_style(clamped).unwrap();
|
||||||
|
assert_eq!(computed["display"], "flow-root");
|
||||||
|
assert_eq!(computed["-webkit-line-clamp"], "2");
|
||||||
|
assert_eq!(computed["-webkit-box-orient"], "vertical");
|
||||||
|
assert_eq!(computed["text-overflow"], "clip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncation_markers_are_shaped_and_painted_without_changing_dom_text() {
|
||||||
|
let ellipsis_tree = parse_html(
|
||||||
|
r#"<html><body style="margin:0"><div id="nowrap" style="position:absolute;top:0;width:120px;font:16px/20px Arial;
|
||||||
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">ABCDEFGHIJKLMNO</div>
|
||||||
|
<div id="clamp" style="position:absolute;top:20px;width:120px;font:16px/20px Arial;display:-webkit-box;
|
||||||
|
-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden">one<br>two<br>three</div>
|
||||||
|
<div id="exact" style="position:absolute;top:80px;width:120px;font:16px/20px Arial;display:-webkit-box;
|
||||||
|
-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden">one<br>two</div>
|
||||||
|
<div id="visible" style="position:absolute;top:120px;width:120px;font:16px/20px Arial;
|
||||||
|
white-space:nowrap;overflow:visible;text-overflow:ellipsis">ABCDEFGHIJKLMNO</div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let clip_tree = parse_html(
|
||||||
|
r#"<html><body style="margin:0"><div id="nowrap" style="position:absolute;top:0;width:120px;font:16px/20px Arial;
|
||||||
|
white-space:nowrap;overflow:hidden;text-overflow:clip">ABCDEFGHIJKLMNO</div>
|
||||||
|
<div id="clamp" style="position:absolute;top:20px;width:120px;font:16px/20px Arial;overflow:hidden">one<br>two<br>three</div>
|
||||||
|
<div id="exact" style="position:absolute;top:80px;width:120px;font:16px/20px Arial;overflow:hidden">one<br>two</div>
|
||||||
|
<div id="visible" style="position:absolute;top:120px;width:120px;font:16px/20px Arial;
|
||||||
|
white-space:nowrap;overflow:visible;text-overflow:clip">ABCDEFGHIJKLMNO</div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let ellipsis = paint_dom(&ellipsis_tree, (180.0, 160.0), None).unwrap();
|
||||||
|
let clip = paint_dom(&clip_tree, (180.0, 160.0), None).unwrap();
|
||||||
|
|
||||||
|
let nowrap_edge_difference = (85..120)
|
||||||
|
.flat_map(|x| (0..20).map(move |y| (x, y)))
|
||||||
|
.filter(|&(x, y)| ellipsis.pixel(x, y) != clip.pixel(x, y))
|
||||||
|
.count();
|
||||||
|
assert!(
|
||||||
|
nowrap_edge_difference > 8,
|
||||||
|
"ellipsis must replace end glyph pixels: {nowrap_edge_difference}"
|
||||||
|
);
|
||||||
|
let clamp_marker_difference = (20..55)
|
||||||
|
.flat_map(|x| (40..60).map(move |y| (x, y)))
|
||||||
|
.filter(|&(x, y)| ellipsis.pixel(x, y) != clip.pixel(x, y))
|
||||||
|
.count();
|
||||||
|
assert!(
|
||||||
|
clamp_marker_difference > 4,
|
||||||
|
"line clamp must paint a separately shaped marker: {clamp_marker_difference}"
|
||||||
|
);
|
||||||
|
let exact_line_difference = (0..140)
|
||||||
|
.flat_map(|x| (80..120).map(move |y| (x, y)))
|
||||||
|
.filter(|&(x, y)| ellipsis.pixel(x, y) != clip.pixel(x, y))
|
||||||
|
.count();
|
||||||
|
assert_eq!(
|
||||||
|
exact_line_difference, 0,
|
||||||
|
"an exactly-full clamp must not synthesize an overflow marker"
|
||||||
|
);
|
||||||
|
let visible_overflow_difference = (0..180)
|
||||||
|
.flat_map(|x| (120..140).map(move |y| (x, y)))
|
||||||
|
.filter(|&(x, y)| ellipsis.pixel(x, y) != clip.pixel(x, y))
|
||||||
|
.count();
|
||||||
|
assert_eq!(
|
||||||
|
visible_overflow_difference, 0,
|
||||||
|
"text-overflow must not synthesize a marker when overflow is visible"
|
||||||
|
);
|
||||||
|
for id in ["nowrap", "clamp", "exact", "visible"] {
|
||||||
|
assert_eq!(
|
||||||
|
ellipsis_tree.text_content(ellipsis_tree.get_element_by_id(id).unwrap()),
|
||||||
|
clip_tree.text_content(clip_tree.get_element_by_id(id).unwrap()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn percentage_border_radius_paints_circles_ellipses_and_replaced_clips() {
|
fn percentage_border_radius_paints_circles_ellipses_and_replaced_clips() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -675,6 +675,8 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
| "flow-root"
|
| "flow-root"
|
||||||
| "table"
|
| "table"
|
||||||
| "inline-table"
|
| "inline-table"
|
||||||
|
| "-webkit-box"
|
||||||
|
| "-webkit-inline-box"
|
||||||
| "contents"
|
| "contents"
|
||||||
| "inherit"
|
| "inherit"
|
||||||
| "initial"
|
| "initial"
|
||||||
@@ -690,6 +692,7 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
style.flow_root = false;
|
style.flow_root = false;
|
||||||
style.display_contents = false;
|
style.display_contents = false;
|
||||||
style.display_inherit = false;
|
style.display_inherit = false;
|
||||||
|
style.webkit_box_display = None;
|
||||||
}
|
}
|
||||||
match value.as_str() {
|
match value.as_str() {
|
||||||
"none" => style.display = crate::Display::None,
|
"none" => style.display = crate::Display::None,
|
||||||
@@ -728,6 +731,19 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
style.flow_root = true;
|
style.flow_root = true;
|
||||||
style.is_table_box = true;
|
style.is_table_box = true;
|
||||||
}
|
}
|
||||||
|
"-webkit-box" => {
|
||||||
|
// The unclamped legacy display is an old flexbox. When it
|
||||||
|
// forms a vertical line-clamp root, the post-declaration
|
||||||
|
// adjustment below converts it to a flow-root just as
|
||||||
|
// Blink and Gecko do at computed-value time.
|
||||||
|
style.display = crate::Display::Flex;
|
||||||
|
style.webkit_box_display = Some(false);
|
||||||
|
}
|
||||||
|
"-webkit-inline-box" => {
|
||||||
|
style.display = crate::Display::Flex;
|
||||||
|
style.is_inline_block = true;
|
||||||
|
style.webkit_box_display = Some(true);
|
||||||
|
}
|
||||||
"contents" => {
|
"contents" => {
|
||||||
// `display:contents` can override an earlier `display:none`
|
// `display:contents` can override an earlier `display:none`
|
||||||
// in the cascade (responsive desktop/mobile wrappers do
|
// in the cascade (responsive desktop/mobile wrappers do
|
||||||
@@ -1438,6 +1454,27 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
_ => style.white_space,
|
_ => style.white_space,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
"text-overflow" => {
|
||||||
|
style.text_overflow = match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"ellipsis" => crate::TextOverflow::Ellipsis,
|
||||||
|
"clip" | "initial" | "unset" | "revert" | "revert-layer" => {
|
||||||
|
crate::TextOverflow::Clip
|
||||||
|
}
|
||||||
|
_ => style.text_overflow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
"-webkit-line-clamp" => {
|
||||||
|
if let Some(lines) = webkit_line_clamp_value(value) {
|
||||||
|
style.webkit_line_clamp = lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"-webkit-box-orient" => {
|
||||||
|
style.webkit_box_orient_vertical = match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"vertical" | "block-axis" => true,
|
||||||
|
"horizontal" | "inline-axis" | "initial" | "unset" | "revert" | "revert-layer" => false,
|
||||||
|
_ => style.webkit_box_orient_vertical,
|
||||||
|
};
|
||||||
|
}
|
||||||
"overflow-wrap" | "word-wrap" => {
|
"overflow-wrap" | "word-wrap" => {
|
||||||
style.overflow_wrap = match value.trim().to_ascii_lowercase().as_str() {
|
style.overflow_wrap = match value.trim().to_ascii_lowercase().as_str() {
|
||||||
"normal" | "initial" => Some(crate::OverflowWrap::Normal),
|
"normal" | "initial" => Some(crate::OverflowWrap::Normal),
|
||||||
@@ -1617,6 +1654,49 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
normalize_webkit_line_clamp_display(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the legacy WebKit line-clamp display adjustment from specified
|
||||||
|
/// provenance. Calling this after every accepted declaration makes the result
|
||||||
|
/// independent of whether display, orientation, or clamp appeared first.
|
||||||
|
pub(crate) fn normalize_webkit_line_clamp_display(style: &mut LayoutStyle) {
|
||||||
|
let Some(inline) = style.webkit_box_display else { return };
|
||||||
|
if style.webkit_box_orient_vertical && style.webkit_line_clamp.is_some() {
|
||||||
|
style.display = crate::Display::Block;
|
||||||
|
style.is_inline_block = inline;
|
||||||
|
style.flow_root = true;
|
||||||
|
} else {
|
||||||
|
style.display = crate::Display::Flex;
|
||||||
|
style.is_inline_block = inline;
|
||||||
|
style.flow_root = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the legacy positive-integer clamp and match Chromium's saturation at
|
||||||
|
/// the signed 32-bit maximum. CSS accepts arbitrarily long integer tokens;
|
||||||
|
/// rejecting them because a Rust integer conversion overflowed would make
|
||||||
|
/// both `CSS.supports()` and the cascade diverge from the browser.
|
||||||
|
fn webkit_line_clamp_value(value: &str) -> Option<Option<u32>> {
|
||||||
|
let value = value.trim();
|
||||||
|
if value.eq_ignore_ascii_case("none")
|
||||||
|
|| matches!(
|
||||||
|
value.to_ascii_lowercase().as_str(),
|
||||||
|
"initial" | "unset" | "revert" | "revert-layer"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Some(None);
|
||||||
|
}
|
||||||
|
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let first_nonzero = value.bytes().find(|byte| *byte != b'0')?;
|
||||||
|
debug_assert!(first_nonzero.is_ascii_digit());
|
||||||
|
let lines = value
|
||||||
|
.parse::<u64>()
|
||||||
|
.unwrap_or(u64::MAX)
|
||||||
|
.min(i32::MAX as u64) as u32;
|
||||||
|
Some(Some(lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a declaration is syntactically supported by the renderer's CSS
|
/// Whether a declaration is syntactically supported by the renderer's CSS
|
||||||
@@ -1745,6 +1825,9 @@ pub fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
| "text-decoration-line"
|
| "text-decoration-line"
|
||||||
| "line-height"
|
| "line-height"
|
||||||
| "white-space"
|
| "white-space"
|
||||||
|
| "text-overflow"
|
||||||
|
| "-webkit-line-clamp"
|
||||||
|
| "-webkit-box-orient"
|
||||||
| "overflow-wrap"
|
| "overflow-wrap"
|
||||||
| "word-wrap"
|
| "word-wrap"
|
||||||
| "word-break"
|
| "word-break"
|
||||||
@@ -1866,6 +1949,8 @@ pub fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
| "flow-root"
|
| "flow-root"
|
||||||
| "table"
|
| "table"
|
||||||
| "inline-table"
|
| "inline-table"
|
||||||
|
| "-webkit-box"
|
||||||
|
| "-webkit-inline-box"
|
||||||
| "contents"
|
| "contents"
|
||||||
),
|
),
|
||||||
"position" => matches!(
|
"position" => matches!(
|
||||||
@@ -1891,6 +1976,15 @@ pub fn supports_declaration(name: &str, value: &str) -> bool {
|
|||||||
value.to_ascii_lowercase().as_str(),
|
value.to_ascii_lowercase().as_str(),
|
||||||
"normal" | "nowrap" | "pre" | "pre-wrap" | "pre-line" | "break-spaces"
|
"normal" | "nowrap" | "pre" | "pre-wrap" | "pre-line" | "break-spaces"
|
||||||
),
|
),
|
||||||
|
"text-overflow" => matches!(
|
||||||
|
value.to_ascii_lowercase().as_str(),
|
||||||
|
"clip" | "ellipsis"
|
||||||
|
),
|
||||||
|
"-webkit-line-clamp" => webkit_line_clamp_value(value).is_some(),
|
||||||
|
"-webkit-box-orient" => matches!(
|
||||||
|
value.to_ascii_lowercase().as_str(),
|
||||||
|
"horizontal" | "vertical" | "inline-axis" | "block-axis"
|
||||||
|
),
|
||||||
"overflow-wrap" | "word-wrap" => matches!(
|
"overflow-wrap" | "word-wrap" => matches!(
|
||||||
value.to_ascii_lowercase().as_str(),
|
value.to_ascii_lowercase().as_str(),
|
||||||
"normal" | "break-word" | "anywhere"
|
"normal" | "break-word" | "anywhere"
|
||||||
@@ -7416,6 +7510,75 @@ mod tests {
|
|||||||
assert!(!supports_declaration("text-indent", "10px hanging"));
|
assert!(!supports_declaration("text-indent", "10px hanging"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncation_properties_parse_strictly_and_share_support_truth() {
|
||||||
|
assert!(supports_declaration("text-overflow", "clip"));
|
||||||
|
assert!(supports_declaration("text-overflow", "ellipsis"));
|
||||||
|
assert!(!supports_declaration("text-overflow", "clip ellipsis"));
|
||||||
|
assert!(!supports_declaration("text-overflow", "fade"));
|
||||||
|
|
||||||
|
for value in ["1", "2", "4294967295", "999999999999999999999", "none"] {
|
||||||
|
assert!(supports_declaration("-webkit-line-clamp", value), "{value}");
|
||||||
|
}
|
||||||
|
for value in ["0", "-1", "1.5", "2px", ""] {
|
||||||
|
assert!(!supports_declaration("-webkit-line-clamp", value), "{value}");
|
||||||
|
}
|
||||||
|
for value in ["horizontal", "vertical", "inline-axis", "block-axis"] {
|
||||||
|
assert!(supports_declaration("-webkit-box-orient", value), "{value}");
|
||||||
|
}
|
||||||
|
assert!(supports_declaration("display", "-webkit-box"));
|
||||||
|
assert!(supports_declaration("display", "-webkit-inline-box"));
|
||||||
|
|
||||||
|
let invalid = compute_style(
|
||||||
|
"div",
|
||||||
|
Some("text-overflow:ellipsis;text-overflow:fade;-webkit-line-clamp:2;-webkit-line-clamp:0"),
|
||||||
|
);
|
||||||
|
assert_eq!(invalid.text_overflow, crate::TextOverflow::Ellipsis);
|
||||||
|
assert_eq!(invalid.webkit_line_clamp, Some(2));
|
||||||
|
|
||||||
|
let saturated = compute_style(
|
||||||
|
"div",
|
||||||
|
Some("-webkit-line-clamp:999999999999999999999"),
|
||||||
|
);
|
||||||
|
assert_eq!(saturated.webkit_line_clamp, Some(i32::MAX as u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn webkit_clamp_display_adjustment_is_declaration_order_independent() {
|
||||||
|
for css in [
|
||||||
|
"display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2",
|
||||||
|
"-webkit-line-clamp:2;display:-webkit-box;-webkit-box-orient:block-axis",
|
||||||
|
"-webkit-box-orient:vertical;-webkit-line-clamp:2;display:-webkit-box",
|
||||||
|
] {
|
||||||
|
let style = compute_style("div", Some(css));
|
||||||
|
assert_eq!(style.display, crate::Display::Block, "{css}");
|
||||||
|
assert!(style.flow_root, "{css}");
|
||||||
|
assert!(!style.is_inline_block, "{css}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let inline = compute_style(
|
||||||
|
"span",
|
||||||
|
Some("display:-webkit-inline-box;-webkit-box-orient:vertical;-webkit-line-clamp:2"),
|
||||||
|
);
|
||||||
|
assert_eq!(inline.display, crate::Display::Block);
|
||||||
|
assert!(inline.flow_root);
|
||||||
|
assert!(inline.is_inline_block);
|
||||||
|
|
||||||
|
let horizontal = compute_style(
|
||||||
|
"div",
|
||||||
|
Some("display:-webkit-box;-webkit-box-orient:horizontal;-webkit-line-clamp:2"),
|
||||||
|
);
|
||||||
|
assert_eq!(horizontal.display, crate::Display::Flex);
|
||||||
|
assert!(!horizontal.flow_root);
|
||||||
|
|
||||||
|
let cleared = compute_style(
|
||||||
|
"div",
|
||||||
|
Some("display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;-webkit-line-clamp:none"),
|
||||||
|
);
|
||||||
|
assert_eq!(cleared.display, crate::Display::Flex);
|
||||||
|
assert!(!cleared.flow_root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn text_break_longhands_parse_alias_and_css_wide_values() {
|
fn text_break_longhands_parse_alias_and_css_wide_values() {
|
||||||
let values = compute_style(
|
let values = compute_style(
|
||||||
|
|||||||
Reference in new issue
Block a user