diff --git a/scripts/h.ts b/scripts/h.ts
index 87e87de..843d4f3 100644
--- a/scripts/h.ts
+++ b/scripts/h.ts
@@ -1,9 +1,10 @@
/* eslint-disable no-use-before-define */
// preact is fine, but i won't need it for the project.
+// Note: This is a minimal implementation that only do jsx to html string conversion.
export type HTMLTag = keyof HTMLElementTagNameMap
-export type ProprsWithChildren
= P & { children?: Child | Child[] }
+export type ProprsWithChildren
= P & { children?: Child | Child[], ref?: Ref }
export type Component = (props: ProprsWithChildren
) => VNode
@@ -16,9 +17,10 @@ export type DeepOptionalProps = {
export type InferElement = HTMLElementTagNameMap[T]
export interface VNode {
- type: HTMLTag | Component
+ type: HTMLTag | Component
| 'svg'
props: ProprsWithChildren
children: Child[]
+ __id__?: string
}
export type JSXElement = E extends HTMLTag ? VNode>>
@@ -43,16 +45,30 @@ export function h(
export const Fragment = Symbol('Fragment') as unknown as Component
export type FragmentType = typeof Fragment
-function normalizeKey(key: string): string {
+function normalizeKey(key: string, isSvg: boolean): string {
+ if (isSvg) {
+ const svgSpecialCases: Record = {
+ className: 'class',
+ htmlFor: 'for',
+ viewBox: 'viewBox',
+ fillRule: 'fill-rule',
+ clipRule: 'clip-rule',
+ strokeWidth: 'stroke-width',
+ strokeLinecap: 'stroke-linecap',
+ strokeLinejoin: 'stroke-linejoin',
+ strokeDasharray: 'stroke-dasharray',
+ strokeDashoffset: 'stroke-dashoffset'
+ }
+ return svgSpecialCases[key] || key
+ }
const specialCases: Record = {
className: 'class',
htmlFor: 'for'
}
-
return specialCases[key] || key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
}
-function renderProps(props: ProprsWithChildren>): string {
+function renderProps(props: ProprsWithChildren>, isSvg: boolean): string {
if (!props) { return '' }
return Object.entries(props)
.filter(([key]) => key !== 'children')
@@ -61,15 +77,15 @@ function renderProps(props: ProprsWithChildren>): string {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const style = Object.entries(value)
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- .map(([k, v]) => `${normalizeKey(k)}:${v}`)
+ .map(([k, v]) => `${normalizeKey(k, isSvg)}:${v}`)
.join(';')
return `style="${style}"`
}
if (typeof value === 'boolean' && value) {
- return normalizeKey(key)
+ return normalizeKey(key, isSvg)
}
if (typeof value === 'string' || typeof value === 'number') {
- return `${normalizeKey(key)}="${value}"`
+ return `${normalizeKey(key, isSvg)}="${value}"`
}
return ''
})
@@ -77,7 +93,31 @@ function renderProps(props: ProprsWithChildren>): string {
.join(' ')
}
-export function renderToString(node: Child): string {
+const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
+const SVG_TAGS = new Set([
+ 'svg',
+ 'path',
+ 'rect',
+ 'circle',
+ 'line',
+ 'g',
+ 'defs',
+ 'pattern',
+ 'mask',
+ 'use',
+ 'polyline',
+ 'polygon',
+ 'text',
+ 'tspan'
+])
+
+export function renderToString(node: VNode): string {
+ const { vnode } = processVNode(node)
+
+ return processNodeToStr(vnode)
+}
+
+export function processNodeToStr(node: Child): string {
if (node == null || typeof node === 'boolean') {
return ''
}
@@ -86,18 +126,22 @@ export function renderToString(node: Child): string {
return String(node)
}
- const { type, props, children } = node as VNode
+ const { type, props, children, __id__ } = node as VNode
+
+ const refAttr = __id__ ? `data-ref="${__id__}"` : ''
if (type === Fragment) {
- return children.map(renderToString).join('')
+ return children.map(processNodeToStr).join('')
}
if (typeof type === 'function') {
- return renderToString(type(props))
+ return processNodeToStr(type(props))
}
- const propsString = renderProps(props)
- const childrenString = children.map(renderToString).join('')
+ const isSvg = typeof type === 'string' && SVG_TAGS.has(type)
+
+ const propsString = renderProps(props, isSvg)
+ const childrenString = children.map(processNodeToStr).join('')
// Self-closing tags
const voidElements = new Set([
@@ -116,9 +160,55 @@ export function renderToString(node: Child): string {
'track',
'wbr'
])
+ if (isSvg && type === 'svg') {
+ return `${childrenString} `
+ }
+
if (voidElements.has(type)) {
- return `<${type}${propsString ? ' ' + propsString : ''}/>`
+ return `<${type}${propsString ? ' ' + propsString : ''} ${refAttr}/>`
+ }
+
+ return `<${type}${propsString ? ' ' + propsString : ''} ${refAttr}>${childrenString}${type}>`
+}
+
+export interface RefObject {
+ current: T | null
+}
+
+export type RefCallback = (instance: T | null) => void
+export type Ref = RefObject | RefCallback
+
+export function useRef(initialValue: T | null = null): RefObject {
+ return { current: initialValue }
+}
+
+export function processVNode(rootNode: VNode) {
+ const refMap: Record> = {}
+ let refId = 0
+
+ function processNode(node: VNode): VNode {
+ if (typeof node.type === 'function') {
+ const result = node.type(node.props)
+ return processNode(result)
+ }
+
+ const processedNode = { ...node }
+
+ if (node.props?.ref) {
+ processedNode.__id__ = `ref_${refId++}`
+ refMap[processedNode.__id__] = node.props.ref
+ }
+
+ processedNode.children = node.children.map((child) => {
+ if (child && typeof child === 'object' && 'type' in child) {
+ return processNode(child as VNode)
+ }
+ return child
+ })
+
+ return processedNode
}
- return `<${type}${propsString ? ' ' + propsString : ''}>${childrenString}${type}>`
+ const processedVNode = processNode(rootNode)
+ return { vnode: processedVNode, refMap }
}
diff --git a/scripts/hydrate.ts b/scripts/hydrate.ts
new file mode 100644
index 0000000..101ceb8
--- /dev/null
+++ b/scripts/hydrate.ts
@@ -0,0 +1,7 @@
+import { type VNode, processVNode } from './h'
+
+export function hydrate(rootNode: VNode) {
+ const { refMap } = processVNode(rootNode)
+
+ return { refMap }
+}
diff --git a/scripts/render.tsx b/scripts/render.tsx
index c930622..3bd5e68 100644
--- a/scripts/render.tsx
+++ b/scripts/render.tsx
@@ -6,7 +6,8 @@ import yaml from 'js-yaml'
import markdownit from 'markdown-it'
import path from 'path'
import type { Component } from './h'
-import { Fragment, h, renderToString } from './h'
+import { Fragment, h, renderToString, useRef } from './h'
+import { hydrate } from './hydrate'
import type { Theme } from './theme'
///
@@ -30,11 +31,11 @@ const Icons = {
),
@@ -43,14 +44,14 @@ const Icons = {
),
@@ -59,17 +60,17 @@ const Icons = {
),
@@ -107,7 +108,7 @@ const data = yaml.load(fs.readFileSync(path.join(Dirs.docs, 'index.yaml'), 'utf8
const pages = Object.entries(data)
const commonCSS = minifyCSS(fs.readFileSync(path.join(Dirs.script, 'style.css'), 'utf8'))
-const coomonScript = minifyJS(fs.readFileSync(path.join(Dirs.script, 'theme.ts'), 'utf8'))
+const commonScript = minifyJS(fs.readFileSync(path.join(Dirs.script, 'theme.ts'), 'utf8'))
function createTag(tag: T, value: DocTagValue): FormattedDocTag {
return { tag, value }
@@ -259,6 +260,8 @@ function Menu() {
structure.push(root)
}
+ const ref = useRef()
+
return (