@readme/markdown 15.0.1 → 15.1.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.
package/dist/main.js CHANGED
@@ -12179,6 +12179,9 @@ const GlossaryContext = (0,external_amd_react_commonjs_react_commonjs2_react_roo
12179
12179
 
12180
12180
 
12181
12181
 
12182
+ /** Tippy portals to document.body by default, which would break the
12183
+ * `.rm-ReadMe[data-color-mode] …` descendant selectors in style.scss. */
12184
+ const appendToReadMeRoot = (ref) => ref.closest('.rm-ReadMe') ?? document.body;
12182
12185
  const Glossary = ({ children, term: termProp, terms }) => {
12183
12186
  const term = (Array.isArray(children) ? children[0] : children) || termProp;
12184
12187
  if (!term)
@@ -12186,7 +12189,7 @@ const Glossary = ({ children, term: termProp, terms }) => {
12186
12189
  const foundTerm = terms.find(i => term.toLowerCase() === i?.term?.toLowerCase());
12187
12190
  if (!foundTerm)
12188
12191
  return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("span", null, term);
12189
- return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement((react_default()), { content: external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("div", { className: "GlossaryItem-tooltip-content" },
12192
+ return (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement((react_default()), { appendTo: appendToReadMeRoot, content: external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("div", { className: "GlossaryItem-tooltip-content" },
12190
12193
  external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("strong", { className: "GlossaryItem-term" }, foundTerm.term),
12191
12194
  " - ",
12192
12195
  foundTerm.definition), offset: [-5, 5], placement: "bottom-start" },
@@ -54765,12 +54768,104 @@ const mdast = (text, opts = {}) => {
54765
54768
  */
54766
54769
  const jsxAcornParser = external_acorn_.Parser.extend(acorn_jsx_default()());
54767
54770
 
54771
+ ;// ./lib/utils/literal-expression.ts
54772
+
54773
+ const unsupported = (description) => new Error(`not a literal expression: ${description}`);
54774
+ /** Narrows a resolved value for arithmetic, so operands never coerce to `NaN` or `"[object Object]"`. */
54775
+ const asNumber = (value) => {
54776
+ if (typeof value !== 'number')
54777
+ throw unsupported('non-numeric operand');
54778
+ return value;
54779
+ };
54780
+ const asOperand = (value) => {
54781
+ if (typeof value !== 'number' && typeof value !== 'string')
54782
+ throw unsupported('non-primitive operand');
54783
+ return value;
54784
+ };
54785
+ const ARITHMETIC_OPERATORS = {
54786
+ // `+` doubles as string concatenation, so it accepts a string on either side.
54787
+ '+': (left, right) => (typeof left === 'number' && typeof right === 'number' ? left + right : `${left}${right}`),
54788
+ '-': (left, right) => asNumber(left) - asNumber(right),
54789
+ '*': (left, right) => asNumber(left) * asNumber(right),
54790
+ '/': (left, right) => asNumber(left) / asNumber(right),
54791
+ };
54792
+ /** Resolve one estree node, recursing into arrays and objects. Throws for anything not on the allowlist. */
54793
+ const resolveNode = (node) => {
54794
+ switch (node.type) {
54795
+ case 'Literal':
54796
+ // Regex and bigint `Literal`s hold values a tree consumer can't serialise;
54797
+ // a BigInt throws outright in `JSON.stringify`.
54798
+ if ('regex' in node || 'bigint' in node)
54799
+ throw unsupported('regex or bigint literal');
54800
+ return node.value;
54801
+ case 'Identifier':
54802
+ if (node.name !== 'undefined')
54803
+ throw unsupported(`identifier \`${node.name}\``);
54804
+ return undefined;
54805
+ case 'TemplateLiteral':
54806
+ if (node.expressions.length)
54807
+ throw unsupported('template substitution');
54808
+ return node.quasis.map(quasi => quasi.value.cooked).join('');
54809
+ case 'UnaryExpression':
54810
+ if (node.operator !== '-')
54811
+ throw unsupported(`operator \`${node.operator}\``);
54812
+ return -asNumber(resolveNode(node.argument));
54813
+ case 'BinaryExpression': {
54814
+ const applyOperator = ARITHMETIC_OPERATORS[node.operator];
54815
+ if (!applyOperator)
54816
+ throw unsupported(`operator \`${node.operator}\``);
54817
+ return applyOperator(asOperand(resolveNode(node.left)), asOperand(resolveNode(node.right)));
54818
+ }
54819
+ case 'ArrayExpression':
54820
+ return node.elements.map(element => (element === null ? null : resolveNode(element)));
54821
+ case 'ObjectExpression':
54822
+ return node.properties.reduce((memo, property) => {
54823
+ if (property.type !== 'Property' || property.computed)
54824
+ throw unsupported('computed or spread property');
54825
+ const { key } = property;
54826
+ if (key.type !== 'Identifier' && key.type !== 'Literal')
54827
+ throw unsupported('property key');
54828
+ const name = key.type === 'Identifier' ? key.name : String(key.value);
54829
+ // `__proto__` would replace the object's prototype rather than add a property.
54830
+ if (name === '__proto__')
54831
+ throw unsupported('`__proto__` key');
54832
+ memo[name] = resolveNode(property.value);
54833
+ return memo;
54834
+ }, {});
54835
+ default:
54836
+ throw unsupported(node.type);
54837
+ }
54838
+ };
54839
+ /**
54840
+ * Resolve a JSX attribute expression's source to its value without executing it.
54841
+ *
54842
+ * Supports the literal syntax attributes actually use — `true`, `"a"`, `` `a` ``,
54843
+ * `undefined`, `{ textAlign: "left" }`, `["left"]`, `1 + 1`, `'https://' + 'x.com'` —
54844
+ * and refuses everything else, so no identifier, member access, call, assignment or
54845
+ * function body can ever run. The trade-off is that expressions needing a scope or a
54846
+ * method call (`{item.url}`, `{"a".toUpperCase()}`) throw, and callers keep the raw
54847
+ * source instead — the same fallback they already used for an expression that threw.
54848
+ * mdxish rendering is unaffected, since it resolves those later with its scoped
54849
+ * evaluator; only consumers reading attributes straight off the tree see the source.
54850
+ *
54851
+ * @param source expression body, without the surrounding braces
54852
+ * @throws if `source` is unparseable or isn't a supported literal expression
54853
+ */
54854
+ const evaluateLiteralExpression = (source) => {
54855
+ const expression = jsxAcornParser.parseExpressionAt(source, 0, { ecmaVersion: 'latest' });
54856
+ // acorn stops at the first complete expression, so anything trailing means this isn't one.
54857
+ if (expression.end !== source.trimEnd().length)
54858
+ throw unsupported('trailing content');
54859
+ return resolveNode(expression);
54860
+ };
54861
+
54768
54862
  ;// ./processor/utils.ts
54769
54863
 
54770
54864
 
54771
54865
 
54772
54866
 
54773
54867
 
54868
+
54774
54869
  /**
54775
54870
  * Evaluate a JavaScript expression source and return its value.
54776
54871
  *
@@ -54868,8 +54963,10 @@ const getAttrs = (jsx) => jsx.attributes.reduce((memo, attr) => {
54868
54963
  memo[attr.name] = decode_decodeHTMLStrict(attr.value);
54869
54964
  }
54870
54965
  else if (attr.value?.value !== undefined) {
54966
+ // Expression values only survive to here when safeMode is off; `flattenAttributeExpressions`
54967
+ // rewrites them to plain strings at the head of the pipeline otherwise.
54871
54968
  try {
54872
- memo[attr.name] = evaluate(attr.value.value);
54969
+ memo[attr.name] = evaluateLiteralExpression(attr.value.value);
54873
54970
  }
54874
54971
  catch {
54875
54972
  memo[attr.name] = attr.value.value;
@@ -55650,6 +55747,31 @@ const embedTransformer = () => {
55650
55747
  };
55651
55748
  /* harmony default export */ const transform_embeds = (embedTransformer);
55652
55749
 
55750
+ ;// ./processor/transform/flatten-attribute-expressions.ts
55751
+
55752
+
55753
+ /**
55754
+ * Rewrites JSX attribute expressions (`icon={String(1 + 3)}`) into plain string attributes
55755
+ * holding their literal source, so nothing downstream can evaluate them.
55756
+ *
55757
+ * This is the RMDX counterpart to mdxish's `preserveExpressionsAsText` parse option: safeMode's
55758
+ * contract is enforced once, at the head of the pipeline, rather than at every `getAttrs()` call
55759
+ * site. Only registered when safeMode is on.
55760
+ */
55761
+ const flattenAttributeExpressions = () => tree => {
55762
+ visit(tree, isMDXElement, (node) => {
55763
+ node.attributes.forEach(attr => {
55764
+ if (!('name' in attr))
55765
+ return;
55766
+ if (attr.value === null || typeof attr.value === 'string')
55767
+ return;
55768
+ attr.value = attr.value.value;
55769
+ });
55770
+ });
55771
+ return tree;
55772
+ };
55773
+ /* harmony default export */ const flatten_attribute_expressions = (flattenAttributeExpressions);
55774
+
55653
55775
  ;// ./node_modules/gemoji/index.js
55654
55776
  /**
55655
55777
  * @typedef Gemoji
@@ -76954,13 +77076,36 @@ const MARKER_PATTERNS = [
76954
77076
  // Pattern for ** bold **
76955
77077
  // Groups: 1=wordBefore, 2=marker, 3=contentWithSpaceAfter, 4=trailingSpace1, 5=contentWithSpaceBefore, 6=trailingSpace2, 7=afterChar
76956
77078
  // trailingSpace1 is for "** text **" pattern, trailingSpace2 is for "**text **" pattern
76957
- const asteriskBoldRegex = /([^*\s]+)?\s*(\*\*)(?:\s+((?:[^*\n]|\*(?!\*))+?)(\s*)\2|((?:[^*\n]|\*(?!\*))+?)(\s+)\2)(\S|$)?/g;
77079
+ //
77080
+ // The wordBefore and whitespace prefixes are deliberately bounded ({1,64} /
77081
+ // {0,8}) rather than unbounded (+ / *). An unbounded prefix makes matchAll
77082
+ // re-scan an arbitrarily long run from every character position, which turns
77083
+ // the pass O(n²) on text nodes containing huge unbroken tokens (pasted base64
77084
+ // payloads, minified code). Bounding the prefix caps the backtracking per
77085
+ // position; a prefix longer than the bound just starts the match later, and
77086
+ // the cut-off chars flow into the preceding text node instead — adjacent text
77087
+ // parts are merged before splicing, so the emitted AST is unchanged.
77088
+ //
77089
+ // The content quantifiers are bounded too ({1,500}). The underscore content
77090
+ // clauses can scan across `_` (needed for snake_case content), so without a
77091
+ // bound every `_` in a marker-dense token (base64url, snake_case identifiers)
77092
+ // re-scans to end-of-line looking for a closer — O(n²) again. Content already
77093
+ // can't cross a newline, and 500 chars covers any sentence-length emphasis
77094
+ // phrase; longer spans stay unnormalized rather than costing quadratic scans.
77095
+ const asteriskBoldRegex = /([^*\s]{1,64})?\s{0,8}(\*\*)(?:\s+((?:[^*\n]|\*(?!\*)){1,500}?)(\s*)\2|((?:[^*\n]|\*(?!\*)){1,500}?)(\s+)\2)(\S|$)?/g;
76958
77096
  // Pattern for __ bold __
76959
- const underscoreBoldRegex = /([^_\s]+)?\s*(__)(?:\s+((?:__(?! )|_(?!_)|[^_\n])+?)(\s*)\2|((?:__(?! )|_(?!_)|[^_\n])+?)(\s+)\2)(\S|$)?/g;
77097
+ const underscoreBoldRegex = /([^_\s]{1,64})?\s{0,8}(__)(?:\s+((?:__(?! )|_(?!_)|[^_\n]){1,500}?)(\s*)\2|((?:__(?! )|_(?!_)|[^_\n]){1,500}?)(\s+)\2)(\S|$)?/g;
76960
77098
  // Pattern for * italic *
76961
- const asteriskItalicRegex = /([^*\s]+)?\s*(\*)(?!\*)(?:\s+([^*\n]+?)(\s*)\2|([^*\n]+?)(\s+)\2)(\S|$)?/g;
77099
+ const asteriskItalicRegex = /([^*\s]{1,64})?\s{0,8}(\*)(?!\*)(?:\s+([^*\n]{1,500}?)(\s*)\2|([^*\n]{1,500}?)(\s+)\2)(\S|$)?/g;
76962
77100
  // Pattern for _ italic _
76963
- const underscoreItalicRegex = /([^_\s]+)?\s*(_)(?!_)(?:\s+((?:[^_\n]|_(?! ))+?)(\s*)\2|((?:[^_\n]|_(?! ))+?)(\s+)\2)(\S|$)?/g;
77101
+ const underscoreItalicRegex = /([^_\s]{1,64})?\s{0,8}(_)(?!_)(?:\s+((?:[^_\n]|_(?! )){1,500}?)(\s*)\2|((?:[^_\n]|_(?! )){1,500}?)(\s+)\2)(\S|$)?/g;
77102
+ // Every loose alternation requires whitespace beside a marker — after the
77103
+ // opening (`** text**`) or before the closing (`**text **`) — so
77104
+ // marker-beside-whitespace is an exact gate for the loose families. A single
77105
+ // linear probe skips them entirely on marker-dense tokens whose markers are
77106
+ // all intraword (base64url payloads, snake_case identifiers).
77107
+ const asteriskBesideWhitespaceRegex = /\*\s|\s\*/;
77108
+ const underscoreBesideWhitespaceRegex = /_\s|\s_/;
76964
77109
  // CommonMark ignores intraword underscores or asteriks, but we want to italicize/bold the inner part
76965
77110
  // Pattern for intraword _word_ in words like hello_world_
76966
77111
  const intrawordUnderscoreItalicRegex = /(\w)_(?!_)([a-zA-Z0-9]+)_(?![\w_])/g;
@@ -76970,6 +77115,23 @@ const intrawordUnderscoreBoldRegex = /(\w)__([a-zA-Z0-9]+)__(?![\w_])/g;
76970
77115
  const intrawordAsteriskItalicRegex = /(\w)\*(?!\*)([a-zA-Z0-9]+)\*(?![\w*])/g;
76971
77116
  // Pattern for intraword **word** in words like hello**world**
76972
77117
  const intrawordAsteriskBoldRegex = /(\w)\*\*([a-zA-Z0-9]+)\*\*(?![\w*])/g;
77118
+ // All regex families, in match-precedence order: collected matches are
77119
+ // stable-sorted by match.index, so at an equal index the earlier row wins the
77120
+ // overlap filter — keep this order. `gate` names the per-node precondition
77121
+ // (see the gate record in the visitor) checked before the family's regex
77122
+ // runs, so a node that can't possibly match never pays for a full scan: the
77123
+ // intraword families need their marker character present, and the loose
77124
+ // families additionally need that marker beside whitespace.
77125
+ const REGEX_FAMILIES = [
77126
+ { regex: asteriskBoldRegex, isBold: true, marker: '**', gate: 'asteriskLoose' },
77127
+ { regex: underscoreBoldRegex, isBold: true, marker: '__', gate: 'underscoreLoose' },
77128
+ { regex: asteriskItalicRegex, isBold: false, marker: '*', gate: 'asteriskLoose' },
77129
+ { regex: underscoreItalicRegex, isBold: false, marker: '_', gate: 'underscoreLoose' },
77130
+ { regex: intrawordUnderscoreItalicRegex, isBold: false, isIntraword: true, marker: '_', gate: 'underscore' },
77131
+ { regex: intrawordUnderscoreBoldRegex, isBold: true, isIntraword: true, marker: '__', gate: 'underscore' },
77132
+ { regex: intrawordAsteriskItalicRegex, isBold: false, isIntraword: true, marker: '*', gate: 'asterisk' },
77133
+ { regex: intrawordAsteriskBoldRegex, isBold: true, isIntraword: true, marker: '**', gate: 'asterisk' },
77134
+ ];
76973
77135
  /**
76974
77136
  * Finds opening emphasis marker in a text value.
76975
77137
  * Returns marker info if found, null otherwise.
@@ -77161,6 +77323,21 @@ function isInsideInlineHtmlCode(index, parent) {
77161
77323
  * malformed emphasis syntax. This plugin post-processes the AST to handle these cases.
77162
77324
  */
77163
77325
  const normalizeEmphasisAST = () => (tree) => {
77326
+ // Back-scanning siblings for <code>…</code> html pairs costs O(children)
77327
+ // per text node — O(children²) per parent, which bites on marker-dense
77328
+ // paragraphs where micromark emits thousands of inline children. Most
77329
+ // parents have no html children at all, so cache that check per parent and
77330
+ // skip the back-scan entirely. The cached flag survives our splices: they
77331
+ // only ever swap text nodes for text/strong/emphasis, never html.
77332
+ const hasHtmlChild = new WeakMap();
77333
+ const mayBeInsideInlineHtmlCode = (index, parent) => {
77334
+ let flag = hasHtmlChild.get(parent);
77335
+ if (flag === undefined) {
77336
+ flag = parent.children.some(child => child.type === 'html');
77337
+ hasHtmlChild.set(parent, flag);
77338
+ }
77339
+ return flag && isInsideInlineHtmlCode(index, parent);
77340
+ };
77164
77341
  visit(tree, 'text', function visitor(node, index, parent) {
77165
77342
  if (index === undefined || !parent)
77166
77343
  return undefined;
@@ -77172,41 +77349,39 @@ const normalizeEmphasisAST = () => (tree) => {
77172
77349
  // raw HTML <code>...</code> inside table cells, which the table re-parser
77173
77350
  // parses as MDX JSX (not as an mdast `inlineCode` node).
77174
77351
  if ((parent.type === 'mdxJsxTextElement' || parent.type === 'mdxJsxFlowElement') &&
77175
- 'name' in parent && parent.name === 'code') {
77352
+ 'name' in parent &&
77353
+ parent.name === 'code') {
77176
77354
  return undefined;
77177
77355
  }
77356
+ const text = node.value;
77357
+ // The regexes below can't match without their marker character, but
77358
+ // running them anyway costs a scan of the whole node — ruinous on huge
77359
+ // pasted payloads (base64 attachments, minified code). Checked before the
77360
+ // html-sibling scan so marker-free text never pays for that either.
77361
+ const hasAsterisk = text.includes('*');
77362
+ const hasUnderscore = text.includes('_');
77363
+ if (!hasAsterisk && !hasUnderscore)
77364
+ return undefined;
77178
77365
  // In GFM tables, inline <code>...</code> is represented as sibling `html`
77179
77366
  // nodes rather than as an mdxJsxTextElement, so the check above doesn't
77180
77367
  // apply. Scan backwards through siblings to see if we are enclosed by a
77181
77368
  // <code>…</code> inline HTML pair.
77182
- if (isInsideInlineHtmlCode(index, parent)) {
77369
+ if (mayBeInsideInlineHtmlCode(index, parent)) {
77183
77370
  return undefined;
77184
77371
  }
77185
- const text = node.value;
77372
+ const gates = {
77373
+ asterisk: hasAsterisk,
77374
+ asteriskLoose: hasAsterisk && asteriskBesideWhitespaceRegex.test(text),
77375
+ underscore: hasUnderscore,
77376
+ underscoreLoose: hasUnderscore && underscoreBesideWhitespaceRegex.test(text),
77377
+ };
77186
77378
  const allMatches = [];
77187
- [...text.matchAll(asteriskBoldRegex)].forEach(match => {
77188
- allMatches.push({ isBold: true, marker: '**', match });
77189
- });
77190
- [...text.matchAll(underscoreBoldRegex)].forEach(match => {
77191
- allMatches.push({ isBold: true, marker: '__', match });
77192
- });
77193
- [...text.matchAll(asteriskItalicRegex)].forEach(match => {
77194
- allMatches.push({ isBold: false, marker: '*', match });
77195
- });
77196
- [...text.matchAll(underscoreItalicRegex)].forEach(match => {
77197
- allMatches.push({ isBold: false, marker: '_', match });
77198
- });
77199
- [...text.matchAll(intrawordUnderscoreItalicRegex)].forEach(match => {
77200
- allMatches.push({ isBold: false, isIntraword: true, marker: '_', match });
77201
- });
77202
- [...text.matchAll(intrawordUnderscoreBoldRegex)].forEach(match => {
77203
- allMatches.push({ isBold: true, isIntraword: true, marker: '__', match });
77204
- });
77205
- [...text.matchAll(intrawordAsteriskItalicRegex)].forEach(match => {
77206
- allMatches.push({ isBold: false, isIntraword: true, marker: '*', match });
77207
- });
77208
- [...text.matchAll(intrawordAsteriskBoldRegex)].forEach(match => {
77209
- allMatches.push({ isBold: true, isIntraword: true, marker: '**', match });
77379
+ REGEX_FAMILIES.forEach(({ regex, gate, ...info }) => {
77380
+ if (!gates[gate])
77381
+ return;
77382
+ [...text.matchAll(regex)].forEach(match => {
77383
+ allMatches.push({ ...info, match });
77384
+ });
77210
77385
  });
77211
77386
  if (allMatches.length === 0)
77212
77387
  return undefined;
@@ -77303,9 +77478,22 @@ const normalizeEmphasisAST = () => (tree) => {
77303
77478
  parts.push({ type: 'text', value: remainingText });
77304
77479
  }
77305
77480
  }
77306
- if (parts.length > 0) {
77307
- parent.children.splice(index, 1, ...parts);
77308
- return [SKIP, index + parts.length];
77481
+ // Merge adjacent text parts so the emitted AST doesn't depend on where a
77482
+ // match happened to start (the bounded prefixes above can shift a match
77483
+ // start rightward, splitting what used to be a single text node).
77484
+ const mergedParts = parts.reduce((acc, part) => {
77485
+ const prev = acc[acc.length - 1];
77486
+ if (part.type === 'text' && prev?.type === 'text') {
77487
+ prev.value += part.value;
77488
+ }
77489
+ else {
77490
+ acc.push(part);
77491
+ }
77492
+ return acc;
77493
+ }, []);
77494
+ if (mergedParts.length > 0) {
77495
+ parent.children.splice(index, 1, ...mergedParts);
77496
+ return [SKIP, index + mergedParts.length];
77309
77497
  }
77310
77498
  return undefined;
77311
77499
  });
@@ -81419,6 +81607,7 @@ const variables = ({ asMdx } = { asMdx: true }) => tree => {
81419
81607
 
81420
81608
 
81421
81609
 
81610
+
81422
81611
  const defaultTransforms = {
81423
81612
  calloutTransformer: callouts,
81424
81613
  codeTabsTransformer: code_tabs,
@@ -81446,6 +81635,9 @@ const astProcessor = (opts = {}) => {
81446
81635
  const components = opts.components || {};
81447
81636
  let processor = remark()
81448
81637
  .use(remarkMdx)
81638
+ // Must precede every other transformer: it strips evaluable attribute expressions so no
81639
+ // downstream `getAttrs()` call can reach `evaluate()`.
81640
+ .use(opts.safeMode ? flatten_attribute_expressions : undefined)
81449
81641
  .use(remarkPlugins)
81450
81642
  .use(opts.remarkPlugins)
81451
81643
  .use(transform_variables, { asMdx: false })
@@ -96025,12 +96217,59 @@ const hastscript_lib_h = create_h_createH(node_modules_property_information_html
96025
96217
  /** @type {ReturnType<createH>} */
96026
96218
  const lib_s = create_h_createH(node_modules_property_information_svg, 'g', svg_case_sensitive_tag_names_svgCaseSensitiveTagNames)
96027
96219
 
96220
+ ;// ./utils/user.ts
96221
+ /**
96222
+ * Coerce a user variable value to a string for substitution into markdown text.
96223
+ * Non-string values (arrays, objects, numbers) are stringified via JSON or `String()`
96224
+ * so that `<<var>>` syntax doesn't produce `[object Object]` for structured data like
96225
+ * JWT `keys`.
96226
+ */
96227
+ const stringifyVariableValue = (value) => {
96228
+ if (typeof value === 'string')
96229
+ return value;
96230
+ if (value == null)
96231
+ return '';
96232
+ if (typeof value === 'object') {
96233
+ try {
96234
+ return JSON.stringify(value) ?? '';
96235
+ }
96236
+ catch {
96237
+ console.warn('[WARNING] Could not stringify a structured user variable.');
96238
+ return '';
96239
+ }
96240
+ }
96241
+ return String(value);
96242
+ };
96243
+ /**
96244
+ * Flatten `variables.user` into a string-keyed string-valued record by coercing
96245
+ * each value. Used by markdown substitution paths that need a plain
96246
+ * `Record<string, string>` lookup.
96247
+ */
96248
+ const flattenUserVariables = (user) => Object.fromEntries(Object.entries(user).map(([name, value]) => [name, stringifyVariableValue(value)]));
96249
+ const User = (variables) => {
96250
+ const { user = {}, defaults = [] } = variables || {};
96251
+ return new Proxy(user, {
96252
+ get(target, attribute) {
96253
+ if (typeof attribute === 'symbol') {
96254
+ return '';
96255
+ }
96256
+ if (attribute in target) {
96257
+ return target[attribute];
96258
+ }
96259
+ const def = defaults.find((d) => d.name === attribute);
96260
+ return def ? def.default : attribute.toUpperCase();
96261
+ },
96262
+ });
96263
+ };
96264
+ /* harmony default export */ const user = (User);
96265
+
96028
96266
  ;// ./processor/plugin/toc.ts
96029
96267
 
96030
96268
 
96031
96269
 
96032
96270
 
96033
96271
 
96272
+
96034
96273
  const HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
96035
96274
  const isHeadingTag = (tag) => (tag ? HEADING_TAGS.includes(tag) : false);
96036
96275
  const isStandardHtmlElement = (node) => STANDARD_HTML_TAGS.has(node.tagName.toLowerCase());
@@ -96142,7 +96381,7 @@ const flattenVariables = (variables) => {
96142
96381
  if (!variables)
96143
96382
  return {};
96144
96383
  return {
96145
- ...variables.user,
96384
+ ...flattenUserVariables(variables.user),
96146
96385
  ...Object.fromEntries((variables.defaults || []).filter(d => !(d.name in variables.user)).map(d => [d.name, d.default])),
96147
96386
  };
96148
96387
  };
@@ -96290,7 +96529,7 @@ const exports_exports = (doc) => {
96290
96529
 
96291
96530
  const hast = (text, opts = {}) => {
96292
96531
  const components = Object.entries(opts.components || {}).reduce((memo, [name, doc]) => {
96293
- memo[name] = lib_mdast(doc);
96532
+ memo[name] = lib_mdast(doc, { safeMode: opts.safeMode });
96294
96533
  return memo;
96295
96534
  }, {});
96296
96535
  const processor = ast_processor(opts)
@@ -106926,6 +107165,7 @@ function normalizeTableSeparator(content) {
106926
107165
  ;// ./processor/transform/mdxish/variables-code.ts
106927
107166
 
106928
107167
 
107168
+
106929
107169
  // Single combined regex so that resolved values from one pattern are never re-scanned by the other.
106930
107170
  const COMBINED_VARIABLE_REGEX = new RegExp(`${variable_.VARIABLE_REGEXP}|${variable_.MDX_VARIABLE_REGEXP}`, 'giu');
106931
107171
  // Flatten variables into a single object for easy lookup
@@ -106934,7 +107174,7 @@ function variables_code_flattenVariables(variables) {
106934
107174
  return {};
106935
107175
  return {
106936
107176
  ...Object.fromEntries((variables.defaults || []).map(d => [d.name, d.default])),
106937
- ...variables.user,
107177
+ ...flattenUserVariables(variables.user),
106938
107178
  };
106939
107179
  }
106940
107180
  function resolveCodeVariables(value, resolvedVariables) {
@@ -107478,24 +107718,6 @@ const Contexts = ({ children, terms = [], variables = { user: {}, defaults: [] }
107478
107718
  };
107479
107719
  /* harmony default export */ const contexts = (Contexts);
107480
107720
 
107481
- ;// ./utils/user.ts
107482
- const User = (variables) => {
107483
- const { user = {}, defaults = [] } = variables || {};
107484
- return new Proxy(user, {
107485
- get(target, attribute) {
107486
- if (typeof attribute === 'symbol') {
107487
- return '';
107488
- }
107489
- if (attribute in target) {
107490
- return target[attribute];
107491
- }
107492
- const def = defaults.find((d) => d.name === attribute);
107493
- return def ? def.default : attribute.toUpperCase();
107494
- },
107495
- });
107496
- };
107497
- /* harmony default export */ const user = (User);
107498
-
107499
107721
  ;// ./lib/utils/makeUseMdxComponents.ts
107500
107722
 
107501
107723
 
@@ -107528,6 +107750,7 @@ const makeUseMDXComponents = (more = {}) => {
107528
107750
 
107529
107751
  ;// ./lib/utils/mdxish/mdxish-variables.ts
107530
107752
 
107753
+
107531
107754
  // The `$` guard skips template-literal interpolation: `${user.name}` embeds `{user.name}`, and
107532
107755
  // substituting it would leave a mangled `` `Hi $Name` `` behind. Those belong to an expression,
107533
107756
  // which either evaluated already or is meant to stay literal.
@@ -107551,7 +107774,7 @@ function resolveAttributeVariables(value, user) {
107551
107774
  .replace(MDX_VARIABLE_REGEX, (source, escapePrefix, name, escapeSuffix) => {
107552
107775
  if (escapePrefix || escapeSuffix)
107553
107776
  return source;
107554
- return user[name];
107777
+ return stringifyVariableValue(user[name]);
107555
107778
  });
107556
107779
  }
107557
107780
 
@@ -107827,7 +108050,8 @@ const run_run = (string, _opts = {}) => {
107827
108050
 
107828
108051
  const tags = (doc) => {
107829
108052
  const set = new Set();
107830
- visit(lib_mdast(doc), isMDXElement, (node) => {
108053
+ // Tag names never depend on evaluated attribute values, so always parse in safeMode.
108054
+ visit(lib_mdast(doc, { safeMode: true }), isMDXElement, (node) => {
107831
108055
  if (node.name?.match(/^[A-Z]/)) {
107832
108056
  set.add(node.name);
107833
108057
  }
@@ -107843,13 +108067,14 @@ const tags = (doc) => {
107843
108067
 
107844
108068
 
107845
108069
 
107846
- const { micromarkExtensions, fromMarkdownExtensions } = mdxishExtensions(FEATURES.tags);
108070
+ const { micromarkExtensions, fromMarkdownExtensions } = mdxishExtensions(FEATURES.tags, { safeMode: true });
107847
108071
  const mdxishTags_tags = (doc) => {
107848
108072
  const set = new Set();
107849
108073
  const processor = remark()
107850
108074
  .data('micromarkExtensions', micromarkExtensions)
107851
108075
  .data('fromMarkdownExtensions', fromMarkdownExtensions)
107852
- .use(mdx_blocks)
108076
+ // Tag names never depend on evaluated attribute values, so always parse in safeMode.
108077
+ .use(mdx_blocks, { safeMode: true })
107853
108078
  .use(mdxish_tables);
107854
108079
  const tree = processor.parse(doc);
107855
108080
  visit(processor.runSync(tree), isMDXElement, (node) => {