@ng-native/fabric 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,9 +16,10 @@ type FontFace = NonNullable<StyleSheet['fonts']>[number];
16
16
  export declare const DROP_MATCHED_WEIGHT_AND_STYLE = true;
17
17
  /**
18
18
  * The name a face is registered under: the family, then its weight and its style where it
19
- * declares them. `Inter`, `Inter-600`, `Inter-italic`, `Inter-600-italic`.
19
+ * declares them. `Inter`, `Inter-600`, `Inter-italic`, `Inter-600-italic`. A variable font's
20
+ * range is both its ends, `Inter-100to900`, so two files that split a family by range differ.
20
21
  */
21
- export declare function faceName(face: Pick<FontFace, 'family' | 'weight' | 'style'>): string;
22
+ export declare function faceName(face: Pick<FontFace, 'family' | 'weight' | 'weightRange' | 'style'>): string;
22
23
  /**
23
24
  * The face CSS's matching picks among one family's, for a wanted weight and style.
24
25
  *
@@ -30,6 +31,8 @@ export declare function matchFace(faces: readonly FontFace[], weight: number, st
30
31
  /** Every face the engine has seen a sheet declare, by family. */
31
32
  export declare class FontFaces {
32
33
  private readonly byFamily;
34
+ /** Forget every face, before they are taken in again from the sheets still registered. */
35
+ clear(): void;
33
36
  /** Take faces in; whether any was new, which changes what text already matched may match. */
34
37
  add(faces: readonly FontFace[]): boolean;
35
38
  /**
@@ -6,12 +6,15 @@
6
6
  export const DROP_MATCHED_WEIGHT_AND_STYLE = true;
7
7
  /**
8
8
  * The name a face is registered under: the family, then its weight and its style where it
9
- * declares them. `Inter`, `Inter-600`, `Inter-italic`, `Inter-600-italic`.
9
+ * declares them. `Inter`, `Inter-600`, `Inter-italic`, `Inter-600-italic`. A variable font's
10
+ * range is both its ends, `Inter-100to900`, so two files that split a family by range differ.
10
11
  */
11
12
  export function faceName(face) {
12
13
  let name = face.family;
13
14
  if (face.weight !== undefined)
14
15
  name += `-${face.weight}`;
16
+ else if (face.weightRange)
17
+ name += `-${face.weightRange[0]}to${face.weightRange[1]}`;
15
18
  if (face.style)
16
19
  name += `-${face.style}`;
17
20
  return name;
@@ -42,7 +45,10 @@ export function matchFace(faces, weight, style) {
42
45
  const order = STYLE_ORDER[style] ?? STYLE_ORDER['normal'];
43
46
  const wantedStyle = order.find((one) => faces.some((face) => slant(face) === one));
44
47
  const candidates = faces.filter((face) => slant(face) === wantedStyle);
45
- const weightOfFace = (face) => face.weight ?? 400;
48
+ // A face that covers a range has every weight in it: the nearest it has to the one wanted.
49
+ const weightOfFace = (face) => face.weightRange
50
+ ? Math.min(Math.max(weight, face.weightRange[0]), face.weightRange[1])
51
+ : (face.weight ?? 400);
46
52
  const heavier = (from, to) => candidates
47
53
  .filter((face) => weightOfFace(face) >= from && weightOfFace(face) <= to)
48
54
  .sort((a, b) => weightOfFace(a) - weightOfFace(b))[0];
@@ -61,6 +67,10 @@ const familyKey = (family) => family.toLowerCase();
61
67
  /** Every face the engine has seen a sheet declare, by family. */
62
68
  export class FontFaces {
63
69
  byFamily = new Map();
70
+ /** Forget every face, before they are taken in again from the sheets still registered. */
71
+ clear() {
72
+ this.byFamily.clear();
73
+ }
64
74
  /** Take faces in; whether any was new, which changes what text already matched may match. */
65
75
  add(faces) {
66
76
  let added = false;
@@ -105,6 +115,7 @@ export class FontFaces {
105
115
  // upright one Android draws its system font slanted in place of the face, which iOS draws
106
116
  // upright anyway.
107
117
  delete style['fontStyle'];
118
+ // A face of one weight draws it. One that covers a range is told which, so the weight stays.
108
119
  if (face.weight !== undefined)
109
120
  delete style['fontWeight'];
110
121
  }
@@ -1 +1 @@
1
- {"version":3,"file":"font-faces.js","sourceRoot":"","sources":["../src/font-faces.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Font matching, which native does not do.\n *\n * A device finds a family by name and nothing else, so each `@font-face` is registered under a\n * name of its own (`faceName`), and a text asking for a declared family with a weight or a style\n * is pointed at the face CSS would pick for it. That happens on the resolved style, after the\n * cascade, because a family and a weight often come from different rules or from an ancestor.\n */\nimport type { StyleSheet } from './css.ts';\n\ntype FontFace = NonNullable<StyleSheet['fonts']>[number];\n\n/**\n * When a face was picked, whether to drop the `fontWeight` and `fontStyle` it was picked for from\n * what is committed. The face already draws them, and kept, the platform may embolden or slant it\n * again on top.\n */\nexport const DROP_MATCHED_WEIGHT_AND_STYLE = true;\n\n/**\n * The name a face is registered under: the family, then its weight and its style where it\n * declares them. `Inter`, `Inter-600`, `Inter-italic`, `Inter-600-italic`.\n */\nexport function faceName(face: Pick<FontFace, 'family' | 'weight' | 'style'>): string {\n let name = face.family;\n if (face.weight !== undefined) name += `-${face.weight}`;\n if (face.style) name += `-${face.style}`;\n return name;\n}\n\n/** CSS's fallback order for a style: the closest slant first, upright last. */\nconst STYLE_ORDER: Readonly<Record<string, readonly string[]>> = {\n italic: ['italic', 'oblique', 'normal'],\n oblique: ['oblique', 'italic', 'normal'],\n normal: ['normal', 'oblique', 'italic'],\n};\n\n/** A `fontWeight` as a number: `normal` and unset are 400, `bold` 700. NaN for anything else. */\nfunction weightOf(value: unknown): number {\n if (value === undefined || value === 'normal') return 400;\n if (value === 'bold') return 700;\n return Number(value);\n}\n\n/**\n * The face CSS's matching picks among one family's, for a wanted weight and style.\n *\n * Style first, then weight, as in CSS Fonts 4 section 5.2. For a weight from 400 to 500, the\n * weights up to 500 are tried first, then lighter ones, then heavier; under 400, lighter then\n * heavier; over 500, heavier then lighter. Each direction nearest first.\n */\nexport function matchFace(\n faces: readonly FontFace[],\n weight: number,\n style: string,\n): FontFace | undefined {\n const slant = (face: FontFace) => face.style ?? 'normal';\n const order = STYLE_ORDER[style] ?? STYLE_ORDER['normal']!;\n const wantedStyle = order.find((one) => faces.some((face) => slant(face) === one));\n const candidates = faces.filter((face) => slant(face) === wantedStyle);\n\n const weightOfFace = (face: FontFace) => face.weight ?? 400;\n const heavier = (from: number, to: number) =>\n candidates\n .filter((face) => weightOfFace(face) >= from && weightOfFace(face) <= to)\n .sort((a, b) => weightOfFace(a) - weightOfFace(b))[0];\n const lighter = (upTo: number) =>\n candidates\n .filter((face) => weightOfFace(face) <= upTo)\n .sort((a, b) => weightOfFace(b) - weightOfFace(a))[0];\n\n if (weight >= 400 && weight <= 500) {\n return heavier(weight, 500) ?? lighter(weight) ?? heavier(500, Infinity);\n }\n if (weight < 400) return lighter(weight) ?? heavier(weight, Infinity);\n return heavier(weight, Infinity) ?? lighter(weight);\n}\n\n/** A family as CSS compares it: without regard to case. */\nconst familyKey = (family: string) => family.toLowerCase();\n\n/** Every face the engine has seen a sheet declare, by family. */\nexport class FontFaces {\n private readonly byFamily = new Map<string, FontFace[]>();\n\n /** Take faces in; whether any was new, which changes what text already matched may match. */\n add(faces: readonly FontFace[]): boolean {\n let added = false;\n for (const face of faces) {\n const known = this.byFamily.get(familyKey(face.family));\n if (known?.some((one) => faceName(one) === faceName(face))) continue;\n if (known) known.push(face);\n else this.byFamily.set(familyKey(face.family), [face]);\n added = true;\n }\n return added;\n }\n\n /**\n * Point a resolved style's family at the face its weight and style ask for. A family nothing\n * declared is left alone, and so is the family of one whose match is the face registered under\n * the family itself.\n */\n apply(style: Record<string, unknown>): void {\n if (this.byFamily.size === 0) return;\n const family = style['fontFamily'];\n if (typeof family !== 'string') return;\n const faces = this.byFamily.get(familyKey(family));\n if (!faces) return;\n\n const weight = weightOf(style['fontWeight']);\n if (Number.isNaN(weight)) return;\n const face = matchFace(faces, weight, String(style['fontStyle'] ?? 'normal'));\n if (!face) return;\n const name = faceName(face);\n if (name !== family) style['fontFamily'] = name;\n\n if (!DROP_MATCHED_WEIGHT_AND_STYLE) return;\n // The style goes whichever face matched: a slanted face draws its slant, and beside an\n // upright one Android draws its system font slanted in place of the face, which iOS draws\n // upright anyway.\n delete style['fontStyle'];\n if (face.weight !== undefined) delete style['fontWeight'];\n }\n}\n"]}
1
+ {"version":3,"file":"font-faces.js","sourceRoot":"","sources":["../src/font-faces.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Font matching, which native does not do.\n *\n * A device finds a family by name and nothing else, so each `@font-face` is registered under a\n * name of its own (`faceName`), and a text asking for a declared family with a weight or a style\n * is pointed at the face CSS would pick for it. That happens on the resolved style, after the\n * cascade, because a family and a weight often come from different rules or from an ancestor.\n */\nimport type { StyleSheet } from './css.ts';\n\ntype FontFace = NonNullable<StyleSheet['fonts']>[number];\n\n/**\n * When a face was picked, whether to drop the `fontWeight` and `fontStyle` it was picked for from\n * what is committed. The face already draws them, and kept, the platform may embolden or slant it\n * again on top.\n */\nexport const DROP_MATCHED_WEIGHT_AND_STYLE = true;\n\n/**\n * The name a face is registered under: the family, then its weight and its style where it\n * declares them. `Inter`, `Inter-600`, `Inter-italic`, `Inter-600-italic`. A variable font's\n * range is both its ends, `Inter-100to900`, so two files that split a family by range differ.\n */\nexport function faceName(\n face: Pick<FontFace, 'family' | 'weight' | 'weightRange' | 'style'>,\n): string {\n let name = face.family;\n if (face.weight !== undefined) name += `-${face.weight}`;\n else if (face.weightRange) name += `-${face.weightRange[0]}to${face.weightRange[1]}`;\n if (face.style) name += `-${face.style}`;\n return name;\n}\n\n/** CSS's fallback order for a style: the closest slant first, upright last. */\nconst STYLE_ORDER: Readonly<Record<string, readonly string[]>> = {\n italic: ['italic', 'oblique', 'normal'],\n oblique: ['oblique', 'italic', 'normal'],\n normal: ['normal', 'oblique', 'italic'],\n};\n\n/** A `fontWeight` as a number: `normal` and unset are 400, `bold` 700. NaN for anything else. */\nfunction weightOf(value: unknown): number {\n if (value === undefined || value === 'normal') return 400;\n if (value === 'bold') return 700;\n return Number(value);\n}\n\n/**\n * The face CSS's matching picks among one family's, for a wanted weight and style.\n *\n * Style first, then weight, as in CSS Fonts 4 section 5.2. For a weight from 400 to 500, the\n * weights up to 500 are tried first, then lighter ones, then heavier; under 400, lighter then\n * heavier; over 500, heavier then lighter. Each direction nearest first.\n */\nexport function matchFace(\n faces: readonly FontFace[],\n weight: number,\n style: string,\n): FontFace | undefined {\n const slant = (face: FontFace) => face.style ?? 'normal';\n const order = STYLE_ORDER[style] ?? STYLE_ORDER['normal']!;\n const wantedStyle = order.find((one) => faces.some((face) => slant(face) === one));\n const candidates = faces.filter((face) => slant(face) === wantedStyle);\n\n // A face that covers a range has every weight in it: the nearest it has to the one wanted.\n const weightOfFace = (face: FontFace) =>\n face.weightRange\n ? Math.min(Math.max(weight, face.weightRange[0]), face.weightRange[1])\n : (face.weight ?? 400);\n const heavier = (from: number, to: number) =>\n candidates\n .filter((face) => weightOfFace(face) >= from && weightOfFace(face) <= to)\n .sort((a, b) => weightOfFace(a) - weightOfFace(b))[0];\n const lighter = (upTo: number) =>\n candidates\n .filter((face) => weightOfFace(face) <= upTo)\n .sort((a, b) => weightOfFace(b) - weightOfFace(a))[0];\n\n if (weight >= 400 && weight <= 500) {\n return heavier(weight, 500) ?? lighter(weight) ?? heavier(500, Infinity);\n }\n if (weight < 400) return lighter(weight) ?? heavier(weight, Infinity);\n return heavier(weight, Infinity) ?? lighter(weight);\n}\n\n/** A family as CSS compares it: without regard to case. */\nconst familyKey = (family: string) => family.toLowerCase();\n\n/** Every face the engine has seen a sheet declare, by family. */\nexport class FontFaces {\n private readonly byFamily = new Map<string, FontFace[]>();\n\n /** Forget every face, before they are taken in again from the sheets still registered. */\n clear(): void {\n this.byFamily.clear();\n }\n\n /** Take faces in; whether any was new, which changes what text already matched may match. */\n add(faces: readonly FontFace[]): boolean {\n let added = false;\n for (const face of faces) {\n const known = this.byFamily.get(familyKey(face.family));\n if (known?.some((one) => faceName(one) === faceName(face))) continue;\n if (known) known.push(face);\n else this.byFamily.set(familyKey(face.family), [face]);\n added = true;\n }\n return added;\n }\n\n /**\n * Point a resolved style's family at the face its weight and style ask for. A family nothing\n * declared is left alone, and so is the family of one whose match is the face registered under\n * the family itself.\n */\n apply(style: Record<string, unknown>): void {\n if (this.byFamily.size === 0) return;\n const family = style['fontFamily'];\n if (typeof family !== 'string') return;\n const faces = this.byFamily.get(familyKey(family));\n if (!faces) return;\n\n const weight = weightOf(style['fontWeight']);\n if (Number.isNaN(weight)) return;\n const face = matchFace(faces, weight, String(style['fontStyle'] ?? 'normal'));\n if (!face) return;\n const name = faceName(face);\n if (name !== family) style['fontFamily'] = name;\n\n if (!DROP_MATCHED_WEIGHT_AND_STYLE) return;\n // The style goes whichever face matched: a slanted face draws its slant, and beside an\n // upright one Android draws its system font slanted in place of the face, which iOS draws\n // upright anyway.\n delete style['fontStyle'];\n // A face of one weight draws it. One that covers a range is told which, so the weight stays.\n if (face.weight !== undefined) delete style['fontWeight'];\n }\n}\n"]}
package/dist/host.d.ts CHANGED
@@ -104,6 +104,13 @@ export declare abstract class HostEngine {
104
104
  * that measures when content changes, as a browser does, there is nothing to do.
105
105
  */
106
106
  remeasure(_node: HostNode): void;
107
+ /**
108
+ * Style `node` as an element of the template `like` is written in, rather than of the one that
109
+ * created it, or as its own again when `like` is null: the content view a scroll view makes for
110
+ * its children, which the app styles through a class of its own and so from its own component
111
+ * styles.
112
+ */
113
+ adoptScope(_node: HostNode, _like: HostNode | null): void;
107
114
  /**
108
115
  * Take the user to a node: a text input gets the cursor, and anything else is brought on
109
116
  * screen. What a DOM element's own `focus()` does, for code that calls it on an element.
package/dist/host.js CHANGED
@@ -22,6 +22,13 @@ export class HostEngine {
22
22
  * that measures when content changes, as a browser does, there is nothing to do.
23
23
  */
24
24
  remeasure(_node) { }
25
+ /**
26
+ * Style `node` as an element of the template `like` is written in, rather than of the one that
27
+ * created it, or as its own again when `like` is null: the content view a scroll view makes for
28
+ * its children, which the app styles through a class of its own and so from its own component
29
+ * styles.
30
+ */
31
+ adoptScope(_node, _like) { }
25
32
  /**
26
33
  * Take the user to a node: a text input gets the cursor, and anything else is brought on
27
34
  * screen. What a DOM element's own `focus()` does, for code that calls it on an element.
package/dist/host.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AA4EA;;;;;GAKG;AACH,MAAM,OAAgB,UAAU;IAwB9B;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAgB;QACrB,cAAc,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,KAAe,IAAS,CAAC;IAEnC;;;OAGG;IACH,KAAK,CAAC,KAAe,IAAS,CAAC;IAE/B;;;OAGG;IACH,MAAM,CAAC,KAAe,EAAE,QAAsD,IAAS,CAAC;IAExF,+FAA+F;IAC/F,IAAI,YAAY;QACd,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;OAGG;IACH,aAAa,CACX,KAAe,EACf,OAAiB,EACjB,KAAiB,EACjB,MAAmB,EACnB,QAAqC;QAErC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,YAAY,CACV,KAAe,EACf,OAAiB,EACjB,KAAgB,EAChB,SAAyB,EACzB,MAAmB,EACnB,QAAqC,EACrC,MAAsB;QAEtB,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["/**\n * The contract the shared packages actually depend on, rather than the Fabric implementation.\n *\n * `@ng-native/components` is meant to be renderer-agnostic: it describes a view and a press,\n * and leaves the drawing to a host. In practice they were coupled to Fabric twice over - `inject(Engine)`, which is a concrete\n * class whose constructor wants a `FabricUIManager`, and `EngineNode`, which is a Fabric tree\n * node. A web prototype could only satisfy both by casting a hand-written object through\n * `as unknown as Engine`, which typechecks and tells you nothing.\n *\n * So this is the seam, measured rather than guessed: every `engine.*` call and every `node.*`\n * field those three packages actually make, and nothing else. `Engine` implements `HostEngine` and\n * `EngineNode` extends `HostNode`, so nothing about the native path changes; a browser host\n * implements the same two and needs no cast.\n *\n * It lives in this package because everything already imports from here and importing it costs a\n * web build nothing - the Fabric bindings are reached lazily, through a global the browser does\n * not have. If that stops being true, moving these two declarations to a package of their own is\n * mechanical: nothing here refers to Fabric.\n *\n * ## What is deliberately not in it\n *\n * `tagOf` and `shadowNodeOf` hand out Fabric's own handles, for Reanimated worklets and\n * react-native-gesture-handler. The three components that use them - `worklet-style`,\n * `worklet-scroll` and `native-gesture` - inject the concrete `Engine` and are native-only by\n * nature, which is the honest place to draw that line rather than pretending a browser could\n * answer.\n */\nimport type {\n DrivenProperty,\n EventFeed,\n ScrollAxis,\n ScrollDrive,\n ScrollRange,\n StaticTransform,\n} from './native-drive.ts';\nimport type { ResponderHandlers, WindowFrame } from './engine.ts';\n\n/** What a host node has to be able to answer. `EngineNode` and a DOM element both can. */\nexport interface HostNode {\n /** `element` or `text`, so a caller can tell a node from the words inside it. */\n readonly kind: string;\n /**\n * The props the host is holding for this node.\n *\n * A real bag rather than an accessor, because `ViewBase` deletes from it: an input that a\n * directive consumes has to stop being an attribute the host writes through.\n */\n props: Record<string, unknown>;\n /** The text, when `kind` is `text`. Empty otherwise. */\n text: string;\n children: readonly HostNode[];\n parent: HostNode | null;\n /**\n * Set by `claimHost` from every host primitive's constructor, so a host can tell an element a\n * component owns from a bare one somebody wrote by hand.\n *\n * Part of the contract rather than a Fabric detail, because the shared packages are what set\n * it: `ViewBase`'s constructor calls `claimHost` on every element it hosts. What a host does\n * with it is its own business - Fabric warns about an unclaimed primitive name, and a browser\n * has nothing to warn about.\n */\n claimed?: true;\n /**\n * The engine that made this node, when the host can say. A primitive reads it rather than\n * injecting `HostEngine`, because that lookup walks every element injector up to the root and\n * runs once per element on screen. A host whose nodes cannot carry it leaves it out, and the\n * primitive injects as before.\n */\n readonly host?: HostEngine;\n}\n\n/** Something that writes its node's props once it has its inputs. See `HostEngine.settle`. */\nexport interface Settling {\n settleProps(): void;\n}\n\n/**\n * What a host has to be able to do.\n *\n * An abstract class rather than an interface plus an `InjectionToken`, because Angular can inject\n * an abstract class directly and the alternative is two names for one thing.\n */\nexport abstract class HostEngine {\n /** Write a prop. `undefined` and `null` both clear it, which is how native says \"default\". */\n abstract setProp(node: HostNode, key: string, value: unknown): void;\n /** Listen for one of the host's own events. Returns its own teardown. */\n abstract setEventListener(\n node: HostNode,\n topLevelType: string,\n fn: (event: unknown) => void,\n ): () => void;\n /** Claim gestures on a node, through whatever arbitration the host has. */\n abstract setResponder(node: HostNode, handlers: ResponderHandlers): () => void;\n /** Where the node is on screen, asynchronously, because native answers on its own thread. */\n abstract measure(node: HostNode, into: (frame: WindowFrame) => void): void;\n /** Tell a native view to do something imperative: scroll, focus, blur. */\n abstract dispatchCommand(node: HostNode, name: string, args?: readonly unknown[]): void;\n /** Flush pending work. Native batches; a browser may not need to. */\n abstract commit(): boolean;\n /** The node holding focus, or null. Verified to still be attached before it is handed back. */\n abstract get focused(): HostNode | null;\n /** Turn whatever an app passed as an image source into what the host wants. */\n abstract resolveAsset(value: unknown): unknown;\n /** Turn a colour the app wrote into whatever the host draws with. */\n abstract color(value: unknown): unknown;\n\n /**\n * Call `target.settleProps()` before the next commit, once the element's static inputs are set.\n *\n * A host primitive writes its props from `ngOnChanges` and `ngOnInit`, which run in its parent\n * view's update pass. A parent that detaches itself before its first check never has one, so\n * without this its pressables would take no press and carry no role or `testID`, where on the\n * web a listener and an attribute are there from creation. The default settles in a microtask,\n * for a host with no commit of its own to hang it on.\n */\n settle(target: Settling): void {\n queueMicrotask(() => target.settleProps());\n }\n\n /**\n * Measure a node again from what native now holds for it, in a commit of its own. For a host\n * that measures when content changes, as a browser does, there is nothing to do.\n */\n remeasure(_node: HostNode): void {}\n\n /**\n * Take the user to a node: a text input gets the cursor, and anything else is brought on\n * screen. What a DOM element's own `focus()` does, for code that calls it on an element.\n */\n focus(_node: HostNode): void {}\n\n /**\n * Scroll whatever the node is in just far enough to show all of it; `visibleBottom` is where in\n * the window the visible area ends, when something such as the keyboard covers its bottom.\n */\n reveal(_node: HostNode, _options?: { visibleBottom?: number; margin?: number }): void {}\n\n /** Whether `driveByScroll` moves views on the native side. A host without it moves nothing. */\n get drivesScroll(): boolean {\n return false;\n }\n\n /**\n * Move `view` by `scroll`'s offset along `axis` on the native side, through `range`, keeping\n * `statics` in its transform. Null when the host cannot, and the caller moves the view itself.\n */\n driveByScroll(\n _view: HostNode,\n _scroll: HostNode,\n _axis: ScrollAxis,\n _range: ScrollRange,\n _statics?: readonly StaticTransform[],\n ): ScrollDrive | null {\n return null;\n }\n\n /**\n * Move `view` by a value `source`'s native events carry, such as a keyboard's height, on the\n * native side, plus `shift` when one is given, which `ScrollDrive.shift` changes. Null when the\n * host cannot. See `driveByScroll`.\n */\n driveByEvent(\n _view: HostNode,\n _source: HostNode,\n _feed: EventFeed,\n _property: DrivenProperty,\n _range: ScrollRange,\n _statics?: readonly StaticTransform[],\n _shift?: number | null,\n ): ScrollDrive | null {\n return null;\n }\n}\n"]}
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AA4EA;;;;;GAKG;AACH,MAAM,OAAgB,UAAU;IAwB9B;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAgB;QACrB,cAAc,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,KAAe,IAAS,CAAC;IAEnC;;;;;OAKG;IACH,UAAU,CAAC,KAAe,EAAE,KAAsB,IAAS,CAAC;IAE5D;;;OAGG;IACH,KAAK,CAAC,KAAe,IAAS,CAAC;IAE/B;;;OAGG;IACH,MAAM,CAAC,KAAe,EAAE,QAAsD,IAAS,CAAC;IAExF,+FAA+F;IAC/F,IAAI,YAAY;QACd,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;OAGG;IACH,aAAa,CACX,KAAe,EACf,OAAiB,EACjB,KAAiB,EACjB,MAAmB,EACnB,QAAqC;QAErC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,YAAY,CACV,KAAe,EACf,OAAiB,EACjB,KAAgB,EAChB,SAAyB,EACzB,MAAmB,EACnB,QAAqC,EACrC,MAAsB;QAEtB,OAAO,IAAI,CAAC;IACd,CAAC;CACF","sourcesContent":["/**\n * The contract the shared packages actually depend on, rather than the Fabric implementation.\n *\n * `@ng-native/components` is meant to be renderer-agnostic: it describes a view and a press,\n * and leaves the drawing to a host. In practice they were coupled to Fabric twice over - `inject(Engine)`, which is a concrete\n * class whose constructor wants a `FabricUIManager`, and `EngineNode`, which is a Fabric tree\n * node. A web prototype could only satisfy both by casting a hand-written object through\n * `as unknown as Engine`, which typechecks and tells you nothing.\n *\n * So this is the seam, measured rather than guessed: every `engine.*` call and every `node.*`\n * field those three packages actually make, and nothing else. `Engine` implements `HostEngine` and\n * `EngineNode` extends `HostNode`, so nothing about the native path changes; a browser host\n * implements the same two and needs no cast.\n *\n * It lives in this package because everything already imports from here and importing it costs a\n * web build nothing - the Fabric bindings are reached lazily, through a global the browser does\n * not have. If that stops being true, moving these two declarations to a package of their own is\n * mechanical: nothing here refers to Fabric.\n *\n * ## What is deliberately not in it\n *\n * `tagOf` and `shadowNodeOf` hand out Fabric's own handles, for Reanimated worklets and\n * react-native-gesture-handler. The three components that use them - `worklet-style`,\n * `worklet-scroll` and `native-gesture` - inject the concrete `Engine` and are native-only by\n * nature, which is the honest place to draw that line rather than pretending a browser could\n * answer.\n */\nimport type {\n DrivenProperty,\n EventFeed,\n ScrollAxis,\n ScrollDrive,\n ScrollRange,\n StaticTransform,\n} from './native-drive.ts';\nimport type { ResponderHandlers, WindowFrame } from './engine.ts';\n\n/** What a host node has to be able to answer. `EngineNode` and a DOM element both can. */\nexport interface HostNode {\n /** `element` or `text`, so a caller can tell a node from the words inside it. */\n readonly kind: string;\n /**\n * The props the host is holding for this node.\n *\n * A real bag rather than an accessor, because `ViewBase` deletes from it: an input that a\n * directive consumes has to stop being an attribute the host writes through.\n */\n props: Record<string, unknown>;\n /** The text, when `kind` is `text`. Empty otherwise. */\n text: string;\n children: readonly HostNode[];\n parent: HostNode | null;\n /**\n * Set by `claimHost` from every host primitive's constructor, so a host can tell an element a\n * component owns from a bare one somebody wrote by hand.\n *\n * Part of the contract rather than a Fabric detail, because the shared packages are what set\n * it: `ViewBase`'s constructor calls `claimHost` on every element it hosts. What a host does\n * with it is its own business - Fabric warns about an unclaimed primitive name, and a browser\n * has nothing to warn about.\n */\n claimed?: true;\n /**\n * The engine that made this node, when the host can say. A primitive reads it rather than\n * injecting `HostEngine`, because that lookup walks every element injector up to the root and\n * runs once per element on screen. A host whose nodes cannot carry it leaves it out, and the\n * primitive injects as before.\n */\n readonly host?: HostEngine;\n}\n\n/** Something that writes its node's props once it has its inputs. See `HostEngine.settle`. */\nexport interface Settling {\n settleProps(): void;\n}\n\n/**\n * What a host has to be able to do.\n *\n * An abstract class rather than an interface plus an `InjectionToken`, because Angular can inject\n * an abstract class directly and the alternative is two names for one thing.\n */\nexport abstract class HostEngine {\n /** Write a prop. `undefined` and `null` both clear it, which is how native says \"default\". */\n abstract setProp(node: HostNode, key: string, value: unknown): void;\n /** Listen for one of the host's own events. Returns its own teardown. */\n abstract setEventListener(\n node: HostNode,\n topLevelType: string,\n fn: (event: unknown) => void,\n ): () => void;\n /** Claim gestures on a node, through whatever arbitration the host has. */\n abstract setResponder(node: HostNode, handlers: ResponderHandlers): () => void;\n /** Where the node is on screen, asynchronously, because native answers on its own thread. */\n abstract measure(node: HostNode, into: (frame: WindowFrame) => void): void;\n /** Tell a native view to do something imperative: scroll, focus, blur. */\n abstract dispatchCommand(node: HostNode, name: string, args?: readonly unknown[]): void;\n /** Flush pending work. Native batches; a browser may not need to. */\n abstract commit(): boolean;\n /** The node holding focus, or null. Verified to still be attached before it is handed back. */\n abstract get focused(): HostNode | null;\n /** Turn whatever an app passed as an image source into what the host wants. */\n abstract resolveAsset(value: unknown): unknown;\n /** Turn a colour the app wrote into whatever the host draws with. */\n abstract color(value: unknown): unknown;\n\n /**\n * Call `target.settleProps()` before the next commit, once the element's static inputs are set.\n *\n * A host primitive writes its props from `ngOnChanges` and `ngOnInit`, which run in its parent\n * view's update pass. A parent that detaches itself before its first check never has one, so\n * without this its pressables would take no press and carry no role or `testID`, where on the\n * web a listener and an attribute are there from creation. The default settles in a microtask,\n * for a host with no commit of its own to hang it on.\n */\n settle(target: Settling): void {\n queueMicrotask(() => target.settleProps());\n }\n\n /**\n * Measure a node again from what native now holds for it, in a commit of its own. For a host\n * that measures when content changes, as a browser does, there is nothing to do.\n */\n remeasure(_node: HostNode): void {}\n\n /**\n * Style `node` as an element of the template `like` is written in, rather than of the one that\n * created it, or as its own again when `like` is null: the content view a scroll view makes for\n * its children, which the app styles through a class of its own and so from its own component\n * styles.\n */\n adoptScope(_node: HostNode, _like: HostNode | null): void {}\n\n /**\n * Take the user to a node: a text input gets the cursor, and anything else is brought on\n * screen. What a DOM element's own `focus()` does, for code that calls it on an element.\n */\n focus(_node: HostNode): void {}\n\n /**\n * Scroll whatever the node is in just far enough to show all of it; `visibleBottom` is where in\n * the window the visible area ends, when something such as the keyboard covers its bottom.\n */\n reveal(_node: HostNode, _options?: { visibleBottom?: number; margin?: number }): void {}\n\n /** Whether `driveByScroll` moves views on the native side. A host without it moves nothing. */\n get drivesScroll(): boolean {\n return false;\n }\n\n /**\n * Move `view` by `scroll`'s offset along `axis` on the native side, through `range`, keeping\n * `statics` in its transform. Null when the host cannot, and the caller moves the view itself.\n */\n driveByScroll(\n _view: HostNode,\n _scroll: HostNode,\n _axis: ScrollAxis,\n _range: ScrollRange,\n _statics?: readonly StaticTransform[],\n ): ScrollDrive | null {\n return null;\n }\n\n /**\n * Move `view` by a value `source`'s native events carry, such as a keyboard's height, on the\n * native side, plus `shift` when one is given, which `ScrollDrive.shift` changes. Null when the\n * host cannot. See `driveByScroll`.\n */\n driveByEvent(\n _view: HostNode,\n _source: HostNode,\n _feed: EventFeed,\n _property: DrivenProperty,\n _range: ScrollRange,\n _statics?: readonly StaticTransform[],\n _shift?: number | null,\n ): ScrollDrive | null {\n return null;\n }\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -11,9 +11,11 @@
11
11
  * animal and lives in `@ng-native/metro`.
12
12
  */
13
13
  export { StyleResolver, matches, styleSheetOf, type AttributeTest, type Combinator, type Compound, type Conditions, type DeferredDeclaration, type GradientTemplate, type HslChannel, type MediaCondition, type NthTest, type RuleIndex, type StyleCache, type StyleRule, type StyleSheet, type StyleTarget, type TokenKind, type TokenValue, } from './css.ts';
14
+ export { ELEMENT_STYLES, ELEMENT_STYLES_CSS } from './element-styles.ts';
14
15
  export { EngineIntersectionObserver, installDeferTriggers } from './defer-triggers.ts';
15
16
  export { faceName } from './font-faces.ts';
16
- export { Engine, SyntheticEvent, claimHost, declareNativeProps, fontsRegistered, markComponentHost, nativePlatform, onFontsRegistered, registerHoist, registerPlatformComponents, registerViewName, type PlatformViewName, viewNameOf, type EngineNode, type EngineOptions, type EngineStats, type FabricNode, type FabricNodeSet, type FabricUIManager, type HoistOptions, type NativeSyntheticEvent, type NodeKind, type ResponderEvent, type ResponderHandlers, type TouchPayload, type ViewNameNode, type WindowFrame, } from './engine.ts';
17
+ export { firstFamily } from './inline-token.ts';
18
+ export { DIRECT_EVENTS, Engine, SyntheticEvent, claimHost, keepNativeView, declareNativeProps, fontsLoading, fontsRegistered, fontsSettled, markComponentHost, nativePlatform, onFontsRegistered, onFontsSettled, registerHoist, registerPlatformComponents, registerViewName, type PlatformViewName, type ViewNameOptions, viewNameOf, type EngineNode, type EngineOptions, type EngineStats, type FabricNode, type FabricNodeSet, type FabricUIManager, type HoistOptions, type NativeSyntheticEvent, type NodeKind, type ResponderEvent, type ResponderHandlers, type TouchPayload, type ViewNameNode, type WindowFrame, } from './engine.ts';
17
19
  export { reportUnboundFormsInput, type NamedHostNode } from './forms-inputs.ts';
18
20
  export { HostEngine, type HostNode, type Settling } from './host.ts';
19
21
  export { getFabricUIManager } from './fabric.ts';
package/dist/index.js CHANGED
@@ -11,9 +11,11 @@
11
11
  * animal and lives in `@ng-native/metro`.
12
12
  */
13
13
  export { StyleResolver, matches, styleSheetOf, } from "./css.js";
14
+ export { ELEMENT_STYLES, ELEMENT_STYLES_CSS } from "./element-styles.js";
14
15
  export { EngineIntersectionObserver, installDeferTriggers } from "./defer-triggers.js";
15
16
  export { faceName } from "./font-faces.js";
16
- export { Engine, SyntheticEvent, claimHost, declareNativeProps, fontsRegistered, markComponentHost, nativePlatform, onFontsRegistered, registerHoist, registerPlatformComponents, registerViewName, viewNameOf, } from "./engine.js";
17
+ export { firstFamily } from "./inline-token.js";
18
+ export { DIRECT_EVENTS, Engine, SyntheticEvent, claimHost, keepNativeView, declareNativeProps, fontsLoading, fontsRegistered, fontsSettled, markComponentHost, nativePlatform, onFontsRegistered, onFontsSettled, registerHoist, registerPlatformComponents, registerViewName, viewNameOf, } from "./engine.js";
17
19
  export { reportUnboundFormsInput } from "./forms-inputs.js";
18
20
  export { HostEngine } from "./host.js";
19
21
  export { getFabricUIManager } from "./fabric.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,aAAa,EACb,OAAO,EACP,YAAY,GAiBb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EACL,MAAM,EACN,cAAc,EACd,SAAS,EACT,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,0BAA0B,EAC1B,gBAAgB,EAEhB,UAAU,GAeX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,uBAAuB,EAAsB,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAAE,UAAU,EAAgC,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,WAAW,GAQZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,MAAM,EACN,WAAW,EACX,QAAQ,EACR,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,KAAK,GAMN,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * The engine's public surface.\n *\n * Nothing here imports Angular, and nothing here may start to: the engine stays\n * framework-agnostic (see docs/ARCHITECTURE.md), which is what keeps the commit logic testable without Angular in\n * the loop and a second adapter possible later.\n *\n * The CSS runtime lives here rather than in a package of its own because the cascade is driven\n * from the commit walk - the resolver runs per node, per tick - and because it is as\n * framework-agnostic as the rest of the engine. The build-time half of CSS is a different\n * animal and lives in `@ng-native/metro`.\n */\nexport {\n StyleResolver,\n matches,\n styleSheetOf,\n type AttributeTest,\n type Combinator,\n type Compound,\n type Conditions,\n type DeferredDeclaration,\n type GradientTemplate,\n type HslChannel,\n type MediaCondition,\n type NthTest,\n type RuleIndex,\n type StyleCache,\n type StyleRule,\n type StyleSheet,\n type StyleTarget,\n type TokenKind,\n type TokenValue,\n} from './css.ts';\nexport { EngineIntersectionObserver, installDeferTriggers } from './defer-triggers.ts';\nexport { faceName } from './font-faces.ts';\nexport {\n Engine,\n SyntheticEvent,\n claimHost,\n declareNativeProps,\n fontsRegistered,\n markComponentHost,\n nativePlatform,\n onFontsRegistered,\n registerHoist,\n registerPlatformComponents,\n registerViewName,\n type PlatformViewName,\n viewNameOf,\n type EngineNode,\n type EngineOptions,\n type EngineStats,\n type FabricNode,\n type FabricNodeSet,\n type FabricUIManager,\n type HoistOptions,\n type NativeSyntheticEvent,\n type NodeKind,\n type ResponderEvent,\n type ResponderHandlers,\n type TouchPayload,\n type ViewNameNode,\n type WindowFrame,\n} from './engine.ts';\nexport { reportUnboundFormsInput, type NamedHostNode } from './forms-inputs.ts';\nexport { HostEngine, type HostNode, type Settling } from './host.ts';\nexport { getFabricUIManager } from './fabric.ts';\nexport {\n pinnedRange,\n type DrivenProperty,\n type EventFeed,\n type NativeAnimated,\n type ScrollAxis,\n type ScrollDrive,\n type ScrollRange,\n type StaticTransform,\n} from './native-drive.ts';\nexport {\n bezier,\n interpolate,\n redirect,\n sample,\n settled,\n step,\n tracksOf,\n tween,\n type AnimationSpec,\n type Keyframe,\n type RunningAnimation,\n type Transition,\n type TransitionSpec,\n} from './transition.ts';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,aAAa,EACb,OAAO,EACP,YAAY,GAiBb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EACL,aAAa,EACb,MAAM,EACN,cAAc,EACd,SAAS,EACT,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,0BAA0B,EAC1B,gBAAgB,EAGhB,UAAU,GAeX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,uBAAuB,EAAsB,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAAE,UAAU,EAAgC,MAAM,WAAW,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,WAAW,GAQZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,MAAM,EACN,WAAW,EACX,QAAQ,EACR,MAAM,EACN,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,KAAK,GAMN,MAAM,iBAAiB,CAAC","sourcesContent":["/**\n * The engine's public surface.\n *\n * Nothing here imports Angular, and nothing here may start to: the engine stays\n * framework-agnostic (see docs/ARCHITECTURE.md), which is what keeps the commit logic testable without Angular in\n * the loop and a second adapter possible later.\n *\n * The CSS runtime lives here rather than in a package of its own because the cascade is driven\n * from the commit walk - the resolver runs per node, per tick - and because it is as\n * framework-agnostic as the rest of the engine. The build-time half of CSS is a different\n * animal and lives in `@ng-native/metro`.\n */\nexport {\n StyleResolver,\n matches,\n styleSheetOf,\n type AttributeTest,\n type Combinator,\n type Compound,\n type Conditions,\n type DeferredDeclaration,\n type GradientTemplate,\n type HslChannel,\n type MediaCondition,\n type NthTest,\n type RuleIndex,\n type StyleCache,\n type StyleRule,\n type StyleSheet,\n type StyleTarget,\n type TokenKind,\n type TokenValue,\n} from './css.ts';\nexport { ELEMENT_STYLES, ELEMENT_STYLES_CSS } from './element-styles.ts';\nexport { EngineIntersectionObserver, installDeferTriggers } from './defer-triggers.ts';\nexport { faceName } from './font-faces.ts';\nexport { firstFamily } from './inline-token.ts';\nexport {\n DIRECT_EVENTS,\n Engine,\n SyntheticEvent,\n claimHost,\n keepNativeView,\n declareNativeProps,\n fontsLoading,\n fontsRegistered,\n fontsSettled,\n markComponentHost,\n nativePlatform,\n onFontsRegistered,\n onFontsSettled,\n registerHoist,\n registerPlatformComponents,\n registerViewName,\n type PlatformViewName,\n type ViewNameOptions,\n viewNameOf,\n type EngineNode,\n type EngineOptions,\n type EngineStats,\n type FabricNode,\n type FabricNodeSet,\n type FabricUIManager,\n type HoistOptions,\n type NativeSyntheticEvent,\n type NodeKind,\n type ResponderEvent,\n type ResponderHandlers,\n type TouchPayload,\n type ViewNameNode,\n type WindowFrame,\n} from './engine.ts';\nexport { reportUnboundFormsInput, type NamedHostNode } from './forms-inputs.ts';\nexport { HostEngine, type HostNode, type Settling } from './host.ts';\nexport { getFabricUIManager } from './fabric.ts';\nexport {\n pinnedRange,\n type DrivenProperty,\n type EventFeed,\n type NativeAnimated,\n type ScrollAxis,\n type ScrollDrive,\n type ScrollRange,\n type StaticTransform,\n} from './native-drive.ts';\nexport {\n bezier,\n interpolate,\n redirect,\n sample,\n settled,\n step,\n tracksOf,\n tween,\n type AnimationSpec,\n type Keyframe,\n type RunningAnimation,\n type Transition,\n type TransitionSpec,\n} from './transition.ts';\n"]}
@@ -1,15 +1,6 @@
1
+ import type { TokenValue } from './css.ts';
1
2
  /**
2
- * A custom property set on an element to a value with a `var()` inside it,
3
- * `[style.--size]="'calc(var(--gap) * 2)'"`, read into the form the same text in a stylesheet is
4
- * compiled to, which the cascade then works out where it is set, as it does that one
5
- * (`resolveAliases` in css.ts).
6
- *
7
- * The shapes are the build-time conversion's and no more (`deferCalcToken`, `deferHslToken` and
8
- * `deferChannelsToken` in `@ng-native/metro/css/compile.cjs`): `calc()`, `min()` or `max()` of
9
- * numbers, lengths in `px` or `rem`, angles, times and `var()`, with + - * / and brackets; an
10
- * `hsl()` with a `var()` for a channel; and `rgb()` or `hsl()` of one channels token, with an
11
- * alpha written or tokened. Anything else is undefined, as a stylesheet refuses it.
3
+ * A value with a `var()` inside it, or a `color-mix()`, as a token made of others; undefined if it
4
+ * is no such shape.
12
5
  */
13
- import type { TokenValue } from './css.ts';
14
- /** A value with a `var()` inside it as a token made of others; undefined if it is no such shape. */
15
6
  export declare function derivedToken(text: string): TokenValue | undefined;
@@ -1,11 +1,26 @@
1
- import { tokenFromValue } from "./inline-token.js";
2
- const SPACE = /\s*/y;
3
- const FUNCTION = /([a-z]+)\(\s*/iy;
4
- const VAR = /var\(\s*(--[\w-]+)\s*/iy;
1
+ /**
2
+ * A custom property set on an element to a value with a `var()` inside it,
3
+ * `[style.--size]="'calc(var(--gap) * 2)'"`, read into the form the same text in a stylesheet is
4
+ * compiled to, which the cascade then works out where it is set, as it does that one
5
+ * (`resolveAliases` in css.ts).
6
+ *
7
+ * The shapes are the build-time conversion's and no more (`deferCalcToken`, `deferHslToken`,
8
+ * `deferChannelsToken` and `deferMixToken` in `@ng-native/metro/css/compile.cjs`): `calc()`,
9
+ * `min()` or `max()` of numbers, lengths in `px` or `rem`, angles, times and `var()`, with + - * /
10
+ * and brackets; an `hsl()` with a `var()` for a channel; `rgb()` or `hsl()` of one channels token,
11
+ * with an alpha written or tokened; and a `color-mix()` of any of these colours, tokens and
12
+ * colours written out. Anything else is undefined, as a stylesheet refuses it.
13
+ */
14
+ import { MIX_SPACES } from "./color-mix.js";
15
+ import { cssTrim, tokenFromValue } from "./inline-token.js";
16
+ /** CSS whitespace, and no other: a no-break space is part of a word, as it is in a browser. */
17
+ const SPACE = /[ \t\n\r\f]*/y;
18
+ const FUNCTION = /([a-z][a-z-]*)\([ \t\n\r\f]*/iy;
19
+ const VAR = /var\([ \t\n\r\f]*(--[\w-]+)[ \t\n\r\f]*/iy;
5
20
  const LITERAL = /([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(%|[a-z]+)?/iy;
6
- const WORD = /[^\s,/()]+/y;
21
+ const WORD = /[^ \t\n\r\f,/()]+/y;
7
22
  /** A fallback that is a `var()` or arithmetic, rather than a value written out. */
8
- const NESTED = /^\s*(var|calc|min|max)\(/i;
23
+ const NESTED = /^[ \t\n\r\f]*(var|calc|min|max)\(/i;
9
24
  /** What one of each unit counts as: points, degrees, milliseconds, or itself. */
10
25
  const PER_UNIT = {
11
26
  '': 1,
@@ -18,7 +33,9 @@ const PER_UNIT = {
18
33
  ms: 1,
19
34
  s: 1000,
20
35
  };
36
+ const PERCENTAGE = /([+-]?(?:\d+\.?\d*|\.\d+))%/y;
21
37
  const HUE_UNITS = new Set(['', 'deg', 'grad', 'rad', 'turn']);
38
+ const HUE_METHODS = new Set(['shorter', 'longer', 'increasing', 'decreasing']);
22
39
  const MATH = new Set(['calc', 'min', 'max']);
23
40
  const SPACES = {
24
41
  rgb: 'rgb',
@@ -120,7 +137,11 @@ function factor(cursor, kind) {
120
137
  return value === undefined ? { reference: name } : { reference: name, fallback: value };
121
138
  }
122
139
  const fn = match(cursor, FUNCTION)?.[1];
123
- return fn ? math(cursor, fn, kind) : literal(cursor, unitsOf(kind));
140
+ if (fn)
141
+ return math(cursor, fn, kind);
142
+ const value = literal(cursor, unitsOf(kind));
143
+ // A percentage, kept apart from a bare number, as `literal` in the compiler keeps it.
144
+ return cursor.text[cursor.at - 1] === '%' ? { percentage: value } : value;
124
145
  }
125
146
  /**
126
147
  * A `var()` fallback in arithmetic, as its slot reads it: another `var()` or arithmetic of its
@@ -129,16 +150,21 @@ function factor(cursor, kind) {
129
150
  */
130
151
  function leafFallback(text, kind) {
131
152
  if (text !== undefined && NESTED.test(text)) {
132
- return whole(text.trim(), (cursor) => factor(cursor, kind)) ?? fail();
153
+ return whole(cssTrim(text), (cursor) => factor(cursor, kind)) ?? fail();
133
154
  }
134
155
  const token = fallbackToken(text);
135
156
  if (kind !== 'length')
136
- return token?.number;
157
+ return numberFallback(token, kind);
137
158
  if (typeof token?.length === 'number')
138
159
  return token.length;
139
- const rem = text === undefined ? undefined : /^\s*(-?\d*\.?\d+)rem\s*$/i.exec(text);
160
+ const rem = text === undefined ? undefined : /^[ \t\n\r\f]*(-?\d*\.?\d+)rem[ \t\n\r\f]*$/i.exec(text);
140
161
  return rem ? Number(rem[1]) * PER_UNIT['rem'] : undefined;
141
162
  }
163
+ /** A fallback where a number is wanted: a percentage kept apart from a bare number. */
164
+ function numberFallback(token, kind) {
165
+ const percentage = kind === 'number' && typeof token?.length === 'string' && token.number !== undefined;
166
+ return percentage ? { percentage: token.number } : token?.number;
167
+ }
142
168
  /** The rest of a `calc()`, or of a `min()` or `max()` of sums, once its name is read. */
143
169
  function math(cursor, fn, kind) {
144
170
  const name = fn.toLowerCase();
@@ -233,7 +259,8 @@ function hslToken(args, legacy) {
233
259
  if (args.length !== 3 && args.length !== 4)
234
260
  fail();
235
261
  const [h, s, l, alpha] = args.map((arg, index) => hslChannel(arg, index, legacy));
236
- return { hsl: { h: h, s: s, l: l, ...(alpha === undefined ? {} : { alpha }) } };
262
+ const legacyFlag = legacy ? { legacy: true } : {};
263
+ return { hsl: { h: h, s: s, l: l, ...(alpha === undefined ? {} : { alpha }), ...legacyFlag } };
237
264
  }
238
265
  function colourToken(text) {
239
266
  return whole(text, (cursor) => {
@@ -245,8 +272,217 @@ function colourToken(text) {
245
272
  return space === 'hsl' ? hslToken(args, legacy) : fail();
246
273
  });
247
274
  }
248
- /** A value with a `var()` inside it as a token made of others; undefined if it is no such shape. */
275
+ /** The index just past the bracket that closes the function the cursor is inside. */
276
+ function closing(cursor) {
277
+ for (let at = cursor.at, depth = 0; at < cursor.text.length; at++) {
278
+ const char = cursor.text[at];
279
+ if (char === '(')
280
+ depth++;
281
+ else if (char === ')' && depth-- === 0)
282
+ return at + 1;
283
+ }
284
+ return fail();
285
+ }
286
+ /**
287
+ * A `var()` in a colour, as `reference` in the compiler's colour-expression.cjs reads it: the
288
+ * tokens its `var()` fallbacks name, tried in turn, then a colour, or one made of other tokens.
289
+ */
290
+ function referenceColour({ reference: name, fallback }) {
291
+ const alternatives = [];
292
+ let rest = fallback === undefined ? undefined : cssTrim(fallback);
293
+ for (let next; rest && (next = whole(rest, nextReference)); rest = next.fallback === undefined ? undefined : cssTrim(next.fallback)) {
294
+ alternatives.push(next.reference);
295
+ }
296
+ return {
297
+ reference: name,
298
+ ...(alternatives.length ? { alternatives } : {}),
299
+ ...(rest ? colourFallback(rest) : {}),
300
+ };
301
+ }
302
+ /** The colour a `var()` falls back to, or one made of other tokens; refused when it is neither. */
303
+ function colourFallback(text) {
304
+ const token = tokenFromValue(text);
305
+ if (token?.hsl || token?.deferredColour)
306
+ return { fallbackToken: token };
307
+ return { fallback: token?.color ?? fail() };
308
+ }
309
+ const nextReference = (cursor) => reference(cursor, (match(cursor, VAR) ?? fail())[1]);
310
+ /**
311
+ * One colour in a `color-mix()`: a `var()`, another mix, an `rgb()` or `hsl()` of tokens, or a
312
+ * colour written out, which is kept as written for the mix to read.
313
+ */
314
+ function mixColour(cursor) {
315
+ const name = match(cursor, VAR)?.[1];
316
+ if (name)
317
+ return referenceColour(reference(cursor, name));
318
+ const start = cursor.at;
319
+ const fn = match(cursor, FUNCTION)?.[1]?.toLowerCase();
320
+ if (fn === 'color-mix')
321
+ return mixExpression(cursor);
322
+ if (!fn)
323
+ return { color: (match(cursor, WORD) ?? fail())[0] };
324
+ if (RELATIVE[fn] && takeWord(cursor, 'from'))
325
+ return relativeExpression(cursor, RELATIVE[fn]);
326
+ cursor.at = closing(cursor);
327
+ return functionColour(cursor.text.slice(start, cursor.at));
328
+ }
329
+ /** A colour function in a mix: of tokens, as a token of it is read, or written out, as it is. */
330
+ function functionColour(text) {
331
+ if (!text.includes('var('))
332
+ return { color: text };
333
+ const token = colourToken(text);
334
+ return token?.hsl ? { hsl: token.hsl } : (token?.deferredColour ?? fail());
335
+ }
336
+ /** One side of a mix: its colour, and the percentage written before or after it, if one is. */
337
+ function mixSide(cursor) {
338
+ const percentage = () => {
339
+ const found = match(cursor, PERCENTAGE);
340
+ match(cursor, SPACE);
341
+ return found ? Number(found[1]) : undefined;
342
+ };
343
+ const before = percentage();
344
+ const colour = mixColour(cursor);
345
+ match(cursor, SPACE);
346
+ const after = before === undefined ? percentage() : undefined;
347
+ const written = before ?? after;
348
+ return written === undefined ? { colour } : { colour, percentage: written };
349
+ }
350
+ /**
351
+ * The rest of a `color-mix(in <space> [<method> hue], <colour> [p%], <colour> [q%])` once its name
352
+ * is read, as `mixExpression` in the compiler reads it.
353
+ */
354
+ function mixExpression(cursor) {
355
+ const words = [];
356
+ while (!take(cursor, ',')) {
357
+ words.push((match(cursor, WORD) ?? fail())[0].toLowerCase());
358
+ match(cursor, SPACE);
359
+ }
360
+ const [keyword, space, method, hue, ...rest] = words;
361
+ if (keyword !== 'in' || !MIX_SPACES.has(space) || rest.length)
362
+ fail();
363
+ if (method !== undefined && (hue !== 'hue' || !HUE_METHODS.has(method)))
364
+ fail();
365
+ const a = mixSide(cursor);
366
+ expect(cursor, ',');
367
+ const b = mixSide(cursor);
368
+ expect(cursor, ')');
369
+ return {
370
+ mix: {
371
+ space: space,
372
+ ...(method ? { hue: method } : {}),
373
+ a: a.colour,
374
+ ...(a.percentage === undefined ? {} : { aPercentage: a.percentage }),
375
+ b: b.colour,
376
+ ...(b.percentage === undefined ? {} : { bPercentage: b.percentage }),
377
+ },
378
+ };
379
+ }
380
+ /** A whole `color-mix()`, worked out where it is set, as `deferMixToken` in the compiler reads it. */
381
+ function mixToken(text) {
382
+ const deferredColour = whole(text, (cursor) => {
383
+ const colour = mixColour(cursor);
384
+ return 'mix' in colour || 'relative' in colour ? colour : fail();
385
+ });
386
+ return deferredColour && { deferredColour };
387
+ }
388
+ /** Each relative colour function, as `RELATIVE` in the compiler's colour-expression.cjs. */
389
+ const RELATIVE = {
390
+ rgb: { space: 'srgb', keywords: ['r', 'g', 'b'], full: [255, 255, 255] },
391
+ rgba: { space: 'srgb', keywords: ['r', 'g', 'b'], full: [255, 255, 255] },
392
+ hsl: { space: 'hsl', keywords: ['h', 's', 'l'], full: [null, 100, 100] },
393
+ hsla: { space: 'hsl', keywords: ['h', 's', 'l'], full: [null, 100, 100] },
394
+ hwb: { space: 'hwb', keywords: ['h', 'w', 'b'], full: [null, 100, 100] },
395
+ lab: { space: 'lab', keywords: ['l', 'a', 'b'], full: [100, 125, 125] },
396
+ lch: { space: 'lch', keywords: ['l', 'c', 'h'], full: [100, 150, null] },
397
+ oklab: { space: 'oklab', keywords: ['l', 'a', 'b'], full: [1, 0.4, 0.4] },
398
+ oklch: { space: 'oklch', keywords: ['l', 'c', 'h'], full: [1, 0.4, null] },
399
+ };
400
+ /** Whether the next word is `word`, in any case; taken, with the space after it, if so. */
401
+ function takeWord(cursor, word) {
402
+ const start = cursor.at;
403
+ if (match(cursor, WORD)?.[0].toLowerCase() === word) {
404
+ match(cursor, SPACE);
405
+ return true;
406
+ }
407
+ cursor.at = start;
408
+ return false;
409
+ }
410
+ /**
411
+ * The rest of `<space>(from <colour> <channel> <channel> <channel> [/ <alpha>])` once `from` is
412
+ * read, as `relativeExpression` in the compiler reads it.
413
+ */
414
+ function relativeExpression(cursor, { space, keywords, full }) {
415
+ const from = mixColour(cursor);
416
+ const names = [...keywords, 'alpha'];
417
+ const channels = [0, 1, 2].map((index) => {
418
+ match(cursor, SPACE);
419
+ return relativeChannel(cursor, names, full[index]);
420
+ });
421
+ const alpha = take(cursor, '/') ? relativeChannel(cursor, names, 1) : undefined;
422
+ expect(cursor, ')');
423
+ return { relative: { space, from, channels, ...(alpha === undefined ? {} : { alpha }) } };
424
+ }
425
+ /**
426
+ * One channel: a `calc()` of numbers and keywords, a keyword, a number, a percentage of the
427
+ * channel's range, or an angle for a hue.
428
+ */
429
+ function relativeChannel(cursor, keywords, full) {
430
+ const fn = match(cursor, FUNCTION)?.[1]?.toLowerCase();
431
+ if (fn) {
432
+ const value = fn === 'calc' ? channelSum(cursor, keywords) : fail();
433
+ expect(cursor, ')');
434
+ return value;
435
+ }
436
+ const found = match(cursor, LITERAL);
437
+ if (!found)
438
+ return channelOperand(cursor, keywords);
439
+ const value = Number(found[1]) * channelScale((found[2] ?? '').toLowerCase(), full);
440
+ return Number.isFinite(value) ? value : fail();
441
+ }
442
+ /** What one of a written channel's unit is: a share of its range, degrees of a hue, or itself. */
443
+ function channelScale(unit, full) {
444
+ if (unit === '%')
445
+ return full === null ? NaN : full / 100;
446
+ if (full !== null)
447
+ return unit ? NaN : 1;
448
+ return HUE_UNITS.has(unit) ? PER_UNIT[unit] : NaN;
449
+ }
450
+ /** `a + b - c`, each side a product of numbers, keywords and bracketed sums. */
451
+ function channelSum(cursor, keywords) {
452
+ let value = channelProduct(cursor, keywords);
453
+ for (let op; (op = ['+', '-'].find((char) => take(cursor, char)));) {
454
+ value = [op, value, channelProduct(cursor, keywords)];
455
+ }
456
+ return value;
457
+ }
458
+ function channelProduct(cursor, keywords) {
459
+ let value = channelFactor(cursor, keywords);
460
+ for (let op; (op = ['*', '/'].find((char) => take(cursor, char)));) {
461
+ value = [op, value, channelFactor(cursor, keywords)];
462
+ }
463
+ return value;
464
+ }
465
+ function channelFactor(cursor, keywords) {
466
+ if (take(cursor, '(')) {
467
+ const value = channelSum(cursor, keywords);
468
+ expect(cursor, ')');
469
+ return value;
470
+ }
471
+ const found = match(cursor, LITERAL);
472
+ // A percentage or an angle stands alone: inside arithmetic the keywords are numbers.
473
+ if (found)
474
+ return found[2] ? fail() : Number(found[1]);
475
+ return channelOperand(cursor, keywords);
476
+ }
477
+ function channelOperand(cursor, keywords) {
478
+ const word = (match(cursor, WORD) ?? fail())[0];
479
+ return keywords.includes(word) ? word : fail();
480
+ }
481
+ /**
482
+ * A value with a `var()` inside it, or a `color-mix()`, as a token made of others; undefined if it
483
+ * is no such shape.
484
+ */
249
485
  export function derivedToken(text) {
250
- return calcToken(text) ?? colourToken(text);
486
+ return calcToken(text) ?? colourToken(text) ?? mixToken(text);
251
487
  }
252
488
  //# sourceMappingURL=inline-derived-token.js.map