diff --git a/src/renderer.ts b/src/renderer.ts index 9c3404a..8b06267 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,393 +1,465 @@ -import type { MaybeRef, Reactive, Ref, ToRefs } from '@vue/reactivity' -import { WatchSource, computed, effect, isRef, reactive, ref, toRef, toRefs, toValue, unref } from '@vue/reactivity' -import { type } from 'arktype' -import patch from 'morphdom' -import type { Component } from './component' -import type { Flow } from './flow' -import type { BaseNode, DocumentNode, ElementNode, FragmentNode, ParseOptions, TextNode, ValueNode } from './parser' -import { NodeType, TextMode, parse } from './parser' -import { convertSnakeToCamel } from './utils' -import type { AnimationAttr, AnimationAttrSource } from './animation' -import { createAnimate, useAnimationAttr } from './animation' - -export type ComponentSpace = Map> -export const root: ComponentSpace = new Map() -export const flows = new Map() -export const textModes = new Map() - -export const textModeResolver = (name: string) => textModes.get(name) ?? TextMode.DATA - -// eslint-disable-next-line import/no-mutable-exports -export let globals: Context = reactive({}) -export function addGlobals(additional: Context) { - globals = reactive(Object.assign(toRefs(globals), toRefs(additional))) -} -export function getGlobals() { - return globals +import type { Reactive, ToRefs } from "@vue/reactivity"; +import { + computed, + effect, + isRef, + reactive, + toRefs, + toValue, +} from "@vue/reactivity"; +import patch from "morphdom"; +import type { Component } from "./component"; +import type { Flow } from "./flow"; +import type { + BaseNode, + ElementNode, + FragmentNode, + ParseOptions, + TextNode, + ValueNode, +} from "./parser"; +import { NodeType, TextMode, parse } from "./parser"; +import { convertSnakeToCamel } from "./utils"; +import type { AnimationAttr, AnimationAttrSource } from "./animation"; +import { createAnimate, useAnimationAttr } from "./animation"; + +export type ComponentSpace = Map>; +export const root: ComponentSpace = new Map(); +export const flows = new Map(); +export const textModes = new Map(); + +export const textModeResolver = (name: string) => + textModes.get(name) ?? TextMode.DATA; + +export type Context = Reactive>; +export type ContextHandler = { + context: Context; + addContext: (ctx: Context) => void; +}; +export function useContext(upstream?: Context): ContextHandler { + let context = upstream ?? reactive({}); + function addContext(ctx: Context) { + context = reactive(Object.assign(toRefs(context), toRefs(ctx))); + } + return { context, addContext }; } -export type Context = Reactive> -// eslint-disable-next-line import/no-mutable-exports -export let activeContext: Context = reactive({}) -export function setContext(ctx: Context) { - activeContext = ctx -} export function mergeContext(target: Context, from: Context): Context { - const intersection = Object.keys(from).filter(k => k in target) + const intersection = Object.keys(from).filter((k) => k in target); for (const key of intersection) { - target[key] = from[key] + target[key] = from[key]; } - const fromRefs = Object.fromEntries(Object.entries(toRefs(from)).filter(([k]) => !(k in target))) - const result = Object.assign(toRefs(target), fromRefs) - return reactive(result) -} -export function addActiveContext(additional: Context) { - activeContext = mergeContext(activeContext, additional) -} -export function getContext() { - return activeContext + const fromRefs = Object.fromEntries( + Object.entries(toRefs(from)).filter(([k]) => !(k in target)) + ); + const result = Object.assign(toRefs(target), fromRefs); + return reactive(result); } -export function runInContext(ctx: Context, fn: () => T): T { - const oldContext = activeContext - setContext(ctx) - const result = fn() - setContext(oldContext) - return result -} - -export type MaybeArray = T | T[] +export type MaybeArray = T | T[]; export function toArray(o: MaybeArray): T[] { - return Array.isArray(o) ? o : [o] + return Array.isArray(o) ? o : [o]; } export function unwrapRefs(o: Context): Record { - return Object.fromEntries(Object.entries(o).map(([k, v]) => { - if (typeof v === 'function') { - return [k, v] - } - return [k, toValue(v)] - })) + return Object.fromEntries( + Object.entries(o).map(([k, v]) => { + if (typeof v === "function") { + return [k, v]; + } + return [k, toValue(v)]; + }) + ); } -// export function resolve(o: Record): ToRefs> { -// return Object.fromEntries(Object.entries(o).map(([k, v]: [string, MaybeRef]) => [k.replace(/^([:#@])/, ''), ref(v.value ?? v)])) -// } - -export type Processor = (source: string, context?: T) => unknown -export type ProcessorUpdater = (context: T) => T -export function createProcessor(o: T): [Processor, ProcessorUpdater] { - const [processor, update] = _createProcessor(o) + +export type Processor = ( + source: string, + context?: T +) => unknown; +export type ProcessorUpdater = (context: T) => T; +export function createProcessor( + o: T +): [Processor, ProcessorUpdater] { + const [processor, update] = _createProcessor(o); return [ (source) => { - if (typeof source === 'string') { - return processor(source, o) - } - else { - throw new TypeError('invalid source') + if (typeof source === "string") { + return processor(source, o); + } else { + throw new TypeError("invalid source"); } }, (context) => { - return update(context) + return update(context); }, - ] + ]; } -export function _createProcessor(o: T): [Processor, ProcessorUpdater] { - const context = o + +export function _createProcessor( + o: T +): [Processor, ProcessorUpdater] { + const context = o; function processor(source: string, ctx?: T) { // eslint-disable-next-line no-new-func - const adhoc = new Function(`return (function($__eich_ctx){with($__eich_ctx){return (${source});}});`)() as any + const adhoc = new Function( + `return (function($__eich_ctx){with($__eich_ctx){return (${source});}});` + )() as any; if (ctx == null && o == null) { - throw new TypeError('missing context') + throw new TypeError("missing context"); } - return adhoc(ctx ?? context) + return adhoc(ctx ?? context); } function update(ctx: T) { - return mergeContext(context, ctx) as T + return mergeContext(context, ctx) as T; } - return [processor, update] + return [processor, update]; } -export type AttrSource = string | ExprAttrSource | FlowAttrSource | EventAttrSource -export type ExprAttrSource = `:${string}` -export type FlowAttrSource = `#${string}` -export type EventAttrSource = `@${string}` - -export const FLOW = Symbol('flow') -export type FlowAttr = [typeof FLOW, FlowAttrSource] -export const EVENT = Symbol('event') -export type EventAttr = [typeof EVENT, EventAttrSource, (args: any[]) => void] -export type Attr = unknown | FlowAttr | EventAttr | AnimationAttr -export type Attrs = Record -export function useAttrs(attrSources: Record, context: Context, processor?: Processor): Attrs { - return Object.fromEntries(Object.entries(attrSources).map(([k, v]) => [ - convertSnakeToCamel((k.startsWith(':') || k.startsWith('#') || k.startsWith('@')) ? k.slice(1) : k), - useAttr(k, v, context, processor), - ])) as Attrs +export type AttrSource = + | string + | ExprAttrSource + | FlowAttrSource + | EventAttrSource; +export type ExprAttrSource = `:${string}`; +export type FlowAttrSource = `#${string}`; +export type EventAttrSource = `@${string}`; + +export const FLOW = Symbol("flow"); +export type FlowAttr = [typeof FLOW, FlowAttrSource]; +export const EVENT = Symbol("event"); +export type EventAttr = [typeof EVENT, EventAttrSource, (args: any[]) => void]; +export type Attr = unknown | FlowAttr | EventAttr | AnimationAttr; +export type Attrs = Record; +export function useAttrs( + attrSources: Record, + context: Context, + processor?: Processor +): Attrs { + return Object.fromEntries( + Object.entries(attrSources).map(([k, v]) => [ + convertSnakeToCamel( + k.startsWith(":") || k.startsWith("#") || k.startsWith("@") + ? k.slice(1) + : k + ), + useAttr(k, v, context, processor), + ]) + ) as Attrs; } -export function useAttr(key: string, source: string, context: Context, processor?: Processor) { - if (key.startsWith(':')) { - return useExprAttr(source as ExprAttrSource, context, processor) - } - else if (key.startsWith('#')) { - return useFlowAttr(key, source as FlowAttrSource) - } - else if (key.startsWith('@')) { - return useEventAttr(key, source as EventAttrSource, processor) - } - else if (key.startsWith('$')) { - return useAnimationAttr(key, source as AnimationAttrSource) - } - else { - return useStringAttr(source) +export function useAttr( + key: string, + source: string, + context: Context, + processor?: Processor +) { + if (key.startsWith(":")) { + return useExprAttr(source as ExprAttrSource, context, processor); + } else if (key.startsWith("#")) { + return useFlowAttr(key, source as FlowAttrSource); + } else if (key.startsWith("@")) { + return useEventAttr(key, source as EventAttrSource, processor); + } else if (key.startsWith("$")) { + return useAnimationAttr(key, source as AnimationAttrSource); + } else { + return useStringAttr(source); } } -export function useExprAttr(source: ExprAttrSource, context: Context, processor?: Processor) { - return computed(() => processor!(source, context)) +export function useExprAttr( + source: ExprAttrSource, + context: Context, + processor?: Processor +) { + return computed(() => processor!(source, context)); } export function useFlowAttr(key: string, source: FlowAttrSource) { - return [FLOW, source, key.slice(1)] + return [FLOW, source, key.slice(1)]; } -export function useEventAttr(key: string, source: EventAttrSource, processor?: Processor) { - const wrapped = `function(){ ${source} }` - const handler = processor!(wrapped) as (args: any[]) => void - return [EVENT, key.slice(1), handler] +export function useEventAttr( + key: string, + source: EventAttrSource, + processor?: Processor +) { + const wrapped = `function(){ ${source} }`; + const handler = processor!(wrapped) as (args: any[]) => void; + return [EVENT, key.slice(1), handler]; } export function useStringAttr(source: string) { - return computed(() => source) + return computed(() => source); } export function getCommonAttrs(attrs: Attrs) { - return Object.fromEntries(Object.entries(attrs).filter(([_, v]) => !(Array.isArray(v) && (v[0] === FLOW || v[0] === EVENT)))) + return Object.fromEntries( + Object.entries(attrs).filter( + ([_, v]) => !(Array.isArray(v) && (v[0] === FLOW || v[0] === EVENT)) + ) + ); } export function isExprAttr(attr: Attr) { - return !Array.isArray(attr) && isRef(attr) + return !Array.isArray(attr) && isRef(attr); } export function isFlowAttr(attr: Attr) { - return Array.isArray(attr) && attr[0] === FLOW + return Array.isArray(attr) && attr[0] === FLOW; } export function isEventAttr(attr: Attr) { - return Array.isArray(attr) && attr[0] === EVENT + return Array.isArray(attr) && attr[0] === EVENT; } export function createDelegate() { return (attrs: Attrs, node: Node) => { for (const [_, value] of Object.entries(attrs)) { - if ((value as EventAttr)[0] !== EVENT) - continue - const [_, event, handler] = value - node.addEventListener(event, (...args) => handler(args)) + if ((value as EventAttr)[0] !== EVENT) continue; + const [_, event, handler] = value; + node.addEventListener(event, (...args) => handler(args)); } - } + }; } -export function useEmit void>>(attrs: Attrs) { +export function useEmit void>>( + attrs: Attrs +) { return (event: keyof T, ...args: any[]) => { - const eventAttr = (attrs)[event] + const eventAttr = (attrs)[event]; if (!isEventAttr(eventAttr)) { - console.warn(`[sciux laplace] event ${String(event)} is not an event attribute`) - } - else { - const [_, __, handler] = eventAttr + console.warn( + `[sciux laplace] event ${String(event)} is not an event attribute` + ); + } else { + const [_, __, handler] = (eventAttr); if (handler) { - handler(args) - } - else { - console.warn(`[sciux laplace] event ${String(event)} not found`) + handler(args); + } else { + console.warn(`[sciux laplace] event ${String(event)} not found`); } } - } + }; } -export function renderComp(element: ElementNode, space: ComponentSpace) { - const comp = space.get(element.tag) +export function renderComp( + element: ElementNode, + space: ComponentSpace, + context: ContextHandler +) { + const comp = space.get(element.tag); if (!comp) { - console.warn(`[sciux laplace] component <${element.tag}> not found`) - return null + console.warn(`[sciux laplace] component <${element.tag}> not found`); + return null; } - return _renderComp(comp, element) + return _renderComp(comp, element, context); } -export function _renderComp>(comp: Component, element: ElementNode): Node | null { - addActiveContext(getGlobals()) - const [processor, update] - = element.processor && element.updater +export function _renderComp< + T extends string, + A extends Record +>( + comp: Component, + element: ElementNode, + context: ContextHandler +): Node | null { + const [processor, update] = + element.processor && element.updater ? [element.processor, element.updater] - : createProcessor(activeContext) - element.processor = processor - element.updater = update - const delegate = createDelegate() - const animate = createAnimate(getContext(), element) - const attributes = useAttrs( - Object.fromEntries(element.attributes.map(({ name, value }) => [name, value])), - unwrapRefs(activeContext), - processor, - ) + : createProcessor(context.context); - const { name, attrs: _typedAttrs, setup, provides, globals: compGlobals, defaults, space } = comp(attributes as ToRefs, activeContext) + element.processor = processor; + element.updater = update; + const delegate = createDelegate(); + const animate = createAnimate(context.context, element); + const attributes = useAttrs( + Object.fromEntries( + element.attributes.map(({ name, value }) => [name, value]) + ), + unwrapRefs(context.context), + processor + ); + + const { + name, + attrs: _typedAttrs, + setup, + provides, + globals: compGlobals, + defaults, + space, + } = comp(attributes as ToRefs, context.context); for (const [key, value] of Object.entries(defaults ?? {})) { if (!(key in attributes)) { - attributes[key] = computed(() => value) + attributes[key] = computed(() => value); } } - addGlobals(reactive(compGlobals ?? {})) + context.addContext(reactive(compGlobals ?? {})); if (name !== element.tag) { - throw new Error(`[sciux laplace] component <${element.tag}> does not match <${name}>`) + throw new Error( + `[sciux laplace] component <${element.tag}> does not match <${name}>` + ); } - const oldContext = activeContext - - return runInContext(mergeContext( - mergeContext(getContext(), getGlobals()), - reactive(provides ?? {}), - ), () => { - if (!setup) - return null - const [childrenProcessor] = createProcessor(activeContext) - const node = setup( - () => renderRoots(element.children, childrenProcessor, space), - ) - animate(attributes, node) - delegate(attributes, node) - const roots = renderRoots(element.children, childrenProcessor, space) + return (() => { + if (!setup) return null; + const [childrenProcessor] = createProcessor(context.context); + const node = setup(() => + renderRoots(element.children, childrenProcessor, space, context) + ); + animate(attributes, node); + delegate(attributes, node); + const roots = renderRoots( + element.children, + childrenProcessor, + space, + context + ); effect(() => { - const newNode = setup( - () => roots, - ) + const newNode = setup(() => roots); - patch(node, newNode) - }) - activeContext = oldContext + patch(node, newNode); + }); - return node - }) + return node; + })(); } -export function renderValue(element: ValueNode) { - addActiveContext(getGlobals()) - const interalContext = getContext() - const [processor, update] - = element.processor && element.updater +export function renderValue(element: ValueNode, context: ContextHandler) { + const [processor, update] = + element.processor && element.updater ? [element.processor, element.updater] - : createProcessor(interalContext) - element.processor = processor - element.updater = update - const node = document.createTextNode(((processor(element.value) as any).toString())) + : createProcessor(context.context); + + element.processor = processor; + element.updater = update; + const node = document.createTextNode( + (processor(element.value) as any).toString() + ); + effect(() => { - node.textContent = (processor(element.value) as any).toString() - }) - return node + node.textContent = (processor(element.value) as any).toString(); + }); + + return node; } export function renderText(text: string) { - return document.createTextNode(text) + return document.createTextNode(text); } export function renderNode( node: BaseNode, - processor: Processor = createProcessor(activeContext)[0], space: ComponentSpace, + context: ContextHandler, + processor?: Processor ): Node | Node[] { - node.domNode = void 0 - if (node.type === NodeType.TEXT) { - const domNode = renderText((node as TextNode).content) - node.domNode = domNode - return domNode - } - else if (node.type === NodeType.VALUE) { - const domNode = renderValue(node as ValueNode) - node.domNode = domNode - return domNode - } - else if (node.type === NodeType.ELEMENT) { - const elementNode = node as ElementNode - const originalAttrs = elementNode.attributes - let flowAttrs = elementNode.attributes.filter(attr => attr.name.startsWith('#')) + processor = processor ?? createProcessor(context.context)[0]; - let result: Node | Node[] | null = null + node.domNode = void 0; + if (node.type === NodeType.TEXT) { + const domNode = renderText((node as TextNode).content); + node.domNode = domNode; + return domNode; + } else if (node.type === NodeType.VALUE) { + const domNode = renderValue(node as ValueNode, context); + node.domNode = domNode; + return domNode; + } else if (node.type === NodeType.ELEMENT) { + const elementNode = node as ElementNode; + const originalAttrs = elementNode.attributes; + let flowAttrs = elementNode.attributes.filter((attr) => + attr.name.startsWith("#") + ); + + let result: Node | Node[] | null = null; // Pre-flow if (flowAttrs.length <= 0) { - result = renderComp(elementNode, space) - } - else { - const { name, value } = flowAttrs[0] - const flow = flows.get(name.slice(1))?.(processor) - if (flow && flow.type === 'pre') { - flowAttrs = (node as ElementNode).attributes = (node as ElementNode).attributes.filter(attr => attr.name !== name) - - result = flow.flow(value, node, (node: BaseNode) => renderNode(node, processor, space)) + result = renderComp(elementNode, space, context); + } else { + const { name, value } = flowAttrs[0]; + const flow = flows.get(name.slice(1))?.(processor); + if (flow && flow.type === "pre") { + flowAttrs = (node as ElementNode).attributes = ( + node as ElementNode + ).attributes.filter((attr) => attr.name !== name); + + result = flow.flow(value, node, (node: BaseNode) => + renderNode(node, space, context, processor) + ); } } - if (!result) - result = renderComp(elementNode, space) + + if (!result) result = renderComp(elementNode, space, context); for (const attr of flowAttrs) { - const { name: nameSource, value } = attr - const [name, ...rest] = nameSource.split(':') - const flow = flows.get(name.slice(1))?.(processor, ...rest) - if (flow && flow.type === 'post') { - const nodes = toArray(result) + const { name: nameSource, value } = attr; + const [name, ...rest] = nameSource.split(":"); + const flow = flows.get(name.slice(1))?.(processor, ...rest); + if (flow && flow.type === "post") { + const nodes = toArray(result); for (const n of nodes) { - flow.flow(value, n!, node) + flow.flow(value, n!, node); } } - // } - (node as ElementNode).attributes = originalAttrs + (node as ElementNode).attributes = originalAttrs; - const domNode = result ?? [] + const domNode = result ?? []; if (node.domNode) { - patch(node.domNode, domNode as Node) - } - else { - node.domNode = domNode + patch(node.domNode, domNode as Node); + } else { + node.domNode = domNode; } - node.space = space - return domNode + node.space = space; + return domNode; + } else if (node.type === NodeType.COMMENT) { + return []; + } else if (node.type === NodeType.FRAGMENT) { + return (node as FragmentNode).children + .map((x) => renderNode(x, space, context, processor)) + .flatMap((x) => x); } - else if (node.type === NodeType.COMMENT) { - return [] - } - else if (node.type === NodeType.FRAGMENT) { - return (node as FragmentNode).children.map(x => renderNode(x, processor, space)).flatMap(x => x) - } - throw new Error('Unreachable') + throw new Error("Unreachable"); } -export function renderRoots(roots: BaseNode[], processor?: Processor, space: ComponentSpace = root) { - const nodes: Node[] = [] +export function renderRoots( + roots: BaseNode[], + processor?: Processor, + space: ComponentSpace = root, + upstreamContext?: Context +) { + const context = useContext(upstreamContext); + const nodes: Node[] = []; roots.forEach((root) => { - const result = renderNode(root, processor, space) + const result = renderNode(root, space, context, processor); if (Array.isArray(result)) { - nodes.push(...result) - } - else { - nodes.push(result) + nodes.push(...result); + } else { + nodes.push(result); } - }) - return nodes + }); + return nodes; } -export function createUpdater() { +export function createUpdater(context: ContextHandler) { return (node: BaseNode) => { - renderNode(node, node.processor, node.space ?? root) - } + renderNode(node, node.space ?? root, context, node.processor); + }; } -export function render(source: string, target?: Node, parseOptions: ParseOptions = {}) { +export function render( + source: string, + target?: Node, + parseOptions: ParseOptions = {} +) { const ast = parse(source, { resolver: textModeResolver, ...parseOptions, - }) + }); + + const context = useContext(); - const nodes = renderRoots(ast.children) + const nodes = renderRoots(ast.children, undefined, undefined, context); nodes.forEach((node) => { - if (node) - target?.appendChild(node) - }) + if (node) target?.appendChild(node); + }); - return [ast, createUpdater()] + return [ast, createUpdater(context)]; }