From 4b1015823122706aaa8d09b1ecf705c891af097d Mon Sep 17 00:00:00 2001
From: kanno <812137533@qq.com>
Date: Fri, 14 Feb 2025 14:54:55 +0800
Subject: [PATCH] feat: using jsx write docs
---
Makefile | 2 +-
scripts/h.ts | 124 +++++++++++
scripts/jsx-namespace.d.ts | 21 ++
scripts/render.ts | 393 --------------------------------
scripts/render.tsx | 444 +++++++++++++++++++++++++++++++++++++
scripts/serve.ts | 2 +-
tsconfig.json | 3 +
7 files changed, 594 insertions(+), 395 deletions(-)
create mode 100644 scripts/h.ts
create mode 100644 scripts/jsx-namespace.d.ts
delete mode 100644 scripts/render.ts
create mode 100644 scripts/render.tsx
diff --git a/Makefile b/Makefile
index e12cf66..e1a271d 100644
--- a/Makefile
+++ b/Makefile
@@ -39,4 +39,4 @@ dev-docs:
./node_modules/.bin/tsx scripts/serve.ts
build-docs:
- ./node_modules/.bin/tsx scripts/render.ts
+ ./node_modules/.bin/tsx scripts/render.tsx
diff --git a/scripts/h.ts b/scripts/h.ts
new file mode 100644
index 0000000..87e87de
--- /dev/null
+++ b/scripts/h.ts
@@ -0,0 +1,124 @@
+/* eslint-disable no-use-before-define */
+// preact is fine, but i won't need it for the project.
+
+export type HTMLTag = keyof HTMLElementTagNameMap
+
+export type ProprsWithChildren
= P & { children?: Child | Child[] }
+
+export type Component
= (props: ProprsWithChildren
) => VNode
+
+export type Child = string | number | boolean | null | undefined | VNode
+
+export type DeepOptionalProps = {
+ [K in keyof T]?: T[K] extends object ? DeepOptionalProps : T[K]
+}
+
+export type InferElement = HTMLElementTagNameMap[T]
+
+export interface VNode {
+ type: HTMLTag | Component
+ props: ProprsWithChildren
+ children: Child[]
+}
+
+export type JSXElement = E extends HTMLTag ? VNode>>
+ : E extends Component ? VNode
+ : never
+
+export function h(
+ type: T,
+ props: T extends FragmentType ? null
+ : T extends HTMLTag ? (DeepOptionalProps> | null)
+ : T extends Component ? P
+ : never,
+ ...children: Child[]
+): JSXElement {
+ return {
+ type,
+ props: props || null,
+ children: children.flat().filter(Boolean)
+ } as JSXElement
+}
+
+export const Fragment = Symbol('Fragment') as unknown as Component
+export type FragmentType = typeof Fragment
+
+function normalizeKey(key: string): string {
+ const specialCases: Record = {
+ className: 'class',
+ htmlFor: 'for'
+ }
+
+ return specialCases[key] || key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
+}
+
+function renderProps(props: ProprsWithChildren>): string {
+ if (!props) { return '' }
+ return Object.entries(props)
+ .filter(([key]) => key !== 'children')
+ .map(([key, value]) => {
+ if (key === 'style' && typeof value === 'object') {
+ // 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}`)
+ .join(';')
+ return `style="${style}"`
+ }
+ if (typeof value === 'boolean' && value) {
+ return normalizeKey(key)
+ }
+ if (typeof value === 'string' || typeof value === 'number') {
+ return `${normalizeKey(key)}="${value}"`
+ }
+ return ''
+ })
+ .filter(Boolean)
+ .join(' ')
+}
+
+export function renderToString(node: Child): string {
+ if (node == null || typeof node === 'boolean') {
+ return ''
+ }
+
+ if (typeof node === 'string' || typeof node === 'number') {
+ return String(node)
+ }
+
+ const { type, props, children } = node as VNode
+
+ if (type === Fragment) {
+ return children.map(renderToString).join('')
+ }
+
+ if (typeof type === 'function') {
+ return renderToString(type(props))
+ }
+
+ const propsString = renderProps(props)
+ const childrenString = children.map(renderToString).join('')
+
+ // Self-closing tags
+ const voidElements = new Set([
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'link',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr'
+ ])
+ if (voidElements.has(type)) {
+ return `<${type}${propsString ? ' ' + propsString : ''}/>`
+ }
+
+ return `<${type}${propsString ? ' ' + propsString : ''}>${childrenString}${type}>`
+}
diff --git a/scripts/jsx-namespace.d.ts b/scripts/jsx-namespace.d.ts
new file mode 100644
index 0000000..c68d113
--- /dev/null
+++ b/scripts/jsx-namespace.d.ts
@@ -0,0 +1,21 @@
+import { Child, DeepOptionalProps, InferElement, VNode } from './h'
+
+declare global {
+ namespace JSX {
+ type Element = VNode
+ type SVGTag = keyof SVGElementTagNameMap
+ export interface ElementChildrenAttribute {
+ children: Child | Child[]
+ }
+ export type IntrinsicElements = {
+ [K in ElementTag]: K extends keyof SVGElementTagNameMap ? DeepOptionalProps
+ : K extends 'meta' ? DeepOptionalProps
+ : DeepOptionalProps> | ElementChildrenAttribute
+ }
+ export type ElementChildrenAttribute = {
+ children: Any
+ }
+ }
+}
+
+export {}
diff --git a/scripts/render.ts b/scripts/render.ts
deleted file mode 100644
index d350c6a..0000000
--- a/scripts/render.ts
+++ /dev/null
@@ -1,393 +0,0 @@
-import esbuild from 'esbuild'
-import fs from 'fs'
-import fsp from 'fs/promises'
-import hljs from 'highlight.js'
-import yaml from 'js-yaml'
-import markdownit from 'markdown-it'
-import path from 'path'
-import type { Theme } from './theme'
-
-const md = markdownit({ html: true })
-
-const docsDir = path.join(__dirname, '..', 'docs')
-
-const devDir = path.join(__dirname, '..', 'dev')
-
-const destDir = path.join(__dirname, '..', 'display')
-
-const scriptDir = __dirname
-
-const target = ['chrome58', 'safari11', 'firefox57', 'edge16']
-
-type TagElement = 'p' | 'ul' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'pre' | `pre.${string}`
-
-type TagValue = T extends 'ul' ? string[] : string
-
-interface FormattedTag {
- tag: T
- value: TagValue
-}
-
-type AnyFormattedTag = FormattedTag
-
-interface RenderMetadata {
- title: string
- body: AnyFormattedTag[]
-}
-
-const data = yaml.load(fs.readFileSync(path.join(docsDir, 'index.yaml'), 'utf8')) as Record
-
-const pages = Object.entries(data)
-
-function createTag(tag: T, value: TagValue): FormattedTag {
- return { tag, value }
-}
-
-export interface Descriptor {
- kind: 'script' | 'style' | 'title'
- text: string
- attrs?: string[]
-}
-
-interface InjectHTMLTagOptions {
- html: string
- injectTo: 'body' | 'head'
- descriptors: Descriptor | Descriptor[]
-}
-
-// Refactor this function
-export function injectHTMLTag(options: InjectHTMLTagOptions) {
- const regExp = options.injectTo === 'head' ? /([ \t]*)<\/head>/i : /([ \t]*)<\/body>/i
- options.descriptors = Array.isArray(options.descriptors) ? options.descriptors : [options.descriptors]
- const descriptors = options.descriptors.map((d) => {
- if (d.attrs && d.attrs.length > 0) {
- return `<${d.kind} ${d.attrs.join(' ')}>${d.text}${d.kind}>`
- }
- return `<${d.kind}>${d.text}${d.kind}>`
- })
- return options.html.replace(regExp, (match) => `${descriptors.join('\n')}${match}`)
-}
-
-function minifyCSS(css: string) {
- return esbuild.transformSync(css, { target, loader: 'css', minify: true }).code
-}
-
-function minifyJS(js: string) {
- return esbuild.transformSync(js, { target, loader: 'ts', minify: true }).code
-}
-
-function buildAndMinifyJS(entry: string) {
- const r = esbuild.buildSync({
- bundle: true,
- format: 'iife',
- loader: {
- '.ts': 'ts'
- },
- define: {
- LIVE_RELOAD: 'false'
- },
- minify: true,
- write: false,
- entryPoints: [entry]
- })
- if (r.outputFiles.length) {
- return r.outputFiles[0].text
- }
- throw new Error('No output files')
-}
-
-const formatedPages = pages.reduce((acc, [page, pageData]) => {
- if (typeof pageData === 'string') {
- if (pageData.endsWith('.yaml')) {
- pageData = yaml.load(fs.readFileSync(path.join(docsDir, pageData), 'utf8')) as RenderMetadata
- }
- }
- if (typeof pageData === 'object') {
- pageData.body = pageData.body.map((sec) => {
- const tag = Object.keys(sec)[0]
- // @ts-expect-error safe
- return createTag(tag as TagElement, sec[tag] as TagValue)
- })
- }
- // @ts-expect-error safe
- acc.push([page, pageData])
- return acc
-}, [] as [string, RenderMetadata][])
-
-const hljsPath = path.dirname(require.resolve('highlight.js/package.json', { paths: [process.cwd()] }))
-
-// We use github highlighting style
-
-function pipeOriginalCSSIntoThemeSystem(css: string, theme: Theme) {
- let wrappered = ''
- if (theme === 'dark') {
- wrappered = `html[data-theme="dark"] { ${css} }\n`
- } else {
- wrappered = `html:not([data-theme="dark"]) { ${css} }\n`
- }
-
- return minifyCSS(wrappered)
-}
-
-const hljsGithubCSS = {
- light: pipeOriginalCSSIntoThemeSystem(fs.readFileSync(path.join(hljsPath, 'styles/github.css'), 'utf-8'), 'light'),
- dark: pipeOriginalCSSIntoThemeSystem(fs.readFileSync(path.join(hljsPath, 'styles/github-dark.css'), 'utf-8'), 'dark')
-}
-
-const commonCSS = minifyCSS(fs.readFileSync(path.join(scriptDir, 'style.css'), 'utf8'))
-
-const coomonScript = minifyJS(fs.readFileSync(path.join(scriptDir, 'theme.ts'), 'utf8'))
-
-const assert = {
- ul: (tag: FormattedTag): tag is FormattedTag<'ul'> => {
- return tag.tag === 'ul'
- },
- pre: (tag: FormattedTag): tag is FormattedTag<'pre' | `pre.${string}`> => {
- if (tag.tag.startsWith('pre')) { return true }
- return false
- },
- base: (tag: FormattedTag): tag is FormattedTag> => {
- if (tag.tag !== 'ul') { return true }
- return false
- }
-}
-
-function toID(text: string) {
- return text.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '')
-}
-
-function renderMainSection(page: string, pageData: RenderMetadata): string {
- const handler = (c: FormattedTag): string => {
- if (assert.ul(c)) {
- return `${c.value.map((li) => `- ${md.renderInline(li.trim())}
`).join('')}
`
- }
- if (assert.pre(c)) {
- if (c.tag.startsWith('pre.')) {
- const lang = c.tag.split('.')[1]
- return `${hljs.highlight(c.value.trim(), { language: lang }).value}
`
- }
- return `${md.render(c.value.trim())}
`
- }
- if (assert.base(c)) {
- // For heading metadata
- if (/^h[2-6]$/.test(c.tag)) {
- const slug = toID(c.value)
- return `<${c.tag} id="${slug}">#${
- md.renderInline(c.value.trim())
- }${c.tag}>`
- }
-
- return `<${c.tag}>${md.renderInline(c.value.trim())}${c.tag}>`
- }
- throw new Error('Unreachable')
- }
- return pageData.body
- .reduce((acc, cur) => (acc.push(handler(cur)), acc), [])
- .join('\n')
-}
-
-interface HeadingBase {
- value: string
- id: string
-}
-
-interface HeadingMetadata extends HeadingBase {
- h3s: HeadingBase[]
-}
-
-interface HeadingStruct {
- key: string
- title: string
- h2s: HeadingMetadata[]
-}
-
-function renderMenu(): string {
- const structure: HeadingStruct[] = []
- for (const [pageName, pageData] of formatedPages) {
- if (pageName === 'index') { continue }
- const h2s: HeadingMetadata[] = []
- const root = { key: pageName, title: pageData.title, h2s }
- let h3s: HeadingBase[] = []
-
- for (const c of pageData.body) {
- if (assert.base(c)) {
- if (c.tag === 'h2') {
- h3s = []
- h2s.push({ value: c.value, id: toID(c.value), h3s })
- } else if (c.tag === 'h3') {
- h3s.push({ value: c.value, id: toID(c.value) })
- }
- }
- }
- structure.push(root)
- }
-
- const navs: string[] = []
-
- navs.push('Home')
-
- for (const { key, title, h2s } of structure) {
- navs.push(`${title}`)
- for (const h2 of h2s) {
- navs.push(`${h2.value}`)
- if (h2.h3s.length > 0) {
- navs.push('')
- for (const h3 of h2.h3s) {
- navs.push(`- ${h3.value}
`)
- }
- navs.push('
')
- }
- }
- }
- return navs.join('')
-}
-
-const icons = {
- moon: `
-
-`.trim(),
- sun: `
-
-`.trim(),
- github: `
-
-`.trim(),
- menu: ``.trim()
-}
-
-function widget() {
- const html: string[] = []
- html.push('')
- return html
-}
-
-function buildExampleDisplay() {
- let html = fs.readFileSync(path.join(devDir, 'index.html'), 'utf8')
- html = html.replace(/`)
- html.push('