port ujsx from Deno-only to cross-platform (Node/Bun/Deno)
Add npm project configuration (package.json, tsconfig.json, tsup, vitest) matching the taskgraph_ts conventions. All source imports changed from .ts to .js extensions for Node16 module resolution. Tests migrated from Deno.test to vitest. Fixed strict type errors (noUncheckedIndexedAccess). Preserved deno.json with sloppy-imports for dual Deno/Node compatibility. Subpath exports: schema, h, reactive, context, events, pointer, host, transform, jsx-runtime — plus barrel export at root. Build: ESM + CJS dual output via tsup. 22 tests passing.
This commit is contained in:
54
src/core/context.ts
Normal file
54
src/core/context.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { signal, effect, batch, type Signal, type ReadonlySignal } from "@preact/signals-core";
|
||||
|
||||
export type Density = "full" | "compact" | "minimal";
|
||||
|
||||
export interface ContextValue {
|
||||
density: Density;
|
||||
target: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const defaultContext: ContextValue = {
|
||||
density: "full",
|
||||
target: "markdown",
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
export class Context {
|
||||
private value: Signal<ContextValue>;
|
||||
|
||||
constructor(initial?: Partial<ContextValue>) {
|
||||
this.value = signal<ContextValue>({ ...defaultContext, ...initial });
|
||||
}
|
||||
|
||||
get(): ContextValue {
|
||||
return this.value.value;
|
||||
}
|
||||
|
||||
get signal(): ReadonlySignal<ContextValue> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set(partial: Partial<ContextValue>): void {
|
||||
batch(() => {
|
||||
this.value.value = { ...this.value.value, ...partial };
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(fn: (value: ContextValue) => void): () => void {
|
||||
return effect(() => {
|
||||
fn(this.value.value);
|
||||
});
|
||||
}
|
||||
|
||||
fork(overrides: Partial<ContextValue>): Context {
|
||||
const forked = new Context({ ...this.value.value, ...overrides });
|
||||
return forked;
|
||||
}
|
||||
}
|
||||
|
||||
export type Direction = "ujsx→mdast" | "mdast→ujsx" | "ujsx→jpath" | "jpath→ujsx" | "ujsx→hast" | "hast→ujsx";
|
||||
|
||||
export interface RenderContext extends ContextValue {
|
||||
direction: Direction;
|
||||
}
|
||||
51
src/core/events.ts
Normal file
51
src/core/events.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface EventEnvelope<TType extends string = string, TPayload = unknown> {
|
||||
readonly type: TType;
|
||||
readonly id: string;
|
||||
readonly payload: TPayload;
|
||||
}
|
||||
|
||||
export interface PubSubLike<TEventMap extends Record<string, unknown> = Record<string, unknown>> {
|
||||
publish<TType extends Extract<keyof TEventMap, string>>(
|
||||
type: TType,
|
||||
id: string,
|
||||
payload: TEventMap[TType],
|
||||
): void;
|
||||
subscribe<TType extends Extract<keyof TEventMap, string>>(
|
||||
type: TType,
|
||||
id: string,
|
||||
): AsyncIterable<EventEnvelope<TType, TEventMap[TType]>>;
|
||||
}
|
||||
|
||||
export type UjsxEventMap = {
|
||||
"root.render": { childCount: number };
|
||||
"root.unmount": Record<string, unknown>;
|
||||
"instance.create": { kind: "text" | "element"; tag?: string; value?: string; props?: Record<string, unknown> };
|
||||
"component.invoke": { type: string };
|
||||
"type.call": { objectName: string; methodName: string; args: unknown[] };
|
||||
"transform.apply": { ruleName: string; direction: string };
|
||||
};
|
||||
|
||||
export type UjsxEnvelope<TType extends keyof UjsxEventMap = keyof UjsxEventMap> = EventEnvelope<
|
||||
TType,
|
||||
UjsxEventMap[TType]
|
||||
>;
|
||||
|
||||
export function createPubSubEmitter<TEventMap extends Record<string, unknown>>(
|
||||
pubsub: PubSubLike<TEventMap>,
|
||||
) {
|
||||
return function emit<TType extends Extract<keyof TEventMap, string>>(
|
||||
type: TType,
|
||||
id: string,
|
||||
payload: TEventMap[TType],
|
||||
): void {
|
||||
pubsub.publish(type, id, payload);
|
||||
};
|
||||
}
|
||||
|
||||
export function proxyEventEmitter(pubsub: PubSubLike<UjsxEventMap>) {
|
||||
return {
|
||||
onTypeCall(objectName: string, methodName: string, args: unknown[]): void {
|
||||
pubsub.publish("type.call", `call_${Date.now()}`, { objectName, methodName, args });
|
||||
},
|
||||
};
|
||||
}
|
||||
49
src/core/h.ts
Normal file
49
src/core/h.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { UNode, UElement, URoot, UType, UComponent, UniversalProps } from "./schema.js";
|
||||
|
||||
let _idCounter = 0;
|
||||
|
||||
export function h(type: UType, props?: UniversalProps | null, ...children: UNode[]): UElement | URoot {
|
||||
const resolvedProps: UniversalProps = props ? { ...props } : {};
|
||||
const flatChildren = children.flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[];
|
||||
|
||||
if (type === "root") {
|
||||
return {
|
||||
type: "root",
|
||||
props: resolvedProps,
|
||||
children: flatChildren,
|
||||
} as URoot;
|
||||
}
|
||||
|
||||
return {
|
||||
type: type as string,
|
||||
props: resolvedProps,
|
||||
children: flatChildren,
|
||||
} as UElement;
|
||||
}
|
||||
|
||||
export function createRoot(id: string | undefined, ...children: UNode[]): URoot {
|
||||
return {
|
||||
type: "root",
|
||||
props: { id: id ?? `root_${++_idCounter}` },
|
||||
children: children.flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[],
|
||||
};
|
||||
}
|
||||
|
||||
export function createComponent<P extends UniversalProps>(
|
||||
name: string,
|
||||
render: (props: P) => UNode,
|
||||
targets?: string[],
|
||||
): UComponent<P> {
|
||||
const c = render as unknown as UComponent<P>;
|
||||
c.displayName = name;
|
||||
c.targets = targets;
|
||||
return c;
|
||||
}
|
||||
|
||||
export function Fragment(props: { children?: UNode[] }): UNode[] {
|
||||
return ((props.children ?? []) as UNode[]).flat(Infinity as 1).filter((c: UNode) => c != null && c !== false) as UNode[];
|
||||
}
|
||||
|
||||
export const jsx = h;
|
||||
export const jsxs = h;
|
||||
export const jsxDEV = h;
|
||||
2
src/core/jsx-runtime.ts
Normal file
2
src/core/jsx-runtime.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { jsx, jsxs, jsxDEV, Fragment } from "./h.js";
|
||||
export type {} from "./schema.js";
|
||||
70
src/core/pointer.ts
Normal file
70
src/core/pointer.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { signal, type Signal, type ReadonlySignal } from "@preact/signals-core";
|
||||
import type { UNode, UElement } from "./schema.js";
|
||||
import { isUElement } from "./schema.js";
|
||||
|
||||
export class ValuePointer<T> {
|
||||
private _signal: Signal<T>;
|
||||
private _path: string[];
|
||||
|
||||
constructor(initial: T, path: string[] = []) {
|
||||
this._signal = signal(initial);
|
||||
this._path = path;
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._signal.value;
|
||||
}
|
||||
|
||||
set value(v: T) {
|
||||
this._signal.value = v;
|
||||
}
|
||||
|
||||
get reactive(): ReadonlySignal<T> {
|
||||
return this._signal;
|
||||
}
|
||||
|
||||
get path(): string[] {
|
||||
return this._path;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectNode(root: UNode, path: string[]): UNode | undefined {
|
||||
let current: UNode | undefined = root;
|
||||
for (const segment of path) {
|
||||
if (current === undefined || !isUElement(current)) return undefined;
|
||||
const el = current as UElement;
|
||||
const index = parseInt(segment, 10);
|
||||
if (!isNaN(index) && index >= 0 && index < el.children.length) {
|
||||
current = el.children[index];
|
||||
} else {
|
||||
const propVal = el.props[segment];
|
||||
if (propVal !== undefined && typeof propVal === "object" && propVal !== null) {
|
||||
current = propVal as UNode;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export function setNode(root: UNode, path: string[], value: UNode): UNode {
|
||||
if (path.length === 0) return value;
|
||||
|
||||
const [head, ...tail] = path;
|
||||
if (!isUElement(root)) return root;
|
||||
|
||||
const rootEl = root as UElement;
|
||||
const index = parseInt(head!, 10);
|
||||
|
||||
if (!isNaN(index)) {
|
||||
const children = [...rootEl.children];
|
||||
if (index >= 0 && index < children.length) {
|
||||
children[index] = setNode(children[index]!, tail, value);
|
||||
}
|
||||
return { ...rootEl, children } as UElement;
|
||||
}
|
||||
|
||||
const props = { ...rootEl.props, [head as string]: value };
|
||||
return { ...rootEl, props } as UElement;
|
||||
}
|
||||
93
src/core/reactive.ts
Normal file
93
src/core/reactive.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { signal, computed, effect, batch, type Signal, type ReadonlySignal } from "@preact/signals-core";
|
||||
import type { UNode, UElement, UniversalProps, UComponent } from "./schema.js";
|
||||
|
||||
export interface ReactiveNode {
|
||||
readonly type: string;
|
||||
readonly signal: ReadonlySignal<UNode>;
|
||||
readonly dispose: () => void;
|
||||
}
|
||||
|
||||
export function reactiveComponent<P extends UniversalProps>(
|
||||
component: UComponent<P>,
|
||||
propsSignal: Signal<P>,
|
||||
): ReactiveNode {
|
||||
const nodeSignal = computed(() => {
|
||||
const props = propsSignal.value;
|
||||
return component(props);
|
||||
});
|
||||
|
||||
return {
|
||||
get type() {
|
||||
return component.displayName ?? "anonymous";
|
||||
},
|
||||
signal: nodeSignal,
|
||||
dispose: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
export function reactiveElement(
|
||||
type: string,
|
||||
propsSignal: Signal<UniversalProps>,
|
||||
childrenSignals: ReadonlySignal<UNode>[],
|
||||
): ReactiveNode {
|
||||
const nodeSignal = computed(() => {
|
||||
const children = childrenSignals.map((s) => s.value);
|
||||
return {
|
||||
type,
|
||||
props: propsSignal.value,
|
||||
children,
|
||||
} as UElement;
|
||||
});
|
||||
|
||||
return {
|
||||
type,
|
||||
signal: nodeSignal,
|
||||
dispose: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
export class ReactiveRoot {
|
||||
private root: Signal<UNode>;
|
||||
private renderDisposer: (() => void) | null = null;
|
||||
|
||||
constructor(initial: UNode) {
|
||||
this.root = signal(initial);
|
||||
}
|
||||
|
||||
get value(): ReadonlySignal<UNode> {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
update(fn: (current: UNode) => UNode): void {
|
||||
batch(() => {
|
||||
this.root.value = fn(this.root.value);
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(listener: (node: UNode) => void): () => void {
|
||||
return effect(() => {
|
||||
listener(this.root.value);
|
||||
});
|
||||
}
|
||||
|
||||
render(emit: (event: { type: string; id: string; payload: unknown }) => void): () => void {
|
||||
this.renderDisposer = effect(() => {
|
||||
const node = this.root.value;
|
||||
emit({
|
||||
type: "root.render",
|
||||
id: `root_${Date.now()}`,
|
||||
payload: node,
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
if (this.renderDisposer) {
|
||||
this.renderDisposer();
|
||||
this.renderDisposer = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { signal, computed, effect, batch };
|
||||
|
||||
export type { Signal, ReadonlySignal };
|
||||
79
src/core/schema.ts
Normal file
79
src/core/schema.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Type } from "@alkdev/typebox";
|
||||
|
||||
export const UJSX = Type.Module({
|
||||
UPrimitive: Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]),
|
||||
|
||||
PropValue: Type.Union([
|
||||
Type.String(),
|
||||
Type.Number(),
|
||||
Type.Boolean(),
|
||||
Type.Null(),
|
||||
Type.Array(Type.Unknown()),
|
||||
Type.Ref("UNode"),
|
||||
Type.Record(Type.String(), Type.Unknown()),
|
||||
Type.Function([...Type.Rest(Type.Array(Type.Unknown()))], Type.Unknown()),
|
||||
]),
|
||||
|
||||
UniversalProps: Type.Object(
|
||||
{},
|
||||
{ additionalProperties: Type.Union([Type.Ref("PropValue"), Type.Undefined()]) },
|
||||
),
|
||||
|
||||
UElement: Type.Object({
|
||||
type: Type.String(),
|
||||
props: Type.Ref("UniversalProps"),
|
||||
children: Type.Array(Type.Ref("UNode")),
|
||||
}),
|
||||
|
||||
URoot: Type.Object({
|
||||
type: Type.Literal("root"),
|
||||
props: Type.Ref("UniversalProps"),
|
||||
children: Type.Array(Type.Ref("UNode")),
|
||||
}),
|
||||
|
||||
UNode: Type.Union([Type.Ref("UPrimitive"), Type.Ref("UElement"), Type.Ref("URoot")]),
|
||||
});
|
||||
|
||||
// TypeScript types — the Module is for runtime validation;
|
||||
// TS types are defined directly for clean inference.
|
||||
// ComponentFn types in the schema are runtime-only (not serializable).
|
||||
|
||||
export type UPrimitive = string | number | boolean | null;
|
||||
export type PropValue = string | number | boolean | null | unknown[] | UNode | Record<string, unknown> | ((...args: unknown[]) => unknown);
|
||||
export type UniversalProps = Record<string, PropValue | undefined>;
|
||||
export type UElement = {
|
||||
type: string;
|
||||
props: UniversalProps;
|
||||
children: UNode[];
|
||||
};
|
||||
export type URoot = {
|
||||
type: "root";
|
||||
props: UniversalProps;
|
||||
children: UNode[];
|
||||
};
|
||||
export type UNode = UPrimitive | UElement | URoot;
|
||||
|
||||
export type ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode;
|
||||
export type UType = string | ComponentFn;
|
||||
|
||||
export interface UComponent<P extends UniversalProps = UniversalProps> {
|
||||
(props: P & { children?: UNode[] }): UNode;
|
||||
displayName?: string;
|
||||
targets?: string[];
|
||||
}
|
||||
|
||||
export function isUElement(node: UNode): node is UElement {
|
||||
return typeof node === "object" && node !== null && "type" in node && "props" in node && "children" in node && node.type !== "root";
|
||||
}
|
||||
|
||||
export function isURoot(node: UNode): node is URoot {
|
||||
return typeof node === "object" && node !== null && "type" in node && node.type === "root";
|
||||
}
|
||||
|
||||
export function isUPrimitive(node: UNode): node is UPrimitive {
|
||||
return typeof node === "string" || typeof node === "number" || typeof node === "boolean" || node === null;
|
||||
}
|
||||
|
||||
// Runtime validation using TypeBox schemas
|
||||
// Use UJSX.Import("UElement") etc. at call sites with Value.Check
|
||||
export { UJSX as schema };
|
||||
110
src/host/config.ts
Normal file
110
src/host/config.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { UNode, UElement, URoot, ComponentFn, UComponent } from "../core/schema.js";
|
||||
import { isURoot, isUPrimitive } from "../core/schema.js";
|
||||
import { Context } from "../core/context.js";
|
||||
|
||||
export interface HostConfig<TTag extends string, Instance, RootCtx> {
|
||||
name: string;
|
||||
createRootContext(container: unknown, options?: Record<string, unknown>, context?: Context): RootCtx;
|
||||
finalizeRoot?(ctx: RootCtx): void;
|
||||
createInstance(tag: TTag, props: Record<string, unknown>, ctx: RootCtx, parent?: Instance): Instance;
|
||||
createTextInstance(text: string, ctx: RootCtx, parent?: Instance): Instance;
|
||||
appendChild(parent: Instance, child: Instance, ctx: RootCtx): void;
|
||||
insertBefore?(parent: Instance, child: Instance, before: Instance, ctx: RootCtx): void;
|
||||
removeChild?(parent: Instance, child: Instance, ctx: RootCtx): void;
|
||||
prepareUpdate?(
|
||||
instance: Instance,
|
||||
tag: TTag,
|
||||
prevProps: Record<string, unknown>,
|
||||
nextProps: Record<string, unknown>,
|
||||
ctx: RootCtx,
|
||||
): unknown | null;
|
||||
commitUpdate?(
|
||||
instance: Instance,
|
||||
payload: unknown,
|
||||
tag: TTag,
|
||||
prevProps: Record<string, unknown>,
|
||||
nextProps: Record<string, unknown>,
|
||||
ctx: RootCtx,
|
||||
): void;
|
||||
emit?(type: string, id: string, payload: unknown): void;
|
||||
}
|
||||
|
||||
export interface Root<TTag extends string, Instance, RootCtx> {
|
||||
host: HostConfig<TTag, Instance, RootCtx>;
|
||||
ctx: RootCtx;
|
||||
container: unknown;
|
||||
context: Context;
|
||||
render(node: UNode): void;
|
||||
unmount(): void;
|
||||
}
|
||||
|
||||
export function createRoot<TTag extends string, Instance, RootCtx>(
|
||||
host: HostConfig<TTag, Instance, RootCtx>,
|
||||
container: unknown,
|
||||
options?: Record<string, unknown>,
|
||||
context?: Context,
|
||||
): Root<TTag, Instance, RootCtx> {
|
||||
const ctx = host.createRootContext(container, options, context);
|
||||
const rootContext = context ?? new Context();
|
||||
|
||||
function mountNode(node: UNode, parentInst?: Instance): Instance | undefined {
|
||||
if (node == null || node === false) return undefined;
|
||||
|
||||
if (isUPrimitive(node)) {
|
||||
const text = node === null ? "" : String(node);
|
||||
const t = host.createTextInstance(text, ctx, parentInst);
|
||||
if (parentInst) host.appendChild(parentInst, t, ctx);
|
||||
host.emit?.("instance.create", `text_${Date.now()}`, { kind: "text", value: text });
|
||||
return t;
|
||||
}
|
||||
|
||||
if (isURoot(node)) {
|
||||
for (const child of node.children) {
|
||||
mountNode(child, parentInst);
|
||||
}
|
||||
return parentInst;
|
||||
}
|
||||
|
||||
// node must be a UElement here
|
||||
const el = node as UElement;
|
||||
|
||||
// Function component — type is a function (runtime-only, before resolution)
|
||||
if (typeof el.type === "function") {
|
||||
const component = el.type as ComponentFn;
|
||||
const out = component({ ...el.props, children: el.children });
|
||||
host.emit?.("component.invoke", `comp_${Date.now()}`, { type: (component as unknown as UComponent).displayName ?? "anonymous" });
|
||||
return mountNode(out, parentInst);
|
||||
}
|
||||
|
||||
// Intrinsic element
|
||||
const tag = el.type as TTag;
|
||||
const inst = host.createInstance(tag, el.props as Record<string, unknown>, ctx, parentInst);
|
||||
host.emit?.("instance.create", `${tag}_${Date.now()}`, { kind: "element", tag, props: el.props });
|
||||
|
||||
for (const child of el.children) {
|
||||
mountNode(child, inst);
|
||||
}
|
||||
|
||||
if (parentInst) host.appendChild(parentInst, inst, ctx);
|
||||
return inst;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
ctx,
|
||||
container,
|
||||
context: rootContext,
|
||||
render(node: UNode) {
|
||||
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
|
||||
for (const child of payloadChildren) {
|
||||
mountNode(child, undefined);
|
||||
}
|
||||
host.finalizeRoot?.(ctx);
|
||||
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
|
||||
},
|
||||
unmount() {
|
||||
host.finalizeRoot?.(ctx);
|
||||
host.emit?.("root.unmount", `root_${Date.now()}`, {});
|
||||
},
|
||||
};
|
||||
}
|
||||
23
src/mod.ts
Normal file
23
src/mod.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export { UJSX as schema } from "./core/schema.js";
|
||||
export type { UPrimitive, PropValue, UniversalProps, UElement, URoot, UNode, ComponentFn, UType, UComponent } from "./core/schema.js";
|
||||
export { isUElement, isURoot, isUPrimitive } from "./core/schema.js";
|
||||
|
||||
export { h, createRoot, createComponent, Fragment, jsx, jsxs, jsxDEV } from "./core/h.js";
|
||||
|
||||
export { reactiveComponent, reactiveElement, ReactiveRoot } from "./core/reactive.js";
|
||||
export { signal, computed, effect, batch } from "./core/reactive.js";
|
||||
export type { Signal, ReadonlySignal, ReactiveNode } from "./core/reactive.js";
|
||||
|
||||
export { Context } from "./core/context.js";
|
||||
export type { Density, Direction, RenderContext } from "./core/context.js";
|
||||
|
||||
export { createPubSubEmitter, proxyEventEmitter } from "./core/events.js";
|
||||
export type { EventEnvelope, PubSubLike, UjsxEventMap, UjsxEnvelope } from "./core/events.js";
|
||||
|
||||
export { ValuePointer, selectNode, setNode } from "./core/pointer.js";
|
||||
|
||||
export { createRoot as createHostRoot } from "./host/config.js";
|
||||
export type { HostConfig, Root } from "./host/config.js";
|
||||
|
||||
export { TransformRegistry, childCtx, matchesSchema, ctx as transformCtx } from "./transform/registry.js";
|
||||
export type { TransformContext, TransformFn, TransformRule } from "./transform/registry.js";
|
||||
63
src/transform/registry.ts
Normal file
63
src/transform/registry.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { TSchema } from "@alkdev/typebox";
|
||||
import { Value } from "@alkdev/typebox/value";
|
||||
import type { Direction } from "../core/context.js";
|
||||
|
||||
export interface TransformContext<A = unknown> {
|
||||
ancestors: A[];
|
||||
index: number;
|
||||
direction: Direction;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type TransformFn<T, U, A> = (node: T, ctx: TransformContext<A>) => U;
|
||||
|
||||
export interface TransformRule<TInput, TOutput, A = unknown> {
|
||||
name: string;
|
||||
direction: Direction;
|
||||
schema?: TSchema;
|
||||
match: (node: TInput) => boolean;
|
||||
transform: (node: TInput, ctx: TransformContext<A>, next: TransformFn<TInput, TOutput, A>) => TOutput;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export class TransformRegistry<TInput, TOutput, A = unknown> {
|
||||
private rules: TransformRule<TInput, TOutput, A>[] = [];
|
||||
|
||||
register(rule: TransformRule<TInput, TOutput, A>): void {
|
||||
this.rules.push(rule);
|
||||
this.rules.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
}
|
||||
|
||||
transform(node: TInput, ctx: TransformContext<A>): TOutput {
|
||||
const rule = this.rules.find((r) => r.direction === ctx.direction && r.match(node));
|
||||
if (!rule) {
|
||||
throw new Error(`No transform rule found for node in direction '${ctx.direction}'`);
|
||||
}
|
||||
return rule.transform(node, ctx, (n, c) => this.transform(n, c));
|
||||
}
|
||||
|
||||
transformAll(nodes: TInput[], ctx: TransformContext<A>): TOutput[] {
|
||||
return nodes.map((node, i) => this.transform(node, { ...ctx, index: i, ancestors: ctx.ancestors }));
|
||||
}
|
||||
}
|
||||
|
||||
export function childCtx<A>(parent: A, ctx: TransformContext<A>, index: number): TransformContext<A> {
|
||||
return {
|
||||
...ctx,
|
||||
ancestors: [...ctx.ancestors, parent],
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
export function matchesSchema(schema: TSchema, node: unknown): boolean {
|
||||
return Value.Check(schema, node);
|
||||
}
|
||||
|
||||
export function ctx<A>(
|
||||
direction: Direction,
|
||||
ancestors: A[] = [],
|
||||
index = 0,
|
||||
metadata: Record<string, unknown> = {},
|
||||
): TransformContext<A> {
|
||||
return { ancestors, index, direction, metadata };
|
||||
}
|
||||
Reference in New Issue
Block a user