@nowline/renderer 0.2.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.
@@ -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,35 @@
1
+ {
2
+ "name": "@nowline/renderer",
3
+ "version": "0.2.0",
4
+ "description": "Nowline SVG renderer — positioned model → SVG string",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "src/"
18
+ ],
19
+ "dependencies": {
20
+ "@nowline/layout": "0.2.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "langium": "~4.2.2",
25
+ "typescript": "~5.7.0",
26
+ "vitest": "^3.1.0",
27
+ "@nowline/core": "0.2.0"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -b tsconfig.json",
31
+ "watch": "tsc -b tsconfig.json --watch",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest"
34
+ }
35
+ }
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,107 @@
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
+ // --- Capacity-icon library ---
16
+ //
17
+ // Built-in named glyphs for the `capacity-icon:` style property and (where
18
+ // reused) the `icon:` style property. Each glyph is a small inline SVG fragment
19
+ // drawn on a 24x24 viewBox using `currentColor` so the renderer can paint it in
20
+ // the resolved entity text color and at any pixel size by setting the wrapping
21
+ // `<svg>` element's `width` / `height`.
22
+ //
23
+ // Adapted from Lucide (https://lucide.dev) under the ISC License — paths are
24
+ // transcribed verbatim; only the wrapping `<svg>` element differs (we set
25
+ // width / height at render time and bind colors to currentColor).
26
+ //
27
+ // Why a curated SVG library instead of Unicode glyphs? Per spec, built-in
28
+ // capacity-icon names render identically across every output platform (web,
29
+ // CLI export, embedded SVG, etc.). Unicode emoji (`👤`, `⏱`) render in
30
+ // platform-specific fonts (Apple, Google, Microsoft, Linux), so the same DSL
31
+ // would produce visually inconsistent output. SVG paths are pixel-deterministic.
32
+ // Authors who *want* the host-platform emoji can use an inline literal
33
+ // (`capacity-icon:"👤"`) or declare a custom symbol via the `symbol` keyword.
34
+ //
35
+ // `multiplier` is intentionally absent from this map: U+00D7 MULTIPLICATION
36
+ // SIGN is a stable typographic operator with consistent rendering across every
37
+ // system font, so it renders as a `<text>` element instead of an SVG path.
38
+ export interface CapacityIconSvg {
39
+ /** SVG viewBox attribute. All glyphs are normalized to a 24x24 box. */
40
+ viewBox: string;
41
+ /**
42
+ * Inline SVG fragment — paths/circles/lines using `currentColor` for stroke
43
+ * and `none` (or `currentColor`) for fill. The renderer wraps this in an
44
+ * `<svg>` element whose `width`/`height` set the rendered size and whose
45
+ * `color` (or an enclosing `text`/`g` color) drives the glyph color.
46
+ */
47
+ body: string;
48
+ /** Visible 1-3 ASCII character fallback used when SVG output is constrained
49
+ * to ASCII (e.g. terminal text-mode export). Must match the `Built-in glyph
50
+ * table` in specs/rendering.md. */
51
+ ascii: string;
52
+ }
53
+
54
+ export const CAPACITY_ICON_SVG: Record<string, CapacityIconSvg> = {
55
+ // Lucide `user` — a single figure (head + shoulders).
56
+ person: {
57
+ viewBox: '0 0 24 24',
58
+ body:
59
+ '<circle cx="12" cy="8" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
60
+ '<path d="M4 21a8 8 0 0 1 16 0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
61
+ ascii: 'p',
62
+ },
63
+ // Lucide `users` — paired figures (foreground + smaller silhouette behind).
64
+ people: {
65
+ viewBox: '0 0 24 24',
66
+ body:
67
+ '<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"/>' +
68
+ '<circle cx="9" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
69
+ '<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"/>' +
70
+ '<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"/>',
71
+ ascii: 'P',
72
+ },
73
+ // Lucide `star` — five-pointed star, filled with currentColor for visual weight.
74
+ points: {
75
+ viewBox: '0 0 24 24',
76
+ 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"/>',
77
+ ascii: '*',
78
+ },
79
+ // Lucide `timer` — stopwatch silhouette (face circle, top crown line, hand).
80
+ time: {
81
+ viewBox: '0 0 24 24',
82
+ body:
83
+ '<line x1="10" x2="14" y1="2" y2="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
84
+ '<line x1="12" x2="15" y1="14" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
85
+ '<circle cx="12" cy="14" r="8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
86
+ ascii: 't',
87
+ },
88
+ };
89
+
90
+ /** Returns true when `name` is a renderer-curated capacity glyph. Useful for
91
+ * differentiating built-ins from custom glyph declarations and inline literals
92
+ * in the upcoming layout/render passes (m6/m7). */
93
+ export function hasCapacityIconSvg(name: string): boolean {
94
+ return Object.hasOwn(CAPACITY_ICON_SVG, name);
95
+ }
96
+
97
+ /** ASCII fallbacks for every built-in capacity-icon name, including the ones
98
+ * that render as text (`multiplier`) or render nothing (`none`). The spec's
99
+ * `Built-in glyph table` is the source of truth — keep these in sync. */
100
+ export const CAPACITY_ICON_ASCII: Record<string, string> = {
101
+ none: '',
102
+ multiplier: 'x',
103
+ person: CAPACITY_ICON_SVG.person.ascii,
104
+ people: CAPACITY_ICON_SVG.people.ascii,
105
+ points: CAPACITY_ICON_SVG.points.ascii,
106
+ time: CAPACITY_ICON_SVG.time.ascii,
107
+ };
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
+ }