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}` +} + +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}` + 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 (