@weborigami/origami 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/exports/buildExports.js +3 -3
- package/exports/exports.js +5 -1
- package/package.json +14 -14
- package/src/builtins/@basename.js +6 -0
- package/src/builtins/@explore.js +1 -1
- package/src/builtins/@fetch.js +7 -0
- package/src/builtins/@inline.js +25 -9
- package/src/builtins/@loaders/ori.js +12 -5
- package/src/builtins/@map.js +10 -4
- package/src/builtins/@mapDeep.js +22 -0
- package/src/builtins/@mdHtml.js +5 -0
- package/src/builtins/@once.js +18 -0
- package/src/builtins/@ori.js +5 -1
- package/src/builtins/@package.js +28 -0
- package/src/builtins/@tree/dot.js +5 -8
- package/src/builtins/@tree/sitemap.js +4 -3
- package/src/cli/cli.js +8 -1
- package/src/common/processUnpackedContent.js +5 -3
- package/src/common/utilities.d.ts +2 -0
- package/src/common/utilities.js +26 -1
- package/src/misc/explore.css +88 -0
- package/src/misc/explore.js.inline +119 -0
- package/src/misc/explore.ori +33 -0
- package/src/server/server.js +17 -27
- package/src/builtins/@loaders/orit.js +0 -48
- package/src/misc/explore.orit +0 -241
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2024 Jan Miksovsky and other contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/exports/buildExports.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
|
|
2
2
|
import { keyMapsForExtensions, map } from "@weborigami/async-tree";
|
|
3
|
-
import unpackOrigamiTemplate from "../src/builtins/@loaders/
|
|
3
|
+
import unpackOrigamiTemplate from "../src/builtins/@loaders/ori.js";
|
|
4
4
|
import { transformObject } from "../src/common/utilities.js";
|
|
5
5
|
import PathTransform from "./PathTransform.js";
|
|
6
6
|
|
|
@@ -14,8 +14,8 @@ const specialBuiltinNames = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
// Top-level template for the export file
|
|
17
|
-
const templateText =
|
|
18
|
-
{{ _ }}
|
|
17
|
+
const templateText = `=\`// This file is generated by running buildExports.js -- do not edit by hand.
|
|
18
|
+
{{ _ }}\``;
|
|
19
19
|
|
|
20
20
|
// Generate a top-level export file for the entire project. For each .js file in
|
|
21
21
|
// the given source tree, generate an appropriate statement that includes that
|
package/exports/exports.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// This file is generated by running buildExports.js -- do not edit by hand.
|
|
2
2
|
export { default as apply } from "../src/builtins/@apply.js";
|
|
3
3
|
export { default as arrows } from "../src/builtins/@arrows.js";
|
|
4
|
+
export { default as basename } from "../src/builtins/@basename.js";
|
|
4
5
|
export { default as builtins } from "../src/builtins/@builtins.js";
|
|
5
6
|
export { default as cache } from "../src/builtins/@cache.js";
|
|
6
7
|
export { default as config } from "../src/builtins/@config.js";
|
|
@@ -10,6 +11,7 @@ export { default as debug } from "../src/builtins/@debug.js";
|
|
|
10
11
|
export { default as document } from "../src/builtins/@document.js";
|
|
11
12
|
export { default as equals } from "../src/builtins/@equals.js";
|
|
12
13
|
export { default as explore } from "../src/builtins/@explore.js";
|
|
14
|
+
export { default as fetch } from "../src/builtins/@fetch.js";
|
|
13
15
|
export { default as files } from "../src/builtins/@files.js";
|
|
14
16
|
export { default as filter } from "../src/builtins/@filter.js";
|
|
15
17
|
export { default as globs } from "../src/builtins/@globs.js";
|
|
@@ -33,19 +35,21 @@ export { default as loadersJson } from "../src/builtins/@loaders/json.js";
|
|
|
33
35
|
export { default as loadersMd } from "../src/builtins/@loaders/md.js";
|
|
34
36
|
export { default as loadersMjs } from "../src/builtins/@loaders/mjs.js";
|
|
35
37
|
export { default as loadersOri } from "../src/builtins/@loaders/ori.js";
|
|
36
|
-
export { default as loadersOrit } from "../src/builtins/@loaders/orit.js";
|
|
37
38
|
export { default as loadersTxt } from "../src/builtins/@loaders/txt.js";
|
|
38
39
|
export { default as loadersXhtml } from "../src/builtins/@loaders/xhtml.js";
|
|
39
40
|
export { default as loadersYaml } from "../src/builtins/@loaders/yaml.js";
|
|
40
41
|
export { default as loadersYml } from "../src/builtins/@loaders/yml.js";
|
|
41
42
|
export { default as map } from "../src/builtins/@map.js";
|
|
43
|
+
export { default as mapDeep } from "../src/builtins/@mapDeep.js";
|
|
42
44
|
export { default as match } from "../src/builtins/@match.js";
|
|
43
45
|
export { default as mdHtml } from "../src/builtins/@mdHtml.js";
|
|
44
46
|
export { default as node } from "../src/builtins/@node.js";
|
|
45
47
|
export { default as not } from "../src/builtins/@not.js";
|
|
48
|
+
export { default as once } from "../src/builtins/@once.js";
|
|
46
49
|
export { default as or } from "../src/builtins/@or.js";
|
|
47
50
|
export { default as ori } from "../src/builtins/@ori.js";
|
|
48
51
|
export { default as pack } from "../src/builtins/@pack.js";
|
|
52
|
+
export { default as package } from "../src/builtins/@package.js";
|
|
49
53
|
export { default as parseJson } from "../src/builtins/@parse/json.js";
|
|
50
54
|
export { default as parseYaml } from "../src/builtins/@parse/yaml.js";
|
|
51
55
|
export { default as project } from "../src/builtins/@project.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/origami",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"description": "Web Origami language, CLI, framework, and server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,23 +13,23 @@
|
|
|
13
13
|
"main": "./exports/exports.js",
|
|
14
14
|
"types": "./index.ts",
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/chai": "4.3.
|
|
17
|
-
"@types/mocha": "10.0.
|
|
18
|
-
"@types/node": "20.
|
|
19
|
-
"typescript": "5.
|
|
16
|
+
"@types/chai": "4.3.11",
|
|
17
|
+
"@types/mocha": "10.0.6",
|
|
18
|
+
"@types/node": "20.11.3",
|
|
19
|
+
"typescript": "5.3.3"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@weborigami/async-tree": "
|
|
23
|
-
"@weborigami/language": "
|
|
24
|
-
"@weborigami/types": "0.0.
|
|
22
|
+
"@weborigami/async-tree": "0.0.39",
|
|
23
|
+
"@weborigami/language": "0.0.39",
|
|
24
|
+
"@weborigami/types": "0.0.39",
|
|
25
25
|
"graphviz-wasm": "3.0.1",
|
|
26
26
|
"highlight.js": "11.9.0",
|
|
27
|
-
"marked": "
|
|
28
|
-
"marked-gfm-heading-id": "3.1.
|
|
29
|
-
"marked-highlight": "2.0
|
|
30
|
-
"marked-smartypants": "1.1.
|
|
31
|
-
"sharp": "0.
|
|
32
|
-
"yaml": "2.3.
|
|
27
|
+
"marked": "11.1.1",
|
|
28
|
+
"marked-gfm-heading-id": "3.1.2",
|
|
29
|
+
"marked-highlight": "2.1.0",
|
|
30
|
+
"marked-smartypants": "1.1.5",
|
|
31
|
+
"sharp": "0.33.2",
|
|
32
|
+
"yaml": "2.3.4"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "ori exports/buildExports.js src > exports/exports.js",
|
package/src/builtins/@explore.js
CHANGED
|
@@ -16,7 +16,7 @@ const miscFiles = Scope.treeWithScope(new OrigamiFiles(miscDir), builtins);
|
|
|
16
16
|
*/
|
|
17
17
|
export default async function explore() {
|
|
18
18
|
const scope = Scope.getScope(this);
|
|
19
|
-
const templateFile = await miscFiles.get("explore.
|
|
19
|
+
const templateFile = await miscFiles.get("explore.ori");
|
|
20
20
|
const template = await templateFile.unpack();
|
|
21
21
|
|
|
22
22
|
const data = await getScopeData(scope);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default async function fetchBuiltin(href) {
|
|
2
|
+
const response = await fetch(href);
|
|
3
|
+
return response.ok ? await response.arrayBuffer() : undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
fetchBuiltin.usage = `@fetch href\tReturns the contents of the given URL as an ArrayBuffer`;
|
|
7
|
+
fetchBuiltin.documentation = "https://weborigami.org/languages/@fetch.html";
|
package/src/builtins/@inline.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import unpackText from "../builtins/@loaders/txt.js";
|
|
2
|
+
import * as utilities from "../common/utilities.js";
|
|
1
3
|
import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
|
|
2
|
-
import
|
|
4
|
+
import unpackOrigamiExpression from "./@loaders/ori.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Inline any Origami expressions found inside {{...}} placeholders in the input
|
|
@@ -13,16 +15,30 @@ import unpackOrigamiTemplate from "./@loaders/orit.js";
|
|
|
13
15
|
*/
|
|
14
16
|
export default async function inline(input) {
|
|
15
17
|
assertScopeIsDefined(this);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
// Get the input text and any attached front matter.
|
|
20
|
+
let inputDocument;
|
|
21
|
+
if (input["@text"]) {
|
|
22
|
+
inputDocument = input;
|
|
23
|
+
} else if (/** @type {any} */ (input).unpack) {
|
|
24
|
+
// Have the input unpack itself.
|
|
25
|
+
inputDocument = await /** @type {any} */ (input).unpack();
|
|
26
|
+
} else {
|
|
27
|
+
// Unpack the input as a text document with possible front matter.
|
|
28
|
+
inputDocument = await unpackText(input);
|
|
18
29
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
30
|
+
|
|
31
|
+
// Treat the input text as the body of an Origami template literal.
|
|
32
|
+
const inputText = utilities.toString(inputDocument);
|
|
33
|
+
const templateDocument = Object.assign({}, inputDocument, {
|
|
34
|
+
"@text": `=\`${inputText}\``,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const templateFn = await unpackOrigamiExpression(templateDocument);
|
|
38
|
+
const templateResult = await templateFn(inputDocument);
|
|
23
39
|
return inputDocument
|
|
24
|
-
? Object.assign({}, inputDocument, { "@text": String(
|
|
25
|
-
:
|
|
40
|
+
? Object.assign({}, inputDocument, { "@text": String(templateResult) })
|
|
41
|
+
: templateResult;
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
inline.usage = `@inline <text>\tInline Origami expressions found in the text`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Scope } from "@weborigami/language";
|
|
2
2
|
import * as compile from "../../../../language/src/compiler/compile.js";
|
|
3
3
|
import processUnpackedContent from "../../common/processUnpackedContent.js";
|
|
4
|
+
import * as utilities from "../../common/utilities.js";
|
|
4
5
|
import builtins from "../@builtins.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -8,14 +9,20 @@ import builtins from "../@builtins.js";
|
|
|
8
9
|
*
|
|
9
10
|
* @type {import("@weborigami/language").FileUnpackFunction}
|
|
10
11
|
*/
|
|
11
|
-
export default async function unpackOrigamiExpression(
|
|
12
|
-
|
|
12
|
+
export default async function unpackOrigamiExpression(
|
|
13
|
+
inputDocument,
|
|
14
|
+
options = {}
|
|
15
|
+
) {
|
|
16
|
+
const parent =
|
|
17
|
+
options.parent ??
|
|
18
|
+
/** @type {any} */ (inputDocument).parent ??
|
|
19
|
+
/** @type {any} */ (inputDocument)[utilities.parentSymbol];
|
|
13
20
|
|
|
14
21
|
// Compile the body text as an Origami expression and evaluate it.
|
|
15
|
-
const
|
|
16
|
-
const fn = compile.expression(
|
|
22
|
+
const inputText = utilities.toString(inputDocument);
|
|
23
|
+
const fn = compile.expression(inputText);
|
|
17
24
|
const parentScope = parent ? Scope.getScope(parent) : builtins;
|
|
18
25
|
let content = await fn.call(parentScope);
|
|
19
26
|
|
|
20
|
-
return processUnpackedContent(content, parent);
|
|
27
|
+
return processUnpackedContent(content, parent, inputDocument);
|
|
21
28
|
}
|
package/src/builtins/@map.js
CHANGED
|
@@ -74,6 +74,7 @@ export default function treeMap(param1, param2) {
|
|
|
74
74
|
inverseKeyMap,
|
|
75
75
|
keyMap,
|
|
76
76
|
keyName,
|
|
77
|
+
needsSourceValue,
|
|
77
78
|
valueName,
|
|
78
79
|
} = options;
|
|
79
80
|
|
|
@@ -105,7 +106,7 @@ export default function treeMap(param1, param2) {
|
|
|
105
106
|
|
|
106
107
|
// Extend the key function to include the value and key in scope.
|
|
107
108
|
let extendedKeyMap;
|
|
108
|
-
let
|
|
109
|
+
let extendedInverseKeyMap;
|
|
109
110
|
if (extensions) {
|
|
110
111
|
let { resultExtension, sourceExtension } = parseExtensions(extensions);
|
|
111
112
|
const keyFns = keyMapsForExtensions({
|
|
@@ -113,7 +114,7 @@ export default function treeMap(param1, param2) {
|
|
|
113
114
|
sourceExtension,
|
|
114
115
|
});
|
|
115
116
|
extendedKeyMap = keyFns.keyMap;
|
|
116
|
-
|
|
117
|
+
extendedInverseKeyMap = keyFns.inverseKeyMap;
|
|
117
118
|
} else if (keyMap) {
|
|
118
119
|
const resolvedKeyFn = toFunction(keyMap);
|
|
119
120
|
async function scopedKeyFn(sourceKey, tree) {
|
|
@@ -135,7 +136,11 @@ export default function treeMap(param1, param2) {
|
|
|
135
136
|
}
|
|
136
137
|
const keyFns = cachedKeyMaps(scopedKeyFn);
|
|
137
138
|
extendedKeyMap = keyFns.keyMap;
|
|
138
|
-
|
|
139
|
+
extendedInverseKeyMap = keyFns.inverseKeyMap;
|
|
140
|
+
} else {
|
|
141
|
+
// Use sidecar keyMap/inverseKeyMap functions if the valueMap defines them.
|
|
142
|
+
extendedKeyMap = valueMap?.keyMap;
|
|
143
|
+
extendedInverseKeyMap = valueMap?.inverseKeyMap;
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
const transform = function mapTreelike(treelike) {
|
|
@@ -143,8 +148,9 @@ export default function treeMap(param1, param2) {
|
|
|
143
148
|
return map({
|
|
144
149
|
deep,
|
|
145
150
|
description,
|
|
146
|
-
inverseKeyMap:
|
|
151
|
+
inverseKeyMap: extendedInverseKeyMap,
|
|
147
152
|
keyMap: extendedKeyMap,
|
|
153
|
+
needsSourceValue,
|
|
148
154
|
valueMap: extendedValueFn,
|
|
149
155
|
})(tree);
|
|
150
156
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isPlainObject } from "@weborigami/async-tree";
|
|
2
|
+
import treeMap from "./@map.js";
|
|
3
|
+
|
|
4
|
+
export default function mapDeep(param1, param2) {
|
|
5
|
+
// Identify whether the valueMap/options are the first parameter
|
|
6
|
+
// or the second.
|
|
7
|
+
let source;
|
|
8
|
+
let options;
|
|
9
|
+
if (param2 === undefined) {
|
|
10
|
+
options = param1;
|
|
11
|
+
} else {
|
|
12
|
+
source = param1;
|
|
13
|
+
if (isPlainObject(param2)) {
|
|
14
|
+
options = param2;
|
|
15
|
+
} else {
|
|
16
|
+
options = { valueMap: param2 };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
options.deep = true;
|
|
21
|
+
return treeMap(source, options);
|
|
22
|
+
}
|
package/src/builtins/@mdHtml.js
CHANGED
|
@@ -3,6 +3,7 @@ import { marked } from "marked";
|
|
|
3
3
|
import { gfmHeadingId as markedGfmHeadingId } from "marked-gfm-heading-id";
|
|
4
4
|
import { markedHighlight } from "marked-highlight";
|
|
5
5
|
import { markedSmartypants } from "marked-smartypants";
|
|
6
|
+
import { replaceExtension } from "../common/utilities.js";
|
|
6
7
|
|
|
7
8
|
marked.use(
|
|
8
9
|
markedGfmHeadingId(),
|
|
@@ -41,5 +42,9 @@ export default async function mdHtml(input) {
|
|
|
41
42
|
: html;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
mdHtml.keyMap = (sourceKey) => replaceExtension(sourceKey, ".md", ".html");
|
|
46
|
+
mdHtml.inverseKeyMap = (resultKey) =>
|
|
47
|
+
replaceExtension(resultKey, ".html", ".md");
|
|
48
|
+
|
|
44
49
|
mdHtml.usage = `@mdHtml <markdown>\tRender the markdown text as HTML`;
|
|
45
50
|
mdHtml.documentation = "https://weborigami.org/language/@mdHtml.html";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
|
|
2
|
+
|
|
3
|
+
const fnPromiseMap = new WeakMap();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Evaluate the given function only once and cache the result.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
9
|
+
* @this {AsyncTree|null}
|
|
10
|
+
* @param {Function} fn
|
|
11
|
+
*/
|
|
12
|
+
export default async function once(fn) {
|
|
13
|
+
assertScopeIsDefined(this);
|
|
14
|
+
if (!fnPromiseMap.has(fn)) {
|
|
15
|
+
fnPromiseMap.set(fn, fn.call(this));
|
|
16
|
+
}
|
|
17
|
+
return fnPromiseMap.get(fn);
|
|
18
|
+
}
|
package/src/builtins/@ori.js
CHANGED
|
@@ -39,7 +39,11 @@ export default async function ori(expression) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async function formatResult(result) {
|
|
42
|
-
if (
|
|
42
|
+
if (
|
|
43
|
+
typeof result === "string" ||
|
|
44
|
+
result instanceof ArrayBuffer ||
|
|
45
|
+
result instanceof TypedArray
|
|
46
|
+
) {
|
|
43
47
|
// Use as is
|
|
44
48
|
return result;
|
|
45
49
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Tree, keysFromPath } from "@weborigami/async-tree";
|
|
2
|
+
import { Scope } from "@weborigami/language";
|
|
3
|
+
import project from "./@project.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @this {import("@weborigami/types").AsyncTree|null}
|
|
7
|
+
* @param {string[]} packageKeys
|
|
8
|
+
*/
|
|
9
|
+
export default async function packageBuiltin(...packageKeys) {
|
|
10
|
+
let scope = this;
|
|
11
|
+
if (!scope) {
|
|
12
|
+
const projectRoot = await project.call(null);
|
|
13
|
+
scope = Scope.getScope(projectRoot);
|
|
14
|
+
}
|
|
15
|
+
const packageRoot = await Tree.traverse(
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
scope,
|
|
18
|
+
"node_modules",
|
|
19
|
+
...packageKeys
|
|
20
|
+
);
|
|
21
|
+
const mainPath = await Tree.traverse(packageRoot, "package.json", "main");
|
|
22
|
+
const mainKeys = keysFromPath(mainPath);
|
|
23
|
+
const mainContainerKeys = mainKeys.slice(0, -1);
|
|
24
|
+
const mainFileName = mainKeys[mainKeys.length - 1];
|
|
25
|
+
const mainContainer = await Tree.traverse(packageRoot, ...mainContainerKeys);
|
|
26
|
+
const packageExports = await mainContainer.import(mainFileName);
|
|
27
|
+
return packageExports;
|
|
28
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Tree, isPlainObject, isStringLike } from "@weborigami/async-tree";
|
|
2
2
|
import { extname } from "@weborigami/language";
|
|
3
3
|
import * as serialize from "../../common/serialize.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
hasNonPrintableCharacters,
|
|
6
|
+
keySymbol,
|
|
7
|
+
} from "../../common/utilities.js";
|
|
5
8
|
import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -36,12 +39,6 @@ ${treeArcs.join("\n")}
|
|
|
36
39
|
}`;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
// Return true if the text appears to contain non-printable binary characters.
|
|
40
|
-
function probablyBinary(text) {
|
|
41
|
-
// https://stackoverflow.com/a/1677660/76472
|
|
42
|
-
return /[\x00-\x08\x0E-\x1F]/.test(text);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
42
|
async function statements(tree, nodePath, nodeLabel, options) {
|
|
46
43
|
let result = [];
|
|
47
44
|
const createLinks = options.createLinks ?? true;
|
|
@@ -110,7 +107,7 @@ async function statements(tree, nodePath, nodeLabel, options) {
|
|
|
110
107
|
let i = 0;
|
|
111
108
|
for (const key of Object.keys(nodes)) {
|
|
112
109
|
let label = String(nodes[key].label);
|
|
113
|
-
if (
|
|
110
|
+
if (hasNonPrintableCharacters(label)) {
|
|
114
111
|
nodes[key].label = "[binary data]";
|
|
115
112
|
} else if (label) {
|
|
116
113
|
let clippedStart = false;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Tree } from "@weborigami/async-tree";
|
|
2
2
|
import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
|
|
3
3
|
import builtins from "../@builtins.js";
|
|
4
|
-
import
|
|
4
|
+
import unpackOrigamiExpression from "../@loaders/ori.js";
|
|
5
5
|
import paths from "./paths.js";
|
|
6
6
|
|
|
7
|
-
const templateText =
|
|
7
|
+
const templateText = `=\`<?xml version="1.0" encoding="UTF-8"?>
|
|
8
8
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
9
9
|
{{ @map(=\`
|
|
10
10
|
<url>
|
|
@@ -12,6 +12,7 @@ const templateText = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
12
12
|
</url>
|
|
13
13
|
\`)(_) }}
|
|
14
14
|
</urlset>
|
|
15
|
+
\`
|
|
15
16
|
`;
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -50,7 +51,7 @@ export default async function sitemap(treelike, baseHref = "") {
|
|
|
50
51
|
.filter((path) => path.endsWith(".html"))
|
|
51
52
|
.map((path) => (path.endsWith("index.html") ? path.slice(0, -10) : path));
|
|
52
53
|
|
|
53
|
-
const templateFn = await
|
|
54
|
+
const templateFn = await unpackOrigamiExpression(templateText);
|
|
54
55
|
const templateResult = await templateFn.call(builtins, htmlPaths);
|
|
55
56
|
return String(templateResult);
|
|
56
57
|
}
|
package/src/cli/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ import project from "../builtins/@project.js";
|
|
|
9
9
|
import { keySymbol } from "../common/utilities.js";
|
|
10
10
|
import showUsage from "./showUsage.js";
|
|
11
11
|
|
|
12
|
+
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
13
|
+
|
|
12
14
|
async function main(...args) {
|
|
13
15
|
const expression = args.join(" ");
|
|
14
16
|
|
|
@@ -42,7 +44,12 @@ async function main(...args) {
|
|
|
42
44
|
const scope = Scope.getScope(tree);
|
|
43
45
|
const result = await ori.call(scope, expression);
|
|
44
46
|
if (result !== undefined) {
|
|
45
|
-
const output =
|
|
47
|
+
const output =
|
|
48
|
+
result instanceof ArrayBuffer
|
|
49
|
+
? new Uint8Array(result)
|
|
50
|
+
: typeof result === "string" || result instanceof TypedArray
|
|
51
|
+
? result
|
|
52
|
+
: String(result);
|
|
46
53
|
await stdout.write(output);
|
|
47
54
|
|
|
48
55
|
// If stdout points to the console, and the result didn't end in a newline,
|
|
@@ -10,17 +10,19 @@ import builtins from "../builtins/@builtins.js";
|
|
|
10
10
|
*
|
|
11
11
|
* @param {any} content
|
|
12
12
|
* @param {AsyncTree|null} parent
|
|
13
|
-
* @param {any} [
|
|
13
|
+
* @param {any} [inputDocument]
|
|
14
14
|
* @returns
|
|
15
15
|
*/
|
|
16
|
-
export default function processUnpackedContent(content, parent,
|
|
16
|
+
export default function processUnpackedContent(content, parent, inputDocument) {
|
|
17
17
|
if (typeof content === "function") {
|
|
18
18
|
// Wrap the function such to add ambients to the scope.
|
|
19
19
|
const fn = content;
|
|
20
20
|
|
|
21
21
|
// Use the parent's scope, adding any attached data.
|
|
22
22
|
const parentScope = parent ? Scope.getScope(parent) : builtins;
|
|
23
|
-
const extendedScope =
|
|
23
|
+
const extendedScope = Tree.isTreelike(inputDocument)
|
|
24
|
+
? new Scope(inputDocument, parentScope)
|
|
25
|
+
: parentScope;
|
|
24
26
|
|
|
25
27
|
/** @this {AsyncTree|null} */
|
|
26
28
|
async function extendScope(input, ...rest) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
export const keySymbol: unique symbol;
|
|
3
3
|
export const parentSymbol: unique symbol;
|
|
4
|
+
export function hasNonPrintableCharacters(text: string): boolean;
|
|
4
5
|
export function isTransformApplied(Transform: Function, object: any): boolean;
|
|
6
|
+
export function replaceExtension(key: string, sourceExtension: string, resultExtension: string): string;
|
|
5
7
|
export function toFunction(object: any): Function;
|
|
6
8
|
export function toString(object: any): string|null;
|
|
7
9
|
export function transformObject(Transform: Function, object: any): any;
|
package/src/common/utilities.js
CHANGED
|
@@ -3,6 +3,13 @@ import { Tree, isPlainObject, isStringLike } from "@weborigami/async-tree";
|
|
|
3
3
|
const textDecoder = new TextDecoder();
|
|
4
4
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
5
5
|
|
|
6
|
+
// Return true if the text appears to contain non-printable binary characters;
|
|
7
|
+
// used to infer whether a file is binary or text.
|
|
8
|
+
export function hasNonPrintableCharacters(text) {
|
|
9
|
+
// https://stackoverflow.com/a/1677660/76472
|
|
10
|
+
return /[\x00-\x08\x0E-\x1F]/.test(text);
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export function isTransformApplied(Transform, obj) {
|
|
7
14
|
let transformName = Transform.name;
|
|
8
15
|
if (!transformName) {
|
|
@@ -25,6 +32,22 @@ export const keySymbol = Symbol("key");
|
|
|
25
32
|
|
|
26
33
|
export const parentSymbol = Symbol("parent");
|
|
27
34
|
|
|
35
|
+
/**
|
|
36
|
+
* If the given key ends in the source extension (which will generally include a
|
|
37
|
+
* period), replace that extension with the result extension (which again should
|
|
38
|
+
* generally include a period). Otherwise, return the key as is.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} key
|
|
41
|
+
* @param {string} sourceExtension
|
|
42
|
+
* @param {string} resultExtension
|
|
43
|
+
*/
|
|
44
|
+
export function replaceExtension(key, sourceExtension, resultExtension) {
|
|
45
|
+
if (!key.endsWith(sourceExtension)) {
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
return key.slice(0, -sourceExtension.length) + resultExtension;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/**
|
|
29
52
|
* Convert the given object to a function.
|
|
30
53
|
*
|
|
@@ -80,7 +103,9 @@ export function toString(object) {
|
|
|
80
103
|
return object["@text"];
|
|
81
104
|
} else if (object instanceof ArrayBuffer || object instanceof TypedArray) {
|
|
82
105
|
// Serialize data as UTF-8.
|
|
83
|
-
|
|
106
|
+
const decoded = textDecoder.decode(object);
|
|
107
|
+
// If the result has non-printable characters, it's probably not a string.
|
|
108
|
+
return hasNonPrintableCharacters(decoded) ? null : decoded;
|
|
84
109
|
} else if (isStringLike(object)) {
|
|
85
110
|
return String(object);
|
|
86
111
|
} else {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
html {
|
|
6
|
+
height: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
background: #333;
|
|
11
|
+
color: #eee;
|
|
12
|
+
display: grid;
|
|
13
|
+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
grid-template-columns: 200px 1fr;
|
|
16
|
+
grid-template-rows: minMax(0, 1fr);
|
|
17
|
+
height: 100%;
|
|
18
|
+
margin: 0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
nav {
|
|
23
|
+
display: grid;
|
|
24
|
+
gap: 1em;
|
|
25
|
+
grid-auto-rows: min-content;
|
|
26
|
+
grid-template-columns: minmax(0, 1fr);
|
|
27
|
+
overflow: auto;
|
|
28
|
+
padding: 1em 0.5em;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#label {
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#scopeToolbar {
|
|
36
|
+
display: grid;
|
|
37
|
+
grid-template-columns: repeat(4, auto);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
button {
|
|
41
|
+
background: transparent;
|
|
42
|
+
border: solid 1px #555;
|
|
43
|
+
color: inherit;
|
|
44
|
+
font-size: smaller;
|
|
45
|
+
font-family: inherit;
|
|
46
|
+
font-weight: inherit;
|
|
47
|
+
padding: 0.25em;
|
|
48
|
+
}
|
|
49
|
+
button:hover {
|
|
50
|
+
border-color: #999;
|
|
51
|
+
}
|
|
52
|
+
button:active {
|
|
53
|
+
border-color: #eee;
|
|
54
|
+
}
|
|
55
|
+
button[aria-pressed="true"] {
|
|
56
|
+
background: #555;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ul {
|
|
60
|
+
list-style: none;
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
h2 {
|
|
66
|
+
color: #999;
|
|
67
|
+
font-size: inherit;
|
|
68
|
+
margin: 0.25em 0;
|
|
69
|
+
padding-left: 0.25em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
li {
|
|
73
|
+
padding: 0.25em;
|
|
74
|
+
padding-left: 1em;
|
|
75
|
+
text-indent: -0.75em;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
a {
|
|
79
|
+
color: inherit;
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
iframe {
|
|
84
|
+
background: white;
|
|
85
|
+
border: none;
|
|
86
|
+
height: 100%;
|
|
87
|
+
width: 100%;
|
|
88
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
let defaultPath;
|
|
2
|
+
let frame;
|
|
3
|
+
|
|
4
|
+
const modes = {
|
|
5
|
+
Content: "",
|
|
6
|
+
Index: "!@index",
|
|
7
|
+
YAML: "!@yaml",
|
|
8
|
+
SVG: "!@svg",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Extract the path from the URL hash.
|
|
12
|
+
function getPathFromHash() {
|
|
13
|
+
return window.location.hash.slice(1); // Remove `#`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getModeFromLocation() {
|
|
17
|
+
const href = document.location.href;
|
|
18
|
+
const match = /[\/](?<command>\!(?:@index|@yaml|@svg))$/.exec(href);
|
|
19
|
+
const command = match?.groups?.command ?? "";
|
|
20
|
+
const mode =
|
|
21
|
+
Object.keys(modes).find((key) => modes[key] === command) ?? "Content";
|
|
22
|
+
return mode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function removeDocumentPath(path) {
|
|
26
|
+
const documentPath = document.location.pathname;
|
|
27
|
+
if (path.startsWith(documentPath)) {
|
|
28
|
+
// Remove the document path prefix.
|
|
29
|
+
path = path.slice(documentPath.length);
|
|
30
|
+
}
|
|
31
|
+
if (path.startsWith("/")) {
|
|
32
|
+
// Remove the leading slash.
|
|
33
|
+
path = path.slice(1);
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function selectMode(newMode) {
|
|
39
|
+
const currentMode = getModeFromLocation();
|
|
40
|
+
if (newMode !== currentMode) {
|
|
41
|
+
let newPath = removeDocumentPath(frame.contentDocument.location.pathname);
|
|
42
|
+
const currentExtension = modes[currentMode];
|
|
43
|
+
if (currentExtension && newPath.endsWith(currentExtension)) {
|
|
44
|
+
// Remove the current extension.
|
|
45
|
+
newPath = newPath.slice(0, -currentExtension.length);
|
|
46
|
+
}
|
|
47
|
+
const newExtension = modes[newMode];
|
|
48
|
+
const separator = newPath.endsWith("/") ? "" : "/";
|
|
49
|
+
const newFullPath = `${newPath}${separator}${newExtension}`;
|
|
50
|
+
setPath(newFullPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setPath(path) {
|
|
55
|
+
// Show the indicated page in the frame.
|
|
56
|
+
const abbreviatedPath = `/${path}`;
|
|
57
|
+
const fullPath = `${document.location.pathname}/${path}`;
|
|
58
|
+
const framePathname = frame.contentDocument.location.pathname;
|
|
59
|
+
if (framePathname !== abbreviatedPath && framePathname !== fullPath) {
|
|
60
|
+
// Use `replace` to avoid affecting browser history.
|
|
61
|
+
frame.contentWindow.location.replace(fullPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If the path ends with a file name corresponding to a mode, select
|
|
65
|
+
// the corresponding mode button.
|
|
66
|
+
const mode = getModeFromLocation();
|
|
67
|
+
const selectedButtonId = `button${mode}`;
|
|
68
|
+
scopeToolbar.querySelectorAll("button").forEach((button) => {
|
|
69
|
+
const pressed = button.id === selectedButtonId ? "true" : "false";
|
|
70
|
+
button.setAttribute("aria-pressed", pressed);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// When hash changes, load the indicated page.
|
|
75
|
+
window.addEventListener("hashchange", () => {
|
|
76
|
+
const hashPath = getPathFromHash();
|
|
77
|
+
const newPath = hashPath !== undefined ? hashPath : defaultPath;
|
|
78
|
+
if (newPath) {
|
|
79
|
+
setPath(newPath);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Initialize
|
|
84
|
+
window.addEventListener("load", () => {
|
|
85
|
+
// Refresh title on page load.
|
|
86
|
+
frame = document.getElementById("frame");
|
|
87
|
+
frame.addEventListener("load", () => {
|
|
88
|
+
if (frame.contentDocument.location.href !== "about:blank") {
|
|
89
|
+
document.title = frame.contentDocument.title;
|
|
90
|
+
const newPath = removeDocumentPath(
|
|
91
|
+
frame.contentDocument.location.pathname
|
|
92
|
+
);
|
|
93
|
+
const hash = `#${newPath}`;
|
|
94
|
+
if (window.location.hash !== hash) {
|
|
95
|
+
// Use `replace` to avoid affecting browser history.
|
|
96
|
+
window.location.replace(hash);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
buttonContent.addEventListener("click", () => {
|
|
102
|
+
selectMode("Content");
|
|
103
|
+
});
|
|
104
|
+
buttonIndex.addEventListener("click", () => {
|
|
105
|
+
selectMode("Index");
|
|
106
|
+
});
|
|
107
|
+
buttonYAML.addEventListener("click", () => {
|
|
108
|
+
selectMode("YAML");
|
|
109
|
+
});
|
|
110
|
+
buttonSVG.addEventListener("click", () => {
|
|
111
|
+
selectMode("SVG");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Navigate to any path already in the hash.
|
|
115
|
+
defaultPath = getPathFromHash();
|
|
116
|
+
if (defaultPath) {
|
|
117
|
+
setPath(defaultPath);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
=`<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Web Origami Explorer</title>
|
|
7
|
+
<style>{{ explore.css }}</style>
|
|
8
|
+
<script>{{ explore.js.inline }}</script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<nav>
|
|
12
|
+
<div id="label">Web Origami Explorer</div>
|
|
13
|
+
<div id="scopeToolbar">
|
|
14
|
+
<button id="buttonContent">Content</button>
|
|
15
|
+
<button id="buttonIndex">Index</button>
|
|
16
|
+
<button id="buttonSVG">SVG</button>
|
|
17
|
+
<button id="buttonYAML">YAML</button>
|
|
18
|
+
</div>
|
|
19
|
+
{{ @map(=`
|
|
20
|
+
<ul>
|
|
21
|
+
<h2>{{ _/name }}</h2>
|
|
22
|
+
{{ @map(=`
|
|
23
|
+
<li>
|
|
24
|
+
<a href="./!@explore/{{ _ }}" target="frame">{{ _ }}</a>
|
|
25
|
+
</li>
|
|
26
|
+
`)(_/keys) }}
|
|
27
|
+
</ul>
|
|
28
|
+
`)(_) }}
|
|
29
|
+
</nav>
|
|
30
|
+
<iframe id="frame" name="frame"></iframe>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
`
|
package/src/server/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@weborigami/async-tree";
|
|
8
8
|
import { Scope, extname } from "@weborigami/language";
|
|
9
9
|
import * as serialize from "../common/serialize.js";
|
|
10
|
+
import { toString } from "../common/utilities.js";
|
|
10
11
|
import { mediaTypeForExtension, mediaTypeIsText } from "./mediaTypes.js";
|
|
11
12
|
|
|
12
13
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
@@ -53,11 +54,14 @@ export async function handleRequest(request, response, tree) {
|
|
|
53
54
|
// For parsing purposes, we assume HTTPS -- it doesn't affect parsing.
|
|
54
55
|
const url = new URL(request.url, `https://${request.headers.host}`);
|
|
55
56
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
// Split on occurrences of `/!`, which represent Origami debug commands.
|
|
58
|
+
// Command arguments can contain slashes; don't treat those as path keys.
|
|
59
|
+
const parts = url.pathname.split(/\/!/);
|
|
60
|
+
const keys = parts.flatMap((part, index) => {
|
|
61
|
+
const decoded = decodeURIComponent(part);
|
|
62
|
+
// Split keys that aren't commands; add back the `!` to commands.
|
|
63
|
+
return index % 2 === 0 ? keysFromPath(decoded) : `!${decoded}`;
|
|
64
|
+
});
|
|
61
65
|
|
|
62
66
|
// If the path ends with a trailing slash, the final key will be an empty
|
|
63
67
|
// string. Change that to "index.html".
|
|
@@ -131,7 +135,7 @@ export async function handleRequest(request, response, tree) {
|
|
|
131
135
|
|
|
132
136
|
let data;
|
|
133
137
|
if (mediaType) {
|
|
134
|
-
data = mediaTypeIsText[mediaType] ?
|
|
138
|
+
data = mediaTypeIsText[mediaType] ? toString(resource) : resource;
|
|
135
139
|
} else {
|
|
136
140
|
data = textOrObject(resource);
|
|
137
141
|
}
|
|
@@ -152,7 +156,7 @@ export async function handleRequest(request, response, tree) {
|
|
|
152
156
|
const validResponse = typeof data === "string" || data instanceof TypedArray;
|
|
153
157
|
|
|
154
158
|
if (!validResponse) {
|
|
155
|
-
const typeName = data
|
|
159
|
+
const typeName = data?.constructor?.name ?? typeof data;
|
|
156
160
|
console.error(
|
|
157
161
|
`A served tree must return a string or a TypedArray (such as a Buffer) but returned an instance of ${typeName}.`
|
|
158
162
|
);
|
|
@@ -233,26 +237,12 @@ ${message}
|
|
|
233
237
|
* Convert to a string if we can, but leave objects that convert to something
|
|
234
238
|
* like "[object Object]" alone.
|
|
235
239
|
*
|
|
236
|
-
* @param {any}
|
|
240
|
+
* @param {any} object
|
|
237
241
|
*/
|
|
238
|
-
function textOrObject(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// See if we can convert the object to a string.
|
|
245
|
-
const text = String(obj);
|
|
246
|
-
|
|
247
|
-
// See if we ended up with a default string.
|
|
248
|
-
const constructor = obj.constructor;
|
|
249
|
-
const name = constructor.name || "Object";
|
|
250
|
-
if (text === `[object Object]` || text === `[object ${name}]`) {
|
|
251
|
-
// Got a default string, so probably not what we wanted.
|
|
252
|
-
// Return original object.
|
|
253
|
-
return obj;
|
|
254
|
-
} else {
|
|
255
|
-
// We appear to have cast the object to a string; return that.
|
|
256
|
-
return text;
|
|
242
|
+
function textOrObject(object) {
|
|
243
|
+
// Return buffers and typed arrays as is.
|
|
244
|
+
if (object instanceof ArrayBuffer || object instanceof TypedArray) {
|
|
245
|
+
return object;
|
|
257
246
|
}
|
|
247
|
+
return toString(object);
|
|
258
248
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Scope } from "@weborigami/language";
|
|
2
|
-
import * as compile from "../../../../language/src/compiler/compile.js";
|
|
3
|
-
import processUnpackedContent from "../../common/processUnpackedContent.js";
|
|
4
|
-
import * as utilities from "../../common/utilities.js";
|
|
5
|
-
import builtins from "../@builtins.js";
|
|
6
|
-
import unpackText from "./txt.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Load and evaluate an Origami template from a file.
|
|
10
|
-
*
|
|
11
|
-
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
12
|
-
* @type {import("@weborigami/language").FileUnpackFunction}
|
|
13
|
-
*/
|
|
14
|
-
export default async function unpackOrigamiTemplate(input, options = {}) {
|
|
15
|
-
const parent =
|
|
16
|
-
options.parent ??
|
|
17
|
-
/** @type {any} */ (input).parent ??
|
|
18
|
-
/** @type {any} */ (input)[utilities.parentSymbol];
|
|
19
|
-
|
|
20
|
-
// Get the input text and any attached front matter.
|
|
21
|
-
let inputDocument;
|
|
22
|
-
if (input["@text"]) {
|
|
23
|
-
inputDocument = input;
|
|
24
|
-
} else {
|
|
25
|
-
// Unpack the input as a text document with possible front matter.
|
|
26
|
-
inputDocument = await unpackText(input, options);
|
|
27
|
-
}
|
|
28
|
-
const text = utilities.toString(inputDocument);
|
|
29
|
-
|
|
30
|
-
// Compile the body text as an Origami expression and evaluate it.
|
|
31
|
-
const expression = compile.templateDocument(text);
|
|
32
|
-
const parentScope = parent ? Scope.getScope(parent) : builtins;
|
|
33
|
-
const lambda = await expression.call(parentScope);
|
|
34
|
-
|
|
35
|
-
// Wrap the lambda with a function that will attach the input data to the
|
|
36
|
-
// result.
|
|
37
|
-
/** @this {AsyncTree|null} */
|
|
38
|
-
const fn = async function createTemplateResult(templateInput) {
|
|
39
|
-
const text = await lambda.call(this, templateInput);
|
|
40
|
-
/** @type {any} */
|
|
41
|
-
const result = new String(text);
|
|
42
|
-
result.unpack = () => templateInput;
|
|
43
|
-
return result;
|
|
44
|
-
};
|
|
45
|
-
fn.code = lambda.code;
|
|
46
|
-
|
|
47
|
-
return processUnpackedContent(fn, parent, inputDocument);
|
|
48
|
-
}
|
package/src/misc/explore.orit
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
-
<title>Web Origami Explorer</title>
|
|
7
|
-
<style>
|
|
8
|
-
* {
|
|
9
|
-
box-sizing: border-box;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
html {
|
|
13
|
-
height: 100%;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
body {
|
|
17
|
-
background: #333;
|
|
18
|
-
color: #eee;
|
|
19
|
-
display: grid;
|
|
20
|
-
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
21
|
-
font-size: 13px;
|
|
22
|
-
grid-template-columns: 200px 1fr;
|
|
23
|
-
grid-template-rows: minMax(0, 1fr);
|
|
24
|
-
height: 100%;
|
|
25
|
-
margin: 0;
|
|
26
|
-
overflow: hidden;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
nav {
|
|
30
|
-
display: grid;
|
|
31
|
-
gap: 1em;
|
|
32
|
-
grid-auto-rows: min-content;
|
|
33
|
-
grid-template-columns: minmax(0, 1fr);
|
|
34
|
-
overflow: auto;
|
|
35
|
-
padding: 1em 0.5em;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
#label {
|
|
39
|
-
font-weight: bold;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
#scopeToolbar {
|
|
43
|
-
display: grid;
|
|
44
|
-
grid-template-columns: repeat(4, auto);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
button {
|
|
48
|
-
background: transparent;
|
|
49
|
-
border: solid 1px #555;
|
|
50
|
-
color: inherit;
|
|
51
|
-
font-size: smaller;
|
|
52
|
-
font-family: inherit;
|
|
53
|
-
font-weight: inherit;
|
|
54
|
-
padding: 0.25em;
|
|
55
|
-
}
|
|
56
|
-
button:hover {
|
|
57
|
-
border-color: #999;
|
|
58
|
-
}
|
|
59
|
-
button:active {
|
|
60
|
-
border-color: #eee;
|
|
61
|
-
}
|
|
62
|
-
button[aria-pressed="true"] {
|
|
63
|
-
background: #555;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
ul {
|
|
67
|
-
list-style: none;
|
|
68
|
-
margin: 0;
|
|
69
|
-
padding: 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
h2 {
|
|
73
|
-
color: #999;
|
|
74
|
-
font-size: inherit;
|
|
75
|
-
margin: 0.25em 0;
|
|
76
|
-
padding-left: 0.25em;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
li {
|
|
80
|
-
padding: 0.25em;
|
|
81
|
-
padding-left: 1em;
|
|
82
|
-
text-indent: -0.75em;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
a {
|
|
86
|
-
color: inherit;
|
|
87
|
-
text-decoration: none;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
iframe {
|
|
91
|
-
background: white;
|
|
92
|
-
border: none;
|
|
93
|
-
height: 100%;
|
|
94
|
-
width: 100%;
|
|
95
|
-
}
|
|
96
|
-
</style>
|
|
97
|
-
<script>
|
|
98
|
-
let defaultPath;
|
|
99
|
-
let path;
|
|
100
|
-
let frame;
|
|
101
|
-
|
|
102
|
-
const modes = {
|
|
103
|
-
Content: "",
|
|
104
|
-
Index: "!@index",
|
|
105
|
-
YAML: "!@yaml",
|
|
106
|
-
SVG: "!@svg",
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// Extract the path from the URL hash.
|
|
110
|
-
function getPathFromHash() {
|
|
111
|
-
return window.location.hash.slice(1); // Remove `#`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function getModeFromLocation() {
|
|
115
|
-
const href = document.location.href;
|
|
116
|
-
const match = /[\/](?<command>\!(?:@index|@yaml|@svg))$/.exec(href);
|
|
117
|
-
const command = match?.groups.command ?? "";
|
|
118
|
-
const mode = Object.keys(modes).find(key => modes[key] === command) ?? "Content";
|
|
119
|
-
return mode;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function removeDocumentPath(path) {
|
|
123
|
-
const documentPath = document.location.pathname;
|
|
124
|
-
if (path.startsWith(documentPath)) {
|
|
125
|
-
// Remove the document path prefix.
|
|
126
|
-
path = path.slice(documentPath.length);
|
|
127
|
-
}
|
|
128
|
-
if (path.startsWith("/")) {
|
|
129
|
-
// Remove the leading slash.
|
|
130
|
-
path = path.slice(1);
|
|
131
|
-
}
|
|
132
|
-
return path;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function selectMode(newMode) {
|
|
136
|
-
const currentMode = getModeFromLocation();
|
|
137
|
-
if (newMode !== currentMode) {
|
|
138
|
-
let newPath = removeDocumentPath(frame.contentDocument.location.pathname);
|
|
139
|
-
const currentExtension = modes[currentMode];
|
|
140
|
-
if (currentExtension && newPath.endsWith(currentExtension)) {
|
|
141
|
-
// Remove the current extension.
|
|
142
|
-
newPath = newPath.slice(0, -currentExtension.length);
|
|
143
|
-
}
|
|
144
|
-
const newExtension = modes[newMode];
|
|
145
|
-
const separator = newPath.endsWith("/") ? "" : "/";
|
|
146
|
-
const newFullPath = `${newPath}${separator}${newExtension}`;
|
|
147
|
-
setPath(newFullPath);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function setPath(path) {
|
|
152
|
-
currentPath = path;
|
|
153
|
-
|
|
154
|
-
// Show the indicated page in the frame.
|
|
155
|
-
const abbreviatedPath = `/${path}`;
|
|
156
|
-
const fullPath = `${document.location.pathname}/${path}`;
|
|
157
|
-
const framePathname = frame.contentDocument.location.pathname;
|
|
158
|
-
if (framePathname !== abbreviatedPath && framePathname !== fullPath) {
|
|
159
|
-
// Use `replace` to avoid affecting browser history.
|
|
160
|
-
frame.contentWindow.location.replace(fullPath);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// If the path ends with a file name corresponding to a mode, select
|
|
164
|
-
// the corresponding mode button.
|
|
165
|
-
const mode = getModeFromLocation();
|
|
166
|
-
const selectedButtonId = `button${mode}`;
|
|
167
|
-
scopeToolbar.querySelectorAll("button").forEach(button => {
|
|
168
|
-
const pressed = button.id === selectedButtonId ? "true" : "false";
|
|
169
|
-
button.setAttribute("aria-pressed", pressed);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// When hash changes, load the indicated page.
|
|
174
|
-
window.addEventListener("hashchange", () => {
|
|
175
|
-
const hashPath = getPathFromHash();
|
|
176
|
-
const newPath = hashPath !== undefined ? hashPath : defaultPath;
|
|
177
|
-
if (newPath) {
|
|
178
|
-
setPath(newPath);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Initialize
|
|
183
|
-
window.addEventListener("load", () => {
|
|
184
|
-
// Refresh title on page load.
|
|
185
|
-
frame = document.getElementById("frame");
|
|
186
|
-
frame.addEventListener("load", () => {
|
|
187
|
-
if (frame.contentDocument.location.href !== "about:blank") {
|
|
188
|
-
document.title = frame.contentDocument.title;
|
|
189
|
-
const newPath = removeDocumentPath(frame.contentDocument.location.pathname);
|
|
190
|
-
const hash = `#${newPath}`;
|
|
191
|
-
if (window.location.hash !== hash) {
|
|
192
|
-
// Use `replace` to avoid affecting browser history.
|
|
193
|
-
window.location.replace(hash);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
buttonContent.addEventListener("click", () => {
|
|
199
|
-
selectMode("Content");
|
|
200
|
-
});
|
|
201
|
-
buttonIndex.addEventListener("click", () => {
|
|
202
|
-
selectMode("Index");
|
|
203
|
-
});
|
|
204
|
-
buttonYAML.addEventListener("click", () => {
|
|
205
|
-
selectMode("YAML");
|
|
206
|
-
});
|
|
207
|
-
buttonSVG.addEventListener("click", () => {
|
|
208
|
-
selectMode("SVG");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Navigate to any path already in the hash.
|
|
212
|
-
defaultPath = getPathFromHash();
|
|
213
|
-
if (defaultPath) {
|
|
214
|
-
setPath(defaultPath);
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
</script>
|
|
218
|
-
</head>
|
|
219
|
-
<body>
|
|
220
|
-
<nav>
|
|
221
|
-
<div id="label">Web Origami Explorer</div>
|
|
222
|
-
<div id="scopeToolbar">
|
|
223
|
-
<button id="buttonContent">Content</button>
|
|
224
|
-
<button id="buttonIndex">Index</button>
|
|
225
|
-
<button id="buttonSVG">SVG</button>
|
|
226
|
-
<button id="buttonYAML">YAML</button>
|
|
227
|
-
</div>
|
|
228
|
-
{{ @map(=`
|
|
229
|
-
<ul>
|
|
230
|
-
<h2>{{ _/name }}</h2>
|
|
231
|
-
{{ @map(=`
|
|
232
|
-
<li>
|
|
233
|
-
<a href="./!@explore/{{ _ }}" target="frame">{{ _ }}</a>
|
|
234
|
-
</li>
|
|
235
|
-
`)(_/keys) }}
|
|
236
|
-
</ul>
|
|
237
|
-
`)(_) }}
|
|
238
|
-
</nav>
|
|
239
|
-
<iframe id="frame" name="frame"></iframe>
|
|
240
|
-
</body>
|
|
241
|
-
</html>
|