| โฆ | โฆ | @@ -0,0 +1,1090 @@ | |
| 457 | + | (container.type === "code" || | |
| 458 | + | container.type === "html" || | |
| 459 | + | container.type === "math" || | |
| 460 | + | container.type === "table"); | |
๐ก Medium markdown/blocks.ts:455
A table absorbs subsequent block starts as data rows, so headings, lists, block quotes, and fenced code lose their block formatting until a blank line ends the table. Because table is treated as a matched leaf here, tryStart() never gets a chance to recognize those blocks; let table lines pass through block-start detection.
| 1 | let matchedLeaf = |
| 2 | container.type !== "paragraph" && |
| 3 | (container.type === "code" || |
| 4 | container.type === "html" || |
| 5 | container.type === "math"); |
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/blocks.ts around lines 455-460: |
| 2 | |
| 3 | A table absorbs subsequent block starts as data rows, so headings, lists, block quotes, and fenced code lose their block formatting until a blank line ends the table. Because `table` is treated as a matched leaf here, `tryStart()` never gets a chance to recognize those blocks; let table lines pass through block-start detection. |
| โฆ | โฆ | @@ -0,0 +1,834 @@ | |
| 160 | + | parseDefinition( | |
| 161 | + | lines: ContentLine[], | |
| 162 | + | ): { definition: ParsedDefinition; lines: number; restStart?: number } | null { | |
| 163 | + | const content = this.contentOf(lines); | |
๐ก Medium markdown/parseMarkdown.ts:163
A paragraph with many consecutive reference definitions takes quadratic time to parse, freezing rendering for large documents. Each parseDefinition call rebuilds the remaining text with contentOf and scans the remaining lines to compute used, while extractDefinitions copies the suffix with slice; parse from one shared buffer and advance a cursor instead.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/parseMarkdown.ts around line 163: |
| 2 | |
| 3 | A paragraph with many consecutive reference definitions takes quadratic time to parse, freezing rendering for large documents. Each `parseDefinition` call rebuilds the remaining text with `contentOf` and scans the remaining lines to compute `used`, while `extractDefinitions` copies the suffix with `slice`; parse from one shared buffer and advance a cursor instead. |
| โฆ | โฆ | @@ -0,0 +1,1327 @@ | |
| 1122 | + | this.options, | |
| 1123 | + | this.points, | |
| 1124 | + | ); | |
| 1125 | + | children = sub.parse(); | |
๐ High markdown/inlines.ts:1125
Rendering deeply nested :codex-followup[...] directives throws a JavaScript call-stack overflow instead of producing Markdown. Each label creates a nested InlineParser and calls sub.parse() with no nesting limit; bound directive nesting or parse labels iteratively.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/inlines.ts around line 1125: |
| 2 | |
| 3 | Rendering deeply nested `:codex-followup[...]` directives throws a JavaScript call-stack overflow instead of producing Markdown. Each label creates a nested `InlineParser` and calls `sub.parse()` with no nesting limit; bound directive nesting or parse labels iteratively. |
| โฆ | โฆ | @@ -0,0 +1,1327 @@ | |
| 1115 | + | this.source, | |
| 1116 | + | { | |
| 1117 | + | text: s.slice(labelStart, labelEnd), | |
| 1118 | + | contentStarts: [0], | |
๐ข Low markdown/inlines.ts:1118
Multiline directive-label children get incorrect source positions when container prefixes are removed; for example, in > :codex-followup[a\n> b], the position for b ends before its actual source location. This subparser replaces the parent's segmented source map with a single linear segment, so slice and rebase the parent's contentStarts and sourceStarts for the label.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/inlines.ts around line 1118: |
| 2 | |
| 3 | Multiline directive-label children get incorrect source positions when container prefixes are removed; for example, in `> :codex-followup[a\n> b]`, the position for `b` ends before its actual source location. This subparser replaces the parent's segmented source map with a single linear segment, so slice and rebase the parent's `contentStarts` and `sourceStarts` for the label. |
| โฆ | โฆ | @@ -0,0 +1,1327 @@ | |
| 1043 | + | end = close < 0 ? -1 : close + 3; | |
| 1044 | + | } | |
| 1045 | + | } else if (s.startsWith("<?", start)) { | |
| 1046 | + | const close = s.indexOf("?>", start + 2); | |
๐ก Medium markdown/inlines.ts:1046
An unterminated <? sequence makes Markdown parsing quadratic: every < rescans the remaining paragraph with s.indexOf("?>", start + 2), so a long run of <? without a closer synchronously blocks rendering. Cache failed closer searches (including --> and ]]>) so later openers don't rescan a suffix already known to have no match.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/inlines.ts around line 1046: |
| 2 | |
| 3 | An unterminated `<?` sequence makes Markdown parsing quadratic: every `<` rescans the remaining paragraph with `s.indexOf("?>", start + 2)`, so a long run of `<?` without a closer synchronously blocks rendering. Cache failed closer searches (including `-->` and `]]>`) so later openers don't rescan a suffix already known to have no match. |
| โฆ | โฆ | @@ -0,0 +1,834 @@ | |
| 650 | + | ) { | |
| 651 | + | const head = first.children[0]; | |
| 652 | + | // micromark-extension-gfm-task-list-item: whitespace, then more than the end of input. | |
| 653 | + | const match = head?.type === "text" ? /^\[([ \t]|[xX])\][ \t]+/.exec(head.value) : null; | |
๐ก Medium markdown/parseMarkdown.ts:653
An escaped or entity-authored [x] at the start of a list item is treated as a checked task marker, so ordinary bullet text is removed and the itemโs checked value is set to true. This regex matches decoded inline text; verify the marker in the source before mutating the children.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/parseMarkdown.ts around line 653: |
| 2 | |
| 3 | An escaped or entity-authored `[x]` at the start of a list item is treated as a checked task marker, so ordinary bullet text is removed and the itemโs `checked` value is set to `true`. This regex matches decoded inline text; verify the marker in the source before mutating the children. |
| โฆ | โฆ | @@ -0,0 +1,461 @@ | |
| 368 | + | if (name === "br" || !scope.open.some((element) => element.tagName === "p")) | |
| 369 | + | throw new Bail(); | |
| 370 | + | } | |
| 371 | + | const index = scope.open.findLastIndex((element) => element.tagName === name); | |
๐ก Medium markdown/rawHtml.ts:371
A raw end tag such as </h1> inside a Markdown heading is ignored here, so following content stays inside the heading instead of being emitted outside it; the same misnesting occurs for Markdown containers such as blockquotes and lists. Track Markdown ancestors and bail when an end tag would close one, so the caller can use rehype-raw.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/rawHtml.ts around line 371: |
| 2 | |
| 3 | A raw end tag such as `</h1>` inside a Markdown heading is ignored here, so following content stays inside the heading instead of being emitted outside it; the same misnesting occurs for Markdown containers such as blockquotes and lists. Track Markdown ancestors and bail when an end tag would close one, so the caller can use `rehype-raw`. |
| โฆ | โฆ | @@ -0,0 +1,461 @@ | |
| 353 | + | scope.open.pop(); | |
| 354 | + | if (name === "a" && (scope.inLink || scope.open.some((element) => element.tagName === "a"))) | |
| 355 | + | throw new Bail(); | |
| 356 | + | const element = h(name, token.attributes) as Element; | |
๐ก Medium markdown/rawHtml.ts:356
A raw <a.foo> is parsed as an actual a with class foo, so </a.foo> does not match and the following text is incorrectly kept inside the link. h interprets . and # in name as selector syntax; construct a literal element or bail when name contains these characters.
| 1 | + if (/[.#]/.test(name)) throw new Bail(); |
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/rawHtml.ts around line 356: |
| 2 | |
| 3 | A raw `<a.foo>` is parsed as an actual `a` with class `foo`, so `</a.foo>` does not match and the following text is incorrectly kept inside the link. `h` interprets `.` and `#` in `name` as selector syntax; construct a literal element or bail when `name` contains these characters. |
| โฆ | โฆ | @@ -0,0 +1,623 @@ | |
| 613 | + | throw error; | |
| 614 | + | } | |
| 615 | + | const footer = converter.footer(); | |
| 616 | + | if (footer) children.push(NEWLINE(), footer); | |
๐ High markdown/toHast.ts:609
A footnote reference inside a link in a footnote definition makes converter.footer() throw Bail outside the catch, so toChatHast throws instead of returning null and using the rehype fallback. Move footer conversion into the existing try block so this input follows the fallback path.
| 1 | try { |
| 2 | children = wrap(converter.all(tree), false); |
| 3 | + const footer = converter.footer(); |
| 4 | + if (footer) children.push(NEWLINE(), footer); |
| 5 | } catch (error) { |
| 6 | if (error instanceof Bail) return null; |
| 7 | throw error; |
| 8 | } |
| 9 | - const footer = converter.footer(); |
| 10 | - if (footer) children.push(NEWLINE(), footer); |
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/toHast.ts around lines 609-616: |
| 2 | |
| 3 | A footnote reference inside a link in a footnote definition makes `converter.footer()` throw `Bail` outside the catch, so `toChatHast` throws instead of returning `null` and using the rehype fallback. Move footer conversion into the existing `try` block so this input follows the fallback path. |
| โฆ | โฆ | @@ -0,0 +1,834 @@ | |
| 662 | + | if (head.value.length === 0) { | |
| 663 | + | first.children.shift(); | |
| 664 | + | } else { | |
| 665 | + | head.position!.start = this.point(head.position!.start.offset! + 4); | |
๐ก Medium markdown/parseMarkdown.ts:653
A task marker followed immediately by a line ending is rendered as ordinary text, so - [x]\n done displays [x] instead of marking the list item checked. The regex only accepts spaces or tabs after ]; accept line endings and remove the full matched whitespace when stripping the marker.
| 1 | - const match = head?.type === "text" ? /^\[([ \t]|[xX])\][ \t]+/.exec(head.value) : null; |
| 2 | + const match = head?.type === "text" ? /^\[([ \t]|[xX])\](?:[ \t]+|\r\n[ \t]*|[\r\n][ \t]*)/.exec(head.value) : null; |
| 3 | @@ |
| 4 | - head.value = head.value.slice(4); |
| 5 | + head.value = head.value.slice(match[0].length); |
| 6 | @@ |
| 7 | - head.position!.start = this.point(head.position!.start.offset! + 4); |
| 8 | + head.position!.start = this.point(head.position!.start.offset! + match[0].length); |
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/parseMarkdown.ts around lines 653-665: |
| 2 | |
| 3 | A task marker followed immediately by a line ending is rendered as ordinary text, so `- [x]\n done` displays `[x]` instead of marking the list item checked. The regex only accepts spaces or tabs after `]`; accept line endings and remove the full matched whitespace when stripping the marker. |
| โฆ | โฆ | @@ -0,0 +1,461 @@ | |
| 374 | + | if (FORMATTING.has(name)) throw new Bail(); | |
| 375 | + | return; | |
| 376 | + | } | |
| 377 | + | for (const element of scope.open.slice(index + 1)) { | |
๐ก Medium markdown/rawHtml.ts:377
For <div><span><div>one</span>two</div></div>, this end-tag handling closes the inner div when it processes </span>, so two is placed outside that div; parse5 ignores </span> because the intervening div is a special element. Bail when the search encounters a special element before the matching end tag.
๐ Reply "fix it for me" or copy this AI Prompt for your agent:
| 1 | In file @apps/web/src/markdown/rawHtml.ts around line 377: |
| 2 | |
| 3 | For `<div><span><div>one</span>two</div></div>`, this end-tag handling closes the inner `div` when it processes `</span>`, so `two` is placed outside that `div`; parse5 ignores `</span>` because the intervening `div` is a special element. Bail when the search encounters a special element before the matching end tag. |