{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rich-text-markdown",
  "title": "Rich Text Markdown",
  "description": "Rich Text Markdown from the Get IT Done design system.",
  "dependencies": [],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/rich-text-toolbar.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/rich-text-markdown.ts",
      "content": "import type { RichTextAction } from '@/components/ui/rich-text-toolbar';\n\nexport interface TextEdit {\n  value: string;\n  start: number;\n  end: number;\n}\n\nconst inlineMarkers: Partial<Record<RichTextAction, [string, string]>> = {\n  bold: ['**', '**'],\n  italic: ['*', '*'],\n  underline: ['<u>', '</u>'],\n  strikethrough: ['~~', '~~'],\n  code: ['`', '`'],\n};\n\nconst linePrefixes: Partial<Record<RichTextAction, string>> = {\n  heading1: '# ',\n  heading2: '## ',\n  quote: '> ',\n  bulletList: '- ',\n};\n\nconst orderedPattern = /^\\d+\\. /;\nconst prefixPattern = /^(?:#{1,6} |> |- |\\d+\\. )/;\n\nconst lineBounds = (value: string, start: number, end: number) => {\n  const from = value.lastIndexOf('\\n', start - 1) + 1;\n  const nextBreak = value.indexOf('\\n', end);\n  return { from, to: nextBreak === -1 ? value.length : nextBreak };\n};\n\n// Wraps the selection in markers, or removes them when the selection is already wrapped (inside or around).\nconst toggleInline = ({ value, start, end }: TextEdit, [open, close]: [string, string]): TextEdit => {\n  const selected = value.slice(start, end);\n  if (selected.startsWith(open) && selected.endsWith(close) && selected.length >= open.length + close.length) {\n    const inner = selected.slice(open.length, selected.length - close.length);\n    return { value: value.slice(0, start) + inner + value.slice(end), start, end: start + inner.length };\n  }\n  if (value.slice(start - open.length, start) === open && value.slice(end, end + close.length) === close) {\n    return {\n      value: value.slice(0, start - open.length) + selected + value.slice(end + close.length),\n      start: start - open.length,\n      end: end - open.length,\n    };\n  }\n  return {\n    value: value.slice(0, start) + open + selected + close + value.slice(end),\n    start: start + open.length,\n    end: end + open.length,\n  };\n};\n\n// Sets or toggles a prefix on every line touched by the selection. Lists renumber, headings and quotes replace other block prefixes.\nconst toggleLines = ({ value, start, end }: TextEdit, action: RichTextAction): TextEdit => {\n  const { from, to } = lineBounds(value, start, end);\n  const lines = value.slice(from, to).split('\\n');\n  const ordered = action === 'orderedList';\n  const prefix = linePrefixes[action];\n  const allHave = lines.every((line) => (ordered ? orderedPattern.test(line) : line.startsWith(prefix as string)));\n  const next = lines.map((line, index) => {\n    const bare = line.replace(prefixPattern, '');\n    if (allHave) return bare;\n    return ordered ? `${index + 1}. ${bare}` : `${prefix}${bare}`;\n  });\n  const joined = next.join('\\n');\n  return { value: value.slice(0, from) + joined + value.slice(to), start: from, end: from + joined.length };\n};\n\nconst insertLink = ({ value, start, end }: TextEdit, image: boolean): TextEdit => {\n  const label = value.slice(start, end) || (image ? 'alt' : 'link');\n  const url = 'https://';\n  const head = image ? '![' : '[';\n  const text = `${head}${label}](${url})`;\n  const urlStart = start + head.length + label.length + 2;\n  return { value: value.slice(0, start) + text + value.slice(end), start: urlStart, end: urlStart + url.length };\n};\n\nexport const applyRichTextAction = (edit: TextEdit, action: RichTextAction): TextEdit => {\n  const inline = inlineMarkers[action];\n  if (inline) return toggleInline(edit, inline);\n  if (action === 'link') return insertLink(edit, false);\n  if (action === 'image') return insertLink(edit, true);\n  // Actions without a markdown form (undo, align, colour) are handled by the host editor.\n  if (!linePrefixes[action] && action !== 'orderedList') return edit;\n  return toggleLines(edit, action);\n};\n\n// Which toolbar toggles are on for the current selection (markers around it, or a prefix on its first line).\nexport const activeRichTextActions = ({ value, start, end }: TextEdit): Partial<Record<RichTextAction, boolean>> => {\n  const result: Partial<Record<RichTextAction, boolean>> = {};\n  const selected = value.slice(start, end);\n  (Object.keys(inlineMarkers) as RichTextAction[]).forEach((action) => {\n    const [open, close] = inlineMarkers[action] as [string, string];\n    const around = value.slice(start - open.length, start) === open && value.slice(end, end + close.length) === close;\n    const inside =\n      selected.length > open.length + close.length && selected.startsWith(open) && selected.endsWith(close);\n    // `*` must not read as active inside `**` pairs.\n    const bold = action === 'italic' && value.slice(start - 2, start) === '**' && value.slice(end, end + 2) === '**';\n    result[action] = (around || inside) && !bold;\n  });\n  const { from } = lineBounds(value, start, start);\n  const line = value.slice(from, value.indexOf('\\n', from) === -1 ? value.length : value.indexOf('\\n', from));\n  result.heading2 = line.startsWith('## ');\n  result.heading1 = line.startsWith('# ');\n  result.quote = line.startsWith('> ');\n  result.bulletList = line.startsWith('- ');\n  result.orderedList = orderedPattern.test(line);\n  return result;\n};\n\n// Enter inside a list item starts the next item; Enter on an empty item ends the list.\nexport const continueList = ({ value, start, end }: TextEdit): TextEdit | null => {\n  if (start !== end) return null;\n  const { from } = lineBounds(value, start, end);\n  const line = value.slice(from, start);\n  const bullet = /^- (.*)$/.exec(line);\n  const number = /^(\\d+)\\. (.*)$/.exec(line);\n  const match = bullet ?? number;\n  if (!match) return null;\n  const content = bullet ? bullet[1] : (number as RegExpExecArray)[2];\n  if (content === '') {\n    return { value: value.slice(0, from) + value.slice(start), start: from, end: from };\n  }\n  const marker = bullet ? '- ' : `${Number((number as RegExpExecArray)[1]) + 1}. `;\n  const insert = `\\n${marker}`;\n  return {\n    value: value.slice(0, start) + insert + value.slice(end),\n    start: start + insert.length,\n    end: start + insert.length,\n  };\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}