@refrakt-md/svelte 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/Renderer.svelte +22 -1
- package/src/ThemeShell.svelte +34 -9
- package/src/index.ts +1 -0
- package/src/theme.ts +13 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@refrakt-md/svelte",
|
|
3
3
|
"description": "Svelte renderer for refrakt.md content",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@markdoc/markdoc": "0.4.0",
|
|
30
|
-
"@refrakt-md/behaviors": "0.
|
|
31
|
-
"@refrakt-md/
|
|
30
|
+
"@refrakt-md/behaviors": "0.6.0",
|
|
31
|
+
"@refrakt-md/transform": "0.6.0",
|
|
32
|
+
"@refrakt-md/types": "0.6.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"svelte": "^5.0.0"
|
package/src/Renderer.svelte
CHANGED
|
@@ -24,6 +24,25 @@
|
|
|
24
24
|
return result;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function escapeAttr(str: string): string {
|
|
28
|
+
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Serialize an SVG tag tree to HTML string. Using {@html} avoids SVG namespace
|
|
32
|
+
* issues that occur when <svelte:element> creates child SVG elements (path, circle, etc.)
|
|
33
|
+
* outside the SVG namespace context during client-side navigation. */
|
|
34
|
+
function svgToHtml(tag: SerializedTag): string {
|
|
35
|
+
const attrs = Object.entries(htmlAttrs(tag.attributes))
|
|
36
|
+
.map(([k, v]) => ` ${k}="${escapeAttr(v)}"`)
|
|
37
|
+
.join('');
|
|
38
|
+
const children = tag.children.map(child => {
|
|
39
|
+
if (typeof child === 'string') return escapeAttr(child);
|
|
40
|
+
if (isTag(child)) return svgToHtml(child);
|
|
41
|
+
return '';
|
|
42
|
+
}).join('');
|
|
43
|
+
return `<${tag.name}${attrs}>${children}</${tag.name}>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
const globalOverrides = getElementOverrides();
|
|
28
47
|
const merged = $derived(overrides
|
|
29
48
|
? { ...globalOverrides, ...overrides }
|
|
@@ -55,10 +74,12 @@
|
|
|
55
74
|
<Renderer node={child} overrides={merged} />
|
|
56
75
|
{/each}
|
|
57
76
|
</ElementOverride>
|
|
77
|
+
{:else if node.name === 'svg'}
|
|
78
|
+
{@html svgToHtml(node)}
|
|
58
79
|
{:else}
|
|
59
80
|
<svelte:element this={node.name} {...htmlAttrs(node.attributes)}>
|
|
60
81
|
{#each node.children as child}
|
|
61
|
-
{#if node.attributes?.['data-codeblock'] && typeof child === 'string'}
|
|
82
|
+
{#if (node.attributes?.['data-codeblock'] || node.attributes?.['data-raw-html']) && typeof child === 'string'}
|
|
62
83
|
{@html child}
|
|
63
84
|
{:else}
|
|
64
85
|
<Renderer node={child} overrides={merged} />
|
package/src/ThemeShell.svelte
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SvelteTheme } from './theme.js';
|
|
3
|
+
import { isLayoutConfig } from './theme.js';
|
|
3
4
|
import { setRegistry, setElementOverrides } from './context.js';
|
|
4
5
|
import { setContext, tick } from 'svelte';
|
|
5
6
|
import { matchRouteRule } from './route-rules.js';
|
|
6
|
-
import { initRuneBehaviors } from '@refrakt-md/behaviors';
|
|
7
|
+
import { initRuneBehaviors, initLayoutBehaviors, registerElements, RfContext } from '@refrakt-md/behaviors';
|
|
8
|
+
import { layoutTransform } from '@refrakt-md/transform';
|
|
9
|
+
import Renderer from './Renderer.svelte';
|
|
7
10
|
|
|
8
11
|
interface OgMeta {
|
|
9
12
|
title?: string;
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
}>;
|
|
37
40
|
url: string;
|
|
38
41
|
seo?: PageSeo;
|
|
42
|
+
headings?: Array<{ level: number; text: string; id: string }>;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
let { theme, page }: { theme: SvelteTheme; page: PageData } = $props();
|
|
@@ -47,25 +51,42 @@
|
|
|
47
51
|
if (theme.elements) setElementOverrides(theme.elements);
|
|
48
52
|
// svelte-ignore state_referenced_locally
|
|
49
53
|
setContext('pages', page.pages);
|
|
54
|
+
// svelte-ignore state_referenced_locally
|
|
55
|
+
setContext('currentUrl', page.url);
|
|
56
|
+
|
|
57
|
+
// Populate RfContext for framework-neutral web components (Nav, Sandbox, etc.)
|
|
58
|
+
// svelte-ignore state_referenced_locally
|
|
59
|
+
RfContext.pages = page.pages;
|
|
60
|
+
// svelte-ignore state_referenced_locally
|
|
61
|
+
RfContext.currentUrl = page.url;
|
|
62
|
+
registerElements();
|
|
50
63
|
|
|
51
64
|
// Pick layout via route rules (reactive so layout updates on client-side navigation)
|
|
52
|
-
const layoutName = $derived(matchRouteRule(page.url, theme.manifest.routeRules));
|
|
53
|
-
const
|
|
65
|
+
const layoutName = $derived(matchRouteRule(page.url, theme.manifest.routeRules ?? []));
|
|
66
|
+
const layoutEntry = $derived(theme.layouts[layoutName] ?? theme.layouts['default']);
|
|
54
67
|
|
|
55
|
-
// Initialize rune behaviors after render, re-run on navigation.
|
|
68
|
+
// Initialize rune + layout behaviors after render, re-run on navigation.
|
|
56
69
|
// The {#key page.url} block in the template ensures full DOM recreation on
|
|
57
70
|
// navigation, so behaviors always run on fresh DOM and old behavior-modified
|
|
58
71
|
// elements are simply discarded (no cleanup/restore conflicts with Svelte).
|
|
59
72
|
$effect(() => {
|
|
60
73
|
void page.url; // re-run when page changes
|
|
61
|
-
|
|
74
|
+
// Keep RfContext in sync for web components on client-side navigation
|
|
75
|
+
RfContext.pages = page.pages;
|
|
76
|
+
RfContext.currentUrl = page.url;
|
|
77
|
+
let cleanupRunes: (() => void) | undefined;
|
|
78
|
+
let cleanupLayout: (() => void) | undefined;
|
|
62
79
|
let active = true;
|
|
63
80
|
tick().then(() => {
|
|
64
|
-
if (active)
|
|
81
|
+
if (active) {
|
|
82
|
+
cleanupRunes = initRuneBehaviors();
|
|
83
|
+
cleanupLayout = initLayoutBehaviors();
|
|
84
|
+
}
|
|
65
85
|
});
|
|
66
86
|
return () => {
|
|
67
87
|
active = false;
|
|
68
|
-
|
|
88
|
+
cleanupRunes?.();
|
|
89
|
+
cleanupLayout?.();
|
|
69
90
|
};
|
|
70
91
|
});
|
|
71
92
|
</script>
|
|
@@ -101,8 +122,11 @@
|
|
|
101
122
|
</svelte:head>
|
|
102
123
|
|
|
103
124
|
{#key page.url}
|
|
104
|
-
{#if
|
|
105
|
-
|
|
125
|
+
{#if isLayoutConfig(layoutEntry)}
|
|
126
|
+
{@const tree = layoutTransform(layoutEntry, page, 'rf')}
|
|
127
|
+
<Renderer node={tree} />
|
|
128
|
+
{:else if layoutEntry}
|
|
129
|
+
<svelte:component this={layoutEntry}
|
|
106
130
|
title={page.title}
|
|
107
131
|
description={page.description}
|
|
108
132
|
frontmatter={page.frontmatter}
|
|
@@ -110,6 +134,7 @@
|
|
|
110
134
|
renderable={page.renderable}
|
|
111
135
|
pages={page.pages}
|
|
112
136
|
url={page.url}
|
|
137
|
+
headings={page.headings}
|
|
113
138
|
/>
|
|
114
139
|
{/if}
|
|
115
140
|
{/key}
|
package/src/index.ts
CHANGED
|
@@ -5,4 +5,5 @@ export { serialize, serializeTree } from './serialize.js';
|
|
|
5
5
|
export { setRegistry, getComponent, setElementOverrides, getElementOverrides } from './context.js';
|
|
6
6
|
export type { ComponentRegistry, ElementOverrides } from './context.js';
|
|
7
7
|
export type { SvelteTheme } from './theme.js';
|
|
8
|
+
export { isLayoutConfig } from './theme.js';
|
|
8
9
|
export { matchRouteRule } from './route-rules.js';
|
package/src/theme.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ThemeManifest } from '@refrakt-md/types';
|
|
2
|
+
import type { LayoutConfig } from '@refrakt-md/transform';
|
|
2
3
|
import type { Component } from 'svelte';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -7,10 +8,20 @@ import type { Component } from 'svelte';
|
|
|
7
8
|
*/
|
|
8
9
|
export interface SvelteTheme {
|
|
9
10
|
manifest: ThemeManifest;
|
|
10
|
-
/** Layout name → Svelte component */
|
|
11
|
-
layouts: Record<string, Component<any
|
|
11
|
+
/** Layout name → declarative LayoutConfig or Svelte component (legacy) */
|
|
12
|
+
layouts: Record<string, Component<any> | LayoutConfig>;
|
|
12
13
|
/** typeof name → Svelte component (the component registry) */
|
|
13
14
|
components: Record<string, Component<any>>;
|
|
14
15
|
/** HTML element name → Svelte component (element-level overrides) */
|
|
15
16
|
elements?: Record<string, Component<any>>;
|
|
16
17
|
}
|
|
18
|
+
|
|
19
|
+
/** Runtime check: LayoutConfig has a `block` string + `slots` object */
|
|
20
|
+
export function isLayoutConfig(value: unknown): value is LayoutConfig {
|
|
21
|
+
return (
|
|
22
|
+
value !== null &&
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
typeof (value as any).block === 'string' &&
|
|
25
|
+
typeof (value as any).slots === 'object'
|
|
26
|
+
);
|
|
27
|
+
}
|