@vanilla-bean/components 1.1.0 → 2.0.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 (75) hide show
  1. package/Component/Component.js +226 -40
  2. package/Component/Component.scenarios.js +2 -2
  3. package/Component/Component.test.js +427 -40
  4. package/Component/README.md +480 -0
  5. package/Component/observeElementConnection.js +1 -1
  6. package/Elem/README.md +373 -0
  7. package/README.md +2 -2
  8. package/components/BottomSheet/BottomSheet.js +8 -16
  9. package/components/BottomSheet/README.md +1 -1
  10. package/components/Button/Button.js +12 -18
  11. package/components/Button/Button.lld.md +1 -1
  12. package/components/Calendar/Calendar.js +139 -59
  13. package/components/Calendar/CalendarEvent.js +9 -4
  14. package/components/Calendar/Toolbar.js +28 -20
  15. package/components/Calendar/index.js +1 -0
  16. package/components/Code/Code.js +26 -30
  17. package/components/Code/Code.lld.md +1 -1
  18. package/components/ColorPicker/ColorPicker.js +58 -58
  19. package/components/Dialog/Dialog.js +79 -73
  20. package/components/Dialog/Dialog.lld.md +1 -1
  21. package/components/Dialog/README.md +10 -10
  22. package/components/Form/Form.js +21 -17
  23. package/components/Form/Form.lld.md +2 -2
  24. package/components/Form/README.md +3 -3
  25. package/components/Icon/Icon.js +19 -11
  26. package/components/Input/Input.js +85 -67
  27. package/components/Input/README.md +7 -9
  28. package/components/Keyboard/Key.js +7 -10
  29. package/components/Keyboard/Keyboard.js +38 -46
  30. package/components/Keyboard/Keyboard.lld.md +1 -1
  31. package/components/Label/Label.js +52 -50
  32. package/components/Link/Link.js +14 -20
  33. package/components/List/List.js +28 -30
  34. package/components/Menu/Menu.js +19 -10
  35. package/components/Menu/Menu.lld.md +2 -2
  36. package/components/Notify/Notify.js +29 -19
  37. package/components/Notify/Notify.lld.md +2 -2
  38. package/components/Page/Page.lld.md +1 -1
  39. package/components/Popover/Popover.js +49 -44
  40. package/components/RadioButton/RadioButton.js +33 -29
  41. package/components/RadioButton/RadioButton.lld.md +1 -1
  42. package/components/Router/README.md +12 -12
  43. package/components/Router/Router.js +19 -21
  44. package/components/Select/README.md +5 -5
  45. package/components/Select/Select.js +25 -31
  46. package/components/Table/README.md +4 -4
  47. package/components/Table/Table.js +55 -36
  48. package/components/Table/Table.lld.md +1 -1
  49. package/components/TagList/Tag.js +16 -14
  50. package/components/TagList/TagList.js +15 -8
  51. package/components/TagList/TagList.lld.md +1 -1
  52. package/components/Tooltip/Tooltip.js +12 -25
  53. package/components/TooltipWrapper/TooltipWrapper.js +55 -49
  54. package/components/TooltipWrapper/TooltipWrapper.lld.md +2 -2
  55. package/components/Whiteboard/Whiteboard.js +45 -43
  56. package/components/Whiteboard/Whiteboard.lld.md +1 -1
  57. package/devTools/build.js +43 -0
  58. package/devTools/buildTypes.js +322 -0
  59. package/devTools/createComponent.js +155 -0
  60. package/devTools/extractJSDoc.js +395 -0
  61. package/devTools/processTemplate.js +500 -0
  62. package/devTools/updateComponentIndex.js +16 -0
  63. package/devTools/updateDemoViewIndex.js +90 -0
  64. package/eslint.config.cjs +5 -2
  65. package/index.d.ts +120 -62
  66. package/package.json +23 -22
  67. package/spellcheck.config.cjs +3 -0
  68. package/styled/README.md +329 -0
  69. package/theme/.test.js +2 -2
  70. package/theme/README.md +8 -8
  71. package/theme/colors.js +16 -13
  72. package/theme/colors.test.js +28 -0
  73. package/utils/browser.js +1 -1
  74. package/utils/element.js +2 -2
  75. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
@@ -0,0 +1,155 @@
1
+ import fs from 'fs';
2
+
3
+ import { capitalize } from '../utils/string';
4
+
5
+ if (!process.argv[2]) {
6
+ console.error('Usage: bun run create:component <ComponentName> [optionsJSON]');
7
+ process.exit(1);
8
+ }
9
+
10
+ let options = {};
11
+ try {
12
+ options = process.argv[3] ? JSON.parse(process.argv[3]) : {};
13
+ } catch {
14
+ console.error('Invalid JSON options:', process.argv[3]);
15
+ process.exit(1);
16
+ }
17
+
18
+ const name = capitalize(process.argv[2]);
19
+ const componentFolder = `components/${name}`;
20
+
21
+ if (fs.existsSync(componentFolder)) {
22
+ console.error(`Component '${name}' already exists at ${componentFolder}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ fs.mkdirSync(componentFolder);
27
+
28
+ const demoOptions = Object.entries(options)
29
+ .map(
30
+ ([key, value]) =>
31
+ `${key}: ${
32
+ typeof value === 'object'
33
+ ? JSON.stringify(value)
34
+ .replaceAll(/"(\w+)":/g, ` $1: `)
35
+ .replaceAll('"', `'`)
36
+ : value
37
+ },`,
38
+ )
39
+ .join('\n');
40
+
41
+ const testFile = `import { ${name} } from '.';
42
+
43
+ describe('${name}', () => {
44
+ test('renders', () => {
45
+ new ${name}({ appendTo: container });
46
+
47
+ expect(container.firstElementChild).toBeDefined();
48
+ });
49
+ });
50
+ `;
51
+
52
+ const demoFile = `import DemoView from '../../demo/DemoView';
53
+ import { ${name} } from '.';
54
+
55
+ export default class Demo extends DemoView {
56
+ build() {
57
+ this.component = new ${name}({
58
+ ${demoOptions}
59
+ });
60
+ }
61
+ }
62
+ `;
63
+
64
+ const readMeFile = `# ${name}
65
+
66
+ [[extract-description ${name}.js]]
67
+
68
+ ## Usage
69
+
70
+ \`\`\`js
71
+ [[import ./demo.js]]/^(\\t*).+component = new.+\\(\\s*{(.|\\n)+?\\1}?\\);/gm
72
+ \`\`\`
73
+
74
+ ## Options
75
+
76
+ [[extract-options ${name}.js]]
77
+
78
+ ## Methods
79
+
80
+ [[extract-methods ${name}.js]]
81
+
82
+ ## Properties
83
+
84
+ [[extract-properties ${name}.js]]
85
+
86
+ ## Events
87
+
88
+ [[extract-events ${name}.js]]
89
+
90
+ ## Dependencies
91
+
92
+ [[extract-imports ${name}.js]]
93
+
94
+ ## Design
95
+
96
+ ![design](../${name}/design.excalidraw.png)
97
+ `;
98
+
99
+ const componentFile = `import { Component } from '../../Component';
100
+
101
+ /**
102
+ * [Component description - describe what this component does and its primary purpose]
103
+ *
104
+ * [Optional: Additional context about when to use this component or special behaviors]
105
+ *
106
+ * @param {object} [options={}] - Component configuration options
107
+ * @param {...(Component|HTMLElement|string)} children - Child elements to append
108
+ * @returns {${name}} Component instance with reactive options
109
+ * @example
110
+ * // Basic usage
111
+ * new ${name}({
112
+ * // Add example options here
113
+ * });
114
+ */
115
+ export default class ${name} extends Component {
116
+ /**
117
+ * Creates component structure before options are processed.
118
+ * Assign child elements to instance properties here so handlers can reference them.
119
+ */
120
+ build() {
121
+ // Create child elements here - this runs before options are processed
122
+ }
123
+
124
+ // Declare the option schema once with static schema - what can exist, the defaults,
125
+ // and how each key routes. Descriptor fields:
126
+ // default - initial value, merged automatically (child classes override per-key)
127
+ // set(value, next) - change handler; omit next() to own the key, call it to continue
128
+ // enum: [...] - valid values; anything else throws
129
+ // attribute: true / priority: true - setAttribute routing / processed first
130
+ // Declared keys with no DOM match live in this.options silently - a bare {} declares
131
+ // a data-only key. data: true exists only to force store-only on DOM-name collisions.
132
+ //
133
+ // static schema = {
134
+ // label: {
135
+ // default: 'Untitled',
136
+ // set(value) {
137
+ // this.myChild.elem.textContent = value;
138
+ // },
139
+ // },
140
+ // items: {},
141
+ // };
142
+ //
143
+ // Custom events: static events = ['select'];
144
+ // Computed defaults (no constructor needed): static prepareOptions(options, children) { return options; }
145
+ }
146
+ `;
147
+
148
+ await Bun.write(`${componentFolder}/${name}.js`, componentFile);
149
+ await Bun.write(`${componentFolder}/.test.js`, testFile);
150
+ await Bun.write(`${componentFolder}/demo.js`, demoFile);
151
+ await Bun.write(`${componentFolder}/README.md`, readMeFile);
152
+ await Bun.write(`${componentFolder}/index.js`, `export { default as ${name} } from './${name}';`);
153
+
154
+ await Bun.spawn(['bun', 'run', 'build:index']).exited;
155
+ await Bun.spawn(['bun', 'run', 'format']).exited;
@@ -0,0 +1,395 @@
1
+ import { readFileSync } from 'fs';
2
+
3
+ /**
4
+ * Extracts JSDoc information from JavaScript files for documentation generation.
5
+ *
6
+ * Parses JSDoc comments and extracts component descriptions, parameters, methods,
7
+ * properties, and events for automated README generation.
8
+ * @param {string} filePath - Path to JavaScript file to parse
9
+ * @returns {object} Extracted JSDoc information structured for documentation templates
10
+ */
11
+ export function extractJSDoc(filePath) {
12
+ try {
13
+ const content = readFileSync(filePath, 'utf8');
14
+
15
+ return {
16
+ description: extractDescription(content),
17
+ options: extractOptions(content),
18
+ methods: extractMethods(content),
19
+ properties: extractProperties(content),
20
+ events: extractEvents(content),
21
+ imports: extractImports(content),
22
+ examples: extractExamples(content),
23
+ };
24
+ } catch (error) {
25
+ console.warn(`Failed to extract JSDoc from ${filePath}:`, error.message);
26
+ return {
27
+ description: '',
28
+ options: [],
29
+ methods: [],
30
+ properties: [],
31
+ events: [],
32
+ imports: [],
33
+ examples: [],
34
+ };
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Extracts component description from class-level JSDoc comment.
40
+ * @param {string} content - File content to parse
41
+ * @returns {string} Component description text
42
+ */
43
+ function extractDescription(content) {
44
+ // Match class JSDoc comment - handle both export default class and class patterns
45
+ const classMatches = [
46
+ content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*export\s+default\s+class\s+\w+/),
47
+ content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*(?:export\s+)?class\s+\w+/),
48
+ ];
49
+
50
+ const classMatch = classMatches.find(match => match);
51
+ if (!classMatch) return '';
52
+
53
+ const jsdoc = classMatch[1];
54
+
55
+ // Extract description (everything before first @tag)
56
+ const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
57
+
58
+ if (!descMatch) return '';
59
+
60
+ // Clean up the description text - handle multi-line descriptions
61
+ const lines = descMatch[1].split('\n');
62
+ const cleanedLines = lines.map(line => line.replace(/^\s*\*\s?/, '')).filter(line => line.trim() !== '');
63
+
64
+ return cleanedLines.join(' ').replace(/\s+/g, ' ').trim();
65
+ }
66
+
67
+ /**
68
+ * Extracts constructor options from JSDoc `@param` tags.
69
+ * @param {string} content - File content to parse
70
+ * @returns {Array} Array of option objects with name, type, description, and default
71
+ */
72
+ function extractOptions(content) {
73
+ // Find class-level JSDoc (which contains constructor info) or constructor JSDoc
74
+ const jsDocMatches = [
75
+ content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*export\s+default\s+class\s+\w+/),
76
+ content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*constructor\s*\(/),
77
+ content.match(/\/\*\*\s*\n([\s\S]*?)\*\/\s*(?:export\s+)?class\s+\w+/),
78
+ ];
79
+
80
+ const jsDocMatch = jsDocMatches.find(match => match);
81
+ if (!jsDocMatch) return [];
82
+
83
+ const jsdoc = jsDocMatch[1];
84
+
85
+ // Extract @param tags for options (handle both options.prop and direct param patterns)
86
+ const paramMatches = [
87
+ // options.property pattern
88
+ ...jsdoc.matchAll(
89
+ /^\s*\*\s*@param\s+{([^}]+)}\s+(\[?options\.(\w+)(?:=([^\]]+))?\]?)\s*-\s*(.+?)(?=\s*^\s*\*\s*@|\s*^\s*\*\s*$|\s*\*\/)/gms,
90
+ ),
91
+ // Direct parameter pattern (for constructor params like options={})
92
+ ...jsdoc.matchAll(
93
+ /^\s*\*\s*@param\s+{([^}]+)}\s+(\[?(\w+)(?:=([^\]]+))?\]?)\s*-\s*(.+?)(?=\s*^\s*\*\s*@|\s*^\s*\*\s*$|\s*\*\/)/gms,
94
+ ),
95
+ ];
96
+
97
+ const codeDefaults = extractDefaultOptionsFromCode(content);
98
+
99
+ return paramMatches
100
+ .filter(match => match[2].includes('options.'))
101
+ .map(match => {
102
+ const [, type, fullParam, name, defaultValue, description] = match;
103
+ const optional = fullParam.startsWith('[') && fullParam.endsWith(']');
104
+ const optionName = name.replace('options.', '');
105
+
106
+ return {
107
+ name: optionName,
108
+ type: type.trim(),
109
+ description: description.trim().replace(/\s+/g, ' '),
110
+ optional,
111
+ default: defaultValue || codeDefaults[optionName] || null,
112
+ isSubProperty: fullParam.includes('options.'),
113
+ };
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Reads the `defaultOptions` object literal from source and extracts simple key→value pairs.
119
+ * Only captures string, number, and boolean literals - skips getters and complex values.
120
+ * @param {string} content - File content
121
+ * @returns {object} Map of option name → default value string
122
+ */
123
+ function extractDefaultOptionsFromCode(content) {
124
+ const defaults = {};
125
+ const block = content.match(/const\s+defaultOptions\s*=\s*\{([\s\S]*?)\};/);
126
+ if (!block) return defaults;
127
+
128
+ const pairs = [
129
+ ...block[1].matchAll(/\b(\w+):\s*(?:'([^']*)'|"([^"]*)"|(`[^`]*`)|(-?\d+\.?\d*(?:e[+-]?\d+)?)|true|false)/g),
130
+ ];
131
+
132
+ pairs.forEach(match => {
133
+ const [full, key, singleQuoted, doubleQuoted, template, number] = match;
134
+ if (singleQuoted !== undefined) defaults[key] = `'${singleQuoted}'`;
135
+ else if (doubleQuoted !== undefined) defaults[key] = `"${doubleQuoted}"`;
136
+ else if (template !== undefined) defaults[key] = template;
137
+ else if (number !== undefined) defaults[key] = number;
138
+ else if (full.endsWith('true')) defaults[key] = 'true';
139
+ else if (full.endsWith('false')) defaults[key] = 'false';
140
+ });
141
+
142
+ return defaults;
143
+ }
144
+
145
+ /**
146
+ * Extracts method information from JSDoc comments.
147
+ * @param {string} content - File content to parse
148
+ * @returns {Array} Array of method objects with name, description, parameters, and return info
149
+ */
150
+ function extractMethods(content) {
151
+ // Match method JSDoc comments (excluding constructor and private methods)
152
+ // Match JSDoc comments that are within the class body, not at class level
153
+
154
+ // Find class body content (everything between class declaration and closing brace)
155
+ const classBodyMatch = content.match(/class\s+\w+[^{]*\{([\s\S]*)\}[^}]*$/);
156
+ if (!classBodyMatch) return [];
157
+
158
+ const classBody = classBodyMatch[1];
159
+
160
+ // Match each JSDoc block with the method that IMMEDIATELY follows it.
161
+ // `(?:(?!\/\*\*)[\s\S])*?` - non-greedy content that cannot cross into a new `/**`,
162
+ // so the captured block can never span multiple JSDoc comments or method bodies.
163
+ // get/set accessors are excluded; underscore-prefixed names are excluded.
164
+ const matches = [
165
+ ...classBody.matchAll(
166
+ /\/\*\*((?:(?!\/\*\*)[\s\S])*?)\*\/[ \t]*\n[ \t]*(?!constructor\b)(?!get\b)(?!set\b)(?!_)(\w+)\s*\([^)]*\)\s*\{/g,
167
+ ),
168
+ ];
169
+
170
+ return matches.map(match => {
171
+ const [, jsdoc, name] = match;
172
+
173
+ // Extract description
174
+ const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
175
+ const description = descMatch
176
+ ? descMatch[1]
177
+ .split('\n')
178
+ .map(line => line.replace(/^\s*\*\s?/, ''))
179
+ .join(' ')
180
+ .replace(/\s+/g, ' ')
181
+ .trim()
182
+ : '';
183
+
184
+ // Extract parameters
185
+ const paramMatches = [
186
+ ...jsdoc.matchAll(
187
+ /^\s*\*\s*@param\s+{([^}]+)}\s+(\[?(\w+)(?:=([^\]]+))?\]?)\s*-\s*(.+?)(?=\s*^\s*\*\s*@|\s*^\s*\*\s*$)/gms,
188
+ ),
189
+ ];
190
+
191
+ const parameters = paramMatches.map(paramMatch => {
192
+ const [, type, fullParam, name, defaultValue, desc] = paramMatch;
193
+ const optional = fullParam.startsWith('[') && fullParam.endsWith(']');
194
+
195
+ return {
196
+ name,
197
+ type: type.trim(),
198
+ description: desc.trim().replace(/\s+/g, ' '),
199
+ optional,
200
+ default: defaultValue || null,
201
+ };
202
+ });
203
+
204
+ // Extract return information - type only; description is not used in type generation.
205
+ const returnMatch = jsdoc.match(/^\s*\*\s*@returns?\s+\{([^}]+)\}/m);
206
+ const returnInfo = returnMatch ? { type: returnMatch[1].trim() } : null;
207
+
208
+ return {
209
+ name,
210
+ description,
211
+ parameters,
212
+ returns: returnInfo,
213
+ };
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Extracts property information from JSDoc comments and getter methods.
219
+ * @param {string} content - File content to parse
220
+ * @returns {Array} Array of property objects with name, type, description, and access info
221
+ */
222
+ function extractProperties(content) {
223
+ const properties = [];
224
+
225
+ // Extract getter properties - same anti-span pattern as extractMethods.
226
+ const getterMatches = [
227
+ ...content.matchAll(/\/\*\*((?:(?!\/\*\*)[\s\S])*?)\*\/[ \t]*\n[ \t]*get\s+(\w+)\s*\(\)\s*\{/g),
228
+ ];
229
+
230
+ getterMatches.forEach(match => {
231
+ const [, jsdoc, name] = match;
232
+
233
+ const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
234
+ const description = descMatch
235
+ ? descMatch[1]
236
+ .split('\n')
237
+ .map(line => line.replace(/^\s*\*\s?/, ''))
238
+ .join(' ')
239
+ .replace(/\s+/g, ' ')
240
+ .trim()
241
+ : '';
242
+
243
+ const returnMatch = jsdoc.match(/^\s*\*\s*@returns?\s+\{([^}]+)\}/m);
244
+ const type = returnMatch ? returnMatch[1].trim() : 'unknown';
245
+
246
+ properties.push({
247
+ name,
248
+ type,
249
+ description,
250
+ access: 'readonly',
251
+ });
252
+ });
253
+
254
+ // Extract documented properties (this.property assignments with JSDoc)
255
+ const propMatches = [...content.matchAll(/\/\*\*((?:(?!\/\*\*)[\s\S])*?)\*\/[ \t]*\n[ \t]*(?:this\.)?(\w+)\s*=/g)];
256
+
257
+ propMatches.forEach(match => {
258
+ const [, jsdoc, name] = match;
259
+
260
+ const descMatch = jsdoc.match(/^\s*\*\s*(.+?)(?=\s*^\s*\*\s*@|\s*$)/ms);
261
+ const description = descMatch
262
+ ? descMatch[1]
263
+ .split('\n')
264
+ .map(line => line.replace(/^\s*\*\s?/, ''))
265
+ .join(' ')
266
+ .replace(/\s+/g, ' ')
267
+ .trim()
268
+ : '';
269
+
270
+ const typeMatch = jsdoc.match(/^\s*\*\s*@type\s+{([^}]+)}/m);
271
+ const type = typeMatch ? typeMatch[1].trim() : 'unknown';
272
+
273
+ properties.push({
274
+ name,
275
+ type,
276
+ description,
277
+ access: 'read-write',
278
+ });
279
+ });
280
+
281
+ return properties;
282
+ }
283
+
284
+ /**
285
+ * Extracts event information from emit() calls and JSDoc `@fires` tags.
286
+ * @param {string} content - File content to parse
287
+ * @returns {Array} Array of event objects with name and description
288
+ */
289
+ function extractEvents(content) {
290
+ const events = [];
291
+
292
+ // Extract from emit() calls
293
+ const emitMatches = [...content.matchAll(/\.emit\s*\(\s*['"`]([^'"`]+)['"`]/g)];
294
+
295
+ emitMatches.forEach(match => {
296
+ const [, eventName] = match;
297
+ events.push({
298
+ name: eventName,
299
+ description: `Emitted when ${eventName} occurs`,
300
+ });
301
+ });
302
+
303
+ // Extract from dispatchEvent calls
304
+ const dispatchMatches = [...content.matchAll(/\.dispatchEvent\s*\(\s*new\s+CustomEvent\s*\(\s*['"`]([^'"`]+)['"`]/g)];
305
+
306
+ dispatchMatches.forEach(match => {
307
+ const [, eventName] = match;
308
+ if (!events.find(e => e.name === eventName)) {
309
+ events.push({
310
+ name: eventName,
311
+ description: `Custom event: ${eventName}`,
312
+ });
313
+ }
314
+ });
315
+
316
+ return events;
317
+ }
318
+
319
+ /**
320
+ * Extracts import statements to determine component dependencies.
321
+ * @param {string} content - File content to parse
322
+ * @returns {Array} Array of import objects with module and imported items
323
+ */
324
+ function extractImports(content) {
325
+ const importMatches = [
326
+ ...content.matchAll(/^import\s+(?:{([^}]+)}|\*\s+as\s+(\w+)|(\w+))\s+from\s+['"`]([^'"`]+)['"`];/gm),
327
+ ];
328
+
329
+ return importMatches
330
+ .filter(([, , , , module]) => /^\.\.\//.test(module)) // external relative only, not same-dir ./
331
+ .map(match => {
332
+ const [, namedImports, namespaceImport, defaultImport, module] = match;
333
+
334
+ let imports = [];
335
+
336
+ if (namedImports) {
337
+ imports = namedImports.split(',').map(imp => imp.trim());
338
+ } else if (namespaceImport) {
339
+ imports = [`* as ${namespaceImport}`];
340
+ } else if (defaultImport) {
341
+ imports = [defaultImport];
342
+ }
343
+
344
+ return {
345
+ module: module.replace(/^(\.\.?\/)+/, ''),
346
+ imports,
347
+ };
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Extracts example code from JSDoc `@example` tags.
353
+ * @param {string} content - File content to parse
354
+ * @returns {Array} Array of example objects with code and optional description
355
+ */
356
+ function extractExamples(content) {
357
+ const examples = [];
358
+
359
+ // Find all JSDoc blocks with @example tags
360
+ const jsdocBlocks = [...content.matchAll(/\/\*\*\s*\n([\s\S]*?)\*\//g)];
361
+
362
+ jsdocBlocks.forEach(([, jsdoc]) => {
363
+ const exampleMatches = [...jsdoc.matchAll(/^\s*\*\s*@example\s*\n([\s\S]*?)(?=^\s*\*\s*@|\s*$)/gms)];
364
+
365
+ exampleMatches.forEach(([, exampleContent]) => {
366
+ const code = exampleContent
367
+ .split('\n')
368
+ .map(line => line.replace(/^\s*\*\s?/, ''))
369
+ .join('\n')
370
+ .trim();
371
+
372
+ if (code) {
373
+ examples.push({
374
+ code,
375
+ description: '', // Could extract description if needed
376
+ });
377
+ }
378
+ });
379
+ });
380
+
381
+ return examples;
382
+ }
383
+
384
+ // CLI usage
385
+ if (import.meta.main) {
386
+ const filePath = process.argv[2];
387
+
388
+ if (!filePath) {
389
+ console.error('Usage: bun extractJSDoc.js <path-to-js-file>');
390
+ process.exit(1);
391
+ }
392
+
393
+ const result = extractJSDoc(filePath);
394
+ console.log(JSON.stringify(result, null, 2));
395
+ }