@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.
- package/.agent/skills/add-utility/SKILL.md +65 -0
- package/.agent/workflows/add-utility.md +2 -0
- package/.agent/workflows/build.md +2 -0
- package/.agent/workflows/dev.md +2 -0
- package/AGENTS.md +30 -0
- package/dist/senangstart-css.js +607 -180
- package/dist/senangstart-css.min.js +234 -195
- package/dist/senangstart-tw.js +274 -8
- package/dist/senangstart-tw.min.js +1 -1
- package/docs/SYNTAX-REFERENCE.md +1731 -1590
- package/docs/guide/preflight.md +20 -1
- package/docs/ms/guide/preflight.md +19 -0
- package/docs/ms/reference/breakpoints.md +14 -0
- package/docs/ms/reference/visual/border-radius.md +50 -10
- package/docs/ms/reference/visual/contain.md +57 -0
- package/docs/ms/reference/visual/content-visibility.md +53 -0
- package/docs/ms/reference/visual/placeholder-color.md +92 -0
- package/docs/ms/reference/visual/ring-color.md +2 -2
- package/docs/ms/reference/visual/ring-offset.md +3 -3
- package/docs/ms/reference/visual/ring.md +5 -5
- package/docs/ms/reference/visual/writing-mode.md +53 -0
- package/docs/ms/reference/visual.md +6 -0
- package/docs/public/assets/senangstart-css.min.js +234 -195
- package/docs/public/llms.txt +45 -12
- package/docs/reference/breakpoints.md +14 -0
- package/docs/reference/visual/border-radius.md +50 -10
- package/docs/reference/visual/contain.md +57 -0
- package/docs/reference/visual/content-visibility.md +53 -0
- package/docs/reference/visual/placeholder-color.md +92 -0
- package/docs/reference/visual/ring-color.md +2 -2
- package/docs/reference/visual/ring-offset.md +3 -3
- package/docs/reference/visual/ring.md +5 -5
- package/docs/reference/visual/writing-mode.md +53 -0
- package/docs/reference/visual.md +7 -0
- package/docs/syntax-reference.json +2185 -2009
- package/package.json +1 -1
- package/scripts/convert-tailwind.js +300 -26
- package/scripts/generate-docs.js +403 -403
- package/src/cdn/senangstart-engine.js +5 -5
- package/src/cdn/tw-conversion-engine.js +305 -8
- package/src/cli/commands/build.js +51 -13
- package/src/cli/commands/dev.js +157 -93
- package/src/compiler/generators/css.js +467 -208
- package/src/compiler/generators/preflight.js +26 -13
- package/src/compiler/generators/typescript.js +3 -1
- package/src/compiler/index.js +27 -3
- package/src/compiler/parser.js +13 -6
- package/src/compiler/tokenizer.js +25 -23
- package/src/config/defaults.js +3 -0
- package/src/core/tokenizer-core.js +46 -19
- package/src/definitions/index.js +4 -1
- package/src/definitions/visual-borders.js +10 -10
- package/src/definitions/visual-performance.js +126 -0
- package/src/definitions/visual.js +25 -9
- package/src/utils/common.js +456 -27
- package/src/utils/node-io.js +82 -0
- package/tests/integration/dev-recovery.test.js +231 -0
- package/tests/unit/cli/memory-limits.test.js +169 -0
- package/tests/unit/compiler/css-generation-error-handling.test.js +204 -0
- package/tests/unit/compiler/generators/css-errors.test.js +102 -0
- package/tests/unit/compiler/generators/css.test.js +102 -5
- package/tests/unit/convert-tailwind.test.js +518 -431
- package/tests/unit/utils/common.test.js +376 -26
- package/tests/unit/utils/file-timeout.test.js +154 -0
- package/tests/unit/utils/theme-validation.test.js +181 -0
- package/tests/unit/compiler/generators/css.coverage.test.js +0 -833
- package/tests/unit/convert-tailwind.cli.test.js +0 -95
- package/tests/unit/security.test.js +0 -206
- /package/tests/unit/{convert-tailwind.coverage.test.js ā convert-tailwind-edgecases.test.js} +0 -0
package/scripts/generate-docs.js
CHANGED
|
@@ -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
|
+
}
|