Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2,462 changes: 1,713 additions & 749 deletions examples/music-player/dragon/north-star-check.json

Large diffs are not rendered by default.

24 changes: 19 additions & 5 deletions packages/dragon/src/analysis/computed-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import type { GenBFaults } from '../faults/gen-b.ts';
import { GEN_B_FAULTS } from '../faults/gen-b.ts';
import type { Diagnostic } from '../types.ts';
import type { UaDataset } from '../ua/datasets.ts';
import { uaRows } from '../ua/datasets.ts';
import { textFontsOf, uaRows } from '../ua/datasets.ts';
import { checkInlineLevel } from './blockify.ts';
import { uaTagOf } from './elements.ts';
import { UNSTYLED_TAGS, uaTagOf } from './elements.ts';
import { isReplacedTag } from './elements/replaced.ts';
import type { ResolvedElement, ResolvedText, ResolvedValue } from './resolve.ts';
import { PAINT_VALUES } from './paint-values/index.ts';
Expand Down Expand Up @@ -359,7 +359,9 @@ function listMarkerOf(el: ResolvedElement): string | null {

// css-cascade-5 §6.3: Chrome's UA defaults that the captured tables do not model. A tag inside an ancestor a UA rule keys on
// (nested lists), display: list-item with a marker (its ::marker box), UA border styles without a proof (hr's inset), a UA font size Chrome's
// minimum logical font size clamps, and text that inherits a UA font-weight or font-style no longhand models (headings, address).
// minimum logical font size clamps, text that inherits a UA font-weight or font-style no longhand models (headings, address, b,
// strong, em, i), and a phrasing tag whose text-font row was captured under a parent at the initial text font set inside an
// ancestor whose UA row already sets that property.
function checkUserAgentDefaults(root: ResolvedElement, targets: readonly string[], ua: UaDataset, diagnostics: Diagnostic[], reported: Set<string>, realFaceAt: (address: string) => boolean, faults: GenBFaults): void {
const once = (id: string, push: () => void): void => {
if (reported.has(id)) return;
Expand All @@ -385,6 +387,18 @@ function checkUserAgentDefaults(root: ResolvedElement, targets: readonly string[
manual: `Use a div in place of <${tag}> ${el.element.address}, or move it out of <${ancestor.element.tag}> ${ancestor.element.address}.`,
})));
}
const own = textFontsOf(ua, tag);
if (UNSTYLED_TAGS.has(tag)) {
for (const [p, v] of Object.entries(own)) {
const setter = [...ancestors].reverse().find((a) => textFontsOf(ua, a.element.tag)[p] !== undefined);
if (setter === undefined) continue;
once(`ua-text-font|${el.element.address}|${p}`, () => diagnostics.push(diagnostic('DRAGON_UNSUPPORTED_ELEMENT', {
origin: el.element.node.origin,
message: `<${tag}> ${el.element.address} inside <${setter.element.tag}> ${setter.element.address}: Chrome's captured ${p}: ${v} for <${tag}> holds under a parent at the initial ${p}, and <${setter.element.tag}> ${setter.element.address} sets ${p}: ${textFontsOf(ua, setter.element.tag)[p]} from Chrome's user-agent stylesheet`,
manual: `Use a span in place of <${tag}> ${el.element.address}, or move it out of <${setter.element.tag}> ${setter.element.address}.`,
})));
}
}
if (!here) {
const display = keywordOf(el.props.get('display') as ResolvedValue);
if (display === 'list-item') {
Expand All @@ -405,9 +419,9 @@ function checkUserAgentDefaults(root: ResolvedElement, targets: readonly string[
perTarget(el, 'font-size', `font-size: ${valueToString(size.value)} on <${tag}> ${el.element.address} comes from Chrome's user-agent stylesheet and is below Chrome's minimum logical font size (${ua.minimumLogicalFontSize}px), which Chrome clamps depending on the device pixel ratio`, `Set a px font-size on <${tag}> ${el.element.address} or one of its ancestors.`);
}
}
const fonts = Object.keys(uaRows(ua, uaTagOf(tag)).textFonts).length > 0 ? el : fontTag;
const fonts = Object.keys(own).length > 0 ? el : fontTag;
if (!here && fonts !== null) {
const row = uaRows(ua, uaTagOf(fonts.element.tag)).textFonts;
const row = textFontsOf(ua, fonts.element.tag);
const set = Object.entries(row).map(([p, v]) => `${p}: ${v}`).join('; ');
for (const c of el.children) {
// TXT1a-2: a real bundled face at the UA weight and style draws it (synthesis is refused as DRAGON_SYNTHETIC_FONT_STYLE).
Expand Down
7 changes: 4 additions & 3 deletions packages/dragon/src/analysis/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ export const SUPPORTED_TAGS: ReadonlySet<string> = new Set([
'span', 'a', 'label',
'br',
...REPLACED_TAGS,
'b', 'strong', 'em', 'i',
]);

/**
* Tags whose Chrome 145 UA rules set no longhand Dragon models (the elementKey and phrasingKey tables: span, a without href,
* label, whose only rule is cursor: default, and br, which has none). They take dragon-unstyled's captured defaults;
* test/blockify.test.ts pins the tables.
* label, whose only rule is cursor: default, br, which has none, and b, strong, em and i, whose rules set only a text font). They
* take dragon-unstyled's captured defaults, and their text font row (datasets.ts); test/blockify.test.ts pins the tables.
*/
export const UNSTYLED_TAGS: ReadonlySet<string> = new Set(['span', 'a', 'label', 'br']);
export const UNSTYLED_TAGS: ReadonlySet<string> = new Set(['span', 'a', 'label', 'br', 'b', 'strong', 'em', 'i']);

/** The UA dataset row an element's tag reads. */
export function uaTagOf(tag: string): UaKey {
Expand Down
25 changes: 14 additions & 11 deletions packages/dragon/src/profiles/web.ts

Large diffs are not rendered by default.

25 changes: 21 additions & 4 deletions packages/dragon/src/ua/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as darwinArm64Dark from './chrome-145.darwin-arm64.dark.generated.ts';
import * as darwinArm64 from './chrome-145.darwin-arm64.generated.ts';
import type { CapturedTag, ReplacedKey } from './chrome-145.darwin-arm64.generated.ts';
import { UNSTYLED_TAGS } from '../analysis/elements.ts';

export type { CapturedTag, ReplacedKey } from './chrome-145.darwin-arm64.generated.ts';

Expand All @@ -18,8 +19,11 @@ export type UaDataset = {
readonly userAgentDeclared: { readonly [T in CapturedTag]: { readonly ltr: { readonly [property: string]: string }; readonly rtl: { readonly [property: string]: string } } };
/** Ancestor tags under which a Chrome UA rule gives the tag values userAgentDeclared does not model. */
readonly userAgentContexts: { readonly [T in CapturedTag]: readonly string[] };
/** Inherited font properties a UA rule sets per tag that no longhand models (font-weight, font-style). */
readonly userAgentTextFonts: { readonly [T in CapturedTag]: { readonly [property: string]: string } };
/**
* Inherited font properties a UA rule sets per tag that no longhand models (font-weight, font-style); the phrasing tags that read
* dragon-unstyled (b, strong, em, i) carry their phrasingKeyTextFonts row, captured under a parent at the initial text font.
*/
readonly userAgentTextFonts: { readonly [T in CapturedTag]: TextFontRow } & { readonly [K in PhrasingKey]?: TextFontRow };
/** Chrome's minimum logical font size in px, which clamps an em font size under the keyword-sized root. */
readonly minimumLogicalFontSize: number;
/** REPL-0: the replaced keys (iframe, img with a src), in tables of their own; uaRows reads them. */
Expand All @@ -31,6 +35,8 @@ export type UaDataset = {
readonly replacedKeyForced: { readonly [K in ReplacedKey]: DirRows };
};

type TextFontRow = { readonly [property: string]: string };

/** A row key of the UA dataset: a captured tag, or a replaced key (REPL-0). */
export type UaKey = CapturedTag | ReplacedKey;

Expand Down Expand Up @@ -61,10 +67,21 @@ export function uaRows(ua: UaDataset, key: UaKey): UaRows {
/** The platform the committed Chrome references and the UA dataset were captured on. */
export const REFERENCE_PLATFORM = 'darwin-arm64';

const DATASETS: ReadonlyMap<string, UaDataset> = new Map([[darwinArm64.platform, darwinArm64]]);
/** A captured dataset whose text-font table also holds the phrasingKeyTextFonts row of every phrasing tag that reads dragon-unstyled. */
function withPhrasingTextFonts(ds: typeof darwinArm64 | typeof darwinArm64Dark): UaDataset {
const rows = Object.entries(ds.phrasingKeyTextFonts).filter(([tag]) => UNSTYLED_TAGS.has(tag));
return { ...ds, userAgentTextFonts: { ...ds.userAgentTextFonts, ...Object.fromEntries(rows) } };
}

const DATASETS: ReadonlyMap<string, UaDataset> = new Map([[darwinArm64.platform, withPhrasingTextFonts(darwinArm64)]]);

/** The same capture under html{color-scheme:dark}, keyed by the same platforms. */
const DARK_DATASETS: ReadonlyMap<string, UaDataset> = new Map([[darwinArm64Dark.platform, darwinArm64Dark]]);
const DARK_DATASETS: ReadonlyMap<string, UaDataset> = new Map([[darwinArm64Dark.platform, withPhrasingTextFonts(darwinArm64Dark)]]);

/** The text font a UA rule gives an element's tag (userAgentTextFonts); empty for a tag no such rule names. */
export function textFontsOf(ua: UaDataset, tag: string): TextFontRow {
return (ua.userAgentTextFonts as { readonly [tag: string]: TextFontRow | undefined })[tag] ?? {};
}

export type UaDatasetChoice =
| { readonly kind: 'ok'; readonly dataset: UaDataset }
Expand Down
33 changes: 31 additions & 2 deletions packages/dragon/test/blockify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { ResolvedElement } from '../src/analysis/resolve.ts';
import { resolveTree } from '../src/analysis/resolve.ts';
import { parseStylesheet } from '../src/css/stylesheet.ts';
import type { Diagnostic } from '../src/types.ts';
import { referenceDataset } from '../src/ua/datasets.ts';
import { darkDatasetFor, referenceDataset, textFontsOf, uaDatasetFor } from '../src/ua/datasets.ts';
import { DOC, inputFor, staticClass, text } from './helpers.ts';

const SRC: SourceRef = { uri: 's.css', revision: 'r', hash: 'h' };
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('span, a and label in the element table', () => {
expect(uaTagOf(t)).toBe('dragon-unstyled');
}
expect(uaTagOf('div')).toBe('div');
expect([...UNSTYLED_TAGS].sort()).toEqual(['a', 'br', 'label', 'span']);
expect([...UNSTYLED_TAGS].sort()).toEqual(['a', 'b', 'br', 'em', 'i', 'label', 'span', 'strong']);
});
// The captured Chrome tables are what makes dragon-unstyled right for them: no modelled UA declaration, context or text font,
// and every computed longhand equal to dragon-unstyled's, in light and dark.
Expand Down Expand Up @@ -114,6 +114,35 @@ describe('br in the element table (INL1a)', () => {
}
});

describe('b, strong, em and i in the element table (T133, R7)', () => {
it('are supported, appended last (after br and the replaced tags), and read dragon-unstyled\'s UA row', () => {
expect([...SUPPORTED_TAGS].slice(-4)).toEqual(['b', 'strong', 'em', 'i']);
for (const t of ['b', 'strong', 'em', 'i']) expect(uaTagOf(t), t).toBe('dragon-unstyled');
});
for (const [scheme, ds, choice] of [['light', light, uaDatasetFor('darwin-arm64')], ['dark', dark, darkDatasetFor('darwin-arm64')]] as const) {
it(`${scheme}: their only UA rule is a text font, every computed longhand equals dragon-unstyled's, and the dataset carries the row`, () => {
if (choice.kind !== 'ok') throw new Error(choice.reason);
for (const k of ['b', 'strong', 'em', 'i'] as const) {
expect(ds.phrasingKeySpecs[k].attributes, k).toEqual({});
expect(ds.phrasingKeyLonghands[k], k).toEqual([]);
expect(ds.phrasingKeyDeclared[k], k).toEqual({ ltr: {}, rtl: {} });
expect(ds.phrasingKeyContexts[k], k).toEqual([]);
expect(ds.phrasingKeyUnmodelled[k], k).toEqual({ ltr: {}, rtl: {} });
expect(ds.phrasingKeyForced[k], k).toEqual({ ltr: {}, rtl: {} });
for (const p of LONGHANDS) expect(ds.phrasingKeyComputed[k][p], `${k} ${p}`).toBe(ds.computed['dragon-unstyled'][p]);
expect(textFontsOf(choice.dataset, k), k).toBe(ds.phrasingKeyTextFonts[k]);
}
expect(textFontsOf(choice.dataset, 'b')).toEqual({ 'font-weight': '700' });
expect(textFontsOf(choice.dataset, 'em')).toEqual({ 'font-style': 'italic' });
// Only the supported phrasing tags gain a row; the captured tags keep theirs, and every other tag has none.
for (const k of ['br', 'label', 'code', 'small', 'sub', 'sup']) expect(k in choice.dataset.userAgentTextFonts, k).toBe(k === 'br' || k === 'label');
expect(textFontsOf(choice.dataset, 'h1')).toBe(ds.userAgentTextFonts.h1);
expect(textFontsOf(choice.dataset, 'span')).toEqual({});
expect(choice.dataset.computed).toBe(ds.computed);
});
}
});

describe('blockifiedDisplay: Chrome 145.0.7632.6 on a flex item and an absolutely positioned span (notes/T057)', () => {
const at = (text: string): string | null => {
const v: ResolvedValue = { value: parseValueText('display', text), origin: 'author', span: null, declaration: null, declared: null, losing: [] };
Expand Down
10 changes: 8 additions & 2 deletions packages/dragon/test/inline-lowering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,16 @@ describe('refusals', () => {
const wrap = compile((r) => [el(r, 'p', 'div', [], [text(r, 't0', 'a '), el(r, 's', 'span', ['nw'], [text(r, 't1', 'b c')])])]).diagnostics.filter((d) => d.message.startsWith('text-wrap-mode'));
expect(wrap.map((d) => String(d.target)).sort()).toEqual(['ios', 'web']);
});
it('the phrasing tags INL1a does not reproduce stay refused: b, strong, em, i (synthetic bold and italic), small, code, sub and sup', () => {
for (const tag of ['b', 'strong', 'em', 'i', 'small', 'code', 'sub', 'sup']) {
it('the phrasing tags INL1a does not reproduce stay refused: small, code, sub and sup', () => {
for (const tag of ['small', 'code', 'sub', 'sup']) {
const got = compile((r) => [el(r, 'p', 'div', [], [text(r, 't0', 'a '), el(r, 's', tag, [], [text(r, 't1', 'b')])])]).diagnostics.filter((d) => d.code === 'DRAGON_UNSUPPORTED_ELEMENT');
expect(got.length, tag).toBeGreaterThan(0);
}
});
it('b, strong, em and i are elements since T133; in Ahem only ios refuses them, for the UA font-weight or font-style (synthetic bold and italic)', () => {
for (const tag of ['b', 'strong', 'em', 'i']) {
const got = compile((r) => [el(r, 'p', 'div', [], [text(r, 't0', 'a '), el(r, 's', tag, [], [text(r, 't1', 'b')])])]).diagnostics;
expect(got.map((d) => `${d.code} ${String(d.target)}`), tag).toEqual(['DRAGON_UNSUPPORTED_FONT ios']);
}
});
});
21 changes: 21 additions & 0 deletions packages/layout/break-vectors/dpr-2.625/inline-tags-faces-rtl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"case": "inline-tags-faces-rtl",
"dpr": 2.625,
"texts": [
{"id":"r1:text0","lines":[[0,6]]},
{"id":"b:text0","lines":[[0,4]]},
{"id":"r2:text0","lines":[[0,5]]},
{"id":"strong:text0","lines":[[0,13],[13,35]]},
{"id":"r3:text0","lines":[[0,6]]},
{"id":"em:text0","lines":[[0,8]]},
{"id":"r4:text0","lines":[[0,5]]},
{"id":"i:text0","lines":[[0,10]]},
{"id":"bi:text0","lines":[[0,11]]},
{"id":"r5:text0","lines":[[0,10]]},
{"id":"r6:text0","lines":[[0,5]]},
{"id":"lb:text0","lines":[[0,4]]},
{"id":"r7:text0","lines":[[0,5]]},
{"id":"ls:text0","lines":[[0,11]]},
{"id":"he:text0","lines":[[0,16]]}
]
}
21 changes: 21 additions & 0 deletions packages/layout/break-vectors/dpr-2.625/inline-tags-faces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"case": "inline-tags-faces",
"dpr": 2.625,
"texts": [
{"id":"r1:text0","lines":[[0,6]]},
{"id":"b:text0","lines":[[0,4]]},
{"id":"r2:text0","lines":[[0,5]]},
{"id":"strong:text0","lines":[[0,13],[13,35]]},
{"id":"r3:text0","lines":[[0,6]]},
{"id":"em:text0","lines":[[0,8]]},
{"id":"r4:text0","lines":[[0,5]]},
{"id":"i:text0","lines":[[0,10]]},
{"id":"bi:text0","lines":[[0,11]]},
{"id":"r5:text0","lines":[[0,10]]},
{"id":"r6:text0","lines":[[0,5]]},
{"id":"lb:text0","lines":[[0,4]]},
{"id":"r7:text0","lines":[[0,5]]},
{"id":"ls:text0","lines":[[0,11]]},
{"id":"he:text0","lines":[[0,16]]}
]
}
21 changes: 21 additions & 0 deletions packages/layout/break-vectors/dpr-2/inline-tags-faces-rtl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"case": "inline-tags-faces-rtl",
"dpr": 2,
"texts": [
{"id":"r1:text0","lines":[[0,6]]},
{"id":"b:text0","lines":[[0,4]]},
{"id":"r2:text0","lines":[[0,5]]},
{"id":"strong:text0","lines":[[0,13],[13,35]]},
{"id":"r3:text0","lines":[[0,6]]},
{"id":"em:text0","lines":[[0,8]]},
{"id":"r4:text0","lines":[[0,5]]},
{"id":"i:text0","lines":[[0,10]]},
{"id":"bi:text0","lines":[[0,11]]},
{"id":"r5:text0","lines":[[0,10]]},
{"id":"r6:text0","lines":[[0,5]]},
{"id":"lb:text0","lines":[[0,4]]},
{"id":"r7:text0","lines":[[0,5]]},
{"id":"ls:text0","lines":[[0,11]]},
{"id":"he:text0","lines":[[0,16]]}
]
}
21 changes: 21 additions & 0 deletions packages/layout/break-vectors/dpr-2/inline-tags-faces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"case": "inline-tags-faces",
"dpr": 2,
"texts": [
{"id":"r1:text0","lines":[[0,6]]},
{"id":"b:text0","lines":[[0,4]]},
{"id":"r2:text0","lines":[[0,5]]},
{"id":"strong:text0","lines":[[0,13],[13,35]]},
{"id":"r3:text0","lines":[[0,6]]},
{"id":"em:text0","lines":[[0,8]]},
{"id":"r4:text0","lines":[[0,5]]},
{"id":"i:text0","lines":[[0,10]]},
{"id":"bi:text0","lines":[[0,11]]},
{"id":"r5:text0","lines":[[0,10]]},
{"id":"r6:text0","lines":[[0,5]]},
{"id":"lb:text0","lines":[[0,4]]},
{"id":"r7:text0","lines":[[0,5]]},
{"id":"ls:text0","lines":[[0,11]]},
{"id":"he:text0","lines":[[0,16]]}
]
}
21 changes: 21 additions & 0 deletions packages/layout/break-vectors/dpr-3/inline-tags-faces-rtl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"case": "inline-tags-faces-rtl",
"dpr": 3,
"texts": [
{"id":"r1:text0","lines":[[0,6]]},
{"id":"b:text0","lines":[[0,4]]},
{"id":"r2:text0","lines":[[0,5]]},
{"id":"strong:text0","lines":[[0,13],[13,35]]},
{"id":"r3:text0","lines":[[0,6]]},
{"id":"em:text0","lines":[[0,8]]},
{"id":"r4:text0","lines":[[0,5]]},
{"id":"i:text0","lines":[[0,10]]},
{"id":"bi:text0","lines":[[0,11]]},
{"id":"r5:text0","lines":[[0,10]]},
{"id":"r6:text0","lines":[[0,5]]},
{"id":"lb:text0","lines":[[0,4]]},
{"id":"r7:text0","lines":[[0,5]]},
{"id":"ls:text0","lines":[[0,11]]},
{"id":"he:text0","lines":[[0,16]]}
]
}
21 changes: 21 additions & 0 deletions packages/layout/break-vectors/dpr-3/inline-tags-faces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"case": "inline-tags-faces",
"dpr": 3,
"texts": [
{"id":"r1:text0","lines":[[0,6]]},
{"id":"b:text0","lines":[[0,4]]},
{"id":"r2:text0","lines":[[0,5]]},
{"id":"strong:text0","lines":[[0,13],[13,35]]},
{"id":"r3:text0","lines":[[0,6]]},
{"id":"em:text0","lines":[[0,8]]},
{"id":"r4:text0","lines":[[0,5]]},
{"id":"i:text0","lines":[[0,10]]},
{"id":"bi:text0","lines":[[0,11]]},
{"id":"r5:text0","lines":[[0,10]]},
{"id":"r6:text0","lines":[[0,5]]},
{"id":"lb:text0","lines":[[0,4]]},
{"id":"r7:text0","lines":[[0,5]]},
{"id":"ls:text0","lines":[[0,11]]},
{"id":"he:text0","lines":[[0,16]]}
]
}
4 changes: 2 additions & 2 deletions packages/layout/test/inline-box-placelines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ describe('placeLines is exactly placeIfcLines(buildIfc(...)) (T058J2 (F))', () =
n += compare(label, v.input as LayoutInput, NO_ENGINE_FAULTS, measurer);
}
expect(n).toBeGreaterThan(1000);
// The text-latin vectors (vectors/text-latin/dpr-<d>, TXT1a-2's 11 shaped cases at DPR 1, 2, 3 and 2.625) are the shaped ones.
// The text-latin vectors (vectors/text-latin/dpr-<d>, TXT1a-2's 11 shaped cases and INL1a-tags' 2 at DPR 1, 2, 3 and 2.625) are the shaped ones.
expect(shaped).toBe(files.filter((f) => f.slice(VECTORS.length).startsWith('text-latin/')).length);
expect(shaped).toBe(44);
expect(shaped).toBe((11 + 2) * 4);
});
});
Loading
Loading