{"id":18110370,"url":"https://github.com/patrickg/html-svelte-parser","last_synced_at":"2025-04-14T00:44:13.643Z","repository":{"id":64074872,"uuid":"572544278","full_name":"PatrickG/html-svelte-parser","owner":"PatrickG","description":"HTML to Svelte parser.","archived":false,"fork":false,"pushed_at":"2024-01-11T10:11:52.000Z","size":200,"stargazers_count":16,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-09T09:06:22.995Z","etag":null,"topics":["dom","html","html-svelte-parser","html-to-svelte","parser","svelte","svelte-parser","sveltekit"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PatrickG.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-11-30T14:00:07.000Z","updated_at":"2024-08-21T21:43:56.000Z","dependencies_parsed_at":"2022-12-02T05:32:12.359Z","dependency_job_id":null,"html_url":"https://github.com/PatrickG/html-svelte-parser","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PatrickG%2Fhtml-svelte-parser","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PatrickG%2Fhtml-svelte-parser/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PatrickG%2Fhtml-svelte-parser/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PatrickG%2Fhtml-svelte-parser/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PatrickG","download_url":"https://codeload.github.com/PatrickG/html-svelte-parser/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248804721,"owners_count":21164127,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["dom","html","html-svelte-parser","html-to-svelte","parser","svelte","svelte-parser","sveltekit"],"created_at":"2024-11-01T00:08:40.451Z","updated_at":"2025-04-14T00:44:13.620Z","avatar_url":"https://github.com/PatrickG.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cimg alt=\"html-svelte-parser Logo\" src=\"./static/logo.svg\" width=\"64px\" /\u003e html-svelte-parser\n\nHTML to Svelte parser that works on both the server (Node.js) and the client (browser).\n\nTo replace an element with a svelte component, check out the [`processNode`](#processnode) option.\n\n#### Example\n\n_Paragraph.svelte_\n\n```svelte\n\u003cp\u003e\u003cslot /\u003e\u003c/p\u003e\n```\n\n_App.svelte_\n\n```svelte\n\u003cscript\u003e\n\timport { Html, isTag } from 'html-svelte-parser';\n\timport Paragraph from './Paragraph.svelte';\n\u003c/script\u003e\n\n\u003cHtml\n\thtml=\"\u003cp\u003eHello, World!\u003c/p\u003e\"\n\tprocessNode={node =\u003e {\n\t\tif (isTag(node) \u0026\u0026 node.name === 'p') {\n\t\t\treturn { component: Paragraph };\n\t\t}\n\t}}\n/\u003e\n\n\u003c!--\n\tEquivalent to:\n\n\t\u003cParagraph\u003eHello, World!\u003c/Paragraph\u003e\n--\u003e\n```\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003eTable of Contents\u003c/summary\u003e\n\n- [Install](#install)\n- [Usage](#usage)\n  - [processNode](#processnode)\n    - [Modify/remove nodes](#modifyremove-nodes)\n    - [Replace nodes](#replace-nodes)\n  - [Usage with sveltekit](#usage-with-sveltekit)\n  - [Named slots](#named-slots)\n- [Credits](#credits)\n\n\u003c/details\u003e\n\n## Install\n\nInstall the [NPM package _html-svelte-parser_](https://www.npmjs.com/package/html-svelte-parser) with your favorite package manager:\n\n```sh\nnpm install html-svelte-parser\n# pnpm add html-svelte-parser\n# yarn add html-svelte-parser\n```\n\n## Usage\n\n```svelte\n\u003cscript\u003e\n\timport { Html } from 'html-svelte-parser';\n\u003c/script\u003e\n\n\u003c!-- Single element: --\u003e\n\u003cHtml html=\"\u003ch1\u003esingle\u003c/h1\u003e\" /\u003e\n\n\u003c!-- Multiple elements: --\u003e\n\u003cul\u003e\n\t\u003cHtml html=\"\u003cli\u003eItem 1\u003c/li\u003e\u003cli\u003eItem 2\u003c/li\u003e\" /\u003e\n\u003c/ul\u003e\n\n\u003c!-- Nested elements: --\u003e\n\u003cHtml html=\"\u003cdiv\u003e\u003cp\u003eLorem ipsum\u003c/p\u003e\u003c/div\u003e\" /\u003e\n\n\u003c!-- Element with attributes: --\u003e\n\u003cHtml\n\thtml={`\u003chr id=\"foo\" class=\"bar\" data-attr=\"baz\" custom=\"qux\" style=\"top:42px;\"\u003e`}\n/\u003e\n```\n\n### processNode\n\nThe `processNode` option is a function that allows you to modify or remove a DOM node or replace it with a svelte component. It receives one argument which is [domhandler](https://github.com/fb55/domhandler)'s node (either [`Element`](https://github.com/fb55/domhandler/blob/88fb7a71446e221f5a09cd3c41713c51043be2a7/src/node.ts#L271) or [`Text`](https://github.com/fb55/domhandler/blob/88fb7a71446e221f5a09cd3c41713c51043be2a7/src/node.ts#L155)):\n\n```svelte\n\u003cHtml\n\thtml=\"\u003cbr\u003e\"\n\tprocessNode={domNode =\u003e {\n\t\tconsole.dir(domNode, { depth: null });\n\t}}\n/\u003e\n```\n\nConsole output:\n\n```js\nElement {\n  type: 'tag',\n  parent: null,\n  prev: null,\n  next: null,\n  startIndex: null,\n  endIndex: null,\n  children: [],\n  name: 'br',\n  attribs: {}\n}\n```\n\n#### Modify/remove nodes\n\nYou can directly modify the DOM nodes or remove them by returning `false`:\n\n```svelte\n\u003cscript\u003e\n\timport { Html, isTag, Text } from 'html-svelte-parser';\n\n\tconst html = `\n\t\t\u003cp id=\"remove\"\u003eremove me\u003c/p\u003e\n\t\t\u003cp id=\"keep\"\u003ekeep me\u003c/p\u003e\n\t`;\n\n\t/** @type {import('html-svelte-parser').ProcessNode} */\n\tconst processNode = domNode =\u003e {\n\t\tif (isTag(domNode)) {\n\t\t\tif (domNode.attribs.id === 'remove') {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tif (domNode.attribs.id === 'keep') {\n\t\t\t\tdomNode.attribs.id = 'i-stay';\n\t\t\t\tdomNode.children = [new Text('i stay!')];\n\t\t\t}\n\t\t}\n\t};\n\u003c/script\u003e\n\n\u003cHtml {html} {processNode} /\u003e\n\n\u003c!--\n\tEquivalent to:\n\n\t\u003cp id=\"i-stay\"\u003ei stay!\u003c/p\u003e\n--\u003e\n```\n\n#### Replace nodes\n\nTo replaced a DOM node with a svelte component return an object with a `component` property.\\\nAdditionally the object can have a `props` property.\n\n_Span.svelte_\n\n```svelte\n\u003cspan {...$$props}\u003e\u003cslot /\u003e\u003c/span\u003e\n```\n\n_App.svelte_\n\n```svelte\n\u003cscript\u003e\n\timport { Html, isTag, Text } from 'html-svelte-parser';\n\timport Span from './Span.svelte';\n\n\tconst html = `\u003cp id=\"replace\"\u003etext\u003c/p\u003e`;\n\n\t/** @type {import('html-svelte-parser').ProcessNode} */\n\tconst processNode = domNode =\u003e {\n\t\tif (isTag(domNode) \u0026\u0026 domNode.attribs.id === 'replace') {\n\t\t\tdomNode.children = [new Text('replaced')];\n\t\t\treturn { component: Span, props: { class: 'my-span' } };\n\t\t}\n\t};\n\u003c/script\u003e\n\n\u003cHtml {html} {processNode} /\u003e\n\n\u003c!--\n\tEquivalent to:\n\n\t\u003cspan class=\"my-span\"\u003ereplaced\u003c/span\u003e\n--\u003e\n```\n\n### Usage with sveltekit\n\n`html-svelte-parser` exports more than just the `Html` component, which makes it possible to delegate the work of parsing and processing to the server. An added bonus, you ship less code to the client.\n\n\u003cdetails\u003e\n\u003csummary\u003eOther files\u003c/summary\u003e\n\n_components/Button.svelte_\n\n```svelte\n\u003cscript\u003e\n\t/** @type {string | undefined} */\n\texport let href = undefined;\n\n\t/** @type {'button' | 'submit' | 'reset'}*/\n\texport let type = 'button';\n\u003c/script\u003e\n\n{#if href}\n\t\u003ca {...$$restProps} {href}\u003e\u003cslot /\u003e\u003c/a\u003e\n{:else}\n\t\u003cbutton {...$$restProps} {type}\u003e\u003cslot /\u003e\u003c/button\u003e\n{/if}\n```\n\n\u003c/details\u003e\n\n_+page.server.js_\n\n```js\nimport { isTag, parse } from 'html-svelte-parser';\n\n/** @type {import('./$types').PageServerLoad} */\nexport const load = () =\u003e {\n\treturn {\n\t\tcontent: parse(\n\t\t\t`\u003cp\u003e\u003ca class=\"btn\" href=\"https://svelte.dev/\"\u003eSvelte\u003c/a\u003e rocks\u003c/p\u003e`,\n\t\t\t{\n\t\t\t\tprocessNode(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tisTag(node) \u0026\u0026\n\t\t\t\t\t\tnode.name === 'a' \u0026\u0026\n\t\t\t\t\t\tnode.attribs.class?.split(/\\s/).includes('btn')\n\t\t\t\t\t) {\n\t\t\t\t\t\t// We use a `string` for the `component` property.\n\t\t\t\t\t\treturn { component: 'Button', props: node.attribs };\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t};\n};\n```\n\n_+page.js_\n\n```js\nimport { loadComponents } from 'html-svelte-parser';\n\n/** @type {import('./$types').PageLoad} */\nexport const load = ({ data }) =\u003e ({\n\tcontent: loadComponents(data.content, componentName =\u003e {\n\t\t// `componentName` is the `component` we returned in `+page.server.js`\n\t\treturn import(`./components/${componentName}.svelte`);\n\t}),\n});\n```\n\n_+page.svelte_\n\n```svelte\n\u003cscript\u003e\n\timport { Renderer } from 'html-svelte-parser';\n\n\t/** @type {import('./$types').PageData} */\n\texport let data;\n\u003c/script\u003e\n\n\u003cRenderer {...data.content} /\u003e\n\n\u003c!--\n\tEquivalent to:\n\n\t\u003cp\u003e\u003cButton class=\"btn\" href=\"https://svelte.dev/\"\u003eSvelte\u003c/Button\u003e rocks\u003c/p\u003e\n--\u003e\n```\n\n### Named slots\n\nWhat if your component has named slots? Unfortunately it is currently not possible to render named slots dynamically with svelte.\\\nFortunately, we can work around the problem with a wrapper component and the `Renderer` component.\n\n\u003cdetails\u003e\n\u003csummary\u003eOther files\u003c/summary\u003e\n\n_Button.svelte_\n\n```svelte\n\u003cscript\u003e\n\t/** @type {string | undefined} */\n\texport let href = undefined;\n\n\t/** @type {'button' | 'submit' | 'reset'}*/\n\texport let type = 'button';\n\u003c/script\u003e\n\n{#if href}\n\t\u003ca {...$$restProps} {href}\u003e\u003cslot /\u003e\u003c/a\u003e\n{:else}\n\t\u003cbutton {...$$restProps} {type}\u003e\u003cslot /\u003e\u003c/button\u003e\n{/if}\n```\n\n_Card.svelte_\n\n```svelte\n\u003cdiv class=\"card\"\u003e\n\t{#if $$slots.title}\n\t\t\u003cdiv class=\"title\"\u003e\u003cslot name=\"title\" /\u003e\u003c/div\u003e\n\t{/if}\n\n\t\u003cdiv class=\"content\"\u003e\u003cslot /\u003e\u003c/div\u003e\n\n\t{#if $$slots.actions}\n\t\t\u003cdiv class=\"actions\"\u003e\u003cslot name=\"actions\" /\u003e\u003c/div\u003e\n\t{/if}\n\u003c/div\u003e\n```\n\n\u003c/details\u003e\n\n_CardWrapper.svelte_\\\nThis is our wrapper component.\n\n```svelte\n\u003cscript\u003e\n\timport { Renderer } from 'html-svelte-parser';\n\timport Card from './Card.svelte';\n\n\t/** @type {import('html-svelte-parser').RendererProps} */\n\texport let title;\n\n\t/** @type {import('html-svelte-parser').RendererProps} */\n\texport let content;\n\n\t/** @type {import('html-svelte-parser').RendererProps} */\n\texport let actions;\n\u003c/script\u003e\n\n\u003cCard\u003e\n\t\u003cRenderer slot=\"title\" {...title} /\u003e\n\t\u003cRenderer {...content} /\u003e\n\t\u003cRenderer slot=\"actions\" {...actions} /\u003e\n\u003c/Card\u003e\n```\n\n_App.svelte_\n\n```svelte\n\u003cscript\u003e\n\timport { Html, isTag } from 'html-svelte-parser';\n\timport Button from './Button.svelte';\n\timport CardWrapper from './CardWrapper.svelte';\n\n\tconst html = `\n\t\t\u003cdiv class=\"card\"\u003e\n\t\t\t\u003ch1 class=\"card--title\"\u003eMy Card\u003c/h1\u003e\n\n\t\t\t\u003cdiv class=\"card--content\"\u003e\n\t\t\t\t\u003cp\u003eThis gets replaced with a nice Card component\u003c/p\u003e\n\t\t\t\t\u003cp\u003e\u003ca href=\"https://svelte.dev/\"\u003eSvelte\u003c/a\u003e is cool.\u003c/p\u003e\n\t\t\t\u003c/div\u003e\n\n\t\t\t\u003cdiv class=\"card--actions\"\u003e\n\t\t\t\t\u003ca class=\"btn\" href=\"/whatever\"\u003eCall to action\u003c/a\u003e\n\t\t\t\u003c/div\u003e\n\t\t\u003c/div\u003e\n\t`;\n\n\t// lets define some helpers\n\n\tconst hasClass = (\n\t\t/** @type {import('domhandler').Element} */ node,\n\t\t/** @type {string} */ className,\n\t) =\u003e node.attribs.class?.split(/\\s/).includes(className);\n\n\tconst findChildWithClass = (\n\t\t/** @type {import('domhandler').ParentNode} */ node,\n\t\t/** @type {string} */ className,\n\t) =\u003e\n\t\t/** @type {import('domhandler').Element | undefined} */ (\n\t\t\tnode.children.find(child =\u003e isTag(child) \u0026\u0026 hasClass(child, className))\n\t\t);\n\n\t/** @type {import('html-svelte-parser').ProcessNode} */\n\tconst processNode = node =\u003e {\n\t\tif (!isTag(node)) return;\n\n\t\t// add attributes to external links\n\t\tif (node.name === 'a' \u0026\u0026 !node.attribs.href?.startsWith('/')) {\n\t\t\tnode.attribs.target = '_blank';\n\t\t\tnode.attribs.rel = 'noreferrer nofollow';\n\t\t}\n\n\t\tif (hasClass(node, 'card')) {\n\t\t\treturn {\n\t\t\t\tcomponent: CardWrapper,\n\n\t\t\t\t// don't process child nodes / no \"default\" slot for `CardWrapper`\n\t\t\t\tnoChildren: true,\n\n\t\t\t\t// transform specific child nodes into props that get passed to\n\t\t\t\t// `CardWrapper` and can be rendered in a named slot with `Renderer`\n\t\t\t\trendererProps: {\n\t\t\t\t\t// even if `findChildWithClass` returns undefined, `CardWrapper`\n\t\t\t\t\t// still gets a `title` prop\n\t\t\t\t\ttitle: findChildWithClass(node, 'card--title'),\n\n\t\t\t\t\t// for `content` and `actions`, we want only the children of the\n\t\t\t\t\t// selected element to be rendered\n\t\t\t\t\tcontent: findChildWithClass(node, 'card--content')?.children,\n\t\t\t\t\tactions: findChildWithClass(node, 'card--actions')?.children,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (hasClass(node, 'btn')) {\n\t\t\treturn { component: Button, props: node.attribs };\n\t\t}\n\t};\n\u003c/script\u003e\n\n\u003cHtml {html} {processNode} /\u003e\n\n\u003c!--\n\tEquivalent to\n\n\t\u003cCard\u003e\n\t\t\u003csvelte:fragment slot=\"title\"\u003e\n\t\t\t\u003ch1 class=\"card--title\"\u003eMy Card\u003c/h1\u003e\n\t\t\u003c/svelte:fragment\u003e\n\n\t\t\u003cp\u003eThis gets replaced with a nice Card component\u003c/p\u003e\n\t\t\u003cp\u003e\u003ca href=\"https://svelte.dev/\" target=\"_blank\" rel=\"noreferrer nofollow\"\u003eSvelte\u003c/a\u003e is cool.\u003c/p\u003e\n\n\t\t\u003csvelte:fragment slot=\"actions\"\u003e\n\t\t\t\u003cButton class=\"btn\" href=\"/whatever\"\u003eCall to action\u003c/Button\u003e\n\t\t\u003c/svelte:fragment\u003e\n\t\u003c/Card\u003e\n--\u003e\n```\n\n## TODO\n\n- API docs\n- cleanup tests \u0026 more tests\n- GH page\n\n## Credits\n\n- [html-dom-parser](https://github.com/remarkablemark/html-dom-parser)\n- [htmlparser2](https://github.com/fb55/htmlparser2)\n- [domhandler](https://github.com/fb55/domhandler)\n- [dom-serializer](https://github.com/cheeriojs/dom-serializer)\n\nInspired by [html-react-parser](https://github.com/remarkablemark/html-react-parser) and [html-to-react](https://github.com/aknuds1/html-to-react).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickg%2Fhtml-svelte-parser","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatrickg%2Fhtml-svelte-parser","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatrickg%2Fhtml-svelte-parser/lists"}