Skip to content

Commit b71db7c

Browse files
it is time
1 parent 2c559a4 commit b71db7c

29 files changed

+534
-393
lines changed
File renamed without changes.

apps/docs/src/routes/playground/append-only/+page.svelte

Lines changed: 0 additions & 13 deletions
This file was deleted.

apps/docs/src/routes/playground/append-only/initial-content.md

Lines changed: 0 additions & 38 deletions
This file was deleted.
File renamed without changes.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"build": "pnpm -r build",
8-
"dev": "concurrently \"pnpm --filter hast-to-svelte build:watch\" \"pnpm --filter sveltedown build:watch\" \"pnpm --filter docs dev\"",
8+
"dev": "concurrently \"pnpm --filter svehast build:watch\" \"pnpm --filter sveltedown build:watch\" \"pnpm --filter docs dev\"",
99
"check": "pnpm -r check",
1010
"format": "prettier --write .",
1111
"lint": "prettier --check . && eslint .",

packages/hast-to-svelte/README.md

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,72 @@
1-
# Svelte library
1+
# `svehast`
22

3-
Everything you need to build a Svelte library, powered by [`sv`](https://npmjs.com/package/sv).
3+
A component for rendering [`hast`](https://github.com/syntax-tree/hast) trees. If you're just trying to render markdown, you may be looking for [`sveltedown`](https://npmjs.com/package/sveltedown) instead.
44

5-
Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
5+
## API
66

7-
## Creating a project
7+
This package exports a single component: `Hast`. You can use it like this:
88

9-
If you're seeing this, you've probably already done this step. Congrats!
9+
```svelte
10+
<Hast node={/* Root */} />
11+
```
12+
13+
`node` must be a `Root` node. If you have a different kind of `hast` node, you can turn it into a root node pretty easily: `{ type: 'root', children: [myNode] }`.
1014

11-
```sh
12-
# create a new project in the current directory
13-
npx sv create
15+
It also supports custom renderers. Normally, the easiest way to declare these is as snippets that are direct children of `Hast`:
1416

15-
# create a new project in my-app
16-
npx sv create my-app
17+
```svelte
18+
<Hast node={/* Root */}>
19+
{#snippet a({ tagName, props, children, node })}
20+
<a {...props} href="/haha-all-links-are-now-the-same">
21+
{@render children()}
22+
</a>
23+
{/snippet}
24+
</Hast>
1725
```
1826

19-
## Developing
27+
But you can also pass snippets as arguments to the `Hast` component (see [`RendererArg`](#rendererarg) below for argument details):
2028

21-
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
29+
```svelte
30+
{#snippet a({ tagName, props, children, node })}
31+
<a {...props} href="/haha-all-links-are-now-the-same">
32+
{@render children()}
33+
</a>
34+
{/snippet}
35+
36+
<Hast node={/* Root */} {a}/>
37+
```
2238

23-
```sh
24-
npm run dev
39+
You can also map nodes to other nodes. For example, if you wanted to only ever render down to a `h3`, you could map headings 4-6 back to `h3`:
2540

26-
# or start the server and open the app in a new browser tab
27-
npm run dev -- --open
41+
```svelte
42+
<Hast node={/* Root */} h4="h3" h5="h3" h6="h3">
2843
```
2944

30-
Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
45+
That's pretty much it!
3146

32-
## Building
47+
## Types
3348

34-
To build your library:
49+
This package exports a few types that might help you build your own extensions.
3550

36-
```sh
37-
npm pack
38-
```
51+
### `Renderer`
3952

40-
To create a production version of your showcase app:
53+
The type of a custom renderer. This is either a HTML/SVG tag name (for remapping) or a `Snippet` accepting a `RenderArg` as its only argument.
4154

42-
```sh
43-
npm run build
44-
```
55+
### `RendererArg`
4556

46-
You can preview the production build with `npm run preview`.
57+
The argument a custom renderer accepts:
4758

48-
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
59+
- `tagName` is the HTML/SVG tag name to render
60+
- `props` are the props. Typically you should spread these onto the element you're rendering
61+
- `children` is the snippet you need to render as a child. It will be `undefined` for void elements like `<img>`.
62+
- `node` is the original and unmodified `hast` node
4963

50-
## Publishing
64+
A note on `tagName`: This is the name associated with the _resolved_ renderer, not the one we started with. So if we started with a `hast` element with a `tagName` of `h6`, but `h6` had been mapped to `h3`, the tag name passed to your custom renderer would be `h3`. If you need the _original_ tag name, you can find it on the `node` prop, as that remains unchanged.
5165

52-
Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
66+
### `Renderers`
5367

54-
To publish your library to [npm](https://www.npmjs.com):
68+
A map of all HTML/SVG tag names that Svelte can render to their corresponding [`Renderer`](#renderer) definition.
5569

56-
```sh
57-
npm publish
58-
```
70+
### `HTMLElements`
71+
72+
This is `SvelteHTMLElements` without the special `svelte:` elements and with no index signature. Essentially, it's a map of all HTML and SVG tags that Svelte can render to the props that those tag types can have. You probably don't need this.

packages/hast-to-svelte/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "hast-to-svelte",
2+
"name": "svehast",
33
"version": "0.0.1",
44
"scripts": {
55
"build": "npm run prepack",
@@ -40,6 +40,7 @@
4040
"@types/node": "catalog:",
4141
"@vitest/browser": "catalog:",
4242
"config": "workspace:*",
43+
"hastscript": "catalog:",
4344
"playwright": "catalog:",
4445
"publint": "catalog:",
4546
"svelte": "catalog:",
@@ -55,5 +56,8 @@
5556
"dependencies": {
5657
"property-information": "catalog:",
5758
"style-to-object": "catalog:"
59+
},
60+
"publishConfig": {
61+
"access": "public"
5862
}
5963
}

packages/hast-to-svelte/src/Hast.svelte

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
<script lang="ts">
22
import type { Root, RootContent } from 'hast';
3-
import type { Renderer, Renderers, SpecificSvelteHTMLElements } from './types.js';
4-
import { svg, html } from 'property-information';
3+
import type { Renderer, Renderers, HTMLElements } from './types.js';
4+
import { svg, html, type Schema } from 'property-information';
55
import { key, reset } from './key.js';
66
import type { Snippet } from 'svelte';
77
import { sveltify_props, sveltify_children } from './ast.js';
88
import { get_renderer } from './renderers.js';
99
10-
let { node, renderers }: { node: Root; renderers: Renderers } = $props();
10+
let { node, ...renderers }: { node: Root } & Renderers = $props();
1111
1212
$effect.pre(() => {
1313
node;
1414
reset();
1515
});
1616
</script>
1717

18-
{#snippet nodes(children: RootContent[])}
18+
{#snippet nodes(schema: Schema, children: RootContent[])}
1919
{#each children as node (key(node))}
2020
{#if node.type === 'text'}
2121
{node.value}
2222
{:else if node.type === 'element'}
23-
{@const schema = node.tagName.toLowerCase() === 'svg' ? svg : html}
24-
{@const props = sveltify_props(schema, node)}
23+
{@const child_schema = node.tagName.toLowerCase() === 'svg' ? svg : schema}
24+
{@const props = sveltify_props(child_schema, node)}
2525
{@const has_children = node.children.length > 0}
2626
{@const [resolved_tag_name, renderer] = get_renderer(
2727
node.tagName,
2828
renderers,
29-
(has_children ? element : void_element) as Renderer<keyof SpecificSvelteHTMLElements>
29+
(has_children ? element : void_element) as Renderer<keyof HTMLElements>
3030
)}
3131

3232
{#snippet children()}
33-
{@render nodes(sveltify_children(node))}
33+
{@render nodes(child_schema, sveltify_children(node))}
3434
{/snippet}
3535

3636
{@render renderer({
@@ -61,4 +61,4 @@
6161
</svelte:element>
6262
{/snippet}
6363

64-
{@render nodes(node.children)}
64+
{@render nodes(html, node.children)}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render } from 'vitest-browser-svelte';
3+
import Hast from './Hast.svelte';
4+
import { h } from 'hastscript';
5+
6+
// This test file is minimal because this is pretty thoroughly tested in `sveltedown`.
7+
// If there are ever any bugs we need to document, we can add more tests here.
8+
9+
describe('Hast', () => {
10+
it('should render a simple div', async () => {
11+
const { container } = render(Hast, {
12+
node: { type: 'root', children: [h('div', 'Hello, world!')] }
13+
});
14+
await expect.element(container).to_equal_html('<div>Hello, world!</div>');
15+
});
16+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { sveltify_props, sveltify_children } from './ast.js';
3+
import { h, s } from 'hastscript';
4+
import { html, svg } from 'property-information';
5+
6+
const WHITESPACE_CASES = [
7+
{ display: JSON.stringify(' ').replaceAll('"', ''), actual: ' ' },
8+
{ display: JSON.stringify('\t').replaceAll('"', ''), actual: '\t' },
9+
{ display: JSON.stringify('\n').replaceAll('"', ''), actual: '\n' },
10+
{ display: JSON.stringify('\r').replaceAll('"', ''), actual: '\r' },
11+
{ display: JSON.stringify('\f').replaceAll('"', ''), actual: '\f' }
12+
] as const;
13+
14+
describe('sveltify_props', () => {
15+
it('should un-jsx className', () => {
16+
// this node has a `className` prop instead of a `class` prop, because
17+
// why would you not use JSX prop naming in a HTML
18+
const node = h('div.dumb');
19+
expect(node).toMatchObject({ properties: { className: ['dumb'] } });
20+
expect(sveltify_props(html, node)).toEqual({ class: 'dumb' });
21+
});
22+
23+
it('should un-jsx data attributes', () => {
24+
const node = h('div', { 'data-test': 'test' });
25+
expect(node).toMatchObject({ properties: { dataTest: 'test' } });
26+
expect(sveltify_props(html, node)).toEqual({ 'data-test': 'test' });
27+
});
28+
29+
it('should un-jsx svg attributes', () => {
30+
const node = s('svg', { 'stroke-linejoin': '100' });
31+
expect(node).toMatchObject({ properties: { strokeLineJoin: '100' } });
32+
expect(sveltify_props(svg, node)).toEqual({ 'stroke-linejoin': '100' });
33+
});
34+
35+
describe.each(['td', 'th'])('%s', (tag_name) => {
36+
it.each(['center', 'left', 'right', 'justify', 'char'])(
37+
'should replace obsolete align in table elements with styles (%s)',
38+
(align) => {
39+
const node = h(tag_name, { align });
40+
expect(node).toMatchObject({ properties: { align } });
41+
expect(sveltify_props(html, node)).toEqual({ style: `text-align: ${align}` });
42+
}
43+
);
44+
45+
it.each(['center', 'left', 'right', 'justify', 'char'])(
46+
'should replace obsolete align in table elements with styles, appending to existing styles (%s)',
47+
(align) => {
48+
const node = h(tag_name, { align, style: 'color: red;' });
49+
expect(node).toMatchObject({ properties: { align, style: 'color: red;' } });
50+
expect(sveltify_props(html, node)).toEqual({ style: `color: red; text-align: ${align}` });
51+
}
52+
);
53+
});
54+
});
55+
56+
describe('sveltify_children', () => {
57+
describe.each(['table', 'tbody', 'thead', 'tfoot', 'tr'])('%s', (tag_name) => {
58+
it.each(WHITESPACE_CASES)(
59+
`should remove whitespace-only text nodes from table elements ($display))`,
60+
({ actual: whitespace }) => {
61+
const node = h(tag_name, [whitespace, h('div', 'Hello, world!'), whitespace.repeat(10)]);
62+
expect(sveltify_children(node)).toEqual([h('div', 'Hello, world!')]);
63+
}
64+
);
65+
});
66+
67+
describe.each(['div', 'span', 'p', 'h1'])('%s', (tag_name) => {
68+
it.each(WHITESPACE_CASES)(
69+
'should preserve whitespace-only text nodes from table elements ($display))',
70+
({ actual: whitespace }) => {
71+
const node = h(tag_name, [whitespace, h('div', 'Hello, world!'), whitespace.repeat(10)]);
72+
expect(sveltify_children(node)).toEqual(
73+
h(tag_name, [whitespace, h('div', 'Hello, world!'), whitespace.repeat(10)]).children
74+
);
75+
}
76+
);
77+
});
78+
});

0 commit comments

Comments
 (0)