@weborigami/origami 0.0.48 → 0.0.49

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.
@@ -1,4 +1,5 @@
1
1
  // This file is generated by running buildExports.js -- do not edit by hand.
2
+ export { default as addNextPrevious } from "../src/builtins/@addNextPrevious.js";
2
3
  export { default as apply } from "../src/builtins/@apply.js";
3
4
  export { default as arrows } from "../src/builtins/@arrows.js";
4
5
  export { default as basename } from "../src/builtins/@basename.js";
@@ -38,6 +39,7 @@ export { default as js } from "../src/builtins/@js.js";
38
39
  export { default as json } from "../src/builtins/@json.js";
39
40
  export { default as keys } from "../src/builtins/@keys.js";
40
41
  export { default as keysJson } from "../src/builtins/@keysJson.js";
42
+ export { default as makeParser } from "../src/builtins/@makeParser.js";
41
43
  export { default as map } from "../src/builtins/@map.js";
42
44
  export { default as mapDeep } from "../src/builtins/@mapDeep.js";
43
45
  export { default as match } from "../src/builtins/@match.js";
@@ -72,6 +74,7 @@ export { default as setDeep } from "../src/builtins/@setDeep.js";
72
74
  export { default as shell } from "../src/builtins/@shell.js";
73
75
  export { default as shuffle } from "../src/builtins/@shuffle.js";
74
76
  export { default as sitemap } from "../src/builtins/@sitemap.js";
77
+ export { default as slug } from "../src/builtins/@slug.js";
75
78
  export { default as sort } from "../src/builtins/@sort.js";
76
79
  export { default as sortBy } from "../src/builtins/@sortBy.js";
77
80
  export { default as static } from "../src/builtins/@static.js";
package/index.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import { Treelike, Unpackable } from "@weborigami/async-tree";
7
- import { AsyncTree } from "@weborigami/types";
8
7
 
9
8
  /**
10
9
  * A class constructor is an object with a `new` method that returns an
@@ -21,5 +20,3 @@ export interface JsonObject {
21
20
  export type JsonValue = boolean | number | string | Date | JsonObject | JsonValue[] | null;
22
21
 
23
22
  export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
24
-
25
- export type TreelikeTransform = (value: Treelike) => AsyncTree;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/origami",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
4
4
  "description": "Web Origami language, CLI, framework, and server",
5
5
  "type": "module",
6
6
  "repository": {
@@ -13,22 +13,22 @@
13
13
  "main": "./exports/exports.js",
14
14
  "types": "./index.ts",
15
15
  "devDependencies": {
16
- "@types/node": "20.11.27",
17
- "typescript": "5.4.2"
16
+ "@types/node": "20.12.7",
17
+ "typescript": "5.4.5"
18
18
  },
19
19
  "dependencies": {
20
- "@weborigami/async-tree": "0.0.48",
21
- "@weborigami/language": "0.0.48",
22
- "@weborigami/types": "0.0.48",
20
+ "@weborigami/async-tree": "0.0.49",
21
+ "@weborigami/language": "0.0.49",
22
+ "@weborigami/types": "0.0.49",
23
23
  "exif-parser": "0.1.12",
24
- "graphviz-wasm": "3.0.1",
24
+ "graphviz-wasm": "3.0.2",
25
25
  "highlight.js": "11.9.0",
26
- "marked": "12.0.1",
26
+ "marked": "12.0.2",
27
27
  "marked-gfm-heading-id": "3.1.3",
28
28
  "marked-highlight": "2.1.1",
29
29
  "marked-smartypants": "1.1.6",
30
- "sharp": "0.33.2",
31
- "yaml": "2.4.1"
30
+ "sharp": "0.33.3",
31
+ "yaml": "2.4.2"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "ori exports/buildExports.js src > exports/exports.js",
@@ -0,0 +1,47 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import getTreeArgument from "../misc/getTreeArgument.js";
3
+
4
+ /**
5
+ * Add nextKey/previousKey properties to values.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ *
9
+ * @this {AsyncTree|null}
10
+ * @param {import("@weborigami/async-tree").Treelike} treelike
11
+ */
12
+ export default async function sequence(treelike) {
13
+ const tree = await getTreeArgument(this, arguments, treelike, "@sequence");
14
+ let keys;
15
+ return Object.create(tree, {
16
+ get: {
17
+ value: async (key) => {
18
+ let value = await tree.get(key);
19
+
20
+ if (Tree.isTreelike(value)) {
21
+ value = await Tree.plain(value);
22
+ } else if (typeof value === "object") {
23
+ // Clone value to avoid modifying the original object.
24
+ value = { ...value };
25
+ } else {
26
+ return value;
27
+ }
28
+
29
+ if (keys === undefined) {
30
+ keys = Array.from(await tree.keys());
31
+ }
32
+ const index = keys.indexOf(key);
33
+ if (index === -1) {
34
+ // Key is supported but not published in `keys`
35
+ return value;
36
+ }
37
+
38
+ // Extend value with nextKey/previousKey properties.
39
+ return Object.assign(value, {
40
+ nextKey: keys[index + 1],
41
+ previousKey: keys[index - 1],
42
+ });
43
+ },
44
+ writable: true,
45
+ },
46
+ });
47
+ }
@@ -1,4 +1,4 @@
1
- import { Tree, isPlainObject } from "@weborigami/async-tree";
1
+ import { Tree } from "@weborigami/async-tree";
2
2
  import { Scope } from "@weborigami/language";
3
3
  import ExplorableSiteTransform from "../common/ExplorableSiteTransform.js";
4
4
  import { isTransformApplied, transformObject } from "../common/utilities.js";
@@ -19,12 +19,13 @@ export default async function debug(treelike) {
19
19
  // apply its own scope to the tree.
20
20
  let tree = await getTreeArgument(this, arguments, treelike, "@debug");
21
21
 
22
+ if (!isTransformApplied(DebugTransform, tree)) {
23
+ tree = transformObject(DebugTransform, tree);
24
+ }
22
25
  if (!isTransformApplied(ExplorableSiteTransform, tree)) {
23
26
  tree = transformObject(ExplorableSiteTransform, tree);
24
27
  }
25
28
 
26
- tree = transformObject(DebugTransform, tree);
27
-
28
29
  return tree;
29
30
  }
30
31
 
@@ -36,20 +37,19 @@ function DebugTransform(Base) {
36
37
  return class Debug extends OriCommandTransform(Base) {
37
38
  async get(key) {
38
39
  let value = await super.get(key);
40
+ const scope = Scope.getScope(this);
39
41
 
40
- // Since this transform is for diagnostic purposes, cast arrays
41
- // or plain objects to trees so we can debug them too.
42
- if (value instanceof Array || isPlainObject(value)) {
42
+ // Since this transform is for diagnostic purposes, cast any treelike
43
+ // result to a tree so we can debug the result too.
44
+ if (Tree.isTreelike(value)) {
43
45
  value = Tree.from(value);
44
- }
45
-
46
- // Ensure debug transforms are applied to explorable results.
47
- if (Tree.isAsyncTree(value)) {
48
- value = transformObject(ExplorableSiteTransform, value);
49
- value = transformObject(DebugTransform, value);
50
- }
51
-
52
- if (value?.unpack) {
46
+ if (!value.parent && !value.scope) {
47
+ value = Scope.treeWithScope(value, scope);
48
+ }
49
+ if (!isTransformApplied(DebugTransform, value)) {
50
+ value = transformObject(DebugTransform, value);
51
+ }
52
+ } else if (value?.unpack) {
53
53
  // If the value isn't a tree, but has a tree attached via an `unpack`
54
54
  // method, wrap the unpack method to provide debug support for it.
55
55
  const original = value.unpack.bind(value);
@@ -61,13 +61,11 @@ function DebugTransform(Base) {
61
61
  /** @type {any} */
62
62
  let tree = Tree.from(content);
63
63
  if (!tree.parent && !tree.scope) {
64
- const scope = Scope.getScope(this);
65
64
  tree = Scope.treeWithScope(tree, scope);
66
65
  }
67
- if (!isTransformApplied(ExplorableSiteTransform, tree)) {
68
- tree = transformObject(ExplorableSiteTransform, tree);
66
+ if (!isTransformApplied(DebugTransform, tree)) {
67
+ tree = transformObject(DebugTransform, tree);
69
68
  }
70
- tree = transformObject(DebugTransform, tree);
71
69
  return tree;
72
70
  };
73
71
  }
@@ -8,9 +8,8 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
8
8
  * @this {AsyncTree|null}
9
9
  * @param {StringLike} text
10
10
  * @param {any} [data]
11
- * @returns
12
11
  */
13
- export default function documentBuiltin(text, data) {
12
+ export default async function documentBuiltin(text, data) {
14
13
  assertScopeIsDefined(this, "document");
15
14
  return documentObject(text, data);
16
15
  }
@@ -1,4 +1,4 @@
1
- import { isUnpackable, symbols } from "@weborigami/async-tree";
1
+ import { ObjectTree, isUnpackable, symbols } from "@weborigami/async-tree";
2
2
  import { compile } from "@weborigami/language";
3
3
  import documentObject from "../common/documentObject.js";
4
4
  import { toString } from "../common/utilities.js";
@@ -24,7 +24,13 @@ export default async function inline(input) {
24
24
  }
25
25
  const inputIsDocument = input["@text"] !== undefined;
26
26
  const origami = inputIsDocument ? input["@text"] : toString(input);
27
- const parent = input.parent ?? input[symbols.parent];
27
+
28
+ let parent = /** @type {any} */ (input).parent ?? input[symbols.parent];
29
+ if (!parent) {
30
+ // Construct a temporary parent that has the right scope.
31
+ parent = new ObjectTree({});
32
+ parent.scope = this;
33
+ }
28
34
 
29
35
  // If the input document is a plain object, include it in scope for the
30
36
  // evaluated expression.
@@ -0,0 +1,9 @@
1
+ const parsers = {};
2
+
3
+ export default function makeParser(text) {
4
+ if (!parsers[text]) {
5
+ const regexp = new RegExp(text);
6
+ parsers[text] = (input) => input.match(regexp)?.groups;
7
+ }
8
+ return parsers[text];
9
+ }
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  cachedKeyFunctions,
3
+ isPlainObject,
3
4
  keyFunctionsForExtensions,
4
5
  map,
5
6
  } from "@weborigami/async-tree";
6
7
  import { Scope } from "@weborigami/language";
7
8
  import addValueKeyToScope from "../common/addValueKeyToScope.js";
8
9
  import { toFunction } from "../common/utilities.js";
10
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
9
11
 
10
12
  /**
11
13
  * Map a hierarchical tree of keys and values to a new tree of keys and values.
@@ -14,38 +16,41 @@ import { toFunction } from "../common/utilities.js";
14
16
  * @typedef {import("@weborigami/async-tree").Treelike} Treelike
15
17
  * @typedef {import("@weborigami/async-tree").ValueKeyFn} ValueKeyFn
16
18
  * @typedef {import("@weborigami/async-tree").TreeTransform} TreeTransform
17
- * @typedef {import("../../index.ts").TreelikeTransform} TreelikeTransform
18
19
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
19
20
  *
20
21
  * @typedef {{ deep?: boolean, description?: string, extension?: string,
21
22
  * extensions?: string, inverseKey?: KeyFn, key?: ValueKeyFn, keyMap?:
22
- * ValueKeyFn, value?: ValueKeyFn, valueFn?: ValueKeyFn }} TreeMapOptions
23
+ * ValueKeyFn, needsSourceValue?: boolean, value?: ValueKeyFn, valueMap?:
24
+ * ValueKeyFn }} MapOptionsDictionary
23
25
  *
24
- * @this {import("@weborigami/types").AsyncTree|null}
26
+ * @typedef {ValueKeyFn|MapOptionsDictionary} OptionsOrValueFn
25
27
  *
26
28
  * @overload
27
- * @param {ValueKeyFn} param1
28
- * @returns {TreelikeTransform}
29
- *
30
- * @overload
31
- * @param {TreeMapOptions} param1
32
- * @returns {TreelikeTransform}
33
- *
34
- * @overload
35
- * @param {Treelike} param1
36
- * @param {ValueKeyFn} param2
29
+ * @param {Treelike} source
30
+ * @param {OptionsOrValueFn} instructions
37
31
  * @returns {AsyncTree}
38
32
  *
39
33
  * @overload
40
- * @param {Treelike} param1
41
- * @param {TreeMapOptions} param2
42
- * @returns {AsyncTree}
34
+ * @param {OptionsOrValueFn} instructions
35
+ * @returns {TreeTransform}
36
+ *
37
+ * @this {AsyncTree|null}
38
+ * @param {Treelike|OptionsOrValueFn} param1
39
+ * @param {OptionsOrValueFn} [param2]
43
40
  */
44
41
  export default function treeMap(param1, param2) {
45
- // Identify whether the valueFn/options are the first parameter
46
- // or the second.
42
+ assertScopeIsDefined(this, "map");
43
+
44
+ // Identify whether the map instructions are in the first parameter or the
45
+ // second. If present, identify the function to apply to values.
46
+
47
+ /** @type {Treelike|undefined} */
47
48
  let source;
48
- let options;
49
+ /** @type {OptionsOrValueFn} */
50
+ let instructions;
51
+ /** @type {ValueKeyFn|undefined} */
52
+ let valueFn;
53
+
49
54
  if (arguments.length === 0) {
50
55
  throw new TypeError(
51
56
  `@map: You must give @map a function or a dictionary of options.`
@@ -53,29 +58,35 @@ export default function treeMap(param1, param2) {
53
58
  } else if (!param1) {
54
59
  throw new TypeError(`@map: The first argument was undefined.`);
55
60
  } else if (arguments.length === 1) {
56
- options = param1;
57
- } else if (!param2) {
61
+ // One argument
62
+ // @ts-ignore
63
+ instructions = param1;
64
+ } else if (param2 === undefined) {
58
65
  throw new TypeError(`@map: The second argument was undefined.`);
59
66
  } else {
67
+ // Two arguments
60
68
  source = param1;
61
- options = param2;
69
+ instructions = param2;
62
70
  }
63
71
 
64
- // Identify whether the valueFn/options is a valueFn function
65
- // or an options dictionary.
66
- let valueFn;
67
- if (
68
- typeof options === "function" ||
69
- typeof (/** @type {any} */ (options)?.unpack) === "function"
72
+ // Identify whether the map instructions take the form of a value function or
73
+ // a dictionary of options.
74
+ /** @type {MapOptionsDictionary} */
75
+ let options;
76
+ if (isPlainObject(instructions)) {
77
+ // @ts-ignore
78
+ options = instructions;
79
+ valueFn = options?.value ?? options?.valueMap;
80
+ } else if (
81
+ typeof instructions === "function" ||
82
+ typeof (/** @type {any} */ (instructions)?.unpack) === "function"
70
83
  ) {
71
- valueFn = options;
84
+ valueFn = instructions;
72
85
  options = {};
73
- } else if (!options) {
86
+ } else {
74
87
  throw new TypeError(
75
88
  `@map: You must specify a value function or options dictionary.`
76
89
  );
77
- } else {
78
- valueFn = options.value ?? options.valueMap;
79
90
  }
80
91
 
81
92
  let { deep, description, inverseKey, needsSourceValue } = options;
@@ -131,8 +142,8 @@ export default function treeMap(param1, param2) {
131
142
  extendedInverseKeyFn = keyFns.inverseKey;
132
143
  } else {
133
144
  // Use sidecar keyFn/inverseKey functions if the valueFn defines them.
134
- extendedKeyFn = valueFn?.key;
135
- extendedInverseKeyFn = valueFn?.inverseKey;
145
+ extendedKeyFn = /** @type {any} */ (valueFn)?.key;
146
+ extendedInverseKeyFn = /** @type {any} */ (valueFn)?.inverseKey;
136
147
  }
137
148
 
138
149
  const transform = function mapTreelike(treelike) {
@@ -1,22 +1,71 @@
1
1
  import { isPlainObject } from "@weborigami/async-tree";
2
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
2
3
  import treeMap from "./@map.js";
3
4
 
5
+ /**
6
+ * Map a hierarchical tree of keys and values to a new tree of keys and values.
7
+ *
8
+ * @typedef {import("@weborigami/async-tree").KeyFn} KeyFn
9
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
10
+ * @typedef {import("@weborigami/async-tree").ValueKeyFn} ValueKeyFn
11
+ * @typedef {import("@weborigami/async-tree").TreeTransform} TreeTransform
12
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
13
+ *
14
+ * @typedef {{ deep?: boolean, description?: string, extension?: string,
15
+ * extensions?: string, inverseKey?: KeyFn, key?: ValueKeyFn, keyMap?:
16
+ * ValueKeyFn, needsSourceValue?: boolean, value?: ValueKeyFn, valueMap?:
17
+ * ValueKeyFn }} MapOptionsDictionary
18
+ *
19
+ * @typedef {ValueKeyFn|MapOptionsDictionary} OptionsOrValueFn
20
+ *
21
+ * @overload
22
+ * @param {Treelike} source
23
+ * @param {OptionsOrValueFn} instructions
24
+ * @returns {AsyncTree}
25
+ *
26
+ * @overload
27
+ * @param {OptionsOrValueFn} instructions
28
+ * @returns {TreeTransform}
29
+ *
30
+ * @this {AsyncTree|null}
31
+ * @param {Treelike|OptionsOrValueFn} param1
32
+ * @param {OptionsOrValueFn} [param2]
33
+ */
4
34
  export default function mapDeep(param1, param2) {
5
- // Identify whether the valueFn/options are the first parameter
6
- // or the second.
7
- let source;
8
- let options;
9
- if (param2 === undefined) {
10
- options = param1;
35
+ assertScopeIsDefined(this, "mapDeep");
36
+
37
+ // Identify whether the map instructions are the first parameter or the
38
+ // second.
39
+ if (arguments.length === 1) {
40
+ // One argument, which is a dictionary or function.
41
+ /** @type {MapOptionsDictionary} */
42
+ const options = isPlainObject(param1)
43
+ ? // Dictionary
44
+ { ...param1, deep: true }
45
+ : // Function
46
+ { deep: true, value: param1 };
47
+ const transform = treeMap.call(this, options);
48
+ return transform;
11
49
  } else {
12
- source = param1;
13
- if (isPlainObject(param2)) {
14
- options = param2;
15
- } else {
16
- options = { value: param2 };
17
- }
18
- }
50
+ // Two arguments, second is a dictionary or function.
51
+ /** @type {Treelike} */
52
+ const source = param1;
53
+ /** @type {MapOptionsDictionary} */
54
+ const options = isPlainObject(param2)
55
+ ? // Dictionary
56
+ { ...param2, deep: true }
57
+ : // Function
58
+ { deep: true, value: param2 };
19
59
 
20
- options.deep = true;
21
- return treeMap(source, options);
60
+ // We go through some type gymnastics to convince TypeScript that the return
61
+ // value is an AsyncTree. Using `.call()` with the overloaded `@map`
62
+ // function seems to confuse TypeScript into thinking the call will return a
63
+ // TreeTransform.
64
+
65
+ /** @type {AsyncTree} */
66
+ let tree;
67
+ // @ts-ignore
68
+ tree = treeMap.call(this, source, options);
69
+ return tree;
70
+ }
22
71
  }
@@ -62,7 +62,7 @@ async function formatResult(result) {
62
62
  } else if (
63
63
  !(result instanceof Array) &&
64
64
  (typeof result !== "object" ||
65
- result.toString !== getRealmObjectPrototype(result).toString)
65
+ result.toString !== getRealmObjectPrototype(result)?.toString)
66
66
  ) {
67
67
  text = result.toString();
68
68
  } else if (typeof result === "object") {
@@ -1,4 +1,7 @@
1
- export default async function parseJson(text) {
1
+ import { toString } from "../../common/utilities.js";
2
+
3
+ export default async function parseJson(input) {
4
+ const text = toString(input);
2
5
  return text ? JSON.parse(text) : undefined;
3
6
  }
4
7
 
@@ -1,7 +1,9 @@
1
1
  import * as serialize from "../../common/serialize.js";
2
+ import { toString } from "../../common/utilities.js";
2
3
 
3
- export default async function parseYaml(text) {
4
- return text ? serialize.parseYaml(String(text)) : undefined;
4
+ export default async function parseYaml(input) {
5
+ const text = toString(input);
6
+ return text ? serialize.parseYaml(text) : undefined;
5
7
  }
6
8
 
7
9
  parseYaml.usage = `parseYaml <text>\tParse text as YAML (including JSON)`;
@@ -7,7 +7,7 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
7
7
  * @this {import("@weborigami/types").AsyncTree|null}
8
8
  * @param {Function} fn
9
9
  */
10
- export default async function perf(fn, count = 10000) {
10
+ export default async function perf(fn, count = 1) {
11
11
  assertScopeIsDefined(this, "perf");
12
12
  const start = performance.now();
13
13
  for (let i = 0; i < count; i++) {
@@ -24,7 +24,7 @@ export default async function rss(jsonFeedTree) {
24
24
  <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
25
25
  <channel>
26
26
  <atom:link href="${rssUrl}" rel="self" type="application/rss+xml"/>
27
- <title>${title}</title>
27
+ <title><![CDATA[${title}]]></title>
28
28
  <link>${home_page_url}</link>
29
29
  <description>${description}</description>
30
30
  ${itemsRss}</channel>
@@ -33,15 +33,19 @@ ${itemsRss}</channel>
33
33
 
34
34
  function itemRss(jsonFeedItem) {
35
35
  const { content_html, date_published, id, title, url } = jsonFeedItem;
36
- // RSS wants dates in RFC-822.
37
- const date = date_published?.toUTCString() ?? null;
36
+ // RSS wants dates in RFC-822, essentially the same as ISO 8601.
37
+ const date =
38
+ date_published instanceof Date
39
+ ? date_published.toUTCString()
40
+ : date_published;
38
41
  const dateElement = date ? ` <pubDate>${date}</pubDate>\n` : "";
39
42
  const guidElement = id ? ` <guid>${id}</guid>\n` : "";
40
43
  const contentElement = content_html
41
44
  ? ` <description><![CDATA[${content_html}]]></description>\n`
42
45
  : "";
46
+ const titleElement = `<![CDATA[${title}]]>`;
43
47
  return ` <item>
44
- ${dateElement} <title>${title}</title>
48
+ ${dateElement} <title>${titleElement}</title>
45
49
  <link>${url}</link>
46
50
  ${guidElement}${contentElement} </item>
47
51
  `;
@@ -0,0 +1,15 @@
1
+ export default function slug(filename) {
2
+ let slug = filename.toLowerCase();
3
+
4
+ // Convert spaces to dashes
5
+ slug = slug.replace(/\s+/g, "-");
6
+
7
+ // Remove special characters except dashes, letters, numbers, and periods.
8
+ slug = slug.replace(/[^\w\-\.]/g, "");
9
+
10
+ // Trim leading or trailing dashes.
11
+ slug = slug.replace(/^-+/, "");
12
+ slug = slug.replace(/-+$/, "");
13
+
14
+ return slug;
15
+ }
@@ -1,6 +1,5 @@
1
- import { KeyFn, Treelike, ValueKeyFn } from "@weborigami/async-tree";
1
+ import { KeyFn, Treelike, TreeTransform, ValueKeyFn } from "@weborigami/async-tree";
2
2
  import { AsyncTree } from "@weborigami/types";
3
- import { TreelikeTransform } from "../../index.ts";
4
3
 
5
4
  type TreeMapOptions = {
6
5
  deep?: boolean;
@@ -8,10 +7,10 @@ type TreeMapOptions = {
8
7
  extensions?: string;
9
8
  inverseKey?: KeyFn;
10
9
  key?: ValueKeyFn;
10
+ needsSourceValue?: boolean;
11
11
  value?: ValueKeyFn;
12
12
  };
13
13
 
14
- export default function treeMap(value: ValueKeyFn): TreelikeTransform;
15
- export default function treeMap(options: TreeMapOptions): TreelikeTransform;
16
- export default function treeMap(treelike: Treelike, value: ValueKeyFn): AsyncTree;
17
- export default function treeMap(treelike: Treelike, options: TreeMapOptions): AsyncTree;
14
+ export default function treeMap(options: ValueKeyFn | TreeMapOptions): TreeTransform;
15
+ export default function treeMap(treelike: Treelike, options: ValueKeyFn | TreeMapOptions): AsyncTree;
16
+ export default function treeMap(param1: Treelike | ValueKeyFn | TreeMapOptions, param2?: ValueKeyFn | TreeMapOptions): AsyncTree | TreeTransform;
@@ -1,7 +1,7 @@
1
1
  import { Tree, keysJson } from "@weborigami/async-tree";
2
2
  import { Scope } from "@weborigami/language";
3
3
  import index from "../builtins/@index.js";
4
- import { transformObject } from "../common/utilities.js";
4
+ import { isTransformApplied, transformObject } from "../common/utilities.js";
5
5
 
6
6
  /**
7
7
  * Wraps a tree (typically a SiteTree) to turn a standard site into an
@@ -33,10 +33,10 @@ export default function ExplorableSiteTransform(Base) {
33
33
 
34
34
  // Ask the tree if it has the key.
35
35
  let value = await super.get(key);
36
+ const scope = Scope.getScope(this);
36
37
 
37
38
  if (value === undefined) {
38
39
  // The tree doesn't have the key; try the defaults.
39
- const scope = Scope.getScope(this);
40
40
  if (scope) {
41
41
  if (key === "index.html") {
42
42
  value = await index.call(scope, this);
@@ -46,15 +46,19 @@ export default function ExplorableSiteTransform(Base) {
46
46
  }
47
47
  }
48
48
 
49
- // Ensure this transform is applied to any explorable result. This lets
50
- // the user browse into data and explorable trees of types other than the
51
- // current class.
52
49
  if (Tree.isAsyncTree(value)) {
53
- value = transformObject(ExplorableSiteTransform, value);
54
- }
50
+ // Ensure this transform is applied to any tree result so the user
51
+ // browse into data and trees of classes other than the current class.
52
+ if (!isTransformApplied(ExplorableSiteTransform, value)) {
53
+ value = transformObject(ExplorableSiteTransform, value);
54
+ }
55
55
 
56
- if (value?.unpack) {
57
- // If the value isn't a tree, but has a tree attached via a `unpack`
56
+ if (key.endsWith?.("/")) {
57
+ // Instead of return the tree directly, return an index for it.
58
+ value = await index.call(scope, value);
59
+ }
60
+ } else if (value?.unpack) {
61
+ // If the value isn't a tree, but has a tree attached via an `unpack`
58
62
  // method, wrap the unpack method to add this transform.
59
63
  const original = value.unpack.bind(value);
60
64
  value.unpack = async () => {
@@ -68,7 +72,9 @@ export default function ExplorableSiteTransform(Base) {
68
72
  const scope = Scope.getScope(this);
69
73
  tree = Scope.treeWithScope(tree, scope);
70
74
  }
71
- tree = transformObject(ExplorableSiteTransform, tree);
75
+ if (!isTransformApplied(ExplorableSiteTransform, tree)) {
76
+ tree = transformObject(ExplorableSiteTransform, tree);
77
+ }
72
78
  return tree;
73
79
  };
74
80
  }
@@ -1,5 +1,5 @@
1
- import { isPlainObject } from "@weborigami/async-tree";
2
- import txtHandler from "../builtins/txt_handler.js";
1
+ import { isPlainObject, isUnpackable } from "@weborigami/async-tree";
2
+ // import txtHandler from "../builtins/txt_handler.js";
3
3
 
4
4
  /**
5
5
  * In Origami, a text document object is any object with a `@text` property and
@@ -12,9 +12,15 @@ import txtHandler from "../builtins/txt_handler.js";
12
12
  * @param {StringLike|PlainObject} input
13
13
  * @param {any} [data]
14
14
  */
15
- export default function documentObject(input, data) {
15
+ export default async function documentObject(input, data) {
16
16
  let text;
17
17
  let inputData;
18
+
19
+ if (isUnpackable(input)) {
20
+ // Unpack the input first, might already be a document object.
21
+ input = await input.unpack();
22
+ }
23
+
18
24
  if (isPlainObject(input)) {
19
25
  text = input["@text"];
20
26
  inputData = input;
@@ -22,12 +28,15 @@ export default function documentObject(input, data) {
22
28
  text = String(input);
23
29
  inputData = null;
24
30
  }
25
- const base = {
26
- pack() {
27
- return txtHandler.pack(this);
28
- },
29
- };
30
- const result = Object.create(base);
31
+ // TODO: Either restore this code, or move responsibility for packing a
32
+ // document to HandleExtensionsTransform set().
33
+ // const base = {
34
+ // pack() {
35
+ // return txtHandler.pack(this);
36
+ // },
37
+ // };
38
+ // const result = Object.create(base);
39
+ const result = {};
31
40
  Object.assign(result, inputData, data, { "@text": text });
32
41
  return result;
33
42
  }
@@ -45,10 +45,13 @@ export const keySymbol = Symbol("key");
45
45
  * @param {string} resultExtension
46
46
  */
47
47
  export function replaceExtension(key, sourceExtension, resultExtension) {
48
- if (!key.endsWith(sourceExtension)) {
48
+ if (!key) {
49
+ return undefined;
50
+ } else if (!key.endsWith(sourceExtension)) {
49
51
  return key;
52
+ } else {
53
+ return key.slice(0, -sourceExtension.length) + resultExtension;
50
54
  }
51
- return key.slice(0, -sourceExtension.length) + resultExtension;
52
55
  }
53
56
 
54
57
  /**
@@ -1,8 +1,8 @@
1
1
  /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
2
- import { ObjectTree, Tree } from "@weborigami/async-tree";
2
+ import { ObjectTree } from "@weborigami/async-tree";
3
3
  import { Scope } from "@weborigami/language";
4
4
  import ori from "../builtins/@ori.js";
5
- import { keySymbol, transformObject } from "../common/utilities.js";
5
+ import { keySymbol } from "../common/utilities.js";
6
6
 
7
7
  /**
8
8
  * Add support for commands prefixed with `!`.
@@ -34,11 +34,6 @@ export default function OriCommandTransform(Base) {
34
34
  const extendedScope = new Scope(ambientsTree, Scope.getScope(this));
35
35
  const source = key.slice(1).trim();
36
36
  value = await ori.call(extendedScope, source, { formatResult: false });
37
-
38
- // Ensure this transform is applied to any subtree.
39
- if (Tree.isAsyncTree(value)) {
40
- value = transformObject(OriCommandTransform, value);
41
- }
42
37
  }
43
38
 
44
39
  return value;
@@ -25,7 +25,7 @@ export default async function constructResponse(request, resource) {
25
25
  if (resource instanceof Response) {
26
26
  // Already a Response, return as is.
27
27
  return resource;
28
- } else if (!resource) {
28
+ } else if (resource == null) {
29
29
  return null;
30
30
  }
31
31
 
@@ -42,14 +42,8 @@ export default async function constructResponse(request, resource) {
42
42
  mediaType = extension ? mediaTypeForExtension[extension] : undefined;
43
43
  }
44
44
 
45
- if (
46
- mediaType === undefined &&
47
- !url.pathname.endsWith("/") &&
48
- (Tree.isAsyncTree(resource) ||
49
- isPlainObject(resource) ||
50
- resource instanceof Array)
51
- ) {
52
- // Redirect to an index page for the result.
45
+ if (!url.pathname.endsWith("/") && Tree.isTreelike(resource)) {
46
+ // Treelike resource: redirect to its index page.
53
47
  const Location = `${request.url}/`;
54
48
  return new Response("ok", {
55
49
  headers: {
@@ -120,8 +120,11 @@ function keysFromUrl(url) {
120
120
  pathKeys[pathKeys.length - 1] = "index.html";
121
121
  }
122
122
 
123
- // Add back the `!` to commands.
124
- const commandKeys = parts.map((command) => `!${command}`);
123
+ // Decode the text of the commands, prefix spaces with a backslash, and add
124
+ // back the `!` character.
125
+ const commandKeys = parts.map(
126
+ (command) => `!${decodeURIComponent(command).replace(/ /g, "\\ ")}`
127
+ );
125
128
 
126
129
  const keys = [...pathKeys, ...commandKeys];
127
130
  return keys;