@bookklik/senangstart-css 0.2.9 → 0.2.12

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 (69) hide show
  1. package/.agent/skills/add-utility/SKILL.md +65 -0
  2. package/.agent/workflows/add-utility.md +2 -0
  3. package/.agent/workflows/build.md +2 -0
  4. package/.agent/workflows/dev.md +2 -0
  5. package/AGENTS.md +30 -0
  6. package/dist/senangstart-css.js +607 -180
  7. package/dist/senangstart-css.min.js +234 -195
  8. package/dist/senangstart-tw.js +274 -8
  9. package/dist/senangstart-tw.min.js +1 -1
  10. package/docs/SYNTAX-REFERENCE.md +1731 -1590
  11. package/docs/guide/preflight.md +20 -1
  12. package/docs/ms/guide/preflight.md +19 -0
  13. package/docs/ms/reference/breakpoints.md +14 -0
  14. package/docs/ms/reference/visual/border-radius.md +50 -10
  15. package/docs/ms/reference/visual/contain.md +57 -0
  16. package/docs/ms/reference/visual/content-visibility.md +53 -0
  17. package/docs/ms/reference/visual/placeholder-color.md +92 -0
  18. package/docs/ms/reference/visual/ring-color.md +2 -2
  19. package/docs/ms/reference/visual/ring-offset.md +3 -3
  20. package/docs/ms/reference/visual/ring.md +5 -5
  21. package/docs/ms/reference/visual/writing-mode.md +53 -0
  22. package/docs/ms/reference/visual.md +6 -0
  23. package/docs/public/assets/senangstart-css.min.js +234 -195
  24. package/docs/public/llms.txt +45 -12
  25. package/docs/reference/breakpoints.md +14 -0
  26. package/docs/reference/visual/border-radius.md +50 -10
  27. package/docs/reference/visual/contain.md +57 -0
  28. package/docs/reference/visual/content-visibility.md +53 -0
  29. package/docs/reference/visual/placeholder-color.md +92 -0
  30. package/docs/reference/visual/ring-color.md +2 -2
  31. package/docs/reference/visual/ring-offset.md +3 -3
  32. package/docs/reference/visual/ring.md +5 -5
  33. package/docs/reference/visual/writing-mode.md +53 -0
  34. package/docs/reference/visual.md +7 -0
  35. package/docs/syntax-reference.json +2185 -2009
  36. package/package.json +1 -1
  37. package/scripts/convert-tailwind.js +300 -26
  38. package/scripts/generate-docs.js +403 -403
  39. package/src/cdn/senangstart-engine.js +5 -5
  40. package/src/cdn/tw-conversion-engine.js +305 -8
  41. package/src/cli/commands/build.js +51 -13
  42. package/src/cli/commands/dev.js +157 -93
  43. package/src/compiler/generators/css.js +467 -208
  44. package/src/compiler/generators/preflight.js +26 -13
  45. package/src/compiler/generators/typescript.js +3 -1
  46. package/src/compiler/index.js +27 -3
  47. package/src/compiler/parser.js +13 -6
  48. package/src/compiler/tokenizer.js +25 -23
  49. package/src/config/defaults.js +3 -0
  50. package/src/core/tokenizer-core.js +46 -19
  51. package/src/definitions/index.js +4 -1
  52. package/src/definitions/visual-borders.js +10 -10
  53. package/src/definitions/visual-performance.js +126 -0
  54. package/src/definitions/visual.js +25 -9
  55. package/src/utils/common.js +456 -27
  56. package/src/utils/node-io.js +82 -0
  57. package/tests/integration/dev-recovery.test.js +231 -0
  58. package/tests/unit/cli/memory-limits.test.js +169 -0
  59. package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
  60. package/tests/unit/compiler/generators/css-errors.test.js +102 -0
  61. package/tests/unit/compiler/generators/css.test.js +102 -5
  62. package/tests/unit/convert-tailwind.test.js +518 -431
  63. package/tests/unit/utils/common.test.js +376 -26
  64. package/tests/unit/utils/file-timeout.test.js +154 -0
  65. package/tests/unit/utils/theme-validation.test.js +181 -0
  66. package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
  67. package/tests/unit/convert-tailwind.cli.test.js +0 -95
  68. package/tests/unit/security.test.js +0 -206
  69. /package/tests/unit/{convert-tailwind.coverage.test.js → convert-tailwind-edgecases.test.js} +0 -0
@@ -1,403 +1,403 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * SenangStart CSS - Documentation Generator
5
- * Generates markdown documentation from utility definitions
6
- *
7
- * Usage: node scripts/generate-docs.js [--locale en|ms] [--dry-run]
8
- */
9
-
10
- import fs from 'fs';
11
- import path from 'path';
12
- import { fileURLToPath } from 'url';
13
- import { getDefinitionsByCategory, getAllDefinitions } from '../src/definitions/index.js';
14
-
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = path.dirname(__filename);
17
- const rootDir = path.resolve(__dirname, '..');
18
-
19
- // Configuration
20
- const config = {
21
- docsDir: path.join(rootDir, 'docs'),
22
- locales: ['en', 'ms'],
23
- categories: {
24
- layout: 'layout',
25
- space: 'space',
26
- visual: 'visual'
27
- }
28
- };
29
-
30
- /**
31
- * Generate markdown for a single definition
32
- */
33
- function generateMarkdown(definition, locale = 'en') {
34
- const isMs = locale === 'ms';
35
- const lines = [];
36
-
37
- // Title
38
- lines.push(`# ${formatTitle(definition.name)}`);
39
- lines.push('');
40
-
41
- // Description
42
- const description = isMs ? definition.descriptionMs : definition.description;
43
- lines.push(description);
44
- lines.push('');
45
-
46
- // Syntax section (if available)
47
- if (definition.syntax) {
48
- lines.push(isMs ? '## Sintaks' : '## Syntax');
49
- lines.push('```');
50
- lines.push(definition.syntax);
51
- lines.push('```');
52
- lines.push('');
53
- }
54
-
55
- // Values table
56
- if (definition.values && definition.values.length > 0) {
57
- lines.push(isMs ? '## Nilai' : '## Values');
58
- lines.push('');
59
-
60
- // Check if values have properties (like space definitions)
61
- const hasProperty = definition.values[0].property !== undefined;
62
-
63
- if (hasProperty) {
64
- lines.push(isMs
65
- ? '| Properti | CSS Output | Huraian |'
66
- : '| Property | CSS Output | Description |');
67
- lines.push('|--------|------------|-------------|');
68
-
69
- for (const v of definition.values) {
70
- const desc = isMs ? (v.descriptionMs || v.description) : v.description;
71
- const cssDisplay = v.css.replace(/{value}/g, '{value}').replace(/;/g, '');
72
- lines.push(`| \`${v.property}\` | \`${cssDisplay}\` | ${desc} |`);
73
- }
74
- } else {
75
- lines.push(isMs
76
- ? '| Nilai | CSS Output | Huraian |'
77
- : '| Value | CSS Output | Description |');
78
- lines.push('|-------|------------|-------------|');
79
-
80
- for (const v of definition.values) {
81
- const desc = isMs ? (v.descriptionMs || v.description) : v.description;
82
- const cssDisplay = v.css.replace(/;$/, '');
83
- lines.push(`| \`${v.value}\` | \`${cssDisplay}\` | ${desc} |`);
84
- }
85
- }
86
- lines.push('');
87
- }
88
-
89
- // Scale values (if applicable)
90
- if (definition.scaleValues) {
91
- lines.push(isMs ? '## Nilai Skala' : '## Scale Values');
92
- lines.push('');
93
- lines.push(definition.scaleValues.map(v => `\`${v}\``).join(', '));
94
- lines.push('');
95
- }
96
-
97
- // Examples
98
- if (definition.examples && definition.examples.length > 0) {
99
- lines.push(isMs ? '## Contoh' : '## Examples');
100
- lines.push('');
101
- lines.push('```html');
102
- for (const example of definition.examples) {
103
- lines.push(example.code);
104
- }
105
- lines.push('```');
106
- lines.push('');
107
- }
108
-
109
- // Preview (live HTML demos)
110
- if (definition.preview && definition.preview.length > 0) {
111
- lines.push(isMs ? '## Pratonton' : '## Preview');
112
- lines.push('');
113
-
114
- for (let i = 0; i < definition.preview.length; i++) {
115
- const p = definition.preview[i];
116
- const title = isMs ? (p.titleMs || p.title) : p.title;
117
-
118
- // Wrapper container for each preview
119
- lines.push('<div space="p-x:big p-b:medium m-t:medium" visual="border-w:thin border:neutral-100 dark:border:neutral-800 rounded:medium">');
120
- lines.push('');
121
-
122
- // Title for this preview
123
- if (title) {
124
- lines.push(`### ${title}`);
125
- lines.push('');
126
- }
127
-
128
- // Live preview
129
- lines.push(`<div layout="flex col" space="g:medium">`);
130
-
131
- // Description with highlighted code (above the preview)
132
- const desc = isMs ? p.descriptionMs : p.description;
133
- const highlightCode = p.highlightValue ? `<code>${definition.property}="${p.highlightValue}"</code>` : '';
134
- lines.push(` <p space="m:none" visual="text:neutral-600 dark:text:neutral-400 text-sm">${highlightCode}${highlightCode && desc ? ' - ' : ''}${desc}</p>`);
135
-
136
- lines.push(p.html);
137
- lines.push('</div>');
138
- lines.push('');
139
-
140
- // Code block showing the HTML used (collapsible)
141
- lines.push('<details>');
142
- lines.push(`<summary>${isMs ? 'Lihat Kod' : 'View Code'}</summary>`);
143
- lines.push('');
144
- lines.push('```html');
145
- lines.push(p.html);
146
- lines.push('```');
147
- lines.push('');
148
- lines.push('</details>');
149
- lines.push('');
150
-
151
- // Close wrapper container
152
- lines.push('</div>');
153
- lines.push('');
154
- }
155
- }
156
-
157
- // Arbitrary values support
158
- if (definition.supportsArbitrary) {
159
- lines.push(isMs ? '## Nilai Arbitrari' : '## Arbitrary Values');
160
- lines.push('');
161
- lines.push(isMs
162
- ? 'Sokong nilai tersuai menggunakan sintaks kurungan segi empat:'
163
- : 'Supports custom values using bracket syntax:');
164
- lines.push('');
165
- lines.push('```html');
166
- lines.push(`<div ${definition.property}="${definition.name.split('-')[0]}:[custom-value]">Custom</div>`);
167
- lines.push('```');
168
- lines.push('');
169
- }
170
-
171
- // Footnotes (e.g., Tailwind scale compatibility notes)
172
- if (definition.footnotes && definition.footnotes.length > 0) {
173
- lines.push(isMs ? '## Nota' : '## Notes');
174
- lines.push('');
175
-
176
- for (const footnote of definition.footnotes) {
177
- const title = isMs ? (footnote.titleMs || footnote.title) : footnote.title;
178
- const content = isMs ? (footnote.contentMs || footnote.content) : footnote.content;
179
-
180
- // Use VitePress TIP alert for footnotes
181
- lines.push('> [!TIP]');
182
- lines.push(`> **${title}**`);
183
- lines.push(`> `);
184
- lines.push(`> ${content}`);
185
- if (footnote.link) {
186
- lines.push(`> `);
187
- lines.push(`> [${isMs ? 'Rujukan' : 'Reference'}](${footnote.link})`);
188
- }
189
- lines.push('');
190
- }
191
- }
192
-
193
- // Responsive
194
- // lines.push(isMs ? '## Responsif' : '## Responsive');
195
- // lines.push('');
196
- // lines.push('```html');
197
- // if (definition.examples && definition.examples[0]) {
198
- // const baseExample = definition.examples[0].code;
199
- // lines.push(`<!-- ${isMs ? 'Contoh responsif' : 'Responsive example'} -->`);
200
- // lines.push(`<div ${definition.property}="mob:... tab:... lap:...">`);
201
- // lines.push(` ${isMs ? 'Kandungan responsif' : 'Responsive content'}`);
202
- // lines.push('</div>');
203
- // }
204
- // lines.push('```');
205
- // lines.push('');
206
-
207
- return lines.join('\n');
208
- }
209
-
210
- /**
211
- * Format definition name as title
212
- */
213
- function formatTitle(name) {
214
- return name
215
- .split('-')
216
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
217
- .join(' ');
218
- }
219
-
220
- /**
221
- * Get output path for a definition
222
- */
223
- function getOutputPath(definition, locale) {
224
- const category = config.categories[definition.category] || 'other';
225
- const filename = `${definition.name}.md`;
226
-
227
- if (locale === 'ms') {
228
- return path.join(config.docsDir, 'ms', 'reference', category, filename);
229
- }
230
- return path.join(config.docsDir, 'reference', category, filename);
231
- }
232
-
233
- /**
234
- * Ensure directory exists
235
- */
236
- function ensureDir(filePath) {
237
- const dir = path.dirname(filePath);
238
- if (!fs.existsSync(dir)) {
239
- fs.mkdirSync(dir, { recursive: true });
240
- }
241
- }
242
-
243
- /**
244
- * Generate all documentation
245
- */
246
- function generateAllDocs(options = {}) {
247
- const { dryRun = false, locale = 'all' } = options;
248
- const definitions = getAllDefinitions();
249
- const results = { generated: [], skipped: [], errors: [] };
250
-
251
- const locales = locale === 'all' ? config.locales : [locale];
252
-
253
- console.log(`\nšŸ“š SenangStart CSS Documentation Generator`);
254
- console.log(` Generating docs for ${definitions.length} definitions...`);
255
- if (dryRun) {
256
- console.log(` (Dry run - no files will be written)\n`);
257
- }
258
-
259
- for (const def of definitions) {
260
- for (const loc of locales) {
261
- try {
262
- const outputPath = getOutputPath(def, loc);
263
- const markdown = generateMarkdown(def, loc);
264
-
265
- if (dryRun) {
266
- console.log(` Would create: ${path.relative(rootDir, outputPath)}`);
267
- results.generated.push(outputPath);
268
- } else {
269
- ensureDir(outputPath);
270
- fs.writeFileSync(outputPath, markdown, 'utf8');
271
- console.log(` āœ“ Created: ${path.relative(rootDir, outputPath)}`);
272
- results.generated.push(outputPath);
273
- }
274
- } catch (error) {
275
- console.error(` āœ— Error generating ${def.name} (${loc}): ${error.message}`);
276
- results.errors.push({ definition: def.name, locale: loc, error: error.message });
277
- }
278
- }
279
- }
280
-
281
- // Summary
282
- console.log(`\nšŸ“Š Summary:`);
283
- console.log(` Generated: ${results.generated.length} files`);
284
- console.log(` Skipped: ${results.skipped.length} files`);
285
- console.log(` Errors: ${results.errors.length} files`);
286
-
287
- return results;
288
- }
289
-
290
- /**
291
- * Compare generated docs with existing docs
292
- * Returns list of differences
293
- */
294
- function compareDocs(options = {}) {
295
- const definitions = getAllDefinitions();
296
- const differences = [];
297
-
298
- console.log(`\nšŸ” Comparing definitions with existing documentation...`);
299
-
300
- for (const def of definitions) {
301
- for (const locale of config.locales) {
302
- const outputPath = getOutputPath(def, locale);
303
-
304
- if (!fs.existsSync(outputPath)) {
305
- differences.push({
306
- type: 'missing',
307
- definition: def.name,
308
- locale,
309
- path: outputPath
310
- });
311
- continue;
312
- }
313
-
314
- const existingContent = fs.readFileSync(outputPath, 'utf8');
315
- const generatedContent = generateMarkdown(def, locale);
316
-
317
- // Simple comparison - check if values table matches
318
- // This is a basic check; could be made more sophisticated
319
- const existingValues = extractValuesFromTable(existingContent);
320
- const definedValues = def.values?.map(v => v.value || v.property) || [];
321
-
322
- const missingInDocs = definedValues.filter(v => !existingValues.includes(v));
323
- const extraInDocs = existingValues.filter(v => !definedValues.includes(v));
324
-
325
- if (missingInDocs.length > 0 || extraInDocs.length > 0) {
326
- differences.push({
327
- type: 'mismatch',
328
- definition: def.name,
329
- locale,
330
- path: outputPath,
331
- missingInDocs,
332
- extraInDocs
333
- });
334
- }
335
- }
336
- }
337
-
338
- if (differences.length === 0) {
339
- console.log(' āœ“ All documentation is in sync!\n');
340
- } else {
341
- console.log(` ⚠ Found ${differences.length} differences:\n`);
342
- for (const diff of differences) {
343
- if (diff.type === 'missing') {
344
- console.log(` Missing: ${diff.definition} (${diff.locale})`);
345
- } else {
346
- console.log(` Mismatch: ${diff.definition} (${diff.locale})`);
347
- if (diff.missingInDocs?.length > 0) {
348
- console.log(` - Missing in docs: ${diff.missingInDocs.join(', ')}`);
349
- }
350
- if (diff.extraInDocs?.length > 0) {
351
- console.log(` - Extra in docs: ${diff.extraInDocs.join(', ')}`);
352
- }
353
- }
354
- }
355
- }
356
-
357
- return differences;
358
- }
359
-
360
- /**
361
- * Extract values from a markdown table
362
- */
363
- function extractValuesFromTable(markdown) {
364
- const values = [];
365
- const lines = markdown.split('\n');
366
-
367
- for (let line of lines) {
368
- line = line.trim();
369
- // Skip non-table lines, header rows, and separator rows
370
- if (!line.startsWith('|')) continue;
371
- if (line.includes('Value') || line.includes('Nilai') || line.includes('Property') || line.includes('Properti')) continue;
372
- if (line.includes('---')) continue;
373
-
374
- // Extract first cell (value name) - look for backtick-quoted value
375
- const cellMatch = line.match(/\|\s*`([^`]+)`/);
376
- if (cellMatch) {
377
- values.push(cellMatch[1]);
378
- }
379
- }
380
-
381
- return values;
382
- }
383
-
384
- // CLI handling
385
- const args = process.argv.slice(2);
386
- const options = {
387
- dryRun: args.includes('--dry-run'),
388
- locale: 'all',
389
- compare: args.includes('--compare')
390
- };
391
-
392
- // Parse locale option
393
- const localeIndex = args.indexOf('--locale');
394
- if (localeIndex !== -1 && args[localeIndex + 1]) {
395
- options.locale = args[localeIndex + 1];
396
- }
397
-
398
- // Run
399
- if (options.compare) {
400
- compareDocs(options);
401
- } else {
402
- generateAllDocs(options);
403
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SenangStart CSS - Documentation Generator
5
+ * Generates markdown documentation from utility definitions
6
+ *
7
+ * Usage: node scripts/generate-docs.js [--locale en|ms] [--dry-run]
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { getDefinitionsByCategory, getAllDefinitions } from '../src/definitions/index.js';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const rootDir = path.resolve(__dirname, '..');
18
+
19
+ // Configuration
20
+ const config = {
21
+ docsDir: path.join(rootDir, 'docs'),
22
+ locales: ['en', 'ms'],
23
+ categories: {
24
+ layout: 'layout',
25
+ space: 'space',
26
+ visual: 'visual'
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Generate markdown for a single definition
32
+ */
33
+ function generateMarkdown(definition, locale = 'en') {
34
+ const isMs = locale === 'ms';
35
+ const lines = [];
36
+
37
+ // Title
38
+ lines.push(`# ${formatTitle(definition.name)}`);
39
+ lines.push('');
40
+
41
+ // Description
42
+ const description = isMs ? definition.descriptionMs : definition.description;
43
+ lines.push(description);
44
+ lines.push('');
45
+
46
+ // Syntax section (if available)
47
+ if (definition.syntax) {
48
+ lines.push(isMs ? '## Sintaks' : '## Syntax');
49
+ lines.push('```');
50
+ lines.push(definition.syntax);
51
+ lines.push('```');
52
+ lines.push('');
53
+ }
54
+
55
+ // Values table
56
+ if (definition.values && definition.values.length > 0) {
57
+ lines.push(isMs ? '## Nilai' : '## Values');
58
+ lines.push('');
59
+
60
+ // Check if values have properties (like space definitions)
61
+ const hasProperty = definition.values[0].property !== undefined;
62
+
63
+ if (hasProperty) {
64
+ lines.push(isMs
65
+ ? '| Properti | CSS Output | Huraian |'
66
+ : '| Property | CSS Output | Description |');
67
+ lines.push('|--------|------------|-------------|');
68
+
69
+ for (const v of definition.values) {
70
+ const desc = isMs ? (v.descriptionMs || v.description) : v.description;
71
+ const cssDisplay = v.css.replace(/{value}/g, '{value}').replace(/;/g, '');
72
+ lines.push(`| \`${v.property}\` | \`${cssDisplay}\` | ${desc} |`);
73
+ }
74
+ } else {
75
+ lines.push(isMs
76
+ ? '| Nilai | CSS Output | Huraian |'
77
+ : '| Value | CSS Output | Description |');
78
+ lines.push('|-------|------------|-------------|');
79
+
80
+ for (const v of definition.values) {
81
+ const desc = isMs ? (v.descriptionMs || v.description) : v.description;
82
+ const cssDisplay = v.css.replace(/;$/, '');
83
+ lines.push(`| \`${v.value}\` | \`${cssDisplay}\` | ${desc} |`);
84
+ }
85
+ }
86
+ lines.push('');
87
+ }
88
+
89
+ // Scale values (if applicable)
90
+ if (definition.scaleValues) {
91
+ lines.push(isMs ? '## Nilai Skala' : '## Scale Values');
92
+ lines.push('');
93
+ lines.push(definition.scaleValues.map(v => `\`${v}\``).join(', '));
94
+ lines.push('');
95
+ }
96
+
97
+ // Examples
98
+ if (definition.examples && definition.examples.length > 0) {
99
+ lines.push(isMs ? '## Contoh' : '## Examples');
100
+ lines.push('');
101
+ lines.push('```html');
102
+ for (const example of definition.examples) {
103
+ lines.push(example.code);
104
+ }
105
+ lines.push('```');
106
+ lines.push('');
107
+ }
108
+
109
+ // Preview (live HTML demos)
110
+ if (definition.preview && definition.preview.length > 0) {
111
+ lines.push(isMs ? '## Pratonton' : '## Preview');
112
+ lines.push('');
113
+
114
+ for (let i = 0; i < definition.preview.length; i++) {
115
+ const p = definition.preview[i];
116
+ const title = isMs ? (p.titleMs || p.title) : p.title;
117
+
118
+ // Wrapper container for each preview
119
+ lines.push('<div space="p-x:big p-b:medium m-t:medium" visual="border-w:thin border:neutral-100 dark:border:neutral-800 rounded:medium">');
120
+ lines.push('');
121
+
122
+ // Title for this preview
123
+ if (title) {
124
+ lines.push(`### ${title}`);
125
+ lines.push('');
126
+ }
127
+
128
+ // Live preview
129
+ lines.push(`<div layout="flex col" space="g:medium">`);
130
+
131
+ // Description with highlighted code (above the preview)
132
+ const desc = isMs ? p.descriptionMs : p.description;
133
+ const highlightCode = p.highlightValue ? `<code>${definition.property}="${p.highlightValue}"</code>` : '';
134
+ lines.push(` <p space="m:none" visual="text:neutral-600 dark:text:neutral-400 text-sm">${highlightCode}${highlightCode && desc ? ' - ' : ''}${desc}</p>`);
135
+
136
+ lines.push(p.html);
137
+ lines.push('</div>');
138
+ lines.push('');
139
+
140
+ // Code block showing the HTML used (collapsible)
141
+ lines.push('<details>');
142
+ lines.push(`<summary>${isMs ? 'Lihat Kod' : 'View Code'}</summary>`);
143
+ lines.push('');
144
+ lines.push('```html');
145
+ lines.push(p.html);
146
+ lines.push('```');
147
+ lines.push('');
148
+ lines.push('</details>');
149
+ lines.push('');
150
+
151
+ // Close wrapper container
152
+ lines.push('</div>');
153
+ lines.push('');
154
+ }
155
+ }
156
+
157
+ // Arbitrary values support
158
+ if (definition.supportsArbitrary) {
159
+ lines.push(isMs ? '## Nilai Arbitrari' : '## Arbitrary Values');
160
+ lines.push('');
161
+ lines.push(isMs
162
+ ? 'Sokong nilai tersuai menggunakan sintaks kurungan segi empat:'
163
+ : 'Supports custom values using bracket syntax:');
164
+ lines.push('');
165
+ lines.push('```html');
166
+ lines.push(`<div ${definition.property}="${definition.name.split('-')[0]}:[custom-value]">Custom</div>`);
167
+ lines.push('```');
168
+ lines.push('');
169
+ }
170
+
171
+ // Footnotes (e.g., Tailwind scale compatibility notes)
172
+ if (definition.footnotes && definition.footnotes.length > 0) {
173
+ lines.push(isMs ? '## Nota' : '## Notes');
174
+ lines.push('');
175
+
176
+ for (const footnote of definition.footnotes) {
177
+ const title = isMs ? (footnote.titleMs || footnote.title) : footnote.title;
178
+ const content = isMs ? (footnote.contentMs || footnote.content) : footnote.content;
179
+
180
+ // Use VitePress TIP alert for footnotes
181
+ lines.push('> [!TIP]');
182
+ lines.push(`> **${title}**`);
183
+ lines.push(`> `);
184
+ lines.push(`> ${content}`);
185
+ if (footnote.link) {
186
+ lines.push(`> `);
187
+ lines.push(`> [${isMs ? 'Rujukan' : 'Reference'}](${footnote.link})`);
188
+ }
189
+ lines.push('');
190
+ }
191
+ }
192
+
193
+ // Responsive
194
+ // lines.push(isMs ? '## Responsif' : '## Responsive');
195
+ // lines.push('');
196
+ // lines.push('```html');
197
+ // if (definition.examples && definition.examples[0]) {
198
+ // const baseExample = definition.examples[0].code;
199
+ // lines.push(`<!-- ${isMs ? 'Contoh responsif' : 'Responsive example'} -->`);
200
+ // lines.push(`<div ${definition.property}="mob:... tab:... lap:...">`);
201
+ // lines.push(` ${isMs ? 'Kandungan responsif' : 'Responsive content'}`);
202
+ // lines.push('</div>');
203
+ // }
204
+ // lines.push('```');
205
+ // lines.push('');
206
+
207
+ return lines.join('\n');
208
+ }
209
+
210
+ /**
211
+ * Format definition name as title
212
+ */
213
+ function formatTitle(name) {
214
+ return name
215
+ .split('-')
216
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
217
+ .join(' ');
218
+ }
219
+
220
+ /**
221
+ * Get output path for a definition
222
+ */
223
+ function getOutputPath(definition, locale) {
224
+ const category = config.categories[definition.category] || 'other';
225
+ const filename = `${definition.name}.md`;
226
+
227
+ if (locale === 'ms') {
228
+ return path.join(config.docsDir, 'ms', 'reference', category, filename);
229
+ }
230
+ return path.join(config.docsDir, 'reference', category, filename);
231
+ }
232
+
233
+ /**
234
+ * Ensure directory exists
235
+ */
236
+ function ensureDir(filePath) {
237
+ const dir = path.dirname(filePath);
238
+ if (!fs.existsSync(dir)) {
239
+ fs.mkdirSync(dir, { recursive: true });
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Generate all documentation
245
+ */
246
+ function generateAllDocs(options = {}) {
247
+ const { dryRun = false, locale = 'all' } = options;
248
+ const definitions = getAllDefinitions();
249
+ const results = { generated: [], skipped: [], errors: [] };
250
+
251
+ const locales = locale === 'all' ? config.locales : [locale];
252
+
253
+ console.log(`\nšŸ“š SenangStart CSS Documentation Generator`);
254
+ console.log(` Generating docs for ${definitions.length} definitions...`);
255
+ if (dryRun) {
256
+ console.log(` (Dry run - no files will be written)\n`);
257
+ }
258
+
259
+ for (const def of definitions) {
260
+ for (const loc of locales) {
261
+ try {
262
+ const outputPath = getOutputPath(def, loc);
263
+ const markdown = generateMarkdown(def, loc);
264
+
265
+ if (dryRun) {
266
+ console.log(` Would create: ${path.relative(rootDir, outputPath)}`);
267
+ results.generated.push(outputPath);
268
+ } else {
269
+ ensureDir(outputPath);
270
+ fs.writeFileSync(outputPath, markdown, 'utf8');
271
+ console.log(` āœ“ Created: ${path.relative(rootDir, outputPath)}`);
272
+ results.generated.push(outputPath);
273
+ }
274
+ } catch (error) {
275
+ console.error(` āœ— Error generating ${def.name} (${loc}): ${error.message}`);
276
+ results.errors.push({ definition: def.name, locale: loc, error: error.message });
277
+ }
278
+ }
279
+ }
280
+
281
+ // Summary
282
+ console.log(`\nšŸ“Š Summary:`);
283
+ console.log(` Generated: ${results.generated.length} files`);
284
+ console.log(` Skipped: ${results.skipped.length} files`);
285
+ console.log(` Errors: ${results.errors.length} files`);
286
+
287
+ return results;
288
+ }
289
+
290
+ /**
291
+ * Compare generated docs with existing docs
292
+ * Returns list of differences
293
+ */
294
+ function compareDocs(options = {}) {
295
+ const definitions = getAllDefinitions();
296
+ const differences = [];
297
+
298
+ console.log(`\nšŸ” Comparing definitions with existing documentation...`);
299
+
300
+ for (const def of definitions) {
301
+ for (const locale of config.locales) {
302
+ const outputPath = getOutputPath(def, locale);
303
+
304
+ if (!fs.existsSync(outputPath)) {
305
+ differences.push({
306
+ type: 'missing',
307
+ definition: def.name,
308
+ locale,
309
+ path: outputPath
310
+ });
311
+ continue;
312
+ }
313
+
314
+ const existingContent = fs.readFileSync(outputPath, 'utf8');
315
+ const generatedContent = generateMarkdown(def, locale);
316
+
317
+ // Simple comparison - check if values table matches
318
+ // This is a basic check; could be made more sophisticated
319
+ const existingValues = extractValuesFromTable(existingContent);
320
+ const definedValues = def.values?.map(v => v.value || v.property) || [];
321
+
322
+ const missingInDocs = definedValues.filter(v => !existingValues.includes(v));
323
+ const extraInDocs = existingValues.filter(v => !definedValues.includes(v));
324
+
325
+ if (missingInDocs.length > 0 || extraInDocs.length > 0) {
326
+ differences.push({
327
+ type: 'mismatch',
328
+ definition: def.name,
329
+ locale,
330
+ path: outputPath,
331
+ missingInDocs,
332
+ extraInDocs
333
+ });
334
+ }
335
+ }
336
+ }
337
+
338
+ if (differences.length === 0) {
339
+ console.log(' āœ“ All documentation is in sync!\n');
340
+ } else {
341
+ console.log(` ⚠ Found ${differences.length} differences:\n`);
342
+ for (const diff of differences) {
343
+ if (diff.type === 'missing') {
344
+ console.log(` Missing: ${diff.definition} (${diff.locale})`);
345
+ } else {
346
+ console.log(` Mismatch: ${diff.definition} (${diff.locale})`);
347
+ if (diff.missingInDocs?.length > 0) {
348
+ console.log(` - Missing in docs: ${diff.missingInDocs.join(', ')}`);
349
+ }
350
+ if (diff.extraInDocs?.length > 0) {
351
+ console.log(` - Extra in docs: ${diff.extraInDocs.join(', ')}`);
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ return differences;
358
+ }
359
+
360
+ /**
361
+ * Extract values from a markdown table
362
+ */
363
+ function extractValuesFromTable(markdown) {
364
+ const values = [];
365
+ const lines = markdown.split('\n');
366
+
367
+ for (let line of lines) {
368
+ line = line.trim();
369
+ // Skip non-table lines, header rows, and separator rows
370
+ if (!line.startsWith('|')) continue;
371
+ if (line.includes('Value') || line.includes('Nilai') || line.includes('Property') || line.includes('Properti')) continue;
372
+ if (line.includes('---')) continue;
373
+
374
+ // Extract first cell (value name) - look for backtick-quoted value
375
+ const cellMatch = line.match(/\|\s*`([^`]+)`/);
376
+ if (cellMatch) {
377
+ values.push(cellMatch[1]);
378
+ }
379
+ }
380
+
381
+ return values;
382
+ }
383
+
384
+ // CLI handling
385
+ const args = process.argv.slice(2);
386
+ const options = {
387
+ dryRun: args.includes('--dry-run'),
388
+ locale: 'all',
389
+ compare: args.includes('--compare')
390
+ };
391
+
392
+ // Parse locale option
393
+ const localeIndex = args.indexOf('--locale');
394
+ if (localeIndex !== -1 && args[localeIndex + 1]) {
395
+ options.locale = args[localeIndex + 1];
396
+ }
397
+
398
+ // Run
399
+ if (options.compare) {
400
+ compareDocs(options);
401
+ } else {
402
+ generateAllDocs(options);
403
+ }