mirror of
https://github.com/marko-js/marko.git
synced 2026-02-01 16:07:13 +00:00
Merge remote-tracking branch 'x/interop-translator'
This commit is contained in:
commit
a39fc3ec12
@ -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
88
.sizes.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@ -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
63
package-lock.json
generated
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
26
packages/runtime/package.json
Normal file
26
packages/runtime/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
55
packages/runtime/scripts/bundle.ts
Normal file
55
packages/runtime/scripts/bundle.ts
Normal 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
|
||||
)
|
||||
),
|
||||
]);
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
BIN
packages/runtime/src/__tests__/serializer.test.ts
Normal file
BIN
packages/runtime/src/__tests__/serializer.test.ts
Normal file
Binary file not shown.
26
packages/runtime/src/common/context.ts
Normal file
26
packages/runtime/src/common/context.ts
Normal 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);
|
||||
}
|
||||
77
packages/runtime/src/common/helpers.ts
Normal file
77
packages/runtime/src/common/helpers.ts
Normal 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);
|
||||
}
|
||||
58
packages/runtime/src/common/types.ts
Normal file
58
packages/runtime/src/common/types.ts
Normal 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;
|
||||
330
packages/runtime/src/dom/control-flow.ts
Normal file
330
packages/runtime/src/dom/control-flow.ts
Normal 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;
|
||||
}
|
||||
163
packages/runtime/src/dom/dom.ts
Normal file
163
packages/runtime/src/dom/dom.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
56
packages/runtime/src/dom/event.ts
Normal file
56
packages/runtime/src/dom/event.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
packages/runtime/src/dom/fragment.ts
Normal file
96
packages/runtime/src/dom/fragment.ts
Normal 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;
|
||||
59
packages/runtime/src/dom/index.ts
Normal file
59
packages/runtime/src/dom/index.ts
Normal 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";
|
||||
61
packages/runtime/src/dom/queue.ts
Normal file
61
packages/runtime/src/dom/queue.ts
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
146
packages/runtime/src/dom/reconcile-domdiff.ts
Normal file
146
packages/runtime/src/dom/reconcile-domdiff.ts
Normal 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++]));
|
||||
}
|
||||
}
|
||||
}
|
||||
83
packages/runtime/src/dom/reconcile-listdiff.ts
Normal file
83
packages/runtime/src/dom/reconcile-listdiff.ts
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
1
packages/runtime/src/dom/reconcile.ts
Normal file
1
packages/runtime/src/dom/reconcile.ts
Normal file
@ -0,0 +1 @@
|
||||
export { reconcile } from "./reconcile-longest-increasing-subsequence";
|
||||
289
packages/runtime/src/dom/renderer.ts
Normal file
289
packages/runtime/src/dom/renderer.ts
Normal 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 };
|
||||
}
|
||||
171
packages/runtime/src/dom/resume.ts
Normal file
171
packages/runtime/src/dom/resume.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
28
packages/runtime/src/dom/schedule.ts
Normal file
28
packages/runtime/src/dom/schedule.ts
Normal 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);
|
||||
}
|
||||
101
packages/runtime/src/dom/scope.ts
Normal file
101
packages/runtime/src/dom/scope.ts
Normal 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._;
|
||||
}
|
||||
}
|
||||
330
packages/runtime/src/dom/signals.ts
Normal file
330
packages/runtime/src/dom/signals.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
168
packages/runtime/src/dom/walker.ts
Normal file
168
packages/runtime/src/dom/walker.ts
Normal 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;
|
||||
}
|
||||
83
packages/runtime/src/html/attrs.ts
Normal file
83
packages/runtime/src/html/attrs.ts
Normal 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;
|
||||
}
|
||||
119
packages/runtime/src/html/content.ts
Normal file
119
packages/runtime/src/html/content.ts
Normal 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 = "<";
|
||||
break;
|
||||
case "&":
|
||||
replacement = "&";
|
||||
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, "'", "'");
|
||||
case "'":
|
||||
case ">":
|
||||
case " ":
|
||||
case "\t":
|
||||
case "\n":
|
||||
case "\r":
|
||||
case "\f":
|
||||
return quoteValue(val, i + 1, '"', """);
|
||||
default:
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
} while (i < len);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
function escapeIfNeeded(escape: (val: string) => string) {
|
||||
return (val: unknown) => {
|
||||
if (!val && val !== 0) {
|
||||
return "‍";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
82
packages/runtime/src/html/dynamic-tag.ts
Normal file
82
packages/runtime/src/html/dynamic-tag.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
32
packages/runtime/src/html/index.ts
Normal file
32
packages/runtime/src/html/index.ts
Normal 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";
|
||||
52
packages/runtime/src/html/reorder-runtime.ts
Normal file
52
packages/runtime/src/html/reorder-runtime.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
451
packages/runtime/src/html/serializer.ts
Normal file
451
packages/runtime/src/html/serializer.ts
Normal 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;
|
||||
}
|
||||
456
packages/runtime/src/html/writer.ts
Normal file
456
packages/runtime/src/html/writer.ts
Normal 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
1
packages/runtime/src/types.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare const MARKO_DEBUG: boolean;
|
||||
10
packages/runtime/tsconfig.json
Normal file
10
packages/runtime/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["**/__tests__"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
43
packages/translator-interop/package.json
Normal file
43
packages/translator-interop/package.json
Normal 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"
|
||||
}
|
||||
@ -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._);
|
||||
@ -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);
|
||||
@ -0,0 +1 @@
|
||||
<h1>Hello world</h1>
|
||||
@ -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._);
|
||||
@ -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);
|
||||
@ -0,0 +1,11 @@
|
||||
class {
|
||||
onCreate() {
|
||||
this.state = { count: 0 };
|
||||
}
|
||||
increment() {
|
||||
this.state.count++;
|
||||
}
|
||||
}
|
||||
<button onClick("increment")>
|
||||
${state.count}
|
||||
</button>
|
||||
@ -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");
|
||||
@ -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);
|
||||
@ -0,0 +1,2 @@
|
||||
<!-- use tags -->
|
||||
<h1>Hello world</h1>
|
||||
@ -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");
|
||||
@ -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);
|
||||
@ -0,0 +1,4 @@
|
||||
<let/count = 0/>
|
||||
<button onClick() { count++ }>
|
||||
${count}
|
||||
</button>
|
||||
87
packages/translator-interop/src/__tests__/main.test.ts
Normal file
87
packages/translator-interop/src/__tests__/main.test.ts
Normal 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];
|
||||
}
|
||||
});
|
||||
}
|
||||
66
packages/translator-interop/src/build-aggregate-error.ts
Normal file
66
packages/translator-interop/src/build-aggregate-error.ts
Normal 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;
|
||||
}
|
||||
169
packages/translator-interop/src/feature-detection.ts
Normal file
169
packages/translator-interop/src/feature-detection.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
174
packages/translator-interop/src/index.ts
Normal file
174
packages/translator-interop/src/index.ts
Normal 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;
|
||||
}
|
||||
45
packages/translator/package.json
Normal file
45
packages/translator/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
31
packages/translator/scripts/bundle.ts
Normal file
31
packages/translator/scripts/bundle.ts
Normal 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" },
|
||||
})
|
||||
)
|
||||
);
|
||||
@ -0,0 +1,4 @@
|
||||
# Render "End"
|
||||
```html
|
||||
abcdefghijkl
|
||||
```
|
||||
@ -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
|
||||
```
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export const steps = [{}];
|
||||
@ -0,0 +1,4 @@
|
||||
# Render "End"
|
||||
```html
|
||||
abcde
|
||||
```
|
||||
@ -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
|
||||
```
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export const steps = [{}];
|
||||
@ -0,0 +1,4 @@
|
||||
# Render "End"
|
||||
```html
|
||||
abcde
|
||||
```
|
||||
@ -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
|
||||
```
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
export const steps = [{}];
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -0,0 +1 @@
|
||||
<div/>
|
||||
@ -0,0 +1,6 @@
|
||||
<attrs/{ x }/>
|
||||
<custom-tag>
|
||||
<if=x>
|
||||
<@thing x=1>Hello</@thing>
|
||||
</if>
|
||||
</custom-tag>
|
||||
@ -0,0 +1,2 @@
|
||||
export const skip_dom = true;
|
||||
export const skip_ssr = true;
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -0,0 +1 @@
|
||||
<div/>
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"@items <item>[]": {},
|
||||
"@other": {}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
<hello>
|
||||
<for|a| in={ a: 1, b: 2 }>
|
||||
<@item/>
|
||||
</for>
|
||||
<@other/>
|
||||
</hello>
|
||||
@ -0,0 +1,2 @@
|
||||
export const skip_dom = true;
|
||||
export const skip_ssr = true;
|
||||
@ -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);
|
||||
@ -0,0 +1,12 @@
|
||||
<attrs/{ x }/>
|
||||
<${x}>
|
||||
<@header class="my-header">
|
||||
Header content
|
||||
</@header>
|
||||
|
||||
<@footer class="my-footer">
|
||||
Footer content
|
||||
</@footer>
|
||||
|
||||
Body content
|
||||
</>
|
||||
@ -0,0 +1,2 @@
|
||||
export const skip_dom = true;
|
||||
export const skip_ssr = true;
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -0,0 +1 @@
|
||||
<div/>
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"@item <item>": {
|
||||
"@*": "expression"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<attrs/{ x }/>
|
||||
<hello>
|
||||
<if=x>
|
||||
<@item|y|>
|
||||
${y}
|
||||
</@item>
|
||||
</if>
|
||||
</hello>
|
||||
@ -0,0 +1,2 @@
|
||||
export const skip_dom = true;
|
||||
export const skip_ssr = true;
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -0,0 +1 @@
|
||||
<div/>
|
||||
@ -0,0 +1,13 @@
|
||||
{
|
||||
"@list <list>": {
|
||||
"@items <item>[]": {
|
||||
"@*": "expression"
|
||||
}
|
||||
},
|
||||
"@cols <col>[]": {
|
||||
"@*": "expression",
|
||||
"@rows <row>[]": {
|
||||
"@*": "expression"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -0,0 +1,2 @@
|
||||
export const skip_dom = true;
|
||||
export const skip_ssr = true;
|
||||
@ -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);
|
||||
@ -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);
|
||||
@ -0,0 +1,3 @@
|
||||
<hello>
|
||||
<@foo>Foo!</@foo>
|
||||
</hello>
|
||||
@ -0,0 +1,2 @@
|
||||
export const skip_dom = true;
|
||||
export const skip_ssr = true;
|
||||
@ -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>
|
||||
```
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user