@fuzdev/fuz_ui 0.185.2 → 0.187.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 (44) hide show
  1. package/dist/ApiModule.svelte +22 -6
  2. package/dist/ApiModule.svelte.d.ts.map +1 -1
  3. package/dist/DeclarationLink.svelte +1 -1
  4. package/dist/DocsLink.svelte +2 -2
  5. package/dist/DocsTertiaryNav.svelte +2 -2
  6. package/dist/Mdz.svelte +5 -0
  7. package/dist/Mdz.svelte.d.ts +1 -0
  8. package/dist/Mdz.svelte.d.ts.map +1 -1
  9. package/dist/MdzNodeView.svelte +19 -8
  10. package/dist/MdzNodeView.svelte.d.ts +1 -1
  11. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  12. package/dist/ModuleLink.svelte +1 -1
  13. package/dist/TypeLink.svelte +1 -1
  14. package/dist/library.svelte.d.ts +24 -27
  15. package/dist/library.svelte.d.ts.map +1 -1
  16. package/dist/library.svelte.js +16 -16
  17. package/dist/mdz.d.ts +13 -0
  18. package/dist/mdz.d.ts.map +1 -1
  19. package/dist/mdz.js +73 -280
  20. package/dist/mdz_components.d.ts +12 -0
  21. package/dist/mdz_components.d.ts.map +1 -1
  22. package/dist/mdz_components.js +8 -0
  23. package/dist/mdz_helpers.d.ts +108 -0
  24. package/dist/mdz_helpers.d.ts.map +1 -0
  25. package/dist/mdz_helpers.js +237 -0
  26. package/dist/mdz_lexer.d.ts +93 -0
  27. package/dist/mdz_lexer.d.ts.map +1 -0
  28. package/dist/mdz_lexer.js +727 -0
  29. package/dist/mdz_to_svelte.d.ts +5 -2
  30. package/dist/mdz_to_svelte.d.ts.map +1 -1
  31. package/dist/mdz_to_svelte.js +13 -2
  32. package/dist/mdz_token_parser.d.ts +14 -0
  33. package/dist/mdz_token_parser.d.ts.map +1 -0
  34. package/dist/mdz_token_parser.js +374 -0
  35. package/dist/svelte_preprocess_mdz.js +23 -7
  36. package/package.json +10 -9
  37. package/src/lib/library.svelte.ts +36 -35
  38. package/src/lib/mdz.ts +106 -302
  39. package/src/lib/mdz_components.ts +9 -0
  40. package/src/lib/mdz_helpers.ts +251 -0
  41. package/src/lib/mdz_lexer.ts +1003 -0
  42. package/src/lib/mdz_to_svelte.ts +15 -2
  43. package/src/lib/mdz_token_parser.ts +460 -0
  44. package/src/lib/svelte_preprocess_mdz.ts +23 -7
@@ -12,7 +12,7 @@ import {UnreachableError} from '@fuzdev/fuz_util/error.js';
12
12
  import {escape_svelte_text} from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
13
13
  import {escape_js_string} from '@fuzdev/fuz_util/string.js';
14
14
 
15
- import type {MdzNode} from './mdz.js';
15
+ import {type MdzNode, resolve_relative_path} from './mdz.js';
16
16
 
17
17
  /**
18
18
  * Result of converting `MdzNode` arrays to Svelte markup.
@@ -39,11 +39,15 @@ export interface MdzToSvelteResult {
39
39
  * If content references a component not in this map, `has_unconfigured_tags` is set.
40
40
  * @param elements Allowed HTML element names (e.g., `new Set(['aside', 'details'])`).
41
41
  * If content references an element not in this set, `has_unconfigured_tags` is set.
42
+ * @param base Base path for resolving relative links (e.g., `'/docs/mdz/'`).
43
+ * When provided, relative references (`./`, `../`) are resolved to absolute paths
44
+ * and passed through `resolve()`. Trailing slash recommended.
42
45
  */
43
46
  export const mdz_to_svelte = (
44
47
  nodes: Array<MdzNode>,
45
48
  components: Record<string, string>,
46
49
  elements: ReadonlySet<string>,
50
+ base?: string,
47
51
  ): MdzToSvelteResult => {
48
52
  const imports: Map<string, {path: string; kind: 'default' | 'named'}> = new Map();
49
53
  let has_unconfigured_tags = false;
@@ -80,7 +84,16 @@ export const mdz_to_svelte = (
80
84
  case 'Link': {
81
85
  const children_markup = render_nodes(node.children);
82
86
  if (node.link_type === 'internal') {
83
- if (node.reference.startsWith('#') || node.reference.startsWith('?')) {
87
+ if (node.reference.startsWith('.') && base) {
88
+ const resolved = resolve_relative_path(node.reference, base);
89
+ imports.set('resolve', {path: '$app/paths', kind: 'named'});
90
+ return `<a href={resolve('${escape_js_string(resolved)}')}>${children_markup}</a>`;
91
+ }
92
+ if (
93
+ node.reference.startsWith('#') ||
94
+ node.reference.startsWith('?') ||
95
+ node.reference.startsWith('.')
96
+ ) {
84
97
  return `<a href={'${escape_js_string(node.reference)}'}>${children_markup}</a>`;
85
98
  }
86
99
  imports.set('resolve', {path: '$app/paths', kind: 'named'});
@@ -0,0 +1,460 @@
1
+ /**
2
+ * mdz token parser — consumes a `MdzToken[]` stream to build the `MdzNode[]` AST.
3
+ *
4
+ * Phase 2 of the two-phase lexer+parser alternative to the single-pass parser
5
+ * in `mdz.ts`. Phase 1 (lexer) is in `mdz_lexer.ts`.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import type {
11
+ MdzNode,
12
+ MdzTextNode,
13
+ MdzBoldNode,
14
+ MdzItalicNode,
15
+ MdzStrikethroughNode,
16
+ MdzLinkNode,
17
+ MdzHeadingNode,
18
+ MdzElementNode,
19
+ MdzComponentNode,
20
+ } from './mdz.js';
21
+ import {extract_single_tag} from './mdz_helpers.js';
22
+ import {
23
+ MdzLexer,
24
+ type MdzToken,
25
+ type MdzTokenHeadingStart,
26
+ type MdzTokenBoldOpen,
27
+ type MdzTokenItalicOpen,
28
+ type MdzTokenStrikethroughOpen,
29
+ type MdzTokenLinkTextOpen,
30
+ type MdzTokenLinkRef,
31
+ type MdzTokenAutolink,
32
+ type MdzTokenTagOpen,
33
+ type MdzTokenTagSelfClose,
34
+ } from './mdz_lexer.js';
35
+
36
+ /**
37
+ * Parses text to an array of `MdzNode` using a two-phase lexer+parser approach.
38
+ */
39
+ export const mdz_parse_lexer = (text: string): Array<MdzNode> => {
40
+ const tokens = new MdzLexer(text).tokenize();
41
+ return new MdzTokenParser(tokens).parse();
42
+ };
43
+
44
+ class MdzTokenParser {
45
+ #tokens: Array<MdzToken>;
46
+ #index: number = 0;
47
+
48
+ constructor(tokens: Array<MdzToken>) {
49
+ this.#tokens = tokens;
50
+ }
51
+
52
+ parse(): Array<MdzNode> {
53
+ const root_nodes: Array<MdzNode> = [];
54
+ const paragraph_children: Array<MdzNode> = [];
55
+
56
+ while (this.#index < this.#tokens.length) {
57
+ const token = this.#tokens[this.#index]!;
58
+
59
+ // Block-level tokens
60
+ if (token.type === 'heading_start') {
61
+ const flushed = this.#flush_paragraph(paragraph_children, true);
62
+ if (flushed) root_nodes.push(flushed);
63
+ root_nodes.push(this.#parse_heading());
64
+ continue;
65
+ }
66
+
67
+ if (token.type === 'hr') {
68
+ const flushed = this.#flush_paragraph(paragraph_children, true);
69
+ if (flushed) root_nodes.push(flushed);
70
+ root_nodes.push({type: 'Hr', start: token.start, end: token.end});
71
+ this.#index++;
72
+ continue;
73
+ }
74
+
75
+ if (token.type === 'codeblock') {
76
+ const flushed = this.#flush_paragraph(paragraph_children, true);
77
+ if (flushed) root_nodes.push(flushed);
78
+ root_nodes.push({
79
+ type: 'Codeblock',
80
+ lang: token.lang,
81
+ content: token.content,
82
+ start: token.start,
83
+ end: token.end,
84
+ });
85
+ this.#index++;
86
+ continue;
87
+ }
88
+
89
+ if (token.type === 'paragraph_break') {
90
+ const flushed = this.#flush_paragraph(paragraph_children, true);
91
+ if (flushed) root_nodes.push(flushed);
92
+ this.#index++;
93
+ continue;
94
+ }
95
+
96
+ // Inline tokens → paragraph children
97
+ const node = this.#parse_inline();
98
+ if (node) paragraph_children.push(node);
99
+ }
100
+
101
+ // Flush remaining content
102
+ const final_paragraph = this.#flush_paragraph(paragraph_children, true);
103
+ if (final_paragraph) root_nodes.push(final_paragraph);
104
+
105
+ return root_nodes;
106
+ }
107
+
108
+ #parse_heading(): MdzHeadingNode {
109
+ const token = this.#tokens[this.#index]! as MdzTokenHeadingStart;
110
+ const start = token.start;
111
+ const level = token.level;
112
+ this.#index++;
113
+
114
+ const children: Array<MdzNode> = [];
115
+
116
+ // Collect inline nodes until heading_end marker
117
+ while (this.#index < this.#tokens.length) {
118
+ const t = this.#tokens[this.#index]!;
119
+ if (t.type === 'heading_end') {
120
+ this.#index++; // consume the marker
121
+ break;
122
+ }
123
+ const node = this.#parse_inline();
124
+ if (node) children.push(node);
125
+ }
126
+
127
+ const end = children.length > 0 ? children[children.length - 1]!.end : start + level + 1;
128
+
129
+ return {type: 'Heading', level, children, start, end};
130
+ }
131
+
132
+ #parse_inline(): MdzNode | null {
133
+ if (this.#index >= this.#tokens.length) return null;
134
+
135
+ const token = this.#tokens[this.#index]!;
136
+
137
+ switch (token.type) {
138
+ case 'text':
139
+ this.#index++;
140
+ return {type: 'Text', content: token.content, start: token.start, end: token.end};
141
+
142
+ case 'code':
143
+ this.#index++;
144
+ return {type: 'Code', content: token.content, start: token.start, end: token.end};
145
+
146
+ case 'bold_open':
147
+ return this.#parse_bold();
148
+
149
+ case 'italic_open':
150
+ return this.#parse_italic();
151
+
152
+ case 'strikethrough_open':
153
+ return this.#parse_strikethrough();
154
+
155
+ case 'link_text_open':
156
+ return this.#parse_link();
157
+
158
+ case 'autolink':
159
+ return this.#parse_autolink();
160
+
161
+ case 'tag_open':
162
+ return this.#parse_tag_node();
163
+
164
+ case 'tag_self_close':
165
+ return this.#parse_self_close_tag();
166
+
167
+ // Orphaned close tokens - treat as text
168
+ case 'bold_close':
169
+ this.#index++;
170
+ return {type: 'Text', content: '**', start: token.start, end: token.end};
171
+
172
+ case 'italic_close':
173
+ this.#index++;
174
+ return {type: 'Text', content: '_', start: token.start, end: token.end};
175
+
176
+ case 'strikethrough_close':
177
+ this.#index++;
178
+ return {type: 'Text', content: '~', start: token.start, end: token.end};
179
+
180
+ case 'link_text_close':
181
+ this.#index++;
182
+ return {type: 'Text', content: ']', start: token.start, end: token.end};
183
+
184
+ case 'link_ref':
185
+ this.#index++;
186
+ return {
187
+ type: 'Text',
188
+ content: `(${token.reference})`,
189
+ start: token.start,
190
+ end: token.end,
191
+ };
192
+
193
+ case 'tag_close':
194
+ this.#index++;
195
+ return {
196
+ type: 'Text',
197
+ content: `</${token.name}>`,
198
+ start: token.start,
199
+ end: token.end,
200
+ };
201
+
202
+ default:
203
+ this.#index++;
204
+ return null;
205
+ }
206
+ }
207
+
208
+ #parse_bold(): MdzBoldNode | MdzTextNode {
209
+ const token = this.#tokens[this.#index]! as MdzTokenBoldOpen;
210
+ const start = token.start;
211
+ this.#index++;
212
+
213
+ const children: Array<MdzNode> = [];
214
+
215
+ while (this.#index < this.#tokens.length) {
216
+ const t = this.#tokens[this.#index]!;
217
+ if (t.type === 'bold_close') {
218
+ this.#index++;
219
+ return {type: 'Bold', children, start, end: t.end};
220
+ }
221
+ if (
222
+ t.type === 'paragraph_break' ||
223
+ t.type === 'heading_start' ||
224
+ t.type === 'hr' ||
225
+ t.type === 'codeblock'
226
+ ) {
227
+ break;
228
+ }
229
+ const node = this.#parse_inline();
230
+ if (node) children.push(node);
231
+ }
232
+
233
+ // Unclosed - treat as text
234
+ return {type: 'Text', content: '**', start, end: start + 2};
235
+ }
236
+
237
+ #parse_italic(): MdzItalicNode | MdzTextNode {
238
+ const token = this.#tokens[this.#index]! as MdzTokenItalicOpen;
239
+ const start = token.start;
240
+ this.#index++;
241
+
242
+ const children: Array<MdzNode> = [];
243
+
244
+ while (this.#index < this.#tokens.length) {
245
+ const t = this.#tokens[this.#index]!;
246
+ if (t.type === 'italic_close') {
247
+ this.#index++;
248
+ return {type: 'Italic', children, start, end: t.end};
249
+ }
250
+ if (
251
+ t.type === 'paragraph_break' ||
252
+ t.type === 'heading_start' ||
253
+ t.type === 'hr' ||
254
+ t.type === 'codeblock'
255
+ ) {
256
+ break;
257
+ }
258
+ const node = this.#parse_inline();
259
+ if (node) children.push(node);
260
+ }
261
+
262
+ return {type: 'Text', content: '_', start, end: start + 1};
263
+ }
264
+
265
+ #parse_strikethrough(): MdzStrikethroughNode | MdzTextNode {
266
+ const token = this.#tokens[this.#index]! as MdzTokenStrikethroughOpen;
267
+ const start = token.start;
268
+ this.#index++;
269
+
270
+ const children: Array<MdzNode> = [];
271
+
272
+ while (this.#index < this.#tokens.length) {
273
+ const t = this.#tokens[this.#index]!;
274
+ if (t.type === 'strikethrough_close') {
275
+ this.#index++;
276
+ return {type: 'Strikethrough', children, start, end: t.end};
277
+ }
278
+ if (
279
+ t.type === 'paragraph_break' ||
280
+ t.type === 'heading_start' ||
281
+ t.type === 'hr' ||
282
+ t.type === 'codeblock'
283
+ ) {
284
+ break;
285
+ }
286
+ const node = this.#parse_inline();
287
+ if (node) children.push(node);
288
+ }
289
+
290
+ return {type: 'Text', content: '~', start, end: start + 1};
291
+ }
292
+
293
+ #parse_link(): MdzLinkNode | MdzTextNode {
294
+ const open_token = this.#tokens[this.#index]! as MdzTokenLinkTextOpen;
295
+ const start = open_token.start;
296
+ this.#index++;
297
+
298
+ const children: Array<MdzNode> = [];
299
+
300
+ while (this.#index < this.#tokens.length) {
301
+ const t = this.#tokens[this.#index]!;
302
+ if (t.type === 'link_text_close') {
303
+ this.#index++;
304
+
305
+ // Expect link_ref next
306
+ if (this.#index < this.#tokens.length && this.#tokens[this.#index]!.type === 'link_ref') {
307
+ const ref_token = this.#tokens[this.#index]! as MdzTokenLinkRef;
308
+ this.#index++;
309
+ return {
310
+ type: 'Link',
311
+ reference: ref_token.reference,
312
+ children,
313
+ link_type: ref_token.link_type,
314
+ start,
315
+ end: ref_token.end,
316
+ };
317
+ }
318
+
319
+ // No link_ref - treat as text
320
+ return {type: 'Text', content: '[', start, end: start + 1};
321
+ }
322
+ if (
323
+ t.type === 'paragraph_break' ||
324
+ t.type === 'heading_start' ||
325
+ t.type === 'hr' ||
326
+ t.type === 'codeblock'
327
+ ) {
328
+ break;
329
+ }
330
+ const node = this.#parse_inline();
331
+ if (node) children.push(node);
332
+ }
333
+
334
+ return {type: 'Text', content: '[', start, end: start + 1};
335
+ }
336
+
337
+ #parse_autolink(): MdzLinkNode {
338
+ const token = this.#tokens[this.#index]! as MdzTokenAutolink;
339
+ this.#index++;
340
+ return {
341
+ type: 'Link',
342
+ reference: token.reference,
343
+ children: [{type: 'Text', content: token.reference, start: token.start, end: token.end}],
344
+ link_type: token.link_type,
345
+ start: token.start,
346
+ end: token.end,
347
+ };
348
+ }
349
+
350
+ #parse_tag_node(): MdzElementNode | MdzComponentNode | MdzTextNode {
351
+ const open_token = this.#tokens[this.#index]! as MdzTokenTagOpen;
352
+ const start = open_token.start;
353
+ const tag_name = open_token.name;
354
+ const node_type: 'Component' | 'Element' = open_token.is_component ? 'Component' : 'Element';
355
+ this.#index++;
356
+
357
+ const children: Array<MdzNode> = [];
358
+
359
+ while (this.#index < this.#tokens.length) {
360
+ const t = this.#tokens[this.#index]!;
361
+ if (t.type === 'tag_close' && t.name === tag_name) {
362
+ this.#index++;
363
+ return {type: node_type, name: tag_name, children, start, end: t.end};
364
+ }
365
+ const node = this.#parse_inline();
366
+ if (node) children.push(node);
367
+ }
368
+
369
+ // Unclosed tag
370
+ return {type: 'Text', content: '<', start, end: start + 1};
371
+ }
372
+
373
+ #parse_self_close_tag(): MdzElementNode | MdzComponentNode {
374
+ const token = this.#tokens[this.#index]! as MdzTokenTagSelfClose;
375
+ const node_type: 'Component' | 'Element' = token.is_component ? 'Component' : 'Element';
376
+ this.#index++;
377
+ return {type: node_type, name: token.name, children: [], start: token.start, end: token.end};
378
+ }
379
+
380
+ // -- Paragraph flushing --
381
+
382
+ #flush_paragraph(paragraph_children: Array<MdzNode>, trim_trailing = false): MdzNode | null {
383
+ if (paragraph_children.length === 0) return null;
384
+
385
+ if (trim_trailing) {
386
+ // Trim trailing newlines from last text node
387
+ const last = paragraph_children[paragraph_children.length - 1]!;
388
+ if (last.type === 'Text') {
389
+ const trimmed = last.content.replace(/\n+$/, '');
390
+ if (trimmed) {
391
+ last.content = trimmed;
392
+ last.end = last.start + trimmed.length;
393
+ } else {
394
+ paragraph_children.pop();
395
+ }
396
+ }
397
+
398
+ // Skip whitespace-only paragraphs
399
+ const has_content = paragraph_children.some(
400
+ (n) => n.type !== 'Text' || n.content.trim().length > 0,
401
+ );
402
+ if (!has_content) {
403
+ paragraph_children.length = 0;
404
+ return null;
405
+ }
406
+ }
407
+
408
+ // Single tag extraction (MDX convention)
409
+ const single_tag = extract_single_tag(paragraph_children);
410
+ if (single_tag) {
411
+ paragraph_children.length = 0;
412
+ return single_tag;
413
+ }
414
+
415
+ // Merge adjacent text nodes
416
+ const merged = this.#merge_adjacent_text(paragraph_children.slice());
417
+ paragraph_children.length = 0;
418
+
419
+ if (merged.length === 0) return null;
420
+
421
+ return {
422
+ type: 'Paragraph',
423
+ children: merged,
424
+ start: merged[0]!.start,
425
+ end: merged[merged.length - 1]!.end,
426
+ };
427
+ }
428
+
429
+ #merge_adjacent_text(nodes: Array<MdzNode>): Array<MdzNode> {
430
+ if (nodes.length <= 1) return nodes;
431
+
432
+ const merged: Array<MdzNode> = [];
433
+ let pending_text: MdzTextNode | null = null;
434
+
435
+ for (const node of nodes) {
436
+ if (node.type === 'Text') {
437
+ if (pending_text) {
438
+ pending_text = {
439
+ type: 'Text',
440
+ content: pending_text.content + node.content,
441
+ start: pending_text.start,
442
+ end: node.end,
443
+ };
444
+ } else {
445
+ pending_text = {...node} as MdzTextNode;
446
+ }
447
+ } else {
448
+ if (pending_text) {
449
+ merged.push(pending_text);
450
+ pending_text = null;
451
+ }
452
+ merged.push(node);
453
+ }
454
+ }
455
+
456
+ if (pending_text) merged.push(pending_text);
457
+
458
+ return merged;
459
+ }
460
+ }
@@ -305,6 +305,22 @@ const find_mdz_usages = (
305
305
  const content_attr = find_attribute(node, 'content');
306
306
  if (!content_attr) return;
307
307
 
308
+ // Extract optional static base prop for relative path resolution.
309
+ // If base is present but dynamic, skip precompilation entirely —
310
+ // MdzPrecompiled doesn't resolve relative paths at runtime,
311
+ // so precompiling with unresolved relative links would be wrong.
312
+ const base_attr = find_attribute(node, 'base');
313
+ let base: string | undefined;
314
+ if (base_attr) {
315
+ const base_value = extract_static_string(base_attr.value, context.bindings);
316
+ if (base_value === null) return; // dynamic base — fall back to runtime
317
+ base = base_value;
318
+ }
319
+
320
+ // Collect attributes to exclude from precompiled output
321
+ const exclude_attrs: Set<AST.Attribute> = new Set([content_attr]);
322
+ if (base_attr) exclude_attrs.add(base_attr);
323
+
308
324
  // Extract static string value
309
325
  const content_value = extract_static_string(content_attr.value, context.bindings);
310
326
  if (content_value !== null) {
@@ -312,7 +328,7 @@ const find_mdz_usages = (
312
328
  let result;
313
329
  try {
314
330
  const nodes = mdz_parse(content_value);
315
- result = mdz_to_svelte(nodes, context.components, context.elements);
331
+ result = mdz_to_svelte(nodes, context.components, context.elements, base);
316
332
  } catch (error) {
317
333
  handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
318
334
  return;
@@ -322,7 +338,7 @@ const find_mdz_usages = (
322
338
  if (result.has_unconfigured_tags) return;
323
339
 
324
340
  const consumed = collect_consumed_bindings(content_attr.value, context.bindings);
325
- const replacement = build_replacement(node, content_attr, result.markup, context.source);
341
+ const replacement = build_replacement(node, exclude_attrs, result.markup, context.source);
326
342
  transformed_usages.set(node.name, (transformed_usages.get(node.name) ?? 0) + 1);
327
343
  transformations.push({
328
344
  start: node.start,
@@ -349,7 +365,7 @@ const find_mdz_usages = (
349
365
  try {
350
366
  for (const branch of chain) {
351
367
  const nodes = mdz_parse(branch.value);
352
- const result = mdz_to_svelte(nodes, context.components, context.elements);
368
+ const result = mdz_to_svelte(nodes, context.components, context.elements, base);
353
369
  if (result.has_unconfigured_tags) return;
354
370
  branch_results.push({markup: result.markup, imports: result.imports});
355
371
  }
@@ -373,7 +389,7 @@ const find_mdz_usages = (
373
389
  }
374
390
  children_markup += '{/if}';
375
391
 
376
- const replacement = build_replacement(node, content_attr, children_markup, context.source);
392
+ const replacement = build_replacement(node, exclude_attrs, children_markup, context.source);
377
393
 
378
394
  // Merge imports from all branches
379
395
  const merged_imports: Map<string, PreprocessImportInfo> = new Map();
@@ -478,14 +494,14 @@ const remove_dead_const_bindings = (
478
494
  */
479
495
  const build_replacement = (
480
496
  node: AST.Component,
481
- content_attr: AST.Attribute,
497
+ exclude_attrs: ReadonlySet<AST.Attribute>,
482
498
  children_markup: string,
483
499
  source: string,
484
500
  ): string => {
485
- // Collect source ranges of all attributes EXCEPT content
501
+ // Collect source ranges of all attributes except excluded ones (content, base when resolved)
486
502
  const other_attr_ranges: Array<{start: number; end: number}> = [];
487
503
  for (const attr of node.attributes) {
488
- if (attr === content_attr) continue;
504
+ if (exclude_attrs.has(attr as AST.Attribute)) continue;
489
505
  other_attr_ranges.push({start: attr.start, end: attr.end});
490
506
  }
491
507