stream-markdown-parser 0.0.80 → 0.0.81

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/index.js CHANGED
@@ -6489,7 +6489,7 @@ const DEFAULT_RENDERER_OPTIONS = {
6489
6489
  xhtmlOut: false,
6490
6490
  breaks: false
6491
6491
  };
6492
- const hasOwn = Object.prototype.hasOwnProperty;
6492
+ const hasOwn$1 = Object.prototype.hasOwnProperty;
6493
6493
  const defaultRules = {
6494
6494
  code_inline(tokens, idx) {
6495
6495
  return renderCodeInlineToken(tokens[idx]);
@@ -6889,16 +6889,16 @@ var Renderer = class {
6889
6889
  if (!merged) merged = { ...base$1 };
6890
6890
  return merged;
6891
6891
  };
6892
- if (hasOwn.call(overrides, "highlight") && overrides.highlight !== base$1.highlight) ensureMerged().highlight = overrides.highlight;
6893
- if (hasOwn.call(overrides, "langPrefix")) {
6892
+ if (hasOwn$1.call(overrides, "highlight") && overrides.highlight !== base$1.highlight) ensureMerged().highlight = overrides.highlight;
6893
+ if (hasOwn$1.call(overrides, "langPrefix")) {
6894
6894
  const value = overrides.langPrefix;
6895
6895
  if (value !== base$1.langPrefix) ensureMerged().langPrefix = value;
6896
6896
  }
6897
- if (hasOwn.call(overrides, "xhtmlOut")) {
6897
+ if (hasOwn$1.call(overrides, "xhtmlOut")) {
6898
6898
  const value = overrides.xhtmlOut;
6899
6899
  if (value !== base$1.xhtmlOut) ensureMerged().xhtmlOut = value;
6900
6900
  }
6901
- if (hasOwn.call(overrides, "breaks")) {
6901
+ if (hasOwn$1.call(overrides, "breaks")) {
6902
6902
  const value = overrides.breaks;
6903
6903
  if (value !== base$1.breaks) ensureMerged().breaks = value;
6904
6904
  }
@@ -8733,10 +8733,10 @@ const config = {
8733
8733
  commonmark: commonmark_default
8734
8734
  };
8735
8735
  function hasExplicitChunkOverride(presetOptions, userOptions, keys) {
8736
- const hasOwn$1 = (obj, key) => !!obj && Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== void 0;
8736
+ const hasOwn$1$1 = (obj, key) => !!obj && Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== void 0;
8737
8737
  for (let i = 0; i < keys.length; i++) {
8738
8738
  const key = keys[i];
8739
- if (hasOwn$1(userOptions, key) || hasOwn$1(presetOptions, key)) return true;
8739
+ if (hasOwn$1$1(userOptions, key) || hasOwn$1$1(presetOptions, key)) return true;
8740
8740
  }
8741
8741
  return false;
8742
8742
  }
@@ -9602,6 +9602,124 @@ function isUnsafeHtmlUrl(value) {
9602
9602
  return false;
9603
9603
  }
9604
9604
 
9605
+ //#endregion
9606
+ //#region src/htmlTagUtils.ts
9607
+ function escapeTagForRegExp(tag) {
9608
+ return tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9609
+ }
9610
+ function findTagCloseIndexOutsideQuotes(input) {
9611
+ let inSingle = false;
9612
+ let inDouble = false;
9613
+ for (let i = 0; i < input.length; i++) {
9614
+ const ch = input[i];
9615
+ if (ch === "\\") {
9616
+ i++;
9617
+ continue;
9618
+ }
9619
+ if (!inDouble && ch === "'") {
9620
+ inSingle = !inSingle;
9621
+ continue;
9622
+ }
9623
+ if (!inSingle && ch === "\"") {
9624
+ inDouble = !inDouble;
9625
+ continue;
9626
+ }
9627
+ if (!inSingle && !inDouble && ch === ">") return i;
9628
+ }
9629
+ return -1;
9630
+ }
9631
+ function parseTagAttrs(openTag) {
9632
+ const attrs = [];
9633
+ const attrRegex = /\s([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
9634
+ let match;
9635
+ while ((match = attrRegex.exec(openTag)) !== null) {
9636
+ const attrName = match[1];
9637
+ if (!attrName) continue;
9638
+ const attrValue = match[2] || match[3] || match[4] || "";
9639
+ attrs.push([attrName, attrValue]);
9640
+ }
9641
+ return attrs;
9642
+ }
9643
+
9644
+ //#endregion
9645
+ //#region src/customHtmlTags.ts
9646
+ const HTML_LIKE_TAG_NAME_RE = /^[a-z][a-z0-9_-]*$/;
9647
+ function isHtmlLikeTagName(tag) {
9648
+ return HTML_LIKE_TAG_NAME_RE.test(String(tag ?? "").trim().toLowerCase());
9649
+ }
9650
+ function normalizeCustomHtmlTagName(value) {
9651
+ const raw = String(value ?? "").trim();
9652
+ if (!raw) return "";
9653
+ if (!raw.startsWith("<")) return isHtmlLikeTagName(raw) ? raw.toLowerCase() : "";
9654
+ let index = 1;
9655
+ while (index < raw.length && /\s/.test(raw[index])) index++;
9656
+ if (raw[index] === "/") {
9657
+ index++;
9658
+ while (index < raw.length && /\s/.test(raw[index])) index++;
9659
+ }
9660
+ const start = index;
9661
+ while (index < raw.length && /[\w-]/.test(raw[index])) index++;
9662
+ const normalized = raw.slice(start, index).toLowerCase();
9663
+ const next = raw[index] ?? "";
9664
+ if (next && !/[\s/>]/.test(next)) return "";
9665
+ return isHtmlLikeTagName(normalized) ? normalized : "";
9666
+ }
9667
+ function normalizeCustomHtmlTags(tags) {
9668
+ if (!tags || tags.length === 0) return [];
9669
+ const seen = /* @__PURE__ */ new Set();
9670
+ const normalized = [];
9671
+ for (const tag of tags) {
9672
+ const value = normalizeCustomHtmlTagName(tag);
9673
+ if (!value || seen.has(value)) continue;
9674
+ seen.add(value);
9675
+ normalized.push(value);
9676
+ }
9677
+ return normalized;
9678
+ }
9679
+ function mergeCustomHtmlTags(...lists) {
9680
+ const seen = /* @__PURE__ */ new Set();
9681
+ const normalized = [];
9682
+ for (const list$1 of lists) for (const tag of normalizeCustomHtmlTags(list$1)) {
9683
+ if (seen.has(tag)) continue;
9684
+ seen.add(tag);
9685
+ normalized.push(tag);
9686
+ }
9687
+ return normalized;
9688
+ }
9689
+ function resolveCustomHtmlTags(tags) {
9690
+ const normalized = normalizeCustomHtmlTags(tags);
9691
+ return {
9692
+ key: normalized.join(","),
9693
+ tags: normalized
9694
+ };
9695
+ }
9696
+ function getHtmlTagFromContent(html) {
9697
+ return normalizeCustomHtmlTagName(html);
9698
+ }
9699
+ function hasCompleteHtmlTagContent(html, tag) {
9700
+ const raw = String(html ?? "");
9701
+ const normalizedTag = normalizeCustomHtmlTagName(tag);
9702
+ if (!normalizedTag) return false;
9703
+ const escaped = escapeTagForRegExp(normalizedTag);
9704
+ const openMatch = raw.match(new RegExp(String.raw`^\s*<\s*${escaped}(?:\s[^>]*)?(\s*\/)?>`, "i"));
9705
+ if (!openMatch) return false;
9706
+ if (openMatch[1]) return true;
9707
+ return new RegExp(String.raw`<\s*\/\s*${escaped}\s*>`, "i").test(raw);
9708
+ }
9709
+ function shouldRenderUnknownHtmlTagAsText(html, tag) {
9710
+ const normalizedTag = normalizeCustomHtmlTagName(tag);
9711
+ return Boolean(normalizedTag) && !STANDARD_HTML_TAGS.has(normalizedTag) && !hasCompleteHtmlTagContent(html, normalizedTag);
9712
+ }
9713
+ function stripCustomHtmlWrapper(html, tag) {
9714
+ const raw = String(html ?? "");
9715
+ const normalizedTag = normalizeCustomHtmlTagName(tag);
9716
+ if (!normalizedTag) return raw;
9717
+ const escaped = escapeTagForRegExp(normalizedTag);
9718
+ const openRe = new RegExp(String.raw`^\s*<\s*${escaped}(?:\s[^>]*)?>\s*`, "i");
9719
+ const closeRe = new RegExp(String.raw`\s*<\s*\/\s*${escaped}\s*>\s*$`, "i");
9720
+ return raw.replace(openRe, "").replace(closeRe, "");
9721
+ }
9722
+
9605
9723
  //#endregion
9606
9724
  //#region src/plugins/fixHtmlInline.ts
9607
9725
  const VOID_TAGS = VOID_HTML_TAGS;
@@ -9621,9 +9739,6 @@ function isHtmlInlineClosingTag(content) {
9621
9739
  function isSelfClosingHtmlInline(content, tag) {
9622
9740
  return VOID_TAGS.has(tag) || /\/\s*>\s*$/.test(content);
9623
9741
  }
9624
- function escapeRegex$2(value) {
9625
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9626
- }
9627
9742
  function findMatchingCloseChildIndex(children, tag) {
9628
9743
  let depth = 0;
9629
9744
  for (let index = 0; index < children.length; index++) {
@@ -9657,7 +9772,7 @@ function getTrailingOpenDepth(children, tag) {
9657
9772
  return depth;
9658
9773
  }
9659
9774
  function findMatchingCloseRangeInHtml(content, tag, startIndex = 0) {
9660
- const tokenRe = new RegExp(String.raw`<\s*(\/?)\s*${escapeRegex$2(tag)}(?=[\s>/])[^>]*>`, "gi");
9775
+ const tokenRe = new RegExp(String.raw`<\s*(\/?)\s*${escapeTagForRegExp(tag)}(?=[\s>/])[^>]*>`, "gi");
9661
9776
  tokenRe.lastIndex = Math.max(0, startIndex);
9662
9777
  let depth = 0;
9663
9778
  let match;
@@ -9678,7 +9793,7 @@ function findMatchingCloseRangeInHtml(content, tag, startIndex = 0) {
9678
9793
  return null;
9679
9794
  }
9680
9795
  function getTrailingCustomTagDepthInHtml(content, tag) {
9681
- const tokenRe = new RegExp(String.raw`<\s*(\/?)\s*${escapeRegex$2(tag)}(?=[\s>/])[^>]*>`, "gi");
9796
+ const tokenRe = new RegExp(String.raw`<\s*(\/?)\s*${escapeTagForRegExp(tag)}(?=[\s>/])[^>]*>`, "gi");
9682
9797
  let depth = 0;
9683
9798
  let match;
9684
9799
  while ((match = tokenRe.exec(content)) !== null) {
@@ -9693,23 +9808,6 @@ function getTrailingCustomTagDepthInHtml(content, tag) {
9693
9808
  }
9694
9809
  return depth;
9695
9810
  }
9696
- function findTagCloseIndexOutsideQuotes$3(html) {
9697
- let inSingle = false;
9698
- let inDouble = false;
9699
- for (let i = 0; i < html.length; i++) {
9700
- const ch = html[i];
9701
- if (ch === "\"" && !inSingle) {
9702
- inDouble = !inDouble;
9703
- continue;
9704
- }
9705
- if (ch === "'" && !inDouble) {
9706
- inSingle = !inSingle;
9707
- continue;
9708
- }
9709
- if (ch === ">" && !inSingle && !inDouble) return i;
9710
- }
9711
- return -1;
9712
- }
9713
9811
  function tokenToRaw$1(token) {
9714
9812
  const shape = token;
9715
9813
  return String(shape.raw ?? shape.content ?? shape.markup ?? "");
@@ -9737,7 +9835,7 @@ function findFirstIncompleteTag(content, tagSet) {
9737
9835
  if (idx < 0) continue;
9738
9836
  const tag = (m[1] ?? "").toLowerCase();
9739
9837
  if (!isCommonHtmlTagOrPrefix(tag, tagSet)) continue;
9740
- if (findTagCloseIndexOutsideQuotes$3(content.slice(idx)) !== -1) continue;
9838
+ if (findTagCloseIndexOutsideQuotes(content.slice(idx)) !== -1) continue;
9741
9839
  if (!first || idx < first.index) first = {
9742
9840
  index: idx,
9743
9841
  tag,
@@ -9749,7 +9847,7 @@ function findFirstIncompleteTag(content, tagSet) {
9749
9847
  if (idx < 0) continue;
9750
9848
  const tag = (m[1] ?? "").toLowerCase();
9751
9849
  if (!isCommonHtmlTagOrPrefix(tag, tagSet)) continue;
9752
- if (findTagCloseIndexOutsideQuotes$3(content.slice(idx)) !== -1) continue;
9850
+ if (findTagCloseIndexOutsideQuotes(content.slice(idx)) !== -1) continue;
9753
9851
  if (!first || idx < first.index) first = {
9754
9852
  index: idx,
9755
9853
  tag,
@@ -9815,7 +9913,7 @@ function fixStreamingHtmlInlineChildren(children, tagSet) {
9815
9913
  cursor = lt + 1;
9816
9914
  continue;
9817
9915
  }
9818
- const closeIdx = findTagCloseIndexOutsideQuotes$3(sub);
9916
+ const closeIdx = findTagCloseIndexOutsideQuotes(sub);
9819
9917
  if (closeIdx === -1) {
9820
9918
  pushTextPart("<", baseToken);
9821
9919
  cursor = lt + 1;
@@ -9853,7 +9951,7 @@ function fixStreamingHtmlInlineChildren(children, tagSet) {
9853
9951
  if (pending) {
9854
9952
  pending.buffer += tokenToRaw$1(child);
9855
9953
  pendingAtEnd = pending.buffer;
9856
- const closeIdx = findTagCloseIndexOutsideQuotes$3(pending.buffer);
9954
+ const closeIdx = findTagCloseIndexOutsideQuotes(pending.buffer);
9857
9955
  if (closeIdx === -1) continue;
9858
9956
  const tagChunk = pending.buffer.slice(0, closeIdx + 1);
9859
9957
  const afterChunk = pending.buffer.slice(closeIdx + 1);
@@ -9871,7 +9969,7 @@ function fixStreamingHtmlInlineChildren(children, tagSet) {
9871
9969
  if (child.type === "html_inline") {
9872
9970
  const content = tokenToRaw$1(child);
9873
9971
  const tagName = (content.match(TAG_NAME_AT_START_RE)?.[1] ?? "").toLowerCase();
9874
- if (tagName && tagSet.has(tagName) && findTagCloseIndexOutsideQuotes$3(content) === -1) {
9972
+ if (tagName && tagSet.has(tagName) && findTagCloseIndexOutsideQuotes(content) === -1) {
9875
9973
  pending = {
9876
9974
  tag: tagName,
9877
9975
  buffer: content,
@@ -9910,11 +10008,8 @@ function applyFixHtmlInlineTokens(md, options = {}) {
9910
10008
  ]);
9911
10009
  const customTagSet = /* @__PURE__ */ new Set();
9912
10010
  if (options.customHtmlTags?.length) for (const t of options.customHtmlTags) {
9913
- const raw = String(t ?? "").trim();
9914
- if (!raw) continue;
9915
- const m = raw.match(/^[<\s/]*([A-Z][\w-]*)/i);
9916
- if (!m) continue;
9917
- const name = m[1].toLowerCase();
10011
+ const name = normalizeCustomHtmlTagName(t);
10012
+ if (!name) continue;
9918
10013
  customTagSet.add(name);
9919
10014
  autoCloseInlineTagSet.add(name);
9920
10015
  }
@@ -9977,7 +10072,7 @@ function applyFixHtmlInlineTokens(md, options = {}) {
9977
10072
  if (chunk) {
9978
10073
  const openToken = toks[openIndex];
9979
10074
  const mergedContent = `${String(openToken.content || "")}\n${chunk}`;
9980
- const openEnd = findTagCloseIndexOutsideQuotes$3(mergedContent);
10075
+ const openEnd = findTagCloseIndexOutsideQuotes(mergedContent);
9981
10076
  const closeRange = openEnd === -1 ? null : findMatchingCloseRangeInHtml(mergedContent, openTag, openEnd + 1);
9982
10077
  if (closeRange) {
9983
10078
  const before = mergedContent.slice(0, closeRange.end);
@@ -10176,7 +10271,7 @@ function applyFixHtmlInlineTokens(md, options = {}) {
10176
10271
  }
10177
10272
  if (customTagSet.has(tag)) {
10178
10273
  const raw$2 = String(t.content ?? "");
10179
- const openEnd = findTagCloseIndexOutsideQuotes$3(raw$2);
10274
+ const openEnd = findTagCloseIndexOutsideQuotes(raw$2);
10180
10275
  const closeRange = openEnd === -1 ? null : findMatchingCloseRangeInHtml(raw$2, tag, openEnd + 1);
10181
10276
  t.loading = !!closeRange ? false : t.loading !== void 0 ? t.loading : true;
10182
10277
  const endTagIndex$1 = closeRange?.start ?? -1;
@@ -12340,17 +12435,11 @@ function isClosingTag(html) {
12340
12435
  function isSelfClosing(tag, html) {
12341
12436
  return /\/\s*>\s*$/.test(html) || VOID_HTML_TAGS.has(tag);
12342
12437
  }
12343
- function normalizeCustomTag$1(t) {
12344
- const raw = String(t ?? "").trim();
12345
- if (!raw) return "";
12346
- const m = raw.match(/^[<\s/]*([A-Z][\w-]*)/i);
12347
- return m ? m[1].toLowerCase() : "";
12348
- }
12349
12438
  function getTagSets(customTags) {
12350
12439
  if (!customTags || customTags.length === 0) return getEmptyTagSets();
12351
12440
  const cached = TAG_SET_CACHE.get(customTags);
12352
12441
  if (cached) return cached;
12353
- const normalized = customTags.map(normalizeCustomTag$1).filter(Boolean);
12442
+ const normalized = customTags.map(normalizeCustomHtmlTagName).filter(Boolean);
12354
12443
  if (!normalized.length) {
12355
12444
  const entry$1 = getEmptyTagSets();
12356
12445
  TAG_SET_CACHE.set(customTags, entry$1);
@@ -12368,18 +12457,6 @@ function tokenToRaw(token) {
12368
12457
  const raw = shape.raw ?? shape.content ?? shape.markup ?? "";
12369
12458
  return String(raw ?? "");
12370
12459
  }
12371
- function parseTagAttrs$1(openTag) {
12372
- const attrs = [];
12373
- const attrRegex = /\s([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
12374
- let match;
12375
- while ((match = attrRegex.exec(openTag)) !== null) {
12376
- const attrName = match[1];
12377
- if (!attrName) continue;
12378
- const attrValue = match[2] || match[3] || match[4] || "";
12379
- attrs.push([attrName, attrValue]);
12380
- }
12381
- return attrs;
12382
- }
12383
12460
  function getAttrValue$1(attrs, name) {
12384
12461
  const lowerName = name.toLowerCase();
12385
12462
  for (let i = attrs.length - 1; i >= 0; i--) {
@@ -12474,7 +12551,7 @@ function parseHtmlInlineCodeToken(token, tokens, i, parseInlineTokens$1, raw, pP
12474
12551
  }, i + 1];
12475
12552
  if (tag === "a") {
12476
12553
  const fragment$1 = collectHtmlFragment(tokens, i, tag);
12477
- const attrs$1 = parseTagAttrs$1(code$1);
12554
+ const attrs$1 = parseTagAttrs(code$1);
12478
12555
  const innerTokens = fragment$1.innerTokens;
12479
12556
  const href = String(getAttrValue$1(attrs$1, "href") ?? "");
12480
12557
  const titleAttr = getAttrValue$1(attrs$1, "title");
@@ -12914,6 +12991,30 @@ function decodeVisibleTextFromRaw(rawText) {
12914
12991
  }
12915
12992
  return output;
12916
12993
  }
12994
+ function getRawIndexForVisibleIndex(rawText, visibleIndex) {
12995
+ let outputIndex = 0;
12996
+ for (let rawIndex = 0; rawIndex < rawText.length; rawIndex++) {
12997
+ const char = rawText[rawIndex];
12998
+ const nextChar = rawText[rawIndex + 1];
12999
+ if (char === "\\" && nextChar && ESCAPABLE_PUNCTUATION.has(nextChar)) {
13000
+ if (outputIndex === visibleIndex) return rawIndex + 1;
13001
+ outputIndex++;
13002
+ rawIndex++;
13003
+ continue;
13004
+ }
13005
+ if (outputIndex === visibleIndex) return rawIndex;
13006
+ outputIndex++;
13007
+ }
13008
+ return -1;
13009
+ }
13010
+ function isEscapedVisibleChar(rawText, visibleIndex, expectedChar) {
13011
+ const rawIndex = getRawIndexForVisibleIndex(rawText, visibleIndex);
13012
+ if (rawIndex === -1) return false;
13013
+ if (expectedChar && rawText[rawIndex] !== expectedChar) return false;
13014
+ let slashCount = 0;
13015
+ for (let i = rawIndex - 1; i >= 0 && rawText[i] === "\\"; i--) slashCount++;
13016
+ return slashCount % 2 === 1;
13017
+ }
12917
13018
  const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
12918
13019
  const WORD_ONLY_RE = /^[\p{L}\p{N}]+$/u;
12919
13020
  function isWordChar(ch) {
@@ -13359,9 +13460,11 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13359
13460
  handleLinkOpen(token);
13360
13461
  break;
13361
13462
  case "image":
13362
- resetCurrentTextNode();
13363
- pushNode(parseImageToken(token));
13364
- i++;
13463
+ if (!recoverOuterImageLinkStartFromImageToken(token)) {
13464
+ resetCurrentTextNode();
13465
+ pushNode(parseImageToken(token));
13466
+ i++;
13467
+ }
13365
13468
  break;
13366
13469
  case "strong_open": {
13367
13470
  resetCurrentTextNode();
@@ -13506,7 +13609,9 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13506
13609
  const displayText = String(token.text ?? "");
13507
13610
  pushText(displayText, displayText);
13508
13611
  i++;
13509
- } else if (recoverMarkdownLinkFromTrailingText(token)) i++;
13612
+ } else if (recoverOuterImageLinkFromSyntheticLinkToken(token)) i++;
13613
+ else if (recoverMarkdownImageFromTrailingBang(token)) i++;
13614
+ else if (recoverMarkdownLinkFromTrailingText(token)) i++;
13510
13615
  else {
13511
13616
  pushToken(token);
13512
13617
  i++;
@@ -13567,6 +13672,8 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13567
13672
  i++;
13568
13673
  return;
13569
13674
  }
13675
+ if (recoverOuterImageLinkFromRawText(content)) return;
13676
+ if (recoverOuterImageLinkMidStateFromText(content)) return;
13570
13677
  if (!(content.includes("*") || content.includes("_") || content.includes("~") || content.includes("`") || content.includes("[") || content.includes("!") || content.includes("$") || content.includes("|") || content.includes("("))) {
13571
13678
  commitTextNode(content, token, tokens[i - 1], nextToken);
13572
13679
  i++;
@@ -13574,12 +13681,12 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13574
13681
  }
13575
13682
  if (handleCheckboxLike(content)) return;
13576
13683
  const preToken = tokens[i - 1];
13577
- if (content === "[" && !nextToken?.markup?.includes("*") || content === "]" && !preToken?.markup?.includes("*")) {
13684
+ if (content === "[" && !nextToken?.markup?.includes("*") && !hasEscapedMarkup(token, "\\[") || content === "]" && !preToken?.markup?.includes("*") && !hasEscapedMarkup(token, "\\]")) {
13578
13685
  i++;
13579
13686
  return;
13580
13687
  }
13581
13688
  if (handleInlineCodeContent(rawContent, token)) return;
13582
- if (handleInlineImageContent(content, token)) return;
13689
+ if (handleInlineImageContent(content)) return;
13583
13690
  if (tokens[i + 1]?.type !== "link_open" && handleInlineLinkContent(content, token)) return;
13584
13691
  const reparsedNodes = tryReparseCollapsedInlineText(rawContent);
13585
13692
  if (reparsedNodes) {
@@ -13593,6 +13700,13 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13593
13700
  i++;
13594
13701
  }
13595
13702
  function handleLinkOpen(token) {
13703
+ if (shouldTreatLinkOpenAsTextInEscapedOuterImageTail()) {
13704
+ const { node: node$1, nextIndex: nextIndex$1 } = parseLinkToken(tokens, i, options);
13705
+ const text$1 = String(node$1.text || node$1.href || "");
13706
+ pushText(text$1, text$1);
13707
+ i = nextIndex$1;
13708
+ return;
13709
+ }
13596
13710
  resetCurrentTextNode();
13597
13711
  const { node, nextIndex } = parseLinkToken(tokens, i, options);
13598
13712
  i = nextIndex;
@@ -13662,6 +13776,149 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13662
13776
  });
13663
13777
  return true;
13664
13778
  }
13779
+ function recoverMarkdownImageFromTrailingBang(token) {
13780
+ if (token.type !== "link") return false;
13781
+ const previous = result[result.length - 1];
13782
+ const previousToken = tokens[i - 1];
13783
+ if (!previous || previous.type !== "text" || previousToken?.type !== "text") return false;
13784
+ const previousContent = String(previous.content ?? "");
13785
+ const previousTokenContent = String(previousToken.content ?? "");
13786
+ if (!previousContent.endsWith("!") || !previousTokenContent.endsWith("!")) return false;
13787
+ if (hasEscapedMarkup(previousToken, "\\!")) return false;
13788
+ const before = previousContent.slice(0, -1);
13789
+ if (before) {
13790
+ previous.content = before;
13791
+ previous.raw = before;
13792
+ currentTextNode = previous;
13793
+ } else {
13794
+ result.pop();
13795
+ currentTextNode = null;
13796
+ }
13797
+ const linkToken = token;
13798
+ const alt = String(linkToken.text ?? linkToken.children?.map((child) => String(child?.content ?? child?.raw ?? "")).join("") ?? "");
13799
+ const href = String(linkToken.href ?? "");
13800
+ const title = linkToken.title == null || linkToken.title === "" ? null : String(linkToken.title);
13801
+ pushParsed({
13802
+ type: "image",
13803
+ src: href,
13804
+ alt,
13805
+ title,
13806
+ raw: String(`![${alt}](${href}${title ? ` "${title}"` : ""})`),
13807
+ loading: Boolean(linkToken.loading)
13808
+ });
13809
+ return true;
13810
+ }
13811
+ function buildLoadingOuterImageLinkNode(imageNode, href = "", title = null) {
13812
+ const text$1 = String(imageNode.alt ?? imageNode.raw ?? "");
13813
+ return {
13814
+ type: "link",
13815
+ href,
13816
+ title,
13817
+ text: text$1,
13818
+ children: [imageNode],
13819
+ raw: String(`[${text$1}](${href}${title ? ` "${title}"` : ""})`),
13820
+ loading: true
13821
+ };
13822
+ }
13823
+ function buildLoadingImageNodeFromRaw(raw$1) {
13824
+ const normalizedRaw = raw$1.startsWith("![") ? raw$1 : `![${raw$1}`;
13825
+ const innerRaw = normalizedRaw.slice(2);
13826
+ const closeIdx = innerRaw.indexOf("](");
13827
+ return {
13828
+ type: "image",
13829
+ src: "",
13830
+ alt: closeIdx === -1 ? innerRaw.replace(/\]$/, "") : innerRaw.slice(0, closeIdx),
13831
+ title: null,
13832
+ raw: normalizedRaw,
13833
+ loading: true
13834
+ };
13835
+ }
13836
+ function recoverOuterImageLinkFromRawText(content) {
13837
+ const outerStart = content.indexOf("[![");
13838
+ if (outerStart === -1) return false;
13839
+ if (typeof raw === "string" && tokens.length === 1 && isEscapedVisibleChar(raw, outerStart, "[")) return false;
13840
+ const before = content.slice(0, outerStart);
13841
+ if (before) pushText(before, before);
13842
+ pushParsed(buildLoadingOuterImageLinkNode(buildLoadingImageNodeFromRaw(content.slice(outerStart + 1))));
13843
+ i++;
13844
+ return true;
13845
+ }
13846
+ function recoverOuterImageLinkStartFromImageToken(token) {
13847
+ if (options?.final) return false;
13848
+ const previousToken = tokens[i - 1];
13849
+ if (previousToken?.type !== "text") return false;
13850
+ if (!String(previousToken.content ?? "").endsWith("[")) return false;
13851
+ if (hasEscapedMarkup(previousToken, "\\[")) return false;
13852
+ const previous = result[result.length - 1];
13853
+ if (previous?.type === "text" && previous.content.endsWith("[")) {
13854
+ const before = previous.content.slice(0, -1);
13855
+ if (before) {
13856
+ previous.content = before;
13857
+ previous.raw = before;
13858
+ currentTextNode = previous;
13859
+ } else {
13860
+ result.pop();
13861
+ currentTextNode = null;
13862
+ }
13863
+ }
13864
+ pushParsed(buildLoadingOuterImageLinkNode(parseImageToken(token)));
13865
+ i++;
13866
+ return true;
13867
+ }
13868
+ function recoverOuterImageLinkFromSyntheticLinkToken(token) {
13869
+ if (token.type !== "link") return false;
13870
+ const linkToken = token;
13871
+ const raw$1 = String(linkToken.raw ?? "");
13872
+ const text$1 = String(linkToken.text ?? "");
13873
+ if (!raw$1.startsWith("[![") && !text$1.startsWith("![")) return false;
13874
+ const imageTitle = linkToken.title == null || linkToken.title === "" ? null : String(linkToken.title);
13875
+ pushParsed(buildLoadingOuterImageLinkNode({
13876
+ type: "image",
13877
+ src: String(linkToken.href ?? ""),
13878
+ alt: text$1.replace(/^!\[/, "").replace(/\]$/, ""),
13879
+ title: imageTitle,
13880
+ raw: raw$1.startsWith("[![") ? raw$1.slice(1) : raw$1,
13881
+ loading: true
13882
+ }));
13883
+ return true;
13884
+ }
13885
+ function recoverOuterImageLinkMidStateFromText(content) {
13886
+ if (!content.startsWith("](")) return false;
13887
+ const outerOpenToken = tokens[i - 2];
13888
+ if (outerOpenToken?.type === "text" && String(outerOpenToken.content ?? "").endsWith("[") && hasEscapedMarkup(outerOpenToken, "\\[")) return false;
13889
+ const previous = result[result.length - 1];
13890
+ if (previous?.type !== "image" && previous?.type !== "link") return false;
13891
+ const previousLink = previous?.type === "link" && Array.isArray(previous.children) && previous.children.length === 1 && previous.children[0]?.type === "image" ? result.pop() : null;
13892
+ const imageNode = previousLink ? previousLink.children[0] : result.pop();
13893
+ if (!imageNode || imageNode.type !== "image") return false;
13894
+ const nextToken = tokens[i + 1];
13895
+ let href = String(previousLink?.href ?? "");
13896
+ let title = previousLink?.title == null ? null : String(previousLink.title);
13897
+ let loading = true;
13898
+ if (nextToken?.type === "link_open") {
13899
+ const { node, nextIndex } = parseLinkToken(tokens, i + 1, options);
13900
+ href = node.href;
13901
+ title = node.title;
13902
+ loading = true;
13903
+ i = nextIndex;
13904
+ } else {
13905
+ href = content.slice(2);
13906
+ if (href.includes("\"")) {
13907
+ const parts = href.split("\"");
13908
+ href = String(parts[0] ?? "").trim();
13909
+ title = parts[1] == null ? null : String(parts[1]).trim();
13910
+ }
13911
+ i++;
13912
+ }
13913
+ const linkNode = buildLoadingOuterImageLinkNode(imageNode, href, title);
13914
+ linkNode.loading = loading;
13915
+ pushParsed(linkNode);
13916
+ return true;
13917
+ }
13918
+ function shouldTreatLinkOpenAsTextInEscapedOuterImageTail() {
13919
+ const outerOpenToken = tokens[i - 3];
13920
+ return tokens[i - 2]?.type === "image" && tokens[i - 1]?.type === "text" && String(tokens[i - 1].content ?? "") === "](" && outerOpenToken?.type === "text" && String(outerOpenToken.content ?? "").endsWith("[") && hasEscapedMarkup(outerOpenToken, "\\[");
13921
+ }
13665
13922
  function handleInlineLinkContent(content, _token) {
13666
13923
  const linkStart = content.indexOf("[");
13667
13924
  if (linkStart === -1) return false;
@@ -13808,19 +14065,21 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13808
14065
  }
13809
14066
  return false;
13810
14067
  }
13811
- function handleInlineImageContent(content, token) {
14068
+ function handleInlineImageContent(content) {
13812
14069
  const imageStart = content.indexOf("![");
13813
14070
  if (imageStart === -1) return false;
13814
14071
  const textNodeContent = content.slice(0, imageStart);
13815
- if (!currentTextNode) currentTextNode = {
14072
+ if (textNodeContent && !currentTextNode) currentTextNode = {
13816
14073
  type: "text",
13817
14074
  content: textNodeContent,
13818
14075
  raw: textNodeContent
13819
14076
  };
13820
- else currentTextNode.content += textNodeContent;
13821
- result.push(currentTextNode);
13822
- currentTextNode = null;
13823
- pushParsed(parseImageToken(token, true));
14077
+ else if (textNodeContent && currentTextNode) currentTextNode.content += textNodeContent;
14078
+ if (currentTextNode) {
14079
+ result.push(currentTextNode);
14080
+ currentTextNode = null;
14081
+ }
14082
+ pushParsed(buildLoadingImageNodeFromRaw(content.slice(imageStart)));
13824
14083
  i++;
13825
14084
  return true;
13826
14085
  }
@@ -14264,27 +14523,6 @@ function parseHeading(tokens, index, options) {
14264
14523
 
14265
14524
  //#endregion
14266
14525
  //#region src/parser/node-parsers/html-block-parser.ts
14267
- function findTagCloseIndexOutsideQuotes$2(input) {
14268
- let inSingle = false;
14269
- let inDouble = false;
14270
- for (let i = 0; i < input.length; i++) {
14271
- const ch = input[i];
14272
- if (ch === "\\") {
14273
- i++;
14274
- continue;
14275
- }
14276
- if (!inDouble && ch === "'") {
14277
- inSingle = !inSingle;
14278
- continue;
14279
- }
14280
- if (!inSingle && ch === "\"") {
14281
- inDouble = !inDouble;
14282
- continue;
14283
- }
14284
- if (!inSingle && !inDouble && ch === ">") return i;
14285
- }
14286
- return -1;
14287
- }
14288
14526
  function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14289
14527
  const lowerTag = tag.toLowerCase();
14290
14528
  const openTagRe = new RegExp(String.raw`^<\s*${lowerTag}(?=\s|>|/)`, "i");
@@ -14296,7 +14534,7 @@ function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14296
14534
  if (lt === -1) return -1;
14297
14535
  const slice = rawHtml.slice(lt);
14298
14536
  if (closeTagRe.test(slice)) {
14299
- const endRel = findTagCloseIndexOutsideQuotes$2(slice);
14537
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14300
14538
  if (endRel === -1) return -1;
14301
14539
  if (depth === 0) return lt + endRel + 1;
14302
14540
  depth--;
@@ -14304,7 +14542,7 @@ function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14304
14542
  continue;
14305
14543
  }
14306
14544
  if (openTagRe.test(slice)) {
14307
- const endRel = findTagCloseIndexOutsideQuotes$2(slice);
14545
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14308
14546
  if (endRel === -1) return -1;
14309
14547
  const rawTag = slice.slice(0, endRel + 1);
14310
14548
  if (!/\/\s*>$/.test(rawTag)) depth++;
@@ -14315,18 +14553,6 @@ function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14315
14553
  }
14316
14554
  return -1;
14317
14555
  }
14318
- function parseTagAttrs(openTag) {
14319
- const attrs = [];
14320
- const attrRegex = /\s([\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
14321
- let match;
14322
- while ((match = attrRegex.exec(openTag)) !== null) {
14323
- const attrName = match[1];
14324
- if (!attrName) continue;
14325
- const attrValue = match[2] || match[3] || match[4] || "";
14326
- attrs.push([attrName, attrValue]);
14327
- }
14328
- return attrs;
14329
- }
14330
14556
  function parseHtmlBlock(token) {
14331
14557
  const raw = String(token.content ?? "");
14332
14558
  if (/^\s*<!--/.test(raw) || /^\s*<!/.test(raw) || /^\s*<\?/.test(raw)) return {
@@ -14344,7 +14570,7 @@ function parseHtmlBlock(token) {
14344
14570
  tag: "",
14345
14571
  loading: false
14346
14572
  };
14347
- const openEnd = findTagCloseIndexOutsideQuotes$2(raw);
14573
+ const openEnd = findTagCloseIndexOutsideQuotes(raw);
14348
14574
  const openTag = openEnd === -1 ? raw : raw.slice(0, openEnd + 1);
14349
14575
  const selfClosing = openEnd !== -1 && /\/\s*>$/.test(openTag);
14350
14576
  const isVoid = VOID_HTML_TAGS.has(tag);
@@ -14463,17 +14689,11 @@ function getEmptyHtmlTagSets() {
14463
14689
  };
14464
14690
  return emptyHtmlTagSets;
14465
14691
  }
14466
- function normalizeCustomTag(t) {
14467
- const raw = String(t ?? "").trim();
14468
- if (!raw) return "";
14469
- const m = raw.match(/^[<\s/]*([A-Z][\w-]*)/i);
14470
- return m ? m[1].toLowerCase() : "";
14471
- }
14472
14692
  function getHtmlTagSets(customTags) {
14473
14693
  if (!customTags || customTags.length === 0) return getEmptyHtmlTagSets();
14474
14694
  const cached = HTML_TAG_SET_CACHE.get(customTags);
14475
14695
  if (cached) return cached;
14476
- const normalized = customTags.map(normalizeCustomTag).filter(Boolean);
14696
+ const normalized = customTags.map(normalizeCustomHtmlTagName).filter(Boolean);
14477
14697
  if (!normalized.length) {
14478
14698
  const entry$1 = getEmptyHtmlTagSets();
14479
14699
  HTML_TAG_SET_CACHE.set(customTags, entry$1);
@@ -14551,27 +14771,6 @@ function parseVmrContainer(tokens, index, options) {
14551
14771
  raw
14552
14772
  }, hasCloseToken ? j + 1 : j];
14553
14773
  }
14554
- function findTagCloseIndexOutsideQuotes$1(input) {
14555
- let inSingle = false;
14556
- let inDouble = false;
14557
- for (let i = 0; i < input.length; i++) {
14558
- const ch = input[i];
14559
- if (ch === "\\") {
14560
- i++;
14561
- continue;
14562
- }
14563
- if (!inDouble && ch === "'") {
14564
- inSingle = !inSingle;
14565
- continue;
14566
- }
14567
- if (!inSingle && ch === "\"") {
14568
- inDouble = !inDouble;
14569
- continue;
14570
- }
14571
- if (!inSingle && !inDouble && ch === ">") return i;
14572
- }
14573
- return -1;
14574
- }
14575
14774
  function stripWrapperNewlines(s) {
14576
14775
  return s.replace(/^\r?\n/, "").replace(/\r?\n$/, "");
14577
14776
  }
@@ -14580,14 +14779,11 @@ function stripTrailingPartialClosingTag(inner, tag) {
14580
14779
  const re = new RegExp(String.raw`[\t ]*<\s*\/\s*${tag}[^>]*$`, "i");
14581
14780
  return inner.replace(re, "");
14582
14781
  }
14583
- function escapeRegex$1(value) {
14584
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14585
- }
14586
14782
  function findMatchingCloseTagRange(rawHtml, tag, startIndex) {
14587
14783
  if (!rawHtml || !tag) return null;
14588
14784
  const lowerTag = tag.toLowerCase();
14589
- const openTagRe = new RegExp(String.raw`^<\s*${escapeRegex$1(lowerTag)}(?=\s|>|/)`, "i");
14590
- const closeTagRe = new RegExp(String.raw`^<\s*\/\s*${escapeRegex$1(lowerTag)}(?=\s|>)`, "i");
14785
+ const openTagRe = new RegExp(String.raw`^<\s*${escapeTagForRegExp(lowerTag)}(?=\s|>|/)`, "i");
14786
+ const closeTagRe = new RegExp(String.raw`^<\s*\/\s*${escapeTagForRegExp(lowerTag)}(?=\s|>)`, "i");
14591
14787
  let depth = 0;
14592
14788
  let index = Math.max(0, startIndex);
14593
14789
  while (index < rawHtml.length) {
@@ -14595,7 +14791,7 @@ function findMatchingCloseTagRange(rawHtml, tag, startIndex) {
14595
14791
  if (lt === -1) break;
14596
14792
  const slice = rawHtml.slice(lt);
14597
14793
  if (closeTagRe.test(slice)) {
14598
- const endRel = findTagCloseIndexOutsideQuotes$1(slice);
14794
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14599
14795
  if (endRel === -1) return null;
14600
14796
  if (depth === 0) return {
14601
14797
  start: lt,
@@ -14606,7 +14802,7 @@ function findMatchingCloseTagRange(rawHtml, tag, startIndex) {
14606
14802
  continue;
14607
14803
  }
14608
14804
  if (openTagRe.test(slice)) {
14609
- const endRel = findTagCloseIndexOutsideQuotes$1(slice);
14805
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14610
14806
  if (endRel === -1) return null;
14611
14807
  const raw = slice.slice(0, endRel + 1);
14612
14808
  if (!/\/\s*>$/.test(raw)) depth++;
@@ -14626,7 +14822,7 @@ function findNextCustomHtmlBlockFromSource(source, tag, startIndex) {
14626
14822
  if (!openMatch || openMatch.index == null) return null;
14627
14823
  const openStart = openMatch.index;
14628
14824
  const openSlice = source.slice(openStart);
14629
- const openEndRel = findTagCloseIndexOutsideQuotes$1(openSlice);
14825
+ const openEndRel = findTagCloseIndexOutsideQuotes(openSlice);
14630
14826
  if (openEndRel === -1) return null;
14631
14827
  const openEnd = openStart + openEndRel;
14632
14828
  if (/\/\s*>\s*$/.test(openSlice.slice(0, openEndRel + 1))) {
@@ -14667,7 +14863,7 @@ function findNextCustomHtmlBlockFromSource(source, tag, startIndex) {
14667
14863
  continue;
14668
14864
  }
14669
14865
  if (isOpenAt(lt)) {
14670
- const rel = findTagCloseIndexOutsideQuotes$1(source.slice(lt));
14866
+ const rel = findTagCloseIndexOutsideQuotes(source.slice(lt));
14671
14867
  if (rel === -1) return null;
14672
14868
  depth++;
14673
14869
  i = lt + rel + 1;
@@ -14723,7 +14919,7 @@ function parseBasicBlockToken(tokens, index, options) {
14723
14919
  const fromSource = findNextCustomHtmlBlockFromSource(source, tag, Math.max(clampNonNegative(cursor), clampNonNegative(mappedLineStart)));
14724
14920
  if (fromSource) options.__customHtmlBlockCursor = fromSource.end;
14725
14921
  const rawHtml = String(fromSource?.raw ?? htmlBlockNode.raw ?? "");
14726
- const openEnd = findTagCloseIndexOutsideQuotes$1(rawHtml);
14922
+ const openEnd = findTagCloseIndexOutsideQuotes(rawHtml);
14727
14923
  const openTag = openEnd !== -1 ? rawHtml.slice(0, openEnd + 1) : rawHtml;
14728
14924
  const selfClosing = openEnd !== -1 && /\/\s*>\s*$/.test(openTag);
14729
14925
  const closeRange = openEnd === -1 ? null : findMatchingCloseTagRange(rawHtml, tag, openEnd + 1);
@@ -14818,26 +15014,17 @@ function parseParagraph(tokens, index, options) {
14818
15014
 
14819
15015
  //#endregion
14820
15016
  //#region src/parser/index.ts
14821
- function normalizeTagName(t) {
14822
- const raw = String(t ?? "").trim();
14823
- if (!raw) return "";
14824
- const m = raw.match(/^[<\s/]*([A-Z][\w-]*)/i);
14825
- return m ? m[1].toLowerCase() : "";
14826
- }
14827
15017
  function getCustomHtmlTagSet(options) {
14828
15018
  const custom = options?.customHtmlTags;
14829
15019
  if (!Array.isArray(custom) || custom.length === 0) return null;
14830
- const normalized = custom.map(normalizeTagName).filter(Boolean);
15020
+ const normalized = normalizeCustomHtmlTags(custom);
14831
15021
  return normalized.length ? new Set(normalized) : null;
14832
15022
  }
14833
15023
  function buildAllowedHtmlTagSet(options) {
14834
15024
  const custom = options?.customHtmlTags;
14835
15025
  if (!Array.isArray(custom) || custom.length === 0) return STANDARD_HTML_TAGS;
14836
15026
  const set = new Set(STANDARD_HTML_TAGS);
14837
- for (const t of custom) {
14838
- const name = normalizeTagName(t);
14839
- if (name) set.add(name);
14840
- }
15027
+ for (const name of normalizeCustomHtmlTags(custom)) if (name) set.add(name);
14841
15028
  return set;
14842
15029
  }
14843
15030
  function stringifyInlineNodeRaw(node) {
@@ -14896,30 +15083,6 @@ function parseStandaloneHtmlDocument(markdown) {
14896
15083
  loading: false
14897
15084
  }];
14898
15085
  }
14899
- function escapeRegex(value) {
14900
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14901
- }
14902
- function findTagCloseIndexOutsideQuotes(input) {
14903
- let inSingle = false;
14904
- let inDouble = false;
14905
- for (let i = 0; i < input.length; i++) {
14906
- const ch = input[i];
14907
- if (ch === "\\") {
14908
- i++;
14909
- continue;
14910
- }
14911
- if (!inDouble && ch === "'") {
14912
- inSingle = !inSingle;
14913
- continue;
14914
- }
14915
- if (!inSingle && ch === "\"") {
14916
- inDouble = !inDouble;
14917
- continue;
14918
- }
14919
- if (!inSingle && !inDouble && ch === ">") return i;
14920
- }
14921
- return -1;
14922
- }
14923
15086
  function getMergeableNodeRaw(node) {
14924
15087
  const raw = node?.raw;
14925
15088
  if (typeof raw === "string") return raw;
@@ -14927,41 +15090,15 @@ function getMergeableNodeRaw(node) {
14927
15090
  if (typeof content === "string") return content;
14928
15091
  return "";
14929
15092
  }
14930
- const SOURCE_EXACT_HTML_BLOCK_TAGS = new Set([
14931
- "article",
14932
- "aside",
14933
- "blockquote",
14934
- "div",
14935
- "figcaption",
14936
- "figure",
14937
- "footer",
14938
- "h1",
14939
- "h2",
14940
- "h3",
14941
- "h4",
14942
- "h5",
14943
- "h6",
14944
- "header",
14945
- "li",
14946
- "main",
14947
- "nav",
14948
- "ol",
14949
- "p",
14950
- "pre",
14951
- "section",
14952
- "summary",
14953
- "table",
14954
- "tbody",
14955
- "td",
14956
- "th",
14957
- "thead",
14958
- "tr",
14959
- "ul"
14960
- ]);
15093
+ function isCloseOnlyHtmlBlockForTag(node, tag) {
15094
+ if (node.type !== "html_block" || !tag) return false;
15095
+ const raw = String(node?.raw ?? node?.content ?? "");
15096
+ return new RegExp(String.raw`^\s*<\s*\/\s*${escapeTagForRegExp(tag)}\s*>\s*$`, "i").test(raw);
15097
+ }
14961
15098
  function findNextHtmlBlockFromSource(source, tag, startIndex) {
14962
15099
  if (!source || !tag) return null;
14963
15100
  const lowerTag = tag.toLowerCase();
14964
- const openRe = new RegExp(String.raw`<\s*${escapeRegex(lowerTag)}(?=\s|>|/)`, "gi");
15101
+ const openRe = new RegExp(String.raw`<\s*${escapeTagForRegExp(lowerTag)}(?=\s|>|/)`, "gi");
14965
15102
  openRe.lastIndex = Math.max(0, startIndex);
14966
15103
  const openMatch = openRe.exec(source);
14967
15104
  if (!openMatch || openMatch.index == null) return null;
@@ -14980,11 +15117,11 @@ function findNextHtmlBlockFromSource(source, tag, startIndex) {
14980
15117
  let index = openEnd + 1;
14981
15118
  const isOpenAt = (pos) => {
14982
15119
  const slice = source.slice(pos);
14983
- return new RegExp(String.raw`^<\s*${escapeRegex(lowerTag)}(?=\s|>|/)`, "i").test(slice);
15120
+ return new RegExp(String.raw`^<\s*${escapeTagForRegExp(lowerTag)}(?=\s|>|/)`, "i").test(slice);
14984
15121
  };
14985
15122
  const isCloseAt = (pos) => {
14986
15123
  const slice = source.slice(pos);
14987
- return new RegExp(String.raw`^<\s*\/\s*${escapeRegex(lowerTag)}(?=\s|>)`, "i").test(slice);
15124
+ return new RegExp(String.raw`^<\s*\/\s*${escapeTagForRegExp(lowerTag)}(?=\s|>)`, "i").test(slice);
14988
15125
  };
14989
15126
  while (index < source.length) {
14990
15127
  const lt = source.indexOf("<", index);
@@ -15070,7 +15207,7 @@ function isDetailsCloseHtmlBlock(node) {
15070
15207
  return /^\s*<\/details\b/i.test(raw);
15071
15208
  }
15072
15209
  function findLastClosingTagStart(raw, tag) {
15073
- const closeRe = new RegExp(String.raw`<\s*\/\s*${escapeRegex(tag)}(?=\s|>)`, "gi");
15210
+ const closeRe = new RegExp(String.raw`<\s*\/\s*${escapeTagForRegExp(tag)}(?=\s|>)`, "gi");
15074
15211
  let last = -1;
15075
15212
  let match;
15076
15213
  while ((match = closeRe.exec(raw)) !== null) last = match.index;
@@ -15196,14 +15333,22 @@ function mergeSplitTopLevelHtmlBlocks(nodes, final, source) {
15196
15333
  let sourceHtmlCursor = 0;
15197
15334
  for (let i = 0; i < merged.length; i++) {
15198
15335
  const node = merged[i];
15199
- if (node?.type !== "html_block") continue;
15336
+ const nodeRaw = getMergeableNodeRaw(node);
15337
+ const nodePos = nodeRaw ? source.indexOf(nodeRaw, sourceHtmlCursor) : -1;
15338
+ if (node?.type !== "html_block") {
15339
+ if (nodePos !== -1) sourceHtmlCursor = nodePos + nodeRaw.length;
15340
+ continue;
15341
+ }
15200
15342
  const tag = String(node?.tag ?? "").toLowerCase();
15201
15343
  if (!tag) continue;
15202
- if (!SOURCE_EXACT_HTML_BLOCK_TAGS.has(tag)) continue;
15203
- const exact = findNextHtmlBlockFromSource(source, tag, sourceHtmlCursor);
15344
+ if (tag === "details") {
15345
+ if (nodePos !== -1) sourceHtmlCursor = nodePos + nodeRaw.length;
15346
+ continue;
15347
+ }
15348
+ const exact = findNextHtmlBlockFromSource(source, tag, nodePos !== -1 ? nodePos : sourceHtmlCursor);
15204
15349
  if (!exact) continue;
15205
15350
  sourceHtmlCursor = exact.end;
15206
- const currentContent = String(node?.content ?? getMergeableNodeRaw(node));
15351
+ const currentContent = String(node?.content ?? nodeRaw);
15207
15352
  const currentRaw = String(node?.raw ?? currentContent);
15208
15353
  const nextContent = buildHtmlBlockContent(exact.raw, tag, exact.closed);
15209
15354
  const desiredLoading = !final && !exact.closed;
@@ -15212,10 +15357,14 @@ function mergeSplitTopLevelHtmlBlocks(nodes, final, source) {
15212
15357
  node.raw = exact.raw;
15213
15358
  node.loading = desiredLoading;
15214
15359
  if (!needsExpansion) continue;
15215
- let tailCursor = findApproximateConsumedPrefixEnd(exact.raw, currentContent);
15360
+ let tailCursor = findApproximateConsumedPrefixEnd(exact.raw, currentRaw);
15216
15361
  if (tailCursor === -1) tailCursor = 0;
15217
15362
  const j = i + 1;
15218
15363
  while (j < merged.length) {
15364
+ if (exact.closed && isCloseOnlyHtmlBlockForTag(merged[j], tag)) {
15365
+ merged.splice(j, 1);
15366
+ continue;
15367
+ }
15219
15368
  const nextRaw = getMergeableNodeRaw(merged[j]);
15220
15369
  if (!nextRaw) break;
15221
15370
  const nextPos = exact.raw.indexOf(nextRaw, tailCursor);
@@ -15394,7 +15543,7 @@ function ensureBlankLineBeforeInlineMultilineCustomHtmlBlocks(markdown, tags) {
15394
15543
  }
15395
15544
  return false;
15396
15545
  };
15397
- const findTagCloseIndexOutsideQuotes$4 = (input) => {
15546
+ const findTagCloseIndexOutsideQuotes$1 = (input) => {
15398
15547
  let inSingle = false;
15399
15548
  let inDouble = false;
15400
15549
  for (let i = 0; i < input.length; i++) {
@@ -15443,7 +15592,7 @@ function ensureBlankLineBeforeInlineMultilineCustomHtmlBlocks(markdown, tags) {
15443
15592
  i++;
15444
15593
  continue;
15445
15594
  }
15446
- const closeIdxRel = findTagCloseIndexOutsideQuotes$4(line.slice(i));
15595
+ const closeIdxRel = findTagCloseIndexOutsideQuotes$1(line.slice(i));
15447
15596
  if (closeIdxRel === -1) {
15448
15597
  hasRenderablePrefix = true;
15449
15598
  i++;
@@ -15547,7 +15696,7 @@ function normalizeCustomHtmlOpeningTagSameLine(markdown, tags) {
15547
15696
  while (i < s.length && isIndentWs(s[i])) i++;
15548
15697
  return s.slice(i);
15549
15698
  };
15550
- const findTagCloseIndexOutsideQuotes$4 = (input) => {
15699
+ const findTagCloseIndexOutsideQuotes$1 = (input) => {
15551
15700
  let inSingle = false;
15552
15701
  let inDouble = false;
15553
15702
  for (let i = 0; i < input.length; i++) {
@@ -15619,7 +15768,7 @@ function normalizeCustomHtmlOpeningTagSameLine(markdown, tags) {
15619
15768
  if (i === nameStart) return line;
15620
15769
  const tagName = line.slice(nameStart, i).toLowerCase();
15621
15770
  if (!tagSet.has(tagName)) return line;
15622
- const gtRel = findTagCloseIndexOutsideQuotes$4(line.slice(i));
15771
+ const gtRel = findTagCloseIndexOutsideQuotes$1(line.slice(i));
15623
15772
  if (gtRel === -1) return line;
15624
15773
  const gt = i + gtRel;
15625
15774
  if (hasClosingTagOnLine(line, gt + 1, tagName)) return line;
@@ -16012,9 +16161,7 @@ function parseMarkdownToStructure(markdown, md, options = {}) {
16012
16161
  else if (/\n[[(]\n*$/.test(safeMarkdown)) safeMarkdown = safeMarkdown.replace(/(\n\[|\n\()+\n*$/g, "\n");
16013
16162
  }
16014
16163
  if (options.customHtmlTags?.length && safeMarkdown.includes("<")) {
16015
- const tags = options.customHtmlTags.map((t) => String(t ?? "").trim()).filter(Boolean).map((t) => {
16016
- return (t.match(/^[<\s/]*([A-Z][\w-]*)/i)?.[1] ?? "").toLowerCase();
16017
- }).filter(Boolean);
16164
+ const tags = normalizeCustomHtmlTags(options.customHtmlTags);
16018
16165
  if (tags.length) {
16019
16166
  safeMarkdown = ensureBlankLineBeforeInlineMultilineCustomHtmlBlocks(safeMarkdown, tags);
16020
16167
  safeMarkdown = normalizeCustomHtmlOpeningTagSameLine(safeMarkdown, tags);
@@ -16172,6 +16319,160 @@ function processTokens(tokens, options) {
16172
16319
  return result;
16173
16320
  }
16174
16321
 
16322
+ //#endregion
16323
+ //#region src/htmlRenderUtils.ts
16324
+ const CUSTOM_TAG_REGEX = /<([a-z][a-z0-9-]*)\b[^>]*>/gi;
16325
+ function hasOwn(obj, key) {
16326
+ return Object.prototype.hasOwnProperty.call(obj, key);
16327
+ }
16328
+ function isCustomHtmlComponentTag(tagName, customComponents) {
16329
+ const lowerTag = tagName.toLowerCase();
16330
+ if (EXTENDED_STANDARD_HTML_TAGS.has(lowerTag)) return false;
16331
+ return hasOwn(customComponents, lowerTag) || hasOwn(customComponents, tagName);
16332
+ }
16333
+ function sanitizeHtmlAttrs(attrs) {
16334
+ const clean = {};
16335
+ for (const [key, value] of Object.entries(attrs)) {
16336
+ const lowerKey = key.toLowerCase();
16337
+ if (DANGEROUS_HTML_ATTRS.has(lowerKey)) continue;
16338
+ if (URL_HTML_ATTRS.has(lowerKey) && value && isUnsafeHtmlUrl(value)) continue;
16339
+ clean[key] = value;
16340
+ }
16341
+ return clean;
16342
+ }
16343
+ function convertHtmlPropValue(value, key) {
16344
+ const lowerKey = key.toLowerCase();
16345
+ if ([
16346
+ "checked",
16347
+ "disabled",
16348
+ "readonly",
16349
+ "required",
16350
+ "autofocus",
16351
+ "multiple",
16352
+ "hidden"
16353
+ ].includes(lowerKey)) return value === "true" || value === "" || value === key;
16354
+ if ([
16355
+ "value",
16356
+ "min",
16357
+ "max",
16358
+ "step",
16359
+ "width",
16360
+ "height",
16361
+ "size",
16362
+ "maxlength"
16363
+ ].includes(lowerKey)) {
16364
+ const num = Number(value);
16365
+ if (value !== "" && !Number.isNaN(num)) return num;
16366
+ }
16367
+ return value;
16368
+ }
16369
+ function convertHtmlAttrsToProps(attrs) {
16370
+ const result = {};
16371
+ for (const [key, value] of Object.entries(attrs)) result[key] = convertHtmlPropValue(value, key);
16372
+ return result;
16373
+ }
16374
+ function isMeaningfulText(text$1) {
16375
+ return text$1.trim().length > 0;
16376
+ }
16377
+ function tokenizeHtml(html) {
16378
+ const tokens = [];
16379
+ let pos = 0;
16380
+ while (pos < html.length) {
16381
+ if (html.startsWith("<!--", pos)) {
16382
+ const commentEnd = html.indexOf("-->", pos);
16383
+ if (commentEnd !== -1) {
16384
+ pos = commentEnd + 3;
16385
+ continue;
16386
+ }
16387
+ break;
16388
+ }
16389
+ const tagStart = html.indexOf("<", pos);
16390
+ if (tagStart === -1) {
16391
+ if (pos < html.length) {
16392
+ const remainingText = html.slice(pos);
16393
+ if (isMeaningfulText(remainingText)) tokens.push({
16394
+ type: "text",
16395
+ content: remainingText
16396
+ });
16397
+ }
16398
+ break;
16399
+ }
16400
+ if (tagStart > pos) {
16401
+ const textContent = html.slice(pos, tagStart);
16402
+ if (isMeaningfulText(textContent)) tokens.push({
16403
+ type: "text",
16404
+ content: textContent
16405
+ });
16406
+ }
16407
+ if (html.startsWith("![CDATA[", tagStart + 1)) {
16408
+ const cdataEnd = html.indexOf("]]>", tagStart);
16409
+ if (cdataEnd !== -1) {
16410
+ tokens.push({
16411
+ type: "text",
16412
+ content: html.slice(tagStart, cdataEnd + 3)
16413
+ });
16414
+ pos = cdataEnd + 3;
16415
+ continue;
16416
+ }
16417
+ break;
16418
+ }
16419
+ if (html.startsWith("!", tagStart + 1)) {
16420
+ const specialEnd = html.indexOf(">", tagStart);
16421
+ if (specialEnd !== -1) {
16422
+ pos = specialEnd + 1;
16423
+ continue;
16424
+ }
16425
+ break;
16426
+ }
16427
+ const tagEnd = html.indexOf(">", tagStart);
16428
+ if (tagEnd === -1) break;
16429
+ const tagContent = html.slice(tagStart + 1, tagEnd).trim();
16430
+ const isClosingTag$1 = tagContent.startsWith("/");
16431
+ const isSelfClosing$1 = tagContent.endsWith("/");
16432
+ if (isClosingTag$1) {
16433
+ const tagName = tagContent.slice(1).trim();
16434
+ tokens.push({
16435
+ type: "tag_close",
16436
+ tagName
16437
+ });
16438
+ } else {
16439
+ const spaceIndex = tagContent.indexOf(" ");
16440
+ let tagName;
16441
+ let attrsStr = "";
16442
+ if (spaceIndex === -1) tagName = isSelfClosing$1 ? tagContent.slice(0, -1).trim() : tagContent.trim();
16443
+ else {
16444
+ tagName = tagContent.slice(0, spaceIndex).trim();
16445
+ attrsStr = tagContent.slice(spaceIndex + 1);
16446
+ }
16447
+ const attrs = {};
16448
+ if (attrsStr) {
16449
+ const attrRegex = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|(\S*)))?/g;
16450
+ let attrMatch;
16451
+ while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
16452
+ const name = attrMatch[1];
16453
+ const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
16454
+ if (name && !name.endsWith("/")) attrs[name] = value;
16455
+ }
16456
+ }
16457
+ tokens.push({
16458
+ type: isSelfClosing$1 || VOID_HTML_TAGS.has(tagName.toLowerCase()) ? "self_closing" : "tag_open",
16459
+ tagName,
16460
+ attrs
16461
+ });
16462
+ }
16463
+ pos = tagEnd + 1;
16464
+ }
16465
+ return tokens;
16466
+ }
16467
+ function hasCustomHtmlComponents(content, customComponents) {
16468
+ if (!content || !content.includes("<")) return false;
16469
+ if (!customComponents || Object.keys(customComponents).length === 0) return false;
16470
+ CUSTOM_TAG_REGEX.lastIndex = 0;
16471
+ let match;
16472
+ while ((match = CUSTOM_TAG_REGEX.exec(content)) !== null) if (isCustomHtmlComponentTag(match[1], customComponents)) return true;
16473
+ return false;
16474
+ }
16475
+
16175
16476
  //#endregion
16176
16477
  //#region src/index.ts
16177
16478
  const _registeredMarkdownPlugins = [];
@@ -16312,5 +16613,5 @@ function getMarkdown(msgId = `editor-${Date.now()}`, options = {}) {
16312
16613
  }
16313
16614
 
16314
16615
  //#endregion
16315
- export { BLOCKED_HTML_TAGS, BLOCKED_HTML_TAG_NAMES, BLOCK_HTML_TAG_NAMES, DANGEROUS_HTML_ATTRS, DANGEROUS_HTML_ATTR_NAMES, ESCAPED_TEX_BRACE_COMMANDS, EXTENDED_STANDARD_HTML_TAGS, EXTENDED_STANDARD_HTML_TAG_NAMES, INLINE_HTML_TAG_NAMES, KATEX_COMMANDS, STANDARD_BLOCK_HTML_TAGS, STANDARD_HTML_TAGS, SVG_HTML_TAG_NAMES, TEX_BRACE_COMMANDS, URL_HTML_ATTRS, URL_HTML_ATTR_NAMES, VOID_HTML_TAGS, VOID_HTML_TAG_NAMES, applyContainers, applyMath, clearRegisteredMarkdownPlugins, findMatchingClose, getMarkdown, isMathLike, isUnsafeHtmlUrl, normalizeStandaloneBackslashT, parseFenceToken, parseInlineTokens, parseMarkdownToStructure, processTokens, registerMarkdownPlugin, setDefaultMathOptions, stripHtmlControlAndWhitespace };
16616
+ export { BLOCKED_HTML_TAGS, BLOCKED_HTML_TAG_NAMES, BLOCK_HTML_TAG_NAMES, DANGEROUS_HTML_ATTRS, DANGEROUS_HTML_ATTR_NAMES, ESCAPED_TEX_BRACE_COMMANDS, EXTENDED_STANDARD_HTML_TAGS, EXTENDED_STANDARD_HTML_TAG_NAMES, INLINE_HTML_TAG_NAMES, KATEX_COMMANDS, STANDARD_BLOCK_HTML_TAGS, STANDARD_HTML_TAGS, SVG_HTML_TAG_NAMES, TEX_BRACE_COMMANDS, URL_HTML_ATTRS, URL_HTML_ATTR_NAMES, VOID_HTML_TAGS, VOID_HTML_TAG_NAMES, applyContainers, applyMath, clearRegisteredMarkdownPlugins, convertHtmlAttrsToProps, convertHtmlPropValue, findMatchingClose, getHtmlTagFromContent, getMarkdown, hasCompleteHtmlTagContent, hasCustomHtmlComponents, isCustomHtmlComponentTag, isHtmlLikeTagName, isMathLike, isUnsafeHtmlUrl, mergeCustomHtmlTags, normalizeCustomHtmlTagName, normalizeCustomHtmlTags, normalizeStandaloneBackslashT, parseFenceToken, parseInlineTokens, parseMarkdownToStructure, processTokens, registerMarkdownPlugin, resolveCustomHtmlTags, sanitizeHtmlAttrs, setDefaultMathOptions, shouldRenderUnknownHtmlTagAsText, stripCustomHtmlWrapper, stripHtmlControlAndWhitespace, tokenizeHtml };
16316
16617
  //# sourceMappingURL=index.js.map