mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 10:36:27 +03:00
A snapshot records each line once, a line stops at the gutter, and a design document's directive governs what follows it
The re-review of the first round found four places where the new
measurements reached further than they should.
- The snapshot recorded the rects of every text node *under* each element,
so a line rendered deep in a page was written down again for every
ancestor above it. On a deep, text-heavy page that multiplies the
capture by its depth and can carry it past the byte cap, and a capture
that fails is a scan that returns nothing at all. Each element records
only its own text now — one line, one entry — and `text_line_rects`
assembles an element's lines by walking the tree the capture already
serialized.
- Merging rects by vertical band alone made two columns that happen to
sit on the same rows into one page-wide line. A fragment joins a row now
only when it runs on from it: a horizontal gap no wider than the row's
own line box. The fragments of a wrapped line are contiguous; a gutter
is not. An inline image wider than the leading splits its line by the
same test, which understates a line rather than overstating it, and that
is the direction this rule should err in.
- In DESIGN.md, where the negative word sits decides what it governs. A
state ("`.card-old` is deprecated") describes whatever its clause is
about; a directive ("never use `.x`") condemns what follows it and
nothing before it. Reading the whole clause for either lost the
sanctioned half of "Use `.kicker` and never `.tagline`". The headings a
document uses to retire a set — "Retired components", "Unsupported
patterns" — are read as negative now, and a heading that names both
sides ("Dos and Don'ts") heads a section of both, so its subsections are
what say which is which.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d1f81fc1c7
commit
0fea696a7f
@@ -69,29 +69,26 @@ const __SNAP_DEFAULT_MAX_BYTES = 48 * 1024 * 1024;
|
||||
function __snapRect4(r) { return [r.x, r.y, r.width, r.height]; }
|
||||
function __snapNum(v) { return typeof v === 'number' ? v : null; }
|
||||
|
||||
// The client rects of the non-blank text nodes under `node` (same walk as
|
||||
// 10-probe.js#__collectTextRects). `deep` includes element descendants.
|
||||
function __snapTextRects(node, deep, out) {
|
||||
// The client rects of `node`'s own non-blank text nodes (same walk as
|
||||
// 10-probe.js#__collectTextRects with `deep` off). Each element records only
|
||||
// its own, so a line that rendered is recorded exactly once in a snapshot.
|
||||
function __snapTextRects(node, out) {
|
||||
for (const child of node.childNodes) {
|
||||
if (child.nodeType === 3) {
|
||||
if (!(child.textContent || '').trim()) continue;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(child);
|
||||
for (const rect of range.getClientRects()) {
|
||||
if (rect.width >= 1 && rect.height >= 1) out.push(rect);
|
||||
}
|
||||
range.detach?.();
|
||||
} else if (deep && child.nodeType === 1) {
|
||||
__snapTextRects(child, true, out);
|
||||
if (child.nodeType !== 3) continue;
|
||||
if (!(child.textContent || '').trim()) continue;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(child);
|
||||
for (const rect of range.getClientRects()) {
|
||||
if (rect.width >= 1 && rect.height >= 1) out.push(rect);
|
||||
}
|
||||
range.detach?.();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// getDirectTextRect(el): union of the client rects of the element's
|
||||
// non-blank direct text nodes (same measure as 10-probe.js).
|
||||
function __snapDirectTextRect(node) {
|
||||
const rects = __snapTextRects(node, false, []);
|
||||
function __snapDirectTextRectOf(rects) {
|
||||
if (rects.length === 0) return null;
|
||||
const left = Math.min(...rects.map(r => r.left));
|
||||
const top = Math.min(...rects.map(r => r.top));
|
||||
@@ -100,16 +97,6 @@ function __snapDirectTextRect(node) {
|
||||
return [left, top, right - left, bottom - top];
|
||||
}
|
||||
|
||||
// Every rect of the element's rendered text, descendants included: the
|
||||
// snapshot half of `text_rects` in 10-probe.js. Recorded rather than derived,
|
||||
// because a union of a long first line and a short tail says nothing about
|
||||
// either, and a rule about lines that is handed only the union has to stand
|
||||
// down. `textLines` on the snapshot is what tells the consumer these are
|
||||
// here at all.
|
||||
function __snapTextRects4(node) {
|
||||
return __snapTextRects(node, true, []).map(r => [r.x, r.y, r.width, r.height]);
|
||||
}
|
||||
|
||||
// ─── Linked stylesheet corpus (JS: injected/index.mjs #709) ────────────────
|
||||
|
||||
// JS: injected/index.mjs#pseudoElementHostSelector
|
||||
@@ -650,10 +637,17 @@ const __impeccableSnapshot = {
|
||||
rec.v = typeof el.checkVisibility === 'function'
|
||||
? (el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true }) ? 1 : 0)
|
||||
: -1;
|
||||
const dtr = __snapDirectTextRect(el);
|
||||
// The element's OWN text rects, unmerged (`dl`), and their union
|
||||
// (`d`, the long-standing field). Own and not the subtree's: every
|
||||
// ancestor would otherwise carry a copy of every line under it, which
|
||||
// on a deep text-heavy page multiplies the snapshot by its depth and
|
||||
// can push it past the byte cap. The consumer walks the tree and
|
||||
// assembles an element's lines from the rects its descendants each
|
||||
// recorded once (`SnapshotDom::text_line_rects`).
|
||||
const own = __snapTextRects(el, []);
|
||||
const dtr = __snapDirectTextRectOf(own);
|
||||
if (dtr) rec.d = dtr;
|
||||
const tr = __snapTextRects4(el);
|
||||
if (tr.length) rec.dl = tr;
|
||||
if (own.length) rec.dl = own.map(r => [r.x, r.y, r.width, r.height]);
|
||||
if (el.isContentEditable) rec.e = true;
|
||||
if (el.hidden) rec.h = true;
|
||||
if (typeof el.id !== 'string') rec.i = true;
|
||||
|
||||
@@ -968,6 +968,33 @@ mod tests {
|
||||
assert!(!hits.iter().any(|h| h.id == "line-length"), "{hits:?}");
|
||||
}
|
||||
|
||||
/// Two columns that happen to sit on the same rows are two flows, not one
|
||||
/// page-wide line. Fragments join a row only when they run on from it —
|
||||
/// a gap no wider than the row's own line box — so a gutter keeps them
|
||||
/// apart and the characters stay where the reader sees them.
|
||||
#[test]
|
||||
fn columns_on_the_same_rows_are_separate_lines() {
|
||||
let mut d = FakeDom::new();
|
||||
let (_h, body) = d.with_page();
|
||||
let p = text_el(&mut d, body, "p", &"w".repeat(240), "16px");
|
||||
d.set_rect(p, 0.0, 100.0, 1020.0, 72.0);
|
||||
// Two 300px columns with a 100px gutter, three rows each.
|
||||
d.set_text_lines(
|
||||
p,
|
||||
&[
|
||||
(0.0, 100.0, 300.0, 19.0),
|
||||
(400.0, 100.0, 300.0, 19.0),
|
||||
(0.0, 124.0, 300.0, 19.0),
|
||||
(400.0, 124.0, 300.0, 19.0),
|
||||
(0.0, 148.0, 300.0, 19.0),
|
||||
(400.0, 148.0, 300.0, 19.0),
|
||||
],
|
||||
);
|
||||
let hits = check_element_quality_dom(&d, p, &BrowserConfig::default());
|
||||
// Six short lines of 40 characters each, not three of 700px.
|
||||
assert!(!hits.iter().any(|h| h.id == "line-length"), "{hits:?}");
|
||||
}
|
||||
|
||||
/// A rect that is already one line is one line, whatever the leading is.
|
||||
/// Dividing every rect by the line box turned a paragraph whose leading
|
||||
/// is tighter than its glyph box into two copies of the same line, and
|
||||
|
||||
@@ -44,10 +44,12 @@ re!(
|
||||
"^\\.[A-Za-z_][A-Za-z0-9_-]*$".to_string()
|
||||
);
|
||||
// A design document's prose, cut where one statement stops and the next
|
||||
// starts. `instead of` / `rather than` open a clause about what the document
|
||||
// is steering *away* from; the reversals (`outside`, `except`, ...) open one
|
||||
// about what it is steering *toward*, which is what lets "no ALL CAPS outside
|
||||
// the `.eyebrow` class" declare `.eyebrow`.
|
||||
// starts: punctuation, a line break, and the phrases that turn a sentence
|
||||
// around. `instead of` / `rather than` open a
|
||||
// clause about what the document is steering *away* from; the reversals
|
||||
// (`outside`, `except`, ...) open one about what it is steering *toward*,
|
||||
// which is what lets "no ALL CAPS outside the `.eyebrow` class" declare
|
||||
// `.eyebrow`.
|
||||
re!(
|
||||
DESIGN_CLAUSE_SPLIT,
|
||||
r"(?i)[.!?;:,()\[\]\n]|\u{2014}|\u{2013}|\binstead of\b|\brather than\b|\bas opposed to\b|\boutside\b|\bexcept\b|\bother than\b|\bunless\b|\bbesides\b|\bapart from\b|\bbeyond\b".to_string()
|
||||
@@ -56,15 +58,30 @@ re!(
|
||||
DESIGN_NEGATING_BOUNDARY,
|
||||
r"(?i)^(?:instead of|rather than|as opposed to)$".to_string()
|
||||
);
|
||||
// Words that condemn whatever their clause names.
|
||||
// A directive: it condemns what comes after it, and nothing before it.
|
||||
// "Use `.kicker` and never `.tagline`" sanctions the first and forbids the
|
||||
// second, and a rule that read the whole clause would lose both.
|
||||
re!(
|
||||
DESIGN_NEGATIVE_WORD,
|
||||
r"(?i)\b(?:no|not|never|nor|none|avoid\w*|don'?t|do not|doesn'?t|drop|deprecat\w*|obsolete|legacy|forbidden|banned|disallow\w*|discourag\w*|retired|remove\w*|stop|wrong|unsupported|anti-?pattern\w*)\b".to_string()
|
||||
DESIGN_DIRECTIVE_NEGATIVE,
|
||||
r"(?i)\b(?:no|not|never|nor|none|avoid\w*|don'?t|do not|doesn'?t|does not|drop|remove\w*|stop|skip)\b".to_string()
|
||||
);
|
||||
// A state: it describes whatever its clause is about, wherever in the clause
|
||||
// the name sits. "`.card-old` is deprecated" names the class first.
|
||||
re!(
|
||||
DESIGN_STATE_NEGATIVE,
|
||||
r"(?i)\b(?:deprecat\w*|obsolete|legacy|forbidden|banned|disallow\w*|discourag\w*|retired|unsupported|wrong|bad|anti-?pattern\w*|no longer|not allowed|not permitted|not supported|not used)\b".to_string()
|
||||
);
|
||||
// Headings that introduce a section of counter-examples.
|
||||
re!(
|
||||
DESIGN_NEGATIVE_HEADING,
|
||||
r"(?i)\b(?:don'?ts?|do not|avoid|never|not to|anti-?patterns?|deprecat\w*|forbidden|banned|legacy|obsolete|mistakes?|wrong|bad)\b".to_string()
|
||||
r"(?i)\b(?:don'?ts?|do not|avoid|never|not to|anti-?patterns?|deprecat\w*|forbidden|banned|legacy|obsolete|retired|unsupported|removed|discourag\w*|disallow\w*|mistakes?|wrong|bad)\b".to_string()
|
||||
);
|
||||
// A heading that names both sides — "Do and Don't", "Dos and Don'ts",
|
||||
// "Do / Do not" — introduces a section of both, so the subsections under it
|
||||
// say which is which and the heading itself condemns nothing.
|
||||
re!(
|
||||
DESIGN_BOTH_SIDES_HEADING,
|
||||
r"(?i)\bdos?\b[^\n]{0,12}?\b(?:do ?n[o']?ts?|do not)\b|\b(?:do ?n[o']?ts?|do not)\b[^\n]{0,12}?\bdos?\b".to_string()
|
||||
);
|
||||
re!(
|
||||
FONT_SIZE_LITERAL_RE,
|
||||
@@ -281,15 +298,23 @@ fn under_negative_heading(masked: &str, start: usize) -> bool {
|
||||
chain.retain(|(l, _)| *l < level);
|
||||
chain.push((level, text));
|
||||
}
|
||||
chain
|
||||
.iter()
|
||||
.any(|(_, text)| DESIGN_NEGATIVE_HEADING.is_match(text))
|
||||
chain.iter().any(|(_, text)| {
|
||||
// "Dos and Don'ts" heads a section of both, and the subsections
|
||||
// under it are what say which is which.
|
||||
DESIGN_NEGATIVE_HEADING.is_match(text) && !DESIGN_BOTH_SIDES_HEADING.is_match(text)
|
||||
})
|
||||
}
|
||||
|
||||
/// The clause the span sits in — from the boundary before it to the boundary
|
||||
/// after it — and whether that clause condemns what it names. A clause opened
|
||||
/// by "instead of" or "rather than" is condemned by the boundary itself,
|
||||
/// whatever words follow.
|
||||
/// after it — and whether that clause condemns what it names.
|
||||
///
|
||||
/// Where the negative word sits decides what it governs. A *state* ("`.x` is
|
||||
/// deprecated") describes whatever the clause is about, so it condemns the
|
||||
/// class wherever in the clause the name appears. A *directive* ("never use
|
||||
/// `.x`") condemns what follows it and nothing before it, which is what keeps
|
||||
/// "Use `.kicker` and never `.tagline`" from losing `.kicker`. A clause
|
||||
/// opened by "instead of" or "rather than" is condemned by the boundary
|
||||
/// itself, whatever words follow.
|
||||
fn clause_condemns(masked: &str, start: usize, end: usize) -> bool {
|
||||
let mut clause_start = 0usize;
|
||||
let mut opened_by_negation = false;
|
||||
@@ -304,7 +329,8 @@ fn clause_condemns(masked: &str, start: usize, end: usize) -> bool {
|
||||
.find(&masked[end..])
|
||||
.map(|m| end + m.start())
|
||||
.unwrap_or(masked.len());
|
||||
DESIGN_NEGATIVE_WORD.is_match(&masked[clause_start..clause_end])
|
||||
DESIGN_STATE_NEGATIVE.is_match(&masked[clause_start..clause_end])
|
||||
|| DESIGN_DIRECTIVE_NEGATIVE.is_match(&masked[clause_start..start])
|
||||
}
|
||||
|
||||
pub fn parse_frontmatter(md: &str) -> Option<Map<String, Value>> {
|
||||
@@ -2117,6 +2143,24 @@ mod tests {
|
||||
let md = "- Buttons: `.btn-primary`, `.btn-old`.\n- `.btn-old` is deprecated.\n";
|
||||
assert_eq!(declared_component_selectors(md), vec![".btn-primary"]);
|
||||
|
||||
// A directive governs what follows it, not the whole clause: a
|
||||
// sentence that sanctions one class and forbids another says both.
|
||||
let md = "- Use `.kicker` and never `.tagline`.\n\
|
||||
- Every label is a `.kicker`, not a `.tagline`.\n";
|
||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
||||
|
||||
// Headings that retire a set, in the words a document uses for it.
|
||||
let md = "## Retired components\n\n- `.tagline`\n\n\
|
||||
## Unsupported patterns\n\n- `.marquee-row`\n\n\
|
||||
## Components\n\n- `.kicker`\n";
|
||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
||||
|
||||
// A heading that names both sides heads a section of both, and its
|
||||
// subsections are what say which is which.
|
||||
let md = "## Dos and Don'ts\n\n### Do\n\n- Label a section with `.kicker`.\n\n\
|
||||
### Don't\n\n- Reach for `.eyebrow`.\n";
|
||||
assert_eq!(declared_component_selectors(md), vec![".kicker"]);
|
||||
|
||||
// What the negative words govern is their own clause. "No ALL CAPS
|
||||
// outside the `.eyebrow` class" declares `.eyebrow`, and "four kinds
|
||||
// and no more" declares all four.
|
||||
@@ -2463,3 +2507,4 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -193,13 +193,24 @@ pub trait Dom {
|
||||
}
|
||||
|
||||
/// The rects of one element's rendered text, merged into the lines they
|
||||
/// rendered on: rects whose vertical band overlaps the band the row started
|
||||
/// with belong to that row, and a row is the union of its fragments.
|
||||
/// rendered on.
|
||||
///
|
||||
/// Sorting is by top then left, so a row's fragments arrive together, and
|
||||
/// membership is tested against the *first* rect of the row rather than the
|
||||
/// row as it grows — an inline-block taller than the leading would otherwise
|
||||
/// swallow the line beneath it.
|
||||
/// Two rects are the same line when they share a row *and* run on from each
|
||||
/// other. Sharing a row is a vertical band overlapping the band the row
|
||||
/// started with by more than half the shorter height — that is what makes an
|
||||
/// inline `<strong>`, a superscript and the text around them one line.
|
||||
/// Running on is a horizontal gap no wider than the row's own line box: the
|
||||
/// fragments of a wrapped line are contiguous, while two columns of text that
|
||||
/// happen to sit on the same rows are separated by a gutter, and unioning
|
||||
/// those would invent a page-wide line neither column ever rendered. An
|
||||
/// inline image wider than the leading splits its line in two by the same
|
||||
/// test, which understates a line rather than overstating it — the direction
|
||||
/// this rule should err in.
|
||||
///
|
||||
/// Rects arrive in whatever order a DOM walked the text (an element's own
|
||||
/// text and its descendants' are interleaved on the page but not in the
|
||||
/// walk), so they are sorted top then left first and each rect joins the
|
||||
/// newest row still level with it.
|
||||
pub fn merge_text_rects_into_lines(rects: Vec<Rect>) -> Vec<Rect> {
|
||||
let mut rects: Vec<Rect> = rects
|
||||
.into_iter()
|
||||
@@ -212,23 +223,41 @@ pub fn merge_text_rects_into_lines(rects: Vec<Rect>) -> Vec<Rect> {
|
||||
.then(a.left.partial_cmp(&b.left).unwrap_or(std::cmp::Ordering::Equal))
|
||||
});
|
||||
let mut lines: Vec<Rect> = Vec::new();
|
||||
// The band of the rect each row started with.
|
||||
// The band of the rect each row started with. Membership is tested
|
||||
// against that rather than against the row as it grows, so an
|
||||
// inline-block taller than the leading does not swallow the line beneath.
|
||||
let mut bands: Vec<(f64, f64)> = Vec::new();
|
||||
for r in rects {
|
||||
if let (Some(line), Some(&(band_top, band_bottom))) = (lines.last_mut(), bands.last()) {
|
||||
let mut joined = false;
|
||||
for i in (0..lines.len()).rev() {
|
||||
let (band_top, band_bottom) = bands[i];
|
||||
// Sorted by top: once a row sits entirely above this rect, every
|
||||
// row before it does too.
|
||||
if band_bottom <= r.top {
|
||||
break;
|
||||
}
|
||||
let overlap = band_bottom.min(r.bottom) - band_top.max(r.top);
|
||||
let shorter = (band_bottom - band_top).min(r.height);
|
||||
if shorter > 0.0 && overlap > shorter / 2.0 {
|
||||
let left = line.left.min(r.left);
|
||||
let top = line.top.min(r.top);
|
||||
let right = line.right.max(r.right);
|
||||
let bottom = line.bottom.max(r.bottom);
|
||||
*line = Rect::from_xywh(left, top, right - left, bottom - top);
|
||||
if shorter <= 0.0 || overlap <= shorter / 2.0 {
|
||||
continue;
|
||||
}
|
||||
let line = lines[i];
|
||||
let gap = (r.left - line.right).max(line.left - r.right);
|
||||
if gap > band_bottom - band_top {
|
||||
continue;
|
||||
}
|
||||
let left = line.left.min(r.left);
|
||||
let top = line.top.min(r.top);
|
||||
let right = line.right.max(r.right);
|
||||
let bottom = line.bottom.max(r.bottom);
|
||||
lines[i] = Rect::from_xywh(left, top, right - left, bottom - top);
|
||||
joined = true;
|
||||
break;
|
||||
}
|
||||
if !joined {
|
||||
bands.push((r.top, r.bottom));
|
||||
lines.push(r);
|
||||
}
|
||||
bands.push((r.top, r.bottom));
|
||||
lines.push(r);
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
@@ -273,12 +273,14 @@ pub struct SnapNode {
|
||||
/// `getDirectTextRect` as `[x, y, width, height]`.
|
||||
#[serde(rename = "d", default)]
|
||||
pub direct_text_rect: Option<[f64; 4]>,
|
||||
/// The client rects of the element's rendered text, descendants included,
|
||||
/// each `[x, y, width, height]`. What a live probe reads on demand, read
|
||||
/// once at capture; the fragments are merged into lines on this side, so
|
||||
/// what travels is the raw list. Empty is "no rendered text", not "not
|
||||
/// recorded" — `Snapshot::text_lines` is what says a capture recorded
|
||||
/// them at all.
|
||||
/// The client rects of the element's OWN direct text, unmerged, each
|
||||
/// `[x, y, width, height]` — `direct_text_rect` before it was merged into
|
||||
/// a union. Own and not the subtree's: an element's rendered lines are
|
||||
/// assembled from these across its descendants
|
||||
/// ([`SnapshotDom::text_line_rects`]), so a line that rendered is
|
||||
/// recorded once rather than once per ancestor. Empty is "no rendered
|
||||
/// text", not "not recorded" — [`Snapshot::text_lines`] is what says a
|
||||
/// capture recorded them at all.
|
||||
#[serde(rename = "dl", default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub text_rects: Vec<[f64; 4]>,
|
||||
#[serde(rename = "e", default)]
|
||||
@@ -925,6 +927,10 @@ impl Dom for SnapshotDom {
|
||||
fn direct_text_rect(&self, el: ElId) -> Option<Rect> {
|
||||
self.snap.node(el).direct_text_rect.as_ref().map(rect4)
|
||||
}
|
||||
/// Assembled from the per-element rects the capture recorded: the
|
||||
/// element's own, then each descendant's, in document order, merged into
|
||||
/// the lines they rendered as.
|
||||
///
|
||||
/// `None` on a capture that never recorded them. The union in `d` is not
|
||||
/// an answer here: a paragraph with one long line and a short tail has
|
||||
/// the same union as one with two even lines, so dividing it by a line
|
||||
@@ -933,9 +939,16 @@ impl Dom for SnapshotDom {
|
||||
if !self.snap.text_lines {
|
||||
return None;
|
||||
}
|
||||
Some(super::dom::merge_text_rects_into_lines(
|
||||
self.snap.node(el).text_rects.iter().map(rect4).collect(),
|
||||
))
|
||||
fn walk(snap: &Snapshot, el: ElId, out: &mut Vec<Rect>) {
|
||||
let node = snap.node(el);
|
||||
out.extend(node.text_rects.iter().map(rect4));
|
||||
for child in &node.children {
|
||||
walk(snap, *child, out);
|
||||
}
|
||||
}
|
||||
let mut rects = Vec::new();
|
||||
walk(&self.snap, el, &mut rects);
|
||||
Some(super::dom::merge_text_rects_into_lines(rects))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1128,23 +1141,29 @@ mod tests {
|
||||
}"#;
|
||||
assert_eq!(snap(OLD).text_line_rects(3), None);
|
||||
|
||||
// Each element records only its own text rects; an element's lines
|
||||
// are assembled from its own plus its descendants'. Here the `<p>`
|
||||
// wraps once and an inline `<b>` sits in the middle of its first
|
||||
// line, so the first line arrives in three pieces from two elements.
|
||||
const NEW: &str = r#"{
|
||||
"v": 1, "textLines": true, "documentElement": 1, "body": 2,
|
||||
"els": [
|
||||
{"t":"HTML","c":[2]},
|
||||
{"t":"BODY","p":1,"c":[3]},
|
||||
{"t":"P","p":2,"c":["hello"],"d":[0,100,1000,43],
|
||||
"dl":[[0,100,600,19],[600,100,400,19],[0,124,120,19]]}
|
||||
{"t":"P","p":2,"c":["hello ",4," there"],"d":[0,100,1000,43],
|
||||
"dl":[[0,100,600,19],[700,100,300,19],[0,124,120,19]]},
|
||||
{"t":"B","p":3,"c":["bold"],"d":[600,100,100,19],"dl":[[600,100,100,19]]}
|
||||
]
|
||||
}"#;
|
||||
let lines = snap(NEW).text_line_rects(3).expect("lines");
|
||||
// The two fragments of the first line are the one line they rendered
|
||||
// as; the tail is its own.
|
||||
assert_eq!(lines.len(), 2);
|
||||
assert_eq!((lines[0].left, lines[0].width), (0.0, 1000.0));
|
||||
assert_eq!((lines[1].top, lines[1].width), (124.0, 120.0));
|
||||
// An element the capture found no rendered text on is not "unknown".
|
||||
assert_eq!(snap(NEW).text_line_rects(2), Some(Vec::new()));
|
||||
// The `<b>` on its own is the one line it rendered.
|
||||
assert_eq!(snap(NEW).text_line_rects(4).expect("lines").len(), 1);
|
||||
// An element the capture found no rendered text under is not
|
||||
// "unknown" — it is an element with no lines.
|
||||
assert_eq!(snap(NEW).text_line_rects(1).map(|l| l.len()), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user