{"id":40588014,"url":"https://github.com/opral/markdown-wc","last_synced_at":"2026-01-21T03:09:19.143Z","repository":{"id":331990982,"uuid":"1127318778","full_name":"opral/markdown-wc","owner":"opral","description":"Portable markdown spec with components (custom elements).","archived":false,"fork":false,"pushed_at":"2026-01-12T01:55:27.000Z","size":462,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-12T02:29:38.473Z","etag":null,"topics":["markdown","web-components"],"latest_commit_sha":null,"homepage":"https://markdown-wc.opral.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/opral.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-03T16:31:53.000Z","updated_at":"2026-01-12T01:55:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/opral/markdown-wc","commit_stats":null,"previous_names":["opral/markdown-wc"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/opral/markdown-wc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opral%2Fmarkdown-wc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opral%2Fmarkdown-wc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opral%2Fmarkdown-wc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opral%2Fmarkdown-wc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/opral","download_url":"https://codeload.github.com/opral/markdown-wc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/opral%2Fmarkdown-wc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28624349,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-21T02:47:06.670Z","status":"ssl_error","status_checked_at":"2026-01-21T02:45:44.886Z","response_time":86,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["markdown","web-components"],"created_at":"2026-01-21T03:09:19.075Z","updated_at":"2026-01-21T03:09:19.130Z","avatar_url":"https://github.com/opral.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\nimports:\n  - ./dist/walking-dinosaur.js\n  - https://cdn.jsdelivr.net/npm/@opral/markdown-wc-doc-elements/dist/doc-video.js\n---\n\n# Markdown WC\n\nEnables writing documentation with web components in markdown.\n\n```markdown\n---\nimports:\n  - https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/walking-dinosaur.js\n---\n\n# Markdown WC\n\nEnables writing documentation with web components in markdown.\n\n\u003cwalking-dinosaur\u003e\u003c/walking-dinosaur\u003e\n```\n\n\u003cwalking-dinosaur\u003e\u003c/walking-dinosaur\u003e\n\u003cdoc-video src=\"https://youtu.be/IMjJ1jvKsZU\"\u003e\u003c/doc-video\u003e\n\n## Why\n\n- Enables writing documentation with components in markdown\n- Interoperable with existing markdown parsers\n- Doesn't depend on a framework like [React MDX](https://mdxjs.com/) or [Svelte MDsveX](https://github.com/pngwn/MDsveX)\n- Doesn't introduce custom syntax like [Markdoc](https://markdoc.dev/)\n\n## Comparison\n\n| Feature                | Markdown | @opral/markdown-wc | React MDX | Svelte MDsveX | Markdoc |\n| ---------------------- | -------- | ------------------ | --------- | ------------- | ------- |\n| Components in markdown | ❌       | ✅                 | ✅        | ✅            | ✅      |\n| Framework agnostic     | ✅       | ✅                 | ❌        | ❌            | ✅      |\n| Portable               | ✅       | ✅                 | ❌        | ❌            | ❌      |\n| No custom syntax       | ✅       | ✅                 | ❌        | ❌            | ❌      |\n\nPortable: the output is standard HTML + web components without framework-specific runtime requirements.\n\n## Quick Start\n\nInstall:\n\n```bash\nnpm install @opral/markdown-wc\n```\n\nServer-side (or build-time) parse:\n\n```ts\nimport { parse } from \"@opral/markdown-wc\"\n\nconst markdown = `\n# Hello World\n\u003cmy-component\u003e\u003c/my-component\u003e\n`\n\nconst { html, frontmatter } = await parse(markdown)\n```\n\nClient-side: register custom elements listed in `frontmatter.imports`:\n\n```ts\nfor (const url of frontmatter.imports ?? []) {\n  await import(url)\n}\n```\n\nThen render `html` inside a container (see Styling below).\n\n## Usage in browser\n\nThe `\u003cmarkdown-wc-embed\u003e` element can be used to embed markdown-wc in a webpage.\nIt renders in the light DOM so the host page's CSS applies to embedded markdown by default.\n\n```html\n\u003cscript\n\ttype=\"module\"\n\tsrc=\"https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/markdown-wc-embed.js\"\n\u003e\u003c/script\u003e\n\u003cbody\u003e\n\t\u003cmarkdown-wc-embed src=\"https://my-markdown-url.com/markdown.md\"\u003e\u003c/markdown-wc-embed\u003e\n\u003c/body\u003e\n```\n\n## Usage in another markdown file\n\nThe `\u003cmarkdown-wc-embed\u003e` element can be used to embed markdown-wc in markdown-wc.\n`imports` must be a list of module URLs (array syntax). Wrap the embed in\n`.markdown-wc-body` if you want the default stylesheet to apply.\n\n```markdown\n---\nimports:\n  - https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/markdown-wc-embed.js\n---\n\n# Hello World\n\nThis is a markdown file that embeds another markdown file 🤯\n\n\u003cdiv class=\"markdown-wc-body\"\u003e\n  \u003cmarkdown-wc-embed src=\"https://cdn.jsdelivr.net/gh/opral/monorepo@latest/packages/markdown-wc/README.md\"\u003e\u003c/markdown-wc-embed\u003e\n\u003c/div\u003e\n```\n\n## Usage as libary\n\nEnables SSR and more control over the rendering process.\n\n```ts\nimport { parse } from \"@opral/markdown-wc\"\n\nconst markdown = `\n\n# Hello World\n\n// This is a web component\n\u003cdoc-card\u003e\n  \u003ch1\u003eCard Title\u003c/h1\u003e\n  \u003cp\u003eCard content\u003c/p\u003e\n\u003c/doc-card\u003e\n`\n\n// Parse markdown\nconst parsed = parse(markdown)\n\n// Optionally open external links in new tabs\n// const parsed = parse(markdown, { externalLinks: true })\n\n// Resolve relative asset URLs (images/links) against a base path\n// Markdown: ![Architecture](./assets/architecture.jpg)\n// Result:   /blog/my-post/assets/architecture.jpg\n// const parsed = parse(markdown, { assetBaseUrl: \"/blog/my-post/\" })\n\n// Register web components\nfor (const url of parsed.frontmatter.imports ?? []) {\n\t// optionally sanitize the imported imported here\n\t// by, for example, only trusting a specific domain\n\tawait import(url)\n}\n\n// render HTML\nrender(parsed.html)\n```\n\n## Security / Sanitization\n\nMarkdown WC does not sanitize HTML output. If you render untrusted or remote\nmarkdown, you must sanitize or trust the source before rendering.\n\n## Styling markdown-wc\n\nMarkdown WC renders standard HTML elements (headings, paragraphs, lists, blockquotes,\ncode blocks, tables, etc.) and does not inject inline styles. This keeps the output\nportable and allows the host application to fully control styling.\n\nCode blocks are syntax-highlighted, but no Highlight.js stylesheet is injected.\nTo enable highlighting styles, load a Highlight.js theme in your app (or provide your own `.hljs` styles).\n\nExample:\n\n```html\n\u003clink\n  rel=\"stylesheet\"\n  href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css\"\n/\u003e\n```\n\nReact example:\n\n```tsx\nexport function MarkdownStyles() {\n  return (\n    \u003clink\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css\"\n    /\u003e\n  );\n}\n```\n\nTo get the default look, import the bundled stylesheet and wrap your rendered HTML\nwith a `.markdown-wc-body` container (recommended to avoid CSS collisions). This\nis also the expected styling model for `\u003cmarkdown-wc-embed\u003e`.\n\n```css\n@import \"@opral/markdown-wc/default.css\";\n```\n\nOr provide your own CSS. Example:\n\n```css\n/* Wrap rendered HTML in a container and style inside it */\n.markdown-wc-body {\n\tcolor: #213547;\n\tfont-size: 16px;\n\tline-height: 1.7;\n}\n\n.markdown-wc-body h1 {\n\tfont-size: 2rem;\n\tfont-weight: 600;\n\tmargin: 0 0 1rem;\n}\n\n.markdown-wc-body h2 {\n\tfont-size: 1.5rem;\n\tfont-weight: 600;\n\tmargin: 2rem 0 0.75rem;\n\tborder-top: 1px solid #e2e2e3;\n\tpadding-top: 1.25rem;\n}\n\n.markdown-wc-body p {\n\tmargin: 1rem 0;\n}\n\n.markdown-wc-body a {\n\tcolor: #3a5ccc;\n\ttext-decoration: underline;\n\ttext-underline-offset: 3px;\n}\n```\n\nAlert styling example (GitHub alerts):\n\n```css\n.markdown-wc-body blockquote[data-mwc-alert] {\n\tborder-left: none;\n\tborder-radius: 8px;\n\tpadding: 12px 16px;\n\tmargin: 1rem 0;\n}\n\n.markdown-wc-body blockquote[data-mwc-alert] [data-mwc-alert-marker] {\n\tdisplay: none;\n}\n\n.markdown-wc-body blockquote[data-mwc-alert=\"note\"] {\n\tbackground: rgba(100, 108, 255, 0.08);\n\tborder: 1px solid rgba(100, 108, 255, 0.16);\n}\n.markdown-wc-body blockquote[data-mwc-alert=\"note\"]::before {\n\tcontent: \"Note\";\n\tdisplay: block;\n\tfont-weight: 600;\n\tmargin-bottom: 0.5rem;\n\tcolor: #3451b2;\n}\n```\n\nCode block styling / copy button hook example:\n\n```css\n.markdown-wc-body pre[data-mwc-codeblock] {\n\tposition: relative;\n\tpadding: 16px 20px;\n\tborder-radius: 8px;\n\tbackground: #f6f6f7;\n\toverflow-x: auto;\n}\n\n.markdown-wc-body pre[data-mwc-codeblock] \u003e button[data-mwc-copy-button] {\n\tposition: absolute;\n\ttop: 8px;\n\tright: 8px;\n\tfont-size: 12px;\n\tpadding: 4px 8px;\n\tborder-radius: 6px;\n\tborder: 1px solid #e2e2e3;\n\tbackground: #fff;\n\topacity: 0;\n}\n\n.markdown-wc-body pre[data-mwc-codeblock]:hover \u003e button[data-mwc-copy-button] {\n\topacity: 1;\n}\n```\n\n## Limitations\n\n- sanitzation of markdown as well as custom elements is not implemented atm\n- SSR is DIY atm (use the `parse` function and SSR the markdown with [lit for example](https://lit.dev/docs/ssr/overview/))\n\n## GitHub-style alerts / callouts\n\nMarkdown WC recognizes GitHub-style alert syntax inside blockquotes:\n\n```md\n\u003e [!NOTE]\n\u003e Highlights information that users should take into account.\n\n\u003e [!TIP]\n\u003e A helpful tip.\n\n\u003e [!IMPORTANT]\n\u003e Crucial information necessary for users to succeed.\n\n\u003e [!WARNING]\n\u003e Critical content demanding immediate attention.\n\n\u003e [!CAUTION]\n\u003e Dangerous or destructive actions.\n```\n\n### Rendered HTML\n\nAlerts are emitted as normal blockquotes annotated with data attributes:\n\n```html\n\u003cblockquote data-mwc-alert=\"note\"\u003e\n\t\u003cp\u003e\n\t\t\u003cspan data-mwc-alert-marker\u003e[!NOTE]\u003c/span\u003e\n\t\tHighlights information that users should take into account.\n\t\u003c/p\u003e\n\u003c/blockquote\u003e\n```\n\nSupported alert types: `note`, `tip`, `important`, `warning`, `caution`.\n\n### Styling alerts\n\nMarkdown WC does not ship alert CSS. Style them in your site using the data attributes.\n\nExample (VitePress-like):\n\n```css\nblockquote[data-mwc-alert] {\n\tborder-left: none;\n\tborder-radius: 8px;\n\tpadding: 16px;\n\tmargin: 16px 0;\n}\n\nblockquote[data-mwc-alert] [data-mwc-alert-marker] {\n\tdisplay: none;\n}\n\nblockquote[data-mwc-alert=\"note\"] {\n\tbackground: rgba(100, 108, 255, 0.08);\n\tborder: 1px solid rgba(100, 108, 255, 0.16);\n}\nblockquote[data-mwc-alert=\"note\"]::before {\n\tcontent: \"Note\";\n\tfont-weight: 600;\n\tcolor: #3451b2;\n\tdisplay: block;\n\tmargin-bottom: 8px;\n}\n```\n\n## Code blocks and copy buttons\n\nMarkdown WC keeps code blocks portable by rendering standard HTML:\n\n```html\n\u003cpre data-mwc-codeblock\u003e\n  \u003ccode class=\"language-js\"\u003e...\u003c/code\u003e\n\u003c/pre\u003e\n```\n\nThe `data-mwc-codeblock` attribute is a stable hook for consumer styling and\nclient-side hydration such as adding a “Copy” button. Markdown WC intentionally\ndoes not render custom UI elements for code blocks; consumers should implement\ncopy buttons and other enhancements with CSS and client-side scripts to keep the\nrenderer framework-agnostic and portable.\n\nExample copy-button script (based on inlang website-v2). This targets code blocks\ninside `.markdown-wc-body` and appends a `button.mwc-copy-button`:\n\n```ts\nconst COPY_BUTTON_CLASS = \"mwc-copy-button\"\n\nfunction ensureCopyButtons(root: Document | Element = document) {\n  const blocks = root.querySelectorAll(\".markdown-wc-body pre:has(\u003e code)\")\n  for (const pre of blocks) {\n    if (pre.querySelector(`.${COPY_BUTTON_CLASS}`)) continue\n\n    const button = document.createElement(\"button\")\n    button.type = \"button\"\n    button.className = COPY_BUTTON_CLASS\n    button.textContent = \"Copy\"\n    ;(pre as HTMLElement).style.position = \"relative\"\n    pre.appendChild(button)\n  }\n}\n\nfunction handleCopyClick(event: Event) {\n  const target = event.target\n  if (!(target instanceof HTMLElement)) return\n  const button = target.closest(`.${COPY_BUTTON_CLASS}`)\n  if (!button) return\n\n  const pre = button.closest(\"pre\")\n  const code = pre?.querySelector(\"code\")?.textContent ?? \"\"\n  navigator.clipboard.writeText(code)\n\n  const previous = button.textContent\n  button.textContent = \"Copied!\"\n  window.setTimeout(() =\u003e {\n    button.textContent = previous || \"Copy\"\n  }, 1500)\n}\n\nexport function initMarkdownCopyButtons() {\n  if (typeof window === \"undefined\") return\n  ensureCopyButtons()\n  document.addEventListener(\"click\", handleCopyClick)\n\n  const observer = new MutationObserver(() =\u003e ensureCopyButtons())\n  observer.observe(document.body, { childList: true, subtree: true })\n}\n```\n\n## Mermaid diagrams\n\nMarkdown WC supports Mermaid fenced code blocks:\n\n```md\n```mermaid\ngraph TD\n  A --\u003e B\n```\n```\n\nWhen Mermaid blocks are detected, Markdown WC emits a `\u003cmarkdown-wc-mermaid\u003e` element and adds an import URL to frontmatter:\n\n```ts\nconst { html, frontmatter } = await parse(markdown)\n// frontmatter.imports includes:\n// \"https://cdn.jsdelivr.net/npm/@opral/markdown-wc/dist/markdown-wc-mermaid.js\"\n```\n\nConsumers must load `frontmatter.imports` on the client so the custom element is registered before render.\n\n## FAQ\n\n### Why not use React MDX or Svelte MDsveX?\n\nReact MDX and Svelte MDsveX are great tools but they introduce a dependency on a specific framework which is a no-go for portability.\n\n### Why not use a `\u003cscript\u003e` tag to import the web components?\n\nMarkdown parsers don't remove the `\u003cscript\u003e` tag from the output. This means that the script tag would be rendered in the final HTML. To increase interoperability, frontmatter is used to define imports.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopral%2Fmarkdown-wc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopral%2Fmarkdown-wc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopral%2Fmarkdown-wc/lists"}