@ohos-ports/slides-grab 1.5.0-beta.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README-ko.md +292 -0
  3. package/README.md +301 -0
  4. package/bin/ohos-postinstall.cjs +118 -0
  5. package/bin/ppt-agent.js +620 -0
  6. package/convert.cjs +20 -0
  7. package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
  8. package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
  9. package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
  10. package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
  11. package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
  12. package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
  13. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
  14. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
  15. package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
  16. package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
  17. package/native/@img/sharp-openharmony-arm64/package.json +16 -0
  18. package/package.json +95 -0
  19. package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
  20. package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
  21. package/scripts/build-viewer.js +444 -0
  22. package/scripts/design-gate.js +258 -0
  23. package/scripts/download-video.js +213 -0
  24. package/scripts/editor-server.js +1167 -0
  25. package/scripts/figma-export.js +169 -0
  26. package/scripts/generate-image.js +116 -0
  27. package/scripts/generate-images.js +164 -0
  28. package/scripts/html2pdf.js +822 -0
  29. package/scripts/html2png.js +246 -0
  30. package/scripts/html2pptx.js +162 -0
  31. package/scripts/import-template.js +86 -0
  32. package/scripts/install-runtime.js +216 -0
  33. package/scripts/render-tldraw.js +44 -0
  34. package/scripts/validate-slides.js +221 -0
  35. package/skills/slides-grab/SKILL.md +65 -0
  36. package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
  37. package/skills/slides-grab-card-news/SKILL.md +35 -0
  38. package/skills/slides-grab-design/SKILL.md +88 -0
  39. package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
  40. package/skills/slides-grab-design/references/design-gate.md +349 -0
  41. package/skills/slides-grab-design/references/design-rules.md +76 -0
  42. package/skills/slides-grab-design/references/design-system-full.md +568 -0
  43. package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
  44. package/skills/slides-grab-export/SKILL.md +55 -0
  45. package/skills/slides-grab-export/references/export-rules.md +27 -0
  46. package/skills/slides-grab-export/references/html2pptx.md +627 -0
  47. package/skills/slides-grab-export/references/ooxml.md +427 -0
  48. package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
  49. package/skills/slides-grab-html/SKILL.md +67 -0
  50. package/skills/slides-grab-image/SKILL.md +82 -0
  51. package/skills/slides-grab-plan/SKILL.md +68 -0
  52. package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
  53. package/skills/slides-grab-plan/references/outline-format.md +47 -0
  54. package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
  55. package/src/codex-imagen.js +182 -0
  56. package/src/design-diversity-data.js +6932 -0
  57. package/src/design-gate-report.js +244 -0
  58. package/src/design-gate-state.js +329 -0
  59. package/src/design-import.js +164 -0
  60. package/src/design-md-parser.js +415 -0
  61. package/src/design-styles-data.js +1928 -0
  62. package/src/design-styles.js +209 -0
  63. package/src/editor/codex-edit.js +584 -0
  64. package/src/editor/edit-subprocess.js +170 -0
  65. package/src/editor/editor-codex-prompt.md +50 -0
  66. package/src/editor/editor.html +1785 -0
  67. package/src/editor/js/editor-bbox.js +332 -0
  68. package/src/editor/js/editor-chat.js +56 -0
  69. package/src/editor/js/editor-direct-edit.js +238 -0
  70. package/src/editor/js/editor-dom.js +59 -0
  71. package/src/editor/js/editor-init.js +405 -0
  72. package/src/editor/js/editor-navigation.js +54 -0
  73. package/src/editor/js/editor-select.js +558 -0
  74. package/src/editor/js/editor-send.js +175 -0
  75. package/src/editor/js/editor-sse.js +163 -0
  76. package/src/editor/js/editor-state.js +41 -0
  77. package/src/editor/js/editor-type.js +71 -0
  78. package/src/editor/js/editor-utils.js +167 -0
  79. package/src/editor/js/model-registry.js +37 -0
  80. package/src/editor/screenshot.js +82 -0
  81. package/src/export-resolution.cjs +68 -0
  82. package/src/figma.js +71 -0
  83. package/src/html2pptx-scale.cjs +120 -0
  84. package/src/html2pptx.cjs +1262 -0
  85. package/src/image-contract.js +329 -0
  86. package/src/image-native.js +468 -0
  87. package/src/nano-banana.js +841 -0
  88. package/src/ohos-browser-bridge.cjs +272 -0
  89. package/src/pptx-raster-export.cjs +299 -0
  90. package/src/resolve.js +110 -0
  91. package/src/slide-mode.cjs +72 -0
  92. package/src/template-fidelity.js +267 -0
  93. package/src/template-import.js +505 -0
  94. package/src/template-layout.js +261 -0
  95. package/src/template-pack.js +255 -0
  96. package/src/tldraw/render.js +473 -0
  97. package/src/validation/cli.js +120 -0
  98. package/src/validation/core.js +1023 -0
  99. package/templates/chart.html +121 -0
  100. package/templates/closing.html +54 -0
  101. package/templates/content.html +50 -0
  102. package/templates/contents.html +60 -0
  103. package/templates/cover.html +64 -0
  104. package/templates/custom/.gitkeep +0 -0
  105. package/templates/custom/README.md +7 -0
  106. package/templates/design-styles/README.md +20 -0
  107. package/templates/design-styles/preview.html +4438 -0
  108. package/templates/diagram-tldraw.html +56 -0
  109. package/templates/diagram.html +98 -0
  110. package/templates/quote.html +31 -0
  111. package/templates/section-divider.html +43 -0
  112. package/templates/split-layout.html +43 -0
  113. package/templates/statistics.html +55 -0
  114. package/templates/team.html +49 -0
  115. package/templates/timeline.html +59 -0
@@ -0,0 +1,164 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ const DEFAULT_MAX_BYTES = 256 * 1024;
5
+ const ALLOWED_PROTOCOLS = new Set(['https:']);
6
+
7
+ function getHeader(headers, name) {
8
+ if (!headers) return '';
9
+ if (typeof headers.get === 'function') return headers.get(name) ?? '';
10
+ return headers[name] ?? headers[name.toLowerCase()] ?? '';
11
+ }
12
+
13
+ function validateFinalDesignUrl(rawUrl, { allowedProtocols = ALLOWED_PROTOCOLS } = {}) {
14
+ let url;
15
+ try {
16
+ url = new URL(rawUrl);
17
+ } catch (cause) {
18
+ throw new DesignImportError(`Invalid final URL after redirects: ${rawUrl}`, { cause });
19
+ }
20
+ if (!allowedProtocols.has(url.protocol)) {
21
+ throw new DesignImportError(
22
+ `Redirect final URL protocol ${url.protocol} is not allowed. Allowed: ${[...allowedProtocols].join(', ')}`,
23
+ );
24
+ }
25
+ return url;
26
+ }
27
+
28
+ async function readResponseBody(response, { maxBytes }) {
29
+ if (response.body && typeof response.body.getReader === 'function') {
30
+ const reader = response.body.getReader();
31
+ const chunks = [];
32
+ let total = 0;
33
+ try {
34
+ while (true) {
35
+ const { done, value } = await reader.read();
36
+ if (done) break;
37
+ const chunk = Buffer.from(value);
38
+ total += chunk.byteLength;
39
+ if (total > maxBytes) {
40
+ if (typeof reader.cancel === 'function') {
41
+ await reader.cancel().catch(() => {});
42
+ }
43
+ throw new DesignImportError(
44
+ `DESIGN.md exceeds max size (${total} > ${maxBytes} bytes).`,
45
+ );
46
+ }
47
+ chunks.push(chunk);
48
+ }
49
+ } finally {
50
+ if (typeof reader.releaseLock === 'function') reader.releaseLock();
51
+ }
52
+ return Buffer.concat(chunks, total);
53
+ }
54
+
55
+ const buffer = Buffer.from(await response.arrayBuffer());
56
+ if (buffer.byteLength > maxBytes) {
57
+ throw new DesignImportError(
58
+ `DESIGN.md exceeds max size (${buffer.byteLength} > ${maxBytes} bytes).`,
59
+ );
60
+ }
61
+ return buffer;
62
+ }
63
+
64
+ export class DesignImportError extends Error {
65
+ constructor(message, { cause } = {}) {
66
+ super(message);
67
+ this.name = 'DesignImportError';
68
+ if (cause) this.cause = cause;
69
+ }
70
+ }
71
+
72
+ export function validateDesignUrl(rawUrl, { allowedProtocols = ALLOWED_PROTOCOLS } = {}) {
73
+ if (typeof rawUrl !== 'string' || rawUrl.trim() === '') {
74
+ throw new DesignImportError('Design URL is required.');
75
+ }
76
+ let url;
77
+ try {
78
+ url = new URL(rawUrl);
79
+ } catch (cause) {
80
+ throw new DesignImportError(`Invalid URL: ${rawUrl}`, { cause });
81
+ }
82
+ if (!allowedProtocols.has(url.protocol)) {
83
+ throw new DesignImportError(
84
+ `URL protocol ${url.protocol} is not allowed. Allowed: ${[...allowedProtocols].join(', ')}`,
85
+ );
86
+ }
87
+ return url;
88
+ }
89
+
90
+ export async function fetchDesignMarkdown(rawUrl, options = {}) {
91
+ const {
92
+ fetchImpl = globalThis.fetch,
93
+ maxBytes = DEFAULT_MAX_BYTES,
94
+ timeoutMs = 15000,
95
+ allowedProtocols,
96
+ } = options;
97
+
98
+ if (typeof fetchImpl !== 'function') {
99
+ throw new DesignImportError('No fetch implementation available; Node.js >= 18 required.');
100
+ }
101
+ const url = validateDesignUrl(rawUrl, { allowedProtocols });
102
+
103
+ const controller = new AbortController();
104
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
105
+
106
+ let response;
107
+ try {
108
+ response = await fetchImpl(url.toString(), {
109
+ redirect: 'follow',
110
+ signal: controller.signal,
111
+ headers: { 'User-Agent': 'slides-grab/import-design (+https://github.com/NomaDamas/slides-grab)' },
112
+ });
113
+
114
+ const finalUrl = validateFinalDesignUrl(response.url ?? url.toString(), { allowedProtocols });
115
+
116
+ if (!response.ok) {
117
+ throw new DesignImportError(`Fetch returned HTTP ${response.status} for ${url}`);
118
+ }
119
+
120
+ const contentType = getHeader(response.headers, 'content-type');
121
+ const looksLikeText = contentType === '' ||
122
+ contentType.includes('text/') ||
123
+ contentType.includes('markdown') ||
124
+ contentType.includes('application/octet-stream');
125
+ if (!looksLikeText) {
126
+ throw new DesignImportError(
127
+ `Refusing to import non-text response (content-type: ${contentType}).`,
128
+ );
129
+ }
130
+
131
+ const buffer = await readResponseBody(response, { maxBytes });
132
+ const text = buffer.toString('utf8');
133
+ return {
134
+ url: finalUrl.toString(),
135
+ contentType,
136
+ bytes: buffer.byteLength,
137
+ fetchedAt: new Date().toISOString(),
138
+ text,
139
+ };
140
+ } catch (cause) {
141
+ if (cause instanceof DesignImportError) throw cause;
142
+ throw new DesignImportError(`Fetch failed for ${url}: ${cause.message}`, { cause });
143
+ } finally {
144
+ clearTimeout(timeoutHandle);
145
+ }
146
+ }
147
+
148
+ export function formatImportedDesignMarkdown({ url, content, fetchedAt }) {
149
+ const banner = [
150
+ '<!--',
151
+ ` Imported by slides-grab import-design`,
152
+ ` source: ${url}`,
153
+ ` fetched-at: ${fetchedAt}`,
154
+ '-->',
155
+ '',
156
+ ].join('\n');
157
+ return `${banner}${content}\n`;
158
+ }
159
+
160
+ export function saveImportedDesign({ outputPath, markdown }) {
161
+ const absolutePath = resolve(outputPath);
162
+ writeFileSync(absolutePath, markdown, 'utf8');
163
+ return absolutePath;
164
+ }
@@ -0,0 +1,415 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename, resolve } from 'node:path';
3
+
4
+ const FRONTMATTER_FENCE = /^---\s*$/;
5
+ const HEX_PATTERN = /#[0-9a-fA-F]{3,8}\b/g;
6
+ const LIST_SCAFFOLD_KEY = '__list__';
7
+
8
+ const SECTION_ALIASES = Object.freeze({
9
+ overview: 'overview',
10
+ 'brand & style': 'overview',
11
+ 'visual theme & atmosphere': 'overview',
12
+ background: 'background',
13
+ colors: 'colors',
14
+ 'color palette & roles': 'colors',
15
+ 'color palette': 'colors',
16
+ palette: 'colors',
17
+ typography: 'fonts',
18
+ 'typography rules': 'fonts',
19
+ fonts: 'fonts',
20
+ layout: 'layout',
21
+ 'layout principles': 'layout',
22
+ 'slide layouts': 'layout',
23
+ components: 'components',
24
+ 'component stylings': 'components',
25
+ 'elevation & depth': 'elevation',
26
+ 'depth & elevation': 'elevation',
27
+ shapes: 'shapes',
28
+ 'signature elements': 'signature',
29
+ 'signature motifs': 'signature',
30
+ signature: 'signature',
31
+ "do's and don'ts": 'dosdonts',
32
+ 'dos and donts': 'dosdonts',
33
+ avoid: 'avoid',
34
+ "don't": 'avoid',
35
+ donts: 'avoid',
36
+ 'responsive behavior': 'responsive',
37
+ 'agent prompt guide': 'agentPrompt',
38
+ });
39
+
40
+ function parseYamlScalar(raw) {
41
+ const value = raw.trim();
42
+ if (value === '') return '';
43
+ if (value === 'null' || value === '~') return null;
44
+ if (value === 'true') return true;
45
+ if (value === 'false') return false;
46
+ if (value !== '' && /^-?\d+(\.\d+)?$/.test(value)) return Number(value);
47
+ if ((value.startsWith('"') && value.endsWith('"')) ||
48
+ (value.startsWith("'") && value.endsWith("'"))) {
49
+ return value.slice(1, -1);
50
+ }
51
+ return value;
52
+ }
53
+
54
+ export function parseYamlFrontMatter(text) {
55
+ const lines = text.split(/\r?\n/);
56
+ const root = {};
57
+ const indentStack = [{ container: root, indent: -1 }];
58
+
59
+ for (const rawLine of lines) {
60
+ if (rawLine.trim() === '' || rawLine.trim().startsWith('#')) continue;
61
+
62
+ const indent = rawLine.match(/^ */)[0].length;
63
+ const line = rawLine.trim();
64
+
65
+ while (indentStack.length > 1 && indentStack[indentStack.length - 1].indent >= indent) {
66
+ indentStack.pop();
67
+ }
68
+ const parent = indentStack[indentStack.length - 1].container;
69
+
70
+ if (line.startsWith('- ')) {
71
+ const valuePart = line.slice(2);
72
+ if (!Array.isArray(parent[LIST_SCAFFOLD_KEY])) {
73
+ parent[LIST_SCAFFOLD_KEY] = [];
74
+ }
75
+ const isInlineMapping = valuePart.includes(': ') &&
76
+ !valuePart.startsWith('"') && !valuePart.startsWith("'");
77
+ if (isInlineMapping) {
78
+ const obj = {};
79
+ const colonIdx = valuePart.indexOf(': ');
80
+ obj[valuePart.slice(0, colonIdx).trim()] = parseYamlScalar(valuePart.slice(colonIdx + 2));
81
+ parent[LIST_SCAFFOLD_KEY].push(obj);
82
+ } else {
83
+ parent[LIST_SCAFFOLD_KEY].push(parseYamlScalar(valuePart));
84
+ }
85
+ continue;
86
+ }
87
+
88
+ const colonIdx = line.indexOf(':');
89
+ if (colonIdx === -1) continue;
90
+ const key = line.slice(0, colonIdx).trim();
91
+ const rest = line.slice(colonIdx + 1);
92
+
93
+ if (rest.trim() === '') {
94
+ const child = {};
95
+ parent[key] = child;
96
+ indentStack.push({ container: child, indent });
97
+ } else {
98
+ parent[key] = parseYamlScalar(rest);
99
+ }
100
+ }
101
+
102
+ return collapseListScaffolds(root);
103
+ }
104
+
105
+ function collapseListScaffolds(node) {
106
+ if (Array.isArray(node)) return node.map(collapseListScaffolds);
107
+ if (node && typeof node === 'object') {
108
+ const isPureListScaffold = LIST_SCAFFOLD_KEY in node && Object.keys(node).length === 1;
109
+ if (isPureListScaffold) {
110
+ return node[LIST_SCAFFOLD_KEY].map(collapseListScaffolds);
111
+ }
112
+ const out = {};
113
+ for (const [k, v] of Object.entries(node)) {
114
+ out[k] = collapseListScaffolds(v);
115
+ }
116
+ return out;
117
+ }
118
+ return node;
119
+ }
120
+
121
+ export function splitFrontMatter(markdown) {
122
+ const lines = markdown.split(/\r?\n/);
123
+ const startsWithFence = lines.length > 0 && FRONTMATTER_FENCE.test(lines[0]);
124
+ if (!startsWithFence) {
125
+ return { frontMatter: '', body: markdown };
126
+ }
127
+ for (let i = 1; i < lines.length; i += 1) {
128
+ if (FRONTMATTER_FENCE.test(lines[i])) {
129
+ return {
130
+ frontMatter: lines.slice(1, i).join('\n'),
131
+ body: lines.slice(i + 1).join('\n'),
132
+ };
133
+ }
134
+ }
135
+ return { frontMatter: '', body: markdown };
136
+ }
137
+
138
+ function canonicalSectionBucket(rawTitle) {
139
+ const titleKey = rawTitle.toLowerCase()
140
+ .replace(/[`*_]/g, '')
141
+ .replace(/^\d+\.\s*/, '')
142
+ .trim();
143
+ return SECTION_ALIASES[titleKey] ?? null;
144
+ }
145
+
146
+ export function extractSections(markdownBody) {
147
+ const lines = markdownBody.split(/\r?\n/);
148
+ const sections = {};
149
+ let currentBucket = null;
150
+
151
+ for (const line of lines) {
152
+ const headingMatch = line.match(/^(#{2,3})\s+(.+?)\s*$/);
153
+ const isLevel2Heading = headingMatch && headingMatch[1].length === 2;
154
+ if (isLevel2Heading) {
155
+ const bucket = canonicalSectionBucket(headingMatch[2].trim());
156
+ if (bucket) {
157
+ currentBucket = bucket;
158
+ if (!sections[bucket]) {
159
+ sections[bucket] = { heading: `## ${headingMatch[2].trim()}`, text: '' };
160
+ }
161
+ continue;
162
+ }
163
+ currentBucket = null;
164
+ continue;
165
+ }
166
+ if (currentBucket) {
167
+ sections[currentBucket].text = (sections[currentBucket].text
168
+ ? sections[currentBucket].text + '\n'
169
+ : '') + line;
170
+ }
171
+ }
172
+
173
+ for (const key of Object.keys(sections)) {
174
+ sections[key].text = sections[key].text.replace(/\n+$/g, '').trim();
175
+ }
176
+ return sections;
177
+ }
178
+
179
+ function bulletsFromMarkdown(text) {
180
+ if (!text) return [];
181
+ const lines = text.split(/\r?\n/);
182
+ const bullets = [];
183
+ let buffer = [];
184
+ const flushBuffer = () => {
185
+ const joined = buffer.join(' ').trim();
186
+ if (joined) bullets.push(joined);
187
+ buffer = [];
188
+ };
189
+ for (const line of lines) {
190
+ const bulletMatch = line.match(/^\s*[-*]\s+(.*)$/);
191
+ const orderedMatch = /^\s*\d+\.\s+/.test(line);
192
+ if (bulletMatch) {
193
+ flushBuffer();
194
+ buffer.push(bulletMatch[1].trim());
195
+ } else if (orderedMatch) {
196
+ flushBuffer();
197
+ buffer.push(line.replace(/^\s*\d+\.\s+/, '').trim());
198
+ } else if (line.trim() === '') {
199
+ flushBuffer();
200
+ } else if (buffer.length > 0) {
201
+ buffer.push(line.trim());
202
+ } else {
203
+ bullets.push(line.trim());
204
+ }
205
+ }
206
+ flushBuffer();
207
+ return bullets.filter((b) => b.length > 0);
208
+ }
209
+
210
+ function colorEntriesFromFrontMatter(frontMatterColors) {
211
+ if (!frontMatterColors || typeof frontMatterColors !== 'object' || Array.isArray(frontMatterColors)) {
212
+ return [];
213
+ }
214
+ const entries = [];
215
+ for (const [role, value] of Object.entries(frontMatterColors)) {
216
+ if (typeof value === 'string') {
217
+ const hexMatch = value.match(HEX_PATTERN);
218
+ entries.push({
219
+ role,
220
+ label: role.replace(/[-_]/g, ' '),
221
+ hex: hexMatch ? hexMatch[0] : value,
222
+ });
223
+ } else if (value && typeof value === 'object' && 'value' in value) {
224
+ entries.push({
225
+ role,
226
+ label: value.label ?? role.replace(/[-_]/g, ' '),
227
+ hex: String(value.value),
228
+ });
229
+ }
230
+ }
231
+ return entries;
232
+ }
233
+
234
+ function colorEntriesFromTableRows(sectionText) {
235
+ const entries = [];
236
+ const tableRows = sectionText.split(/\r?\n/).filter((l) => l.trim().startsWith('|'));
237
+ for (const row of tableRows) {
238
+ if (/^\|[-:\s|]+\|\s*$/.test(row)) continue;
239
+ const cells = row.split('|').map((c) => c.trim()).filter((c) => c.length > 0);
240
+ if (cells.length < 2) continue;
241
+ if (/^role$/i.test(cells[0]) && /^(label|name)$/i.test(cells[1])) continue;
242
+ const hexInRow = (row.match(HEX_PATTERN) || [])[0];
243
+ if (!hexInRow) continue;
244
+ const role = cells[0];
245
+ const label = cells.length >= 2 ? cells[1].replace(HEX_PATTERN, '').replace(/`/g, '').trim() : role;
246
+ entries.push({ role, label: label || role, hex: hexInRow });
247
+ }
248
+ return entries;
249
+ }
250
+
251
+ function colorEntriesFromBullets(sectionText, existing) {
252
+ const entries = [];
253
+ const bulletEntries = bulletsFromMarkdown(sectionText);
254
+ for (const bullet of bulletEntries) {
255
+ const hexes = bullet.match(HEX_PATTERN);
256
+ if (!hexes) continue;
257
+ for (const hex of hexes) {
258
+ const isDuplicate = existing.some((e) => e.hex.toLowerCase() === hex.toLowerCase()) ||
259
+ entries.some((e) => e.hex.toLowerCase() === hex.toLowerCase());
260
+ if (isDuplicate) continue;
261
+ const cleaned = bullet.replace(HEX_PATTERN, '').replace(/`/g, '').replace(/[:|]/g, ' ').trim();
262
+ const [role, ...labelParts] = cleaned.split(/\s+-\s+|\s+—\s+|\s{2,}/);
263
+ entries.push({
264
+ role: role || 'Color',
265
+ label: (labelParts.join(' ') || cleaned || role || 'Color').slice(0, 80),
266
+ hex,
267
+ });
268
+ }
269
+ }
270
+ return entries;
271
+ }
272
+
273
+ function colorsFromSection(sectionText, frontMatterColors) {
274
+ const fromFrontMatter = colorEntriesFromFrontMatter(frontMatterColors);
275
+ if (!sectionText) return fromFrontMatter;
276
+ const fromTable = colorEntriesFromTableRows(sectionText);
277
+ const merged = [...fromFrontMatter, ...fromTable];
278
+ const fromBullets = colorEntriesFromBullets(sectionText, merged);
279
+ return [...merged, ...fromBullets];
280
+ }
281
+
282
+ function sanitizeStyleId(raw) {
283
+ if (!raw) return 'imported-design';
284
+ return String(raw)
285
+ .toLowerCase()
286
+ .replace(/\.[a-z0-9]+$/i, '')
287
+ .replace(/[^a-z0-9]+/g, '-')
288
+ .replace(/^-+|-+$/g, '')
289
+ .slice(0, 64) || 'imported-design';
290
+ }
291
+
292
+ function slugToTitle(slug) {
293
+ return slug.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
294
+ }
295
+
296
+ function truncateString(text, max) {
297
+ if (text.length <= max) return text;
298
+ return `${text.slice(0, max - 1).trim()}…`;
299
+ }
300
+
301
+ function buildStyleObject({ frontMatter, sections, sourceMeta }) {
302
+ const fmName = frontMatter?.name ?? frontMatter?.title ?? null;
303
+ const fmDescription = frontMatter?.description ?? null;
304
+ const id = sanitizeStyleId(sourceMeta?.idHint ?? fmName ?? 'imported-design');
305
+
306
+ const background = bulletsFromMarkdown(sections.background?.text ?? '');
307
+ const fonts = bulletsFromMarkdown(sections.fonts?.text ?? '');
308
+ const layoutBullets = bulletsFromMarkdown(sections.layout?.text ?? '');
309
+ const componentBullets = bulletsFromMarkdown(sections.components?.text ?? '');
310
+ const signature = bulletsFromMarkdown(sections.signature?.text ?? '');
311
+ const avoidBullets = bulletsFromMarkdown(sections.avoid?.text ?? sections.dosdonts?.text ?? '');
312
+ const colors = colorsFromSection(sections.colors?.text ?? '', frontMatter?.colors);
313
+ const overview = sections.overview?.text ?? '';
314
+
315
+ return Object.freeze({
316
+ id,
317
+ title: fmName ?? slugToTitle(id),
318
+ mood: fmDescription ? truncateString(fmDescription, 80) : 'Imported DESIGN.md',
319
+ bestFor: 'Custom DESIGN.md-driven decks',
320
+ background,
321
+ colors,
322
+ fonts,
323
+ layout: [...layoutBullets, ...componentBullets],
324
+ signature,
325
+ avoid: avoidBullets,
326
+ overview,
327
+ source: Object.freeze({
328
+ type: 'design-md',
329
+ path: sourceMeta?.path ?? null,
330
+ url: sourceMeta?.url ?? null,
331
+ fetchedAt: sourceMeta?.fetchedAt ?? null,
332
+ }),
333
+ raw: Object.freeze({
334
+ frontMatter: frontMatter ?? {},
335
+ markdown: sourceMeta?.markdown ?? '',
336
+ sections,
337
+ }),
338
+ });
339
+ }
340
+
341
+ export function parseDesignMarkdown(markdown, sourceMeta = {}) {
342
+ if (typeof markdown !== 'string' || markdown.length === 0) {
343
+ throw new Error('DESIGN.md content is empty.');
344
+ }
345
+ const { frontMatter: fmRaw, body } = splitFrontMatter(markdown);
346
+ const frontMatter = fmRaw ? parseYamlFrontMatter(fmRaw) : {};
347
+ const sections = extractSections(body);
348
+ return buildStyleObject({
349
+ frontMatter,
350
+ sections,
351
+ sourceMeta: { ...sourceMeta, markdown },
352
+ });
353
+ }
354
+
355
+ export function parseDesignMarkdownFile(filePath, extraMeta = {}) {
356
+ const absolutePath = resolve(filePath);
357
+ const text = readFileSync(absolutePath, 'utf8');
358
+ const idHint = basename(absolutePath, '.md');
359
+ return parseDesignMarkdown(text, {
360
+ path: absolutePath,
361
+ idHint,
362
+ ...extraMeta,
363
+ });
364
+ }
365
+
366
+ export function renderDesignStyleForPrompt(style, options = {}) {
367
+ const { maxColors = 12 } = options;
368
+ if (!style) return '';
369
+ const lines = [];
370
+ lines.push(`# Design System: ${style.title}`);
371
+ if (style.mood) lines.push(`Mood: ${style.mood}`);
372
+ if (style.source?.url) lines.push(`Source: ${style.source.url}`);
373
+ else if (style.source?.path) lines.push(`Source: ${style.source.path}`);
374
+ lines.push('');
375
+ if (style.overview) {
376
+ lines.push('## Overview');
377
+ lines.push(style.overview.trim());
378
+ lines.push('');
379
+ }
380
+ if (style.background?.length) {
381
+ lines.push('## Background');
382
+ for (const b of style.background) lines.push(`- ${b}`);
383
+ lines.push('');
384
+ }
385
+ if (style.colors?.length) {
386
+ lines.push('## Colors');
387
+ for (const c of style.colors.slice(0, maxColors)) {
388
+ lines.push(`- ${c.role}: ${c.label} (${c.hex})`);
389
+ }
390
+ lines.push('');
391
+ }
392
+ if (style.fonts?.length) {
393
+ lines.push('## Typography');
394
+ for (const f of style.fonts) lines.push(`- ${f}`);
395
+ lines.push('');
396
+ }
397
+ if (style.layout?.length) {
398
+ lines.push('## Layout');
399
+ for (const l of style.layout) lines.push(`- ${l}`);
400
+ lines.push('');
401
+ }
402
+ if (style.signature?.length) {
403
+ lines.push('## Signature Elements');
404
+ for (const s of style.signature) lines.push(`- ${s}`);
405
+ lines.push('');
406
+ }
407
+ if (style.avoid?.length) {
408
+ lines.push('## Avoid');
409
+ for (const a of style.avoid) lines.push(`- ${a}`);
410
+ lines.push('');
411
+ }
412
+ return lines.join('\n').trim();
413
+ }
414
+
415
+ export { sanitizeStyleId as _sanitizeStyleId };