@weborigami/origami 0.0.38 → 0.0.40
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 +15 -15
- package/src/builtins/@basename.js +6 -0
- package/src/builtins/@explore.js +30 -10
- package/src/builtins/@fetch.js +7 -0
- package/src/builtins/@inline.js +21 -9
- package/src/builtins/@loaders/ori.js +29 -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 +10 -4
- package/src/builtins/@package.js +52 -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/FilterTree.js +4 -2
- package/src/common/GlobTree.js +10 -2
- package/src/common/processUnpackedContent.js +9 -4
- package/src/common/utilities.d.ts +2 -0
- package/src/common/utilities.js +26 -1
- package/src/misc/OriCommandTransform.js +1 -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 +32 -34
- package/src/builtins/@loaders/orit.js +0 -48
- package/src/misc/explore.orit +0 -241
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 {
|
|
@@ -33,7 +33,7 @@ export default function OriCommandTransform(Base) {
|
|
|
33
33
|
ambientsTree[keySymbol] = "ori command";
|
|
34
34
|
const extendedScope = new Scope(ambientsTree, Scope.getScope(this));
|
|
35
35
|
const source = key.slice(1).trim();
|
|
36
|
-
value = await ori.call(extendedScope, source);
|
|
36
|
+
value = await ori.call(extendedScope, source, { formatResult: false });
|
|
37
37
|
|
|
38
38
|
// Ensure this transform is applied to any subtree.
|
|
39
39
|
if (Tree.isAsyncTree(value)) {
|
|
@@ -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);
|
|
@@ -52,18 +53,7 @@ export function treeRouter(tree) {
|
|
|
52
53
|
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
|
-
// We allow the use of %2F in paths as a way to insert a slash into a key, so
|
|
57
|
-
// we parse the path into keys first, then decode them.
|
|
58
|
-
const keys = keysFromPath(url.pathname).map((key) =>
|
|
59
|
-
typeof key === "string" ? decodeURIComponent(key) : key
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// If the path ends with a trailing slash, the final key will be an empty
|
|
63
|
-
// string. Change that to "index.html".
|
|
64
|
-
if (keys[keys.length - 1] === "") {
|
|
65
|
-
keys[keys.length - 1] = "index.html";
|
|
66
|
-
}
|
|
56
|
+
const keys = keysFromUrl(url);
|
|
67
57
|
|
|
68
58
|
const extendedTree =
|
|
69
59
|
url.searchParams && "parent" in tree
|
|
@@ -131,7 +121,7 @@ export async function handleRequest(request, response, tree) {
|
|
|
131
121
|
|
|
132
122
|
let data;
|
|
133
123
|
if (mediaType) {
|
|
134
|
-
data = mediaTypeIsText[mediaType] ?
|
|
124
|
+
data = mediaTypeIsText[mediaType] ? toString(resource) : resource;
|
|
135
125
|
} else {
|
|
136
126
|
data = textOrObject(resource);
|
|
137
127
|
}
|
|
@@ -152,7 +142,7 @@ export async function handleRequest(request, response, tree) {
|
|
|
152
142
|
const validResponse = typeof data === "string" || data instanceof TypedArray;
|
|
153
143
|
|
|
154
144
|
if (!validResponse) {
|
|
155
|
-
const typeName = data
|
|
145
|
+
const typeName = data?.constructor?.name ?? typeof data;
|
|
156
146
|
console.error(
|
|
157
147
|
`A served tree must return a string or a TypedArray (such as a Buffer) but returned an instance of ${typeName}.`
|
|
158
148
|
);
|
|
@@ -172,6 +162,28 @@ export async function handleRequest(request, response, tree) {
|
|
|
172
162
|
return true;
|
|
173
163
|
}
|
|
174
164
|
|
|
165
|
+
function keysFromUrl(url) {
|
|
166
|
+
// Split on occurrences of `/!`, which represent Origami debug commands.
|
|
167
|
+
// Command arguments can contain slashes; don't treat those as path keys.
|
|
168
|
+
const parts = url.pathname.split(/\/!/);
|
|
169
|
+
|
|
170
|
+
// Split everything before the first command by slashes and decode those.
|
|
171
|
+
const path = parts.shift();
|
|
172
|
+
const pathKeys = keysFromPath(path).map((key) => decodeURIComponent(key));
|
|
173
|
+
|
|
174
|
+
// If there are no commands, and the path ends with a trailing slash, the
|
|
175
|
+
// final key will be an empty string. Change that to "index.html".
|
|
176
|
+
if (parts.length === 0 && pathKeys[pathKeys.length - 1] === "") {
|
|
177
|
+
pathKeys[pathKeys.length - 1] = "index.html";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Add back the `!` to commands.
|
|
181
|
+
const commandKeys = parts.map((command) => `!${command}`);
|
|
182
|
+
|
|
183
|
+
const keys = [...pathKeys, ...commandKeys];
|
|
184
|
+
return keys;
|
|
185
|
+
}
|
|
186
|
+
|
|
175
187
|
/**
|
|
176
188
|
* A request listener for use with the node http.createServer and
|
|
177
189
|
* https.createServer calls, letting you serve an async tree as a set of pages.
|
|
@@ -233,26 +245,12 @@ ${message}
|
|
|
233
245
|
* Convert to a string if we can, but leave objects that convert to something
|
|
234
246
|
* like "[object Object]" alone.
|
|
235
247
|
*
|
|
236
|
-
* @param {any}
|
|
248
|
+
* @param {any} object
|
|
237
249
|
*/
|
|
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;
|
|
250
|
+
function textOrObject(object) {
|
|
251
|
+
// Return buffers and typed arrays as is.
|
|
252
|
+
if (object instanceof ArrayBuffer || object instanceof TypedArray) {
|
|
253
|
+
return object;
|
|
257
254
|
}
|
|
255
|
+
return toString(object);
|
|
258
256
|
}
|
|
@@ -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>
|