@hegemonart/get-design-done 1.33.6 → 1.34.2

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,206 @@
1
+ /**
2
+ * design-tokens/_native-shared.cjs — shared precision-contract helpers for the
3
+ * native emitters (swift.cjs / compose.cjs / flutter.cjs), Phase 34.1 Plan 01.
4
+ *
5
+ * The single implementation of the PRECISION CONTRACT documented in
6
+ * reference/native-platforms.md, so all three emitters convert color /
7
+ * dimension / typography / non-mappable values identically:
8
+ * COLOR hex #RGB/#RGBA/#RRGGBB/#RRGGBBAA -> 8-bit channels EXACT;
9
+ * #RGB/#RGBA expand by nibble duplication; alpha tracked.
10
+ * DIMENSION Npx | unit-less -> integer (round-half-up) + logical-px double.
11
+ * TYPOGRAPHY strings -> pass-through.
12
+ * NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim, excluded.
13
+ *
14
+ * Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ // Markers embedded in every emitted line so the symmetric re-extractors can
20
+ // recover the EXACT canonical token key (native identifiers are mangled, e.g.
21
+ // `color-primary` -> `colorPrimary`, so the key must travel alongside).
22
+ const TOKEN_MARKER = '// token: ';
23
+ const NONMAPPABLE_MARKER = '// non-mappable: ';
24
+
25
+ const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
26
+ const PX_RE = /^-?\d+(?:\.\d+)?px$/;
27
+ const UNITLESS_RE = /^-?\d+(?:\.\d+)?$/;
28
+ const NONMAPPABLE_RE = /(^var\(|^calc\(|gradient\(|\b(rem|em)$|\d(rem|em)$)/i;
29
+
30
+ /**
31
+ * Read `.tokens` from a Phase-23 TokenSet or a bare {tokens} map.
32
+ * @param {*} tokenSet
33
+ * @returns {Record<string,string>}
34
+ */
35
+ function readTokens(tokenSet) {
36
+ if (
37
+ !tokenSet ||
38
+ typeof tokenSet !== 'object' ||
39
+ !tokenSet.tokens ||
40
+ typeof tokenSet.tokens !== 'object'
41
+ ) {
42
+ throw new TypeError(
43
+ 'native emitter: expected { tokens: Record<string,string> } (or a Phase-23 TokenSet)',
44
+ );
45
+ }
46
+ return tokenSet.tokens;
47
+ }
48
+
49
+ /**
50
+ * Stable, sorted [key, value] entries so emit(x) === emit(x) byte-for-byte.
51
+ * @param {Record<string,string>} tokens
52
+ * @returns {[string,string][]}
53
+ */
54
+ function sortedEntries(tokens) {
55
+ return Object.keys(tokens)
56
+ .sort()
57
+ .map((k) => [k, String(tokens[k])]);
58
+ }
59
+
60
+ /**
61
+ * Classify a token value per the precision contract. The value is the
62
+ * authority; the key prefix is only a hint (see native-platforms.md §2).
63
+ * @param {string} _key
64
+ * @param {string} value
65
+ * @returns {'non-mappable'|'color'|'dimension'|'typography'}
66
+ */
67
+ function classify(_key, value) {
68
+ const v = String(value).trim();
69
+ if (NONMAPPABLE_RE.test(v)) return 'non-mappable';
70
+ if (HEX_RE.test(v)) return 'color';
71
+ if (PX_RE.test(v) || UNITLESS_RE.test(v)) return 'dimension';
72
+ return 'typography';
73
+ }
74
+
75
+ /**
76
+ * Expand #RGB/#RGBA shorthand to #RRGGBB/#RRGGBBAA and split into 8-bit
77
+ * channels. Alpha defaults to opaque (255) when absent; `hadAlpha` records
78
+ * whether the source carried an alpha channel so the re-extractor can emit a
79
+ * 6- or 8-digit hex faithfully.
80
+ * @param {string} hex
81
+ * @returns {{r:number,g:number,b:number,a:number,hadAlpha:boolean}}
82
+ */
83
+ function parseHexChannels(hex) {
84
+ let h = String(hex).trim().replace(/^#/, '');
85
+ let hadAlpha = false;
86
+ if (h.length === 3) {
87
+ h = h.split('').map((c) => c + c).join('');
88
+ } else if (h.length === 4) {
89
+ h = h.split('').map((c) => c + c).join('');
90
+ hadAlpha = true;
91
+ } else if (h.length === 8) {
92
+ hadAlpha = true;
93
+ }
94
+ const r = parseInt(h.slice(0, 2), 16);
95
+ const g = parseInt(h.slice(2, 4), 16);
96
+ const b = parseInt(h.slice(4, 6), 16);
97
+ const a = hadAlpha ? parseInt(h.slice(6, 8), 16) : 255;
98
+ return { r, g, b, a, hadAlpha };
99
+ }
100
+
101
+ /**
102
+ * Recover the canonical hex from 8-bit channels. Emits 6-digit #RRGGBB when
103
+ * the original had no alpha, 8-digit #RRGGBBAA when it did (contract: implied
104
+ * opaque alpha is dropped on the way back so #3B82F6 round-trips exactly).
105
+ * @param {number} r
106
+ * @param {number} g
107
+ * @param {number} b
108
+ * @param {number} a
109
+ * @param {boolean} hadAlpha
110
+ * @returns {string}
111
+ */
112
+ function channelsToHex(r, g, b, a, hadAlpha) {
113
+ const h2 = (n) => (n & 0xff).toString(16).padStart(2, '0');
114
+ const base = `#${h2(r)}${h2(g)}${h2(b)}`;
115
+ return hadAlpha ? `${base}${h2(a)}` : base;
116
+ }
117
+
118
+ /**
119
+ * Pack channels into a Compose/Flutter 0xAARRGGBB literal (uppercase digits).
120
+ * @param {{r:number,g:number,b:number,a:number}} ch
121
+ * @returns {string} e.g. "0xFF3B82F6"
122
+ */
123
+ function channelsToArgbLiteral({ r, g, b, a }) {
124
+ const h2 = (n) => (n & 0xff).toString(16).toUpperCase().padStart(2, '0');
125
+ return `0x${h2(a)}${h2(r)}${h2(g)}${h2(b)}`;
126
+ }
127
+
128
+ /**
129
+ * Strip a 0xAARRGGBB literal back into channels.
130
+ * @param {string} literal e.g. "0xFF3B82F6"
131
+ * @returns {{r:number,g:number,b:number,a:number}}
132
+ */
133
+ function argbLiteralToChannels(literal) {
134
+ const h = String(literal).replace(/^0x/i, '').padStart(8, '0');
135
+ return {
136
+ a: parseInt(h.slice(0, 2), 16),
137
+ r: parseInt(h.slice(2, 4), 16),
138
+ g: parseInt(h.slice(4, 6), 16),
139
+ b: parseInt(h.slice(6, 8), 16),
140
+ };
141
+ }
142
+
143
+ /**
144
+ * px (or unit-less) -> nearest integer, round-half-up (for pt/dp).
145
+ * @param {string} value
146
+ * @returns {number}
147
+ */
148
+ function pxToInt(value) {
149
+ const n = parseFloat(String(value));
150
+ return Math.floor(n + 0.5);
151
+ }
152
+
153
+ /**
154
+ * px (or unit-less) -> logical-px double form string for Dart (`16` -> `16.0`).
155
+ * @param {string} value
156
+ * @returns {string}
157
+ */
158
+ function pxToDouble(value) {
159
+ const n = parseFloat(String(value));
160
+ return Number.isInteger(n) ? `${n}.0` : String(n);
161
+ }
162
+
163
+ /**
164
+ * Recover the canonical `Npx` string from an emitted numeric form.
165
+ * @param {string|number} num
166
+ * @returns {string}
167
+ */
168
+ function numToPx(num) {
169
+ const n = parseFloat(String(num));
170
+ return Number.isInteger(n) ? `${n}px` : `${n}px`;
171
+ }
172
+
173
+ /**
174
+ * dash-case token -> camelCase Swift/Kotlin/Dart identifier.
175
+ * `color-primary` -> `colorPrimary`; leading digit gets a `t` prefix.
176
+ * @param {string} key
177
+ * @returns {string}
178
+ */
179
+ function swiftIdent(key) {
180
+ let id = String(key).replace(/[^A-Za-z0-9]+(.)?/g, (_, c) =>
181
+ c ? c.toUpperCase() : '',
182
+ );
183
+ if (!id) id = 'token';
184
+ if (/^[0-9]/.test(id)) id = 't' + id;
185
+ return id;
186
+ }
187
+
188
+ module.exports = {
189
+ TOKEN_MARKER,
190
+ NONMAPPABLE_MARKER,
191
+ HEX_RE,
192
+ PX_RE,
193
+ UNITLESS_RE,
194
+ NONMAPPABLE_RE,
195
+ readTokens,
196
+ sortedEntries,
197
+ classify,
198
+ parseHexChannels,
199
+ channelsToHex,
200
+ channelsToArgbLiteral,
201
+ argbLiteralToChannels,
202
+ pxToInt,
203
+ pxToDouble,
204
+ numToPx,
205
+ swiftIdent,
206
+ };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * design-tokens/compose.cjs — Jetpack Compose (Kotlin) theme emitter +
3
+ * re-extractor (Phase 34.1 Plan 01).
4
+ *
5
+ * Consumes the canonical Phase-23 token map ({ tokens: Record<string,string> })
6
+ * and emits a deterministic Kotlin theme source string: `Color` vals, a
7
+ * `Shapes` block (radius tokens), a `Typography` block (font/text tokens), and
8
+ * a `MaterialTheme` wiring object — per the PRECISION CONTRACT in
9
+ * reference/native-platforms.md.
10
+ *
11
+ * Precision contract:
12
+ * COLOR hex -> Color(0xAARRGGBB) long. 8-bit channels EXACT; #RGB
13
+ * expands; alpha 0xFF when absent.
14
+ * DIMENSION Npx -> N.dp (integer dp, round-half-up).
15
+ * TYPOGRAPHY strings -> Kotlin string literal, pass-through.
16
+ * NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim comment, EXCLUDED.
17
+ *
18
+ * Each emitted token line carries a `// token: <original-key>` marker so the
19
+ * symmetric `reextractCompose(source)` recovers the exact canonical key+value.
20
+ * Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const {
26
+ TOKEN_MARKER,
27
+ NONMAPPABLE_MARKER,
28
+ readTokens,
29
+ sortedEntries,
30
+ classify,
31
+ parseHexChannels,
32
+ channelsToArgbLiteral,
33
+ argbLiteralToChannels,
34
+ channelsToHex,
35
+ pxToInt,
36
+ swiftIdent,
37
+ } = require('./_native-shared.cjs');
38
+
39
+ const RADIUS_RE = /(^|[^a-z])radius/i;
40
+
41
+ /**
42
+ * Emit a Jetpack Compose / Kotlin theme source string from a token set.
43
+ *
44
+ * @param {{tokens: Record<string,string>}|Record<string,string>} tokenSet
45
+ * @returns {string}
46
+ */
47
+ function emitCompose(tokenSet) {
48
+ const tokens = readTokens(tokenSet);
49
+ const entries = sortedEntries(tokens);
50
+ const lines = [];
51
+ lines.push('// Generated by get-design-done — Jetpack Compose theme tokens.');
52
+ lines.push('// See reference/native-platforms.md (token-bridge precision contract).');
53
+ lines.push('import androidx.compose.ui.graphics.Color');
54
+ lines.push('import androidx.compose.ui.unit.dp');
55
+ lines.push('import androidx.compose.material3.Shapes');
56
+ lines.push('import androidx.compose.material3.Typography');
57
+ lines.push('import androidx.compose.foundation.shape.RoundedCornerShape');
58
+ lines.push('');
59
+ lines.push('object GDDTheme {');
60
+
61
+ // Colors
62
+ for (const [key, value] of entries) {
63
+ if (classify(key, value) !== 'color') continue;
64
+ const { r, g, b, a, hadAlpha } = parseHexChannels(value);
65
+ lines.push(
66
+ ` val ${swiftIdent(key)} = Color(${channelsToArgbLiteral({ r, g, b, a })}) ${TOKEN_MARKER}${key} hadAlpha=${hadAlpha ? 1 : 0}`,
67
+ );
68
+ }
69
+
70
+ // Dimensions (dp)
71
+ for (const [key, value] of entries) {
72
+ if (classify(key, value) !== 'dimension') continue;
73
+ lines.push(
74
+ ` val ${swiftIdent(key)} = ${pxToInt(value)}.dp ${TOKEN_MARKER}${key}`,
75
+ );
76
+ }
77
+
78
+ // Typography (strings)
79
+ for (const [key, value] of entries) {
80
+ if (classify(key, value) !== 'typography') continue;
81
+ lines.push(
82
+ ` val ${swiftIdent(key)} = ${JSON.stringify(value)} ${TOKEN_MARKER}${key}`,
83
+ );
84
+ }
85
+
86
+ // Non-mappable — verbatim, excluded from identity set
87
+ for (const [key, value] of entries) {
88
+ if (classify(key, value) !== 'non-mappable') continue;
89
+ lines.push(` ${NONMAPPABLE_MARKER}${key} = ${value}`);
90
+ }
91
+
92
+ // Shapes block (from radius dimension tokens) — wires dp into RoundedCornerShape
93
+ lines.push('');
94
+ lines.push(' val Shapes = Shapes(');
95
+ for (const [key, value] of entries) {
96
+ if (classify(key, value) !== 'dimension') continue;
97
+ if (!RADIUS_RE.test(key)) continue;
98
+ lines.push(` // ${key}: RoundedCornerShape(${pxToInt(value)}.dp)`);
99
+ }
100
+ lines.push(' )');
101
+
102
+ // Typography block (MaterialTheme wiring) — references the font tokens above
103
+ lines.push(' val Typography = Typography()');
104
+ lines.push('}');
105
+ lines.push('');
106
+ return lines.join('\n');
107
+ }
108
+
109
+ const COMPOSE_COLOR_RE = new RegExp(
110
+ String.raw`val \w+ = Color\((0x[0-9a-fA-F]{8})\)\s*` +
111
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
112
+ String.raw`(\S+)\s+hadAlpha=([01])`,
113
+ );
114
+ const COMPOSE_DIM_RE = new RegExp(
115
+ String.raw`val \w+ = (\d+)\.dp\s*` +
116
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
117
+ String.raw`(\S+)`,
118
+ );
119
+ const COMPOSE_STR_RE = new RegExp(
120
+ String.raw`val \w+ = ("(?:[^"\\]|\\.)*")\s*` +
121
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
122
+ String.raw`(\S+)`,
123
+ );
124
+
125
+ /**
126
+ * Recover the canonical token map from an emitted Compose source string.
127
+ * Non-mappable lines and the Shapes-comment lines are NOT recovered.
128
+ *
129
+ * @param {string} source
130
+ * @returns {{tokens: Record<string,string>}}
131
+ */
132
+ function reextractCompose(source) {
133
+ /** @type {Record<string,string>} */
134
+ const tokens = {};
135
+ for (const line of String(source).split(/\r?\n/)) {
136
+ if (line.includes(NONMAPPABLE_MARKER)) continue;
137
+ let m;
138
+ if ((m = COMPOSE_COLOR_RE.exec(line))) {
139
+ const ch = argbLiteralToChannels(m[1]);
140
+ tokens[m[2]] = channelsToHex(ch.r, ch.g, ch.b, ch.a, m[3] === '1');
141
+ } else if ((m = COMPOSE_DIM_RE.exec(line))) {
142
+ tokens[m[2]] = `${m[1]}px`;
143
+ } else if ((m = COMPOSE_STR_RE.exec(line))) {
144
+ tokens[m[2]] = JSON.parse(m[1]);
145
+ }
146
+ }
147
+ return { tokens };
148
+ }
149
+
150
+ module.exports = { emitCompose, reextractCompose };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * design-tokens/flutter.cjs — Flutter (Dart) theme emitter + re-extractor
3
+ * (Phase 34.1 Plan 01).
4
+ *
5
+ * Consumes the canonical Phase-23 token map ({ tokens: Record<string,string> })
6
+ * and emits a deterministic Dart theme source string: a `GDDTheme` constants
7
+ * class plus a `themeData()` builder wiring `ColorScheme` + `TextTheme` into a
8
+ * `ThemeData` — per the PRECISION CONTRACT in reference/native-platforms.md.
9
+ *
10
+ * Precision contract:
11
+ * COLOR hex -> Color(0xAARRGGBB). 8-bit channels EXACT; #RGB expands;
12
+ * alpha 0xFF when absent.
13
+ * DIMENSION Npx -> logical-px double (`16` -> `16.0`); NOT rounded.
14
+ * TYPOGRAPHY strings -> Dart string literal, pass-through.
15
+ * NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim comment, EXCLUDED.
16
+ *
17
+ * Each emitted token line carries a `// token: <original-key>` marker so the
18
+ * symmetric `reextractFlutter(source)` recovers the exact canonical key+value.
19
+ * Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const {
25
+ TOKEN_MARKER,
26
+ NONMAPPABLE_MARKER,
27
+ readTokens,
28
+ sortedEntries,
29
+ classify,
30
+ parseHexChannels,
31
+ channelsToArgbLiteral,
32
+ argbLiteralToChannels,
33
+ channelsToHex,
34
+ pxToDouble,
35
+ swiftIdent,
36
+ } = require('./_native-shared.cjs');
37
+
38
+ /**
39
+ * Emit a Flutter / Dart theme source string from a token set.
40
+ *
41
+ * @param {{tokens: Record<string,string>}|Record<string,string>} tokenSet
42
+ * @returns {string}
43
+ */
44
+ function emitFlutter(tokenSet) {
45
+ const tokens = readTokens(tokenSet);
46
+ const entries = sortedEntries(tokens);
47
+ const lines = [];
48
+ lines.push('// Generated by get-design-done — Flutter theme tokens.');
49
+ lines.push('// See reference/native-platforms.md (token-bridge precision contract).');
50
+ lines.push("import 'package:flutter/material.dart';");
51
+ lines.push('');
52
+ lines.push('class GDDTheme {');
53
+
54
+ for (const [key, value] of entries) {
55
+ const kind = classify(key, value);
56
+ const ident = swiftIdent(key);
57
+ if (kind === 'color') {
58
+ const { r, g, b, a, hadAlpha } = parseHexChannels(value);
59
+ lines.push(
60
+ ` static const ${ident} = Color(${channelsToArgbLiteral({ r, g, b, a })}); ${TOKEN_MARKER}${key} hadAlpha=${hadAlpha ? 1 : 0}`,
61
+ );
62
+ } else if (kind === 'dimension') {
63
+ lines.push(
64
+ ` static const ${ident} = ${pxToDouble(value)}; ${TOKEN_MARKER}${key}`,
65
+ );
66
+ } else if (kind === 'typography') {
67
+ lines.push(
68
+ ` static const ${ident} = ${JSON.stringify(value)}; ${TOKEN_MARKER}${key}`,
69
+ );
70
+ } else {
71
+ lines.push(` ${NONMAPPABLE_MARKER}${key} = ${value}`);
72
+ }
73
+ }
74
+
75
+ // ThemeData builder wiring ColorScheme + TextTheme (MaterialTheme analog).
76
+ lines.push('');
77
+ lines.push(' static ThemeData themeData() => ThemeData(');
78
+ lines.push(' colorScheme: ColorScheme.fromSeed(seedColor: GDDTheme.colorPrimary),');
79
+ lines.push(' textTheme: const TextTheme(),');
80
+ lines.push(' );');
81
+ lines.push('}');
82
+ lines.push('');
83
+ return lines.join('\n');
84
+ }
85
+
86
+ const FLUTTER_COLOR_RE = new RegExp(
87
+ String.raw`static const \w+ = Color\((0x[0-9a-fA-F]{8})\);\s*` +
88
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
89
+ String.raw`(\S+)\s+hadAlpha=([01])`,
90
+ );
91
+ const FLUTTER_DIM_RE = new RegExp(
92
+ String.raw`static const \w+ = (\d+(?:\.\d+)?);\s*` +
93
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
94
+ String.raw`(\S+)`,
95
+ );
96
+ const FLUTTER_STR_RE = new RegExp(
97
+ String.raw`static const \w+ = ("(?:[^"\\]|\\.)*");\s*` +
98
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
99
+ String.raw`(\S+)`,
100
+ );
101
+
102
+ /**
103
+ * Recover the canonical token map from an emitted Flutter source string.
104
+ * Non-mappable lines + the ThemeData builder lines are NOT recovered.
105
+ *
106
+ * @param {string} source
107
+ * @returns {{tokens: Record<string,string>}}
108
+ */
109
+ function reextractFlutter(source) {
110
+ /** @type {Record<string,string>} */
111
+ const tokens = {};
112
+ for (const line of String(source).split(/\r?\n/)) {
113
+ if (line.includes(NONMAPPABLE_MARKER)) continue;
114
+ let m;
115
+ if ((m = FLUTTER_COLOR_RE.exec(line))) {
116
+ const ch = argbLiteralToChannels(m[1]);
117
+ tokens[m[2]] = channelsToHex(ch.r, ch.g, ch.b, ch.a, m[3] === '1');
118
+ } else if ((m = FLUTTER_DIM_RE.exec(line))) {
119
+ // Recover canonical Npx: a logical-px double `16.0` -> `16px`.
120
+ tokens[m[2]] = `${parseFloat(m[1])}px`;
121
+ } else if ((m = FLUTTER_STR_RE.exec(line))) {
122
+ tokens[m[2]] = JSON.parse(m[1]);
123
+ }
124
+ }
125
+ return { tokens };
126
+ }
127
+
128
+ module.exports = { emitFlutter, reextractFlutter };
@@ -17,6 +17,12 @@ const { readJsConst } = require('./js-const.cjs');
17
17
  const { readTailwind } = require('./tailwind.cjs');
18
18
  const { readFigma } = require('./figma.cjs');
19
19
 
20
+ // Phase 34.1 — native theme emitters (token-bridge, D-02: extend the engine,
21
+ // do not fork a native IR). See reference/native-platforms.md.
22
+ const { emitSwift, reextractSwift } = require('./swift.cjs');
23
+ const { emitCompose, reextractCompose } = require('./compose.cjs');
24
+ const { emitFlutter, reextractFlutter } = require('./flutter.cjs');
25
+
20
26
  /**
21
27
  * @typedef {Object} TokenSet
22
28
  * @property {Object<string, string>} tokens
@@ -97,4 +103,11 @@ module.exports = {
97
103
  readJsConst,
98
104
  readTailwind,
99
105
  readFigma,
106
+ // Phase 34.1 native token-bridge emitters + symmetric re-extractors.
107
+ emitSwift,
108
+ emitCompose,
109
+ emitFlutter,
110
+ reextractSwift,
111
+ reextractCompose,
112
+ reextractFlutter,
100
113
  };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * design-tokens/swift.cjs — SwiftUI theme emitter + re-extractor
3
+ * (Phase 34.1 Plan 01).
4
+ *
5
+ * Consumes the canonical Phase-23 token map ({ tokens: Record<string,string> })
6
+ * and emits a deterministic SwiftUI theme source string: an `enum GDDTheme`
7
+ * exposing static `Color` / `CGFloat` (pt) / `String` constants per the
8
+ * PRECISION CONTRACT in reference/native-platforms.md.
9
+ *
10
+ * Precision contract (the round-trip's definition of "identity preserved"):
11
+ * COLOR hex #RGB/#RRGGBB/#RRGGBBAA -> Color(red:G/255 ...). 8-bit
12
+ * channels EXACT (numerator-over-255 form avoids float drift);
13
+ * #RGB expands to #RRGGBB; alpha opaque (255) when absent.
14
+ * DIMENSION Npx -> CGFloat integer points (round-half-up).
15
+ * TYPOGRAPHY font-family / strings -> String literal, pass-through.
16
+ * NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim trailing comment,
17
+ * EXCLUDED from the round-trip identity set.
18
+ *
19
+ * Each emitted token line carries a `// token: <original-key>` marker so the
20
+ * symmetric `reextractSwift(source)` recovers the exact canonical key + value.
21
+ * Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const {
27
+ TOKEN_MARKER,
28
+ NONMAPPABLE_MARKER,
29
+ readTokens,
30
+ sortedEntries,
31
+ classify,
32
+ parseHexChannels,
33
+ channelsToHex,
34
+ pxToInt,
35
+ swiftIdent,
36
+ } = require('./_native-shared.cjs');
37
+
38
+ /**
39
+ * Emit a SwiftUI theme source string from a token set.
40
+ *
41
+ * @param {{tokens: Record<string,string>}|Record<string,string>} tokenSet
42
+ * @returns {string}
43
+ */
44
+ function emitSwift(tokenSet) {
45
+ const tokens = readTokens(tokenSet);
46
+ const lines = [];
47
+ lines.push('// Generated by get-design-done — SwiftUI theme tokens.');
48
+ lines.push('// See reference/native-platforms.md (token-bridge precision contract).');
49
+ lines.push('import SwiftUI');
50
+ lines.push('');
51
+ lines.push('enum GDDTheme {');
52
+ for (const [key, value] of sortedEntries(tokens)) {
53
+ const ident = swiftIdent(key);
54
+ const kind = classify(key, value);
55
+ if (kind === 'color') {
56
+ const { r, g, b, a, hadAlpha } = parseHexChannels(value);
57
+ const alphaSlot = hadAlpha
58
+ ? `, opacity: ${a}.0/255.0`
59
+ : `, opacity: 255.0/255.0`;
60
+ lines.push(
61
+ ` static let ${ident} = Color(red: ${r}.0/255.0, green: ${g}.0/255.0, blue: ${b}.0/255.0${alphaSlot}) ${TOKEN_MARKER}${key} hadAlpha=${hadAlpha ? 1 : 0}`,
62
+ );
63
+ } else if (kind === 'dimension') {
64
+ lines.push(
65
+ ` static let ${ident}: CGFloat = ${pxToInt(value)} ${TOKEN_MARKER}${key}`,
66
+ );
67
+ } else if (kind === 'typography') {
68
+ lines.push(
69
+ ` static let ${ident} = ${JSON.stringify(value)} ${TOKEN_MARKER}${key}`,
70
+ );
71
+ } else {
72
+ // non-mappable — verbatim, excluded from identity set
73
+ lines.push(` ${NONMAPPABLE_MARKER}${key} = ${value}`);
74
+ }
75
+ }
76
+ lines.push('}');
77
+ lines.push('');
78
+ return lines.join('\n');
79
+ }
80
+
81
+ const SWIFT_COLOR_RE = new RegExp(
82
+ String.raw`Color\(red:\s*(\d+)\.0/255\.0,\s*green:\s*(\d+)\.0/255\.0,\s*blue:\s*(\d+)\.0/255\.0,\s*opacity:\s*(\d+)\.0/255\.0\)\s*` +
83
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
84
+ String.raw`(\S+)\s+hadAlpha=([01])`,
85
+ );
86
+ const SWIFT_DIM_RE = new RegExp(
87
+ String.raw`:\s*CGFloat\s*=\s*(\d+)\s*` +
88
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
89
+ String.raw`(\S+)`,
90
+ );
91
+ const SWIFT_STR_RE = new RegExp(
92
+ String.raw`static let \w+ = ("(?:[^"\\]|\\.)*")\s*` +
93
+ TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
94
+ String.raw`(\S+)`,
95
+ );
96
+
97
+ /**
98
+ * Recover the canonical token map from an emitted SwiftUI source string.
99
+ * Non-mappable lines (NONMAPPABLE_MARKER) are intentionally NOT recovered.
100
+ *
101
+ * @param {string} source
102
+ * @returns {{tokens: Record<string,string>}}
103
+ */
104
+ function reextractSwift(source) {
105
+ /** @type {Record<string,string>} */
106
+ const tokens = {};
107
+ for (const line of String(source).split(/\r?\n/)) {
108
+ if (line.includes(NONMAPPABLE_MARKER)) continue;
109
+ let m;
110
+ if ((m = SWIFT_COLOR_RE.exec(line))) {
111
+ const [, r, g, b, a, key, hadAlpha] = m;
112
+ tokens[key] = channelsToHex(+r, +g, +b, +a, hadAlpha === '1');
113
+ } else if ((m = SWIFT_DIM_RE.exec(line))) {
114
+ tokens[m[2]] = `${m[1]}px`;
115
+ } else if ((m = SWIFT_STR_RE.exec(line))) {
116
+ tokens[m[2]] = JSON.parse(m[1]);
117
+ }
118
+ }
119
+ return { tokens };
120
+ }
121
+
122
+ module.exports = { emitSwift, reextractSwift };