sveld 0.25.1 → 0.25.3
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/README.md +15 -0
- package/lib/ComponentParser.d.ts +14 -0
- package/lib/ComponentParser.js +283 -106
- package/lib/resolve-alias.js +11 -6
- package/lib/rollup-plugin.js +36 -27
- package/lib/writer/WriterMarkdown.d.ts +2 -1
- package/lib/writer/WriterMarkdown.js +13 -12
- package/lib/writer/writer-markdown-core.d.ts +2 -1
- package/lib/writer/writer-markdown-core.js +13 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -332,6 +332,21 @@ export let kind = "primary";
|
|
|
332
332
|
// inferred type: "string"
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
For template literal default values, `sveld` automatically infers [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) when possible:
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
339
|
+
// inferred type: `ccs-${string}` & {}
|
|
340
|
+
|
|
341
|
+
export let prefix = `prefix-`;
|
|
342
|
+
// inferred type: `prefix-` & {}
|
|
343
|
+
|
|
344
|
+
export let suffix = `-suffix`;
|
|
345
|
+
// inferred type: `-suffix` & {}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The `& {}` intersection type makes the template literal type more permissive, allowing regular strings to be assigned while preserving the template literal type for better autocomplete and type hints. This provides more precise type checking than a generic `string` type while remaining flexible. For complex expressions in template literals, the type defaults to `string` for safety.
|
|
349
|
+
|
|
335
350
|
Use the `@type` tag to explicitly document the type. In the following example, the `kind` property has an enumerated (enum) type.
|
|
336
351
|
|
|
337
352
|
Signature:
|
package/lib/ComponentParser.d.ts
CHANGED
|
@@ -112,10 +112,12 @@ export default class ComponentParser {
|
|
|
112
112
|
private readonly bindings;
|
|
113
113
|
private readonly contexts;
|
|
114
114
|
private variableInfoCache;
|
|
115
|
+
private sourceLinesCache?;
|
|
115
116
|
constructor(options?: ComponentParserOptions);
|
|
116
117
|
private static mapToArray;
|
|
117
118
|
private static assignValue;
|
|
118
119
|
private static formatComment;
|
|
120
|
+
private getCommentTags;
|
|
119
121
|
/**
|
|
120
122
|
* Finds the last comment from an array of leading comments.
|
|
121
123
|
* TypeScript directives are stripped before parsing, so we can safely take the last comment.
|
|
@@ -126,6 +128,16 @@ export default class ComponentParser {
|
|
|
126
128
|
* (e.g., Number.POSITIVE_INFINITY, Math.PI, etc.)
|
|
127
129
|
*/
|
|
128
130
|
private isNumericConstant;
|
|
131
|
+
/**
|
|
132
|
+
* Infers the TypeScript type for an expression in a template literal.
|
|
133
|
+
* Returns a type string that can be used in a template literal type.
|
|
134
|
+
*/
|
|
135
|
+
private inferExpressionType;
|
|
136
|
+
/**
|
|
137
|
+
* Infers a template literal type from a TemplateLiteral AST node.
|
|
138
|
+
* Returns a TypeScript template literal type string like `prefix-${string}`.
|
|
139
|
+
*/
|
|
140
|
+
private inferTemplateLiteralType;
|
|
129
141
|
private sourceAtPos;
|
|
130
142
|
private collectReactiveVars;
|
|
131
143
|
private addProp;
|
|
@@ -137,6 +149,8 @@ export default class ComponentParser {
|
|
|
137
149
|
private buildEventDetailFromProperties;
|
|
138
150
|
private generateContextTypeName;
|
|
139
151
|
private buildVariableInfoCache;
|
|
152
|
+
private static readonly VAR_NAME_REGEX_CACHE;
|
|
153
|
+
private static getVarNameRegexes;
|
|
140
154
|
private findVariableTypeAndDescription;
|
|
141
155
|
private parseContextValue;
|
|
142
156
|
private parseSetContextCall;
|
package/lib/ComponentParser.js
CHANGED
|
@@ -35,6 +35,7 @@ class ComponentParser {
|
|
|
35
35
|
bindings = new Map();
|
|
36
36
|
contexts = new Map();
|
|
37
37
|
variableInfoCache = new Map();
|
|
38
|
+
sourceLinesCache;
|
|
38
39
|
constructor(options) {
|
|
39
40
|
this.options = options;
|
|
40
41
|
}
|
|
@@ -54,6 +55,45 @@ class ComponentParser {
|
|
|
54
55
|
}
|
|
55
56
|
return formatted_comment;
|
|
56
57
|
}
|
|
58
|
+
getCommentTags(parsed) {
|
|
59
|
+
const tags = parsed[0]?.tags ?? [];
|
|
60
|
+
const excludedTags = new Set([
|
|
61
|
+
"type",
|
|
62
|
+
"param",
|
|
63
|
+
"returns",
|
|
64
|
+
"return",
|
|
65
|
+
"extends",
|
|
66
|
+
"restProps",
|
|
67
|
+
"slot",
|
|
68
|
+
"event",
|
|
69
|
+
"typedef",
|
|
70
|
+
]);
|
|
71
|
+
let typeTag;
|
|
72
|
+
const paramTags = [];
|
|
73
|
+
let returnsTag;
|
|
74
|
+
const additionalTags = [];
|
|
75
|
+
for (const tag of tags) {
|
|
76
|
+
if (tag.tag === "type") {
|
|
77
|
+
typeTag = tag;
|
|
78
|
+
}
|
|
79
|
+
else if (tag.tag === "param") {
|
|
80
|
+
paramTags.push(tag);
|
|
81
|
+
}
|
|
82
|
+
else if (tag.tag === "returns" || tag.tag === "return") {
|
|
83
|
+
returnsTag = tag;
|
|
84
|
+
}
|
|
85
|
+
else if (!excludedTags.has(tag.tag)) {
|
|
86
|
+
additionalTags.push(tag);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
type: typeTag,
|
|
91
|
+
param: paramTags,
|
|
92
|
+
returns: returnsTag,
|
|
93
|
+
additional: additionalTags,
|
|
94
|
+
description: parsed[0]?.description,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
57
97
|
/**
|
|
58
98
|
* Finds the last comment from an array of leading comments.
|
|
59
99
|
* TypeScript directives are stripped before parsing, so we can safely take the last comment.
|
|
@@ -91,6 +131,119 @@ class ComponentParser {
|
|
|
91
131
|
}
|
|
92
132
|
return false;
|
|
93
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Infers the TypeScript type for an expression in a template literal.
|
|
136
|
+
* Returns a type string that can be used in a template literal type.
|
|
137
|
+
*/
|
|
138
|
+
inferExpressionType(expr) {
|
|
139
|
+
if (!expr || typeof expr !== "object" || !("type" in expr)) {
|
|
140
|
+
return "string";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* @example `"hello"` → `"hello"`
|
|
144
|
+
* @example `42` → `42`
|
|
145
|
+
* @example `true` → `true`
|
|
146
|
+
*/
|
|
147
|
+
if (expr.type === "Literal") {
|
|
148
|
+
if (typeof expr.value === "string") {
|
|
149
|
+
return `"${expr.value}"`;
|
|
150
|
+
}
|
|
151
|
+
if (typeof expr.value === "number") {
|
|
152
|
+
return `${expr.value}`;
|
|
153
|
+
}
|
|
154
|
+
if (typeof expr.value === "boolean") {
|
|
155
|
+
return `${expr.value}`;
|
|
156
|
+
}
|
|
157
|
+
return "string";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* @example `someVar` → `string`
|
|
161
|
+
*/
|
|
162
|
+
if (expr.type === "Identifier") {
|
|
163
|
+
// For variables, we can't know the exact type, so default to string
|
|
164
|
+
// Users can add explicit @type annotations if they need more precision
|
|
165
|
+
return "string";
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* @example `Math.PI` → `number`
|
|
169
|
+
* @example `Number.MAX_VALUE` → `number`
|
|
170
|
+
* @example `Math.random().toString` → `string`
|
|
171
|
+
*/
|
|
172
|
+
if (expr.type === "MemberExpression") {
|
|
173
|
+
// Check if it's a well-known numeric constant
|
|
174
|
+
if (this.isNumericConstant(expr)) {
|
|
175
|
+
return "number";
|
|
176
|
+
}
|
|
177
|
+
// For other member expressions, default to string
|
|
178
|
+
// Common cases like Math.random().toString(36) will be string
|
|
179
|
+
return "string";
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @example `Math.random().toString(36)` → `string`
|
|
183
|
+
* @example `someFunction()` → `string`
|
|
184
|
+
*/
|
|
185
|
+
if (expr.type === "CallExpression") {
|
|
186
|
+
// Most call expressions in template literals return strings or numbers
|
|
187
|
+
// Default to string for safety
|
|
188
|
+
return "string";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* @example `a + b` → `string`
|
|
192
|
+
* @example `x * y` → `string`
|
|
193
|
+
*/
|
|
194
|
+
if (expr.type === "BinaryExpression") {
|
|
195
|
+
// For string concatenation or arithmetic, default to string
|
|
196
|
+
return "string";
|
|
197
|
+
}
|
|
198
|
+
return "string";
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Infers a template literal type from a TemplateLiteral AST node.
|
|
202
|
+
* Returns a TypeScript template literal type string like `prefix-${string}`.
|
|
203
|
+
*/
|
|
204
|
+
inferTemplateLiteralType(templateLiteral) {
|
|
205
|
+
if (!templateLiteral || typeof templateLiteral !== "object" || templateLiteral.type !== "TemplateLiteral") {
|
|
206
|
+
return "string";
|
|
207
|
+
}
|
|
208
|
+
const quasis = templateLiteral.quasis || [];
|
|
209
|
+
const expressions = templateLiteral.expressions || [];
|
|
210
|
+
// If there are no expressions, it's a static string
|
|
211
|
+
if (expressions.length === 0) {
|
|
212
|
+
if (quasis.length === 1) {
|
|
213
|
+
const staticValue = quasis[0].value?.cooked || quasis[0].value?.raw || "";
|
|
214
|
+
// Escape backticks and backslashes in the static string
|
|
215
|
+
const escaped = staticValue.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
216
|
+
const templateLiteralType = `\`${escaped}\``;
|
|
217
|
+
// Use the (template-literal & {}) trick to make the type more permissive
|
|
218
|
+
return `(${templateLiteralType} & {})`;
|
|
219
|
+
}
|
|
220
|
+
return "string";
|
|
221
|
+
}
|
|
222
|
+
// Build the template literal type
|
|
223
|
+
const parts = [];
|
|
224
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
225
|
+
const quasi = quasis[i];
|
|
226
|
+
const staticValue = quasi.value?.cooked || quasi.value?.raw || "";
|
|
227
|
+
// Escape backticks and backslashes in static parts
|
|
228
|
+
const escaped = staticValue.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
|
|
229
|
+
if (escaped) {
|
|
230
|
+
parts.push(escaped);
|
|
231
|
+
}
|
|
232
|
+
// Add the expression type if there's a corresponding expression
|
|
233
|
+
if (i < expressions.length) {
|
|
234
|
+
const exprType = this.inferExpressionType(expressions[i]);
|
|
235
|
+
parts.push(`\${${exprType}}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// If we couldn't build a meaningful template literal type, fall back to string
|
|
239
|
+
if (parts.length === 0) {
|
|
240
|
+
return "string";
|
|
241
|
+
}
|
|
242
|
+
const templateLiteralType = `\`${parts.join("")}\``;
|
|
243
|
+
// Use the (template-literal & {}) trick to make the type more permissive
|
|
244
|
+
// This allows regular strings to be assigned while preserving template literal type for autocomplete
|
|
245
|
+
return `(${templateLiteralType} & {})`;
|
|
246
|
+
}
|
|
94
247
|
sourceAtPos(start, end) {
|
|
95
248
|
return this.source?.slice(start, end);
|
|
96
249
|
}
|
|
@@ -138,7 +291,9 @@ class ComponentParser {
|
|
|
138
291
|
const name = default_slot ? DEFAULT_SLOT_NAME : (slot_name ?? "");
|
|
139
292
|
const fallback = ComponentParser.assignValue(slot_fallback);
|
|
140
293
|
const props = ComponentParser.assignValue(slot_props);
|
|
141
|
-
const description = slot_description
|
|
294
|
+
const description = slot_description
|
|
295
|
+
? slot_description.substring(slot_description.lastIndexOf("-") + 1).trim()
|
|
296
|
+
: undefined;
|
|
142
297
|
if (this.slots.has(name)) {
|
|
143
298
|
const existing_slot = this.slots.get(name);
|
|
144
299
|
this.slots.set(name, {
|
|
@@ -167,7 +322,7 @@ class ComponentParser {
|
|
|
167
322
|
* `@event` is not specified.
|
|
168
323
|
*/
|
|
169
324
|
const default_detail = !has_argument && !detail ? "null" : ComponentParser.assignValue(detail);
|
|
170
|
-
const event_description = description
|
|
325
|
+
const event_description = description ? description.substring(description.lastIndexOf("-") + 1).trim() : undefined;
|
|
171
326
|
if (this.events.has(name)) {
|
|
172
327
|
const existing_event = this.events.get(name);
|
|
173
328
|
this.events.set(name, {
|
|
@@ -453,7 +608,10 @@ class ComponentParser {
|
|
|
453
608
|
buildVariableInfoCache() {
|
|
454
609
|
if (!this.source)
|
|
455
610
|
return;
|
|
456
|
-
|
|
611
|
+
if (!this.sourceLinesCache) {
|
|
612
|
+
this.sourceLinesCache = this.source.split("\n");
|
|
613
|
+
}
|
|
614
|
+
const lines = this.sourceLinesCache;
|
|
457
615
|
for (let i = 0; i < lines.length; i++) {
|
|
458
616
|
const line = lines[i].trim();
|
|
459
617
|
// Match variable declarations
|
|
@@ -477,14 +635,12 @@ class ComponentParser {
|
|
|
477
635
|
const commentBlock = commentLines.join("\n");
|
|
478
636
|
// Parse the JSDoc
|
|
479
637
|
const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
this.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
});
|
|
487
|
-
}
|
|
638
|
+
const { type: typeTag, description } = this.getCommentTags(parsed);
|
|
639
|
+
if (typeTag) {
|
|
640
|
+
this.variableInfoCache.set(varName, {
|
|
641
|
+
type: this.aliasType(typeTag.type),
|
|
642
|
+
description: description || typeTag.description,
|
|
643
|
+
});
|
|
488
644
|
}
|
|
489
645
|
break;
|
|
490
646
|
}
|
|
@@ -492,6 +648,20 @@ class ComponentParser {
|
|
|
492
648
|
}
|
|
493
649
|
}
|
|
494
650
|
}
|
|
651
|
+
static VAR_NAME_REGEX_CACHE = new Map();
|
|
652
|
+
static getVarNameRegexes(varName) {
|
|
653
|
+
let cached = ComponentParser.VAR_NAME_REGEX_CACHE.get(varName);
|
|
654
|
+
if (!cached) {
|
|
655
|
+
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
656
|
+
cached = [
|
|
657
|
+
new RegExp(`const\\s+${escaped}\\s*=`),
|
|
658
|
+
new RegExp(`let\\s+${escaped}\\s*=`),
|
|
659
|
+
new RegExp(`function\\s+${escaped}\\s*\\(`),
|
|
660
|
+
];
|
|
661
|
+
ComponentParser.VAR_NAME_REGEX_CACHE.set(varName, cached);
|
|
662
|
+
}
|
|
663
|
+
return cached;
|
|
664
|
+
}
|
|
495
665
|
findVariableTypeAndDescription(varName) {
|
|
496
666
|
const cached = this.variableInfoCache.get(varName);
|
|
497
667
|
if (cached) {
|
|
@@ -500,15 +670,18 @@ class ComponentParser {
|
|
|
500
670
|
// Search through the source code directly for JSDoc comments
|
|
501
671
|
if (!this.source)
|
|
502
672
|
return null;
|
|
503
|
-
|
|
504
|
-
|
|
673
|
+
if (!this.sourceLinesCache) {
|
|
674
|
+
this.sourceLinesCache = this.source.split("\n");
|
|
675
|
+
}
|
|
676
|
+
const lines = this.sourceLinesCache;
|
|
677
|
+
const [constRegex, letRegex, funcRegex] = ComponentParser.getVarNameRegexes(varName);
|
|
505
678
|
for (let i = 0; i < lines.length; i++) {
|
|
506
679
|
const line = lines[i].trim();
|
|
507
680
|
// Check if this line declares our variable
|
|
508
681
|
// Match patterns like: const varName = ..., let varName = ..., function varName
|
|
509
|
-
const constMatch = line.match(
|
|
510
|
-
const letMatch = line.match(
|
|
511
|
-
const funcMatch = line.match(
|
|
682
|
+
const constMatch = line.match(constRegex);
|
|
683
|
+
const letMatch = line.match(letRegex);
|
|
684
|
+
const funcMatch = line.match(funcRegex);
|
|
512
685
|
if (constMatch || letMatch || funcMatch) {
|
|
513
686
|
// Look backwards for JSDoc comment
|
|
514
687
|
for (let j = i - 1; j >= 0; j--) {
|
|
@@ -527,14 +700,12 @@ class ComponentParser {
|
|
|
527
700
|
const commentBlock = commentLines.join("\n");
|
|
528
701
|
// Parse the JSDoc
|
|
529
702
|
const parsed = (0, comment_parser_1.parse)(commentBlock, { spacing: "preserve" });
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
};
|
|
537
|
-
}
|
|
703
|
+
const { type: typeTag, description } = this.getCommentTags(parsed);
|
|
704
|
+
if (typeTag) {
|
|
705
|
+
return {
|
|
706
|
+
type: this.aliasType(typeTag.type),
|
|
707
|
+
description: description || typeTag.description,
|
|
708
|
+
};
|
|
538
709
|
}
|
|
539
710
|
break;
|
|
540
711
|
}
|
|
@@ -685,6 +856,7 @@ class ComponentParser {
|
|
|
685
856
|
this.bindings.clear();
|
|
686
857
|
this.contexts.clear();
|
|
687
858
|
this.variableInfoCache.clear();
|
|
859
|
+
this.sourceLinesCache = undefined;
|
|
688
860
|
}
|
|
689
861
|
// Pre-compiled regexes for better performance
|
|
690
862
|
static SCRIPT_BLOCK_REGEX = /(<script[^>]*>)([\s\S]*?)(<\/script>)/gi;
|
|
@@ -718,6 +890,7 @@ class ComponentParser {
|
|
|
718
890
|
// The compile result includes the parsed AST
|
|
719
891
|
this.parsed = compiled.ast || (0, compiler_1.parse)(cleanedSource);
|
|
720
892
|
this.collectReactiveVars();
|
|
893
|
+
this.sourceLinesCache = this.source.split("\n");
|
|
721
894
|
this.buildVariableInfoCache();
|
|
722
895
|
this.parseCustomTypes();
|
|
723
896
|
if (this.parsed?.module) {
|
|
@@ -775,6 +948,11 @@ class ComponentParser {
|
|
|
775
948
|
}
|
|
776
949
|
// Otherwise, don't infer type, just preserve existing type annotation.
|
|
777
950
|
}
|
|
951
|
+
else if (init.type === "TemplateLiteral") {
|
|
952
|
+
// Handle template literals - infer template literal type when possible
|
|
953
|
+
value = this.sourceAtPos(init.start, init.end);
|
|
954
|
+
type = this.inferTemplateLiteralType(init);
|
|
955
|
+
}
|
|
778
956
|
else {
|
|
779
957
|
value = init.raw;
|
|
780
958
|
type = init.value == null ? undefined : typeof init.value;
|
|
@@ -798,12 +976,11 @@ class ComponentParser {
|
|
|
798
976
|
const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
|
|
799
977
|
spacing: "preserve",
|
|
800
978
|
});
|
|
979
|
+
const { type: typeTag, param: paramTags, returns: returnsTag, additional: additionalTags, description: commentDescription, } = this.getCommentTags(comment);
|
|
801
980
|
// Extract @type tag
|
|
802
|
-
const typeTag = comment[0]?.tags.find((t) => t.tag === "type");
|
|
803
981
|
if (typeTag)
|
|
804
982
|
type = this.aliasType(typeTag.type);
|
|
805
983
|
// Extract @param tags
|
|
806
|
-
const paramTags = comment[0]?.tags.filter((t) => t.tag === "param") ?? [];
|
|
807
984
|
if (paramTags.length > 0) {
|
|
808
985
|
params = paramTags
|
|
809
986
|
.filter((tag) => !tag.name.includes(".")) // Exclude nested params like "options.expand"
|
|
@@ -815,27 +992,20 @@ class ComponentParser {
|
|
|
815
992
|
}));
|
|
816
993
|
}
|
|
817
994
|
// Extract @returns/@return tag
|
|
818
|
-
const returnsTag = comment[0]?.tags.find((t) => t.tag === "returns" || t.tag === "return");
|
|
819
995
|
if (returnsTag)
|
|
820
996
|
returnType = this.aliasType(returnsTag.type);
|
|
821
997
|
// Build description from comment description and non-param/non-type tags
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
"extends",
|
|
829
|
-
"restProps",
|
|
830
|
-
"slot",
|
|
831
|
-
"event",
|
|
832
|
-
"typedef",
|
|
833
|
-
].includes(tag.tag)) ?? [];
|
|
834
|
-
if (commentDescription || additionalTags.length > 0) {
|
|
835
|
-
description = commentDescription || "";
|
|
998
|
+
const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
|
|
999
|
+
if (formattedDescription || additionalTags.length > 0) {
|
|
1000
|
+
const descriptionParts = [];
|
|
1001
|
+
if (formattedDescription) {
|
|
1002
|
+
descriptionParts.push(formattedDescription);
|
|
1003
|
+
}
|
|
836
1004
|
for (const tag of additionalTags) {
|
|
837
|
-
|
|
1005
|
+
const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
|
|
1006
|
+
descriptionParts.push(tagStr);
|
|
838
1007
|
}
|
|
1008
|
+
description = descriptionParts.join("\n");
|
|
839
1009
|
}
|
|
840
1010
|
}
|
|
841
1011
|
}
|
|
@@ -971,6 +1141,11 @@ class ComponentParser {
|
|
|
971
1141
|
}
|
|
972
1142
|
// Otherwise, don't infer type, just preserve existing type annotation.
|
|
973
1143
|
}
|
|
1144
|
+
else if (init.type === "TemplateLiteral") {
|
|
1145
|
+
// Handle template literals - infer template literal type when possible
|
|
1146
|
+
value = this.sourceAtPos(init.start, init.end);
|
|
1147
|
+
type = this.inferTemplateLiteralType(init);
|
|
1148
|
+
}
|
|
974
1149
|
else {
|
|
975
1150
|
value = init.raw;
|
|
976
1151
|
type = init.value == null ? undefined : typeof init.value;
|
|
@@ -994,12 +1169,11 @@ class ComponentParser {
|
|
|
994
1169
|
const comment = (0, comment_parser_1.parse)(ComponentParser.formatComment(jsdoc_comment.value), {
|
|
995
1170
|
spacing: "preserve",
|
|
996
1171
|
});
|
|
1172
|
+
const { type: typeTag, param: paramTags, returns: returnsTag, additional: additional_tags, description: commentDescription, } = this.getCommentTags(comment);
|
|
997
1173
|
// Extract @type tag
|
|
998
|
-
const typeTag = comment[0]?.tags.find((t) => t.tag === "type");
|
|
999
1174
|
if (typeTag)
|
|
1000
1175
|
type = this.aliasType(typeTag.type);
|
|
1001
1176
|
// Extract @param tags
|
|
1002
|
-
const paramTags = comment[0]?.tags.filter((t) => t.tag === "param") ?? [];
|
|
1003
1177
|
if (paramTags.length > 0) {
|
|
1004
1178
|
params = paramTags
|
|
1005
1179
|
.filter((tag) => !tag.name.includes(".")) // Exclude nested params like "options.expand"
|
|
@@ -1011,27 +1185,20 @@ class ComponentParser {
|
|
|
1011
1185
|
}));
|
|
1012
1186
|
}
|
|
1013
1187
|
// Extract @returns/@return tag
|
|
1014
|
-
const returnsTag = comment[0]?.tags.find((t) => t.tag === "returns" || t.tag === "return");
|
|
1015
1188
|
if (returnsTag)
|
|
1016
1189
|
returnType = this.aliasType(returnsTag.type);
|
|
1017
1190
|
// Build description from comment description and non-param/non-type tags
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
"extends",
|
|
1025
|
-
"restProps",
|
|
1026
|
-
"slot",
|
|
1027
|
-
"event",
|
|
1028
|
-
"typedef",
|
|
1029
|
-
].includes(tag.tag)) ?? [];
|
|
1030
|
-
if (commentDescription || additional_tags.length > 0) {
|
|
1031
|
-
description = commentDescription || "";
|
|
1191
|
+
const formattedDescription = ComponentParser.assignValue(commentDescription?.trim());
|
|
1192
|
+
if (formattedDescription || additional_tags.length > 0) {
|
|
1193
|
+
const descriptionParts = [];
|
|
1194
|
+
if (formattedDescription) {
|
|
1195
|
+
descriptionParts.push(formattedDescription);
|
|
1196
|
+
}
|
|
1032
1197
|
for (const tag of additional_tags) {
|
|
1033
|
-
|
|
1198
|
+
const tagStr = `@${tag.tag}${tag.name ? ` ${tag.name}` : ""}${tag.description ? ` ${tag.description}` : ""}`;
|
|
1199
|
+
descriptionParts.push(tagStr);
|
|
1034
1200
|
}
|
|
1201
|
+
description = descriptionParts.join("\n");
|
|
1035
1202
|
}
|
|
1036
1203
|
}
|
|
1037
1204
|
}
|
|
@@ -1113,7 +1280,9 @@ class ComponentParser {
|
|
|
1113
1280
|
const existing_event = this.events.get(node.name);
|
|
1114
1281
|
// Check if this event has a JSDoc description
|
|
1115
1282
|
const description = this.eventDescriptions.get(node.name);
|
|
1116
|
-
const event_description = description
|
|
1283
|
+
const event_description = description
|
|
1284
|
+
? description.substring(description.lastIndexOf("-") + 1).trim()
|
|
1285
|
+
: undefined;
|
|
1117
1286
|
if (!existing_event) {
|
|
1118
1287
|
// Add new forwarded event
|
|
1119
1288
|
this.events.set(node.name, {
|
|
@@ -1183,7 +1352,9 @@ class ComponentParser {
|
|
|
1183
1352
|
// If event is marked as dispatched but is NOT actually dispatched, convert it to forwarded
|
|
1184
1353
|
if (event && event.type === "dispatched" && !actuallyDispatchedEvents.has(eventName)) {
|
|
1185
1354
|
const description = this.eventDescriptions.get(eventName);
|
|
1186
|
-
const event_description = description
|
|
1355
|
+
const event_description = description
|
|
1356
|
+
? description.substring(description.lastIndexOf("-") + 1).trim()
|
|
1357
|
+
: undefined;
|
|
1187
1358
|
const forwardedEvent = {
|
|
1188
1359
|
type: "forwarded",
|
|
1189
1360
|
name: eventName,
|
|
@@ -1199,56 +1370,62 @@ class ComponentParser {
|
|
|
1199
1370
|
this.events.set(eventName, forwardedEvent);
|
|
1200
1371
|
}
|
|
1201
1372
|
});
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
.
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (slot_props[key].replace && slot_props[key].value !== undefined) {
|
|
1225
|
-
slot_props[key].value = this.props.get(slot_props[key].value)?.type;
|
|
1226
|
-
}
|
|
1227
|
-
if (slot_props[key].value === undefined)
|
|
1228
|
-
slot_props[key].value = "any";
|
|
1229
|
-
new_props.push(`${key}: ${slot_props[key].value}`);
|
|
1373
|
+
const processedProps = ComponentParser.mapToArray(this.props).map((prop) => {
|
|
1374
|
+
if (this.bindings.has(prop.name)) {
|
|
1375
|
+
const elementTypes = this.bindings
|
|
1376
|
+
.get(prop.name)
|
|
1377
|
+
?.elements.sort()
|
|
1378
|
+
.map((element) => (0, element_tag_map_1.getElementByTag)(element))
|
|
1379
|
+
.join(" | ");
|
|
1380
|
+
return {
|
|
1381
|
+
...prop,
|
|
1382
|
+
type: `null | ${elementTypes}`,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
return prop;
|
|
1386
|
+
});
|
|
1387
|
+
const processedSlots = ComponentParser.mapToArray(this.slots)
|
|
1388
|
+
.map((slot) => {
|
|
1389
|
+
try {
|
|
1390
|
+
const slot_props = JSON.parse(slot.slot_props);
|
|
1391
|
+
const new_props = [];
|
|
1392
|
+
for (const key of Object.keys(slot_props)) {
|
|
1393
|
+
if (slot_props[key].replace && slot_props[key].value !== undefined) {
|
|
1394
|
+
slot_props[key].value = this.props.get(slot_props[key].value)?.type;
|
|
1230
1395
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1396
|
+
if (slot_props[key].value === undefined)
|
|
1397
|
+
slot_props[key].value = "any";
|
|
1398
|
+
new_props.push(`${key}: ${slot_props[key].value}`);
|
|
1233
1399
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
return
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1400
|
+
const formatted_slot_props = new_props.length === 0 ? "Record<string, never>" : `{ ${new_props.join(", ")} }`;
|
|
1401
|
+
return { ...slot, slot_props: formatted_slot_props };
|
|
1402
|
+
}
|
|
1403
|
+
catch (_e) {
|
|
1404
|
+
return slot;
|
|
1405
|
+
}
|
|
1406
|
+
})
|
|
1407
|
+
.sort((a, b) => {
|
|
1408
|
+
if (a.name < b.name)
|
|
1409
|
+
return -1;
|
|
1410
|
+
if (a.name > b.name)
|
|
1411
|
+
return 1;
|
|
1412
|
+
return 0;
|
|
1413
|
+
});
|
|
1414
|
+
const moduleExportsArray = ComponentParser.mapToArray(this.moduleExports);
|
|
1415
|
+
const eventsArray = ComponentParser.mapToArray(this.events);
|
|
1416
|
+
const typedefsArray = ComponentParser.mapToArray(this.typedefs);
|
|
1417
|
+
const contextsArray = ComponentParser.mapToArray(this.contexts);
|
|
1418
|
+
return {
|
|
1419
|
+
props: processedProps,
|
|
1420
|
+
moduleExports: moduleExportsArray,
|
|
1421
|
+
slots: processedSlots,
|
|
1422
|
+
events: eventsArray,
|
|
1423
|
+
typedefs: typedefsArray,
|
|
1247
1424
|
generics: this.generics,
|
|
1248
1425
|
rest_props: this.rest_props,
|
|
1249
1426
|
extends: this.extends,
|
|
1250
1427
|
componentComment: this.componentComment,
|
|
1251
|
-
contexts:
|
|
1428
|
+
contexts: contextsArray,
|
|
1252
1429
|
};
|
|
1253
1430
|
}
|
|
1254
1431
|
}
|
package/lib/resolve-alias.js
CHANGED
|
@@ -7,6 +7,7 @@ const node_fs_1 = require("node:fs");
|
|
|
7
7
|
const node_path_1 = require("node:path");
|
|
8
8
|
const path_1 = require("./path");
|
|
9
9
|
const configCache = new Map();
|
|
10
|
+
const pathPatternRegexCache = new Map();
|
|
10
11
|
const COMMENT_PATTERN = /\/\*[\s\S]*?\*\/|\/\/.*/g;
|
|
11
12
|
const REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/g;
|
|
12
13
|
function clearConfigCache() {
|
|
@@ -83,12 +84,16 @@ function resolvePathAliasAbsolute(importPath, fromDir) {
|
|
|
83
84
|
// e.g., "$lib/*" -> /^\$lib\/(.*)$/
|
|
84
85
|
// e.g., "$lib" -> /^\$lib$/
|
|
85
86
|
// e.g., "@components/*" -> /^@components\/(.*)$/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
let regex = pathPatternRegexCache.get(pattern);
|
|
88
|
+
if (!regex) {
|
|
89
|
+
// Escape special regex chars but keep * for replacement
|
|
90
|
+
const escapedPattern = pattern
|
|
91
|
+
.split("*")
|
|
92
|
+
.map((part) => part.replace(REGEX_SPECIAL_CHARS, "\\$&"))
|
|
93
|
+
.join("(.*)");
|
|
94
|
+
regex = new RegExp(`^${escapedPattern}$`);
|
|
95
|
+
pathPatternRegexCache.set(pattern, regex);
|
|
96
|
+
}
|
|
92
97
|
const match = importPath.match(regex);
|
|
93
98
|
if (match) {
|
|
94
99
|
// Use the first mapping (TypeScript uses the first match)
|
package/lib/rollup-plugin.js
CHANGED
|
@@ -69,42 +69,47 @@ async function generateBundle(input, glob) {
|
|
|
69
69
|
const allComponentsForTypes = new Map();
|
|
70
70
|
const exportEntries = Object.entries(exports);
|
|
71
71
|
const allComponentEntries = Object.entries(allComponents);
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const uniqueFilePaths = new Set();
|
|
73
|
+
for (const [, entry] of exportEntries) {
|
|
74
74
|
const filePath = entry.source;
|
|
75
|
-
const { ext
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
moduleName = name;
|
|
75
|
+
const { ext } = (0, node_path_1.parse)(filePath);
|
|
76
|
+
if (ext === ".svelte") {
|
|
77
|
+
uniqueFilePaths.add((0, node_path_1.resolve)(dir, filePath));
|
|
79
78
|
}
|
|
79
|
+
}
|
|
80
|
+
for (const [, entry] of allComponentEntries) {
|
|
81
|
+
const filePath = entry.source;
|
|
82
|
+
const { ext } = (0, node_path_1.parse)(filePath);
|
|
80
83
|
if (ext === ".svelte") {
|
|
81
|
-
|
|
82
|
-
const { code: processed } = await (0, compiler_1.preprocess)(source, [(0, svelte_preprocess_1.typescript)(), (0, svelte_preprocess_1.replace)([[STYLE_TAG_REGEX, ""]])], {
|
|
83
|
-
filename: (0, node_path_1.basename)(filePath),
|
|
84
|
-
});
|
|
85
|
-
const parser = new ComponentParser_1.default();
|
|
86
|
-
const parsed = parser.parseSvelteComponent(processed, {
|
|
87
|
-
moduleName,
|
|
88
|
-
filePath,
|
|
89
|
-
});
|
|
90
|
-
return {
|
|
91
|
-
moduleName,
|
|
92
|
-
filePath,
|
|
93
|
-
...parsed,
|
|
94
|
-
};
|
|
84
|
+
uniqueFilePaths.add((0, node_path_1.resolve)(dir, filePath));
|
|
95
85
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
}
|
|
87
|
+
const fileContents = await Promise.all(Array.from(uniqueFilePaths).map(async (filePath) => {
|
|
88
|
+
try {
|
|
89
|
+
const content = await (0, promises_1.readFile)(filePath, "utf-8");
|
|
90
|
+
return { path: filePath, content };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.warn(`Warning: Failed to read file ${filePath}:`, error);
|
|
94
|
+
return { path: filePath, content: null };
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
const fileMap = new Map(fileContents.map(({ path, content }) => [path, content]));
|
|
98
|
+
// Helper function to process a single component
|
|
99
|
+
const processComponent = async ([exportName, entry], entries, fileMap) => {
|
|
100
100
|
const filePath = entry.source;
|
|
101
101
|
const { ext, name } = (0, node_path_1.parse)(filePath);
|
|
102
102
|
let moduleName = exportName;
|
|
103
|
-
if (
|
|
103
|
+
if (entries.length === 1 && exportName === "default") {
|
|
104
104
|
moduleName = name;
|
|
105
105
|
}
|
|
106
106
|
if (ext === ".svelte") {
|
|
107
|
-
const
|
|
107
|
+
const resolvedPath = (0, node_path_1.resolve)(dir, filePath);
|
|
108
|
+
const source = fileMap.get(resolvedPath);
|
|
109
|
+
if (source === null || source === undefined) {
|
|
110
|
+
// File was not found or failed to read, skip this component
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
108
113
|
const { code: processed } = await (0, compiler_1.preprocess)(source, [(0, svelte_preprocess_1.typescript)(), (0, svelte_preprocess_1.replace)([[STYLE_TAG_REGEX, ""]])], {
|
|
109
114
|
filename: (0, node_path_1.basename)(filePath),
|
|
110
115
|
});
|
|
@@ -120,7 +125,11 @@ async function generateBundle(input, glob) {
|
|
|
120
125
|
};
|
|
121
126
|
}
|
|
122
127
|
return null;
|
|
123
|
-
}
|
|
128
|
+
};
|
|
129
|
+
// Process exported components (for metadata/JSON/Markdown)
|
|
130
|
+
const componentPromises = exportEntries.map((entry) => processComponent(entry, exportEntries, fileMap));
|
|
131
|
+
// Process all components (for .d.ts generation)
|
|
132
|
+
const allComponentPromises = allComponentEntries.map((entry) => processComponent(entry, allComponentEntries, fileMap));
|
|
124
133
|
const [results, allResults] = await Promise.all([Promise.all(componentPromises), Promise.all(allComponentPromises)]);
|
|
125
134
|
for (const result of results) {
|
|
126
135
|
if (result) {
|
|
@@ -10,10 +10,11 @@ interface TocLine {
|
|
|
10
10
|
}
|
|
11
11
|
export default class WriterMarkdown extends Writer {
|
|
12
12
|
onAppend?: OnAppend;
|
|
13
|
-
|
|
13
|
+
private sourceParts;
|
|
14
14
|
hasToC: boolean;
|
|
15
15
|
toc: TocLine[];
|
|
16
16
|
constructor(options: MarkdownOptions);
|
|
17
|
+
get source(): string;
|
|
17
18
|
appendLineBreaks(): this;
|
|
18
19
|
append(type: AppendType, raw?: string): this;
|
|
19
20
|
tableOfContents(): this;
|
|
@@ -8,15 +8,18 @@ const BACKTICK_REGEX = /`/g;
|
|
|
8
8
|
const WHITESPACE_REGEX = /\s+/g;
|
|
9
9
|
class WriterMarkdown extends Writer_1.default {
|
|
10
10
|
onAppend;
|
|
11
|
-
|
|
11
|
+
sourceParts = [];
|
|
12
12
|
hasToC = false;
|
|
13
13
|
toc = [];
|
|
14
14
|
constructor(options) {
|
|
15
15
|
super({ parser: "markdown", printWidth: 80 });
|
|
16
16
|
this.onAppend = options.onAppend;
|
|
17
17
|
}
|
|
18
|
+
get source() {
|
|
19
|
+
return this.sourceParts.join("");
|
|
20
|
+
}
|
|
18
21
|
appendLineBreaks() {
|
|
19
|
-
this.
|
|
22
|
+
this.sourceParts.push("\n\n");
|
|
20
23
|
return this;
|
|
21
24
|
}
|
|
22
25
|
append(type, raw) {
|
|
@@ -28,9 +31,7 @@ class WriterMarkdown extends Writer_1.default {
|
|
|
28
31
|
case "h5":
|
|
29
32
|
case "h6": {
|
|
30
33
|
const length = Number(type.slice(-1));
|
|
31
|
-
this.
|
|
32
|
-
.map((_) => "#")
|
|
33
|
-
.join("")} ${raw}`;
|
|
34
|
+
this.sourceParts.push(`${"#".repeat(length)} ${raw}`);
|
|
34
35
|
if (this.hasToC && type === "h2") {
|
|
35
36
|
this.toc.push({
|
|
36
37
|
array: Array.from({ length: (length - 1) * 2 }),
|
|
@@ -40,16 +41,16 @@ class WriterMarkdown extends Writer_1.default {
|
|
|
40
41
|
break;
|
|
41
42
|
}
|
|
42
43
|
case "quote":
|
|
43
|
-
this.
|
|
44
|
+
this.sourceParts.push(`> ${raw}`);
|
|
44
45
|
break;
|
|
45
46
|
case "p":
|
|
46
|
-
this.
|
|
47
|
+
this.sourceParts.push(raw ?? "");
|
|
47
48
|
break;
|
|
48
49
|
case "divider":
|
|
49
|
-
this.
|
|
50
|
+
this.sourceParts.push("---");
|
|
50
51
|
break;
|
|
51
52
|
case "raw":
|
|
52
|
-
this.
|
|
53
|
+
this.sourceParts.push(raw ?? "");
|
|
53
54
|
break;
|
|
54
55
|
}
|
|
55
56
|
if (type !== "raw")
|
|
@@ -58,18 +59,18 @@ class WriterMarkdown extends Writer_1.default {
|
|
|
58
59
|
return this;
|
|
59
60
|
}
|
|
60
61
|
tableOfContents() {
|
|
61
|
-
this.
|
|
62
|
+
this.sourceParts.push("<!-- __TOC__ -->");
|
|
62
63
|
this.hasToC = true;
|
|
63
64
|
this.appendLineBreaks();
|
|
64
65
|
return this;
|
|
65
66
|
}
|
|
66
67
|
end() {
|
|
67
|
-
|
|
68
|
+
const source = this.sourceParts.join("");
|
|
69
|
+
return source.replace("<!-- __TOC__ -->", this.toc
|
|
68
70
|
.map(({ array, raw }) => {
|
|
69
71
|
return `${array.join(" ")} - [${raw}](#${raw.toLowerCase().replace(BACKTICK_REGEX, "").replace(WHITESPACE_REGEX, "-")})`;
|
|
70
72
|
})
|
|
71
73
|
.join("\n"));
|
|
72
|
-
return this.source;
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
exports.default = WriterMarkdown;
|
|
@@ -10,10 +10,11 @@ interface TocLine {
|
|
|
10
10
|
}
|
|
11
11
|
export declare class BrowserWriterMarkdown {
|
|
12
12
|
onAppend?: OnAppend;
|
|
13
|
-
|
|
13
|
+
private sourceParts;
|
|
14
14
|
hasToC: boolean;
|
|
15
15
|
toc: TocLine[];
|
|
16
16
|
constructor(options: MarkdownOptions);
|
|
17
|
+
get source(): string;
|
|
17
18
|
appendLineBreaks(): this;
|
|
18
19
|
append(type: AppendType, raw?: string): this;
|
|
19
20
|
tableOfContents(): this;
|
|
@@ -8,14 +8,17 @@ const WHITESPACE_REGEX = /\s+/g;
|
|
|
8
8
|
// Browser-compatible WriterMarkdown that doesn't extend Writer
|
|
9
9
|
class BrowserWriterMarkdown {
|
|
10
10
|
onAppend;
|
|
11
|
-
|
|
11
|
+
sourceParts = [];
|
|
12
12
|
hasToC = false;
|
|
13
13
|
toc = [];
|
|
14
14
|
constructor(options) {
|
|
15
15
|
this.onAppend = options.onAppend;
|
|
16
16
|
}
|
|
17
|
+
get source() {
|
|
18
|
+
return this.sourceParts.join("");
|
|
19
|
+
}
|
|
17
20
|
appendLineBreaks() {
|
|
18
|
-
this.
|
|
21
|
+
this.sourceParts.push("\n\n");
|
|
19
22
|
return this;
|
|
20
23
|
}
|
|
21
24
|
append(type, raw) {
|
|
@@ -27,9 +30,7 @@ class BrowserWriterMarkdown {
|
|
|
27
30
|
case "h5":
|
|
28
31
|
case "h6": {
|
|
29
32
|
const length = Number(type.slice(-1));
|
|
30
|
-
this.
|
|
31
|
-
.map((_) => "#")
|
|
32
|
-
.join("")} ${raw}`;
|
|
33
|
+
this.sourceParts.push(`${"#".repeat(length)} ${raw}`);
|
|
33
34
|
if (this.hasToC && type === "h2") {
|
|
34
35
|
this.toc.push({
|
|
35
36
|
array: Array.from({ length: (length - 1) * 2 }),
|
|
@@ -39,16 +40,16 @@ class BrowserWriterMarkdown {
|
|
|
39
40
|
break;
|
|
40
41
|
}
|
|
41
42
|
case "quote":
|
|
42
|
-
this.
|
|
43
|
+
this.sourceParts.push(`> ${raw}`);
|
|
43
44
|
break;
|
|
44
45
|
case "p":
|
|
45
|
-
this.
|
|
46
|
+
this.sourceParts.push(raw ?? "");
|
|
46
47
|
break;
|
|
47
48
|
case "divider":
|
|
48
|
-
this.
|
|
49
|
+
this.sourceParts.push("---");
|
|
49
50
|
break;
|
|
50
51
|
case "raw":
|
|
51
|
-
this.
|
|
52
|
+
this.sourceParts.push(raw ?? "");
|
|
52
53
|
break;
|
|
53
54
|
}
|
|
54
55
|
if (type !== "raw")
|
|
@@ -57,18 +58,18 @@ class BrowserWriterMarkdown {
|
|
|
57
58
|
return this;
|
|
58
59
|
}
|
|
59
60
|
tableOfContents() {
|
|
60
|
-
this.
|
|
61
|
+
this.sourceParts.push("<!-- __TOC__ -->");
|
|
61
62
|
this.hasToC = true;
|
|
62
63
|
this.appendLineBreaks();
|
|
63
64
|
return this;
|
|
64
65
|
}
|
|
65
66
|
end() {
|
|
66
|
-
|
|
67
|
+
const source = this.sourceParts.join("");
|
|
68
|
+
return source.replace("<!-- __TOC__ -->", this.toc
|
|
67
69
|
.map(({ array, raw }) => {
|
|
68
70
|
return `${array.join(" ")} - [${raw}](#${raw.toLowerCase().replace(BACKTICK_REGEX, "").replace(WHITESPACE_REGEX, "-")})`;
|
|
69
71
|
})
|
|
70
72
|
.join("\n"));
|
|
71
|
-
return this.source;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
exports.BrowserWriterMarkdown = BrowserWriterMarkdown;
|