Ctrl K
ComunidadGuías…

src/lib/markdown-lite.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Enough markdown to read a background, and deliberately no more.
 *
 * The app had no renderer at all: `Role.background` has been documented as
 * markdown since the first commit and every tool writes `##` headings into it,
 * but the only place it was ever shown was a monospace textarea. So people read
 * their own career history as source code.
 *
 * This is a tokeniser for the subset the tools actually emit — headings,
 * bullets, numbered lists, paragraphs, bold and inline code — rather than a
 * markdown library. Reasons, in order:
 *
 *  - This app's whole pitch is one environment variable and five minutes. A
 *    parser and a sanitiser for content nobody else can see is weight that
 *    every self-hoster pays for.
 *  - The output is React elements built from plain strings. There is no
 *    dangerouslySetInnerHTML anywhere in the path, so a paste cannot inject
 *    anything — which is the reason most people reach for a library.
 *  - Anything it does not understand comes back as literal text rather than
 *    disappearing. A background is somebody's raw material; dropping a line
 *    because the syntax was unusual would be the worst bug this file could
 *    have.
 *
 * If a person ever needs tables, links or images here, that is the moment to
 * take the dependency — not before.
 */

export type Block =
  | { type: "heading"; level: 2 | 3; text: string }
  | { type: "bullets"; items: string[] }
  | { type: "ordered"; items: string[] }
  | { type: "paragraph"; text: string };

/** A run of text with at most one mark on it. */
export type Segment = { text: string; bold?: boolean; code?: boolean };

const BULLET = /^\s*[-*+]\s+(.*)$/;
const ORDERED = /^\s*\d+[.)]\s+(.*)$/;
const HEADING = /^(#{2,3})\s*(.*)$/;

/**
 * Markdown to blocks.
 *
 * Lines are taken in order and never reordered or dropped. A list ends at the
 * first line that is not an item of it, which is how people actually type.
 */
export function toBlocks(markdown: string): Block[] {
  const blocks: Block[] = [];
  const lines = markdown.split("\n");
  let paragraph: string[] = [];

  const flushParagraph = () => {
    const text = paragraph.join("\n").trim();
    if (text) blocks.push({ type: "paragraph", text });
    paragraph = [];
  };

  for (let i = 0; i < lines.length; i += 1) {
    const line = lines[i];

    const heading = HEADING.exec(line);
    if (heading) {
      flushParagraph();
      blocks.push({
        type: "heading",
        level: heading[1].length === 2 ? 2 : 3,
        text: heading[2].trim(),
      });
      continue;
    }

    const list = BULLET.exec(line) ? BULLET : ORDERED.exec(line) ? ORDERED : null;
    if (list) {
      flushParagraph();
      const items: string[] = [];
      // Consume every consecutive line of the SAME list shape. A numbered list
      // interrupted by a bullet is two lists, which is what it looks like.
      while (i < lines.length) {
        const match = list.exec(lines[i]);
        if (!match) break;
        items.push(match[1].trim());
        i += 1;
      }
      i -= 1;
      blocks.push({ type: list === BULLET ? "bullets" : "ordered", items });
      continue;
    }

    if (!line.trim()) {
      flushParagraph();
      continue;
    }
    paragraph.push(line);
  }

  flushParagraph();
  return blocks;
}

/**
 * Inline marks within one line: `**bold**` and `` `code` ``.
 *
 * Non-nesting and non-greedy, because the alternative is a parser. An unclosed
 * marker is left as the literal characters somebody typed rather than swallowing
 * the rest of the line.
 */
export function inlineSegments(text: string): Segment[] {
  const segments: Segment[] = [];
  const pattern = /(\*\*([^*]+)\*\*|`([^`]+)`)/g;
  let last = 0;

  for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
    if (match.index > last) segments.push({ text: text.slice(last, match.index) });
    if (match[2] !== undefined) segments.push({ text: match[2], bold: true });
    else segments.push({ text: match[3], code: true });
    last = match.index + match[0].length;
  }

  if (last < text.length) segments.push({ text: text.slice(last) });
  return segments.length > 0 ? segments : [{ text }];
}