{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "markdown",
  "title": "Markdown",
  "description": "Optional safe CommonMark and GFM renderer that emits semantic nodes without raw HTML injection and consumes the shared prose CSS contract.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/markdown",
    "source": "src/wc/components/Markdown/Markdown.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "specialized",
      "status": "experimental",
      "summary": "Optional safe CommonMark and GFM renderer that emits semantic nodes without raw HTML injection and consumes the shared prose CSS contract.",
      "useWhen": [
        "A serializable Markdown string needs the design system's safe, opinionated renderer for headings, lists, links, tables, tasks, quotes, and code."
      ],
      "avoidWhen": [
        "The application already owns a safe semantic-DOM renderer and only needs @ds-mo/ui/prose.css.",
        "The application needs arbitrary HTML, custom plugins, or a full document CMS renderer."
      ],
      "commonlyComposedWith": [
        "component:ds-agent-response",
        "component:ds-code-block"
      ],
      "patterns": [
        "pattern:agent-chat"
      ],
      "accessibility": [
        "Semantic parsed elements preserve heading, list, table, quote, and code relationships.",
        "Raw HTML is ignored and unsafe link schemes are not made interactive.",
        "Link text must remain descriptive in authored content."
      ],
      "states": [
        "Streaming updates are coalesced to one parse and render per animation frame.",
        "Typography, rhythm, opt-out, and overflow styling come from the renderer-neutral .ds-prose source rather than a Markdown-only recipe."
      ],
      "responsiveBehavior": [
        "Tables and code scroll horizontally instead of forcing the transcript wider."
      ],
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/AgentResponse/AgentResponse.stories.ts"
        },
        {
          "label": "Prose distribution decision",
          "path": "docs/prose-foundation.md"
        }
      ]
    },
    "api": {
      "props": {
        "content": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "content",
          "default": "''",
          "required": false,
          "mutable": false
        },
        "streaming": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "streaming",
          "default": "false",
          "required": false,
          "mutable": false
        }
      },
      "events": [],
      "methods": [],
      "slots": []
    },
    "props": {
      "content": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "content",
        "default": "''",
        "required": false,
        "mutable": false
      },
      "streaming": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "streaming",
        "default": "false",
        "required": false,
        "mutable": false
      }
    },
    "events": [],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-markdown",
      "react": "DsMarkdown",
      "vue": "DsMarkdown",
      "angular": "DsMarkdown"
    },
    "consumption": {
      "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
      "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
      "customElements": {
        "import": "import '@ds-mo/ui/dist/components/ds-markdown.js';",
        "example": "<ds-markdown></ds-markdown>"
      },
      "react": {
        "import": "import { DsMarkdown } from '@ds-mo/ui/react';",
        "example": "<DsMarkdown />"
      },
      "vue": {
        "import": "import { DsMarkdown } from '@ds-mo/ui/vue';",
        "example": "<DsMarkdown />"
      },
      "angular": {
        "import": "import { DsMarkdown } from '@ds-mo/ui/angular/ds-markdown';",
        "example": "<ds-markdown></ds-markdown>"
      },
      "peerDependencies": {
        "required": [
          "@ds-mo/tokens ^6.5.0",
          "@ds-mo/icons ^7.0.0"
        ],
        "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
      }
    }
  },
  "dependencies": [
    "@ds-mo/ui",
    "@ds-mo/tokens"
  ],
  "registryDependencies": [
    "code-block"
  ],
  "files": [
    {
      "path": "src/wc/components/Markdown/Markdown.css",
      "content": "@import '../../styles/prose.css';\n\n:host {\n  display: block;\n  min-width: 0;\n  color: inherit;\n  box-sizing: border-box;\n  -webkit-user-select: text;\n  user-select: text;\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Markdown/Markdown.tsx",
      "content": "import { Component, h, Host, Prop, State, VNode, Watch } from '@stencil/core';\nimport type { Nodes, Root } from 'mdast';\nimport { resolveSafeUrl } from '../../utils';\n\ntype MarkdownRender = VNode | string | null | MarkdownRender[];\ntype MarkdownParser = {\n  fromMarkdown: typeof import('mdast-util-from-markdown').fromMarkdown;\n  gfmFromMarkdown: typeof import('mdast-util-gfm').gfmFromMarkdown;\n  gfm: typeof import('micromark-extension-gfm').gfm;\n};\n\nlet parserPromise: Promise<MarkdownParser> | undefined;\n\nfunction loadParser(): Promise<MarkdownParser> {\n  parserPromise ??= Promise.all([\n    import('mdast-util-from-markdown'),\n    import('mdast-util-gfm'),\n    import('micromark-extension-gfm'),\n  ]).then(([fromMarkdownModule, gfmFromMarkdownModule, gfmModule]) => ({\n    fromMarkdown: fromMarkdownModule.fromMarkdown,\n    gfmFromMarkdown: gfmFromMarkdownModule.gfmFromMarkdown,\n    gfm: gfmModule.gfm,\n  }));\n  return parserPromise;\n}\n\n@Component({\n  tag: 'ds-markdown',\n  styleUrl: 'Markdown.css',\n  scoped: true,\n})\nexport class Markdown {\n  @Prop() content: string = '';\n  @Prop() streaming: boolean = false;\n\n  @State() private tree: Root | null = null;\n  private parseFrame?: number;\n  private parseRevision = 0;\n\n  componentWillLoad() {\n    // The browser-targeted named-character decoder used by the Markdown parser\n    // creates a DOM element when its module is evaluated. Keep public package\n    // imports server-safe by loading that parser only in a browser context.\n    if (typeof document !== 'undefined') return this.parse();\n  }\n\n  disconnectedCallback() {\n    this.parseRevision += 1;\n    if (this.parseFrame && typeof cancelAnimationFrame === 'function') {\n      cancelAnimationFrame(this.parseFrame);\n    }\n  }\n\n  @Watch('content')\n  scheduleParse() {\n    if (typeof requestAnimationFrame !== 'function') return;\n    if (this.parseFrame) cancelAnimationFrame(this.parseFrame);\n    this.parseFrame = requestAnimationFrame(() => {\n      this.parseFrame = undefined;\n      void this.parse();\n    });\n  }\n\n  private async parse() {\n    const revision = ++this.parseRevision;\n    try {\n      const { fromMarkdown, gfmFromMarkdown, gfm } = await loadParser();\n      const tree = fromMarkdown(this.content, {\n        extensions: [gfm()],\n        mdastExtensions: [gfmFromMarkdown()],\n      });\n      if (revision === this.parseRevision) this.tree = tree;\n    } catch {\n      if (revision === this.parseRevision) this.tree = null;\n    }\n  }\n\n  private children(node: Nodes): MarkdownRender[] {\n    if (!('children' in node)) return [];\n    return node.children.map(child => this.renderNode(child));\n  }\n\n  private renderNode(node: Nodes): MarkdownRender {\n    switch (node.type) {\n      case 'root':\n        return this.children(node);\n      case 'text':\n        // Keep prose text on an element boundary. Besides giving streaming updates a\n        // stable render target, this avoids axe-core treating a bare Stencil text\n        // vnode as an element while it generates a contrast-result selector.\n        return <span class=\"markdown__text\">{node.value}</span>;\n      case 'paragraph':\n        return <p class=\"markdown__paragraph\">{this.children(node)}</p>;\n      case 'heading': {\n        const Tag = `h${node.depth}`;\n        return <Tag class=\"markdown__heading\">{this.children(node)}</Tag>;\n      }\n      case 'strong':\n        return <strong class=\"markdown__strong\">{this.children(node)}</strong>;\n      case 'emphasis':\n        return <em class=\"markdown__emphasis\">{this.children(node)}</em>;\n      case 'delete':\n        return <del class=\"markdown__delete\">{this.children(node)}</del>;\n      case 'blockquote':\n        return <blockquote class=\"markdown__blockquote\">{this.children(node)}</blockquote>;\n      case 'break':\n        return <br class=\"markdown__break\" />;\n      case 'thematicBreak':\n        return <hr class=\"markdown__thematic-break\" />;\n      case 'inlineCode':\n        return <code class=\"markdown__inline-code\">{node.value}</code>;\n      case 'code':\n        return (\n          <ds-code-block\n            class=\"markdown__code-block\"\n            data-ds-prose=\"off\"\n            code={node.value}\n            language={node.lang ?? ''}\n            filename={node.meta ?? ''}\n          />\n        );\n      case 'link': {\n        const href = resolveSafeUrl(node.url, {\n          protocols: ['http:', 'https:', 'mailto:'],\n        });\n        return href ? (\n          <a class=\"markdown__link\" href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n            {this.children(node)}\n          </a>\n        ) : (\n          this.children(node)\n        );\n      }\n      case 'list': {\n        const Tag = node.ordered ? 'ol' : 'ul';\n        return (\n          <Tag class=\"markdown__list\" start={node.ordered && node.start ? node.start : undefined}>\n            {this.children(node)}\n          </Tag>\n        );\n      }\n      case 'listItem':\n        return (\n          <li class=\"markdown__list-item\">\n            {typeof node.checked === 'boolean' ? (\n              <input\n                class=\"markdown__task-indicator\"\n                type=\"checkbox\"\n                checked={node.checked}\n                disabled\n                aria-hidden=\"true\"\n                tabIndex={-1}\n              />\n            ) : null}\n            {this.children(node)}\n          </li>\n        );\n      case 'table': {\n        const [heading, ...rows] = node.children;\n        return (\n          <div class=\"markdown__table-wrap ds-prose__table-scroll\" tabIndex={0}>\n            <table class=\"markdown__table\">\n              {heading ? (\n                <thead class=\"markdown__table-head\">\n                  <tr class=\"markdown__table-row\">\n                    {heading.children.map(cell => (\n                      <th class=\"markdown__table-heading\" scope=\"col\">\n                        {this.children(cell)}\n                      </th>\n                    ))}\n                  </tr>\n                </thead>\n              ) : null}\n              <tbody class=\"markdown__table-body\">{rows.map(row => this.renderNode(row))}</tbody>\n            </table>\n          </div>\n        );\n      }\n      case 'tableRow':\n        return <tr class=\"markdown__table-row\">{this.children(node)}</tr>;\n      case 'tableCell':\n        return <td class=\"markdown__table-cell\">{this.children(node)}</td>;\n      case 'image': {\n        const href = resolveSafeUrl(node.url, {\n          protocols: ['http:', 'https:', 'mailto:'],\n        });\n        return href ? (\n          <a class=\"markdown__image-link\" href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n            {node.alt || 'View image'}\n          </a>\n        ) : (\n          node.alt || null\n        );\n      }\n      case 'html':\n      case 'definition':\n      case 'footnoteDefinition':\n        return null;\n      case 'footnoteReference':\n        return <sup class=\"markdown__footnote\">{node.label}</sup>;\n      default:\n        return this.children(node);\n    }\n  }\n\n  render() {\n    return (\n      <Host aria-busy={this.streaming ? 'true' : undefined}>\n        <div class=\"markdown ds-prose\">\n          {this.tree ? (\n            this.renderNode(this.tree)\n          ) : (\n            <p class=\"markdown__paragraph\">\n              <span class=\"markdown__text\">{this.content}</span>\n            </p>\n          )}\n        </div>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
