Merge remote-tracking branch 'x/interop-translator'

This commit is contained in:
Michael Rawlings 2023-07-24 12:06:16 -04:00
commit a39fc3ec12
1353 changed files with 45380 additions and 4 deletions

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm exec -- lint-staged
npm exec -- lint-staged && npm run build:types && npm run build:sizes

88
.sizes.json Normal file
View File

@ -0,0 +1,88 @@
{
"examples": {
"counter": "./packages/translator/src/__tests__/fixtures/basic-counter/template.marko",
"comments": "./packages/translator/src/__tests__/fixtures/basic-inert-collapsible-tree/template.marko"
},
"results": [
{
"name": "*",
"total": {
"min": 12704,
"gzip": 5444,
"brotli": 4959
}
},
{
"name": "counter",
"user": {
"min": 355,
"gzip": 280,
"brotli": 240
},
"runtime": {
"min": 2990,
"gzip": 1444,
"brotli": 1292
},
"total": {
"min": 3345,
"gzip": 1724,
"brotli": 1532
}
},
{
"name": "counter 💧",
"user": {
"min": 204,
"gzip": 180,
"brotli": 153
},
"runtime": {
"min": 2453,
"gzip": 1278,
"brotli": 1140
},
"total": {
"min": 2657,
"gzip": 1458,
"brotli": 1293
}
},
{
"name": "comments",
"user": {
"min": 1180,
"gzip": 695,
"brotli": 635
},
"runtime": {
"min": 6991,
"gzip": 3242,
"brotli": 2931
},
"total": {
"min": 8171,
"gzip": 3937,
"brotli": 3566
}
},
{
"name": "comments 💧",
"user": {
"min": 988,
"gzip": 591,
"brotli": 556
},
"runtime": {
"min": 7930,
"gzip": 3648,
"brotli": 3306
},
"total": {
"min": 8918,
"gzip": 4239,
"brotli": 3862
}
}
]
}

View File

@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE.

63
package-lock.json generated
View File

@ -2579,10 +2579,22 @@
"resolved": "packages/compiler",
"link": true
},
"node_modules/@marko/runtime-fluurt": {
"resolved": "packages/runtime",
"link": true
},
"node_modules/@marko/translator-default": {
"resolved": "packages/translator-default",
"link": true
},
"node_modules/@marko/translator-fluurt": {
"resolved": "packages/translator",
"link": true
},
"node_modules/@marko/translator-interop-class-tags": {
"resolved": "packages/translator-interop",
"link": true
},
"node_modules/@nicolo-ribaudo/chokidar-2": {
"version": "2.1.8-no-fsevents.3",
"dev": true,
@ -10160,7 +10172,6 @@
},
"node_modules/tslib": {
"version": "2.4.1",
"dev": true,
"license": "0BSD"
},
"node_modules/tsutils": {
@ -11059,6 +11070,21 @@
"version": "3.2.0",
"license": "Apache-2.0"
},
"packages/runtime": {
"version": "0.0.1",
"license": "MIT"
},
"packages/translator": {
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@marko/babel-utils": "^6.2.1",
"@marko/runtime-fluurt": "^0.0.1"
},
"peerDependencies": {
"@marko/compiler": "^5.23.0"
}
},
"packages/translator-default": {
"name": "@marko/translator-default",
"version": "5.29.2",
@ -11089,6 +11115,41 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/translator-interop": {
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "7.22.5",
"@marko/babel-utils": "^5.21.3",
"@marko/translator-default": "^5.26.4",
"@marko/translator-fluurt": "^0.0.1",
"tslib": "^2.3.1"
},
"peerDependencies": {
"@marko/compiler": "^5.23.0"
}
},
"packages/translator-interop/node_modules/@marko/babel-utils": {
"version": "5.22.1",
"resolved": "https://registry.npmjs.org/@marko/babel-utils/-/babel-utils-5.22.1.tgz",
"integrity": "sha512-IbJECsApQJoe4Ovo4e5i05W7sAtmhyfvyxp1o5s78YFgaOwSD6qiXVT3kiJj9PeZ5ioBD2390+kt/FFEm4+q2Q==",
"dependencies": {
"@babel/runtime": "^7.16.0",
"jsesc": "^3.0.2",
"relative-import-path": "^1.0.0"
}
},
"packages/translator-interop/node_modules/jsesc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"scripts/babel-register.js": {
"dev": true
}

View File

@ -6,6 +6,7 @@
],
"scripts": {
"build": "npm run build --ws && npm run build:types",
"build:sizes": "npm run build --ws && node -r ~ts scripts/sizes.ts",
"build:types": "npm run build:types --ws --if-present && tsc -b tsconfig.build.json",
"change": "changeset add",
"ci:report": "c8 report --reporter=text-lcov > coverage.lcov && codecov",

View File

@ -0,0 +1,26 @@
{
"name": "@marko/runtime-fluurt",
"version": "0.0.1",
"private": true,
"description": "Optimized runtime for Marko templates.",
"keywords": [
"fluurt",
"marko",
"runtime"
],
"homepage": "https://github.com/marko-js/x",
"bugs": "https://github.com/marko-js/x/issues",
"repository": {
"type": "git",
"url": "https://github.com/marko-js/x"
},
"license": "MIT",
"files": [
"dist",
"!**/meta.*.json",
"!**/*.tsbuildinfo"
],
"scripts": {
"build": "node -r ~ts ./scripts/bundle.ts"
}
}

View File

@ -0,0 +1,55 @@
import fs from "fs";
import path from "path";
import { build } from "esbuild";
const absWorkingDir = path.join(__dirname, "..");
Promise.all(
["dist/debug", "dist"].flatMap((env) =>
["dom", "html"].flatMap((name) => {
(["esm", "cjs"] as const).map(async (format) => {
const isProd = env === "dist";
const outdir = path.join(absWorkingDir, `${env}/${name}`);
const { metafile } = await build({
format,
outdir,
absWorkingDir,
bundle: true,
metafile: true,
sourcemap: true,
platform: "browser",
minifySyntax: isProd,
entryPoints: [`src/${name}/index.ts`],
define: { MARKO_DEBUG: String(!isProd) },
mangleProps: isProd ? /^___/ : undefined,
outExtension: { ".js": format === "esm" ? ".mjs" : ".js" },
});
await Promise.all([
fs.promises.writeFile(
`${outdir}/meta.${format}.json`,
JSON.stringify(metafile)
),
fs.promises.writeFile(
`${outdir}/package.json`,
JSON.stringify(
{
main: "index.js",
module: "index.mjs",
exports: {
".": {
import: "./index.mjs",
default: "./index.js",
},
},
types: path.relative(outdir, `dist/${name}/index.d.ts`),
},
null,
2
)
),
]);
});
})
)
);

Binary file not shown.

View File

@ -0,0 +1,26 @@
export let Context: Record<string, unknown> | null = null;
let usesContext = false;
export function pushContext(key: string, value: unknown) {
usesContext = true;
(Context = Object.create(Context))[key] = value;
}
export function popContext() {
Context = Object.getPrototypeOf(Context!) as Record<string, unknown>;
}
export function getInContext(key: string) {
if (
MARKO_DEBUG &&
(!Context || !Object.prototype.hasOwnProperty.call(Context, key))
) {
throw new Error(`Unable to receive ${key} from current context`);
}
return Context![key];
}
export function setContext(v: Record<string, unknown> | null) {
usesContext && (Context = v);
}

View File

@ -0,0 +1,77 @@
export function classValue(value: unknown) {
return toDelimitedString(value, " ", stringifyClassObject);
}
function stringifyClassObject(name: string, value: unknown) {
if (isVoid(value)) {
return "";
}
return name;
}
export function styleValue(value: unknown) {
return toDelimitedString(value, ";", stringifyStyleObject);
}
const NON_DIMENSIONAL = /^(--|ta|or|li|z)|n-c|i(do|nk|m|t)|w$|we/;
function stringifyStyleObject(name: string, value: unknown) {
if (isVoid(value)) {
return "";
}
if (typeof value === "number" && value && !NON_DIMENSIONAL.test(name)) {
(value as unknown as string) += "px";
}
return `${name}:${value}`;
}
function toDelimitedString(
val: unknown,
delimiter: string,
stringify: (n: string, v: unknown) => string | undefined
) {
switch (typeof val) {
case "string":
return val;
case "object":
if (val !== null) {
let result = "";
let curDelimiter = "";
if (Array.isArray(val)) {
for (const v of val) {
const part = toDelimitedString(v, delimiter, stringify);
if (part !== "") {
result += curDelimiter + part;
curDelimiter = delimiter;
}
}
} else {
for (const name in val) {
const v = (val as Record<string, unknown>)[name];
const part = stringify(name, v);
if (part !== "") {
result += curDelimiter + part;
curDelimiter = delimiter;
}
}
}
return result;
}
}
return "";
}
export function isVoid(value: unknown) {
return value == null || value === false;
}
export function alphaEncode(num: number): string {
return num < 52
? String.fromCharCode(num < 26 ? num + 97 : num + (65 - 26))
: alphaEncode((num / 52) | 0) + alphaEncode(num % 52);
}

View File

@ -0,0 +1,58 @@
import type { Renderer as ClientRenderer } from "../dom/renderer";
export type Renderer = (...args: unknown[]) => unknown;
export type CommentWalker = TreeWalker & Record<string, Comment>;
export type ScopeContext = Record<string, [Scope, number | string]>;
export type Scope<
T extends { [x: string | number]: unknown } = {
[x: string | number]: unknown;
}
> = [...unknown[]] & {
___attrs: unknown;
___startNode: (Node & ChildNode) | Accessor | undefined;
___endNode: (Node & ChildNode) | Accessor | undefined;
___cleanup: Set<number | string | Scope> | undefined;
___client: boolean;
___bound: Map<unknown, unknown> | undefined;
___renderer: ClientRenderer | undefined;
___context: ScopeContext | undefined;
_: Scope | undefined;
[x: string | number]: any;
} & T;
// TODO: SECTION_SIBLING that is both a SECTION_START and a SECTION_END (<for> siblings)
// NODE that doesn't have a sectionId and uses the previous sectionId
export const enum ResumeSymbols {
SECTION_START = "^",
SECTION_END = "/",
SECTION_SINGLE_NODES_END = "|",
NODE = "#",
PLACEHOLDER_START = "",
PLACEHOLDER_END = "",
REPLACEMENT_ID = "",
VAR_RESUME = "$h",
VAR_REORDER_RUNTIME = "$r",
}
export const enum AccessorChars {
DYNAMIC = "?",
MARK = "#",
STALE = "&",
SUBSCRIBERS = "*",
CLEANUP = "-",
TAG_VARIABLE = "/",
COND_SCOPE = "!",
LOOP_SCOPE_ARRAY = "!",
COND_CONTEXT = "^",
LOOP_CONTEXT = "^",
COND_RENDERER = "(",
LOOP_SCOPE_MAP = "(",
LOOP_VALUE = ")",
CONTEXT_VALUE = ":",
PREVIOUS_ATTRIBUTES = "~",
}
export type Accessor = string | number;

View File

@ -0,0 +1,330 @@
import { type Accessor, AccessorChars, type Scope } from "../common/types";
import { reconcile } from "./reconcile";
import {
type Renderer,
type RendererOrElementName,
createScopeWithRenderer,
} from "./renderer";
import { destroyScope, getEmptyScope } from "./scope";
import { defaultFragment } from "./fragment";
import {
type IntersectionSignal,
type ValueSignal,
renderBodyClosures,
} from "./signals";
type LoopForEach = (
value: unknown[],
cb: (key: unknown, args: unknown[]) => void
) => void;
export function conditional(
nodeAccessor: Accessor,
dynamicTagAttrs?: IntersectionSignal,
intersection?: IntersectionSignal,
valueWithIntersection?: ValueSignal
): ValueSignal<RendererOrElementName | undefined> {
const rendererAccessor = nodeAccessor + AccessorChars.COND_RENDERER;
const childScopeAccessor = nodeAccessor + AccessorChars.COND_SCOPE;
return (scope, newRenderer, clean) => {
newRenderer ||= undefined;
let currentRenderer = scope[rendererAccessor] as
| RendererOrElementName
| undefined;
if (!clean && !(clean = currentRenderer === newRenderer)) {
currentRenderer = scope[rendererAccessor] = newRenderer;
setConditionalRenderer(scope, nodeAccessor, newRenderer);
dynamicTagAttrs?.(scope);
} else {
valueWithIntersection?.(scope, 0, clean);
}
intersection?.(scope, clean);
renderBodyClosures(currentRenderer, scope[childScopeAccessor], clean);
};
}
// TODO: remove this, use return from conditional instead
export function inConditionalScope<S extends Scope>(
signal: IntersectionSignal,
nodeAccessor: Accessor /* branch?: Renderer */
): IntersectionSignal {
const scopeAccessor = nodeAccessor + AccessorChars.COND_SCOPE;
// const rendererAccessor = nodeAccessor + AccessorChars.COND_RENDERER;
return (scope: Scope, clean?: boolean | 1) => {
const conditionalScope = scope[scopeAccessor] as S;
// const conditionalRenderer = scope[rendererAccessor] as Renderer;
if (conditionalScope /* && conditionalRenderer === branch */) {
signal(conditionalScope, clean);
}
};
}
export function setConditionalRenderer<ChildScope extends Scope>(
scope: Scope,
nodeAccessor: Accessor,
newRenderer: RendererOrElementName | undefined
) {
let newScope: ChildScope;
let prevScope = scope[nodeAccessor + AccessorChars.COND_SCOPE] as ChildScope;
const newFragment = newRenderer?.___fragment ?? defaultFragment;
const prevFragment = prevScope?.___renderer?.___fragment ?? defaultFragment;
if (newRenderer) {
newScope = scope[nodeAccessor + AccessorChars.COND_SCOPE] =
createScopeWithRenderer(
newRenderer,
(scope[nodeAccessor + AccessorChars.COND_CONTEXT] ||= scope.___context),
scope
) as ChildScope;
prevScope = prevScope || getEmptyScope(scope[nodeAccessor] as Comment);
} else {
newScope = getEmptyScope(scope[nodeAccessor] as Comment) as ChildScope;
scope[nodeAccessor + AccessorChars.COND_SCOPE] = undefined;
}
newFragment.___insertBefore(
newScope,
prevFragment.___getParentNode(prevScope),
prevFragment.___getFirstNode(prevScope)
);
prevFragment.___remove(destroyScope(prevScope));
}
export function conditionalOnlyChild(
nodeAccessor: Accessor,
action?: ValueSignal<RendererOrElementName | undefined>
): ValueSignal<RendererOrElementName | undefined> {
const rendererAccessor = nodeAccessor + AccessorChars.COND_RENDERER;
const childScopeAccessor = nodeAccessor + AccessorChars.COND_SCOPE;
return (scope, newRenderer, clean) => {
let currentRenderer = scope[rendererAccessor] as
| RendererOrElementName
| undefined;
if (!clean && currentRenderer !== newRenderer) {
currentRenderer = scope[rendererAccessor] = newRenderer;
setConditionalRendererOnlyChild(scope, nodeAccessor, newRenderer);
}
renderBodyClosures(currentRenderer, scope[childScopeAccessor], clean);
action?.(scope, currentRenderer, clean);
};
}
export function setConditionalRendererOnlyChild(
scope: Scope,
nodeAccessor: Accessor,
newRenderer: RendererOrElementName | undefined
) {
const prevScope = scope[nodeAccessor + AccessorChars.COND_SCOPE] as Scope;
const referenceNode = scope[nodeAccessor] as Element;
referenceNode.textContent = "";
if (newRenderer) {
const newScope = (scope[nodeAccessor + AccessorChars.COND_SCOPE] =
createScopeWithRenderer(
newRenderer,
(scope[nodeAccessor + AccessorChars.COND_CONTEXT] ||= scope.___context),
scope
));
(newRenderer.___fragment ?? defaultFragment).___insertBefore(
newScope,
referenceNode,
null
);
}
prevScope && destroyScope(prevScope);
}
const emptyMarkerMap = /* @__PURE__ */ (() =>
new Map().set(Symbol("empty"), getEmptyScope()))();
export const emptyMarkerArray = [/* @__PURE__ */ getEmptyScope()];
const emptyMap = new Map();
const emptyArray = [] as Scope[];
export function loopOf(nodeAccessor: Accessor, renderer: Renderer) {
return loop(nodeAccessor, renderer, (value, cb) => {
const [all, getKey = keyBySecondArg] = value as typeof value &
[all: unknown[], getKey?: (item: unknown, index: number) => unknown];
let i = 0;
for (const item of all) {
cb(getKey(item, i), [item, i, all]);
i++;
}
});
}
export function loopIn(nodeAccessor: Accessor, renderer: Renderer) {
return loop(nodeAccessor, renderer, (value, cb) => {
const [all, getKey = keyByFirstArg] = value as typeof value &
[
all: Record<string, unknown>,
getKey?: (key: string, v: unknown) => unknown
];
for (const key in all) {
const v = all[key];
cb(getKey(key, v), [key, v, all]);
}
});
}
export function loopTo(nodeAccessor: Accessor, renderer: Renderer) {
return loop(nodeAccessor, renderer, (value, cb) => {
const [to, from = 0, step = 1, getKey = keyByFirstArg] = value as [
to: number,
from: number,
step: number,
getKey?: (v: number) => unknown
];
const steps = (to - from) / step;
for (let i = 0; i <= steps; i++) {
const v = from + i * step;
cb(getKey(v), [v]);
}
});
}
function loop(
nodeAccessor: Accessor,
renderer: Renderer,
forEach: LoopForEach
) {
const loopScopeAccessor = nodeAccessor + AccessorChars.LOOP_SCOPE_ARRAY;
const closureSignals = renderer.___closureSignals;
const params = renderer.___attrs;
return (
scope: Scope,
value: [unknown, (...args: unknown[]) => unknown],
clean: boolean | 1
) => {
if (clean) {
for (const childScope of scope[loopScopeAccessor]) {
params?.(childScope, null, clean);
for (const signal of closureSignals) {
signal(childScope, clean);
}
}
return;
}
const referenceNode = scope[nodeAccessor] as Element | Comment | Text;
// TODO: compiler should use only comment so the text check can be removed
const referenceIsMarker =
referenceNode.nodeType === 8 /* Comment */ ||
referenceNode.nodeType === 3; /* Text */
const oldMap =
(scope[nodeAccessor + AccessorChars.LOOP_SCOPE_MAP] as Map<
unknown,
Scope
>) || (referenceIsMarker ? emptyMarkerMap : emptyMap);
const oldArray =
(scope[nodeAccessor + AccessorChars.LOOP_SCOPE_ARRAY] as Scope[]) ||
Array.from(oldMap.values());
let newMap!: Map<unknown, Scope>;
let newArray!: Scope[];
let afterReference: Node | null;
let parentNode: Node & ParentNode;
let needsReconciliation = true;
forEach(value, (key, args) => {
let childScope = oldMap.get(key);
const isNew = !childScope;
if (!childScope) {
childScope = createScopeWithRenderer(
renderer,
(scope[nodeAccessor + AccessorChars.LOOP_CONTEXT] ||=
scope.___context),
scope
);
// TODO: once we can track moves
// needsReconciliation = true;
} else {
// TODO: track if any childScope has changed index
// needsReconciliation ||= oldArray[index] !== childScope;
}
if (params) {
params(childScope, { value: args });
}
if (closureSignals) {
for (const signal of closureSignals) {
signal(childScope, isNew);
}
}
if (newMap) {
newMap.set(key, childScope);
newArray.push(childScope);
} else {
newMap = new Map([[key, childScope]]);
newArray = [childScope];
}
});
if (!newMap) {
if (referenceIsMarker) {
newMap = emptyMarkerMap;
newArray = emptyMarkerArray;
getEmptyScope(referenceNode as Comment);
} else {
// Fast path when loop is only child of parent
if (renderer.___hasUserEffects) {
for (let i = 0; i < oldArray.length; i++) {
destroyScope(oldArray[i]);
}
}
referenceNode.textContent = "";
newMap = emptyMap;
newArray = emptyArray;
needsReconciliation = false;
}
}
if (needsReconciliation) {
if (referenceIsMarker) {
if (oldMap === emptyMarkerMap) {
getEmptyScope(referenceNode as Comment);
}
const oldLastChild = oldArray[oldArray.length - 1];
const fragment = renderer.___fragment ?? defaultFragment;
afterReference = fragment.___getAfterNode(oldLastChild);
parentNode = fragment.___getParentNode(oldLastChild);
} else {
afterReference = null;
parentNode = referenceNode as Element;
}
reconcile(
parentNode,
oldArray,
newArray!,
afterReference,
renderer.___fragment
);
}
scope[nodeAccessor + AccessorChars.LOOP_SCOPE_MAP] = newMap;
scope[nodeAccessor + AccessorChars.LOOP_SCOPE_ARRAY] = newArray;
};
}
// TODO: remove this, use return from loop instead
export function inLoopScope(
signal: IntersectionSignal,
loopNodeAccessor: Accessor
) {
const loopScopeAccessor = loopNodeAccessor + AccessorChars.LOOP_SCOPE_ARRAY;
return (scope: Scope, clean?: boolean | 1) => {
const loopScopes = scope[loopScopeAccessor] ?? [];
for (const scope of loopScopes) {
signal(scope, clean);
}
};
}
function keyBySecondArg(_item: unknown, index: unknown) {
return index;
}
function keyByFirstArg(name: unknown) {
return name;
}

View File

@ -0,0 +1,163 @@
import { classValue, styleValue } from "../common/helpers";
import { type Accessor, AccessorChars, type Scope } from "../common/types";
import { onDestroy, write } from "./scope";
export const enum NodeType {
Element = 1,
Text = 3,
Comment = 8,
DocumentFragment = 11,
}
export function isDocumentFragment(node: Node): node is DocumentFragment {
return node.nodeType === NodeType.DocumentFragment;
}
export function attr(element: Element, name: string, value: unknown) {
const normalizedValue = normalizeAttrValue(value);
if (normalizedValue === undefined) {
element.removeAttribute(name);
} else {
element.setAttribute(name, normalizedValue);
}
}
export function classAttr(element: Element, value: unknown) {
attr(element, "class", classValue(value) || false);
}
export function styleAttr(element: Element, value: unknown) {
attr(element, "style", styleValue(value) || false);
}
export function data(node: Text | Comment, value: unknown) {
const normalizedValue = normalizeString(value);
// TODO: benchmark if it is actually faster to check data first
if (node.data !== normalizedValue) {
node.data = normalizedValue;
}
}
export function attrs(
scope: Scope,
elementAccessor: Accessor,
nextAttrs: Record<string, unknown>
) {
const prevAttrs = scope[
elementAccessor + AccessorChars.PREVIOUS_ATTRIBUTES
] as typeof nextAttrs | undefined;
const element = scope[elementAccessor] as Element;
if (prevAttrs) {
for (const name in prevAttrs) {
if (!(nextAttrs && name in nextAttrs)) {
element.removeAttribute(name);
}
}
}
// https://jsperf.com/object-keys-vs-for-in-with-closure/194
for (const name in nextAttrs) {
if (!(prevAttrs && nextAttrs[name] === prevAttrs[name])) {
if (name === "class") {
classAttr(element, nextAttrs[name]);
} else if (name === "style") {
styleAttr(element, nextAttrs[name]);
} else if (name !== "renderBody") {
attr(element, name, nextAttrs[name]);
}
}
}
scope[elementAccessor + AccessorChars.PREVIOUS_ATTRIBUTES] = nextAttrs;
}
const doc = document;
const parser = /* @__PURE__ */ doc.createElement("template");
export function html(scope: Scope, value: unknown, index: Accessor) {
const firstChild = scope[index] as Node & ChildNode;
const lastChild = (scope[index + "-"] || firstChild) as Node & ChildNode;
const parentNode = firstChild.parentNode!;
const afterReference = lastChild.nextSibling;
parser.innerHTML = value || value === 0 ? `${value}` : "<!>";
const newContent = parser.content;
write(scope, index, newContent.firstChild);
write(scope, (index + "-") as any as number, newContent.lastChild);
parentNode.insertBefore(newContent, firstChild);
let current = firstChild;
while (current !== afterReference) {
const next = current.nextSibling;
current.remove();
current = next!;
}
}
export function props(scope: Scope, nodeIndex: number, index: number) {
const nextProps = scope[index] as Record<string, unknown>;
const prevProps = scope[index + "-"] as Record<string, unknown> | undefined;
const node = scope[nodeIndex] as Node;
if (prevProps) {
for (const name in prevProps) {
if (!(name in nextProps)) {
(node as any)[name] = undefined;
}
}
}
// https://jsperf.com/object-keys-vs-for-in-with-closure/194
for (const name in nextProps) {
(node as any)[name] = nextProps[name];
}
scope[index + "-"] = nextProps;
}
function normalizeAttrValue(value: unknown) {
if (value || value === 0) {
return value === true ? "" : value + "";
}
}
function normalizeString(value: unknown) {
return value || value === 0 ? value + "" : "\u200d";
}
type EffectFn<S extends Scope> = (scope: S) => void | (() => void);
export function userEffect<S extends Scope>(
scope: S,
index: number,
fn: EffectFn<S>
) {
const cleanup = scope[index] as ReturnType<EffectFn<S>>;
const nextCleanup = fn(scope);
if (cleanup) {
cleanup();
} else {
onDestroy(scope, index);
}
scope[index] = nextCleanup;
}
export function lifecycle(
scope: Scope,
index: number,
thisObj: Record<string, unknown> & {
onMount?: (this: unknown) => void;
onUpdate?: (this: unknown) => void;
onDestroy?: (this: unknown) => void;
}
) {
let storedThis = scope[index] as typeof thisObj;
if (!storedThis) {
storedThis = scope[index] = thisObj;
scope[AccessorChars.CLEANUP + index] = () =>
storedThis.onDestroy?.call(storedThis);
onDestroy(scope, AccessorChars.CLEANUP + index);
storedThis.onMount?.call(storedThis);
} else {
Object.assign(storedThis, thisObj);
storedThis.onUpdate?.call(storedThis);
}
}

View File

@ -0,0 +1,56 @@
type Unset = false | null | undefined;
type EventNames = keyof GlobalEventHandlersEventMap;
const delegationRoots = new WeakMap<
Node,
Map<string, WeakMap<Element, Unset | ((...args: any[]) => void)>>
>();
const eventOpts: AddEventListenerOptions = {
capture: true,
};
export function on<
T extends EventNames,
H extends
| Unset
| ((ev: GlobalEventHandlersEventMap[T], target: Element) => void)
>(element: Element, type: T, handler: H) {
const delegationRoot = element.getRootNode();
let delegationEvents = delegationRoots.get(delegationRoot);
if (!delegationEvents) {
delegationRoots.set(delegationRoot, (delegationEvents = new Map()));
}
let delegationHandlers = delegationEvents.get(type);
if (!delegationHandlers) {
delegationEvents!.set(type, (delegationHandlers = new WeakMap()));
delegationRoot.addEventListener(type, handleDelegated, eventOpts);
}
delegationHandlers.set(element, handler);
}
function handleDelegated(ev: GlobalEventHandlersEventMap[EventNames]) {
let target = ev.target as Element | null;
if (target) {
const delegationRoot = target.getRootNode();
const delegationEvents = delegationRoots.get(delegationRoot);
const delegationHandlers = delegationEvents!.get(ev.type!);
let handler = delegationHandlers!.get(target);
if (ev.bubbles) {
while (
!handler &&
!ev.cancelBubble &&
(target = target!.parentElement!)
) {
handler = delegationHandlers!.get(target);
}
}
if (handler) {
handler(ev, target);
}
}
}

View File

@ -0,0 +1,96 @@
import { type Accessor, AccessorChars, type Scope } from "../common/types";
import { emptyMarkerArray } from "./control-flow";
export type DOMFragment = {
___insertBefore: (
scope: Scope,
parent: Node & ParentNode,
nextSibling: Node | null
) => void;
___remove: (scope: Scope) => void;
___getParentNode: (scope: Scope) => Node & ParentNode;
___getAfterNode: (scope: Scope) => Node | null;
___getFirstNode: (scope: Scope) => Node & ChildNode;
___getLastNode: (scope: Scope) => Node & ChildNode;
};
export const singleNodeFragment: DOMFragment = {
___insertBefore(scope: Scope, parent, nextSibling) {
parent.insertBefore(scope.___startNode as Node, nextSibling);
},
___remove(scope: Scope) {
(scope.___startNode as ChildNode).remove();
},
___getParentNode(scope: Scope) {
return this.___getFirstNode(scope).parentNode!;
},
___getAfterNode(scope: Scope) {
return this.___getLastNode(scope).nextSibling;
},
___getFirstNode(scope: Scope) {
return scope.___startNode as ChildNode;
},
___getLastNode(scope: Scope) {
return scope.___endNode as ChildNode;
},
};
export const staticNodesFragment: DOMFragment = {
...singleNodeFragment,
___insertBefore(scope, parent, nextSibling) {
let current: Node = this.___getFirstNode(scope);
const stop = this.___getAfterNode(scope);
while (current !== stop) {
const next = current.nextSibling;
parent.insertBefore(current, nextSibling);
current = next!;
}
},
___remove(scope) {
let current = this.___getFirstNode(scope);
const stop = this.___getAfterNode(scope);
while (current !== stop) {
const next = current.nextSibling;
current.remove();
current = next!;
}
},
} as DOMFragment;
export const dynamicFragment: DOMFragment = {
...staticNodesFragment,
___getFirstNode: getFirstNode,
___getLastNode: getLastNode,
};
function getFirstNode(
currentScope: Scope,
nodeOrAccessor: (Node & ChildNode) | Accessor = currentScope.___startNode!,
last?: boolean
): Node & ChildNode {
let scopeOrScopes: Scope | Scope[];
if (MARKO_DEBUG) {
if (AccessorChars.COND_SCOPE !== AccessorChars.LOOP_SCOPE_ARRAY) {
throw new Error("Offset mismatch between conditionals and loops");
}
}
return typeof nodeOrAccessor === "object"
? nodeOrAccessor
: !(scopeOrScopes = currentScope[
nodeOrAccessor + AccessorChars.COND_SCOPE
] as Scope | Scope[]) || scopeOrScopes === emptyMarkerArray
? (currentScope[nodeOrAccessor] as Comment)
: (last ? getLastNode : getFirstNode)(
Array.isArray(scopeOrScopes)
? scopeOrScopes[last ? scopeOrScopes.length - 1 : 0]
: scopeOrScopes
);
}
function getLastNode(currentScope: Scope) {
return getFirstNode(currentScope, currentScope.___endNode, true);
}
export const defaultFragment = staticNodesFragment;

View File

@ -0,0 +1,59 @@
export {
conditional,
conditionalOnlyChild,
inConditionalScope,
loopOf,
loopIn,
loopTo,
inLoopScope,
} from "./control-flow";
export {
data,
html,
attr,
attrs,
classAttr,
styleAttr,
props,
userEffect,
lifecycle,
} from "./dom";
export { on } from "./event";
export { staticNodesFragment, dynamicFragment } from "./fragment";
export { init, register, resumeSubscription } from "./resume";
export { pushContext, popContext, getInContext } from "../common/context";
export { queueSource, queueEffect, run } from "./queue";
export { write, bindFunction, bindRenderer } from "./scope";
export type { Scope } from "../common/types";
export {
createRenderer,
createRenderFn,
initContextProvider,
dynamicTagAttrs,
} from "./renderer";
export {
value,
initValue,
intersection,
closure,
dynamicClosure,
contextClosure,
dynamicSubscribers,
childClosures,
setTagVar,
tagVarSignal,
nextTagId,
inChild,
values,
intersections,
} from "./signals";

View File

@ -0,0 +1,61 @@
import type { Scope } from "../common/types";
import type { ValueSignal } from "./signals";
import { schedule } from "./schedule";
const enum BatchOffsets {
SCOPE = 0,
SIGNAL = 1,
VALUE = 2,
TOTAL = 3,
}
const enum EffectOffsets {
SCOPE = 0,
FN = 1,
TOTAL = 2,
}
type ExecFn<S extends Scope = Scope> = (scope: S, arg?: any) => void;
let currentBatch: unknown[] = [];
let currentEffects: unknown[] = [];
export function queueSource<T>(scope: Scope, signal: ValueSignal, value: T) {
schedule();
signal(scope, 0, 1);
currentBatch.push(scope, signal, value);
return value;
}
export function queueEffect<S extends Scope, T extends ExecFn<S>>(
scope: S,
fn: T
) {
currentEffects.push(scope, fn);
}
export function run() {
try {
for (let i = 0; i < currentBatch.length; i += BatchOffsets.TOTAL) {
const scope = currentBatch[i + BatchOffsets.SCOPE] as Scope;
const signal = currentBatch[i + BatchOffsets.SIGNAL] as ValueSignal;
const value = currentBatch[i + BatchOffsets.VALUE] as unknown;
signal(scope, value, true);
}
} finally {
currentBatch = [];
}
runEffects();
}
export function runEffects() {
try {
for (let i = 0; i < currentEffects.length; i += EffectOffsets.TOTAL) {
const scope = currentEffects[i] as Scope;
const fn = currentEffects[i + 1] as (scope: Scope) => void;
fn(scope);
}
} finally {
currentEffects = [];
}
}

View File

@ -0,0 +1,146 @@
import type { Scope } from "../common/types";
import { type DOMFragment, defaultFragment } from "./fragment";
import { destroyScope } from "./scope";
// based off https://github.com/WebReflection/udomdiff/blob/master/esm/index.js
// middle sized ~.6kb minified smaller
export function reconcile(
parent: Node & ParentNode,
oldScopes: Scope[],
newScopes: Scope[],
afterReference: Node | null,
fragment: DOMFragment = defaultFragment
): void {
const bLength = newScopes.length;
let aEnd = oldScopes.length;
let bEnd = bLength;
let aStart = 0;
let bStart = 0;
let map: Map<Scope, number> | null = null;
while (aStart < aEnd || bStart < bEnd) {
// append head, tail, or nodes in between: fast path
if (aEnd === aStart) {
// we could be in a situation where the rest of nodes that
// need to be added are not at the end, and in such case
// the node to `insertBefore`, if the index is more than 0
// must be retrieved, otherwise it's gonna be the first item.
const node =
bEnd < bLength
? bStart
? fragment.___getAfterNode(newScopes[bStart - 1])
: fragment.___getFirstNode(newScopes[bEnd - bStart])
: afterReference;
while (bStart < bEnd)
fragment.___insertBefore(newScopes[bStart++], parent, node);
}
// remove head or tail: fast path
else if (bEnd === bStart) {
while (aStart < aEnd) {
// remove the node only if it's unknown or not live
if (!map || !map.has(oldScopes[aStart]))
fragment.___remove(destroyScope(oldScopes[aStart]));
aStart++;
}
}
// same node: fast path
else if (oldScopes[aStart] === newScopes[bStart]) {
aStart++;
bStart++;
}
// same tail: fast path
else if (oldScopes[aEnd - 1] === newScopes[bEnd - 1]) {
aEnd--;
bEnd--;
}
// reverse swap: also fast path
else if (
oldScopes[aStart] === newScopes[bEnd - 1] &&
newScopes[bStart] === oldScopes[aEnd - 1]
) {
// this is oldKeys "shrink" operation that could happen in these cases:
// [1, 2, 3, 4, 5]
// [1, 4, 3, 2, 5]
// or asymmetric too
// [1, 2, 3, 4, 5]
// [1, 2, 3, 5, 6, 4]
const node = fragment.___getAfterNode(oldScopes[--aEnd]);
fragment.___insertBefore(
newScopes[bStart++],
parent,
fragment.___getAfterNode(oldScopes[aStart++])
);
fragment.___insertBefore(newScopes[--bEnd], parent, node);
// mark the future index as identical (yeah, it's dirty, but cheap 👍)
// The main reason to do this, is that when oldKeys[aEnd] will be reached,
// the loop will likely be on the fast path, as identical to newKeys[bEnd].
// In the best case scenario, the next loop will skip the tail,
// but in the worst one, this node will be considered as already
// processed, bailing out pretty quickly from the map index check
oldScopes[aEnd] = newScopes[bEnd];
}
// map based fallback, "slow" path
else {
// the map requires an O(bEnd - bStart) operation once
// to store all future nodes indexes for later purposes.
// In the worst case scenario, this is oldKeys full O(N) cost,
// and such scenario happens at least when all nodes are different,
// but also if both first and last items of the lists are different
if (!map) {
map = new Map();
let i = bStart;
while (i < bEnd) map.set(newScopes[i], i++);
}
// if it's oldKeys future node, hence it needs some handling
if (map.has(oldScopes[aStart])) {
// grab the index of such node, 'cause it might have been processed
const index = map.get(oldScopes[aStart])!;
// if it's not already processed, look on demand for the next LCS
if (bStart < index && index < bEnd) {
let i = aStart;
// counts the amount of nodes that are the same in the future
let sequence = 1;
while (
++i < aEnd &&
i < bEnd &&
map.get(oldScopes[i]) === index + sequence
)
sequence++;
// effort decision here: if the sequence is longer than replaces
// needed to reach such sequence, which would brings again this loop
// to the fast path, prepend the difference before oldKeys sequence,
// and move only the future list index forward, so that aStart
// and bStart will be aligned again, hence on the fast path.
// An example considering aStart and bStart are both 0:
// oldKeys: [1, 2, 3, 4]
// newKeys: [7, 1, 2, 3, 6]
// this would place 7 before 1 and, from that time on, 1, 2, and 3
// will be processed at zero cost
if (sequence > index - bStart) {
const node = fragment.___getFirstNode(oldScopes[aStart]);
while (bStart < index)
fragment.___insertBefore(newScopes[bStart++], parent, node);
}
// if the effort wasn't good enough, fallback to oldKeys replace,
// moving both source and target indexes forward, hoping that some
// similar node will be found later on, to go back to the fast path
else {
const oldNode = oldScopes[aStart++];
fragment.___insertBefore(
newScopes[bStart++],
fragment.___getParentNode(oldNode),
fragment.___getFirstNode(oldNode)
);
fragment.___remove(destroyScope(oldNode));
}
}
// otherwise move the source forward, 'cause there's nothing to do
else aStart++;
}
// this node has no meaning in the future list, so it's more than safe
// to remove it, and check the next live node out instead, meaning
// that only the live list index should be forwarded
else fragment.___remove(destroyScope(oldScopes[aStart++]));
}
}
}

View File

@ -0,0 +1,83 @@
import type { Scope } from "../common/types";
import { type DOMFragment, defaultFragment } from "./fragment";
import { destroyScope } from "./scope";
// based off https://github.com/luwes/sinuous/blob/master/packages/sinuous/map/src/diff.js
// naive implementation(optimizes swap over sort) but it sure is small ~1kb minified smaller
export function reconcile(
parent: Node & ParentNode,
oldScopes: Scope[],
newScopes: Scope[],
afterReference: Node | null,
fragment: DOMFragment = defaultFragment
): void {
let i: number;
let j: number;
const aIdx = new Map();
const bIdx = new Map();
// Create a mapping from keys to their position in the old list
for (i = 0; i < oldScopes.length; i++) {
aIdx.set(oldScopes[i], i);
}
// Create a mapping from keys to their position in the new list
for (i = 0; i < newScopes.length; i++) {
bIdx.set(newScopes[i], i);
}
for (i = j = 0; i !== oldScopes.length || j !== newScopes.length; ) {
const a = oldScopes[i],
b = newScopes[j];
if (a === null) {
// This is a element that has been moved to earlier in the list
i++;
} else if (newScopes.length <= j) {
// No more elements in new, this is a delete
fragment.___remove(destroyScope(a));
i++;
} else if (oldScopes.length <= i) {
// No more elements in old, this is an addition
fragment.___insertBefore(
b,
parent,
a ? fragment.___getFirstNode(a) : afterReference
);
j++;
} else if (a === b) {
// No difference, we move on
i++;
j++;
} else {
// This gives us the idx of where this element should be
const curElmInNew = bIdx.get(a);
// This gives us the idx of where the wanted element is now
const wantedElmInOld = aIdx.get(b);
if (curElmInNew === undefined) {
// Current element is not in new list, it has been removed
fragment.___remove(destroyScope(a));
i++;
} else if (wantedElmInOld === undefined) {
// New element is not in old list, it has been added
fragment.___insertBefore(
b,
parent,
a ? fragment.___getFirstNode(a) : afterReference
);
j++;
} else {
// Element is in both lists, it has been moved
fragment.___insertBefore(
oldScopes[wantedElmInOld],
parent,
a ? fragment.___getFirstNode(a) : afterReference
);
aIdx.delete(wantedElmInOld);
oldScopes[wantedElmInOld] = null as unknown as Scope;
if (wantedElmInOld > i + 1) i++;
j++;
}
}
}
}

View File

@ -0,0 +1,220 @@
import type { Scope } from "../common/types";
import { type DOMFragment, defaultFragment } from "./fragment";
import { destroyScope } from "./scope";
const WRONG_POS = 2147483647;
export function reconcile(
parent: Node & ParentNode,
oldScopes: Scope[],
newScopes: Scope[],
afterReference: Node | null,
fragment: DOMFragment = defaultFragment
): void {
let oldStart = 0;
let newStart = 0;
let oldEnd = oldScopes.length - 1;
let newEnd = newScopes.length - 1;
let oldStartScope = oldScopes[oldStart];
let newStartScope = newScopes[newStart];
let oldEndScope = oldScopes[oldEnd];
let newEndScope = newScopes[newEnd];
let i: number;
let j: number | undefined;
let k: number;
let nextSibling: Node | null;
let oldScope: Scope | null;
let newScope: Scope;
// Step 1
// tslint:disable-next-line: label-position
outer: {
// Skip nodes with the same key at the beginning.
while (oldStartScope === newStartScope) {
++oldStart;
++newStart;
if (oldStart > oldEnd || newStart > newEnd) {
break outer;
}
oldStartScope = oldScopes[oldStart];
newStartScope = newScopes[newStart];
}
// Skip nodes with the same key at the end.
while (oldEndScope === newEndScope) {
--oldEnd;
--newEnd;
if (oldStart > oldEnd || newStart > newEnd) {
break outer;
}
oldEndScope = oldScopes[oldEnd];
newEndScope = newScopes[newEnd];
}
}
if (oldStart > oldEnd) {
// All old nodes are in the correct place, insert the remaining new nodes.
if (newStart <= newEnd) {
k = newEnd + 1;
nextSibling =
k < newScopes.length
? fragment.___getFirstNode(newScopes[k])
: afterReference;
do {
fragment.___insertBefore(newScopes[newStart++], parent, nextSibling);
} while (newStart <= newEnd);
}
} else if (newStart > newEnd) {
// All new nodes are in the correct place, remove the remaining old nodes.
do {
fragment.___remove(destroyScope(oldScopes[oldStart++]));
} while (oldStart <= oldEnd);
} else {
// Step 2
const oldLength = oldEnd - oldStart + 1;
const newLength = newEnd - newStart + 1;
const aNullable = oldScopes as Array<Scope | null>; // will be removed by js optimizing compilers.
// Mark all nodes as inserted.
const sources = new Array(newLength);
for (i = 0; i < newLength; ++i) {
sources[i] = -1;
}
// When pos === WRONG_POS, it means that one of the nodes in the wrong position.
let pos = 0;
let synced = 0;
const keyIndex: Map<unknown, number> = new Map();
for (j = newStart; j <= newEnd; ++j) {
keyIndex.set(newScopes[j], j);
}
for (i = oldStart; i <= oldEnd && synced < newLength; ++i) {
oldScope = oldScopes[i];
j = keyIndex.get(oldScope);
if (j !== undefined) {
pos = pos > j ? WRONG_POS : j;
++synced;
newScope = newScopes[j];
sources[j - newStart] = i;
aNullable[i] = null;
}
}
if (oldLength === oldScopes.length && synced === 0) {
// None of the newNodes already exist in the DOM
// All newNodes need to be inserted
for (; newStart < newLength; ++newStart) {
fragment.___insertBefore(newScopes[newStart], parent, afterReference);
}
// All oldNodes need to be removed
for (; oldStart < oldLength; ++oldStart) {
fragment.___remove(destroyScope(oldScopes[oldStart]));
}
} else {
i = oldLength - synced;
while (i > 0) {
oldScope = aNullable[oldStart++];
if (oldScope !== null) {
fragment.___remove(destroyScope(oldScope));
i--;
}
}
// Step 3
if (pos === WRONG_POS) {
const seq = longestIncreasingSubsequence(sources);
j = seq.length - 1;
k = newScopes.length;
for (i = newLength - 1; i >= 0; --i) {
if (sources[i] === -1) {
pos = i + newStart;
newScope = newScopes[pos++];
nextSibling =
pos < k
? fragment.___getFirstNode(newScopes[pos])
: afterReference;
fragment.___insertBefore(newScope, parent, nextSibling);
} else {
if (j < 0 || i !== seq[j]) {
pos = i + newStart;
newScope = newScopes[pos++];
nextSibling =
pos < k
? fragment.___getFirstNode(newScopes[pos])
: afterReference;
fragment.___insertBefore(newScope, parent, nextSibling);
} else {
--j;
}
}
}
} else if (synced !== newLength) {
k = newScopes.length;
for (i = newLength - 1; i >= 0; --i) {
if (sources[i] === -1) {
pos = i + newStart;
newScope = newScopes[pos++];
nextSibling =
pos < k
? fragment.___getFirstNode(newScopes[pos])
: afterReference;
fragment.___insertBefore(newScope, parent, nextSibling);
}
}
}
}
}
}
function longestIncreasingSubsequence(a: number[]): number[] {
const p = a.slice();
const result: number[] = [];
result.push(0);
let u: number;
let v: number;
for (let i = 0, il = a.length; i < il; ++i) {
if (a[i] === -1) {
continue;
}
const j = result[result.length - 1];
if (a[j] < a[i]) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
// tslint:disable-next-line:no-bitwise
const c = ((u + v) / 2) | 0;
if (a[result[c]] < a[i]) {
u = c + 1;
} else {
v = c;
}
}
if (a[i] < a[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}

View File

@ -0,0 +1 @@
export { reconcile } from "./reconcile-longest-increasing-subsequence";

View File

@ -0,0 +1,289 @@
import {
type Accessor,
AccessorChars,
type Scope,
type ScopeContext,
} from "../common/types";
import { setContext } from "../common/context";
import type { IntersectionSignal, ValueSignal } from "./signals";
import { createScope } from "./scope";
import { WalkCodes, trimWalkString, walk } from "./walker";
import { queueEffect, runEffects } from "./queue";
import { type DOMFragment, defaultFragment } from "./fragment";
import { attrs } from "./dom";
import { setConditionalRendererOnlyChild } from "./control-flow";
import { register } from "./resume";
const enum NodeType {
Element = 1,
Text = 3,
Comment = 8,
DocumentFragment = 11,
}
export type Renderer = {
___template: string;
___walks: string | undefined;
___setup: SetupFn | undefined;
___closureSignals: IntersectionSignal[];
___clone: () => Node;
___hasUserEffects: 0 | 1;
___sourceNode: Node | undefined;
___fragment: DOMFragment | undefined;
___dynamicStartNodeOffset: Accessor | undefined;
___dynamicEndNodeOffset: Accessor | undefined;
___attrs: ValueSignal | undefined;
___owner: Scope | undefined;
};
export type RendererOrElementName =
| Renderer
| (string & Record<keyof Renderer, undefined>);
type SetupFn = (scope: Scope) => void;
type RenderResult = {
update: (input: unknown) => void;
destroy: () => void;
};
export function createScopeWithRenderer(
renderer: RendererOrElementName,
context: ScopeContext,
ownerScope?: Scope
) {
setContext(context);
const newScope = createScope(context as ScopeContext);
newScope._ = renderer.___owner || ownerScope;
newScope.___renderer = renderer as Renderer;
initRenderer(renderer, newScope);
if (renderer.___closureSignals) {
for (const signal of renderer.___closureSignals) {
signal.___subscribe?.(newScope);
}
}
setContext(null);
return newScope;
}
export function initContextProvider(
scope: Scope,
scopeAccessor: number,
valueAccessor: number,
contextKey: string,
renderer: Renderer
) {
const node: Node = scope[scopeAccessor];
const newScope = createScopeWithRenderer(
renderer,
{
...scope.___context,
[contextKey]: [scope, valueAccessor],
},
scope
);
(renderer.___fragment ?? defaultFragment).___insertBefore(
newScope,
node.parentNode!,
node.nextSibling
);
for (const signal of renderer.___closureSignals) {
signal(newScope, true);
}
}
export function initRenderer(renderer: RendererOrElementName, scope: Scope) {
const dom =
typeof renderer === "string"
? document.createElement(renderer)
: renderer.___clone();
walk(
dom.nodeType === NodeType.DocumentFragment
? dom.firstChild!
: (dom as ChildNode),
renderer.___walks ?? " ",
scope
);
scope.___startNode =
dom.nodeType === NodeType.DocumentFragment
? dom.firstChild!
: (dom as ChildNode);
scope.___endNode =
dom.nodeType === NodeType.DocumentFragment
? dom.lastChild!
: (dom as ChildNode);
if (renderer.___setup) {
renderer.___setup(scope);
}
if (renderer.___dynamicStartNodeOffset !== undefined) {
scope.___startNode = renderer.___dynamicStartNodeOffset;
}
if (renderer.___dynamicEndNodeOffset !== undefined) {
scope.___endNode = renderer.___dynamicEndNodeOffset;
}
return dom;
}
export function dynamicTagAttrs(nodeAccessor: Accessor, renderBody: Renderer) {
return (
scope: Scope,
getAttrs: () => Record<string, unknown>,
clean?: boolean | 1
) => {
const renderer = scope[
nodeAccessor + AccessorChars.COND_RENDERER
] as Renderer;
if (!renderer || renderer === renderBody || (clean && !renderer.___attrs)) {
return;
}
const childScope = scope[nodeAccessor + AccessorChars.COND_SCOPE];
if (typeof renderer === "string") {
// This will always be 0 because in dynamicRenderer we used WalkCodes.Get
const elementAccessor = MARKO_DEBUG ? `#${renderer}/0` : 0;
attrs(childScope, elementAccessor, getAttrs());
setConditionalRendererOnlyChild(childScope, elementAccessor, renderBody);
} else if (renderer.___attrs) {
if (clean) {
renderer.___attrs(childScope, null as any, clean);
} else {
const attributes = getAttrs();
renderer.___attrs(
childScope,
{
...attributes,
renderBody: renderBody ?? attributes.renderBody,
},
clean
);
}
}
};
}
export function createRenderFn(
template: string,
walks: string,
setup?: SetupFn,
attrs?: ValueSignal,
closureSignals?: ValueSignal[],
templateId?: string,
dynamicStartNodeOffset?: number,
dynamicEndNodeOffset?: number
) {
const renderer = createRenderer(
template,
walks,
setup,
closureSignals,
0,
undefined,
dynamicStartNodeOffset,
dynamicEndNodeOffset,
attrs
);
return register(
templateId!,
Object.assign((input: unknown, element: Element): RenderResult => {
const scope = createScope();
queueEffect(scope, () => {
element.replaceChildren(dom);
});
const dom = initRenderer(renderer, scope);
if (attrs) {
attrs(scope, input);
}
runEffects();
return {
update: (newInput: unknown) => {
if (attrs) {
attrs(scope, newInput, 1);
attrs(scope, newInput);
runEffects();
}
},
destroy: () => {
// TODO
},
};
}, renderer)
);
}
export function createRenderer(
template: string,
walks?: string,
setup?: SetupFn,
closureSignals: IntersectionSignal[] = [],
hasUserEffects: 0 | 1 = 0,
fragment?: DOMFragment,
dynamicStartNodeOffset?: Accessor,
dynamicEndNodeOffset?: Accessor,
attrs?: ValueSignal
): Renderer {
return {
___template: template,
___walks: walks && /* @__PURE__ */ trimWalkString(walks),
___setup: setup,
___clone: _clone,
___closureSignals: closureSignals,
___hasUserEffects: hasUserEffects,
___sourceNode: undefined,
___fragment: fragment,
___dynamicStartNodeOffset: dynamicStartNodeOffset,
___dynamicEndNodeOffset: dynamicEndNodeOffset,
___attrs: attrs,
___owner: undefined,
};
}
function _clone(this: Renderer) {
let sourceNode: Node | null | undefined = this.___sourceNode;
if (!sourceNode) {
if (MARKO_DEBUG && this.___template === undefined) {
throw new Error(
"The renderer does not have a template to clone: " +
JSON.stringify(this)
);
}
const walks = this.___walks;
// TODO: there's probably a better way to determine if nodes will be inserted before/after the parsed content
// and therefore we need to put it in a document fragment, even though only a single node results from the parse
const ensureFragment =
walks &&
walks.length < 4 &&
walks.charCodeAt(walks.length - 1) !== WalkCodes.Get;
this.___sourceNode = sourceNode = parse(
this.___template,
ensureFragment as boolean
);
}
return sourceNode.cloneNode(true);
}
const doc = document;
const parser = /* @__PURE__ */ doc.createElement("template");
function parse(template: string, ensureFragment?: boolean) {
let node: Node | null;
parser.innerHTML = template;
const content = parser.content;
if (
ensureFragment ||
(node = content.firstChild) !== content.lastChild ||
(node && node.nodeType === NodeType.Comment)
) {
node = doc.createDocumentFragment();
node.appendChild(content);
} else if (!node) {
node = doc.createTextNode("");
}
return node as Node & { firstChild: ChildNode; lastChild: ChildNode };
}

View File

@ -0,0 +1,171 @@
import { AccessorChars, ResumeSymbols, type Scope } from "../common/types";
import type { Renderer } from "./renderer";
import { bindFunction, bindRenderer } from "./scope";
import type { IntersectionSignal, ValueSignal } from "./signals";
type RegisteredFn<S extends Scope = Scope> = (scope: S) => void;
const registeredObjects = new Map<
string,
RegisteredFn | ValueSignal | Renderer
>();
const doc = document;
export function register<T>(id: string, obj: T): T {
registeredObjects.set(id, obj as any);
return obj;
}
export function init(runtimeId = "M" /* [a-zA-Z0-9]+ */) {
const runtimeLength = runtimeId.length;
const resumeVar = runtimeId + ResumeSymbols.VAR_RESUME;
// TODO: check if this is a fakeArray
// and warn in dev that there are conflicting runtime ids
const initialHydration = (window as any)[resumeVar];
const walker = doc.createTreeWalker(doc, 128 /** NodeFilter.SHOW_COMMENT */);
let currentScopeId: number;
let currentNode: Node & ChildNode;
const scopeLookup: Record<number, Scope> = {};
const getScope = (id: number) =>
scopeLookup[id] ?? (scopeLookup[id] = {} as Scope);
const stack: number[] = [];
const fakeArray = { push: resume };
const bind = (registryId: string, scope: Scope) => {
const obj = registeredObjects.get(registryId);
if (!scope) {
return obj;
} else if ((obj as Renderer).___template) {
return bindRenderer(scope, obj as Renderer);
} else {
return bindFunction(scope, obj as RegisteredFn);
}
};
Object.defineProperty(window, resumeVar, {
get() {
return fakeArray;
},
});
if (initialHydration) {
for (let i = 0; i < initialHydration.length; i += 2) {
resume(initialHydration[i], initialHydration[i + 1]);
}
}
function resume(
scopesFn: (
b: typeof bind,
s: typeof scopeLookup,
...rest: unknown[]
) => Record<string, Scope>,
calls: Array<string | number>
) {
if (doc.readyState !== "loading") {
walker.currentNode = doc;
}
const scopes = scopesFn?.(bind, scopeLookup);
/**
* Loop over all the new hydration scopes and see if a previous walk
* had to create a dummy scope to store Nodes of interest.
* If so merge them and set/replace the scope in the scopeLookup.
*/
for (const scopeIdAsString in scopes) {
const scopeId = parseInt(scopeIdAsString);
const scope = scopes[scopeId];
const storedScope = scopeLookup[scopeId];
if (storedScope !== scope) {
scopeLookup[scopeId] = Object.assign(scope, storedScope);
}
}
while ((currentNode = walker.nextNode() as ChildNode)) {
const nodeValue = currentNode.nodeValue;
if (nodeValue?.startsWith(`${runtimeId}`)) {
const token = nodeValue[runtimeLength];
const scopeId = parseInt(nodeValue.slice(runtimeLength + 1));
const scope = getScope(scopeId);
const data = nodeValue.slice(nodeValue.indexOf(" ") + 1);
if (token === ResumeSymbols.NODE) {
scope[data] = currentNode.previousSibling;
} else if (token === ResumeSymbols.SECTION_START) {
stack.push(currentScopeId);
currentScopeId = scopeId;
scope.___startNode = currentNode;
} else if (token === ResumeSymbols.SECTION_END) {
scope[data] = currentNode;
if (scopeId < currentScopeId) {
const currScope = scopeLookup[currentScopeId];
const currParent = currentNode.parentNode!;
const startNode = currScope.___startNode as Node;
if (currParent !== startNode.parentNode) {
currParent.prepend(startNode);
}
currScope.___endNode = currentNode.previousSibling!;
currentScopeId = stack.pop()!;
}
} else if (token === ResumeSymbols.SECTION_SINGLE_NODES_END) {
scope[
MARKO_DEBUG ? data.slice(0, data.indexOf(" ")) : parseInt(data)
] = currentNode;
// https://jsben.ch/dR7uk
const childScopeIds = JSON.parse(
"[" + data.slice(data.indexOf(" ") + 1) + "]"
);
for (let i = childScopeIds.length - 1; i >= 0; i--) {
const childScope = getScope(childScopeIds[i]);
// TODO: consider whether the single node optimization
// should only apply to elements which means could
// use previousElementSibling instead of a while loop
while (
(currentNode = currentNode.previousSibling!).nodeType ===
8 /* Node.COMMENT_NODE */
);
// TODO: consider only setting ___startNode?
childScope.___startNode = childScope.___endNode = currentNode;
}
} else if (MARKO_DEBUG) {
throw new Error("MALFORMED MARKER: " + nodeValue);
}
}
}
for (let i = 0; i < calls.length; i += 2) {
(registeredObjects.get(calls[i + 1] as string) as RegisteredFn)(
scopeLookup[calls[i] as number]!
);
}
}
}
export function resumeSubscription(
signal: IntersectionSignal,
ownerValueAccessor: string | number,
getOwnerScope = (scope: Scope) => scope._!
) {
const ownerMarkAccessor = ownerValueAccessor + AccessorChars.MARK;
const ownerSubscribersAccessor =
ownerValueAccessor + AccessorChars.SUBSCRIBERS;
return (subscriberScope: Scope) => {
const ownerScope = getOwnerScope(subscriberScope);
const boundSignal = bindFunction(subscriberScope, signal);
const ownerMark = ownerScope[ownerMarkAccessor];
(ownerScope[ownerSubscribersAccessor] ??= new Set()).add(boundSignal);
// TODO: if the mark is not undefined, it means the value was updated clientside
// before this subscriber was flushed.
if (ownerMark === 0) {
// the value has finished updating
// we should trigger an update to `signal`
} else if (ownerMark >= 1) {
// the value is queued for update
// we should mark `signal` and let it be updated when the owner is updated
}
};
}

View File

@ -0,0 +1,28 @@
import { run } from "./queue";
const port2 = /* @__PURE__ */ (() => {
const { port1, port2 } = new MessageChannel();
port1.onmessage = () => {
isScheduled = false;
run();
};
return port2;
})();
export let isScheduled: boolean;
export function schedule() {
if (!isScheduled) {
isScheduled = true;
queueMicrotask(flushAndWaitFrame);
}
}
function flushAndWaitFrame() {
run();
requestAnimationFrame(triggerMacroTask);
}
function triggerMacroTask() {
port2.postMessage(0);
}

View File

@ -0,0 +1,101 @@
import type { Scope, ScopeContext } from "../common/types";
import { queueEffect } from "./queue";
import type { Renderer } from "./renderer";
export function createScope(context?: ScopeContext): Scope {
const scope = {} as Scope;
scope.___client = true;
scope.___context = context;
return scope;
}
const emptyScope = createScope();
export function getEmptyScope(marker?: Comment) {
emptyScope.___startNode = emptyScope.___endNode = marker;
return emptyScope;
}
export function write<S extends Scope, K extends keyof S>(
scope: S,
localIndex: K,
value: S[K]
) {
if (scope[localIndex] !== value) {
scope[localIndex] = value;
return 1;
}
return 0;
}
function binder<T, U = T>(bind: (scope: Scope, value: T) => U) {
return (scope: Scope, value: T): U => {
scope.___bound ??= new Map();
let bound = scope.___bound.get(value) as U;
if (!bound) {
bound = bind(scope, value);
scope.___bound.set(value, bound);
}
return bound;
};
}
export const bindRenderer = binder(
(ownerScope, renderer: Renderer): Renderer => ({
...renderer,
___owner: ownerScope,
})
);
type BindableFunction = (
this: unknown,
scope: Scope,
...args: any[]
) => unknown;
export const bindFunction = binder(
<T extends BindableFunction>(boundScope: Scope, fn: T) =>
fn.length
? function bound(this: unknown, ...args: any[]) {
return fn.call(this, boundScope, ...args);
}
: function bound(this: unknown) {
return fn.call(this, boundScope);
}
);
export function destroyScope(scope: Scope) {
_destroyScope(scope);
scope._?.___cleanup?.delete(scope);
const closureSignals = scope.___renderer?.___closureSignals;
if (closureSignals) {
for (const signal of closureSignals) {
signal.___unsubscribe?.(scope);
}
}
return scope;
}
function _destroyScope(scope: Scope) {
const cleanup = scope.___cleanup;
if (cleanup) {
for (const instance of cleanup) {
if (typeof instance === "object") {
_destroyScope(instance);
} else {
queueEffect(scope, scope[instance] as () => void);
}
}
}
}
export function onDestroy(scope: Scope, localIndex: number | string) {
(scope.___cleanup = scope.___cleanup || new Set()).add(localIndex);
let parentScope = scope._;
while (parentScope && !parentScope.___cleanup?.has(scope)) {
(parentScope.___cleanup = parentScope.___cleanup || new Set()).add(scope);
scope = parentScope;
parentScope = scope._;
}
}

View File

@ -0,0 +1,330 @@
import { type Accessor, AccessorChars, type Scope } from "../common/types";
import { bindFunction } from "./scope";
import type { RendererOrElementName } from "./renderer";
export type Signal = ValueSignal | IntersectionSignal;
export type ValueSignal<T = unknown> = (
scope: Scope,
value: T,
clean?: 1 | boolean
) => void;
export type BoundValueSignal<T = unknown> = (
value: T,
clean?: 1 | boolean
) => void;
export type IntersectionSignal = ((
scope: Scope,
clean?: 1 | boolean
) => void) & {
___subscribe?(scope: Scope): void;
___unsubscribe?(scope: Scope): void;
};
export type BoundIntersectionSignal = ((clean?: 1 | boolean) => void) & {
___subscribe?(scope: Scope): void;
___unsubscribe?(scope: Scope): void;
};
export function initValue<T>(
valueAccessor: Accessor,
fn: ValueSignal<T>
): ValueSignal<T> {
const markAccessor = valueAccessor + AccessorChars.MARK;
return (scope, nextValue, clean) => {
if (clean !== 1 && scope[markAccessor] === undefined) {
fn(scope, nextValue, clean);
}
};
}
export function value<T>(
valueAccessor: Accessor,
render?: ValueSignal<T>,
intersection?: IntersectionSignal,
valueWithIntersection?: ValueSignal<any>
): ValueSignal<T> {
const markAccessor = valueAccessor + AccessorChars.MARK;
return (scope, nextValue, clean) => {
let creation: boolean | undefined;
let currentMark: number;
if (clean === 1) {
currentMark = scope[markAccessor] = (scope[markAccessor] ?? 0) + 1;
} else {
creation = scope[markAccessor] === undefined;
currentMark = scope[markAccessor] ||= 1;
}
if (currentMark === 1) {
if (
clean !== 1 &&
(creation || !(clean &&= scope[valueAccessor] === nextValue))
) {
scope[valueAccessor] = nextValue;
render?.(scope, nextValue);
} else {
valueWithIntersection?.(scope, 0, clean);
}
intersection?.(scope, clean);
}
// closure needs this to be called after the fn
// so it is marked until all downstream have been called
if (clean !== 1) {
scope[markAccessor]--;
}
};
}
let accessorId = 0;
export function intersection(
count: number,
fn: IntersectionSignal,
intersection?: IntersectionSignal,
valueWithIntersection?: ValueSignal
): IntersectionSignal {
const cleanAccessor = AccessorChars.DYNAMIC + accessorId++;
const markAccessor = cleanAccessor + AccessorChars.MARK;
return (scope, clean) => {
let currentMark;
if (clean === 1) {
currentMark = scope[markAccessor] = (scope[markAccessor] ?? 0) + 1;
} else {
if (scope[markAccessor] === undefined) {
scope[markAccessor] = count - 1;
clean = undefined;
} else {
currentMark = scope[markAccessor]--;
clean = scope[cleanAccessor] &&= clean;
}
}
if (currentMark === 1) {
if (clean) {
valueWithIntersection?.(scope, 0, clean);
} else {
scope[cleanAccessor] = true;
fn(scope, clean);
}
intersection?.(scope, clean);
}
};
}
const defaultGetOwnerScope = (scope: Scope) => scope._!;
export function closure<T>(
ownerValueAccessor: Accessor | ((scope: Scope) => Accessor),
fn: ValueSignal<T>,
_getOwnerScope?: (scope: Scope) => Scope,
intersection?: IntersectionSignal,
valueWithIntersection?: ValueSignal<any>
): IntersectionSignal {
const cleanAccessor = AccessorChars.DYNAMIC + accessorId++;
const markAccessor = cleanAccessor + 1;
const getOwnerScope = _getOwnerScope || defaultGetOwnerScope;
const getOwnerValueAccessor =
typeof ownerValueAccessor === "function"
? ownerValueAccessor
: () => ownerValueAccessor as Accessor;
return (scope, clean) => {
let ownerScope, ownerValueAccessor, currentMark;
if (clean === 1) {
currentMark = scope[markAccessor] = (scope[markAccessor] ?? 0) + 1;
} else {
if (scope[markAccessor] === undefined) {
ownerScope = getOwnerScope(scope);
ownerValueAccessor = getOwnerValueAccessor(scope);
const ownerMark = ownerScope[ownerValueAccessor + AccessorChars.MARK];
const ownerHasRun =
ownerMark === undefined ? !ownerScope.___client : ownerMark === 0;
scope[markAccessor] = (currentMark = ownerHasRun ? 1 : 2) - 1;
clean = undefined;
} else {
currentMark = scope[markAccessor]--;
clean = scope[cleanAccessor] &&= clean;
}
}
if (currentMark === 1) {
if (clean) {
valueWithIntersection?.(scope, 0, clean);
} else {
scope[cleanAccessor] = false;
ownerScope ??= getOwnerScope(scope);
ownerValueAccessor ??= getOwnerValueAccessor(scope);
fn?.(scope, ownerScope[ownerValueAccessor]);
}
intersection?.(scope, clean);
}
};
}
export function dynamicClosure<T>(
ownerValueAccessor: Accessor | ((scope: Scope) => Accessor),
fn: ValueSignal<T>,
_getOwnerScope?: (scope: Scope) => Scope,
intersection?: IntersectionSignal,
valueWithIntersection?: ValueSignal
): IntersectionSignal {
const getOwnerScope = _getOwnerScope || defaultGetOwnerScope;
const getOwnerValueAccessor =
typeof ownerValueAccessor === "function"
? ownerValueAccessor
: () => ownerValueAccessor as string;
const signalFn = closure(
getOwnerValueAccessor,
fn,
getOwnerScope,
intersection,
valueWithIntersection
);
return Object.assign(signalFn, {
___subscribe(scope: Scope) {
const ownerScope = getOwnerScope(scope);
const providerSubscriptionsAccessor =
getOwnerValueAccessor(scope) + AccessorChars.SUBSCRIBERS;
ownerScope[providerSubscriptionsAccessor] ??= new Set();
ownerScope[providerSubscriptionsAccessor].add(
bindFunction(scope, signalFn as any)
);
},
___unsubscribe(scope: Scope) {
const ownerScope = getOwnerScope(scope);
const providerSubscriptionsAccessor =
getOwnerValueAccessor(scope) + AccessorChars.SUBSCRIBERS;
ownerScope[providerSubscriptionsAccessor]?.delete(
bindFunction(scope, signalFn as any)
);
},
});
}
export function contextClosure<T>(
valueAccessor: Accessor,
contextKey: string,
fn: ValueSignal<T>,
intersection?: IntersectionSignal,
valueWithIntersection?: ValueSignal
) {
// TODO: might be viable as a reliable way to get a unique id
// const dirtyAccessor = valueAccessor - 2;
return dynamicClosure(
(scope) => scope.___context![contextKey][1],
value(valueAccessor, fn),
(scope) => scope.___context![contextKey][0],
intersection,
valueWithIntersection
);
}
export function childClosures(
closureSignals: IntersectionSignal[],
childAccessor: Accessor
) {
const signal = (scope: Scope, clean?: boolean | 1) => {
const childScope = scope[childAccessor] as Scope;
for (const closureSignal of closureSignals) {
closureSignal(childScope, clean);
}
};
return Object.assign(signal, {
___subscribe(scope: Scope) {
const childScope = scope[childAccessor] as Scope;
for (const closureSignal of closureSignals) {
closureSignal.___subscribe?.(childScope);
}
},
___unsubscribe(scope: Scope) {
const childScope = scope[childAccessor] as Scope;
for (const closureSignal of closureSignals) {
closureSignal.___unsubscribe?.(childScope);
}
},
});
}
export function dynamicSubscribers(valueAccessor: Accessor) {
const subscribersAccessor = valueAccessor + AccessorChars.SUBSCRIBERS;
return (scope: Scope, clean?: boolean | 1) => {
const subscribers = scope[
subscribersAccessor
] as Set<BoundIntersectionSignal>;
if (subscribers) {
for (const subscriber of subscribers) {
subscriber(clean);
}
}
};
}
export function setTagVar(
scope: Scope,
childAccessor: Accessor,
tagVarSignal: ValueSignal
) {
scope[childAccessor][AccessorChars.TAG_VARIABLE] = bindFunction(
scope,
tagVarSignal as any
) as BoundValueSignal;
}
export const tagVarSignal = (
scope: Scope,
value: unknown,
clean?: boolean | 1
) => scope[AccessorChars.TAG_VARIABLE]?.(value, clean);
export const renderBodyClosures = (
renderBody: RendererOrElementName | undefined,
childScope: Scope,
clean?: 1 | boolean
) => {
const signals = renderBody?.___closureSignals;
if (signals) {
for (const signal of signals) {
signal(childScope, clean);
}
}
};
export const inMany = (
scopes: Scope[],
clean: 1 | boolean | undefined,
signal: IntersectionSignal
) => {
for (const scope of scopes) {
signal(scope, clean);
}
};
let tagId = 0;
export function nextTagId() {
return "c" + tagId++;
}
export function inChild(childAccessor: Accessor, signal: ValueSignal) {
return (scope: Scope, _: unknown, clean?: 1 | boolean) => {
signal(scope[childAccessor] as Scope, _, clean);
};
}
export function intersections(
signals: IntersectionSignal[]
): IntersectionSignal {
return (scope, clean) => {
for (const signal of signals) {
signal(scope, clean);
}
};
}
export function values(signals: ValueSignal[]): ValueSignal {
return (scope, _, clean) => {
for (const signal of signals) {
signal(scope, _, clean);
}
};
}

View File

@ -0,0 +1,168 @@
import type { Scope } from "../common/types";
import { NodeType } from "./dom";
import { createScope } from "./scope";
export const walker = /* @__PURE__ */ document.createTreeWalker(document);
// Laws of the walks string:
// - Always prefer Get to Before to After, Inside, or Replace
// - Get must always be used to get a static node from clonable template if possible
// - Replace must only be used to insert between two static text nodes
// - Inside must only be used to insert into elements with no static children
// - After must only be used to insert a last child or immediately following another action (if it makes the walks string smaller)
// - Adjacent actions must always be in source order (Before* Get* Inside* After* || Before* Replace)
// - When an element is both walked into and needs to insert After, you must walk in first (Next) and then walk Out before After
// - Unless the inserted node is Text, Inside, After, & Replace must be followed by Out/Over to skip over unknown children
// - Out must always be followed by After or Over
// - Before must be done before walking into the node
// - Next would walk back in the node we just walked Out of
// - A component must assume the walker is on its first node, and include instructions for walking to its assumed nextSibling
// Reserved Character Codes
// 0-31 [control characters]
// 34 " [double quote]
// 39 ' [single quote]
// 92 \ [backslash]
// 96 ` [backtick]
export const enum WalkCodes {
Get = 32,
Before = 33,
After = 35,
Inside = 36,
Replace = 37,
EndChild = 38,
BeginChild = 47,
Next = 67,
NextEnd = 91,
Over = 97,
OverEnd = 106,
Out = 107,
OutEnd = 116,
Multiplier = 117,
MultiplierEnd = 126,
}
export const enum WalkRangeSizes {
Next = 20, // 67 through 91
Over = 10, // 97 through 106
Out = 10, // 107 through 116
Multiplier = 10, // 117 through 126
}
export function trimWalkString(walkString: string): string {
let end = walkString.length;
while (walkString.charCodeAt(--end) > WalkCodes.BeginChild);
return walkString.slice(0, end + 1);
}
export function walk(startNode: Node, walkCodes: string, scope: Scope) {
walker.currentNode = startNode;
walkInternal(walkCodes, scope, 0);
walker.currentNode = document.documentElement;
}
function walkInternal(
walkCodes: string,
scope: Scope,
currentWalkIndex: number
) {
let value: number;
let storedMultiplier = 0;
let currentMultiplier = 0;
let currentScopeIndex = 0;
while ((value = walkCodes.charCodeAt(currentWalkIndex++))) {
currentMultiplier = storedMultiplier;
storedMultiplier = 0;
if (value >= WalkCodes.Multiplier) {
storedMultiplier =
currentMultiplier * WalkRangeSizes.Multiplier +
value -
WalkCodes.Multiplier;
} else if (value >= WalkCodes.Out) {
value = WalkRangeSizes.Out * currentMultiplier + value - WalkCodes.Out;
while (value--) {
walker.parentNode();
}
walker.nextSibling();
} else if (value >= WalkCodes.Over) {
value = WalkRangeSizes.Over * currentMultiplier + value - WalkCodes.Over;
while (value--) {
!walker.nextSibling() && !walker.nextNode();
}
} else if (value >= WalkCodes.Next) {
value = WalkRangeSizes.Next * currentMultiplier + value - WalkCodes.Next;
while (value--) {
walker.nextNode();
}
} else if (value === WalkCodes.BeginChild) {
currentWalkIndex = walkInternal(
walkCodes,
(scope[
MARKO_DEBUG
? getDebugKey(currentScopeIndex++, "#childScope")
: currentScopeIndex++
] = createScope(scope.___context)),
currentWalkIndex
)!;
} else if (value === WalkCodes.EndChild) {
return currentWalkIndex;
} else if (value === WalkCodes.Get) {
scope[
MARKO_DEBUG
? getDebugKey(currentScopeIndex++, walker.currentNode)
: currentScopeIndex++
] = walker.currentNode;
} else {
const newNode = (scope[
MARKO_DEBUG
? getDebugKey(currentScopeIndex++, "#text")
: currentScopeIndex++
] = document.createTextNode(""));
const current = walker.currentNode;
const parentNode = current.parentNode!;
if (value === WalkCodes.Before) {
parentNode.insertBefore(newNode, current);
} else {
if (value === WalkCodes.After) {
parentNode.insertBefore(newNode, current.nextSibling);
} else {
if (MARKO_DEBUG && value !== WalkCodes.Replace) {
throw new Error(`Unknown walk code: ${value}`);
}
parentNode.replaceChild(newNode, current);
}
walker.currentNode = newNode;
}
} /* else {
if (MARKO_DEBUG && value !== WalkCodes.Replace) {
throw new Error(`Unknown walk code: ${value}`);
}
const current = walker.currentNode;
current.parentNode!.replaceChild(walker.currentNode = scope[currentScopeIndex++] = document.createTextNode(""), current);
} */
}
return currentWalkIndex;
}
function getDebugKey(index: number, node: Node | string) {
if (typeof node === "string") {
return `${node}/${index}`;
} else if (node.nodeType === NodeType.Text) {
return `#text/${index}`;
} else if (node.nodeType === NodeType.Comment) {
return `#comment/${index}`;
} else if (node.nodeType === NodeType.Element) {
return `#${(node as Element).tagName.toLowerCase()}/${index}`;
}
return index;
}

View File

@ -0,0 +1,83 @@
import { classValue, isVoid, styleValue } from "../common/helpers";
import { escapeAttrValue } from "./content";
export function classAttr(val: unknown) {
return stringAttr("class", classValue(val));
}
export function styleAttr(val: unknown) {
return stringAttr("style", styleValue(val));
}
export function attr(name: string, val: unknown) {
return isVoid(val) ? "" : nonVoidUntypedAttr(name, val);
}
export function attrs(data: Record<string, unknown>) {
let result = "";
for (const name in data) {
if (name[0] === "o" && name[1] === "n") {
continue;
}
const val = data[name];
switch (name) {
case "class":
result += classAttr(val);
break;
case "style":
result += styleAttr(val);
break;
case "renderBody":
break;
default:
if (!(isVoid(val) || isInvalidAttrName(name))) {
result += nonVoidUntypedAttr(name, val);
}
}
}
return result;
}
function stringAttr(name: string, val: string) {
return val && ` ${name}=${escapeAttrValue(val)}`;
}
function nonVoidUntypedAttr(name: string, val: unknown) {
switch (typeof val) {
case "string":
return ` ${name + attrAssignment(val)}`;
case "boolean":
return ` ${name}`;
case "number":
return ` ${name}=${val}`;
case "object":
if (val instanceof RegExp) {
return ` ${name}=${escapeAttrValue(val.source)}`;
}
// eslint-disable-next-line no-fallthrough
default:
return ` ${name + attrAssignment(val + "")}`;
}
}
function attrAssignment(val: string) {
return val ? `=${escapeAttrValue(val)}` : "";
}
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// Technically the above includes more invalid characters for attributes.
// In practice however the only character that does not become an attribute name
// is when there is a >.
function isInvalidAttrName(name: string) {
for (let i = name.length; i--; ) {
if (name[i] === ">") {
return true;
}
}
return false;
}

View File

@ -0,0 +1,119 @@
export function toString(val: unknown) {
return val || val === 0 ? val + "" : "";
}
export const escapeXML = escapeIfNeeded((val: string) => {
let result = "";
let lastPos = 0;
for (let i = 0, len = val.length; i < len; i++) {
let replacement: string;
switch (val[i]) {
case "<":
replacement = "&lt;";
break;
case "&":
replacement = "&amp;";
break;
default:
continue;
}
result += val.slice(lastPos, i) + replacement;
lastPos = i + 1;
}
if (lastPos) {
return result + val.slice(lastPos);
}
return val;
});
export const escapeScript = escapeIfNeeded(escapeTagEnding("script"));
export const escapeStyle = escapeIfNeeded(escapeTagEnding("style"));
function escapeTagEnding(tagName: string) {
const openTag = `</${tagName}`;
const escaped = `<\\/${tagName}`;
return (val: string) => {
let result = "";
let lastPos = 0;
let i = val.indexOf(openTag, lastPos);
while (i !== -1) {
result += val.slice(lastPos, i) + escaped;
lastPos = i + 1;
i = val.indexOf(openTag, lastPos);
}
if (lastPos) {
return result + val.slice(lastPos);
}
return val;
};
}
export function escapeAttrValue(val: string) {
const len = val.length;
let i = 0;
do {
switch (val[i]) {
case '"':
return quoteValue(val, i + 1, "'", "&#39;");
case "'":
case ">":
case " ":
case "\t":
case "\n":
case "\r":
case "\f":
return quoteValue(val, i + 1, '"', "&#34;");
default:
i++;
break;
}
} while (i < len);
return val;
}
function escapeIfNeeded(escape: (val: string) => string) {
return (val: unknown) => {
if (!val && val !== 0) {
return "&zwj;";
}
switch (typeof val) {
case "string":
return escape(val);
case "boolean":
return "true";
case "number":
return val + "";
default:
return escape(val + "");
}
};
}
function quoteValue(
val: string,
startPos: number,
quote: string,
escaped: string
) {
let result = quote;
let lastPos = 0;
for (let i = startPos, len = val.length; i < len; i++) {
if (val[i] === quote) {
result += val.slice(lastPos, i) + escaped;
lastPos = i + 1;
}
}
return result + (lastPos ? val.slice(lastPos) : val) + quote;
}

View File

@ -0,0 +1,82 @@
import type { Renderer } from "../common/types";
import {
markResumeScopeStart,
nextScopeId,
peekNextScopeId,
write,
writeScope,
} from "./writer";
import { attrs } from "./attrs";
const voidElements = new Set([
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"link",
"meta",
"param",
"source",
"track",
"wbr",
]);
interface RenderBodyObject {
[x: string]: unknown;
renderBody: Renderer;
}
export function dynamicTag(
tag: unknown | string | Renderer | RenderBodyObject,
input: Record<string, unknown>,
renderBody: (() => void) | undefined
) {
if (!tag && !renderBody) return undefined;
const internalScope = {};
const futureScopeId = peekNextScopeId();
write(`${markResumeScopeStart(futureScopeId)}`);
writeScope(futureScopeId, internalScope);
if (!tag) {
renderBody!();
return internalScope;
}
if (typeof tag === "string") {
nextScopeId();
write(`<${tag}${attrs(input)}>`);
if (!voidElements.has(tag)) {
if (renderBody) {
renderBody();
}
write(`</${tag}>`);
} else if (MARKO_DEBUG && renderBody) {
throw new Error(
`A renderBody was provided for a "${tag}" tag, which cannot have children.`
);
}
return internalScope;
}
const renderer = (tag as RenderBodyObject).renderBody || tag;
if (typeof renderer === "function") {
renderer(
renderBody ? { ...input, renderBody } : input,
null,
internalScope
);
return internalScope;
} else if (MARKO_DEBUG) {
throw new Error(`Invalid renderer passed for dynamic tag: ${tag}`);
}
}

View File

@ -0,0 +1,32 @@
export {
toString,
escapeScript,
escapeStyle,
escapeXML,
escapeAttrValue,
} from "./content";
export { attr, attrs, classAttr, styleAttr } from "./attrs";
export { dynamicTag } from "./dynamic-tag";
export {
createRenderer,
write,
maybeFlush,
fork,
tryPlaceholder,
tryCatch,
nextTagId,
nextScopeId,
markResumeNode,
writeEffect,
writeScope,
markResumeScopeStart,
markResumeControlEnd,
markResumeControlSingleNodeEnd,
} from "./writer";
export { register, SYMBOL_OWNER } from "./serializer";
export { pushContext, popContext, getInContext } from "../common/context";

View File

@ -0,0 +1,52 @@
/* tslint:disable */
import type { CommentWalker } from "../common/types";
export default function (
id: string,
doc: Document,
walker: TreeWalker,
node: Comment,
replacementNode: Node,
targetParent: ParentNode & Node,
targetNode: Node | null | undefined,
refNode: Node | null | undefined,
nextNode: Node | null | undefined,
runtimePrefix: string
) {
runtimePrefix = "RUNTIME_ID$";
id = runtimePrefix + id;
doc = document;
walker =
(doc as any)[runtimePrefix + "w"] ||
((doc as any)[runtimePrefix + "w"] = doc.createTreeWalker(
doc,
128 /** NodeFilter.SHOW_COMMENT */
) as CommentWalker);
while ((node = walker.nextNode() as Comment)) {
if (node.data.indexOf(runtimePrefix) === 0) {
(walker as any)[node.data] = node;
}
}
replacementNode = doc.getElementById(id)!;
targetNode = (walker as any)[id];
targetParent = targetNode!.parentNode!;
while ((refNode = replacementNode.firstChild)) {
targetParent.insertBefore(refNode, targetNode!);
}
nextNode = replacementNode.parentNode!;
nextNode.removeChild(replacementNode.nextSibling!);
nextNode.removeChild(replacementNode);
refNode = (walker as any)[id + "/"];
while (
((nextNode = targetNode!.nextSibling),
targetParent.removeChild(targetNode!) !== refNode)
) {
targetNode = nextNode;
}
}

View File

@ -0,0 +1,451 @@
/* eslint "@typescript-eslint/ban-types": ["error", { "types": { "object": false }, "extendDefaults": true }] */
const { hasOwnProperty } = Object.prototype;
const PARAM_BIND = "b";
const PARAM_SCOPE = "s";
const REF_START_CHARS = "hjkmoquxzABCDEFGHIJKLNPQRTUVWXYZ$_"; // Avoids chars that could evaluate to a reserved word.
const REF_START_CHARS_LEN = REF_START_CHARS.length;
const REF_CHARS =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$_";
const REF_CHARS_LEN = REF_CHARS.length;
const SYMBOL_REGISTRY_ID = Symbol("REGISTRY_ID");
const SYMBOL_SCOPE = Symbol("SCOPE");
export const SYMBOL_OWNER = Symbol("OWNER");
type Serializable<T> = T & {
[SYMBOL_REGISTRY_ID]: string;
[SYMBOL_SCOPE]?: number;
};
export function register<T extends (...args: any[]) => any>(
entry: T,
registryId: string,
scopeId?: number
): Serializable<T> {
(entry as Serializable<T>)[SYMBOL_REGISTRY_ID] = registryId;
(entry as Serializable<T>)[SYMBOL_SCOPE] = scopeId;
return entry as Serializable<T>;
}
export function stringify(root: unknown) {
return new Serializer(new Map()).stringify(root);
}
export class Serializer {
// TODO: hoist these back out?
STACK: object[] = [];
BUFFER: string[] = [""];
ASSIGNMENTS: Map<string, string> = new Map();
INDEX_OR_REF: WeakMap<object, number | string> = new WeakMap();
REF_COUNT = 0;
// These stay
PARENTS: WeakMap<object, object> = new WeakMap();
KEYS: WeakMap<object, number | string> = new WeakMap();
scopeLookup: Map<number, any>;
constructor(scopeLookup: Map<number, any>) {
this.scopeLookup = scopeLookup;
this.BUFFER.pop();
}
stringify(root: unknown) {
if (this.writeProp(root, "", undefined)) {
const { BUFFER, REF_COUNT, ASSIGNMENTS, INDEX_OR_REF } = this;
let result = BUFFER[0];
for (let i = 1, len = BUFFER.length; i < len; i++) {
result += BUFFER[i];
}
if (REF_COUNT) {
if (ASSIGNMENTS.size) {
let ref = INDEX_OR_REF.get(root as object);
if (typeof ref === "number") {
ref = toRefParam(this.REF_COUNT++);
result = ref + "=" + result;
}
for (const [assignmentRef, assignments] of ASSIGNMENTS) {
result += "," + assignments + assignmentRef;
}
result += "," + ref;
this.ASSIGNMENTS = new Map();
}
result =
"(" +
PARAM_BIND +
"," +
PARAM_SCOPE +
"," +
this.refParamsString() +
")=>(" +
result +
")";
} else if (root && (root as object).constructor === Object) {
result = "(" + PARAM_BIND + "," + PARAM_SCOPE + ")=>(" + result + ")";
}
BUFFER.length = 0;
this.INDEX_OR_REF = new WeakMap();
return result;
}
return "void 0";
}
writeProp(
cur: unknown,
accessor: string | number,
parent: object | undefined
): boolean {
const { BUFFER } = this;
switch (typeof cur) {
case "string":
BUFFER.push(quote(cur, 0));
break;
case "number":
BUFFER.push(cur + "");
break;
case "boolean":
BUFFER.push(cur ? "!0" : "!1");
break;
case "function":
case "object":
if (cur === null) {
BUFFER.push("null");
} else {
const ref = this.getRef(cur, accessor, parent);
switch (ref) {
case true:
return false;
case false:
switch (cur.constructor) {
case Function:
return this.writeFunction(
cur as Serializable<(...args: any[]) => any>
);
case Object:
this.writeObject(cur as Record<string, unknown>);
break;
case Array:
this.writeArray(cur as unknown[]);
break;
case Date:
BUFFER.push(
'new Date("' + (cur as Date).toISOString() + '")'
);
break;
case RegExp:
BUFFER.push(cur + "");
break;
case Map:
BUFFER.push("new Map(");
this.writeArray(
Array.from(cur as Map<unknown, unknown> | Set<unknown>)
);
BUFFER.push(")");
break;
case Set:
BUFFER.push("new Set(");
this.writeArray(
Array.from(cur as Map<unknown, unknown> | Set<unknown>)
);
BUFFER.push(")");
break;
case undefined:
BUFFER.push("Object.assign(Object.create(null),");
this.writeObject(cur as Record<string, unknown>);
BUFFER.push("))");
break;
default:
return false;
}
break;
default:
BUFFER.push(ref);
break;
}
}
break;
default:
return false;
}
return true;
}
writeFunction(fn: Serializable<(...args: any[]) => any>) {
const { [SYMBOL_REGISTRY_ID]: registryId, [SYMBOL_SCOPE]: scopeId } = fn;
if (registryId) {
// ASSERT: fnId and scopeId don't need `quote` escaping
const scope =
scopeId !== undefined
? this.scopeLookup.get(scopeId) ?? false
: undefined;
const ref = scope && this.getRef(scope, "", undefined);
if (ref === true || ref === false) {
throw new Error(
"The scope has not yet been defined or is circular. This needs to be fixed in the serializer."
);
}
this.BUFFER.push(`${PARAM_BIND}("${registryId}"${ref ? "," + ref : ""})`);
return true;
}
return false;
}
writeObject(obj: Record<string | symbol, unknown>) {
const { STACK, BUFFER } = this;
let sep = "{";
STACK.push(obj);
if (SYMBOL_OWNER in obj) {
BUFFER.push("{_:");
if (
this.writeProp(
this.scopeLookup.get(obj[SYMBOL_OWNER] as number),
"_",
obj
)
) {
sep = ",";
} else {
BUFFER.pop();
}
}
for (const key in obj) {
if (hasOwnProperty.call(obj, key)) {
const val = obj[key];
const escapedKey = toObjectKey(key);
BUFFER.push(sep + escapedKey + ":");
if (this.writeProp(val, escapedKey, obj)) {
sep = ",";
} else {
BUFFER.pop();
}
}
}
if (sep === "{") {
BUFFER.push("{}");
} else {
BUFFER.push("}");
}
STACK.pop();
}
writeArray(arr: unknown[]) {
const { STACK, BUFFER } = this;
BUFFER.push("[");
STACK.push(arr);
this.writeProp(arr[0], 0, arr);
for (let i = 1, len = arr.length; i < len; i++) {
BUFFER.push(",");
this.writeProp(arr[i], i, arr);
}
STACK.pop();
BUFFER.push("]");
}
getRef(cur: object, accessor: string | number, parent: object | undefined) {
const { STACK, BUFFER, INDEX_OR_REF, ASSIGNMENTS, PARENTS, KEYS } = this;
let ref = INDEX_OR_REF.get(cur);
if (ref === undefined) {
INDEX_OR_REF.set(cur, BUFFER.length);
let knownParent = PARENTS.get(cur);
if (knownParent === undefined) {
PARENTS.set(cur, parent!);
KEYS.set(cur, accessor);
return false;
} else {
let ref = "";
while (knownParent) {
ref = toPropertyAccess(KEYS.get(cur)!) + ref;
knownParent = PARENTS.get((cur = knownParent));
}
return PARAM_SCOPE + ref;
}
}
if (typeof ref === "number") {
ref = this.insertAndGetRef(cur, ref);
}
if (STACK.includes(cur)) {
const parent = STACK[STACK.length - 1];
let parentRef = INDEX_OR_REF.get(parent) as string | number;
if (typeof parentRef === "number") {
parentRef = this.insertAndGetRef(parent, parentRef);
}
ASSIGNMENTS.set(
ref,
(ASSIGNMENTS.get(ref) || "") + toAssignment(parentRef, accessor) + "="
);
return true;
}
return ref;
}
insertAndGetRef(obj: object, pos: number) {
const ref = toRefParam(this.REF_COUNT++);
this.INDEX_OR_REF.set(obj, ref);
if (pos) {
this.BUFFER[pos - 1] += ref + "=";
} else {
this.BUFFER[pos] = ref + "=" + this.BUFFER[pos];
}
return ref;
}
refParamsString() {
let result = REF_START_CHARS[0];
for (let i = 1; i < this.REF_COUNT; i++) {
result += "," + toRefParam(i);
}
this.REF_COUNT = 0;
return result;
}
}
function toObjectKey(name: string) {
const invalidPropertyPos = getInvalidPropertyPos(name);
return invalidPropertyPos === -1 ? name : quote(name, invalidPropertyPos);
}
function toAssignment(parent: string, key: string | number) {
return parent + toPropertyAccess(key);
}
function toPropertyAccess(key: string | number) {
return typeof key === "number" || key[0] === '"'
? "[" + key + "]"
: "." + key;
}
function getInvalidPropertyPos(name: string) {
let char = name[0];
if (char >= "0" && char <= "9") {
// numeric
for (let i = 1, len = name.length; i < len; i++) {
char = name[i];
if (!(char >= "0" && char <= "9")) {
return i;
}
}
} else {
// or valid identifier
for (let i = 0, len = name.length; i < len; i++) {
char = name[i];
if (
!(
(char >= "a" && char <= "z") ||
(char >= "A" && char <= "Z") ||
(char >= "0" && char <= "9") ||
char === "$" ||
char === "_"
)
) {
return i;
}
}
}
return -1;
}
// Creates a JavaScript double quoted string and escapes all characters not listed as DoubleStringCharacters on
// Also includes "<" to escape "</script>" and "\" to avoid invalid escapes in the output.
// http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
function quote(str: string, startPos: number): string {
let result = "";
let lastPos = 0;
for (let i = startPos, len = str.length; i < len; i++) {
let replacement: string;
switch (str[i]) {
case '"':
replacement = '\\"';
break;
case "\\":
replacement = "\\\\";
break;
case "<":
replacement = "\\x3C";
break;
case "\n":
replacement = "\\n";
break;
case "\r":
replacement = "\\r";
break;
case "\u2028":
replacement = "\\u2028";
break;
case "\u2029":
replacement = "\\u2029";
break;
default:
continue;
}
result += str.slice(lastPos, i) + replacement;
lastPos = i + 1;
}
if (lastPos === startPos) {
result = str;
} else {
result += str.slice(lastPos);
}
return '"' + result + '"';
}
function toRefParam(index: number) {
let mod = index % REF_START_CHARS_LEN;
let ref = REF_START_CHARS[mod];
index = (index - mod) / REF_START_CHARS_LEN;
while (index > 0) {
mod = index % REF_CHARS_LEN;
ref += REF_CHARS[mod];
index = (index - mod) / REF_CHARS_LEN;
}
return ref;
}

View File

@ -0,0 +1,456 @@
import type { Writable } from "stream";
import { Context, pushContext, setContext } from "../common/context";
import { type Accessor, type Renderer, ResumeSymbols } from "../common/types";
import reorderRuntime from "./reorder-runtime";
import { Serializer } from "./serializer";
const runtimeId = "M";
const reorderRuntimeString = String(reorderRuntime).replace(
"RUNTIME_ID",
runtimeId
);
type MaybeFlushable = Writable & { flush?(): void };
type PartialScope = Record<string | number, unknown> | unknown[];
let $_buffer: Buffer | null = null;
let $_stream: MaybeFlushable | null = null;
let $_flush: typeof flushToStream | null = null;
let $_promises: Array<Promise<unknown> & { isPlaceholder?: true }> | null =
null;
let $_streamData: {
scopeId: number;
tagId: number;
placeholderId: number;
scopeLookup: Map<number, PartialScope>;
runtimeFlushed: boolean;
serializer?: Serializer;
} | null = null;
export function nextTagId() {
return "s" + $_streamData!.tagId++;
}
export function nextPlaceholderId() {
return $_streamData!.placeholderId++;
}
export function createRenderer(renderer: Renderer) {
type Input = Parameters<Renderer>[0];
return async (
input: Input = {},
context: Record<string, unknown> = {},
stream: MaybeFlushable
) => {
$_buffer = createBuffer();
$_stream = stream;
$_flush = flushToStream;
$_streamData = {
scopeId: 0,
tagId: 0,
placeholderId: 0,
scopeLookup: new Map(),
runtimeFlushed: false,
serializer: undefined,
};
pushContext("$", context);
try {
let renderedPromises: typeof $_promises;
try {
renderer(input);
} finally {
renderedPromises = $_promises;
$_flush();
clearScope();
}
if (renderedPromises) {
await Promise.all(renderedPromises);
}
} catch (err) {
stream.emit("error", err);
} finally {
stream.end();
}
};
}
export function write(data: string) {
$_buffer!.content += data;
}
const TARGET_BUFFER_SIZE = 64000;
export function maybeFlush() {
if (
$_flush === flushToStream &&
$_buffer!.content.length > TARGET_BUFFER_SIZE
) {
flushToStream();
}
}
function flushToStream() {
writeResumeScript();
$_stream!.write($_buffer!.content);
if ($_stream!.flush) {
$_stream!.flush();
}
clearBuffer($_buffer!);
}
export function fork<T>(
promise: Promise<T>,
renderResult: (result: T) => void
) {
$_flush!();
let resolved = false;
let targetFlush = $_flush!;
const forkedBuffer = createBuffer();
$_promises = $_promises || [];
$_promises.push(
resolveWithScope(
promise,
(result) => {
resolved = true;
try {
renderResult(result);
} finally {
mergeBuffers(forkedBuffer, $_buffer!);
if ($_promises) {
const originalTargetFlush = targetFlush;
targetFlush = $_flush!;
Promise.all($_promises).then(
() => (targetFlush = originalTargetFlush)
);
}
}
},
(err) => {
resolved = true;
$_buffer = forkedBuffer;
$_flush = targetFlush;
throw err;
}
)
);
$_flush = () => {
if (resolved) {
targetFlush();
} else {
mergeBuffers($_buffer!, forkedBuffer);
}
};
}
export function tryCatch(
renderBody: () => void,
renderError: (err: Error) => void
) {
const id = nextPlaceholderId();
let err: Error | null = null;
const originalPromises = $_promises;
const originalBuffer = $_buffer!;
const originalFlush = $_flush!;
const tryBuffer = createBuffer();
$_flush = () => {
$_buffer = originalBuffer;
$_flush = originalFlush;
markReplaceStart(id);
mergeBuffers(tryBuffer, $_buffer);
$_flush();
};
try {
$_buffer = tryBuffer;
$_promises = null;
renderBody();
} catch (_err) {
err = _err as Error;
} finally {
const childPromises = $_promises;
$_promises = originalPromises;
if (err) {
$_buffer = originalBuffer;
$_flush = originalFlush;
renderError(err);
} else if (!childPromises) {
$_buffer = originalBuffer;
$_flush = originalFlush;
mergeBuffers(tryBuffer, $_buffer);
} else {
markReplaceEnd(id);
$_promises = $_promises || [];
$_promises.push(
resolveWithScope(Promise.all(childPromises), null, (asyncErr) => {
renderReplacement(renderError, asyncErr, id);
})
);
}
}
}
export function tryPlaceholder(
renderBody: () => void,
renderPlaceholder: () => void
) {
const originalBuffer = $_buffer!;
const originalPromises = $_promises;
const originalFlush = $_flush!;
const asyncBuffer = createBuffer();
let resolved = false;
const targetFlush = originalFlush;
$_flush = () => {
if (resolved) {
targetFlush();
} else {
mergeBuffers($_buffer!, asyncBuffer);
}
};
$_buffer = createBuffer();
$_promises = null;
renderBody();
$_flush();
const childPromises = $_promises!;
$_buffer = originalBuffer;
$_promises = originalPromises;
$_flush = originalFlush;
if (childPromises) {
const contentPromises: Array<Promise<unknown>> = [];
const placeholderPromises: Array<
Promise<unknown> & { isPlaceholder: true }
> = [];
for (const promise of childPromises) {
if (promise.isPlaceholder) {
placeholderPromises.push(
promise as Promise<unknown> & {
isPlaceholder: true;
}
);
} else {
contentPromises.push(promise);
}
}
if (placeholderPromises.length) {
($_promises = originalPromises || []).push(...placeholderPromises);
} else {
$_promises = originalPromises;
}
if (contentPromises.length) {
const id = nextPlaceholderId();
$_promises = $_promises || [];
$_promises.push(
Object.assign(
resolveWithScope(Promise.all(contentPromises), () => {
resolved = true;
renderReplacement(mergeBuffers, asyncBuffer, id);
}),
{ isPlaceholder: true } as const
)
);
markReplaceStart(id);
renderPlaceholder();
markReplaceEnd(id);
return;
}
}
mergeBuffers(asyncBuffer, originalBuffer);
}
/* Async */
export function markReplaceStart(id: number) {
return ($_buffer!.content += `<!${marker(id)}>`);
}
export function markReplaceEnd(id: number) {
return ($_buffer!.content += `<!${marker(id)}/>`);
}
function renderReplacement<T>(render: (data: T) => void, data: T, id: number) {
let runtimeCall = runtimeId + ResumeSymbols.VAR_REORDER_RUNTIME;
if (!$_streamData!.runtimeFlushed) {
runtimeCall = `(${runtimeCall}=${reorderRuntimeString})`;
$_streamData!.runtimeFlushed = true;
}
$_buffer!.content += `<t id="${marker(id)}">`;
render(data);
$_buffer!.content += `</t><script>${runtimeCall}(${id})</script>`;
}
function marker(id: number) {
return `${runtimeId}$${id}`;
}
/* Hydration */
export function nextScopeId() {
return $_streamData!.scopeId++;
}
export function peekNextScopeId() {
return $_streamData!.scopeId;
}
export function writeEffect(scopeId: number, fnId: string) {
$_buffer!.calls += `${scopeId},"${fnId}",`;
}
export function writeScope(
scopeId: number,
scope: PartialScope,
assignTo?: PartialScope | PartialScope[]
) {
if (assignTo !== undefined) {
if (Array.isArray(assignTo)) {
assignTo.push(scope);
} else {
scope = Object.assign(assignTo, scope);
}
}
$_buffer!.scopes = $_buffer!.scopes || {};
$_buffer!.scopes[scopeId] = scope;
$_streamData!.scopeLookup.set(scopeId, scope);
}
export function markResumeNode(scopeId: number, index: Accessor) {
// TODO: can we only include the scope id when it differs from the prvious node marker?
return `<!${runtimeId}${ResumeSymbols.NODE}${scopeId} ${index}>`;
}
export function markResumeScopeStart(scopeId: number, key?: string) {
return `<!${runtimeId}${ResumeSymbols.SECTION_START}${scopeId}${
key ? " " + key : ""
}>`;
}
export function markResumeControlEnd(scopeId: number, index: Accessor) {
return `<!${runtimeId}${ResumeSymbols.SECTION_END}${scopeId} ${index}>`;
}
export function markResumeControlSingleNodeEnd(
scopeId: number,
index: Accessor,
childScopeIds?: number | number[]
) {
return `<!${runtimeId}${
ResumeSymbols.SECTION_SINGLE_NODES_END
}${scopeId} ${index} ${childScopeIds ?? ""}>`;
}
function writeResumeScript() {
if ($_buffer!.calls || $_buffer!.scopes) {
let isFirstFlush;
let serializer = $_streamData!.serializer;
if ((isFirstFlush = !serializer)) {
serializer = $_streamData!.serializer = new Serializer(
$_streamData!.scopeLookup
);
}
$_buffer!.content += `<script>${
isFirstFlush
? `(${runtimeId + ResumeSymbols.VAR_RESUME}=[])`
: runtimeId + ResumeSymbols.VAR_RESUME
}.push(${serializer.stringify($_buffer!.scopes)},[${
$_buffer!.calls
}])</script>`;
}
}
interface Buffer {
content: string;
calls: string;
scopes: Record<string, PartialScope> | null;
}
function createBuffer() {
return {
content: "",
calls: "",
scopes: null,
} as Buffer;
}
function mergeBuffers(source: Buffer, target: Buffer = $_buffer!) {
target.content += source.content;
target.calls += source.calls;
if (source.scopes) {
if (target.scopes) {
Object.assign(target.scopes, source.scopes);
} else {
target.scopes = source.scopes;
}
}
clearBuffer(source);
}
function clearBuffer(buffer: Buffer) {
buffer.content = "";
buffer.calls = "";
buffer.scopes = null;
}
function clearScope() {
$_buffer = $_promises = $_stream = $_flush = $_streamData = null;
setContext(null);
}
function resolveWithScope<T>(
promise: Promise<T>,
onResolve: null | ((r: T) => unknown),
onReject?: (e: Error) => unknown
) {
const originalStream = $_stream;
const originalBuffer = $_buffer;
const originalFlush = $_flush;
const originalIds = $_streamData;
const originalContext = Context;
return promise.then(
onResolve &&
((result) => {
$_stream = originalStream;
$_buffer = originalBuffer;
$_flush = originalFlush;
$_streamData = originalIds;
try {
setContext(originalContext);
onResolve(result);
return $_promises && Promise.all($_promises);
} finally {
$_flush!();
clearScope();
}
}),
onReject &&
((error) => {
$_stream = originalStream;
$_buffer = originalBuffer;
$_flush = originalFlush;
$_streamData = originalIds;
try {
setContext(originalContext);
onReject(error);
return $_promises && Promise.all($_promises);
} finally {
$_flush!();
clearScope();
}
})
);
}

1
packages/runtime/src/types.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare const MARKO_DEBUG: boolean;

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"exclude": ["**/__tests__"],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"tsBuildInfoFile": "dist/.tsbuildinfo"
}
}

View File

@ -0,0 +1,43 @@
{
"name": "@marko/translator-interop-class-tags",
"version": "0.0.1",
"description": "Combines the ClassComponent translator from Marko 5 and the TagsAPI translator from Marko 6",
"keywords": [
"babel",
"fluurt",
"htmljs",
"marko",
"parse",
"parser",
"plugin"
],
"homepage": "https://github.com/marko-js/x/blob/master/packages/translator-interop/README.md",
"bugs": "https://github.com/marko-js/x/issues/new?template=Bug_report.md",
"repository": {
"type": "git",
"url": "https://github.com/marko-js/x/tree/master/packages/translator-interop"
},
"license": "MIT",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"!**/__tests__",
"!**/*.tsbuildinfo"
],
"scripts": {
"build": "echo 'todo'"
},
"dependencies": {
"@babel/code-frame": "7.22.5",
"@marko/babel-utils": "^5.21.3",
"@marko/translator-default": "^5.26.4",
"@marko/translator-fluurt": "^0.0.1",
"tslib": "^2.3.1"
},
"peerDependencies": {
"@marko/compiler": "^5.23.0"
},
"jsnext": "dist/index.esm.js"
}

View File

@ -0,0 +1,19 @@
import { t as _t } from "marko/src/runtime/vdom/index.js";
const _marko_componentType = "packages/translator-interop/src/__tests__/fixtures/ambiguous/template.marko",
_marko_template = _t(_marko_componentType);
export default _marko_template;
import _marko_renderer from "marko/src/runtime/components/renderer.js";
import { r as _marko_registerComponent } from "marko/src/runtime/components/registry";
_marko_registerComponent(_marko_componentType, () => _marko_template);
const _marko_component = {};
_marko_template._ = _marko_renderer(function (input, out, _componentDef, _component, state, $global) {
out.be("h1", null, "0", _component, null, 0);
out.t("Hello world", _component);
out.ee();
}, {
t: _marko_componentType,
i: true,
d: true
}, _marko_component);
import _marko_defineComponent from "marko/src/runtime/components/defineComponent.js";
_marko_template.Component = _marko_defineComponent(_marko_component, _marko_template._);

View File

@ -0,0 +1,15 @@
import { t as _t } from "marko/src/runtime/html/index.js";
const _marko_componentType = "packages/translator-interop/src/__tests__/fixtures/ambiguous/template.marko",
_marko_template = _t(_marko_componentType);
export default _marko_template;
import _marko_renderer from "marko/src/runtime/components/renderer.js";
const _marko_component = {};
_marko_template._ = _marko_renderer(function (input, out, _componentDef, _component, state, $global) {
out.w("<h1>");
out.w("Hello world");
out.w("</h1>");
}, {
t: _marko_componentType,
i: true,
d: true
}, _marko_component);

View File

@ -0,0 +1 @@
<h1>Hello world</h1>

View File

@ -0,0 +1,29 @@
import { t as _t } from "marko/src/runtime/vdom/index.js";
const _marko_componentType = "packages/translator-interop/src/__tests__/fixtures/class/template.marko",
_marko_template = _t(_marko_componentType);
export default _marko_template;
import _marko_renderer from "marko/src/runtime/components/renderer.js";
import { r as _marko_registerComponent } from "marko/src/runtime/components/registry";
_marko_registerComponent(_marko_componentType, () => _marko_template);
const _marko_component = {
onCreate() {
this.state = {
count: 0
};
},
increment() {
this.state.count++;
}
};
_marko_template._ = _marko_renderer(function (input, out, _componentDef, _component, state, $global) {
out.be("button", null, "0", _component, null, 0, {
"onclick": _componentDef.d("click", "increment", false)
});
out.t(state.count, _component);
out.ee();
}, {
t: _marko_componentType,
d: true
}, _marko_component);
import _marko_defineComponent from "marko/src/runtime/components/defineComponent.js";
_marko_template.Component = _marko_defineComponent(_marko_component, _marko_template._);

View File

@ -0,0 +1,24 @@
import { t as _t } from "marko/src/runtime/html/index.js";
const _marko_componentType = "packages/translator-interop/src/__tests__/fixtures/class/template.marko",
_marko_template = _t(_marko_componentType);
export default _marko_template;
import { x as _marko_escapeXml } from "marko/src/runtime/html/helpers/escape-xml.js";
import _marko_renderer from "marko/src/runtime/components/renderer.js";
const _marko_component = {
onCreate() {
this.state = {
count: 0
};
},
increment() {
this.state.count++;
}
};
_marko_template._ = _marko_renderer(function (input, out, _componentDef, _component, state, $global) {
out.w("<button>");
out.w(_marko_escapeXml(state.count));
out.w("</button>");
}, {
t: _marko_componentType,
d: true
}, _marko_component);

View File

@ -0,0 +1,11 @@
class {
onCreate() {
this.state = { count: 0 };
}
increment() {
this.state.count++;
}
}
<button onClick("increment")>
${state.count}
</button>

View File

@ -0,0 +1,5 @@
export const template = "<h1>Hello world</h1>";
export const walks = /* over(1) */"b";
export const setup = function () {};
import { createRenderFn as _createRenderFn } from "@marko/runtime-fluurt/dist/debug/dom";
export default /* @__PURE__ */_createRenderFn(template, walks, setup, void 0, void 0, "packages/translator-interop/src/__tests__/fixtures/explicit/template.marko");

View File

@ -0,0 +1,7 @@
import { write as _write, nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/dist/debug/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
_write("<h1>Hello world</h1>");
}, "packages/translator-interop/src/__tests__/fixtures/explicit/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,2 @@
<!-- use tags -->
<h1>Hello world</h1>

View File

@ -0,0 +1,18 @@
import { on as _on, queueSource as _queueSource, data as _data, register as _register, queueEffect as _queueEffect, value as _value, createRenderFn as _createRenderFn } from "@marko/runtime-fluurt/dist/debug/dom";
const _count_effect = _register("packages/translator-interop/src/__tests__/fixtures/let/template.marko_0_count", _scope => _on(_scope["#button/0"], "click", function () {
const {
count
} = _scope;
_queueSource(_scope, _count, count + 1);
}));
const _count = /* @__PURE__ */_value("count", (_scope, count) => {
_data(_scope["#text/1"], count);
_queueEffect(_scope, _count_effect);
});
const _setup = _scope => {
_count(_scope, 0);
};
export const template = "<button> </button>";
export const walks = /* get, next(1), get, out(1) */" D l";
export const setup = _setup;
export default /* @__PURE__ */_createRenderFn(template, walks, setup, void 0, void 0, "packages/translator-interop/src/__tests__/fixtures/let/template.marko");

View File

@ -0,0 +1,12 @@
import { escapeXML as _escapeXML, markResumeNode as _markResumeNode, write as _write, nextScopeId as _nextScopeId, writeEffect as _writeEffect, writeScope as _writeScope, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/dist/debug/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
const count = 0;
_write(`<button>${_escapeXML(count)}${_markResumeNode(_scope0_id, "#text/1")}</button>${_markResumeNode(_scope0_id, "#button/0")}`);
_writeEffect(_scope0_id, "packages/translator-interop/src/__tests__/fixtures/let/template.marko_0_count");
_writeScope(_scope0_id, {
"count": count
}, _scope0_);
}, "packages/translator-interop/src/__tests__/fixtures/let/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,4 @@
<let/count = 0/>
<button onClick() { count++ }>
${count}
</button>

View File

@ -0,0 +1,87 @@
import fs from "fs";
import path from "path";
import glob from "tiny-glob";
import * as compiler from "@marko/compiler";
import snap from "mocha-snap";
const baseConfig: compiler.Config = {
translator: require.resolve(".."),
babelConfig: {
babelrc: false,
configFile: false,
},
writeVersionComment: false,
};
const htmlConfig: compiler.Config = { ...baseConfig, output: "html" };
const domConfig: compiler.Config = { ...baseConfig, output: "dom" };
describe("translator-interop", () => {
before(() => {
uncachePackage("@marko/translator-default");
uncachePackage("@marko/translator-fluurt");
});
const fixturesDir = path.join(__dirname, "fixtures");
for (const entry of fs.readdirSync(fixturesDir)) {
if (entry.endsWith(".skip")) continue;
describe(entry, () => {
const resolve = (file: string) => path.join(fixturesDir, entry, file);
const fixtureDir = resolve(".");
const snapAllTemplates = async (compilerConfig: compiler.Config) => {
const additionalMarkoFiles = await glob(resolve("**/*.marko"));
const finalConfig: compiler.Config = {
...compilerConfig,
resolveVirtualDependency(_filename, { code, virtualPath }) {
return `virtual:${virtualPath} ${code}`;
},
};
const errors: Error[] = [];
const targetSnap = /* config.error_compiler ? snap.catch : */ snap;
for (const file of additionalMarkoFiles) {
const name = path
.relative(fixtureDir, file)
.replace(
".marko",
/* config.error_compiler ? ".error.txt" : */ ".js"
);
await targetSnap(() => compileCode(file, finalConfig), {
file: name,
dir: fixtureDir,
});
}
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new AggregateError(
errors,
"\n" + errors.map((e) => e.toString()).join("\n")
);
}
};
describe("compile", () => {
it("html", () => snapAllTemplates(htmlConfig));
it("dom", () => snapAllTemplates(domConfig));
});
});
}
});
async function compileCode(templateFile: string, config: compiler.Config) {
return (await compiler.compileFile(templateFile, config)).code;
}
function uncachePackage(packageName: string) {
const resolved = require.resolve(packageName);
const root = path.dirname(resolved);
Object.keys(require.cache).forEach((key) => {
if (key.startsWith(root)) {
delete require.cache[key];
}
});
}

View File

@ -0,0 +1,66 @@
import path from "path";
import type { types as t } from "@marko/compiler";
import { codeFrameColumns } from "@babel/code-frame";
const CWD = process.cwd();
export function buildAggregateError(
file: t.BabelFile,
rootMsg: string,
...paths: [string, t.NodePath][]
) {
const err = new SyntaxError();
const fileName = path.relative(CWD, file.opts.filename as string);
const finalMsg = `${rootMsg}:\n\n${paths
.map(
([msg, path]: [string, t.NodePath]) =>
`\u001b[90m${msg} at ${getFileNameWithLoc(
fileName,
path
)}:\x1b[0m\n${getFrame(file, path)}`
)
.join("\n\n")}`;
if (!("MARKO_DEBUG" in process.env)) {
err.stack = finalMsg;
}
// Prevent babel from changing our error message.
Object.defineProperty(err, "message", {
get() {
return finalMsg;
},
set() {},
});
return err;
}
function getFrame(file: t.BabelFile, { node: { loc } }: t.NodePath) {
return loc
? codeFrameColumns(
file.code,
{
start: {
line: loc.start.line,
column: loc.start.column + 1,
},
end:
loc.end && loc.start.line === loc.end.line
? {
line: loc.end.line,
column: loc.end.column + 1,
}
: undefined,
},
{ highlightCode: true }
)
: "";
}
function getFileNameWithLoc(fileName: string, { node: { loc } }: t.NodePath) {
if (loc) {
return `${fileName}(${loc.start.line},${loc.start.column + 1})`;
}
return fileName;
}

View File

@ -0,0 +1,169 @@
import type { types as t } from "@marko/compiler";
import { taglibs as taglibs6 } from "@marko/translator-fluurt";
import { taglibs as taglibs5 } from "@marko/translator-default";
import { getTagDef, isDynamicTag } from "@marko/babel-utils";
import { buildAggregateError } from "./build-aggregate-error";
const enum FEATURE_TYPE {
CLASS = "class",
TAGS = "tags",
}
type Feature = {
name: string;
path: t.NodePath;
type: FEATURE_TYPE;
};
type FeatureState = {
feature?: Feature;
};
const DEFAULT_FEATURE_TYPE = FEATURE_TYPE.CLASS;
export function isTagsAPI(path: t.NodePath) {
const program = path.hub.file.path;
let featureType = program.node.extra?.___featureType;
if (!featureType) {
const state = {} as FeatureState;
program.node.extra ??= {};
program.traverse(featureDetectionVisitor, state);
featureType = program.node.extra.___featureType =
state.feature?.type || DEFAULT_FEATURE_TYPE;
}
return featureType === FEATURE_TYPE.TAGS;
}
const featureDetectionVisitor = {
MarkoComment(comment, state) {
if (/^\s*use tags\s*$/.test(comment.node.value)) {
addFeature(state, FEATURE_TYPE.TAGS, "<!-- use tags -->", comment);
}
},
MarkoScriptlet(scriptlet, state) {
if (!scriptlet.node.static) {
addFeature(state, FEATURE_TYPE.CLASS, "Scriptlet", scriptlet);
}
},
MarkoClass(markoClass, state) {
addFeature(
state,
FEATURE_TYPE.CLASS,
"Class block",
markoClass.get("body")
);
},
ReferencedIdentifier(ref: t.NodePath<t.Identifier>, state: FeatureState) {
const name = ref.node.name;
if (
(name === "component" || name === "out") &&
!ref.scope.hasBinding(name)
) {
addFeature(state, FEATURE_TYPE.CLASS, `${name} template global`, ref);
}
},
MarkoTag(tag, state) {
if (tag.node.var) {
addFeature(
state,
FEATURE_TYPE.TAGS,
"Tag variable",
tag.get("var") as t.NodePath<t.LVal>
);
}
for (const attr of tag.get("attributes")) {
if (attr.isMarkoAttribute()) {
if (attr.node.arguments?.length) {
addFeature(
state,
FEATURE_TYPE.CLASS,
"Attribute arguments",
(attr.get("arguments") as t.NodePath<t.Expression>[])[0]
);
break;
} else if (attr.node.modifier) {
addFeature(state, FEATURE_TYPE.CLASS, "Attribute modifier", attr);
break;
} else if (attr.node.bound) {
addFeature(state, FEATURE_TYPE.TAGS, "Bound attribute", attr);
break;
} else {
switch (attr.node.name) {
case "key":
case "no-update":
case "no-update-if":
case "no-update-body-if":
addFeature(
state,
FEATURE_TYPE.CLASS,
`"${attr.node.name}" attribute`,
attr
);
break;
}
}
}
}
const tagDef = getTagDef(tag);
if (tagDef) {
const feature = getFeatureByTagName(tagDef.name);
if (feature) {
addFeature(state, feature, `<${tagDef.name}> tag`, tag.get("name"));
}
} else if (isDynamicTag(tag) && tag.node.arguments?.length) {
addFeature(state, FEATURE_TYPE.CLASS, "Dynamic tag arguments", tag);
}
},
} as t.Visitor<FeatureState>;
const getFeatureByTagName = (() => {
const taglib5UniqueTags = new Set(
(taglibs5 as typeof taglibs6).flatMap((taglib) =>
Object.keys(taglib[1]).map((key) => /^<(.*)>$/.exec(key)?.[1])
)
) as Set<string | undefined>;
const taglib6UniqueTags = new Set(
taglibs6.flatMap((taglib) =>
Object.keys(taglib[1]).map((key) => /^<(.*)>$/.exec(key)?.[1])
)
) as Set<string | undefined>;
for (const tagName of taglib5UniqueTags) {
if (taglib6UniqueTags.has(tagName)) {
taglib5UniqueTags.delete(tagName);
taglib6UniqueTags.delete(tagName);
}
}
return (tagName: string) => {
if (taglib5UniqueTags.has(tagName)) return FEATURE_TYPE.CLASS;
if (taglib6UniqueTags.has(tagName)) return FEATURE_TYPE.TAGS;
};
})();
function addFeature(
state: FeatureState,
type: Feature["type"],
name: Feature["name"],
path: Feature["path"]
) {
if (state.feature) {
if (state.feature.type !== type) {
throw buildAggregateError(
path.hub.file,
'Cannot mix "tags api" and "class api" features',
[state.feature.name, state.feature.path],
[name, path]
);
}
} else {
state.feature = {
name,
path,
type,
};
}
}

View File

@ -0,0 +1,174 @@
import { types as t } from "@marko/compiler";
import {
analyze as analyze6,
taglibs as taglibs6,
translate as translate6,
} from "@marko/translator-fluurt";
import {
analyze as analyze5,
taglibs as taglibs5,
translate as translate5,
} from "@marko/translator-default";
import { isTagsAPI } from "./feature-detection";
type TagDef = Record<string, unknown>;
const UNMERGABLE_TAGDEF_KEYS = ["renderer", "template"];
const CANNONICAL_TAGDEF_KEYS = {
migrator: "migrate",
"code-generator": "translate",
codeGenerator: "translate",
"node-factory": "parse",
nodeFactory: "parse",
transformer: "transform",
"parse-options": "parseOptions",
};
const VISITOR_TAGDEF_KEYS = ["parse", "migrate", "transform", "translate"];
export const analyze = mergeVisitors(analyze5, analyze6);
export const translate = mergeVisitors(translate5, translate6);
export const taglibs = mergeTaglibs(taglibs5, taglibs6);
function mergeVisitors(visitor5: t.Visitor = {}, visitor6: t.Visitor = {}) {
const allVisitorKeys = getSetOfAllKeys(visitor5, visitor6) as Set<
keyof t.Visitor
>;
const mergedVisitors = {} as any;
for (const visitorKey of allVisitorKeys) {
mergedVisitors[visitorKey as any] = mergedVisitor(
visitor5[visitorKey] as any,
visitor6[visitorKey] as any
) as any;
}
return mergedVisitors;
}
function mergedVisitor<A, B extends t.Node>(
visitor5: t.VisitNode<A, B> = {},
visitor6: t.VisitNode<A, B> = {}
): t.VisitNode<A, B> {
const visitor5Enter = getVisitorEnter(visitor5);
const visitor5Exit = getVisitorExit(visitor5);
const visitor6Enter = getVisitorEnter(visitor6);
const visitor6Exit = getVisitorExit(visitor6);
const hasExit = visitor5Exit || visitor6Exit;
const visitNode = {
enter(path, state) {
if (isTagsAPI(path)) return visitor6Enter?.call(this, path, state);
else return visitor5Enter?.call(this, path, state);
},
exit(path, state) {
if (isTagsAPI(path)) return visitor6Exit?.call(this, path, state);
else return visitor5Exit?.call(this, path, state);
},
} satisfies t.VisitNodeObject<A, B>;
return hasExit ? visitNode : visitNode.enter;
}
function mergeTaglibs(taglibs5: unknown[][], taglibs6: unknown[][]) {
const taglib5Merged = taglibs5.reduce(
(mergedTaglib, taglib) => Object.assign(mergedTaglib, taglib[1]),
{} as Record<string, TagDef>
);
const taglib6Merged = taglibs6.reduce(
(mergedTaglib, taglib) => Object.assign(mergedTaglib, taglib[1]),
{} as Record<string, TagDef>
);
const allTaglibKeys = getSetOfAllKeys(taglib5Merged, taglib6Merged);
const mergedTaglib = {} as Record<string, unknown>;
for (const taglibKey of allTaglibKeys) {
if (taglibKey.startsWith("<")) {
mergedTaglib[taglibKey] = mergeTagdef(
taglib5Merged[taglibKey],
taglib6Merged[taglibKey]
);
} else if (taglibKey === "migrator") {
mergedTaglib[taglibKey] = mergeVisitors(
normalizeTagDefVisitors(taglib5Merged[taglibKey]),
normalizeTagDefVisitors(taglib6Merged[taglibKey])
);
}
}
return [["@marko/translator-interop-class-tags", mergedTaglib]];
}
function mergeTagdef(tagdef5: TagDef = {}, tagdef6: TagDef = {}) {
const tagdef5Normalized = normalizeTagdef(tagdef5);
const tagdef6Normalized = normalizeTagdef(tagdef6);
const allTagdefKeys = getSetOfAllKeys(tagdef5Normalized, tagdef6Normalized);
const mergedTagdef = {} as Record<string, unknown>;
for (const tagdefKey of allTagdefKeys) {
if (VISITOR_TAGDEF_KEYS.includes(tagdefKey)) {
mergedTagdef[tagdefKey] = mergedVisitor(
normalizeTagDefVisitor(tagdef5Normalized[tagdefKey]),
normalizeTagDefVisitor(tagdef6Normalized[tagdefKey])
);
} else {
mergedTagdef[tagdefKey] =
tagdef5Normalized[tagdefKey] ?? tagdef6Normalized[tagdefKey];
if (
UNMERGABLE_TAGDEF_KEYS.includes(tagdefKey) &&
tagdef5Normalized[tagdefKey] &&
tagdef6Normalized[tagdefKey]
) {
throw new Error(`cannot merge "${tagdefKey}"`);
}
}
}
return mergedTagdef;
}
function normalizeTagdef<T extends Record<string, unknown>>(tagdef: T): T {
const normalized = {} as T;
for (const key in tagdef) {
normalized[
(CANNONICAL_TAGDEF_KEYS[
key as keyof typeof CANNONICAL_TAGDEF_KEYS
] as keyof T) ?? key
] = tagdef[key];
}
return normalized;
}
function getVisitorEnter<A, B extends t.Node>(
visit: t.VisitNode<A, B>
): t.VisitNodeFunction<A, B> | undefined {
if (typeof visit === "function") {
return visit;
}
return visit?.enter;
}
function getVisitorExit<A, B extends t.Node>(
visit: t.VisitNode<A, B>
): t.VisitNodeFunction<A, B> | undefined {
if (typeof visit === "function") {
return undefined;
}
return visit?.exit;
}
function getSetOfAllKeys<
A extends Record<any, any>,
B extends Record<any, any>
>(o1: A, o2: B): Set<keyof (A & B)> {
return new Set(Object.keys(o1).concat(Object.keys(o2)));
}
function normalizeTagDefVisitors(visitor: any): t.Visitor {
return visitor?.default ?? visitor;
}
function normalizeTagDefVisitor(visitor: any): t.VisitNode<any, t.Node> {
return typeof visitor === "function" ? visitor : visitor?.default ?? visitor;
}

View File

@ -0,0 +1,45 @@
{
"name": "@marko/translator-fluurt",
"version": "0.0.1",
"private": true,
"description": "Translates Marko templates to the experimental fast, lean, unified, update & render target.",
"keywords": [
"babel",
"fluurt",
"htmljs",
"marko",
"parse",
"parser",
"plugin"
],
"homepage": "https://github.com/marko-js/x/blob/master/packages/translator/README.md",
"bugs": "https://github.com/marko-js/x/issues/new?template=Bug_report.md",
"repository": {
"type": "git",
"url": "https://github.com/marko-js/x/tree/master/packages/translator"
},
"license": "MIT",
"exports": {
".": {
"import": "./dist/index.mjs",
"default": "./dist/index.js"
}
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist",
"!**/*.tsbuildinfo"
],
"scripts": {
"build": "node -r ~ts ./scripts/bundle.ts"
},
"dependencies": {
"@marko/babel-utils": "^6.2.1",
"@marko/runtime-fluurt": "^0.0.1"
},
"peerDependencies": {
"@marko/compiler": "^5.23.0"
}
}

View File

@ -0,0 +1,31 @@
import fs from "fs";
import path from "path";
import { build } from "esbuild";
const absWorkingDir = path.join(__dirname, "..");
const pkg = JSON.parse(
fs.readFileSync(path.join(absWorkingDir, "package.json"), "utf8")
);
const external = new Set([
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
]);
external.delete("@marko/runtime-fluurt");
Promise.all(
(["esm", "cjs"] as const).map((format) =>
build({
format,
bundle: true,
absWorkingDir,
outdir: "dist",
sourcemap: true,
platform: "node",
external: [...external],
entryPoints: [`src/index.ts`],
define: { MARKO_SRC: "false" },
outExtension: { ".js": format === "esm" ? ".mjs" : ".js" },
})
)
);

View File

@ -0,0 +1,4 @@
# Render "End"
```html
abcdefghijkl
```

View File

@ -0,0 +1,37 @@
# Write
a
# Write
b
# Write
c
# Write
defghi
# Write
jkl
# Render "End"
```html
<html>
<head />
<body>
abcdefghijkl
</body>
</html>
```
# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/#text0
```

View File

@ -0,0 +1,27 @@
import { fork, write } from "@marko/runtime-fluurt/src/html";
import { resolveAfter } from "../../utils/resolve";
const renderer = () => {
write("a");
fork(resolveAfter("b", 1), (result1) => {
write(result1);
fork(resolveAfter("c", 1), (result2) => {
write(result2);
fork(resolveAfter("d", 1), write);
write("e");
});
write("f");
});
write("g");
fork(resolveAfter("h", 1), (result7) => {
write(result7);
fork(resolveAfter("i", 1), (result8) => {
write(result8);
fork(resolveAfter("j", 1), write);
write("k");
});
write("l");
});
};
export default renderer;

View File

@ -0,0 +1 @@
export const steps = [{}];

View File

@ -0,0 +1,4 @@
# Render "End"
```html
abcde
```

View File

@ -0,0 +1,29 @@
# Write
a
# Write
bc
# Write
de
# Render "End"
```html
<html>
<head />
<body>
abcde
</body>
</html>
```
# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/#text0
```

View File

@ -0,0 +1,12 @@
import { fork, write } from "@marko/runtime-fluurt/src/html";
import { resolveAfter } from "../../utils/resolve";
const renderer = () => {
write("a");
fork(resolveAfter("b", 1), write);
write("c");
fork(resolveAfter("d", 2), write);
write("e");
};
export default renderer;

View File

@ -0,0 +1 @@
export const steps = [{}];

View File

@ -0,0 +1,4 @@
# Render "End"
```html
abcde
```

View File

@ -0,0 +1,25 @@
# Write
a
# Write
bcde
# Render "End"
```html
<html>
<head />
<body>
abcde
</body>
</html>
```
# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/#text0
```

View File

@ -0,0 +1,12 @@
import { fork, write } from "@marko/runtime-fluurt/src/html";
import { resolveAfter } from "../../utils/resolve";
const renderer = () => {
write("a");
fork(resolveAfter("b", 2), write);
write("c");
fork(resolveAfter("d", 1), write);
write("e");
};
export default renderer;

View File

@ -0,0 +1 @@
export const steps = [{}];

View File

@ -0,0 +1,7 @@
import { write as _write, nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
_write("<div></div>");
}, "packages/translator/src/__tests__/fixtures/at-tag-inside-if-tag/components/custom-tag/index.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,34 @@
import { write as _write, SYMBOL_OWNER as _SYMBOL_OWNER, nextScopeId as _nextScopeId, writeScope as _writeScope, register as _register, markResumeControlSingleNodeEnd as _markResumeControlSingleNodeEnd, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
import _customTag from "./components/custom-tag/index.marko";
const _renderer = _register(({
x
}, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
let _thing;
const _scope1_id = _nextScopeId();
let _ifScopeId, _scope2_, _ifRenderer;
if (x) {
const _scope2_id = _nextScopeId();
_thing = {
x: 1,
renderBody() {
_write("Hello");
}
};
_writeScope(_scope2_id, _scope2_ = {
[_SYMBOL_OWNER]: _scope1_id
});
_register(_ifRenderer = () => {}, "packages/translator/src/__tests__/fixtures/at-tag-inside-if-tag/template.marko_2_renderer");
_ifScopeId = _scope2_id;
}
_write(`${_markResumeControlSingleNodeEnd(_scope1_id, "#text/0", _ifScopeId)}`);
_writeScope(_scope1_id, {
"#text/0!": _scope2_,
"#text/0(": _ifRenderer
});
_customTag({
thing: _thing
});
}, "packages/translator/src/__tests__/fixtures/at-tag-inside-if-tag/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,6 @@
<attrs/{ x }/>
<custom-tag>
<if=x>
<@thing x=1>Hello</@thing>
</if>
</custom-tag>

View File

@ -0,0 +1,2 @@
export const skip_dom = true;
export const skip_ssr = true;

View File

@ -0,0 +1,7 @@
import { write as _write, nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
_write("<div></div>");
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic-and-static/components/hello/index.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,21 @@
import { nextScopeId as _nextScopeId, maybeFlush as _maybeFlush, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
import _hello from "./components/hello/index.marko";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
const _item = [];
const _scope1_id = _nextScopeId();
for (const a in {
a: 1,
b: 2
}) {
const _scope2_id = _nextScopeId();
_item.push({});
_maybeFlush();
}
_hello({
item: _item,
other: {}
});
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic-and-static/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,4 @@
{
"@items <item>[]": {},
"@other": {}
}

View File

@ -0,0 +1,6 @@
<hello>
<for|a| in={ a: 1, b: 2 }>
<@item/>
</for>
<@other/>
</hello>

View File

@ -0,0 +1,2 @@
export const skip_dom = true;
export const skip_ssr = true;

View File

@ -0,0 +1,27 @@
import { write as _write, dynamicTag as _dynamicTag, markResumeControlEnd as _markResumeControlEnd, nextScopeId as _nextScopeId, writeScope as _writeScope, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register(({
x
}, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
const _dynamicScope = _dynamicTag(x, {
header: {
class: "my-header",
renderBody() {
_write("Header content");
}
},
footer: {
class: "my-footer",
renderBody() {
_write("Footer content");
}
}
}, () => _write("Body content"));
_write(`${_markResumeControlEnd(_scope0_id, "#text/0")}`);
_writeScope(_scope0_id, {
"#text/0!": _dynamicScope,
"#text/0(": x
}, _scope0_);
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic-tag-parent/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,12 @@
<attrs/{ x }/>
<${x}>
<@header class="my-header">
Header content
</@header>
<@footer class="my-footer">
Footer content
</@footer>
Body content
</>

View File

@ -0,0 +1,2 @@
export const skip_dom = true;
export const skip_ssr = true;

View File

@ -0,0 +1,7 @@
import { write as _write, nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
_write("<div></div>");
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic-with-params/components/hello/index.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,35 @@
import { escapeXML as _escapeXML, markResumeNode as _markResumeNode, write as _write, SYMBOL_OWNER as _SYMBOL_OWNER, nextScopeId as _nextScopeId, writeScope as _writeScope, register as _register, markResumeControlSingleNodeEnd as _markResumeControlSingleNodeEnd, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
import _hello from "./components/hello/index.marko";
const _renderer = _register(({
x
}, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
let _item;
const _scope1_id = _nextScopeId();
let _ifScopeId, _scope2_, _ifRenderer;
if (x) {
const _scope2_id = _nextScopeId();
_item = {
renderBody({
value: [y]
}) {
_write(`${_escapeXML(y)}${_markResumeNode(_scope3_id, "#text/0")}`);
}
};
_writeScope(_scope2_id, _scope2_ = {
[_SYMBOL_OWNER]: _scope1_id
});
_register(_ifRenderer = () => {}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic-with-params/template.marko_2_renderer");
_ifScopeId = _scope2_id;
}
_write(`${_markResumeControlSingleNodeEnd(_scope1_id, "#text/0", _ifScopeId)}`);
_writeScope(_scope1_id, {
"#text/0!": _scope2_,
"#text/0(": _ifRenderer
});
_hello({
item: _item
});
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic-with-params/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,5 @@
{
"@item <item>": {
"@*": "expression"
}
}

View File

@ -0,0 +1,8 @@
<attrs/{ x }/>
<hello>
<if=x>
<@item|y|>
${y}
</@item>
</if>
</hello>

View File

@ -0,0 +1,2 @@
export const skip_dom = true;
export const skip_ssr = true;

View File

@ -0,0 +1,7 @@
import { write as _write, nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
_write("<div></div>");
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic/components/hello/index.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,87 @@
import { write as _write, SYMBOL_OWNER as _SYMBOL_OWNER, nextScopeId as _nextScopeId, writeScope as _writeScope, register as _register, markResumeControlSingleNodeEnd as _markResumeControlSingleNodeEnd, maybeFlush as _maybeFlush, escapeXML as _escapeXML, markResumeNode as _markResumeNode, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
import _hello from "./components/hello/index.marko";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
const _col = [];
const _scope1_id = _nextScopeId();
const _item = [];
for (const color of ["red", "blue", "green"]) {
const _scope3_id = _nextScopeId();
let _ifScopeId, _scope4_, _ifRenderer;
if (color === "red") {
const _scope4_id = _nextScopeId();
_item.push({
style: {
color
},
renderBody() {
_write("foo");
}
});
_writeScope(_scope4_id, _scope4_ = {
[_SYMBOL_OWNER]: _scope3_id
});
_register(_ifRenderer = () => {}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic/template.marko_4_renderer");
_ifScopeId = _scope4_id;
} else {
const _scope5_id = _nextScopeId();
_item.push({
style: {
color
},
renderBody() {
_write("bar");
}
});
_writeScope(_scope5_id, _scope4_ = {
[_SYMBOL_OWNER]: _scope3_id
});
_register(_ifRenderer = () => {}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic/template.marko_5_renderer");
_ifScopeId = _scope5_id;
}
_write(`${_markResumeControlSingleNodeEnd(_scope3_id, "#text/0", _ifScopeId)}`);
_writeScope(_scope3_id, {
"#text/0!": _scope4_,
"#text/0(": _ifRenderer
});
_maybeFlush();
}
let _i = 0;
for (const col of [["a", "b"], ["c", "d"]]) {
const _scope8_id = _nextScopeId();
let i = _i++;
const _row = [];
for (const row of col) {
const _scope10_id = _nextScopeId();
_row.push({
row: row,
renderBody() {
_write(`${_escapeXML(row)}${_markResumeNode(_scope11_id, "#text/0")}`);
}
});
_maybeFlush();
}
_col.push({
x: i,
row: _row
});
_maybeFlush();
}
_col.push({
outside: true,
row: {
row: -1,
renderBody() {
_write("Outside");
}
}
});
_hello({
list: {
item: _item
},
col: _col
});
}, "packages/translator/src/__tests__/fixtures/at-tags-dynamic/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,13 @@
{
"@list <list>": {
"@items <item>[]": {
"@*": "expression"
}
},
"@cols <col>[]": {
"@*": "expression",
"@rows <row>[]": {
"@*": "expression"
}
}
}

View File

@ -0,0 +1,24 @@
<hello>
<@list>
<for|color| of=["red", "blue", "green"]>
<if=color === "red">
<@item style={ color }>foo</@item>
</if>
<else>
<@item style={ color }>bar</@item>
</else>
</for>
</@list>
<for|col, i| of=[["a", "b"], ["c", "d"]]>
<@col x=i>
<for|row| of=col>
<@row row=row>${row}</@row>
</for>
</@col>
</for>
<@col outside>
<@row row=-1>Outside</@row>
</@col>
</hello>

View File

@ -0,0 +1,2 @@
export const skip_dom = true;
export const skip_ssr = true;

View File

@ -0,0 +1,6 @@
import { nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
}, "packages/translator/src/__tests__/fixtures/at-tags/components/hello/index.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,17 @@
import { write as _write, nextScopeId as _nextScopeId, register as _register, createRenderer as _createRenderer } from "@marko/runtime-fluurt/src/html";
import _hello from "./components/hello/index.marko";
const _renderer = _register((input, _tagVar, _scope0_) => {
const _scope0_id = _nextScopeId();
_hello({
foo: {
renderBody() {
_write("Foo!");
}
},
renderBody() {
const _scope1_id = _nextScopeId();
}
});
}, "packages/translator/src/__tests__/fixtures/at-tags/template.marko");
export default _renderer;
export const render = /* @__PURE__ */_createRenderer(_renderer);

View File

@ -0,0 +1,3 @@
<hello>
<@foo>Foo!</@foo>
</hello>

View File

@ -0,0 +1,2 @@
export const skip_dom = true;
export const skip_ssr = true;

View File

@ -0,0 +1,44 @@
# Render {}
```html
<input
disabled=""
/>
<button>
enable
</button>
```
# Render
container.querySelector("button").click()
```html
<input />
<button>
disable
</button>
```
# Render
container.querySelector("button").click()
```html
<input
disabled=""
/>
<button>
enable
</button>
```
# Render
container.querySelector("button").click()
```html
<input />
<button>
disable
</button>
```

View File

@ -0,0 +1,67 @@
# Render {}
```html
<input
disabled=""
/>
<button>
enable
</button>
```
# Mutations
```
inserted input0, button1
```
# Render
container.querySelector("button").click()
```html
<input />
<button>
disable
</button>
```
# Mutations
```
input0: attr(disabled) "" => null
button1/#text0: "enable" => "disable"
```
# Render
container.querySelector("button").click()
```html
<input
disabled=""
/>
<button>
enable
</button>
```
# Mutations
```
input0: attr(disabled) null => ""
button1/#text0: "disable" => "enable"
```
# Render
container.querySelector("button").click()
```html
<input />
<button>
disable
</button>
```
# Mutations
```
input0: attr(disabled) "" => null
button1/#text0: "enable" => "disable"
```

Some files were not shown because too many files have changed in this diff Show More