@weborigami/origami 0.6.0 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/origami",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Web Origami language, CLI, framework, and server",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,9 +17,9 @@
17
17
  "typescript": "5.9.3"
18
18
  },
19
19
  "dependencies": {
20
- "@weborigami/async-tree": "0.6.0",
21
- "@weborigami/json-feed-to-rss": "1.0.0",
22
- "@weborigami/language": "0.6.0",
20
+ "@weborigami/async-tree": "0.6.2",
21
+ "@weborigami/json-feed-to-rss": "1.0.1",
22
+ "@weborigami/language": "0.6.2",
23
23
  "css-tree": "3.1.0",
24
24
  "graphviz-wasm": "3.0.2",
25
25
  "highlight.js": "11.11.1",
@@ -5,12 +5,7 @@
5
5
  * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
6
6
  */
7
7
 
8
- import {
9
- castArraylike,
10
- SyncMap,
11
- toPlainValue,
12
- trailingSlash,
13
- } from "@weborigami/async-tree";
8
+ import { castArraylike, toPlainValue } from "@weborigami/async-tree";
14
9
  import * as YAMLModule from "yaml";
15
10
 
16
11
  // The "yaml" package doesn't seem to provide a default export that the browser can
@@ -26,10 +21,9 @@ export function parseYaml(text) {
26
21
  return YAML.parse(text);
27
22
  }
28
23
 
29
- function reduceToMap(values, keys, map) {
30
- // Normalize slashes in keys.
31
- keys = keys.map(trailingSlash.remove);
32
- return castArraylike(keys, values, (entries) => new SyncMap(entries));
24
+ function reduceToMap(map) {
25
+ // createFn parameter returns as map as is
26
+ return castArraylike(map, (result) => result);
33
27
  }
34
28
 
35
29
  /**
package/src/dev/copy.js CHANGED
@@ -1,7 +1,11 @@
1
- import { getTreeArgument, Tree } from "@weborigami/async-tree";
1
+ import {
2
+ AsyncMap,
3
+ getTreeArgument,
4
+ SyncMap,
5
+ Tree,
6
+ } from "@weborigami/async-tree";
2
7
  import { formatError } from "@weborigami/language";
3
8
  import process, { stdout } from "node:process";
4
- import { transformObject } from "../common/utilities.js";
5
9
 
6
10
  /**
7
11
  * @typedef {import("@weborigami/async-tree").Maplike} Maplike
@@ -13,75 +17,87 @@ export default async function copy(source, target) {
13
17
  const sourceTree = await getTreeArgument(source, "copy", { position: 0 });
14
18
  let targetTree = await getTreeArgument(target, "copy", { position: 1 });
15
19
 
20
+ let progressTree;
16
21
  if (stdout.isTTY) {
17
- targetTree =
18
- targetTree instanceof Map
19
- ? transformObject(SyncProgressTransform, targetTree)
20
- : transformObject(AsyncProgressTransform, targetTree);
21
- copyRoot = targetTree;
22
- countFiles = 0;
23
- countCopied = 0;
22
+ progressTree = showSetProgress(targetTree, {
23
+ copied: 0,
24
+ total: 0,
25
+ });
26
+ } else {
27
+ progressTree = targetTree;
24
28
  }
25
29
 
26
- await Tree.assign(targetTree, sourceTree);
30
+ await Tree.assign(progressTree, sourceTree);
27
31
 
28
32
  if (stdout.isTTY) {
29
33
  process.stdout.clearLine(0);
30
34
  process.stdout.cursorTo(0);
31
- copyRoot = null;
32
- countFiles = null;
33
- countCopied = null;
34
35
  }
35
36
  }
36
37
 
37
- let countFiles;
38
- let countCopied;
39
- let copyRoot;
38
+ // Wrap the source tree to show progress on set() operations. Handle both sync
39
+ // and async trees. All child trees will share the same counts object.
40
+ function showSetProgress(source, counts) {
41
+ function showProgress() {
42
+ process.stdout.clearLine(0);
43
+ process.stdout.cursorTo(0);
44
+ process.stdout.write(`Copied ${counts.copied} of ${counts.total}`);
45
+ }
40
46
 
41
- function AsyncProgressTransform(Base) {
42
- return class Progress extends Base {
43
- async set(...args) {
44
- countFiles++;
45
- copyRoot.showProgress();
46
- let result;
47
- try {
48
- result = await super.set(...args);
49
- countCopied++;
50
- } catch (/** @type {any} */ error) {
51
- console.error(formatError(error));
52
- }
53
- copyRoot.showProgress();
54
- return result;
55
- }
47
+ const isSync = source instanceof Map;
48
+ const MapClass = isSync ? SyncMap : AsyncMap;
49
+ const iteratorKey = isSync ? Symbol.iterator : Symbol.asyncIterator;
56
50
 
57
- showProgress() {
58
- process.stdout.clearLine(0);
59
- process.stdout.cursorTo(0);
60
- process.stdout.write(`Copied ${countCopied} of ${countFiles}`);
61
- }
62
- };
63
- }
51
+ const progressTree = Object.assign(new MapClass(), {
52
+ delete: source.delete.bind(source),
53
+ keys: source.keys.bind(source),
54
+ [iteratorKey]: source[iteratorKey].bind(source),
55
+
56
+ // Wrap get() to apply progress tracking
57
+ get(key) {
58
+ return awaitIfPromise(source.get(key), (value) => {
59
+ return Tree.isMap(value) ? showSetProgress(value, counts) : value;
60
+ });
61
+ },
64
62
 
65
- function SyncProgressTransform(Base) {
66
- return class Progress extends Base {
67
- set(...args) {
68
- countFiles++;
69
- copyRoot.showProgress();
70
- let result;
63
+ // Wrap set() to show progress
64
+ set(key, value) {
65
+ counts.total++;
66
+ showProgress();
71
67
  try {
72
- result = super.set(...args);
73
- countCopied++;
68
+ const setResult = source.set(key, value);
69
+ return awaitIfPromise(setResult, () => {
70
+ counts.copied++;
71
+ showProgress();
72
+ return progressTree;
73
+ });
74
74
  } catch (/** @type {any} */ error) {
75
75
  console.error(formatError(error));
76
+ return progressTree;
76
77
  }
77
- copyRoot.showProgress();
78
- return result;
79
- }
78
+ },
79
+ });
80
+
81
+ if (typeof source.child === "function") {
82
+ // @ts-ignore
83
+ progressTree.child = async function (key) {
84
+ counts.total++;
85
+ showProgress();
86
+ const childResult = source.child(key);
87
+ return awaitIfPromise(childResult, (child) => {
88
+ counts.copied++;
89
+ showProgress();
90
+ return showSetProgress(child, counts);
91
+ });
92
+ };
93
+ }
94
+
95
+ return progressTree;
96
+ }
80
97
 
81
- showProgress() {
82
- process.stdout.clearLine(0);
83
- process.stdout.cursorTo(0);
84
- process.stdout.write(`Copied ${countCopied} of ${countFiles}`);
85
- }
86
- };
98
+ // Helper function that awaits a value if it's a Promise, then gives it to the
99
+ // function; otherwise calls the function directly. This helps us write code
100
+ // that can handle both sync and async values.
101
+ function awaitIfPromise(value, fn) {
102
+ return value instanceof Promise ? value.then(fn) : fn(value);
87
103
  }
@@ -1,10 +1,6 @@
1
- import {
2
- getTreeArgument,
3
- pathFromKeys,
4
- symbols,
5
- Tree,
6
- } from "@weborigami/async-tree";
1
+ import { pathFromKeys, symbols, Tree } from "@weborigami/async-tree";
7
2
  import crawlResources from "./crawlResources.js";
3
+ import getSiteArgument from "./getSiteArgument.js";
8
4
  import { getBaseUrl } from "./utilities.js";
9
5
 
10
6
  /**
@@ -17,7 +13,7 @@ import { getBaseUrl } from "./utilities.js";
17
13
  * @param {string} [baseHref]
18
14
  */
19
15
  export default async function audit(maplike, baseHref) {
20
- const tree = await getTreeArgument(maplike, "audit");
16
+ const tree = await getSiteArgument(maplike, "audit");
21
17
  const baseUrl = getBaseUrl(baseHref, maplike);
22
18
 
23
19
  let errors = {};
@@ -1,10 +1,6 @@
1
- import {
2
- DeepObjectMap,
3
- Tree,
4
- getTreeArgument,
5
- keysFromPath,
6
- } from "@weborigami/async-tree";
1
+ import { ObjectMap, Tree, keysFromPath } from "@weborigami/async-tree";
7
2
  import crawlResources from "./crawlResources.js";
3
+ import getSiteArgument from "./getSiteArgument.js";
8
4
  import { addValueToObject, getBaseUrl } from "./utilities.js";
9
5
 
10
6
  /**
@@ -23,7 +19,7 @@ import { addValueToObject, getBaseUrl } from "./utilities.js";
23
19
  * @returns {Promise<AsyncMap>}
24
20
  */
25
21
  export default async function crawlBuiltin(maplike, baseHref) {
26
- const tree = await getTreeArgument(maplike, "crawl");
22
+ const tree = await getSiteArgument(maplike, "crawl");
27
23
  const baseUrl = getBaseUrl(baseHref, maplike);
28
24
 
29
25
  const cache = {};
@@ -53,7 +49,7 @@ export default async function crawlBuiltin(maplike, baseHref) {
53
49
  // for something already, that's better than a function that will get that
54
50
  // value.
55
51
  const result = Tree.deepMerge(
56
- new DeepObjectMap(cache),
52
+ new ObjectMap(cache, { deep: true }),
57
53
  await Tree.invokeFunctions(resources)
58
54
  );
59
55
  return result;
@@ -0,0 +1,12 @@
1
+ import { getTreeArgument, SiteMap } from "@weborigami/async-tree";
2
+
3
+ /**
4
+ * Return a site: if it's a tree, return the tree; if it's a string, create a
5
+ * tree for that URL.
6
+ */
7
+ export default async function getSiteArgument(site, command) {
8
+ if (typeof site === "string") {
9
+ return new SiteMap(site);
10
+ }
11
+ return getTreeArgument(site, command);
12
+ }
package/src/dev/help.yaml CHANGED
@@ -68,6 +68,9 @@ Origami:
68
68
  fetch:
69
69
  args: (url, options)
70
70
  description: Fetch a resource from a URL with support for extensions
71
+ htmlEscape:
72
+ args: (text)
73
+ description: Escape HTML entities in the text
71
74
  image:
72
75
  description: Collection of functions for working with images
73
76
  indexPage:
@@ -178,6 +181,9 @@ Tree:
178
181
  calendar:
179
182
  args: (options)
180
183
  description: Return a tree structure for years/months/days
184
+ child:
185
+ args: (tree, key)
186
+ description: Returns the indicated child node, creating it if necessary
181
187
  clear:
182
188
  args: (map)
183
189
  description: Remove all values from the map
@@ -235,6 +241,9 @@ Tree:
235
241
  inners:
236
242
  args: (tree)
237
243
  description: The tree's interior nodes
244
+ invokeFunctions:
245
+ args: (map)
246
+ description: Getting a map value invokes it if it's a function
238
247
  isMap:
239
248
  args: (object)
240
249
  description: True if object is a map
@@ -283,6 +292,9 @@ Tree:
283
292
  plain:
284
293
  args: (tree)
285
294
  description: Render the tree as a plain JavaScript object
295
+ reduce:
296
+ args: (tree, reduceFn)
297
+ description: Reduce the tree to a single value
286
298
  regExpKeys:
287
299
  args: (tree)
288
300
  description: A tree whose keys are regular expression strings
@@ -295,6 +307,9 @@ Tree:
295
307
  scope:
296
308
  args: (tree)
297
309
  description: A merged view of the tree and its ancestors
310
+ set:
311
+ args: (map, key, value)
312
+ description: Set the value for the key in the map
298
313
  shuffle:
299
314
  args: (map)
300
315
  description: Shuffle the keys of the map
@@ -0,0 +1,14 @@
1
+ import { toString } from "@weborigami/async-tree";
2
+ import loadJsDom from "../common/loadJsDom.js";
3
+
4
+ /**
5
+ * Escapes HTML entities in a string.
6
+ *
7
+ * @param {import("@weborigami/async-tree").Stringlike} html
8
+ */
9
+ export default async function htmlEscape(html) {
10
+ const { JSDOM } = await loadJsDom();
11
+ const div = JSDOM.fragment("<div></div>").firstChild;
12
+ div.textContent = toString(html);
13
+ return div.innerHTML;
14
+ }
@@ -9,7 +9,7 @@ import documentObject from "../common/documentObject.js";
9
9
  *
10
10
  * @param {any} input
11
11
  */
12
- export default async function inline(input) {
12
+ export default async function inline(input, options = {}) {
13
13
  // Get the input text and any attached front matter.
14
14
  if (isUnpackable(input)) {
15
15
  input = await input.unpack();
@@ -21,6 +21,7 @@ export default async function inline(input) {
21
21
  }
22
22
 
23
23
  const parent =
24
+ options.parent ??
24
25
  /** @type {any} */ (input).parent ??
25
26
  /** @type {any} */ (input)[symbols.parent];
26
27
 
@@ -41,7 +41,9 @@ function jsonKeysMap(source) {
41
41
 
42
42
  source: source,
43
43
 
44
- trailingSlashKeys: source.trailingSlashKeys,
44
+ get trailingSlashKeys() {
45
+ return source.trailingSlashKeys;
46
+ },
45
47
  });
46
48
  return result;
47
49
  }
@@ -48,6 +48,17 @@ export default async function ori(expression, options = {}) {
48
48
  // Execute
49
49
  let result = await fn();
50
50
 
51
+ // if (result === undefined) {
52
+ // // Was the code a path traversal?
53
+ // const wasTraversal =
54
+ // fn.code[0] === ops.unpack ||
55
+ // (fn.code[0] instanceof Array && fn.code[0][0] === ops.scope);
56
+ // if (wasTraversal) {
57
+ // // Yes, probably an error
58
+ // console.warn(`ori: warning: undefined ${highlightError(expression)}`);
59
+ // }
60
+ // }
61
+
51
62
  // If result was a function, execute it.
52
63
  if (typeof result === "function") {
53
64
  result = await result();
@@ -14,6 +14,7 @@ export { default as static } from "../origami/static.js";
14
14
  export { default as basename } from "./basename.js";
15
15
  export { default as csv } from "./csv.js";
16
16
  export { default as fetch } from "./fetch.js";
17
+ export { default as htmlEscape } from "./htmlEscape.js";
17
18
  export { default as format } from "./image/format.js";
18
19
  export * as image from "./image/image.js";
19
20
  export { default as resize } from "./image/resize.js";
@@ -14,7 +14,7 @@ const templateText = `(urls) => \`<?xml version="1.0" encoding="UTF-8"?>
14
14
  * @typedef {import("@weborigami/async-tree").Maplike} Maplike
15
15
  *
16
16
  * @param {Maplike} maplike
17
- * @param {{ assumeSlashes?: boolean, base?: string }} options
17
+ * @param {{ base?: string }} options
18
18
  * @returns {Promise<string>}
19
19
  */
20
20
  export default async function sitemap(maplike, options = {}) {
@@ -5,5 +5,8 @@
5
5
  * @param {any} obj
6
6
  */
7
7
  export default function unpack(obj) {
8
- return obj?.unpack?.() ?? obj;
8
+ if (obj == null) {
9
+ throw new ReferenceError("Cannot unpack null or undefined value");
10
+ }
11
+ return obj.unpack?.() ?? obj;
9
12
  }
@@ -121,6 +121,8 @@ export function requestListener(maplike) {
121
121
  */
122
122
  function respondWithError(response, error) {
123
123
  let message = formatError(error);
124
+ // Remove ANSI escape codes from the message.
125
+ message = message.replace(/\x1b\[[0-9;]*m/g, "");
124
126
  // Prevent HTML in the error message from being interpreted as HTML.
125
127
  message = message.replace(/</g, "&lt;").replace(/>/g, "&gt;");
126
128
  const html = `<!DOCTYPE html>