@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.node.js CHANGED
@@ -24807,6 +24807,9 @@ const GlossaryContext = (0,external_react_.createContext)([]);
24807
24807
 
24808
24808
 
24809
24809
 
24810
+ /** Tippy portals to document.body by default, which would break the
24811
+ * `.rm-ReadMe[data-color-mode] …` descendant selectors in style.scss. */
24812
+ const appendToReadMeRoot = (ref) => ref.closest('.rm-ReadMe') ?? document.body;
24810
24813
  const Glossary = ({ children, term: termProp, terms }) => {
24811
24814
  const term = (Array.isArray(children) ? children[0] : children) || termProp;
24812
24815
  if (!term)
@@ -24814,7 +24817,7 @@ const Glossary = ({ children, term: termProp, terms }) => {
24814
24817
  const foundTerm = terms.find(i => term.toLowerCase() === i?.term?.toLowerCase());
24815
24818
  if (!foundTerm)
24816
24819
  return external_react_default().createElement("span", null, term);
24817
- return (external_react_default().createElement(tippy_react_esm, { content: external_react_default().createElement("div", { className: "GlossaryItem-tooltip-content" },
24820
+ return (external_react_default().createElement(tippy_react_esm, { appendTo: appendToReadMeRoot, content: external_react_default().createElement("div", { className: "GlossaryItem-tooltip-content" },
24818
24821
  external_react_default().createElement("strong", { className: "GlossaryItem-term" }, foundTerm.term),
24819
24822
  " - ",
24820
24823
  foundTerm.definition), offset: [-5, 5], placement: "bottom-start" },
@@ -74989,12 +74992,104 @@ const mdast = (text, opts = {}) => {
74989
74992
  */
74990
74993
  const jsxAcornParser = Parser.extend(acorn_jsx_default()());
74991
74994
 
74995
+ ;// ./lib/utils/literal-expression.ts
74996
+
74997
+ const unsupported = (description) => new Error(`not a literal expression: ${description}`);
74998
+ /** Narrows a resolved value for arithmetic, so operands never coerce to `NaN` or `"[object Object]"`. */
74999
+ const asNumber = (value) => {
75000
+ if (typeof value !== 'number')
75001
+ throw unsupported('non-numeric operand');
75002
+ return value;
75003
+ };
75004
+ const asOperand = (value) => {
75005
+ if (typeof value !== 'number' && typeof value !== 'string')
75006
+ throw unsupported('non-primitive operand');
75007
+ return value;
75008
+ };
75009
+ const ARITHMETIC_OPERATORS = {
75010
+ // `+` doubles as string concatenation, so it accepts a string on either side.
75011
+ '+': (left, right) => (typeof left === 'number' && typeof right === 'number' ? left + right : `${left}${right}`),
75012
+ '-': (left, right) => asNumber(left) - asNumber(right),
75013
+ '*': (left, right) => asNumber(left) * asNumber(right),
75014
+ '/': (left, right) => asNumber(left) / asNumber(right),
75015
+ };
75016
+ /** Resolve one estree node, recursing into arrays and objects. Throws for anything not on the allowlist. */
75017
+ const resolveNode = (node) => {
75018
+ switch (node.type) {
75019
+ case 'Literal':
75020
+ // Regex and bigint `Literal`s hold values a tree consumer can't serialise;
75021
+ // a BigInt throws outright in `JSON.stringify`.
75022
+ if ('regex' in node || 'bigint' in node)
75023
+ throw unsupported('regex or bigint literal');
75024
+ return node.value;
75025
+ case 'Identifier':
75026
+ if (node.name !== 'undefined')
75027
+ throw unsupported(`identifier \`${node.name}\``);
75028
+ return undefined;
75029
+ case 'TemplateLiteral':
75030
+ if (node.expressions.length)
75031
+ throw unsupported('template substitution');
75032
+ return node.quasis.map(quasi => quasi.value.cooked).join('');
75033
+ case 'UnaryExpression':
75034
+ if (node.operator !== '-')
75035
+ throw unsupported(`operator \`${node.operator}\``);
75036
+ return -asNumber(resolveNode(node.argument));
75037
+ case 'BinaryExpression': {
75038
+ const applyOperator = ARITHMETIC_OPERATORS[node.operator];
75039
+ if (!applyOperator)
75040
+ throw unsupported(`operator \`${node.operator}\``);
75041
+ return applyOperator(asOperand(resolveNode(node.left)), asOperand(resolveNode(node.right)));
75042
+ }
75043
+ case 'ArrayExpression':
75044
+ return node.elements.map(element => (element === null ? null : resolveNode(element)));
75045
+ case 'ObjectExpression':
75046
+ return node.properties.reduce((memo, property) => {
75047
+ if (property.type !== 'Property' || property.computed)
75048
+ throw unsupported('computed or spread property');
75049
+ const { key } = property;
75050
+ if (key.type !== 'Identifier' && key.type !== 'Literal')
75051
+ throw unsupported('property key');
75052
+ const name = key.type === 'Identifier' ? key.name : String(key.value);
75053
+ // `__proto__` would replace the object's prototype rather than add a property.
75054
+ if (name === '__proto__')
75055
+ throw unsupported('`__proto__` key');
75056
+ memo[name] = resolveNode(property.value);
75057
+ return memo;
75058
+ }, {});
75059
+ default:
75060
+ throw unsupported(node.type);
75061
+ }
75062
+ };
75063
+ /**
75064
+ * Resolve a JSX attribute expression's source to its value without executing it.
75065
+ *
75066
+ * Supports the literal syntax attributes actually use — `true`, `"a"`, `` `a` ``,
75067
+ * `undefined`, `{ textAlign: "left" }`, `["left"]`, `1 + 1`, `'https://' + 'x.com'` —
75068
+ * and refuses everything else, so no identifier, member access, call, assignment or
75069
+ * function body can ever run. The trade-off is that expressions needing a scope or a
75070
+ * method call (`{item.url}`, `{"a".toUpperCase()}`) throw, and callers keep the raw
75071
+ * source instead — the same fallback they already used for an expression that threw.
75072
+ * mdxish rendering is unaffected, since it resolves those later with its scoped
75073
+ * evaluator; only consumers reading attributes straight off the tree see the source.
75074
+ *
75075
+ * @param source expression body, without the surrounding braces
75076
+ * @throws if `source` is unparseable or isn't a supported literal expression
75077
+ */
75078
+ const evaluateLiteralExpression = (source) => {
75079
+ const expression = jsxAcornParser.parseExpressionAt(source, 0, { ecmaVersion: 'latest' });
75080
+ // acorn stops at the first complete expression, so anything trailing means this isn't one.
75081
+ if (expression.end !== source.trimEnd().length)
75082
+ throw unsupported('trailing content');
75083
+ return resolveNode(expression);
75084
+ };
75085
+
74992
75086
  ;// ./processor/utils.ts
74993
75087
 
74994
75088
 
74995
75089
 
74996
75090
 
74997
75091
 
75092
+
74998
75093
  /**
74999
75094
  * Evaluate a JavaScript expression source and return its value.
75000
75095
  *
@@ -75092,8 +75187,10 @@ const getAttrs = (jsx) => jsx.attributes.reduce((memo, attr) => {
75092
75187
  memo[attr.name] = decode_decodeHTMLStrict(attr.value);
75093
75188
  }
75094
75189
  else if (attr.value?.value !== undefined) {
75190
+ // Expression values only survive to here when safeMode is off; `flattenAttributeExpressions`
75191
+ // rewrites them to plain strings at the head of the pipeline otherwise.
75095
75192
  try {
75096
- memo[attr.name] = evaluate(attr.value.value);
75193
+ memo[attr.name] = evaluateLiteralExpression(attr.value.value);
75097
75194
  }
75098
75195
  catch {
75099
75196
  memo[attr.name] = attr.value.value;
@@ -75874,6 +75971,31 @@ const embedTransformer = () => {
75874
75971
  };
75875
75972
  /* harmony default export */ const transform_embeds = (embedTransformer);
75876
75973
 
75974
+ ;// ./processor/transform/flatten-attribute-expressions.ts
75975
+
75976
+
75977
+ /**
75978
+ * Rewrites JSX attribute expressions (`icon={String(1 + 3)}`) into plain string attributes
75979
+ * holding their literal source, so nothing downstream can evaluate them.
75980
+ *
75981
+ * This is the RMDX counterpart to mdxish's `preserveExpressionsAsText` parse option: safeMode's
75982
+ * contract is enforced once, at the head of the pipeline, rather than at every `getAttrs()` call
75983
+ * site. Only registered when safeMode is on.
75984
+ */
75985
+ const flattenAttributeExpressions = () => tree => {
75986
+ visit(tree, isMDXElement, (node) => {
75987
+ node.attributes.forEach(attr => {
75988
+ if (!('name' in attr))
75989
+ return;
75990
+ if (attr.value === null || typeof attr.value === 'string')
75991
+ return;
75992
+ attr.value = attr.value.value;
75993
+ });
75994
+ });
75995
+ return tree;
75996
+ };
75997
+ /* harmony default export */ const flatten_attribute_expressions = (flattenAttributeExpressions);
75998
+
75877
75999
  ;// ./node_modules/gemoji/index.js
75878
76000
  /**
75879
76001
  * @typedef Gemoji
@@ -97178,13 +97300,36 @@ const MARKER_PATTERNS = [
97178
97300
  // Pattern for ** bold **
97179
97301
  // Groups: 1=wordBefore, 2=marker, 3=contentWithSpaceAfter, 4=trailingSpace1, 5=contentWithSpaceBefore, 6=trailingSpace2, 7=afterChar
97180
97302
  // trailingSpace1 is for "** text **" pattern, trailingSpace2 is for "**text **" pattern
97181
- const asteriskBoldRegex = /([^*\s]+)?\s*(\*\*)(?:\s+((?:[^*\n]|\*(?!\*))+?)(\s*)\2|((?:[^*\n]|\*(?!\*))+?)(\s+)\2)(\S|$)?/g;
97303
+ //
97304
+ // The wordBefore and whitespace prefixes are deliberately bounded ({1,64} /
97305
+ // {0,8}) rather than unbounded (+ / *). An unbounded prefix makes matchAll
97306
+ // re-scan an arbitrarily long run from every character position, which turns
97307
+ // the pass O(n²) on text nodes containing huge unbroken tokens (pasted base64
97308
+ // payloads, minified code). Bounding the prefix caps the backtracking per
97309
+ // position; a prefix longer than the bound just starts the match later, and
97310
+ // the cut-off chars flow into the preceding text node instead — adjacent text
97311
+ // parts are merged before splicing, so the emitted AST is unchanged.
97312
+ //
97313
+ // The content quantifiers are bounded too ({1,500}). The underscore content
97314
+ // clauses can scan across `_` (needed for snake_case content), so without a
97315
+ // bound every `_` in a marker-dense token (base64url, snake_case identifiers)
97316
+ // re-scans to end-of-line looking for a closer — O(n²) again. Content already
97317
+ // can't cross a newline, and 500 chars covers any sentence-length emphasis
97318
+ // phrase; longer spans stay unnormalized rather than costing quadratic scans.
97319
+ const asteriskBoldRegex = /([^*\s]{1,64})?\s{0,8}(\*\*)(?:\s+((?:[^*\n]|\*(?!\*)){1,500}?)(\s*)\2|((?:[^*\n]|\*(?!\*)){1,500}?)(\s+)\2)(\S|$)?/g;
97182
97320
  // Pattern for __ bold __
97183
- const underscoreBoldRegex = /([^_\s]+)?\s*(__)(?:\s+((?:__(?! )|_(?!_)|[^_\n])+?)(\s*)\2|((?:__(?! )|_(?!_)|[^_\n])+?)(\s+)\2)(\S|$)?/g;
97321
+ const underscoreBoldRegex = /([^_\s]{1,64})?\s{0,8}(__)(?:\s+((?:__(?! )|_(?!_)|[^_\n]){1,500}?)(\s*)\2|((?:__(?! )|_(?!_)|[^_\n]){1,500}?)(\s+)\2)(\S|$)?/g;
97184
97322
  // Pattern for * italic *
97185
- const asteriskItalicRegex = /([^*\s]+)?\s*(\*)(?!\*)(?:\s+([^*\n]+?)(\s*)\2|([^*\n]+?)(\s+)\2)(\S|$)?/g;
97323
+ const asteriskItalicRegex = /([^*\s]{1,64})?\s{0,8}(\*)(?!\*)(?:\s+([^*\n]{1,500}?)(\s*)\2|([^*\n]{1,500}?)(\s+)\2)(\S|$)?/g;
97186
97324
  // Pattern for _ italic _
97187
- const underscoreItalicRegex = /([^_\s]+)?\s*(_)(?!_)(?:\s+((?:[^_\n]|_(?! ))+?)(\s*)\2|((?:[^_\n]|_(?! ))+?)(\s+)\2)(\S|$)?/g;
97325
+ const underscoreItalicRegex = /([^_\s]{1,64})?\s{0,8}(_)(?!_)(?:\s+((?:[^_\n]|_(?! )){1,500}?)(\s*)\2|((?:[^_\n]|_(?! )){1,500}?)(\s+)\2)(\S|$)?/g;
97326
+ // Every loose alternation requires whitespace beside a marker — after the
97327
+ // opening (`** text**`) or before the closing (`**text **`) — so
97328
+ // marker-beside-whitespace is an exact gate for the loose families. A single
97329
+ // linear probe skips them entirely on marker-dense tokens whose markers are
97330
+ // all intraword (base64url payloads, snake_case identifiers).
97331
+ const asteriskBesideWhitespaceRegex = /\*\s|\s\*/;
97332
+ const underscoreBesideWhitespaceRegex = /_\s|\s_/;
97188
97333
  // CommonMark ignores intraword underscores or asteriks, but we want to italicize/bold the inner part
97189
97334
  // Pattern for intraword _word_ in words like hello_world_
97190
97335
  const intrawordUnderscoreItalicRegex = /(\w)_(?!_)([a-zA-Z0-9]+)_(?![\w_])/g;
@@ -97194,6 +97339,23 @@ const intrawordUnderscoreBoldRegex = /(\w)__([a-zA-Z0-9]+)__(?![\w_])/g;
97194
97339
  const intrawordAsteriskItalicRegex = /(\w)\*(?!\*)([a-zA-Z0-9]+)\*(?![\w*])/g;
97195
97340
  // Pattern for intraword **word** in words like hello**world**
97196
97341
  const intrawordAsteriskBoldRegex = /(\w)\*\*([a-zA-Z0-9]+)\*\*(?![\w*])/g;
97342
+ // All regex families, in match-precedence order: collected matches are
97343
+ // stable-sorted by match.index, so at an equal index the earlier row wins the
97344
+ // overlap filter — keep this order. `gate` names the per-node precondition
97345
+ // (see the gate record in the visitor) checked before the family's regex
97346
+ // runs, so a node that can't possibly match never pays for a full scan: the
97347
+ // intraword families need their marker character present, and the loose
97348
+ // families additionally need that marker beside whitespace.
97349
+ const REGEX_FAMILIES = [
97350
+ { regex: asteriskBoldRegex, isBold: true, marker: '**', gate: 'asteriskLoose' },
97351
+ { regex: underscoreBoldRegex, isBold: true, marker: '__', gate: 'underscoreLoose' },
97352
+ { regex: asteriskItalicRegex, isBold: false, marker: '*', gate: 'asteriskLoose' },
97353
+ { regex: underscoreItalicRegex, isBold: false, marker: '_', gate: 'underscoreLoose' },
97354
+ { regex: intrawordUnderscoreItalicRegex, isBold: false, isIntraword: true, marker: '_', gate: 'underscore' },
97355
+ { regex: intrawordUnderscoreBoldRegex, isBold: true, isIntraword: true, marker: '__', gate: 'underscore' },
97356
+ { regex: intrawordAsteriskItalicRegex, isBold: false, isIntraword: true, marker: '*', gate: 'asterisk' },
97357
+ { regex: intrawordAsteriskBoldRegex, isBold: true, isIntraword: true, marker: '**', gate: 'asterisk' },
97358
+ ];
97197
97359
  /**
97198
97360
  * Finds opening emphasis marker in a text value.
97199
97361
  * Returns marker info if found, null otherwise.
@@ -97385,6 +97547,21 @@ function isInsideInlineHtmlCode(index, parent) {
97385
97547
  * malformed emphasis syntax. This plugin post-processes the AST to handle these cases.
97386
97548
  */
97387
97549
  const normalizeEmphasisAST = () => (tree) => {
97550
+ // Back-scanning siblings for <code>…</code> html pairs costs O(children)
97551
+ // per text node — O(children²) per parent, which bites on marker-dense
97552
+ // paragraphs where micromark emits thousands of inline children. Most
97553
+ // parents have no html children at all, so cache that check per parent and
97554
+ // skip the back-scan entirely. The cached flag survives our splices: they
97555
+ // only ever swap text nodes for text/strong/emphasis, never html.
97556
+ const hasHtmlChild = new WeakMap();
97557
+ const mayBeInsideInlineHtmlCode = (index, parent) => {
97558
+ let flag = hasHtmlChild.get(parent);
97559
+ if (flag === undefined) {
97560
+ flag = parent.children.some(child => child.type === 'html');
97561
+ hasHtmlChild.set(parent, flag);
97562
+ }
97563
+ return flag && isInsideInlineHtmlCode(index, parent);
97564
+ };
97388
97565
  visit(tree, 'text', function visitor(node, index, parent) {
97389
97566
  if (index === undefined || !parent)
97390
97567
  return undefined;
@@ -97396,41 +97573,39 @@ const normalizeEmphasisAST = () => (tree) => {
97396
97573
  // raw HTML <code>...</code> inside table cells, which the table re-parser
97397
97574
  // parses as MDX JSX (not as an mdast `inlineCode` node).
97398
97575
  if ((parent.type === 'mdxJsxTextElement' || parent.type === 'mdxJsxFlowElement') &&
97399
- 'name' in parent && parent.name === 'code') {
97576
+ 'name' in parent &&
97577
+ parent.name === 'code') {
97400
97578
  return undefined;
97401
97579
  }
97580
+ const text = node.value;
97581
+ // The regexes below can't match without their marker character, but
97582
+ // running them anyway costs a scan of the whole node — ruinous on huge
97583
+ // pasted payloads (base64 attachments, minified code). Checked before the
97584
+ // html-sibling scan so marker-free text never pays for that either.
97585
+ const hasAsterisk = text.includes('*');
97586
+ const hasUnderscore = text.includes('_');
97587
+ if (!hasAsterisk && !hasUnderscore)
97588
+ return undefined;
97402
97589
  // In GFM tables, inline <code>...</code> is represented as sibling `html`
97403
97590
  // nodes rather than as an mdxJsxTextElement, so the check above doesn't
97404
97591
  // apply. Scan backwards through siblings to see if we are enclosed by a
97405
97592
  // <code>…</code> inline HTML pair.
97406
- if (isInsideInlineHtmlCode(index, parent)) {
97593
+ if (mayBeInsideInlineHtmlCode(index, parent)) {
97407
97594
  return undefined;
97408
97595
  }
97409
- const text = node.value;
97596
+ const gates = {
97597
+ asterisk: hasAsterisk,
97598
+ asteriskLoose: hasAsterisk && asteriskBesideWhitespaceRegex.test(text),
97599
+ underscore: hasUnderscore,
97600
+ underscoreLoose: hasUnderscore && underscoreBesideWhitespaceRegex.test(text),
97601
+ };
97410
97602
  const allMatches = [];
97411
- [...text.matchAll(asteriskBoldRegex)].forEach(match => {
97412
- allMatches.push({ isBold: true, marker: '**', match });
97413
- });
97414
- [...text.matchAll(underscoreBoldRegex)].forEach(match => {
97415
- allMatches.push({ isBold: true, marker: '__', match });
97416
- });
97417
- [...text.matchAll(asteriskItalicRegex)].forEach(match => {
97418
- allMatches.push({ isBold: false, marker: '*', match });
97419
- });
97420
- [...text.matchAll(underscoreItalicRegex)].forEach(match => {
97421
- allMatches.push({ isBold: false, marker: '_', match });
97422
- });
97423
- [...text.matchAll(intrawordUnderscoreItalicRegex)].forEach(match => {
97424
- allMatches.push({ isBold: false, isIntraword: true, marker: '_', match });
97425
- });
97426
- [...text.matchAll(intrawordUnderscoreBoldRegex)].forEach(match => {
97427
- allMatches.push({ isBold: true, isIntraword: true, marker: '__', match });
97428
- });
97429
- [...text.matchAll(intrawordAsteriskItalicRegex)].forEach(match => {
97430
- allMatches.push({ isBold: false, isIntraword: true, marker: '*', match });
97431
- });
97432
- [...text.matchAll(intrawordAsteriskBoldRegex)].forEach(match => {
97433
- allMatches.push({ isBold: true, isIntraword: true, marker: '**', match });
97603
+ REGEX_FAMILIES.forEach(({ regex, gate, ...info }) => {
97604
+ if (!gates[gate])
97605
+ return;
97606
+ [...text.matchAll(regex)].forEach(match => {
97607
+ allMatches.push({ ...info, match });
97608
+ });
97434
97609
  });
97435
97610
  if (allMatches.length === 0)
97436
97611
  return undefined;
@@ -97527,9 +97702,22 @@ const normalizeEmphasisAST = () => (tree) => {
97527
97702
  parts.push({ type: 'text', value: remainingText });
97528
97703
  }
97529
97704
  }
97530
- if (parts.length > 0) {
97531
- parent.children.splice(index, 1, ...parts);
97532
- return [SKIP, index + parts.length];
97705
+ // Merge adjacent text parts so the emitted AST doesn't depend on where a
97706
+ // match happened to start (the bounded prefixes above can shift a match
97707
+ // start rightward, splitting what used to be a single text node).
97708
+ const mergedParts = parts.reduce((acc, part) => {
97709
+ const prev = acc[acc.length - 1];
97710
+ if (part.type === 'text' && prev?.type === 'text') {
97711
+ prev.value += part.value;
97712
+ }
97713
+ else {
97714
+ acc.push(part);
97715
+ }
97716
+ return acc;
97717
+ }, []);
97718
+ if (mergedParts.length > 0) {
97719
+ parent.children.splice(index, 1, ...mergedParts);
97720
+ return [SKIP, index + mergedParts.length];
97533
97721
  }
97534
97722
  return undefined;
97535
97723
  });
@@ -101643,6 +101831,7 @@ const variables = ({ asMdx } = { asMdx: true }) => tree => {
101643
101831
 
101644
101832
 
101645
101833
 
101834
+
101646
101835
  const defaultTransforms = {
101647
101836
  calloutTransformer: callouts,
101648
101837
  codeTabsTransformer: code_tabs,
@@ -101670,6 +101859,9 @@ const astProcessor = (opts = {}) => {
101670
101859
  const components = opts.components || {};
101671
101860
  let processor = remark()
101672
101861
  .use(remarkMdx)
101862
+ // Must precede every other transformer: it strips evaluable attribute expressions so no
101863
+ // downstream `getAttrs()` call can reach `evaluate()`.
101864
+ .use(opts.safeMode ? flatten_attribute_expressions : undefined)
101673
101865
  .use(remarkPlugins)
101674
101866
  .use(opts.remarkPlugins)
101675
101867
  .use(transform_variables, { asMdx: false })
@@ -116249,12 +116441,59 @@ const hastscript_lib_h = create_h_createH(node_modules_property_information_html
116249
116441
  /** @type {ReturnType<createH>} */
116250
116442
  const lib_s = create_h_createH(node_modules_property_information_svg, 'g', svg_case_sensitive_tag_names_svgCaseSensitiveTagNames)
116251
116443
 
116444
+ ;// ./utils/user.ts
116445
+ /**
116446
+ * Coerce a user variable value to a string for substitution into markdown text.
116447
+ * Non-string values (arrays, objects, numbers) are stringified via JSON or `String()`
116448
+ * so that `<<var>>` syntax doesn't produce `[object Object]` for structured data like
116449
+ * JWT `keys`.
116450
+ */
116451
+ const stringifyVariableValue = (value) => {
116452
+ if (typeof value === 'string')
116453
+ return value;
116454
+ if (value == null)
116455
+ return '';
116456
+ if (typeof value === 'object') {
116457
+ try {
116458
+ return JSON.stringify(value) ?? '';
116459
+ }
116460
+ catch {
116461
+ console.warn('[WARNING] Could not stringify a structured user variable.');
116462
+ return '';
116463
+ }
116464
+ }
116465
+ return String(value);
116466
+ };
116467
+ /**
116468
+ * Flatten `variables.user` into a string-keyed string-valued record by coercing
116469
+ * each value. Used by markdown substitution paths that need a plain
116470
+ * `Record<string, string>` lookup.
116471
+ */
116472
+ const flattenUserVariables = (user) => Object.fromEntries(Object.entries(user).map(([name, value]) => [name, stringifyVariableValue(value)]));
116473
+ const User = (variables) => {
116474
+ const { user = {}, defaults = [] } = variables || {};
116475
+ return new Proxy(user, {
116476
+ get(target, attribute) {
116477
+ if (typeof attribute === 'symbol') {
116478
+ return '';
116479
+ }
116480
+ if (attribute in target) {
116481
+ return target[attribute];
116482
+ }
116483
+ const def = defaults.find((d) => d.name === attribute);
116484
+ return def ? def.default : attribute.toUpperCase();
116485
+ },
116486
+ });
116487
+ };
116488
+ /* harmony default export */ const user = (User);
116489
+
116252
116490
  ;// ./processor/plugin/toc.ts
116253
116491
 
116254
116492
 
116255
116493
 
116256
116494
 
116257
116495
 
116496
+
116258
116497
  const HEADING_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
116259
116498
  const isHeadingTag = (tag) => (tag ? HEADING_TAGS.includes(tag) : false);
116260
116499
  const isStandardHtmlElement = (node) => STANDARD_HTML_TAGS.has(node.tagName.toLowerCase());
@@ -116366,7 +116605,7 @@ const flattenVariables = (variables) => {
116366
116605
  if (!variables)
116367
116606
  return {};
116368
116607
  return {
116369
- ...variables.user,
116608
+ ...flattenUserVariables(variables.user),
116370
116609
  ...Object.fromEntries((variables.defaults || []).filter(d => !(d.name in variables.user)).map(d => [d.name, d.default])),
116371
116610
  };
116372
116611
  };
@@ -116514,7 +116753,7 @@ const exports_exports = (doc) => {
116514
116753
 
116515
116754
  const hast = (text, opts = {}) => {
116516
116755
  const components = Object.entries(opts.components || {}).reduce((memo, [name, doc]) => {
116517
- memo[name] = lib_mdast(doc);
116756
+ memo[name] = lib_mdast(doc, { safeMode: opts.safeMode });
116518
116757
  return memo;
116519
116758
  }, {});
116520
116759
  const processor = ast_processor(opts)
@@ -127150,6 +127389,7 @@ function normalizeTableSeparator(content) {
127150
127389
  ;// ./processor/transform/mdxish/variables-code.ts
127151
127390
 
127152
127391
 
127392
+
127153
127393
  // Single combined regex so that resolved values from one pattern are never re-scanned by the other.
127154
127394
  const COMBINED_VARIABLE_REGEX = new RegExp(`${variable_dist.VARIABLE_REGEXP}|${variable_dist.MDX_VARIABLE_REGEXP}`, 'giu');
127155
127395
  // Flatten variables into a single object for easy lookup
@@ -127158,7 +127398,7 @@ function variables_code_flattenVariables(variables) {
127158
127398
  return {};
127159
127399
  return {
127160
127400
  ...Object.fromEntries((variables.defaults || []).map(d => [d.name, d.default])),
127161
- ...variables.user,
127401
+ ...flattenUserVariables(variables.user),
127162
127402
  };
127163
127403
  }
127164
127404
  function resolveCodeVariables(value, resolvedVariables) {
@@ -127702,24 +127942,6 @@ const Contexts = ({ children, terms = [], variables = { user: {}, defaults: [] }
127702
127942
  };
127703
127943
  /* harmony default export */ const contexts = (Contexts);
127704
127944
 
127705
- ;// ./utils/user.ts
127706
- const User = (variables) => {
127707
- const { user = {}, defaults = [] } = variables || {};
127708
- return new Proxy(user, {
127709
- get(target, attribute) {
127710
- if (typeof attribute === 'symbol') {
127711
- return '';
127712
- }
127713
- if (attribute in target) {
127714
- return target[attribute];
127715
- }
127716
- const def = defaults.find((d) => d.name === attribute);
127717
- return def ? def.default : attribute.toUpperCase();
127718
- },
127719
- });
127720
- };
127721
- /* harmony default export */ const user = (User);
127722
-
127723
127945
  ;// ./lib/utils/makeUseMdxComponents.ts
127724
127946
 
127725
127947
 
@@ -127752,6 +127974,7 @@ const makeUseMDXComponents = (more = {}) => {
127752
127974
 
127753
127975
  ;// ./lib/utils/mdxish/mdxish-variables.ts
127754
127976
 
127977
+
127755
127978
  // The `$` guard skips template-literal interpolation: `${user.name}` embeds `{user.name}`, and
127756
127979
  // substituting it would leave a mangled `` `Hi $Name` `` behind. Those belong to an expression,
127757
127980
  // which either evaluated already or is meant to stay literal.
@@ -127775,7 +127998,7 @@ function resolveAttributeVariables(value, user) {
127775
127998
  .replace(MDX_VARIABLE_REGEX, (source, escapePrefix, name, escapeSuffix) => {
127776
127999
  if (escapePrefix || escapeSuffix)
127777
128000
  return source;
127778
- return user[name];
128001
+ return stringifyVariableValue(user[name]);
127779
128002
  });
127780
128003
  }
127781
128004
 
@@ -128051,7 +128274,8 @@ const run_run = (string, _opts = {}) => {
128051
128274
 
128052
128275
  const tags = (doc) => {
128053
128276
  const set = new Set();
128054
- visit(lib_mdast(doc), isMDXElement, (node) => {
128277
+ // Tag names never depend on evaluated attribute values, so always parse in safeMode.
128278
+ visit(lib_mdast(doc, { safeMode: true }), isMDXElement, (node) => {
128055
128279
  if (node.name?.match(/^[A-Z]/)) {
128056
128280
  set.add(node.name);
128057
128281
  }
@@ -128067,13 +128291,14 @@ const tags = (doc) => {
128067
128291
 
128068
128292
 
128069
128293
 
128070
- const { micromarkExtensions, fromMarkdownExtensions } = mdxishExtensions(FEATURES.tags);
128294
+ const { micromarkExtensions, fromMarkdownExtensions } = mdxishExtensions(FEATURES.tags, { safeMode: true });
128071
128295
  const mdxishTags_tags = (doc) => {
128072
128296
  const set = new Set();
128073
128297
  const processor = remark()
128074
128298
  .data('micromarkExtensions', micromarkExtensions)
128075
128299
  .data('fromMarkdownExtensions', fromMarkdownExtensions)
128076
- .use(mdx_blocks)
128300
+ // Tag names never depend on evaluated attribute values, so always parse in safeMode.
128301
+ .use(mdx_blocks, { safeMode: true })
128077
128302
  .use(mdxish_tables);
128078
128303
  const tree = processor.parse(doc);
128079
128304
  visit(processor.runSync(tree), isMDXElement, (node) => {