@nowline/renderer 0.0.0-dev.20260601071750.g04bdff9

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.
@@ -0,0 +1,54 @@
1
+ // Minimal, dependency-free XML string helpers. Every output SVG string runs
2
+ // through here; characters are escaped consistently so identical inputs
3
+ // produce identical bytes.
4
+ const AMP = /&/g;
5
+ const LT = /</g;
6
+ const GT = />/g;
7
+ const QUOTE = /"/g;
8
+ const APOS = /'/g;
9
+ export function escText(value) {
10
+ return value.replace(AMP, '&amp;').replace(LT, '&lt;').replace(GT, '&gt;');
11
+ }
12
+ export function escAttr(value) {
13
+ return value
14
+ .replace(AMP, '&amp;')
15
+ .replace(LT, '&lt;')
16
+ .replace(GT, '&gt;')
17
+ .replace(QUOTE, '&quot;')
18
+ .replace(APOS, '&#39;');
19
+ }
20
+ export function attrs(values) {
21
+ const keys = Object.keys(values).sort();
22
+ const parts = [];
23
+ for (const key of keys) {
24
+ const v = values[key];
25
+ if (v === null || v === undefined || v === false)
26
+ continue;
27
+ if (v === true) {
28
+ parts.push(key);
29
+ }
30
+ else {
31
+ parts.push(`${key}="${escAttr(String(v))}"`);
32
+ }
33
+ }
34
+ return parts.length ? ` ${parts.join(' ')}` : '';
35
+ }
36
+ export function tag(name, attributes, inner) {
37
+ if (inner === undefined || inner === '') {
38
+ return `<${name}${attrs(attributes)}/>`;
39
+ }
40
+ return `<${name}${attrs(attributes)}>${inner}</${name}>`;
41
+ }
42
+ export function textTag(attributes, content) {
43
+ return `<text${attrs(attributes)}>${escText(content)}</text>`;
44
+ }
45
+ // Fixed-precision number formatter — avoids locale-dependent toFixed quirks
46
+ // and guarantees deterministic output across Node versions.
47
+ export function num(n) {
48
+ if (!Number.isFinite(n))
49
+ return '0';
50
+ if (Number.isInteger(n))
51
+ return n.toString();
52
+ return (Math.round(n * 100) / 100).toString();
53
+ }
54
+ //# sourceMappingURL=xml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xml.js","sourceRoot":"","sources":["../../src/svg/xml.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,wEAAwE;AACxE,2BAA2B;AAE3B,MAAM,GAAG,GAAG,IAAI,CAAC;AACjB,MAAM,EAAE,GAAG,IAAI,CAAC;AAChB,MAAM,EAAE,GAAG,IAAI,CAAC;AAChB,MAAM,KAAK,GAAG,IAAI,CAAC;AACnB,MAAM,IAAI,GAAG,IAAI,CAAC;AAElB,MAAM,UAAU,OAAO,CAAC,KAAa;IACjC,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAa;IACjC,OAAO,KAAK;SACP,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC;SACrB,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC;SACnB,OAAO,CAAC,EAAE,EAAE,MAAM,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;SACxB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAChC,CAAC;AAID,MAAM,UAAU,KAAK,CAAC,MAAiC;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,KAAK;YAAE,SAAS;QAC3D,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YACb,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;aAAM,CAAC;YACJ,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,UAAqC,EAAE,KAAc;IACnF,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACtC,OAAO,IAAI,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;IAC5C,CAAC;IACD,OAAO,IAAI,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,KAAK,KAAK,IAAI,GAAG,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,UAAqC,EAAE,OAAe;IAC1E,OAAO,QAAQ,KAAK,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;AAClE,CAAC;AAED,4EAA4E;AAC5E,4DAA4D;AAC5D,MAAM,UAAU,GAAG,CAAC,CAAS;IACzB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;AAClD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@nowline/renderer",
3
+ "version": "0.0.0-dev.20260601071750.g04bdff9",
4
+ "description": "Nowline SVG renderer — positioned model → SVG string",
5
+ "license": "Apache-2.0",
6
+ "engines": {
7
+ "node": ">=22",
8
+ "pnpm": ">=11"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist/",
22
+ "src/"
23
+ ],
24
+ "dependencies": {
25
+ "@nowline/layout": "0.0.0-dev.20260601071750.g04bdff9"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.9.1",
29
+ "langium": "~4.2.4",
30
+ "typescript": "^6.0.3",
31
+ "vitest": "^4.1.7",
32
+ "@nowline/core": "0.0.0-dev.20260601071750.g04bdff9"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -b tsconfig.json",
36
+ "watch": "tsc -b tsconfig.json --watch",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest"
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type {
2
+ AssetBytes,
3
+ AssetResolver,
4
+ RenderOptions,
5
+ } from './svg/render.js';
6
+ export { renderSvg } from './svg/render.js';
7
+ export { sanitizeSvg } from './svg/sanitize.js';
@@ -0,0 +1,153 @@
1
+ // Built-in link icon paths. All drawn to a 16x16 box; renderer positions them.
2
+
3
+ export const LINK_ICON_PATHS: Record<string, string> = {
4
+ // Generic link/chain icon.
5
+ generic:
6
+ 'M7 3h2a4 4 0 0 1 0 8H7a4 4 0 0 1 0-8Zm0 2a2 2 0 1 0 0 4h2a2 2 0 1 0 0-4H7Zm6 0h2a4 4 0 0 1 0 8h-2a4 4 0 0 1 0-8Z',
7
+ // Abstract Linear triangle.
8
+ linear: 'M2 8l6-6 6 6-6 6Z',
9
+ // GitHub octocat silhouette (simplified).
10
+ github: 'M8 1C4.14 1 1 4.14 1 8c0 3.1 2 5.7 4.8 6.6.35.07.48-.15.48-.34v-1.2c-1.95.43-2.36-.83-2.36-.83-.32-.82-.78-1.04-.78-1.04-.63-.43.05-.42.05-.42.71.05 1.08.73 1.08.73.62 1.06 1.63.75 2.03.57.06-.45.24-.75.44-.92-1.56-.18-3.2-.78-3.2-3.48 0-.77.27-1.4.72-1.9-.07-.18-.31-.9.07-1.88 0 0 .59-.19 1.93.72.56-.16 1.16-.24 1.76-.24.6 0 1.2.08 1.76.24 1.34-.9 1.93-.72 1.93-.72.38.98.14 1.7.07 1.88.45.5.72 1.13.72 1.9 0 2.7-1.65 3.3-3.22 3.48.25.21.47.63.47 1.27v1.88c0 .19.13.41.49.34C13 13.7 15 11.1 15 8c0-3.86-3.14-7-7-7Z',
11
+ // Jira-ish blue diamond.
12
+ jira: 'M8 1 1 8l7 7 7-7Zm0 3.5L11.5 8 8 11.5 4.5 8Z',
13
+ };
14
+
15
+ // --- Curated built-in icon library ---
16
+ //
17
+ // Named glyphs for the `capacity-icon:` style property, the `icon:` style
18
+ // property, and renderer-internal vocabulary like the inline-date pin glyph
19
+ // (`calendar`). Each glyph is a small inline SVG fragment drawn on a 24x24
20
+ // viewBox using `currentColor` so the renderer can paint it in the resolved
21
+ // entity text color and at any pixel size by setting the wrapping `<svg>`
22
+ // element's `width` / `height`.
23
+ //
24
+ // Adapted from Lucide (https://lucide.dev) under the ISC License — paths are
25
+ // transcribed verbatim; only the wrapping `<svg>` element differs (we set
26
+ // width / height at render time and bind colors to currentColor).
27
+ //
28
+ // Why a curated SVG library instead of Unicode glyphs? Per spec, built-in
29
+ // capacity-icon names render identically across every output platform (web,
30
+ // CLI export, embedded SVG, etc.). Unicode emoji (`👤`, `⏱`) render in
31
+ // platform-specific fonts (Apple, Google, Microsoft, Linux), so the same DSL
32
+ // would produce visually inconsistent output. SVG paths are pixel-deterministic.
33
+ // Authors who *want* the host-platform emoji can use an inline literal
34
+ // (`capacity-icon:"👤"`) or declare a custom symbol via the `symbol` keyword.
35
+ //
36
+ // `multiplier` is intentionally absent from this map: U+00D7 MULTIPLICATION
37
+ // SIGN is a stable typographic operator with consistent rendering across every
38
+ // system font, so it renders as a `<text>` element instead of an SVG path.
39
+ export interface CapacityIconSvg {
40
+ /** SVG viewBox attribute. All glyphs are normalized to a 24x24 box. */
41
+ viewBox: string;
42
+ /**
43
+ * Inline SVG fragment — paths/circles/lines using `currentColor` for stroke
44
+ * and `none` (or `currentColor`) for fill. The renderer wraps this in an
45
+ * `<svg>` element whose `width`/`height` set the rendered size and whose
46
+ * `color` (or an enclosing `text`/`g` color) drives the glyph color.
47
+ */
48
+ body: string;
49
+ /** Visible 1-3 ASCII character fallback used when SVG output is constrained
50
+ * to ASCII (e.g. terminal text-mode export). Must match the `Built-in glyph
51
+ * table` in specs/rendering.md. */
52
+ ascii: string;
53
+ }
54
+
55
+ export const CAPACITY_ICON_SVG: Record<string, CapacityIconSvg> = {
56
+ // Lucide `user` — a single figure (head + shoulders).
57
+ person: {
58
+ viewBox: '0 0 24 24',
59
+ body:
60
+ '<circle cx="12" cy="8" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
61
+ '<path d="M4 21a8 8 0 0 1 16 0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
62
+ ascii: 'p',
63
+ },
64
+ // Lucide `users` — paired figures (foreground + smaller silhouette behind).
65
+ people: {
66
+ viewBox: '0 0 24 24',
67
+ body:
68
+ '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
69
+ '<circle cx="9" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
70
+ '<path d="M22 21v-2a4 4 0 0 0-3-3.87" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
71
+ '<path d="M16 3.13a4 4 0 0 1 0 7.75" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
72
+ ascii: 'P',
73
+ },
74
+ // Lucide `star` — five-pointed star, filled with currentColor for visual weight.
75
+ points: {
76
+ viewBox: '0 0 24 24',
77
+ body: '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linejoin="round"/>',
78
+ ascii: '*',
79
+ },
80
+ // Lucide `timer` — stopwatch silhouette (face circle, top crown line, hand).
81
+ time: {
82
+ viewBox: '0 0 24 24',
83
+ body:
84
+ '<line x1="10" x2="14" y1="2" y2="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
85
+ '<line x1="12" x2="15" y1="14" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
86
+ '<circle cx="12" cy="14" r="8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
87
+ ascii: 't',
88
+ },
89
+ };
90
+
91
+ // --- Renderer-internal built-in icons ---
92
+ //
93
+ // Glyphs from the curated library that the renderer reaches for outside the
94
+ // `capacity-icon:` path. Currently just `calendar`, used by the inline-date
95
+ // pin painter (`after:DATE` / `before:DATE` on items, parallels, and groups
96
+ // — see specs/rendering.md "Inline-date glyph"). The validator reserves
97
+ // `calendar` as a built-in name so authors can't shadow it via a `symbol`
98
+ // declaration (rule 17i).
99
+ //
100
+ // `BUILTIN_ICON_SVG` re-exports the capacity-icon entries plus these renderer-
101
+ // internal glyphs so callers walking the curated library by name don't have to
102
+ // know which path each glyph belongs to. The capacity-icon contract
103
+ // (`CAPACITY_ICON_SVG` exposes exactly the four `capacity-icon:` glyphs) stays
104
+ // intact for the m6/m7 sites that depend on it.
105
+
106
+ const RENDERER_BUILTIN_ICON_SVG: Record<string, CapacityIconSvg> = {
107
+ // Lucide `calendar` — month grid (rounded rectangle body, two top tabs
108
+ // for hanger pegs, a horizontal divider for the day-row separator).
109
+ calendar: {
110
+ viewBox: '0 0 24 24',
111
+ body:
112
+ '<rect x="3" y="4" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
113
+ '<line x1="16" x2="16" y1="2" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
114
+ '<line x1="8" x2="8" y1="2" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
115
+ '<line x1="3" x2="21" y1="10" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
116
+ ascii: 'd',
117
+ },
118
+ };
119
+
120
+ /**
121
+ * Union of every built-in glyph the renderer can paint by name: the
122
+ * `capacity-icon:` set plus renderer-internal glyphs (currently `calendar`).
123
+ * Use this when you need any built-in by name; use `CAPACITY_ICON_SVG` when
124
+ * you specifically want the `capacity-icon:` subset.
125
+ */
126
+ export const BUILTIN_ICON_SVG: Record<string, CapacityIconSvg> = {
127
+ ...CAPACITY_ICON_SVG,
128
+ ...RENDERER_BUILTIN_ICON_SVG,
129
+ };
130
+
131
+ /** Returns true when `name` is a renderer-curated capacity glyph. Useful for
132
+ * differentiating built-ins from custom glyph declarations and inline literals
133
+ * in the upcoming layout/render passes (m6/m7). */
134
+ export function hasCapacityIconSvg(name: string): boolean {
135
+ return Object.hasOwn(CAPACITY_ICON_SVG, name);
136
+ }
137
+
138
+ /** ASCII fallbacks for every built-in capacity-icon name, including the ones
139
+ * that render as text (`multiplier`) or render nothing (`none`). The spec's
140
+ * `Built-in glyph table` is the source of truth — keep these in sync.
141
+ *
142
+ * `calendar` is intentionally absent: it is a renderer-internal glyph for
143
+ * inline-date pins (not a `capacity-icon:` value), so it has no role in
144
+ * capacity-suffix ASCII fallback.
145
+ */
146
+ export const CAPACITY_ICON_ASCII: Record<string, string> = {
147
+ none: '',
148
+ multiplier: 'x',
149
+ person: CAPACITY_ICON_SVG.person.ascii,
150
+ people: CAPACITY_ICON_SVG.people.ascii,
151
+ points: CAPACITY_ICON_SVG.points.ascii,
152
+ time: CAPACITY_ICON_SVG.time.ascii,
153
+ };
package/src/svg/ids.ts ADDED
@@ -0,0 +1,12 @@
1
+ // Deterministic id generator. A single counter per renderSvg() call yields
2
+ // ids like `nl-0`, `nl-1`, ... so identical inputs emit identical SVGs.
3
+ // Never uses Math.random, Date.now, or ambient state.
4
+
5
+ export class IdGenerator {
6
+ private counter = 0;
7
+ constructor(private readonly prefix: string = 'nl') {}
8
+ next(label?: string): string {
9
+ const id = `${this.prefix}-${this.counter++}`;
10
+ return label ? `${id}-${label}` : id;
11
+ }
12
+ }