Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a5e01c1
TXT1a-1 compiler and parity (T083J phase B) restacked onto txt1a-1b-v…
thejackshelton Oct 2, 2026
73bb06f
regen.ts MANUAL: the text-latin captures and vectors are written by c…
thejackshelton Oct 2, 2026
767f593
text-latin.test.ts: BASE is now INL1a part C2 (afec4d619), whose FIXT…
thejackshelton Oct 2, 2026
647fa6a
Regenerate outputs for txt1a-1-v2: node --conditions=dragon-internal …
thejackshelton Oct 2, 2026
16a698a
Merge txt1a-1b-v2 (inl1a-lowering with seld-lanes-v2; review of #91) …
thejackshelton Oct 4, 2026
a60030e
text-latin.test.ts: BASE is now inl1a-lowering with seld-lanes-v2 mer…
thejackshelton Oct 4, 2026
6cf1732
Merge branch 'txt1a-1b-v2' into txt1a-1-v2
thejackshelton Oct 4, 2026
bb5786d
Merge branch 'txt1a-1b-v2' into txt1a-1-v2
thejackshelton Oct 4, 2026
787c4ef
Merge branch 'txt1a-1b-v2' into txt1a-1-v2
thejackshelton Oct 4, 2026
7110fbf
Merge branch 'txt1a-1b-v2' into txt1a-1-v2
thejackshelton Oct 4, 2026
378017d
Regenerate outputs for txt1a-1-v2 after merging txt1a-1b-v2 forward (…
thejackshelton Oct 4, 2026
450f7d5
Regenerate outputs for txt1a-1-v2: node --conditions=dragon-internal …
thejackshelton Oct 4, 2026
e6c78f4
Merge txt1a-1b-v2 (local prep at 23bff72e8d, #91's queued head merged…
thejackshelton Oct 6, 2026
f13a76e
Regenerate outputs for txt1a-1-v2 after merging the local 1b prep: no…
thejackshelton Oct 6, 2026
b012c3b
txt1a-1-v2 on the new base: the text-latin registry file registers an…
thejackshelton Oct 6, 2026
d664803
Merge txt1a-1b-v2 at d542a45419 (#102's land-ready head, containing m…
claude Oct 9, 2026
6d14ed8
TXT1a-1 on #102's head: real-font paths after the merge, a regen step…
claude Oct 9, 2026
30137a1
Merge origin/master (a36d7450f2: #102 and #203 landed) into txt1a-1-v…
claude Oct 9, 2026
10ea5b1
TXT1a-1: the text-latin captures become a regen step (text-latin-capt…
claude Oct 9, 2026
b744a6f
Regenerate on CI: pnpm regen (regen-on-ci)
github-actions[bot] Oct 9, 2026
6bc0e63
Merge origin/master (b97cf50c62: #232 GEN-c, #233 CASC 3) into txt1a-…
claude Oct 9, 2026
4ff2514
Regenerate on CI: pnpm regen (regen-on-ci)
github-actions[bot] Oct 9, 2026
fafd726
TXT1a-1: text-latin.test's Swift/Kotlin vector replays move to text-l…
claude Oct 9, 2026
3a44e7c
Merge origin/master (03ba583dff: #102 TXT1a-1b, #231 OVFL-B, #239 nat…
claude Oct 10, 2026
93f54f0
Regenerate on CI: pnpm regen (regen-on-ci)
github-actions[bot] Oct 10, 2026
6f8d87d
text-latin.test.ts: BASE is master at 03ba583dff, whose FIXTURES hold…
claude Oct 10, 2026
3d86c23
Merge remote-tracking branch 'origin/master' into resolve-103
claude Oct 10, 2026
f23099e
text-latin-engine: refuse scrollRanges with the Ahem-only measurer, a…
claude Oct 10, 2026
42f45e2
Regenerate on CI: pnpm regen (regen-on-ci)
github-actions[bot] Oct 10, 2026
ed2d6a3
Merge remote-tracking branch 'origin/master' into land-tmp
claude Oct 10, 2026
1f47e11
Regenerate on CI: pnpm regen (regen-on-ci)
github-actions[bot] Oct 10, 2026
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 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ packages/layout/rt-vectors/animator/** merge=dragon-generated
packages/layout/rt-vectors/hit/** merge=dragon-generated
packages/layout/vectors/*.json merge=dragon-generated
packages/layout/vectors/dpr-*/** merge=dragon-generated
packages/layout/vectors/text-latin/** merge=dragon-generated
packages/parity/emitted/** merge=dragon-generated
packages/parity/expected-breaks/** merge=dragon-generated
packages/parity/expected-dpr/** merge=dragon-generated
Expand All @@ -30,6 +31,7 @@ packages/parity/expected-pixels/** merge=dragon-generated
packages/parity/expected-quads/** merge=dragon-generated
packages/parity/expected-resize/** merge=dragon-generated
packages/parity/expected-scroll/** merge=dragon-generated
packages/parity/expected-text-latin/** merge=dragon-generated
packages/parity/expected/darwin-arm64/** merge=dragon-generated
packages/parity/out/lanes.json merge=dragon-generated
packages/tailwind-sweep/snapshot/** merge=dragon-generated
Expand Down
53 changes: 7 additions & 46 deletions examples/music-player/dragon/north-star-check.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,15 @@
}
},
"summary": {
"diagnostics": 255,
"errors": 250,
"diagnostics": 253,
"errors": 248,
"byCode": {
"DRAGON_NOT_APPLICABLE_NATIVE [android]": 2,
"DRAGON_NOT_APPLICABLE_NATIVE [ios]": 2,
"DRAGON_SELECTOR_DROPPED": 1,
"DRAGON_UNPROVEN_CONTEXT [android]": 2,
"DRAGON_UNPROVEN_CONTEXT [ios]": 2,
"DRAGON_UNPROVEN_CONTEXT [web]": 4,
"DRAGON_UNPROVEN_CONTEXT [web]": 2,
"DRAGON_UNSUPPORTED_ATTRIBUTE": 18,
"DRAGON_UNSUPPORTED_ELEMENT": 8,
"DRAGON_UNSUPPORTED_FONT [android]": 34,
Expand All @@ -163,7 +163,7 @@
"perTarget": {
"web": {
"errors": {
"DRAGON_UNPROVEN_CONTEXT": 4,
"DRAGON_UNPROVEN_CONTEXT": 2,
"DRAGON_UNSUPPORTED_ATTRIBUTE": 18,
"DRAGON_UNSUPPORTED_ELEMENT": 8,
"DRAGON_UNSUPPORTED_PROPERTY": 22,
Expand Down Expand Up @@ -216,7 +216,7 @@
"notApplicableAndroid": 2,
"applicableDeclarations": 289,
"supportedBothTargets": 238,
"supportedWeb": 254,
"supportedWeb": 255,
"supportedIos": 238,
"supportedAndroid": 238,
"supportPercent": 82.4,
Expand Down Expand Up @@ -304,7 +304,7 @@
"rawDiagnosticsPerPass": {
"A-authored": 103,
"B-unwrapped": 103,
"C-context": 167
"C-context": 165
}
},
"features": {
Expand Down Expand Up @@ -684,11 +684,10 @@
"atRule": null,
"property": "font-family",
"value": "'Lato', sans-serif",
"web": "blocked",
"web": "supported",
"ios": "blocked",
"android": "blocked",
"codes": [
"DRAGON_UNPROVEN_CONTEXT",
"DRAGON_UNSUPPORTED_VALUE"
]
},
Expand Down Expand Up @@ -6034,44 +6033,6 @@
"B-unwrapped"
]
},
{
"key": "DRAGON_UNPROVEN_CONTEXT|web|css@594-612|font-family:<pinned> on page/nav-call/nav-title:text0 is used in the text-in-flex-item/row/ltr context, which is not proven (proven: text-in-block/ltr, text-in-block/rtl); font-family values proven in text-in-flex-item/row/ltr: Ahem",
"code": "DRAGON_UNPROVEN_CONTEXT",
"severity": "error",
"target": "web",
"location": {
"kind": "css",
"start": 594,
"end": 612,
"line": 32,
"column": 15,
"text": "'Lato', sans-serif"
},
"message": "font-family:<pinned> on page/nav-call/nav-title:text0 is used in the text-in-flex-item/row/ltr context, which is not proven (proven: text-in-block/ltr, text-in-block/rtl); font-family values proven in text-in-flex-item/row/ltr: Ahem",
"viaRelated": false,
"passes": [
"C-context"
]
},
{
"key": "DRAGON_UNPROVEN_CONTEXT|web|css@594-612|font-family:<pinned> on page/song-call/song-title:text0 is used in the text-in-flex-item/column/ltr context, which is not proven (proven: text-in-block/ltr, text-in-block/rtl); font-family values proven in text-in-flex-item/column/ltr: Ahem",
"code": "DRAGON_UNPROVEN_CONTEXT",
"severity": "error",
"target": "web",
"location": {
"kind": "css",
"start": 594,
"end": 612,
"line": 32,
"column": 15,
"text": "'Lato', sans-serif"
},
"message": "font-family:<pinned> on page/song-call/song-title:text0 is used in the text-in-flex-item/column/ltr context, which is not proven (proven: text-in-block/ltr, text-in-block/rtl); font-family values proven in text-in-flex-item/column/ltr: Ahem",
"viaRelated": false,
"passes": [
"C-context"
]
},
{
"key": "DRAGON_UNSUPPORTED_VALUE|android|css@594-612|font-family: \"Lato\" , sans-serif is unsupported (support profile m1-s5); in text-in-block/ltr use Ahem; in text-in-flex-item/column/ltr use Ahem; in text-in-flex-item/row/ltr use Ahem",
"code": "DRAGON_UNSUPPORTED_VALUE",
Expand Down
11 changes: 9 additions & 2 deletions packages/dragon/src/diagnostics/codes/txt1a.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
// TXT1a: Latin text in real fonts.
import { diagnosticFeature } from '../entry.ts';
import { diagnosticFeature, error, manual } from '../entry.ts';

export const TXT1A = diagnosticFeature([], {});
export const TXT1A = diagnosticFeature(
[
'DRAGON_SYNTHETIC_FONT_STYLE',
],
{
DRAGON_SYNTHETIC_FONT_STYLE: error('This text would be drawn in a synthetic bold or oblique style.', 'Chrome synthesizes the style with a Skia paint effect when no bundled face has the weight or slope; native glyph drawing cannot reproduce those pixels yet (notes/T056-txt1a-spec.md R7).', manual('Bundle the face of that style', 'Add a face with the weight or style the text uses to the font map or an @font-face rule.')),
},
);
26 changes: 26 additions & 0 deletions packages/dragon/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,32 @@ export { emitStatePrograms, STATE_RUNTIME_VERSION, StateEmitError, typedSetters,
export { CLOCK_RUNTIME_VERSION, ClockError, VirtualClock } from './emit/runtime/clock.ts';
export { RUNTIME_MODULES } from './emit/runtime/index.ts';

/**
* TXT1a-1 (notes/T083-txt1a-1.md): the engine lane's layout projection of one case. It is the native projection when a native
* target is checked, and an engine-mode lowering (bundled static faces) when only deferred native font refusals block native.
*/
export function engineLayoutProjection(compiled: object, environment: Environment, assignment: Assignment): LayoutProjection {
const c = caseOf(compiled, assignment);
if (typeof c === 'string') return { kind: 'blocked', reason: c };
const record = internalRecord(compiled) as NonNullable<ReturnType<typeof internalRecord>>;
if (record.direction !== environment.direction) return { kind: 'blocked', reason: `the result was resolved for direction ${record.direction}, not ${environment.direction}` };
if (record.rootFont !== environment.rootFont) return { kind: 'blocked', reason: `the result was resolved for root font ${record.rootFont}, not ${environment.rootFont}` };
if (c.engineLowered === null) return { kind: 'blocked', reason: `no engine lowering: ${c.engineRefusal ?? 'no native or engine lowering'}` };
if (c.resolved === null) return { kind: 'blocked', reason: 'the case did not resolve' };
const viewport = { width: environment.viewport.width, height: environment.viewport.height };
return {
kind: 'ready',
input: {
viewport,
devicePixelRatio: environment.devicePixelRatio,
viewportUnits: { small: viewport, large: viewport, dynamic: viewport },
safeArea: { top: 0, right: 0, bottom: 0, left: 0 },
rootFontSize: rootFontSizeOf(c.resolved),
root: c.engineLowered,
},
};
}

// SELD-R1b (notes/T047-runtime-spec.md RT-9): the hit table and each element's hit facts.
export type { HitFact } from './emit/runtime/hit.ts';
export { HIT_FACTS_VERSION } from './emit/runtime/hit.ts';
Expand Down
52 changes: 35 additions & 17 deletions packages/dragon/src/lower/ios-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,17 +318,37 @@ function gridStep<T>(id: string, step: () => T): T {

const AHEM_EXPECTED = 'Ahem (the milestone-1 layout font)';

/** The face id of the bundled Ahem, as the engine names it (layout text.ts AHEM_FACE_ID); other faces are sha256:<hex>. */
export const AHEM_FACE = 'Ahem';

/** The lowering's font refusal for a text node, or null: the analysis reports it for every case before any lowering (T005 rec 3). */
export function textFontProblem(t: ResolvedText): string | null {
const family = (t.props.get('font-family') as ResolvedValue).value;
if (family.kind === 'family' && family.value === 'Ahem') return null;
return `font-family: ${valueToString(family)} on ${t.node.address} has no layout mapping (expected ${AHEM_EXPECTED})`;
}

/**
* TXT1a-1 engine mode: the bundled face an element's or text node's font resolves to (the face id the engine and its hosts key faces
* by: sha256:<hex> of the bytes), or why the engine cannot lay it out (fallback, synthesis, a variable face).
*/
export type EngineFace = { readonly kind: 'face'; readonly id: string } | { readonly kind: 'refused'; readonly reason: string };

/** The lowering mode: native targets lay out Ahem only; the engine lane also lays out the bundled static faces it resolves. */
export type LowerMode = { readonly kind: 'native' } | { readonly kind: 'engine'; readonly faceOf: (address: string) => EngineFace };

/** The face family the engine reads: Ahem, or in engine mode the resolved bundled face; anything else is a lowering failure. */
function lowerFamily(id: string, family: CssValue, mode: LowerMode): string {
if (family.kind === 'family' && family.value === 'Ahem') return AHEM_FACE;
if (mode.kind === 'native') return fail(id, 'font-family', family, AHEM_EXPECTED);
const face = mode.faceOf(id);
if (face.kind === 'refused') throw new LoweringError(id, 'font-family', `font-family: ${valueToString(family)} on ${id}: ${face.reason}`);
return face.id;
}

/** A font and line-height as the engine reads them (input.ts FontSpec, LineHeightValue), from resolved font-size and line-height. */
function lowerFont(id: string, get: (p: TextLonghand) => CssValue): { readonly font: FontSpec; readonly lineHeight: LineHeightValue } {
const family = get('font-family');
if (family.kind !== 'family' || family.value !== 'Ahem') fail(id, 'font-family', family, AHEM_EXPECTED);
function lowerFont(id: string, get: (p: TextLonghand) => CssValue, mode: LowerMode): { readonly font: FontSpec; readonly lineHeight: LineHeightValue } {
const face = lowerFamily(id, get('font-family'), mode);
const fs = get('font-size');
if (fs.kind !== 'length' || fs.unit !== 'px') fail(id, 'font-size', fs, 'px');
const lh = get('line-height');
Expand All @@ -337,16 +357,14 @@ function lowerFont(id: string, get: (p: TextLonghand) => CssValue): { readonly f
else if (lh.kind === 'number') lineHeight = { kind: 'number', value: lh.value };
else if (lh.kind === 'length' && lh.unit === 'px') lineHeight = { kind: 'px', value: lh.value };
else return fail(id, 'line-height', lh, 'normal | <number> | px');
return { font: { family: 'Ahem', size: fs.value, specifiedSize: { kind: 'px', value: fs.value }, absoluteSize: true }, lineHeight };
return { font: { family: face, size: fs.value, specifiedSize: { kind: 'px', value: fs.value }, absoluteSize: true }, lineHeight };
}

// goal.md principle 3: the text node carries its inherited text styles, so the lowering reads the text node and never its parent.
function lowerText(t: ResolvedText): TextLeaf {
function lowerText(t: ResolvedText, mode: LowerMode): TextLeaf {
const id = t.node.address;
const get = (p: TextLonghand): CssValue => (t.props.get(p) as ResolvedValue).value;
const family = get('font-family');
if (textFontProblem(t) !== null) fail(id, 'font-family', family, AHEM_EXPECTED);
const { font, lineHeight } = lowerFont(id, get);
const { font, lineHeight } = lowerFont(id, get, mode);
const collapse = get('white-space-collapse');
if (collapse.kind !== 'keyword' || collapse.value !== 'collapse') fail(id, 'white-space-collapse', collapse, 'collapse');
const wrap = get('text-wrap-mode');
Expand All @@ -358,9 +376,9 @@ function lowerText(t: ResolvedText): TextLeaf {
* CSS2 §10.8.1: the strut of a block container with inline content, its own font and line-height; an anonymous box's are its
* parent's (inherited). The text leaves inherit the same values, so the strut equals their font until inline boxes change it.
*/
function strutOf(el: ResolvedElement, hasInline: boolean): LineStrut | null {
function strutOf(el: ResolvedElement, hasInline: boolean, mode: LowerMode): LineStrut | null {
if (!hasInline) return null;
return lowerFont(el.element.address, (p) => (el.props.get(p) as ResolvedValue).value);
return lowerFont(el.element.address, (p) => (el.props.get(p) as ResolvedValue).value, mode);
}

const displayOf = (el: ResolvedElement): string => {
Expand All @@ -385,7 +403,7 @@ export function assertTextCarriesContainer(container: LayoutStyle, containerId:
const isInlineLevel = (c: ResolvedElement | ResolvedText): boolean => c.kind === 'text' || (displayOf(c) === 'inline' && !isReplacedTag(c.element.tag));

/** propagated is the element whose overflow the viewport took (viewportOverflow), which uses visible. */
type Lowerer = { readonly faults: CompilerFaults; readonly ua: UaDataset; readonly rootFontSize: number | null; readonly images: ImageNaturals; readonly propagated: string | null };
type Lowerer = { readonly faults: CompilerFaults; readonly ua: UaDataset; readonly rootFontSize: number | null; readonly images: ImageNaturals; readonly mode: LowerMode; readonly propagated: string | null };

/**
* One piece of inline content (CSS2 §9.2.2): a text leaf, a <br> as a LineBreak, or an inline box with its own font and
Expand All @@ -394,9 +412,9 @@ type Lowerer = { readonly faults: CompilerFaults; readonly ua: UaDataset; readon
* carries them (C5).
*/
function lowerInline(c: ResolvedElement | ResolvedText, l: Lowerer): InlineChild {
if (c.kind === 'text') return lowerText(c);
if (c.kind === 'text') return lowerText(c, l.mode);
const id = c.element.address;
const own = lowerFont(id, (p) => (c.props.get(p) as ResolvedValue).value);
const own = lowerFont(id, (p) => (c.props.get(p) as ResolvedValue).value, l.mode);
if (c.element.tag === 'br') {
if (l.faults.brAsSpace) return { kind: 'text', id, text: ' ', font: own.font, lineHeight: own.lineHeight, whiteSpaceCollapse: 'collapse', textWrapMode: 'wrap' };
return { kind: 'br', id, font: own.font, lineHeight: own.lineHeight };
Expand All @@ -420,18 +438,18 @@ function anonymousBox(parent: ResolvedElement, id: string, items: readonly (Reso
// Every non-inherited property of an anonymous box is its initial value.
const style = lowerStyleFrom(id, (p) => values.get(p) as CssValue, (p) => !INHERITED.has(p), l.faults, l.ua, { em: null, rem: null });
for (const t of items) if (t.kind === 'text') assertTextCarriesContainer(style, id, t);
return { kind: 'box', id, boxType: 'anonymous', style, strut: strutOf(parent, items.length > 0), children: items.map((c) => lowerInline(c, l)) };
return { kind: 'box', id, boxType: 'anonymous', style, strut: strutOf(parent, items.length > 0, l.mode), children: items.map((c) => lowerInline(c, l)) };
}

/**
* The layout tree of a document. display: none subtrees generate no boxes (CSS2 §9.2.4), so they are omitted wherever they occur
* (C4) and a display: none root has no layout tree.
*/
export function lowerTree(root: ResolvedElement, faults: CompilerFaults, ua: UaDataset, images: ImageNaturals): LayoutBox {
export function lowerTree(root: ResolvedElement, faults: CompilerFaults, ua: UaDataset, images: ImageNaturals, mode: LowerMode): LayoutBox {
if (displayOf(root) === 'none') throw new LoweringError(root.element.address, 'display', `display: none on the root element ${root.element.address} leaves no layout tree`);
if (isReplacedTag(root.element.tag)) throw new LoweringError(root.element.address, 'display', `the root element ${root.element.address} is a replaced element`);
// The engine input's rootFontSize (V2) needs the root's font size in px, so a root whose font-size did not compute to px is refused here.
return lowerBox(root, { faults, ua, rootFontSize: rootFontSizeOf(root), images, propagated: viewportOverflow(root, faults).source }, null);
return lowerBox(root, { faults, ua, rootFontSize: rootFontSizeOf(root), images, mode, propagated: viewportOverflow(root, faults).source }, null);
}

const OBJECT_FITS: readonly ObjectFit[] = ['fill', 'contain', 'cover', 'none', 'scale-down'];
Expand Down Expand Up @@ -523,5 +541,5 @@ function lowerBox(el: ResolvedElement, l: Lowerer, gridParent: GridContainer | n
}
flush();
// A line strut only when the box holds inline content: a replaced leaf is a box of its own, not a line (REPL-a with INL1a).
return { kind: 'box', id, boxType: 'element', style, strut: strutOf(el, children.some((c) => c.kind !== 'box' && c.kind !== 'replaced')), children };
return { kind: 'box', id, boxType: 'element', style, strut: strutOf(el, children.some((c) => c.kind !== 'box' && c.kind !== 'replaced'), l.mode), children };
}
Loading
Loading