Skip to content

Commit d682d7f

Browse files
fix(docs): exposes svg api on playground
1 parent 665e457 commit d682d7f

11 files changed

Lines changed: 124 additions & 21 deletions

File tree

docs/play/arrow-types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ declare module '@arrow-js/core' {
114114
strings: TemplateStringsArray | string[],
115115
...expressions: unknown[]
116116
): ArrowTemplate
117+
export function svg(
118+
strings: TemplateStringsArray | string[],
119+
...expressions: unknown[]
120+
): ArrowTemplate
117121
export { html as t }
118122

119123
export function reactive<T extends ReactiveTarget>(data: T): Reactive<T>
@@ -406,6 +410,7 @@ type SsrRenderResult = import('@arrow-js/ssr').SsrRenderResult
406410

407411
declare const html: typeof import('@arrow-js/core').html
408412
declare const reactive: typeof import('@arrow-js/core').reactive
413+
declare const svg: typeof import('@arrow-js/core').svg
409414
declare const watch: typeof import('@arrow-js/core').watch
410415
declare const nextTick: typeof import('@arrow-js/core').nextTick
411416
declare const component: typeof import('@arrow-js/core').component

docs/play/preview.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const pick = mod.pick
1818
export const props = mod.props
1919
export const r = mod.r
2020
export const reactive = mod.reactive
21+
export const svg = mod.svg
2122
export const t = mod.t
2223
export const w = mod.w
2324
export const watch = mod.watch

docs/src/pages/docs/content.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,20 @@ export function Routing() {
844844
render the same page shape.
845845
</li>
846846
</ul>
847+
<p>
848+
For browser-only routing, Arrow recommends the native
849+
<a
850+
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation"
851+
target="_blank"
852+
rel="noopener"
853+
>Navigation API</a
854+
>
855+
via <code>window.navigation</code> when your support matrix allows it.
856+
It gives you a single navigation event stream and more reliable
857+
history traversal than wiring everything around the older
858+
<code>history.pushState()</code> flow. Keep a History API fallback if
859+
you still support older browsers.
860+
</p>
847861
848862
${TsCodeBlock(`import { html } from '@arrow-js/core'
849863

packages/sandbox/src/compiler/module.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import {
77
} from './scope'
88
import type { ESTreeNode } from './scope'
99
import { compileTemplateDescriptor } from './template'
10-
import type { TemplateDescriptor } from '../shared/protocol'
10+
import type { ElementNamespace, TemplateDescriptor } from '../shared/protocol'
1111
import { SandboxCompileError } from '../host/errors'
1212

1313
const supportedImports = new Set([
1414
'component',
1515
'c',
1616
'html',
17+
'svg',
1718
't',
1819
'onCleanup',
1920
'pick',
@@ -70,24 +71,25 @@ function readImportBindings(program: ESTreeNode) {
7071
}
7172
}
7273

73-
function isArrowTagIdentifier(
74+
function getArrowTagNamespace(
7475
node: ESTreeNode,
7576
coreLocals: Map<string, string>,
7677
missingImports: Set<string>,
7778
isBound: (name: string) => boolean
78-
) {
79-
if (node.type !== 'Identifier') return false
79+
): ElementNamespace | 'html' | null {
80+
if (node.type !== 'Identifier') return null
8081

8182
const local = String(node.name)
8283
const imported = coreLocals.get(local)
83-
if (imported === 'html' || imported === 't') return true
84+
if (imported === 'svg') return 'svg'
85+
if (imported === 'html' || imported === 't') return 'html'
8486

85-
if ((local === 'html' || local === 't') && !isBound(local)) {
87+
if ((local === 'html' || local === 'svg' || local === 't') && !isBound(local)) {
8688
missingImports.add(local)
87-
return true
89+
return local === 'svg' ? 'svg' : 'html'
8890
}
8991

90-
return false
92+
return null
9193
}
9294

9395
export function preprocessModule(
@@ -103,7 +105,10 @@ export function preprocessModule(
103105
const { coreLocals, lastImportEnd } = readImportBindings(program)
104106
const missingImports = new Set<string>()
105107
const descriptors: TemplateDescriptor[] = []
106-
const taggedTemplates: ESTreeNode[] = []
108+
const taggedTemplates: Array<{
109+
expression: ESTreeNode
110+
namespace?: ElementNamespace
111+
}> = []
107112

108113
collectReferences(program, analysis, (node, scope, parent) => {
109114
if (
@@ -117,9 +122,15 @@ export function preprocessModule(
117122
if (node.type === 'TaggedTemplateExpression') {
118123
const tag = asNode(node.tag)
119124
const isBound = (name: string) => analysis.isNameBound(scope, name)
120-
121-
if (tag && isArrowTagIdentifier(tag, coreLocals, missingImports, isBound)) {
122-
taggedTemplates.push(node)
125+
const tagNamespace = tag
126+
? getArrowTagNamespace(tag, coreLocals, missingImports, isBound)
127+
: null
128+
129+
if (tagNamespace) {
130+
taggedTemplates.push({
131+
expression: node,
132+
namespace: tagNamespace === 'svg' ? 'svg' : undefined,
133+
})
123134
return
124135
}
125136

@@ -129,18 +140,21 @@ export function preprocessModule(
129140
coreLocals.has(String(asNode(tag.object)?.name))
130141
) {
131142
throw new SandboxCompileError(
132-
'Namespace-style Arrow html tags are not supported in @arrow-js/sandbox.'
143+
'Namespace-style Arrow template tags are not supported in @arrow-js/sandbox.'
133144
)
134145
}
135146
}
136147
})
137148

138149
const output = new MagicString(source)
139150

140-
taggedTemplates.sort((left, right) => (right.start || 0) - (left.start || 0))
151+
taggedTemplates.sort(
152+
(left, right) =>
153+
(right.expression.start || 0) - (left.expression.start || 0)
154+
)
141155
let templateIndex = 0
142156

143-
for (const expression of taggedTemplates) {
157+
for (const { expression, namespace } of taggedTemplates) {
144158
const quasi = expression.quasi as ESTreeNode
145159
const strings = (quasi.quasis as ESTreeNode[]).map((part) =>
146160
String((part.value as any).cooked ?? (part.value as any).raw ?? '')
@@ -149,7 +163,9 @@ export function preprocessModule(
149163
output.slice(part.start || 0, part.end || 0)
150164
)
151165
const descriptorId = `${path}#template:${templateIndex++}`
152-
descriptors.push(compileTemplateDescriptor(descriptorId, strings))
166+
descriptors.push(
167+
compileTemplateDescriptor(descriptorId, strings, namespace)
168+
)
153169

154170
output.overwrite(
155171
expression.start || 0,

packages/sandbox/src/compiler/template.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
AttributeBindingDescriptor,
3+
ElementNamespace,
34
ElementDescriptor,
45
TemplateDescriptor,
56
TemplateNodeDescriptor,
@@ -10,6 +11,7 @@ import { SandboxCompileError } from '../host/errors'
1011
const expressionTokenPrefix = '__ARROW_SANDBOX_EXPR_'
1112
const expressionTokenSuffix = '__'
1213
const expressionPattern = /__ARROW_SANDBOX_EXPR_(\d+)__/g
14+
const svgNamespaceUri = 'http://www.w3.org/2000/svg'
1315

1416
function getExpressionToken(index: number) {
1517
return `${expressionTokenPrefix}${index}${expressionTokenSuffix}`
@@ -162,6 +164,8 @@ function compileElementNode(element: Element): ElementDescriptor {
162164
return {
163165
kind: 'element',
164166
tag: element.tagName.toLowerCase(),
167+
namespace:
168+
element.namespaceURI === svgNamespaceUri ? 'svg' : undefined,
165169
staticAttributes,
166170
dynamicAttributes,
167171
eventBindings,
@@ -224,7 +228,8 @@ function collectExpressionIndexes(
224228

225229
export function compileTemplateDescriptor(
226230
templateId: string,
227-
strings: string[]
231+
strings: string[],
232+
namespace?: ElementNamespace
228233
): TemplateDescriptor {
229234
if (typeof document === 'undefined') {
230235
throw new SandboxCompileError(
@@ -238,7 +243,17 @@ export function compileTemplateDescriptor(
238243
}, '')
239244

240245
const template = document.createElement('template')
241-
template.innerHTML = html
246+
if (namespace === 'svg') {
247+
template.innerHTML = `<svg xmlns="${svgNamespaceUri}">${html}</svg>`
248+
const root = template.content.firstChild as SVGElement | null
249+
if (root) {
250+
const content = template.content
251+
while (root.firstChild) content.appendChild(root.firstChild)
252+
content.removeChild(root)
253+
}
254+
} else {
255+
template.innerHTML = html
256+
}
242257

243258
const rootCandidates = Array.from(template.content.childNodes)
244259
.flatMap((node) => compileDomNode(node))

packages/sandbox/src/host/renderer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
VmPatch,
66
} from '../shared/protocol'
77

8+
const svgNamespaceUri = 'http://www.w3.org/2000/svg'
9+
810
interface RegionAnchor {
911
start: Comment
1012
end: Comment
@@ -71,7 +73,10 @@ export class HostRenderer {
7173
return fragment
7274
}
7375
case 'element': {
74-
const element = document.createElement(serialized.tag)
76+
const element =
77+
serialized.namespace === 'svg'
78+
? document.createElementNS(svgNamespaceUri, serialized.tag)
79+
: document.createElement(serialized.tag)
7580
this.nodes.set(serialized.id, element)
7681
this.nodeIds.set(element, serialized.id)
7782

packages/sandbox/src/index.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,36 @@ describe('@arrow-js/sandbox', () => {
549549
instance.destroy()
550550
})
551551

552+
it('supports unbound svg tagged templates in sandbox modules', async () => {
553+
const root = document.createElement('div')
554+
555+
const instance = await sandbox(
556+
`
557+
const data = reactive({ values: [40, 20] })
558+
559+
export default html\`<svg width="100" height="100" viewBox="0 0 100 100">
560+
\${() =>
561+
data.values.map(
562+
(value, index) => svg\`<rect
563+
x="\${index * 10}"
564+
y="\${100 - value}"
565+
width="9"
566+
height="\${value}"
567+
fill="red"
568+
/>\`
569+
)}
570+
</svg>\`
571+
`,
572+
root
573+
)
574+
575+
const rects = Array.from(root.querySelectorAll('rect'))
576+
expect(rects).toHaveLength(2)
577+
expect(rects[0]?.namespaceURI).toBe('http://www.w3.org/2000/svg')
578+
expect(rects[1]?.getAttribute('height')).toBe('20')
579+
instance.destroy()
580+
})
581+
552582
it('rejects namespace-style @arrow-js/core imports', async () => {
553583
const root = document.createElement('div')
554584

packages/sandbox/src/shared/protocol.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ export interface RefBindingDescriptor {
8686
exprIndex: number
8787
}
8888

89+
export type ElementNamespace = 'svg'
90+
8991
export interface ElementDescriptor {
9092
kind: 'element'
9193
tag: string
94+
namespace?: ElementNamespace
9295
staticAttributes: Record<string, string>
9396
dynamicAttributes: AttributeBindingDescriptor[]
9497
eventBindings: EventBindingDescriptor[]
@@ -132,6 +135,7 @@ export interface SerializedElementNode {
132135
kind: 'element'
133136
id: string
134137
tag: string
138+
namespace?: ElementNamespace
135139
attrs: Record<string, string | boolean>
136140
events: Record<string, string>
137141
children: SerializedNode[]

packages/sandbox/src/vm/generated-modules.ts

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

packages/sandbox/src/vm/runtime/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export {
99
pick as props,
1010
reactive,
1111
reactive as r,
12+
svg,
1213
watch,
1314
watch as w,
1415
} from './runtime'

0 commit comments

Comments
 (0)