143 lines · 4.1 KB · TypeScript
RawDownload
interface MarkdownPosition {  readonly start?: {    readonly line?: number;    readonly offset?: number;  };} interface MarkdownAstNode {  readonly type: string;  readonly value?: unknown;  readonly position?: MarkdownPosition;  children?: MarkdownAstNode[];} interface MarkdownFile {  readonly value?: unknown;} interface MarkdownParser {  parse(markdown: string): unknown;} interface RecoveredMarkdown {  readonly blocks: MarkdownAstNode[];  readonly source: string;} const INLINE_PARSE_PREFIX = "t3-markdown-inline-prefix:"; function isSameLineOverIndentedCode(  node: MarkdownAstNode,  parent: MarkdownAstNode | undefined,  markdown: string,): boolean {  if (    node.type !== "code" ||    parent?.type !== "listItem" ||    typeof node.value !== "string" ||    !/^[\t ]/.test(node.value)  ) {    return false;  }   const nodeStart = node.position?.start;  const parentStart = parent.position?.start;  if (    nodeStart?.line === undefined ||    nodeStart.offset === undefined ||    parentStart?.line === undefined ||    nodeStart.line !== parentStart.line  ) {    return false;  }   const sourceCharacter = markdown[nodeStart.offset];  return sourceCharacter !== "`" && sourceCharacter !== "~";} function parseRecoveredMarkdown(value: string, parser: MarkdownParser): RecoveredMarkdown {  // A text prefix forces block-looking input into a paragraph while preserving  // the processor's configured inline extensions (for example, GFM syntax).  // Later root children are kept as blocks so blank-line-separated content is  // never discarded.  const source = `${INLINE_PARSE_PREFIX}${value}`;  const document = parser.parse(source) as MarkdownAstNode;  const blocks = document.children;  const paragraph = blocks?.[0];  const children = paragraph?.type === "paragraph" ? paragraph.children : undefined;  const first = children?.[0];  if (    !blocks ||    !children ||    first?.type !== "text" ||    typeof first.value !== "string" ||    !first.value.startsWith(INLINE_PARSE_PREFIX)  ) {    return { blocks: [{ type: "text", value }], source };  }   const firstValue = first.value.slice(INLINE_PARSE_PREFIX.length);  return {    blocks: [      {        ...paragraph,        type: "paragraph",        children: [...(firstValue ? [{ ...first, value: firstValue }] : []), ...children.slice(1)],      },      ...blocks.slice(1),    ],    source,  };} function blocksFromIndentedCode(node: MarkdownAstNode, parser: MarkdownParser): RecoveredMarkdown {  const value = typeof node.value === "string" ? node.value.trim() : "";  const recovered = parseRecoveredMarkdown(value, parser);  const first = recovered.blocks[0];  return {    ...recovered,    blocks:      first && node.position        ? [{ ...first, position: node.position }, ...recovered.blocks.slice(1)]        : recovered.blocks,  };} /** * CommonMark treats four or more spaces after a list marker as an indented * code block. In chat output, excessive spacing is commonly accidental * alignment such as `-       text`, which otherwise produces a full code card * for every bullet. Only normalize blocks that retain excess indentation and * start on the marker's own line; explicit fences and conventional indented * blocks remain code. */function attachListItemIndentationNormalizer(this: MarkdownParser) {  return (tree: MarkdownAstNode, file: MarkdownFile) => {    if (typeof file.value !== "string") {      return;    }    const markdown = file.value;     const visit = (node: MarkdownAstNode, source: string) => {      if (!node.children) {        return;      }      node.children = node.children.flatMap((child) => {        if (isSameLineOverIndentedCode(child, node, source)) {          const recovered = blocksFromIndentedCode(child, this);          for (const block of recovered.blocks) {            visit(block, recovered.source);          }          return recovered.blocks;        }        visit(child, source);        return [child];      });    };     visit(tree, markdown);  };} export const remarkNormalizeListItemIndentation = attachListItemIndentationNormalizer;