@weborigami/origami 0.6.15 → 0.6.16
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/main.js +2 -0
- package/package.json +4 -3
- package/src/common/findOpenPort.js +58 -0
- package/src/dev/debug2/debug2.js +39 -28
- package/src/dev/debug2/debugChild.js +5 -10
- package/src/dev/debug2/debugCommands.js +7 -25
- package/src/dev/debug2/debugParent.js +113 -158
- package/src/dev/debug2/debugTransform.js +37 -43
- package/src/dev/debug2/expressionTree.js +2 -4
- package/src/dev/debug2/oriEval.js +43 -11
- package/src/dev/dev.js +0 -1
- package/src/dev/help.yaml +6 -0
- package/src/handlers/origamiHandlers.js +1 -0
- package/src/handlers/xml_handler.js +23 -0
- package/src/origami/domNodeToObject.js +93 -0
- package/src/origami/htmlParse.js +22 -0
- package/src/origami/mdOutline.js +17 -5
- package/src/origami/origami.js +2 -1
- package/src/origami/xmlParse.js +33 -0
- package/src/server/server.js +15 -3
- package/src/origami/htmlDom.js +0 -14
package/main.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { default as documentObject } from "./src/common/documentObject.js";
|
|
2
2
|
export * from "./src/common/serialize.js";
|
|
3
3
|
export { default as debugParent } from "./src/dev/debug2/debugParent.js";
|
|
4
|
+
export { default as debugTransform } from "./src/dev/debug2/debugTransform.js";
|
|
4
5
|
export * as Dev from "./src/dev/dev.js";
|
|
6
|
+
export * from "./src/handlers/origamiHandlers.js";
|
|
5
7
|
export * as Origami from "./src/origami/origami.js";
|
|
6
8
|
export { default as origamiHighlightDefinition } from "./src/origami/origamiHighlightDefinition.js";
|
|
7
9
|
export { default as constructResponse } from "./src/server/constructResponse.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/origami",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.16",
|
|
4
4
|
"description": "Web Origami language, CLI, framework, and server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hpcc-js/wasm-graphviz": "^1.21.0",
|
|
21
|
-
"@weborigami/async-tree": "0.6.
|
|
21
|
+
"@weborigami/async-tree": "0.6.16",
|
|
22
22
|
"@weborigami/json-feed-to-rss": "1.0.1",
|
|
23
|
-
"@weborigami/language": "0.6.
|
|
23
|
+
"@weborigami/language": "0.6.16",
|
|
24
24
|
"css-tree": "3.1.0",
|
|
25
25
|
"highlight.js": "11.11.1",
|
|
26
26
|
"jsdom": "28.1.0",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"marked-highlight": "2.2.3",
|
|
30
30
|
"marked-smartypants": "1.1.11",
|
|
31
31
|
"sharp": "0.34.5",
|
|
32
|
+
"whatwg-mimetype": "5.0.0",
|
|
32
33
|
"yaml": "2.8.2"
|
|
33
34
|
},
|
|
34
35
|
"scripts": {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PORT = 5000;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return the first open port number on or after the given port number.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} startPort
|
|
9
|
+
* @returns {Promise<number>}
|
|
10
|
+
*/
|
|
11
|
+
export async function findOpenPort(startPort = DEFAULT_PORT) {
|
|
12
|
+
for (let port = startPort; port <= 65535; port++) {
|
|
13
|
+
if (await isPortAvailable(port)) {
|
|
14
|
+
return port;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new Error(`No open port found on or after ${startPort}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check whether a port is available on both IPv4 and IPv6 loopback addresses
|
|
23
|
+
* by attempting TCP connections. On macOS, IPv4 and IPv6 port spaces are
|
|
24
|
+
* independent (IPV6_V6ONLY=1 by default), so a server bound to [::]:PORT is
|
|
25
|
+
* invisible to a 127.0.0.1 bind check. Using connect probes on both loopbacks
|
|
26
|
+
* catches servers regardless of which protocol family they listen on. Any
|
|
27
|
+
* connection error (ECONNREFUSED, EADDRNOTAVAIL, etc.) means nothing is
|
|
28
|
+
* listening there, so the function is safe on systems without IPv6.
|
|
29
|
+
*
|
|
30
|
+
* @param {number} port
|
|
31
|
+
* @returns {Promise<boolean>}
|
|
32
|
+
*/
|
|
33
|
+
async function isPortAvailable(port) {
|
|
34
|
+
const [v4, v6] = await Promise.all([
|
|
35
|
+
isPortListening("127.0.0.1", port),
|
|
36
|
+
isPortListening("::1", port),
|
|
37
|
+
]);
|
|
38
|
+
return !v4 && !v6;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} host
|
|
43
|
+
* @param {number} port
|
|
44
|
+
* @returns {Promise<boolean>}
|
|
45
|
+
*/
|
|
46
|
+
function isPortListening(host, port) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const socket = net.createConnection(port, host);
|
|
49
|
+
socket.once("connect", () => {
|
|
50
|
+
socket.destroy();
|
|
51
|
+
resolve(true);
|
|
52
|
+
});
|
|
53
|
+
socket.once("error", () => {
|
|
54
|
+
socket.destroy();
|
|
55
|
+
resolve(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/dev/debug2/debug2.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OrigamiFileMap } from "@weborigami/language";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import debugParent from "./debugParent.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -9,25 +10,13 @@ import debugParent from "./debugParent.js";
|
|
|
9
10
|
* extract the source code of the expression to be debugged. (If it were
|
|
10
11
|
* evaluated, the function will be called with the result of the expression.)
|
|
11
12
|
*
|
|
12
|
-
* The `options` argument can include:
|
|
13
|
-
* - `enableUnsafeEval`: if true, enables the `!eval` debug command in the child
|
|
14
|
-
* process; default is false
|
|
15
|
-
* - `debugFilesPath`: path to resources that will be added to the served tree
|
|
16
|
-
*
|
|
17
13
|
* @typedef {import("@weborigami/language").RuntimeState} RuntimeState
|
|
18
14
|
* @typedef {import("@weborigami/language").AnnotatedCode} AnnotatedCode
|
|
19
15
|
*
|
|
20
16
|
* @param {AnnotatedCode} code
|
|
21
|
-
* @param {any | RuntimeState} options
|
|
22
17
|
* @param {RuntimeState} state
|
|
23
18
|
*/
|
|
24
|
-
export default async function debug2(code,
|
|
25
|
-
if (state === undefined) {
|
|
26
|
-
// Options were omitted; shift arguments
|
|
27
|
-
state = options;
|
|
28
|
-
options = [];
|
|
29
|
-
}
|
|
30
|
-
|
|
19
|
+
export default async function debug2(code, state) {
|
|
31
20
|
if (
|
|
32
21
|
!(code instanceof Array) ||
|
|
33
22
|
code.source === undefined ||
|
|
@@ -47,23 +36,45 @@ export default async function debug2(code, options, state) {
|
|
|
47
36
|
throw new Error("Dev.debug2 couldn't work out the parent path.");
|
|
48
37
|
}
|
|
49
38
|
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
options = await execute(options, state);
|
|
53
|
-
} else {
|
|
54
|
-
options = {};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// @ts-ignore
|
|
58
|
-
const enableUnsafeEval = options.enableUnsafeEval ?? false;
|
|
59
|
-
const debugFilesPath = options.debugFilesPath ?? "";
|
|
60
|
-
|
|
61
|
-
await debugParent({
|
|
62
|
-
debugFilesPath,
|
|
63
|
-
enableUnsafeEval,
|
|
39
|
+
// Start the debug server
|
|
40
|
+
const server = await debugParent({
|
|
64
41
|
expression,
|
|
65
42
|
parentPath,
|
|
66
43
|
});
|
|
44
|
+
|
|
45
|
+
// Watch the parent files for changes
|
|
46
|
+
const tree = new OrigamiFileMap(parentPath);
|
|
47
|
+
tree.watch();
|
|
48
|
+
tree.addEventListener?.("change", async (event) => {
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
const { filePath } = event.options;
|
|
51
|
+
if (isJavaScriptFile(filePath)) {
|
|
52
|
+
// Need to restart the child process
|
|
53
|
+
console.log("JavaScript file changed, restarting server…");
|
|
54
|
+
await server.restart();
|
|
55
|
+
} else if (path.basename(filePath) === "package.json") {
|
|
56
|
+
// Need to restart the child process
|
|
57
|
+
console.log("package.json changed, restarting server…");
|
|
58
|
+
await server.restart();
|
|
59
|
+
} else {
|
|
60
|
+
// Just have the child reevaluate the expression
|
|
61
|
+
console.log("File changed, reloading site…");
|
|
62
|
+
await server.reevaluate();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// When server closes, stop watching for file changes
|
|
67
|
+
server.on("close", () => {
|
|
68
|
+
tree.unwatch();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(`Server running at ${server.origin}. Press Ctrl+C to stop.`);
|
|
67
72
|
}
|
|
68
73
|
debug2.needsState = true;
|
|
69
74
|
debug2.unevaluatedArgs = true;
|
|
75
|
+
|
|
76
|
+
function isJavaScriptFile(filePath) {
|
|
77
|
+
const extname = path.extname(filePath).toLowerCase();
|
|
78
|
+
const jsExtensions = [".cjs", ".js", ".mjs", ".ts"];
|
|
79
|
+
return jsExtensions.includes(extname);
|
|
80
|
+
}
|
|
@@ -22,13 +22,6 @@ function fail(message) {
|
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/** @type {string} */
|
|
26
|
-
const debugFilesPath = process.env.ORIGAMI_DEBUG_FILES_PATH ?? "";
|
|
27
|
-
|
|
28
|
-
/** @type {boolean} */
|
|
29
|
-
// @ts-ignore
|
|
30
|
-
const enableUnsafeEval = process.env.ORIGAMI_ENABLE_UNSAFE_EVAL === "1";
|
|
31
|
-
|
|
32
25
|
/** @type {string} */
|
|
33
26
|
// @ts-ignore
|
|
34
27
|
const expression = process.env.ORIGAMI_EXPRESSION;
|
|
@@ -43,6 +36,8 @@ if (parentPath === undefined) {
|
|
|
43
36
|
fail("Missing Origami parent");
|
|
44
37
|
}
|
|
45
38
|
|
|
39
|
+
const quiet = process.env.ORIGAMI_QUIET === "1";
|
|
40
|
+
|
|
46
41
|
// An indirect pointer to the tree of resources;
|
|
47
42
|
let treeHandle = {};
|
|
48
43
|
|
|
@@ -50,7 +45,7 @@ let treeHandle = {};
|
|
|
50
45
|
await evaluateExpression();
|
|
51
46
|
|
|
52
47
|
// Serve the tree of resources
|
|
53
|
-
const listener = requestListener(treeHandle);
|
|
48
|
+
const listener = requestListener(treeHandle, { quiet });
|
|
54
49
|
const server = http.createServer(listener);
|
|
55
50
|
|
|
56
51
|
// Track live connections so we can drain/close cleanly.
|
|
@@ -96,10 +91,8 @@ function beginDrain() {
|
|
|
96
91
|
|
|
97
92
|
async function evaluateExpression() {
|
|
98
93
|
const tree = await expressionTree({
|
|
99
|
-
debugFilesPath,
|
|
100
94
|
expression,
|
|
101
95
|
parentPath,
|
|
102
|
-
enableUnsafeEval,
|
|
103
96
|
});
|
|
104
97
|
if (!tree) {
|
|
105
98
|
fail("Dev.debug2: expression did not evaluate to a maplike resource tree");
|
|
@@ -118,6 +111,8 @@ async function evaluateExpression() {
|
|
|
118
111
|
} catch {
|
|
119
112
|
// Ignore errors.
|
|
120
113
|
}
|
|
114
|
+
|
|
115
|
+
process.send?.({ type: "EVALUATED" });
|
|
121
116
|
}
|
|
122
117
|
|
|
123
118
|
function maybeFinishDrain() {
|
|
@@ -1,29 +1,11 @@
|
|
|
1
1
|
// Subset of commands made available via debugTransform
|
|
2
2
|
|
|
3
3
|
import { Tree } from "@weborigami/async-tree";
|
|
4
|
+
export const keys = Tree.keys;
|
|
5
|
+
export const json = Tree.json;
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import oriEval from "./oriEval.js";
|
|
11
|
-
|
|
12
|
-
export default function debugCommands(enableUnsafeEval = false) {
|
|
13
|
-
return Object.assign(
|
|
14
|
-
{
|
|
15
|
-
keys: Tree.keys,
|
|
16
|
-
json: Tree.json,
|
|
17
|
-
index,
|
|
18
|
-
yaml,
|
|
19
|
-
explore,
|
|
20
|
-
svg,
|
|
21
|
-
version,
|
|
22
|
-
},
|
|
23
|
-
enableUnsafeEval
|
|
24
|
-
? {
|
|
25
|
-
eval: oriEval,
|
|
26
|
-
}
|
|
27
|
-
: {},
|
|
28
|
-
);
|
|
29
|
-
}
|
|
7
|
+
export { default as index } from "../../origami/indexPage.js";
|
|
8
|
+
export { default as yaml } from "../../origami/yaml.js";
|
|
9
|
+
export { default as explore } from "../explore.js";
|
|
10
|
+
export { default as svg } from "../svg.js";
|
|
11
|
+
export { default as version } from "../version.js";
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { OrigamiFileMap } from "@weborigami/language";
|
|
2
1
|
import { fork } from "node:child_process";
|
|
3
2
|
import { EventEmitter } from "node:events";
|
|
4
3
|
import http from "node:http";
|
|
5
|
-
import
|
|
6
|
-
import path from "node:path";
|
|
4
|
+
import { findOpenPort } from "../../common/findOpenPort.js";
|
|
7
5
|
|
|
8
6
|
const PUBLIC_HOST = "127.0.0.1";
|
|
9
|
-
const DEFAULT_PORT = 5000;
|
|
10
7
|
|
|
11
8
|
// Module that loads the server in the child process
|
|
12
9
|
const childModuleUrl = new URL("./debugChild.js", import.meta.url);
|
|
@@ -15,9 +12,6 @@ const childModuleUrl = new URL("./debugChild.js", import.meta.url);
|
|
|
15
12
|
let publicServer;
|
|
16
13
|
let publicOrigin;
|
|
17
14
|
|
|
18
|
-
// The tree of files in the parent path, which we watch for changes
|
|
19
|
-
let tree;
|
|
20
|
-
|
|
21
15
|
// The active child process and port
|
|
22
16
|
/** @typedef {import("node:child_process").ChildProcess} ChildProcess */
|
|
23
17
|
/** @typedef {{ process: ChildProcess, port: number | null }} ChildInfo */
|
|
@@ -44,9 +38,6 @@ let emitter = null;
|
|
|
44
38
|
* whenever files in the parent tree change.
|
|
45
39
|
*
|
|
46
40
|
* Supported `options`:
|
|
47
|
-
* - `debugFilesPath`: path to resources that will be added to the served tree
|
|
48
|
-
* - `enableUnsafeEval`: if true, enables the `!eval` debug command in the child
|
|
49
|
-
* process; default is false
|
|
50
41
|
* - `expression` (required): the Origami expression to evaluate in the child
|
|
51
42
|
* process
|
|
52
43
|
* - `parentPath` (required): the path to the parent tree used for evaluation
|
|
@@ -55,47 +46,32 @@ let emitter = null;
|
|
|
55
46
|
* child server encounters an Origami error while handling a request.
|
|
56
47
|
*
|
|
57
48
|
* @param {Object} options
|
|
58
|
-
* @param {string} options.debugFilesPath
|
|
59
|
-
* @param {boolean} options.enableUnsafeEval
|
|
60
49
|
* @param {string} options.expression
|
|
61
50
|
* @param {string} options.parentPath
|
|
51
|
+
* @param {number} [options.port]
|
|
52
|
+
* @param {boolean} [options.quiet]
|
|
62
53
|
*/
|
|
63
54
|
export default async function debugParent(options) {
|
|
64
55
|
const { parentPath } = options;
|
|
56
|
+
if (parentPath === undefined) {
|
|
57
|
+
throw new Error("Debugger couldn't work out the parent path.");
|
|
58
|
+
}
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
tree.watch();
|
|
68
|
-
tree.addEventListener?.("change", (event) => {
|
|
69
|
-
// @ts-ignore
|
|
70
|
-
const { filePath } = event.options;
|
|
71
|
-
if (isJavaScriptFile(filePath)) {
|
|
72
|
-
// Need to restart the child process
|
|
73
|
-
console.log("JavaScript file changed, restarting server…");
|
|
74
|
-
startChild(options);
|
|
75
|
-
} else if (isPackageJsonFile(filePath)) {
|
|
76
|
-
// Need to restart the child process
|
|
77
|
-
console.log("package.json changed, restarting server…");
|
|
78
|
-
startChild(options);
|
|
79
|
-
} else {
|
|
80
|
-
// Just have the child reevaluate the expression
|
|
81
|
-
console.log("File changed, reloading site…");
|
|
82
|
-
activeChild?.process.send({ type: "REEVALUATE" });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const port = await findOpenPort();
|
|
60
|
+
const port = options.port ?? (await findOpenPort());
|
|
87
61
|
publicOrigin = `http://${PUBLIC_HOST}:${port}`;
|
|
88
62
|
|
|
89
63
|
publicServer = http.createServer(proxyRequest);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
64
|
+
await new Promise((resolve) =>
|
|
65
|
+
publicServer.listen(port, PUBLIC_HOST, resolve),
|
|
66
|
+
);
|
|
67
|
+
await startChild(options);
|
|
94
68
|
|
|
95
69
|
emitter = Object.assign(new EventEmitter(), {
|
|
96
70
|
close,
|
|
71
|
+
origin: publicOrigin,
|
|
72
|
+
reevaluate,
|
|
73
|
+
restart: () => startChild(options),
|
|
97
74
|
});
|
|
98
|
-
|
|
99
75
|
return emitter;
|
|
100
76
|
}
|
|
101
77
|
|
|
@@ -110,7 +86,6 @@ async function close() {
|
|
|
110
86
|
publicServer.closeAllConnections();
|
|
111
87
|
await closed;
|
|
112
88
|
publicServer = null;
|
|
113
|
-
emitter = null;
|
|
114
89
|
|
|
115
90
|
// Drain and stop any children concurrently
|
|
116
91
|
const children = [pendingChild?.process, activeChild?.process].filter(
|
|
@@ -121,9 +96,9 @@ async function close() {
|
|
|
121
96
|
activeChild = null;
|
|
122
97
|
await Promise.all(children.map(drainAndStopChild));
|
|
123
98
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
99
|
+
emitter.emit("close");
|
|
100
|
+
emitter.removeAllListeners();
|
|
101
|
+
emitter = null;
|
|
127
102
|
}
|
|
128
103
|
|
|
129
104
|
/**
|
|
@@ -182,66 +157,6 @@ async function drainAndStopChild(childProcess) {
|
|
|
182
157
|
}, GRACE_MS).unref();
|
|
183
158
|
}
|
|
184
159
|
|
|
185
|
-
// Return the first open port number on or after the given port number.
|
|
186
|
-
async function findOpenPort(startPort = DEFAULT_PORT) {
|
|
187
|
-
for (let port = startPort; port <= 65535; port++) {
|
|
188
|
-
if (await isPortAvailable(port)) {
|
|
189
|
-
return port;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
throw new Error(`No open port found on or after ${startPort}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function isJavaScriptFile(filePath) {
|
|
197
|
-
const extname = path.extname(filePath).toLowerCase();
|
|
198
|
-
const jsExtensions = [".cjs", ".js", ".mjs", ".ts"];
|
|
199
|
-
return jsExtensions.includes(extname);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function isPackageJsonFile(filePath) {
|
|
203
|
-
return path.basename(filePath).toLowerCase() === "package.json";
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Check whether a port is available on both IPv4 and IPv6 loopback addresses
|
|
208
|
-
* by attempting TCP connections. On macOS, IPv4 and IPv6 port spaces are
|
|
209
|
-
* independent (IPV6_V6ONLY=1 by default), so a server bound to [::]:PORT is
|
|
210
|
-
* invisible to a 127.0.0.1 bind check. Using connect probes on both loopbacks
|
|
211
|
-
* catches servers regardless of which protocol family they listen on. Any
|
|
212
|
-
* connection error (ECONNREFUSED, EADDRNOTAVAIL, etc.) means nothing is
|
|
213
|
-
* listening there, so the function is safe on systems without IPv6.
|
|
214
|
-
*
|
|
215
|
-
* @param {number} port
|
|
216
|
-
* @returns {Promise<boolean>}
|
|
217
|
-
*/
|
|
218
|
-
async function isPortAvailable(port) {
|
|
219
|
-
const [v4, v6] = await Promise.all([
|
|
220
|
-
isPortListening("127.0.0.1", port),
|
|
221
|
-
isPortListening("::1", port),
|
|
222
|
-
]);
|
|
223
|
-
return !v4 && !v6;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* @param {string} host
|
|
228
|
-
* @param {number} port
|
|
229
|
-
* @returns {Promise<boolean>}
|
|
230
|
-
*/
|
|
231
|
-
function isPortListening(host, port) {
|
|
232
|
-
return new Promise((resolve) => {
|
|
233
|
-
const socket = net.createConnection(port, host);
|
|
234
|
-
socket.once("connect", () => {
|
|
235
|
-
socket.destroy();
|
|
236
|
-
resolve(true);
|
|
237
|
-
});
|
|
238
|
-
socket.once("error", () => {
|
|
239
|
-
socket.destroy();
|
|
240
|
-
resolve(false);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
160
|
/**
|
|
246
161
|
* Proxy incoming requests to the active child server, or return a 503 if not
|
|
247
162
|
* ready.
|
|
@@ -322,6 +237,30 @@ function proxyRequest(request, response) {
|
|
|
322
237
|
request.pipe(upstreamRequest);
|
|
323
238
|
}
|
|
324
239
|
|
|
240
|
+
async function reevaluate() {
|
|
241
|
+
if (!activeChild) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const child = activeChild;
|
|
246
|
+
|
|
247
|
+
// Wait for the next EVALUATED message from the child
|
|
248
|
+
const evaluated = /** @type {Promise<void>} */ (
|
|
249
|
+
new Promise((resolve) => {
|
|
250
|
+
const onMessage = (/** @type {any} */ msg) => {
|
|
251
|
+
if (msg && typeof msg === "object" && msg.type === "EVALUATED") {
|
|
252
|
+
child.process.off("message", onMessage);
|
|
253
|
+
resolve();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
child.process.on("message", onMessage);
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
child.process.send({ type: "REEVALUATE" });
|
|
261
|
+
await evaluated;
|
|
262
|
+
}
|
|
263
|
+
|
|
325
264
|
/**
|
|
326
265
|
* Start a new child process.
|
|
327
266
|
*
|
|
@@ -329,7 +268,8 @@ function proxyRequest(request, response) {
|
|
|
329
268
|
* it becomes active and any previous active child is drained and stopped.
|
|
330
269
|
*/
|
|
331
270
|
function startChild(options) {
|
|
332
|
-
const {
|
|
271
|
+
const { expression, parentPath } = options;
|
|
272
|
+
const quiet = options.quiet ?? false;
|
|
333
273
|
|
|
334
274
|
// Start the child process, passing parent path via an environment variable.
|
|
335
275
|
/** @type {ChildProcess} */
|
|
@@ -339,10 +279,9 @@ function startChild(options) {
|
|
|
339
279
|
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
|
340
280
|
env: {
|
|
341
281
|
...process.env,
|
|
342
|
-
ORIGAMI_DEBUG_FILES_PATH: debugFilesPath,
|
|
343
|
-
ORIGAMI_ENABLE_UNSAFE_EVAL: enableUnsafeEval ? "1" : "0",
|
|
344
282
|
ORIGAMI_EXPRESSION: expression,
|
|
345
283
|
ORIGAMI_PARENT_PATH: parentPath,
|
|
284
|
+
ORIGAMI_QUIET: quiet ? "1" : "0",
|
|
346
285
|
},
|
|
347
286
|
});
|
|
348
287
|
} catch (error) {
|
|
@@ -354,61 +293,77 @@ function startChild(options) {
|
|
|
354
293
|
// This becomes the pending child immediately
|
|
355
294
|
pendingChild = { process: childProcess, port: null };
|
|
356
295
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
296
|
+
// Returns a Promise that resolves when the child signals READY, or rejects
|
|
297
|
+
// on FATAL or unexpected exit before ready.
|
|
298
|
+
return /** @type {Promise<void>} */ (
|
|
299
|
+
new Promise((resolve, reject) => {
|
|
300
|
+
// Listen for messages from the child about its status
|
|
301
|
+
childProcess.on("message", (/** @type {any} */ message) => {
|
|
302
|
+
if (!message || typeof message !== "object") {
|
|
303
|
+
return;
|
|
304
|
+
} else if (
|
|
305
|
+
message.type === "READY" &&
|
|
306
|
+
typeof message.port === "number"
|
|
307
|
+
) {
|
|
308
|
+
// Only promote to active if this is still the pending child
|
|
309
|
+
if (pendingChild?.process === childProcess) {
|
|
310
|
+
const previousChild = activeChild;
|
|
311
|
+
|
|
312
|
+
activeChild = pendingChild;
|
|
313
|
+
pendingChild.port = message.port;
|
|
314
|
+
pendingChild = null;
|
|
315
|
+
|
|
316
|
+
// Drain previous child in background (don't wait)
|
|
317
|
+
if (
|
|
318
|
+
previousChild?.process &&
|
|
319
|
+
previousChild.process !== childProcess
|
|
320
|
+
) {
|
|
321
|
+
drainAndStopChild(previousChild.process).catch((err) =>
|
|
322
|
+
console.error("[drain]", err),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (emitter) {
|
|
327
|
+
emitter.emit("ready", { origin: publicOrigin });
|
|
328
|
+
}
|
|
329
|
+
resolve();
|
|
330
|
+
} else {
|
|
331
|
+
// This child was superseded by a newer one, kill it
|
|
332
|
+
// console.log("Child process superseded by newer one, killing it...");
|
|
333
|
+
childProcess.kill("SIGTERM");
|
|
334
|
+
}
|
|
335
|
+
} else if (message.type === "EVALUATED") {
|
|
336
|
+
// Let caller know child has reevaluated the expression (after a file change)
|
|
337
|
+
if (emitter) {
|
|
338
|
+
emitter.emit("evaluated");
|
|
339
|
+
}
|
|
340
|
+
} else if (message.type === "FATAL") {
|
|
341
|
+
// Child couldn't start (import error, etc.)
|
|
342
|
+
// Keep previous active child if any; otherwise we'll serve 500/503.
|
|
343
|
+
console.error("[child fatal]", message.error ?? message);
|
|
344
|
+
if (pendingChild?.process === childProcess) {
|
|
345
|
+
pendingChild = null;
|
|
346
|
+
}
|
|
347
|
+
reject(new Error(message.error ?? "Child server failed to start"));
|
|
377
348
|
}
|
|
349
|
+
});
|
|
378
350
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
351
|
+
childProcess.on("exit", (code, signal) => {
|
|
352
|
+
if (activeChild?.process === childProcess) {
|
|
353
|
+
// Active child died unexpectedly.
|
|
354
|
+
activeChild = null;
|
|
355
|
+
}
|
|
356
|
+
if (pendingChild?.process === childProcess) {
|
|
357
|
+
pendingChild = null;
|
|
358
|
+
// Child died before it was ready. If child was ready and resolve()
|
|
359
|
+
// was called, when the child exits, then reject() is a no-op.
|
|
360
|
+
reject(
|
|
361
|
+
new Error(
|
|
362
|
+
`Child exited before ready (code=${code}, signal=${signal})`,
|
|
363
|
+
),
|
|
382
364
|
);
|
|
383
|
-
return;
|
|
384
365
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
} else {
|
|
389
|
-
// This child was superseded by a newer one, kill it
|
|
390
|
-
// console.log("Child process superseded by newer one, killing it...");
|
|
391
|
-
childProcess.kill("SIGTERM");
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (message.type === "FATAL") {
|
|
396
|
-
// Child couldn't start (import error, etc.)
|
|
397
|
-
// Keep previous active child if any; otherwise we'll serve 500/503.
|
|
398
|
-
console.error("[child fatal]", message.error ?? message);
|
|
399
|
-
if (pendingChild?.process === childProcess) {
|
|
400
|
-
pendingChild = null;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
childProcess.on("exit", (code, signal) => {
|
|
406
|
-
if (activeChild?.process === childProcess) {
|
|
407
|
-
// Active child died unexpectedly.
|
|
408
|
-
activeChild = null;
|
|
409
|
-
}
|
|
410
|
-
if (pendingChild?.process === childProcess) {
|
|
411
|
-
pendingChild = null;
|
|
412
|
-
}
|
|
413
|
-
});
|
|
366
|
+
});
|
|
367
|
+
})
|
|
368
|
+
);
|
|
414
369
|
}
|
|
@@ -9,10 +9,9 @@ import {
|
|
|
9
9
|
scope,
|
|
10
10
|
trailingSlash,
|
|
11
11
|
} from "@weborigami/async-tree";
|
|
12
|
-
import { OrigamiFileMap } from "@weborigami/language";
|
|
13
12
|
import indexPage from "../../origami/indexPage.js";
|
|
14
13
|
import yaml from "../../origami/yaml.js";
|
|
15
|
-
import debugCommands from "./debugCommands.js";
|
|
14
|
+
import * as debugCommands from "./debugCommands.js";
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Transform the given map-based tree to add debugging resources:
|
|
@@ -23,19 +22,29 @@ import debugCommands from "./debugCommands.js";
|
|
|
23
22
|
*
|
|
24
23
|
* Also transform a simple object result to YAML for viewing.
|
|
25
24
|
*
|
|
26
|
-
* @
|
|
27
|
-
* @
|
|
28
|
-
*
|
|
25
|
+
* @typedef {import("@weborigami/async-tree").Maplike} Maplike
|
|
26
|
+
* @typedef {import("@weborigami/async-tree").Packed} Packed
|
|
27
|
+
*
|
|
28
|
+
* @param {Maplike|Packed} input
|
|
29
29
|
*/
|
|
30
|
-
export default function debugTransform(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
export default function debugTransform(input) {
|
|
31
|
+
if (isUnpackable(input)) {
|
|
32
|
+
// If the value isn't a tree, but has a tree attached via an `unpack`
|
|
33
|
+
// method, destructively wrap the unpack method to add this transform.
|
|
34
|
+
const original = input.unpack.bind(input);
|
|
35
|
+
input.unpack = async () => {
|
|
36
|
+
const content = await original();
|
|
37
|
+
if (!Tree.isTraversable(content) || typeof content === "function") {
|
|
38
|
+
return content;
|
|
39
|
+
}
|
|
40
|
+
/** @type {any} */
|
|
41
|
+
let tree = Tree.from(content);
|
|
42
|
+
return debugTransform(tree);
|
|
43
|
+
};
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
37
46
|
|
|
38
|
-
const
|
|
47
|
+
const source = Tree.from(input, { deep: true });
|
|
39
48
|
|
|
40
49
|
return Object.assign(new AsyncMap(), {
|
|
41
50
|
description: "debug resources",
|
|
@@ -44,22 +53,15 @@ export default function debugTransform(
|
|
|
44
53
|
// Ask the tree if it has the key.
|
|
45
54
|
let value = await source.get(key);
|
|
46
55
|
|
|
47
|
-
if (value === undefined && debugFiles) {
|
|
48
|
-
// Try the debug files
|
|
49
|
-
value = await debugFiles.get(key);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
56
|
if (value === undefined) {
|
|
53
57
|
// Try the defaults and commands
|
|
54
|
-
if (key === "
|
|
55
|
-
return debugFiles;
|
|
56
|
-
} else if (key === "index.html") {
|
|
58
|
+
if (key === "index.html") {
|
|
57
59
|
// Generate an index page for this site
|
|
58
60
|
value = await indexPage(source);
|
|
59
61
|
} else if (key === ".keys.json") {
|
|
60
62
|
value = await jsonKeys.stringify(source);
|
|
61
63
|
} else if (typeof key === "string" && key.startsWith("!")) {
|
|
62
|
-
value = await invokeOrigamiCommand(
|
|
64
|
+
value = await invokeOrigamiCommand(source, key);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -72,29 +74,21 @@ export default function debugTransform(
|
|
|
72
74
|
Tree.merge(object, {
|
|
73
75
|
"index.html": yamlText,
|
|
74
76
|
});
|
|
75
|
-
} else if (
|
|
77
|
+
} else if (
|
|
78
|
+
Tree.isMaplike(value) &&
|
|
79
|
+
!Tree.isMap(value) &&
|
|
80
|
+
typeof value !== "function"
|
|
81
|
+
) {
|
|
76
82
|
// Make it a map so we can debug it
|
|
77
83
|
value = Tree.from(value);
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
value = debugTransform(value
|
|
84
|
-
} else if (value?.unpack) {
|
|
85
|
-
// If the value isn't a tree, but has a tree attached via an `unpack`
|
|
86
|
-
// method, wrap the unpack method to add this transform.
|
|
87
|
-
const original = value.unpack.bind(value);
|
|
88
|
-
value.unpack = async () => {
|
|
89
|
-
const content = await original();
|
|
90
|
-
if (!Tree.isTraversable(content) || typeof content === "function") {
|
|
91
|
-
return content;
|
|
92
|
-
}
|
|
93
|
-
/** @type {any} */
|
|
94
|
-
let tree = Tree.from(content);
|
|
95
|
-
return debugTransform(tree);
|
|
96
|
-
};
|
|
86
|
+
// Ensure this transform is applied to any map result, or any object with
|
|
87
|
+
// an unpack method that returns a map.
|
|
88
|
+
if (Tree.isMap(value) || value?.unpack) {
|
|
89
|
+
value = debugTransform(value);
|
|
97
90
|
}
|
|
91
|
+
|
|
98
92
|
return value;
|
|
99
93
|
},
|
|
100
94
|
|
|
@@ -114,15 +108,15 @@ export default function debugTransform(
|
|
|
114
108
|
});
|
|
115
109
|
}
|
|
116
110
|
|
|
117
|
-
async function invokeOrigamiCommand(
|
|
111
|
+
async function invokeOrigamiCommand(tree, key) {
|
|
118
112
|
// Key is an Origami command; invoke it.
|
|
119
113
|
const commandName = trailingSlash.remove(key.slice(1).trim());
|
|
120
114
|
|
|
121
115
|
// Look for the indicated command
|
|
122
|
-
const command =
|
|
116
|
+
const command = debugCommands[commandName];
|
|
123
117
|
let value;
|
|
124
118
|
if (command) {
|
|
125
|
-
value = await command(tree);
|
|
119
|
+
value = command instanceof Function ? await command(tree) : command;
|
|
126
120
|
} else {
|
|
127
121
|
// Look for command in scope
|
|
128
122
|
const parentScope = await scope(tree);
|
|
@@ -15,13 +15,11 @@ let version = 0;
|
|
|
15
15
|
* resource tree, then transform that tree with debug resources and return it.
|
|
16
16
|
*
|
|
17
17
|
* @param {Object} options
|
|
18
|
-
* @param {string} options.debugFilesPath
|
|
19
|
-
* @param {boolean} options.enableUnsafeEval
|
|
20
18
|
* @param {string} options.expression
|
|
21
19
|
* @param {string} options.parentPath
|
|
22
20
|
*/
|
|
23
21
|
export default async function expressionTree(options) {
|
|
24
|
-
const {
|
|
22
|
+
const { expression, parentPath } = options;
|
|
25
23
|
|
|
26
24
|
const parent = new OrigamiFileMap(parentPath);
|
|
27
25
|
const globals = await projectGlobals(parent);
|
|
@@ -47,7 +45,7 @@ export default async function expressionTree(options) {
|
|
|
47
45
|
setParent(maplike, parent);
|
|
48
46
|
|
|
49
47
|
// Add debugging resources
|
|
50
|
-
const tree = debugTransform(maplike
|
|
48
|
+
const tree = debugTransform(maplike);
|
|
51
49
|
|
|
52
50
|
/** @type {any} */ (tree).version = version++;
|
|
53
51
|
|
|
@@ -1,17 +1,49 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { trailingSlash, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import { evaluate, projectGlobals } from "@weborigami/language";
|
|
3
|
+
import debugTransform from "./debugTransform.js";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
let lastResult;
|
|
5
|
+
const mapParentToResult = new WeakMap();
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Return a route that evaluates a key containing an expression.
|
|
9
|
+
*
|
|
10
|
+
* Because this route may be used multiple times to return the resources for a
|
|
11
|
+
* page, we cache the result for each parent and key.
|
|
12
|
+
*
|
|
13
|
+
* To allow multiple evaluations of the same expression, we expect the key to be
|
|
14
|
+
* of the form `<counter>,<expression>`, where the counter is a number that will
|
|
15
|
+
* be used for caching but ignored for evaluation. The debugger increments the
|
|
16
|
+
* counter to force reevaluation of the same expression.
|
|
17
|
+
*/
|
|
6
18
|
export default async function oriEval(parent) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
const globals = await projectGlobals(parent);
|
|
20
|
+
return async (key) => {
|
|
21
|
+
const normalizedKey = trailingSlash.remove(key);
|
|
22
|
+
let result = mapParentToResult.get(parent);
|
|
23
|
+
if (result?.key === normalizedKey) {
|
|
24
|
+
return result.value;
|
|
12
25
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
|
|
27
|
+
const regex = /^\d+,(?<expression>.*)$/;
|
|
28
|
+
const match = normalizedKey.match(regex);
|
|
29
|
+
let value;
|
|
30
|
+
if (match) {
|
|
31
|
+
const expression = decodeURIComponent(match.groups.expression);
|
|
32
|
+
value = await evaluate(expression, {
|
|
33
|
+
globals,
|
|
34
|
+
mode: "shell",
|
|
35
|
+
parent,
|
|
36
|
+
});
|
|
37
|
+
if (
|
|
38
|
+
(Tree.isMaplike(value) && typeof value !== "function") ||
|
|
39
|
+
value?.unpack
|
|
40
|
+
) {
|
|
41
|
+
value = debugTransform(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mapParentToResult.set(parent, { key: normalizedKey, value });
|
|
46
|
+
|
|
47
|
+
return value;
|
|
16
48
|
};
|
|
17
49
|
}
|
package/src/dev/dev.js
CHANGED
|
@@ -12,7 +12,6 @@ export { default as audit } from "./crawler/audit.js";
|
|
|
12
12
|
export { default as crawl } from "./crawler/crawl.js";
|
|
13
13
|
export { default as debug } from "./debug.js";
|
|
14
14
|
export { default as debug2 } from "./debug2/debug2.js";
|
|
15
|
-
export { default as eval } from "./debug2/oriEval.js";
|
|
16
15
|
export { default as explore } from "./explore.js";
|
|
17
16
|
export { default as help } from "./help.js";
|
|
18
17
|
export { default as log } from "./log.js";
|
package/src/dev/help.yaml
CHANGED
|
@@ -79,6 +79,9 @@ Origami:
|
|
|
79
79
|
inline:
|
|
80
80
|
args: (text)
|
|
81
81
|
description: Inline Origami expressions found in the text
|
|
82
|
+
htmlParse:
|
|
83
|
+
args: (html)
|
|
84
|
+
description: Parse HTML into a plain JavaScript object
|
|
82
85
|
json:
|
|
83
86
|
args: (obj)
|
|
84
87
|
description: Render the object in JSON format
|
|
@@ -139,6 +142,9 @@ Origami:
|
|
|
139
142
|
unpack:
|
|
140
143
|
args: (buffer)
|
|
141
144
|
description: Unpack the buffer into a usable form
|
|
145
|
+
xmlParse:
|
|
146
|
+
args: (xml)
|
|
147
|
+
description: Parse XML into a plain JavaScript object
|
|
142
148
|
yaml:
|
|
143
149
|
args: (obj)
|
|
144
150
|
description: Render the object in YAML format
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as xml_handler } from "./xml_handler.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { symbols, toString } from "@weborigami/async-tree";
|
|
2
|
+
import xmlParse from "../origami/xmlParse.js";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
mediaType: "application/xml",
|
|
6
|
+
|
|
7
|
+
async unpack(packed, options = {}) {
|
|
8
|
+
const parent = options.parent ?? null;
|
|
9
|
+
const text = toString(packed);
|
|
10
|
+
if (text === null) {
|
|
11
|
+
throw new TypeError("XML handler can only unpack text.");
|
|
12
|
+
}
|
|
13
|
+
const data = await xmlParse(text);
|
|
14
|
+
// Define `parent` as non-enumerable property
|
|
15
|
+
Object.defineProperty(data, symbols.parent, {
|
|
16
|
+
configurable: true,
|
|
17
|
+
enumerable: false,
|
|
18
|
+
value: parent,
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
return data;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const ELEMENT_NODE = 1;
|
|
2
|
+
const TEXT_NODE = 3;
|
|
3
|
+
const CDATA_SECTION_NODE = 4;
|
|
4
|
+
const DOCUMENT_NODE = 9;
|
|
5
|
+
const DOCUMENT_FRAGMENT_NODE = 11;
|
|
6
|
+
|
|
7
|
+
export default function domNodeToObject(node) {
|
|
8
|
+
switch (node.nodeType) {
|
|
9
|
+
case DOCUMENT_NODE:
|
|
10
|
+
return {
|
|
11
|
+
name: "#document",
|
|
12
|
+
children: [...node.childNodes]
|
|
13
|
+
.filter((child) => !isWhitespaceOnly(child))
|
|
14
|
+
.map(domNodeToObject),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
case DOCUMENT_FRAGMENT_NODE:
|
|
18
|
+
return {
|
|
19
|
+
name: "#document-fragment",
|
|
20
|
+
children: [...node.childNodes]
|
|
21
|
+
.filter((child) => !isWhitespaceOnly(child))
|
|
22
|
+
.map(domNodeToObject),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
case ELEMENT_NODE: {
|
|
26
|
+
const attributes = Object.fromEntries(
|
|
27
|
+
[...node.attributes].map((attr) => [attr.name, attr.value]),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const relevantChildren = [...node.childNodes].filter(
|
|
31
|
+
(child) =>
|
|
32
|
+
(child.nodeType === ELEMENT_NODE ||
|
|
33
|
+
child.nodeType === TEXT_NODE ||
|
|
34
|
+
child.nodeType === CDATA_SECTION_NODE) &&
|
|
35
|
+
!isWhitespaceOnly(child),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const onlyText = relevantChildren.every(
|
|
39
|
+
(child) =>
|
|
40
|
+
child.nodeType === TEXT_NODE || child.nodeType === CDATA_SECTION_NODE,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const result = {
|
|
44
|
+
name: node.localName,
|
|
45
|
+
};
|
|
46
|
+
if (Object.keys(attributes).length > 0) {
|
|
47
|
+
result.attributes = attributes;
|
|
48
|
+
}
|
|
49
|
+
if (onlyText) {
|
|
50
|
+
const text = relevantChildren
|
|
51
|
+
.map((child) => collapseWhitespace(child.nodeValue ?? ""))
|
|
52
|
+
.join("")
|
|
53
|
+
.trim();
|
|
54
|
+
if (text.length > 0) {
|
|
55
|
+
result.text = text;
|
|
56
|
+
}
|
|
57
|
+
} else if (relevantChildren.length > 0) {
|
|
58
|
+
result.children = relevantChildren.map(domNodeToObject);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case TEXT_NODE:
|
|
65
|
+
return {
|
|
66
|
+
name: "#text",
|
|
67
|
+
text: collapseWhitespace(node.nodeValue ?? ""),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
case CDATA_SECTION_NODE:
|
|
71
|
+
return {
|
|
72
|
+
name: "#cdata-section",
|
|
73
|
+
text: collapseWhitespace(node.nodeValue ?? ""),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
return {
|
|
78
|
+
name: `#node-${node.nodeType}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Collapse leading or trailing whitespace characters to a single space
|
|
84
|
+
function collapseWhitespace(str) {
|
|
85
|
+
return str.replace(/^\s+/, " ").replace(/\s+$/, " ");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isWhitespaceOnly(node) {
|
|
89
|
+
return (
|
|
90
|
+
(node.nodeType === TEXT_NODE || node.nodeType === CDATA_SECTION_NODE) &&
|
|
91
|
+
node.nodeValue.trim() === ""
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { args } from "@weborigami/async-tree";
|
|
2
|
+
import loadJsDom from "../common/loadJsDom.js";
|
|
3
|
+
import domNodeToObject from "./domNodeToObject.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return the DOM structure for the given HTML as a plain object.
|
|
7
|
+
*
|
|
8
|
+
* @param {import("@weborigami/async-tree").Stringlike} html
|
|
9
|
+
*/
|
|
10
|
+
export default async function htmlParse(html) {
|
|
11
|
+
html = args.stringlike(html, "Origami.htmlParse");
|
|
12
|
+
const { JSDOM } = await loadJsDom();
|
|
13
|
+
const dom = JSDOM.fragment(html);
|
|
14
|
+
let object = domNodeToObject(dom);
|
|
15
|
+
if (
|
|
16
|
+
(object.name === "#document" || object.name === "#document-fragment") &&
|
|
17
|
+
object.children.length === 1
|
|
18
|
+
) {
|
|
19
|
+
object = object.children[0];
|
|
20
|
+
}
|
|
21
|
+
return object;
|
|
22
|
+
}
|
package/src/origami/mdOutline.js
CHANGED
|
@@ -22,18 +22,29 @@ export default async function mdOutline(input) {
|
|
|
22
22
|
const outline = {};
|
|
23
23
|
const stack = [];
|
|
24
24
|
let sectionText = "";
|
|
25
|
+
let sectionTextTrimmed = null;
|
|
25
26
|
/** @type {any} */
|
|
26
27
|
let current = outline;
|
|
27
28
|
for (const token of tokens) {
|
|
28
29
|
if (token.type === "heading") {
|
|
29
30
|
// Current section text gets added as content for the current node.
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
sectionTextTrimmed = sectionText.trim();
|
|
32
|
+
if (sectionTextTrimmed) {
|
|
33
|
+
current._text = sectionTextTrimmed;
|
|
32
34
|
sectionText = "";
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
// Pop the stack to find the right level for this heading
|
|
36
37
|
const { depth, text: headingText } = token;
|
|
38
|
+
|
|
39
|
+
// Did we skip a heading level? If so, create `_skip<n>` nodes
|
|
40
|
+
while (stack.length < depth - 1) {
|
|
41
|
+
const skipNode = {};
|
|
42
|
+
current[`_skip${stack.length + 1}`] = skipNode;
|
|
43
|
+
stack.push(current);
|
|
44
|
+
current = skipNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Pop the stack to find the right level for this heading
|
|
37
48
|
while (stack.length >= depth) {
|
|
38
49
|
current = stack.pop();
|
|
39
50
|
consolidateText(current);
|
|
@@ -51,8 +62,9 @@ export default async function mdOutline(input) {
|
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
// Any remaining section text gets added as content for the current node.
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
sectionTextTrimmed = sectionText.trim();
|
|
66
|
+
if (sectionTextTrimmed) {
|
|
67
|
+
current._text = sectionTextTrimmed;
|
|
56
68
|
current = stack.pop();
|
|
57
69
|
if (current) {
|
|
58
70
|
consolidateText(current);
|
package/src/origami/origami.js
CHANGED
|
@@ -4,8 +4,8 @@ export { default as basename } from "./basename.js";
|
|
|
4
4
|
export { default as csv } from "./csv.js";
|
|
5
5
|
export { default as document } from "./document.js";
|
|
6
6
|
export { default as fetch } from "./fetch.js";
|
|
7
|
-
export { default as htmlDom } from "./htmlDom.js";
|
|
8
7
|
export { default as htmlEscape } from "./htmlEscape.js";
|
|
8
|
+
export { default as htmlParse } from "./htmlParse.js";
|
|
9
9
|
export { default as format } from "./image/format.js";
|
|
10
10
|
export * as image from "./image/image.js";
|
|
11
11
|
export { default as resize } from "./image/resize.js";
|
|
@@ -34,5 +34,6 @@ export { default as static } from "./static.js";
|
|
|
34
34
|
export { default as string } from "./string.js";
|
|
35
35
|
export { default as tsv } from "./tsv.js";
|
|
36
36
|
export { default as unpack } from "./unpack.js";
|
|
37
|
+
export { default as xmlParse } from "./xmlParse.js";
|
|
37
38
|
export { default as yaml } from "./yaml.js";
|
|
38
39
|
export { default as yamlParse } from "./yamlParse.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { args } from "@weborigami/async-tree";
|
|
2
|
+
import loadJsDom from "../common/loadJsDom.js";
|
|
3
|
+
import domNodeToObject from "./domNodeToObject.js";
|
|
4
|
+
|
|
5
|
+
let parser;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return the DOM for the given XML as a plain object.
|
|
9
|
+
*
|
|
10
|
+
* @param {import("@weborigami/async-tree").Stringlike} xml
|
|
11
|
+
*/
|
|
12
|
+
export default async function xmlParse(xml) {
|
|
13
|
+
xml = args.stringlike(xml, "Origami.xmlParse");
|
|
14
|
+
const parser = await getParser();
|
|
15
|
+
const dom = parser.parseFromString(xml, "application/xml");
|
|
16
|
+
let object = domNodeToObject(dom);
|
|
17
|
+
if (
|
|
18
|
+
(object.name === "#document" || object.name === "#document-fragment") &&
|
|
19
|
+
object.children.length === 1
|
|
20
|
+
) {
|
|
21
|
+
object = object.children[0];
|
|
22
|
+
}
|
|
23
|
+
return object;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getParser() {
|
|
27
|
+
if (!parser) {
|
|
28
|
+
const { JSDOM } = await loadJsDom();
|
|
29
|
+
const dom = new JSDOM();
|
|
30
|
+
parser = new dom.window.DOMParser();
|
|
31
|
+
}
|
|
32
|
+
return parser;
|
|
33
|
+
}
|
package/src/server/server.js
CHANGED
|
@@ -88,7 +88,14 @@ export async function handleRequest(request, response, map) {
|
|
|
88
88
|
|
|
89
89
|
export function keysFromUrl(url) {
|
|
90
90
|
const encodedKeys = keysFromPath(url.pathname);
|
|
91
|
-
|
|
91
|
+
// Decode the keys, but stop decoding if we encounter an Origami debugger command
|
|
92
|
+
let foundCommand = false;
|
|
93
|
+
const keys = encodedKeys.map((key) => {
|
|
94
|
+
if (key.startsWith("!")) {
|
|
95
|
+
foundCommand = true;
|
|
96
|
+
}
|
|
97
|
+
return foundCommand ? key : decodeURIComponent(key);
|
|
98
|
+
});
|
|
92
99
|
|
|
93
100
|
// If the keys array is empty (the path was just a trailing slash) or if the
|
|
94
101
|
// path ended with a slash, add "index.html" to the end of the keys.
|
|
@@ -104,12 +111,17 @@ export function keysFromUrl(url) {
|
|
|
104
111
|
* https.createServer calls, letting you serve an async tree as a set of pages.
|
|
105
112
|
*
|
|
106
113
|
* @typedef {import("@weborigami/async-tree").Maplike} Maplike
|
|
114
|
+
* @param {object} options
|
|
115
|
+
* @param {boolean} [options.quiet] If true, suppresses logging of incoming requests.
|
|
107
116
|
* @param {Maplike} maplike
|
|
108
117
|
*/
|
|
109
|
-
export function requestListener(maplike) {
|
|
118
|
+
export function requestListener(maplike, options = {}) {
|
|
119
|
+
const quiet = options.quiet ?? false;
|
|
110
120
|
const tree = Tree.from(maplike);
|
|
111
121
|
return async function (request, response) {
|
|
112
|
-
|
|
122
|
+
if (!quiet) {
|
|
123
|
+
console.log(decodeURI(request.url));
|
|
124
|
+
}
|
|
113
125
|
const handled = await handleRequest(request, response, tree);
|
|
114
126
|
if (!handled) {
|
|
115
127
|
// Not found, return a 404.
|
package/src/origami/htmlDom.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { args } from "@weborigami/async-tree";
|
|
2
|
-
import loadJsDom from "../common/loadJsDom.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Return the DOM for the given HTML string.
|
|
6
|
-
*
|
|
7
|
-
* @param {import("@weborigami/async-tree").Stringlike} html
|
|
8
|
-
*/
|
|
9
|
-
export default async function htmlDom(html) {
|
|
10
|
-
html = args.stringlike(html, "Origami.htmlDom");
|
|
11
|
-
const { JSDOM } = await loadJsDom();
|
|
12
|
-
const dom = JSDOM.fragment(html);
|
|
13
|
-
return dom;
|
|
14
|
-
}
|