stream-markdown-parser 0.0.80 → 0.0.82

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;
@@ -12222,19 +12317,33 @@ const DIFF_HEADER_PREFIXES = [
12222
12317
  "@@ "
12223
12318
  ];
12224
12319
  const NEWLINE_RE = /\r?\n/;
12225
- function splitUnifiedDiff(content) {
12320
+ function flushPendingDiffHunk(orig, updated, pendingOrig, pendingUpdated) {
12321
+ if (pendingOrig.length > 0) orig.push(...pendingOrig);
12322
+ if (pendingUpdated.length > 0) updated.push(...pendingUpdated);
12323
+ pendingOrig.length = 0;
12324
+ pendingUpdated.length = 0;
12325
+ }
12326
+ function splitUnifiedDiff(content, closed) {
12226
12327
  const orig = [];
12227
12328
  const updated = [];
12228
- for (const rawLine of content.split(NEWLINE_RE)) {
12329
+ const pendingOrig = [];
12330
+ const pendingUpdated = [];
12331
+ const lines = content.split(NEWLINE_RE);
12332
+ const stableLineCount = Math.max(0, lines.length - 1);
12333
+ const processLine = (rawLine) => {
12229
12334
  const line = rawLine;
12230
- if (DIFF_HEADER_PREFIXES.some((p) => line.startsWith(p))) continue;
12231
- if (line.length >= 2 && line[0] === "-" && line[1] === " ") orig.push(` ${line.slice(1)}`);
12232
- else if (line.length >= 2 && line[0] === "+" && line[1] === " ") updated.push(` ${line.slice(1)}`);
12335
+ if (DIFF_HEADER_PREFIXES.some((p) => line.startsWith(p))) return;
12336
+ if (line.length >= 2 && line[0] === "-" && line[1] === " ") pendingOrig.push(` ${line.slice(1)}`);
12337
+ else if (line.length >= 2 && line[0] === "+" && line[1] === " ") pendingUpdated.push(` ${line.slice(1)}`);
12233
12338
  else {
12339
+ flushPendingDiffHunk(orig, updated, pendingOrig, pendingUpdated);
12234
12340
  orig.push(line);
12235
12341
  updated.push(line);
12236
12342
  }
12237
- }
12343
+ };
12344
+ for (let index = 0; index < stableLineCount; index++) processLine(lines[index] ?? "");
12345
+ if (closed && stableLineCount < lines.length) processLine(lines[lines.length - 1] ?? "");
12346
+ if (closed || pendingOrig.length > 0 && pendingUpdated.length > 0) flushPendingDiffHunk(orig, updated, pendingOrig, pendingUpdated);
12238
12347
  return {
12239
12348
  original: orig.join("\n"),
12240
12349
  updated: updated.join("\n")
@@ -12254,7 +12363,7 @@ function parseFenceToken(token) {
12254
12363
  let content = String(token.content ?? "");
12255
12364
  if (TRAILING_FENCE_LINE_RE.test(content)) content = content.replace(TRAILING_FENCE_LINE_RE, "");
12256
12365
  if (diff) {
12257
- const { original, updated } = splitUnifiedDiff(content);
12366
+ const { original, updated } = splitUnifiedDiff(content, closed === true);
12258
12367
  return {
12259
12368
  type: "code_block",
12260
12369
  language,
@@ -12340,17 +12449,11 @@ function isClosingTag(html) {
12340
12449
  function isSelfClosing(tag, html) {
12341
12450
  return /\/\s*>\s*$/.test(html) || VOID_HTML_TAGS.has(tag);
12342
12451
  }
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
12452
  function getTagSets(customTags) {
12350
12453
  if (!customTags || customTags.length === 0) return getEmptyTagSets();
12351
12454
  const cached = TAG_SET_CACHE.get(customTags);
12352
12455
  if (cached) return cached;
12353
- const normalized = customTags.map(normalizeCustomTag$1).filter(Boolean);
12456
+ const normalized = customTags.map(normalizeCustomHtmlTagName).filter(Boolean);
12354
12457
  if (!normalized.length) {
12355
12458
  const entry$1 = getEmptyTagSets();
12356
12459
  TAG_SET_CACHE.set(customTags, entry$1);
@@ -12368,18 +12471,6 @@ function tokenToRaw(token) {
12368
12471
  const raw = shape.raw ?? shape.content ?? shape.markup ?? "";
12369
12472
  return String(raw ?? "");
12370
12473
  }
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
12474
  function getAttrValue$1(attrs, name) {
12384
12475
  const lowerName = name.toLowerCase();
12385
12476
  for (let i = attrs.length - 1; i >= 0; i--) {
@@ -12474,7 +12565,7 @@ function parseHtmlInlineCodeToken(token, tokens, i, parseInlineTokens$1, raw, pP
12474
12565
  }, i + 1];
12475
12566
  if (tag === "a") {
12476
12567
  const fragment$1 = collectHtmlFragment(tokens, i, tag);
12477
- const attrs$1 = parseTagAttrs$1(code$1);
12568
+ const attrs$1 = parseTagAttrs(code$1);
12478
12569
  const innerTokens = fragment$1.innerTokens;
12479
12570
  const href = String(getAttrValue$1(attrs$1, "href") ?? "");
12480
12571
  const titleAttr = getAttrValue$1(attrs$1, "title");
@@ -12914,6 +13005,30 @@ function decodeVisibleTextFromRaw(rawText) {
12914
13005
  }
12915
13006
  return output;
12916
13007
  }
13008
+ function getRawIndexForVisibleIndex(rawText, visibleIndex) {
13009
+ let outputIndex = 0;
13010
+ for (let rawIndex = 0; rawIndex < rawText.length; rawIndex++) {
13011
+ const char = rawText[rawIndex];
13012
+ const nextChar = rawText[rawIndex + 1];
13013
+ if (char === "\\" && nextChar && ESCAPABLE_PUNCTUATION.has(nextChar)) {
13014
+ if (outputIndex === visibleIndex) return rawIndex + 1;
13015
+ outputIndex++;
13016
+ rawIndex++;
13017
+ continue;
13018
+ }
13019
+ if (outputIndex === visibleIndex) return rawIndex;
13020
+ outputIndex++;
13021
+ }
13022
+ return -1;
13023
+ }
13024
+ function isEscapedVisibleChar(rawText, visibleIndex, expectedChar) {
13025
+ const rawIndex = getRawIndexForVisibleIndex(rawText, visibleIndex);
13026
+ if (rawIndex === -1) return false;
13027
+ if (expectedChar && rawText[rawIndex] !== expectedChar) return false;
13028
+ let slashCount = 0;
13029
+ for (let i = rawIndex - 1; i >= 0 && rawText[i] === "\\"; i--) slashCount++;
13030
+ return slashCount % 2 === 1;
13031
+ }
12917
13032
  const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
12918
13033
  const WORD_ONLY_RE = /^[\p{L}\p{N}]+$/u;
12919
13034
  function isWordChar(ch) {
@@ -13359,9 +13474,11 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13359
13474
  handleLinkOpen(token);
13360
13475
  break;
13361
13476
  case "image":
13362
- resetCurrentTextNode();
13363
- pushNode(parseImageToken(token));
13364
- i++;
13477
+ if (!recoverOuterImageLinkStartFromImageToken(token)) {
13478
+ resetCurrentTextNode();
13479
+ pushNode(parseImageToken(token));
13480
+ i++;
13481
+ }
13365
13482
  break;
13366
13483
  case "strong_open": {
13367
13484
  resetCurrentTextNode();
@@ -13506,7 +13623,9 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13506
13623
  const displayText = String(token.text ?? "");
13507
13624
  pushText(displayText, displayText);
13508
13625
  i++;
13509
- } else if (recoverMarkdownLinkFromTrailingText(token)) i++;
13626
+ } else if (recoverOuterImageLinkFromSyntheticLinkToken(token)) i++;
13627
+ else if (recoverMarkdownImageFromTrailingBang(token)) i++;
13628
+ else if (recoverMarkdownLinkFromTrailingText(token)) i++;
13510
13629
  else {
13511
13630
  pushToken(token);
13512
13631
  i++;
@@ -13567,6 +13686,8 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13567
13686
  i++;
13568
13687
  return;
13569
13688
  }
13689
+ if (recoverOuterImageLinkFromRawText(content)) return;
13690
+ if (recoverOuterImageLinkMidStateFromText(content)) return;
13570
13691
  if (!(content.includes("*") || content.includes("_") || content.includes("~") || content.includes("`") || content.includes("[") || content.includes("!") || content.includes("$") || content.includes("|") || content.includes("("))) {
13571
13692
  commitTextNode(content, token, tokens[i - 1], nextToken);
13572
13693
  i++;
@@ -13574,12 +13695,12 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13574
13695
  }
13575
13696
  if (handleCheckboxLike(content)) return;
13576
13697
  const preToken = tokens[i - 1];
13577
- if (content === "[" && !nextToken?.markup?.includes("*") || content === "]" && !preToken?.markup?.includes("*")) {
13698
+ if (content === "[" && !nextToken?.markup?.includes("*") && !hasEscapedMarkup(token, "\\[") || content === "]" && !preToken?.markup?.includes("*") && !hasEscapedMarkup(token, "\\]")) {
13578
13699
  i++;
13579
13700
  return;
13580
13701
  }
13581
13702
  if (handleInlineCodeContent(rawContent, token)) return;
13582
- if (handleInlineImageContent(content, token)) return;
13703
+ if (handleInlineImageContent(content)) return;
13583
13704
  if (tokens[i + 1]?.type !== "link_open" && handleInlineLinkContent(content, token)) return;
13584
13705
  const reparsedNodes = tryReparseCollapsedInlineText(rawContent);
13585
13706
  if (reparsedNodes) {
@@ -13593,6 +13714,13 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13593
13714
  i++;
13594
13715
  }
13595
13716
  function handleLinkOpen(token) {
13717
+ if (shouldTreatLinkOpenAsTextInEscapedOuterImageTail()) {
13718
+ const { node: node$1, nextIndex: nextIndex$1 } = parseLinkToken(tokens, i, options);
13719
+ const text$1 = String(node$1.text || node$1.href || "");
13720
+ pushText(text$1, text$1);
13721
+ i = nextIndex$1;
13722
+ return;
13723
+ }
13596
13724
  resetCurrentTextNode();
13597
13725
  const { node, nextIndex } = parseLinkToken(tokens, i, options);
13598
13726
  i = nextIndex;
@@ -13662,6 +13790,149 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13662
13790
  });
13663
13791
  return true;
13664
13792
  }
13793
+ function recoverMarkdownImageFromTrailingBang(token) {
13794
+ if (token.type !== "link") return false;
13795
+ const previous = result[result.length - 1];
13796
+ const previousToken = tokens[i - 1];
13797
+ if (!previous || previous.type !== "text" || previousToken?.type !== "text") return false;
13798
+ const previousContent = String(previous.content ?? "");
13799
+ const previousTokenContent = String(previousToken.content ?? "");
13800
+ if (!previousContent.endsWith("!") || !previousTokenContent.endsWith("!")) return false;
13801
+ if (hasEscapedMarkup(previousToken, "\\!")) return false;
13802
+ const before = previousContent.slice(0, -1);
13803
+ if (before) {
13804
+ previous.content = before;
13805
+ previous.raw = before;
13806
+ currentTextNode = previous;
13807
+ } else {
13808
+ result.pop();
13809
+ currentTextNode = null;
13810
+ }
13811
+ const linkToken = token;
13812
+ const alt = String(linkToken.text ?? linkToken.children?.map((child) => String(child?.content ?? child?.raw ?? "")).join("") ?? "");
13813
+ const href = String(linkToken.href ?? "");
13814
+ const title = linkToken.title == null || linkToken.title === "" ? null : String(linkToken.title);
13815
+ pushParsed({
13816
+ type: "image",
13817
+ src: href,
13818
+ alt,
13819
+ title,
13820
+ raw: String(`![${alt}](${href}${title ? ` "${title}"` : ""})`),
13821
+ loading: Boolean(linkToken.loading)
13822
+ });
13823
+ return true;
13824
+ }
13825
+ function buildLoadingOuterImageLinkNode(imageNode, href = "", title = null) {
13826
+ const text$1 = String(imageNode.alt ?? imageNode.raw ?? "");
13827
+ return {
13828
+ type: "link",
13829
+ href,
13830
+ title,
13831
+ text: text$1,
13832
+ children: [imageNode],
13833
+ raw: String(`[${text$1}](${href}${title ? ` "${title}"` : ""})`),
13834
+ loading: true
13835
+ };
13836
+ }
13837
+ function buildLoadingImageNodeFromRaw(raw$1) {
13838
+ const normalizedRaw = raw$1.startsWith("![") ? raw$1 : `![${raw$1}`;
13839
+ const innerRaw = normalizedRaw.slice(2);
13840
+ const closeIdx = innerRaw.indexOf("](");
13841
+ return {
13842
+ type: "image",
13843
+ src: "",
13844
+ alt: closeIdx === -1 ? innerRaw.replace(/\]$/, "") : innerRaw.slice(0, closeIdx),
13845
+ title: null,
13846
+ raw: normalizedRaw,
13847
+ loading: true
13848
+ };
13849
+ }
13850
+ function recoverOuterImageLinkFromRawText(content) {
13851
+ const outerStart = content.indexOf("[![");
13852
+ if (outerStart === -1) return false;
13853
+ if (typeof raw === "string" && tokens.length === 1 && isEscapedVisibleChar(raw, outerStart, "[")) return false;
13854
+ const before = content.slice(0, outerStart);
13855
+ if (before) pushText(before, before);
13856
+ pushParsed(buildLoadingOuterImageLinkNode(buildLoadingImageNodeFromRaw(content.slice(outerStart + 1))));
13857
+ i++;
13858
+ return true;
13859
+ }
13860
+ function recoverOuterImageLinkStartFromImageToken(token) {
13861
+ if (options?.final) return false;
13862
+ const previousToken = tokens[i - 1];
13863
+ if (previousToken?.type !== "text") return false;
13864
+ if (!String(previousToken.content ?? "").endsWith("[")) return false;
13865
+ if (hasEscapedMarkup(previousToken, "\\[")) return false;
13866
+ const previous = result[result.length - 1];
13867
+ if (previous?.type === "text" && previous.content.endsWith("[")) {
13868
+ const before = previous.content.slice(0, -1);
13869
+ if (before) {
13870
+ previous.content = before;
13871
+ previous.raw = before;
13872
+ currentTextNode = previous;
13873
+ } else {
13874
+ result.pop();
13875
+ currentTextNode = null;
13876
+ }
13877
+ }
13878
+ pushParsed(buildLoadingOuterImageLinkNode(parseImageToken(token)));
13879
+ i++;
13880
+ return true;
13881
+ }
13882
+ function recoverOuterImageLinkFromSyntheticLinkToken(token) {
13883
+ if (token.type !== "link") return false;
13884
+ const linkToken = token;
13885
+ const raw$1 = String(linkToken.raw ?? "");
13886
+ const text$1 = String(linkToken.text ?? "");
13887
+ if (!raw$1.startsWith("[![") && !text$1.startsWith("![")) return false;
13888
+ const imageTitle = linkToken.title == null || linkToken.title === "" ? null : String(linkToken.title);
13889
+ pushParsed(buildLoadingOuterImageLinkNode({
13890
+ type: "image",
13891
+ src: String(linkToken.href ?? ""),
13892
+ alt: text$1.replace(/^!\[/, "").replace(/\]$/, ""),
13893
+ title: imageTitle,
13894
+ raw: raw$1.startsWith("[![") ? raw$1.slice(1) : raw$1,
13895
+ loading: true
13896
+ }));
13897
+ return true;
13898
+ }
13899
+ function recoverOuterImageLinkMidStateFromText(content) {
13900
+ if (!content.startsWith("](")) return false;
13901
+ const outerOpenToken = tokens[i - 2];
13902
+ if (outerOpenToken?.type === "text" && String(outerOpenToken.content ?? "").endsWith("[") && hasEscapedMarkup(outerOpenToken, "\\[")) return false;
13903
+ const previous = result[result.length - 1];
13904
+ if (previous?.type !== "image" && previous?.type !== "link") return false;
13905
+ const previousLink = previous?.type === "link" && Array.isArray(previous.children) && previous.children.length === 1 && previous.children[0]?.type === "image" ? result.pop() : null;
13906
+ const imageNode = previousLink ? previousLink.children[0] : result.pop();
13907
+ if (!imageNode || imageNode.type !== "image") return false;
13908
+ const nextToken = tokens[i + 1];
13909
+ let href = String(previousLink?.href ?? "");
13910
+ let title = previousLink?.title == null ? null : String(previousLink.title);
13911
+ let loading = true;
13912
+ if (nextToken?.type === "link_open") {
13913
+ const { node, nextIndex } = parseLinkToken(tokens, i + 1, options);
13914
+ href = node.href;
13915
+ title = node.title;
13916
+ loading = true;
13917
+ i = nextIndex;
13918
+ } else {
13919
+ href = content.slice(2);
13920
+ if (href.includes("\"")) {
13921
+ const parts = href.split("\"");
13922
+ href = String(parts[0] ?? "").trim();
13923
+ title = parts[1] == null ? null : String(parts[1]).trim();
13924
+ }
13925
+ i++;
13926
+ }
13927
+ const linkNode = buildLoadingOuterImageLinkNode(imageNode, href, title);
13928
+ linkNode.loading = loading;
13929
+ pushParsed(linkNode);
13930
+ return true;
13931
+ }
13932
+ function shouldTreatLinkOpenAsTextInEscapedOuterImageTail() {
13933
+ const outerOpenToken = tokens[i - 3];
13934
+ 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, "\\[");
13935
+ }
13665
13936
  function handleInlineLinkContent(content, _token) {
13666
13937
  const linkStart = content.indexOf("[");
13667
13938
  if (linkStart === -1) return false;
@@ -13808,19 +14079,21 @@ function parseInlineTokens(tokens, raw, pPreToken, options) {
13808
14079
  }
13809
14080
  return false;
13810
14081
  }
13811
- function handleInlineImageContent(content, token) {
14082
+ function handleInlineImageContent(content) {
13812
14083
  const imageStart = content.indexOf("![");
13813
14084
  if (imageStart === -1) return false;
13814
14085
  const textNodeContent = content.slice(0, imageStart);
13815
- if (!currentTextNode) currentTextNode = {
14086
+ if (textNodeContent && !currentTextNode) currentTextNode = {
13816
14087
  type: "text",
13817
14088
  content: textNodeContent,
13818
14089
  raw: textNodeContent
13819
14090
  };
13820
- else currentTextNode.content += textNodeContent;
13821
- result.push(currentTextNode);
13822
- currentTextNode = null;
13823
- pushParsed(parseImageToken(token, true));
14091
+ else if (textNodeContent && currentTextNode) currentTextNode.content += textNodeContent;
14092
+ if (currentTextNode) {
14093
+ result.push(currentTextNode);
14094
+ currentTextNode = null;
14095
+ }
14096
+ pushParsed(buildLoadingImageNodeFromRaw(content.slice(imageStart)));
13824
14097
  i++;
13825
14098
  return true;
13826
14099
  }
@@ -14264,27 +14537,6 @@ function parseHeading(tokens, index, options) {
14264
14537
 
14265
14538
  //#endregion
14266
14539
  //#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
14540
  function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14289
14541
  const lowerTag = tag.toLowerCase();
14290
14542
  const openTagRe = new RegExp(String.raw`^<\s*${lowerTag}(?=\s|>|/)`, "i");
@@ -14296,7 +14548,7 @@ function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14296
14548
  if (lt === -1) return -1;
14297
14549
  const slice = rawHtml.slice(lt);
14298
14550
  if (closeTagRe.test(slice)) {
14299
- const endRel = findTagCloseIndexOutsideQuotes$2(slice);
14551
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14300
14552
  if (endRel === -1) return -1;
14301
14553
  if (depth === 0) return lt + endRel + 1;
14302
14554
  depth--;
@@ -14304,7 +14556,7 @@ function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14304
14556
  continue;
14305
14557
  }
14306
14558
  if (openTagRe.test(slice)) {
14307
- const endRel = findTagCloseIndexOutsideQuotes$2(slice);
14559
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14308
14560
  if (endRel === -1) return -1;
14309
14561
  const rawTag = slice.slice(0, endRel + 1);
14310
14562
  if (!/\/\s*>$/.test(rawTag)) depth++;
@@ -14315,18 +14567,6 @@ function findMatchingCloseTagEnd(rawHtml, tag, startIndex) {
14315
14567
  }
14316
14568
  return -1;
14317
14569
  }
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
14570
  function parseHtmlBlock(token) {
14331
14571
  const raw = String(token.content ?? "");
14332
14572
  if (/^\s*<!--/.test(raw) || /^\s*<!/.test(raw) || /^\s*<\?/.test(raw)) return {
@@ -14344,7 +14584,7 @@ function parseHtmlBlock(token) {
14344
14584
  tag: "",
14345
14585
  loading: false
14346
14586
  };
14347
- const openEnd = findTagCloseIndexOutsideQuotes$2(raw);
14587
+ const openEnd = findTagCloseIndexOutsideQuotes(raw);
14348
14588
  const openTag = openEnd === -1 ? raw : raw.slice(0, openEnd + 1);
14349
14589
  const selfClosing = openEnd !== -1 && /\/\s*>$/.test(openTag);
14350
14590
  const isVoid = VOID_HTML_TAGS.has(tag);
@@ -14463,17 +14703,11 @@ function getEmptyHtmlTagSets() {
14463
14703
  };
14464
14704
  return emptyHtmlTagSets;
14465
14705
  }
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
14706
  function getHtmlTagSets(customTags) {
14473
14707
  if (!customTags || customTags.length === 0) return getEmptyHtmlTagSets();
14474
14708
  const cached = HTML_TAG_SET_CACHE.get(customTags);
14475
14709
  if (cached) return cached;
14476
- const normalized = customTags.map(normalizeCustomTag).filter(Boolean);
14710
+ const normalized = customTags.map(normalizeCustomHtmlTagName).filter(Boolean);
14477
14711
  if (!normalized.length) {
14478
14712
  const entry$1 = getEmptyHtmlTagSets();
14479
14713
  HTML_TAG_SET_CACHE.set(customTags, entry$1);
@@ -14551,27 +14785,6 @@ function parseVmrContainer(tokens, index, options) {
14551
14785
  raw
14552
14786
  }, hasCloseToken ? j + 1 : j];
14553
14787
  }
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
14788
  function stripWrapperNewlines(s) {
14576
14789
  return s.replace(/^\r?\n/, "").replace(/\r?\n$/, "");
14577
14790
  }
@@ -14580,14 +14793,11 @@ function stripTrailingPartialClosingTag(inner, tag) {
14580
14793
  const re = new RegExp(String.raw`[\t ]*<\s*\/\s*${tag}[^>]*$`, "i");
14581
14794
  return inner.replace(re, "");
14582
14795
  }
14583
- function escapeRegex$1(value) {
14584
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14585
- }
14586
14796
  function findMatchingCloseTagRange(rawHtml, tag, startIndex) {
14587
14797
  if (!rawHtml || !tag) return null;
14588
14798
  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");
14799
+ const openTagRe = new RegExp(String.raw`^<\s*${escapeTagForRegExp(lowerTag)}(?=\s|>|/)`, "i");
14800
+ const closeTagRe = new RegExp(String.raw`^<\s*\/\s*${escapeTagForRegExp(lowerTag)}(?=\s|>)`, "i");
14591
14801
  let depth = 0;
14592
14802
  let index = Math.max(0, startIndex);
14593
14803
  while (index < rawHtml.length) {
@@ -14595,7 +14805,7 @@ function findMatchingCloseTagRange(rawHtml, tag, startIndex) {
14595
14805
  if (lt === -1) break;
14596
14806
  const slice = rawHtml.slice(lt);
14597
14807
  if (closeTagRe.test(slice)) {
14598
- const endRel = findTagCloseIndexOutsideQuotes$1(slice);
14808
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14599
14809
  if (endRel === -1) return null;
14600
14810
  if (depth === 0) return {
14601
14811
  start: lt,
@@ -14606,7 +14816,7 @@ function findMatchingCloseTagRange(rawHtml, tag, startIndex) {
14606
14816
  continue;
14607
14817
  }
14608
14818
  if (openTagRe.test(slice)) {
14609
- const endRel = findTagCloseIndexOutsideQuotes$1(slice);
14819
+ const endRel = findTagCloseIndexOutsideQuotes(slice);
14610
14820
  if (endRel === -1) return null;
14611
14821
  const raw = slice.slice(0, endRel + 1);
14612
14822
  if (!/\/\s*>$/.test(raw)) depth++;
@@ -14626,7 +14836,7 @@ function findNextCustomHtmlBlockFromSource(source, tag, startIndex) {
14626
14836
  if (!openMatch || openMatch.index == null) return null;
14627
14837
  const openStart = openMatch.index;
14628
14838
  const openSlice = source.slice(openStart);
14629
- const openEndRel = findTagCloseIndexOutsideQuotes$1(openSlice);
14839
+ const openEndRel = findTagCloseIndexOutsideQuotes(openSlice);
14630
14840
  if (openEndRel === -1) return null;
14631
14841
  const openEnd = openStart + openEndRel;
14632
14842
  if (/\/\s*>\s*$/.test(openSlice.slice(0, openEndRel + 1))) {
@@ -14667,7 +14877,7 @@ function findNextCustomHtmlBlockFromSource(source, tag, startIndex) {
14667
14877
  continue;
14668
14878
  }
14669
14879
  if (isOpenAt(lt)) {
14670
- const rel = findTagCloseIndexOutsideQuotes$1(source.slice(lt));
14880
+ const rel = findTagCloseIndexOutsideQuotes(source.slice(lt));
14671
14881
  if (rel === -1) return null;
14672
14882
  depth++;
14673
14883
  i = lt + rel + 1;
@@ -14723,7 +14933,7 @@ function parseBasicBlockToken(tokens, index, options) {
14723
14933
  const fromSource = findNextCustomHtmlBlockFromSource(source, tag, Math.max(clampNonNegative(cursor), clampNonNegative(mappedLineStart)));
14724
14934
  if (fromSource) options.__customHtmlBlockCursor = fromSource.end;
14725
14935
  const rawHtml = String(fromSource?.raw ?? htmlBlockNode.raw ?? "");
14726
- const openEnd = findTagCloseIndexOutsideQuotes$1(rawHtml);
14936
+ const openEnd = findTagCloseIndexOutsideQuotes(rawHtml);
14727
14937
  const openTag = openEnd !== -1 ? rawHtml.slice(0, openEnd + 1) : rawHtml;
14728
14938
  const selfClosing = openEnd !== -1 && /\/\s*>\s*$/.test(openTag);
14729
14939
  const closeRange = openEnd === -1 ? null : findMatchingCloseTagRange(rawHtml, tag, openEnd + 1);
@@ -14818,26 +15028,17 @@ function parseParagraph(tokens, index, options) {
14818
15028
 
14819
15029
  //#endregion
14820
15030
  //#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
15031
  function getCustomHtmlTagSet(options) {
14828
15032
  const custom = options?.customHtmlTags;
14829
15033
  if (!Array.isArray(custom) || custom.length === 0) return null;
14830
- const normalized = custom.map(normalizeTagName).filter(Boolean);
15034
+ const normalized = normalizeCustomHtmlTags(custom);
14831
15035
  return normalized.length ? new Set(normalized) : null;
14832
15036
  }
14833
15037
  function buildAllowedHtmlTagSet(options) {
14834
15038
  const custom = options?.customHtmlTags;
14835
15039
  if (!Array.isArray(custom) || custom.length === 0) return STANDARD_HTML_TAGS;
14836
15040
  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
- }
15041
+ for (const name of normalizeCustomHtmlTags(custom)) if (name) set.add(name);
14841
15042
  return set;
14842
15043
  }
14843
15044
  function stringifyInlineNodeRaw(node) {
@@ -14896,30 +15097,6 @@ function parseStandaloneHtmlDocument(markdown) {
14896
15097
  loading: false
14897
15098
  }];
14898
15099
  }
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
15100
  function getMergeableNodeRaw(node) {
14924
15101
  const raw = node?.raw;
14925
15102
  if (typeof raw === "string") return raw;
@@ -14927,41 +15104,15 @@ function getMergeableNodeRaw(node) {
14927
15104
  if (typeof content === "string") return content;
14928
15105
  return "";
14929
15106
  }
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
- ]);
15107
+ function isCloseOnlyHtmlBlockForTag(node, tag) {
15108
+ if (node.type !== "html_block" || !tag) return false;
15109
+ const raw = String(node?.raw ?? node?.content ?? "");
15110
+ return new RegExp(String.raw`^\s*<\s*\/\s*${escapeTagForRegExp(tag)}\s*>\s*$`, "i").test(raw);
15111
+ }
14961
15112
  function findNextHtmlBlockFromSource(source, tag, startIndex) {
14962
15113
  if (!source || !tag) return null;
14963
15114
  const lowerTag = tag.toLowerCase();
14964
- const openRe = new RegExp(String.raw`<\s*${escapeRegex(lowerTag)}(?=\s|>|/)`, "gi");
15115
+ const openRe = new RegExp(String.raw`<\s*${escapeTagForRegExp(lowerTag)}(?=\s|>|/)`, "gi");
14965
15116
  openRe.lastIndex = Math.max(0, startIndex);
14966
15117
  const openMatch = openRe.exec(source);
14967
15118
  if (!openMatch || openMatch.index == null) return null;
@@ -14980,11 +15131,11 @@ function findNextHtmlBlockFromSource(source, tag, startIndex) {
14980
15131
  let index = openEnd + 1;
14981
15132
  const isOpenAt = (pos) => {
14982
15133
  const slice = source.slice(pos);
14983
- return new RegExp(String.raw`^<\s*${escapeRegex(lowerTag)}(?=\s|>|/)`, "i").test(slice);
15134
+ return new RegExp(String.raw`^<\s*${escapeTagForRegExp(lowerTag)}(?=\s|>|/)`, "i").test(slice);
14984
15135
  };
14985
15136
  const isCloseAt = (pos) => {
14986
15137
  const slice = source.slice(pos);
14987
- return new RegExp(String.raw`^<\s*\/\s*${escapeRegex(lowerTag)}(?=\s|>)`, "i").test(slice);
15138
+ return new RegExp(String.raw`^<\s*\/\s*${escapeTagForRegExp(lowerTag)}(?=\s|>)`, "i").test(slice);
14988
15139
  };
14989
15140
  while (index < source.length) {
14990
15141
  const lt = source.indexOf("<", index);
@@ -15070,7 +15221,7 @@ function isDetailsCloseHtmlBlock(node) {
15070
15221
  return /^\s*<\/details\b/i.test(raw);
15071
15222
  }
15072
15223
  function findLastClosingTagStart(raw, tag) {
15073
- const closeRe = new RegExp(String.raw`<\s*\/\s*${escapeRegex(tag)}(?=\s|>)`, "gi");
15224
+ const closeRe = new RegExp(String.raw`<\s*\/\s*${escapeTagForRegExp(tag)}(?=\s|>)`, "gi");
15074
15225
  let last = -1;
15075
15226
  let match;
15076
15227
  while ((match = closeRe.exec(raw)) !== null) last = match.index;
@@ -15196,14 +15347,22 @@ function mergeSplitTopLevelHtmlBlocks(nodes, final, source) {
15196
15347
  let sourceHtmlCursor = 0;
15197
15348
  for (let i = 0; i < merged.length; i++) {
15198
15349
  const node = merged[i];
15199
- if (node?.type !== "html_block") continue;
15350
+ const nodeRaw = getMergeableNodeRaw(node);
15351
+ const nodePos = nodeRaw ? source.indexOf(nodeRaw, sourceHtmlCursor) : -1;
15352
+ if (node?.type !== "html_block") {
15353
+ if (nodePos !== -1) sourceHtmlCursor = nodePos + nodeRaw.length;
15354
+ continue;
15355
+ }
15200
15356
  const tag = String(node?.tag ?? "").toLowerCase();
15201
15357
  if (!tag) continue;
15202
- if (!SOURCE_EXACT_HTML_BLOCK_TAGS.has(tag)) continue;
15203
- const exact = findNextHtmlBlockFromSource(source, tag, sourceHtmlCursor);
15358
+ if (tag === "details") {
15359
+ if (nodePos !== -1) sourceHtmlCursor = nodePos + nodeRaw.length;
15360
+ continue;
15361
+ }
15362
+ const exact = findNextHtmlBlockFromSource(source, tag, nodePos !== -1 ? nodePos : sourceHtmlCursor);
15204
15363
  if (!exact) continue;
15205
15364
  sourceHtmlCursor = exact.end;
15206
- const currentContent = String(node?.content ?? getMergeableNodeRaw(node));
15365
+ const currentContent = String(node?.content ?? nodeRaw);
15207
15366
  const currentRaw = String(node?.raw ?? currentContent);
15208
15367
  const nextContent = buildHtmlBlockContent(exact.raw, tag, exact.closed);
15209
15368
  const desiredLoading = !final && !exact.closed;
@@ -15212,10 +15371,14 @@ function mergeSplitTopLevelHtmlBlocks(nodes, final, source) {
15212
15371
  node.raw = exact.raw;
15213
15372
  node.loading = desiredLoading;
15214
15373
  if (!needsExpansion) continue;
15215
- let tailCursor = findApproximateConsumedPrefixEnd(exact.raw, currentContent);
15374
+ let tailCursor = findApproximateConsumedPrefixEnd(exact.raw, currentRaw);
15216
15375
  if (tailCursor === -1) tailCursor = 0;
15217
15376
  const j = i + 1;
15218
15377
  while (j < merged.length) {
15378
+ if (exact.closed && isCloseOnlyHtmlBlockForTag(merged[j], tag)) {
15379
+ merged.splice(j, 1);
15380
+ continue;
15381
+ }
15219
15382
  const nextRaw = getMergeableNodeRaw(merged[j]);
15220
15383
  if (!nextRaw) break;
15221
15384
  const nextPos = exact.raw.indexOf(nextRaw, tailCursor);
@@ -15394,7 +15557,7 @@ function ensureBlankLineBeforeInlineMultilineCustomHtmlBlocks(markdown, tags) {
15394
15557
  }
15395
15558
  return false;
15396
15559
  };
15397
- const findTagCloseIndexOutsideQuotes$4 = (input) => {
15560
+ const findTagCloseIndexOutsideQuotes$1 = (input) => {
15398
15561
  let inSingle = false;
15399
15562
  let inDouble = false;
15400
15563
  for (let i = 0; i < input.length; i++) {
@@ -15443,7 +15606,7 @@ function ensureBlankLineBeforeInlineMultilineCustomHtmlBlocks(markdown, tags) {
15443
15606
  i++;
15444
15607
  continue;
15445
15608
  }
15446
- const closeIdxRel = findTagCloseIndexOutsideQuotes$4(line.slice(i));
15609
+ const closeIdxRel = findTagCloseIndexOutsideQuotes$1(line.slice(i));
15447
15610
  if (closeIdxRel === -1) {
15448
15611
  hasRenderablePrefix = true;
15449
15612
  i++;
@@ -15547,7 +15710,7 @@ function normalizeCustomHtmlOpeningTagSameLine(markdown, tags) {
15547
15710
  while (i < s.length && isIndentWs(s[i])) i++;
15548
15711
  return s.slice(i);
15549
15712
  };
15550
- const findTagCloseIndexOutsideQuotes$4 = (input) => {
15713
+ const findTagCloseIndexOutsideQuotes$1 = (input) => {
15551
15714
  let inSingle = false;
15552
15715
  let inDouble = false;
15553
15716
  for (let i = 0; i < input.length; i++) {
@@ -15619,7 +15782,7 @@ function normalizeCustomHtmlOpeningTagSameLine(markdown, tags) {
15619
15782
  if (i === nameStart) return line;
15620
15783
  const tagName = line.slice(nameStart, i).toLowerCase();
15621
15784
  if (!tagSet.has(tagName)) return line;
15622
- const gtRel = findTagCloseIndexOutsideQuotes$4(line.slice(i));
15785
+ const gtRel = findTagCloseIndexOutsideQuotes$1(line.slice(i));
15623
15786
  if (gtRel === -1) return line;
15624
15787
  const gt = i + gtRel;
15625
15788
  if (hasClosingTagOnLine(line, gt + 1, tagName)) return line;
@@ -16012,9 +16175,7 @@ function parseMarkdownToStructure(markdown, md, options = {}) {
16012
16175
  else if (/\n[[(]\n*$/.test(safeMarkdown)) safeMarkdown = safeMarkdown.replace(/(\n\[|\n\()+\n*$/g, "\n");
16013
16176
  }
16014
16177
  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);
16178
+ const tags = normalizeCustomHtmlTags(options.customHtmlTags);
16018
16179
  if (tags.length) {
16019
16180
  safeMarkdown = ensureBlankLineBeforeInlineMultilineCustomHtmlBlocks(safeMarkdown, tags);
16020
16181
  safeMarkdown = normalizeCustomHtmlOpeningTagSameLine(safeMarkdown, tags);
@@ -16172,6 +16333,160 @@ function processTokens(tokens, options) {
16172
16333
  return result;
16173
16334
  }
16174
16335
 
16336
+ //#endregion
16337
+ //#region src/htmlRenderUtils.ts
16338
+ const CUSTOM_TAG_REGEX = /<([a-z][a-z0-9-]*)\b[^>]*>/gi;
16339
+ function hasOwn(obj, key) {
16340
+ return Object.prototype.hasOwnProperty.call(obj, key);
16341
+ }
16342
+ function isCustomHtmlComponentTag(tagName, customComponents) {
16343
+ const lowerTag = tagName.toLowerCase();
16344
+ if (EXTENDED_STANDARD_HTML_TAGS.has(lowerTag)) return false;
16345
+ return hasOwn(customComponents, lowerTag) || hasOwn(customComponents, tagName);
16346
+ }
16347
+ function sanitizeHtmlAttrs(attrs) {
16348
+ const clean = {};
16349
+ for (const [key, value] of Object.entries(attrs)) {
16350
+ const lowerKey = key.toLowerCase();
16351
+ if (DANGEROUS_HTML_ATTRS.has(lowerKey)) continue;
16352
+ if (URL_HTML_ATTRS.has(lowerKey) && value && isUnsafeHtmlUrl(value)) continue;
16353
+ clean[key] = value;
16354
+ }
16355
+ return clean;
16356
+ }
16357
+ function convertHtmlPropValue(value, key) {
16358
+ const lowerKey = key.toLowerCase();
16359
+ if ([
16360
+ "checked",
16361
+ "disabled",
16362
+ "readonly",
16363
+ "required",
16364
+ "autofocus",
16365
+ "multiple",
16366
+ "hidden"
16367
+ ].includes(lowerKey)) return value === "true" || value === "" || value === key;
16368
+ if ([
16369
+ "value",
16370
+ "min",
16371
+ "max",
16372
+ "step",
16373
+ "width",
16374
+ "height",
16375
+ "size",
16376
+ "maxlength"
16377
+ ].includes(lowerKey)) {
16378
+ const num = Number(value);
16379
+ if (value !== "" && !Number.isNaN(num)) return num;
16380
+ }
16381
+ return value;
16382
+ }
16383
+ function convertHtmlAttrsToProps(attrs) {
16384
+ const result = {};
16385
+ for (const [key, value] of Object.entries(attrs)) result[key] = convertHtmlPropValue(value, key);
16386
+ return result;
16387
+ }
16388
+ function isMeaningfulText(text$1) {
16389
+ return text$1.trim().length > 0;
16390
+ }
16391
+ function tokenizeHtml(html) {
16392
+ const tokens = [];
16393
+ let pos = 0;
16394
+ while (pos < html.length) {
16395
+ if (html.startsWith("<!--", pos)) {
16396
+ const commentEnd = html.indexOf("-->", pos);
16397
+ if (commentEnd !== -1) {
16398
+ pos = commentEnd + 3;
16399
+ continue;
16400
+ }
16401
+ break;
16402
+ }
16403
+ const tagStart = html.indexOf("<", pos);
16404
+ if (tagStart === -1) {
16405
+ if (pos < html.length) {
16406
+ const remainingText = html.slice(pos);
16407
+ if (isMeaningfulText(remainingText)) tokens.push({
16408
+ type: "text",
16409
+ content: remainingText
16410
+ });
16411
+ }
16412
+ break;
16413
+ }
16414
+ if (tagStart > pos) {
16415
+ const textContent = html.slice(pos, tagStart);
16416
+ if (isMeaningfulText(textContent)) tokens.push({
16417
+ type: "text",
16418
+ content: textContent
16419
+ });
16420
+ }
16421
+ if (html.startsWith("![CDATA[", tagStart + 1)) {
16422
+ const cdataEnd = html.indexOf("]]>", tagStart);
16423
+ if (cdataEnd !== -1) {
16424
+ tokens.push({
16425
+ type: "text",
16426
+ content: html.slice(tagStart, cdataEnd + 3)
16427
+ });
16428
+ pos = cdataEnd + 3;
16429
+ continue;
16430
+ }
16431
+ break;
16432
+ }
16433
+ if (html.startsWith("!", tagStart + 1)) {
16434
+ const specialEnd = html.indexOf(">", tagStart);
16435
+ if (specialEnd !== -1) {
16436
+ pos = specialEnd + 1;
16437
+ continue;
16438
+ }
16439
+ break;
16440
+ }
16441
+ const tagEnd = html.indexOf(">", tagStart);
16442
+ if (tagEnd === -1) break;
16443
+ const tagContent = html.slice(tagStart + 1, tagEnd).trim();
16444
+ const isClosingTag$1 = tagContent.startsWith("/");
16445
+ const isSelfClosing$1 = tagContent.endsWith("/");
16446
+ if (isClosingTag$1) {
16447
+ const tagName = tagContent.slice(1).trim();
16448
+ tokens.push({
16449
+ type: "tag_close",
16450
+ tagName
16451
+ });
16452
+ } else {
16453
+ const spaceIndex = tagContent.indexOf(" ");
16454
+ let tagName;
16455
+ let attrsStr = "";
16456
+ if (spaceIndex === -1) tagName = isSelfClosing$1 ? tagContent.slice(0, -1).trim() : tagContent.trim();
16457
+ else {
16458
+ tagName = tagContent.slice(0, spaceIndex).trim();
16459
+ attrsStr = tagContent.slice(spaceIndex + 1);
16460
+ }
16461
+ const attrs = {};
16462
+ if (attrsStr) {
16463
+ const attrRegex = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|(\S*)))?/g;
16464
+ let attrMatch;
16465
+ while ((attrMatch = attrRegex.exec(attrsStr)) !== null) {
16466
+ const name = attrMatch[1];
16467
+ const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
16468
+ if (name && !name.endsWith("/")) attrs[name] = value;
16469
+ }
16470
+ }
16471
+ tokens.push({
16472
+ type: isSelfClosing$1 || VOID_HTML_TAGS.has(tagName.toLowerCase()) ? "self_closing" : "tag_open",
16473
+ tagName,
16474
+ attrs
16475
+ });
16476
+ }
16477
+ pos = tagEnd + 1;
16478
+ }
16479
+ return tokens;
16480
+ }
16481
+ function hasCustomHtmlComponents(content, customComponents) {
16482
+ if (!content || !content.includes("<")) return false;
16483
+ if (!customComponents || Object.keys(customComponents).length === 0) return false;
16484
+ CUSTOM_TAG_REGEX.lastIndex = 0;
16485
+ let match;
16486
+ while ((match = CUSTOM_TAG_REGEX.exec(content)) !== null) if (isCustomHtmlComponentTag(match[1], customComponents)) return true;
16487
+ return false;
16488
+ }
16489
+
16175
16490
  //#endregion
16176
16491
  //#region src/index.ts
16177
16492
  const _registeredMarkdownPlugins = [];
@@ -16312,5 +16627,5 @@ function getMarkdown(msgId = `editor-${Date.now()}`, options = {}) {
16312
16627
  }
16313
16628
 
16314
16629
  //#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 };
16630
+ 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
16631
  //# sourceMappingURL=index.js.map