@weborigami/origami 0.0.48 → 0.0.50

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.
Files changed (72) hide show
  1. package/exports/buildExports.js +2 -2
  2. package/exports/exports.js +29 -13
  3. package/index.ts +0 -3
  4. package/package.json +10 -10
  5. package/src/builtins/@addNextPrevious.js +58 -0
  6. package/src/builtins/{@arrows.js → @arrowsMap.js} +7 -7
  7. package/src/builtins/@changes.js +46 -0
  8. package/src/builtins/@clean.js +19 -0
  9. package/src/builtins/@constructor.js +17 -0
  10. package/src/builtins/@crawl.js +2 -2
  11. package/src/builtins/@debug.js +17 -19
  12. package/src/builtins/@deepMap.js +19 -0
  13. package/src/builtins/@deepMapFn.js +25 -0
  14. package/src/builtins/{@mergeDeep.js → @deepMerge.js} +7 -7
  15. package/src/builtins/@deepTake.js +21 -0
  16. package/src/builtins/@deepTakeFn.js +22 -0
  17. package/src/builtins/{@valuesDeep.js → @deepValues.js} +6 -5
  18. package/src/builtins/@document.js +1 -2
  19. package/src/builtins/@files.js +14 -1
  20. package/src/builtins/@group.js +20 -0
  21. package/src/builtins/@groupFn.js +30 -0
  22. package/src/builtins/@if.js +2 -1
  23. package/src/builtins/@image/format.js +10 -31
  24. package/src/builtins/@image/formatFn.js +15 -0
  25. package/src/builtins/@image/resize.js +7 -28
  26. package/src/builtins/@image/resizeFn.js +14 -0
  27. package/src/builtins/@inline.js +8 -2
  28. package/src/builtins/@invoke.js +1 -1
  29. package/src/builtins/@json.js +5 -1
  30. package/src/builtins/@jsonParse.js +9 -0
  31. package/src/builtins/@map.js +10 -170
  32. package/src/builtins/@mapFn.js +143 -0
  33. package/src/builtins/@mdHtml.js +2 -0
  34. package/src/builtins/@mdTree.js +69 -0
  35. package/src/builtins/@naturalOrder.js +1 -0
  36. package/src/builtins/@ori.js +1 -1
  37. package/src/builtins/@paginate.js +18 -0
  38. package/src/builtins/@paginateFn.js +61 -0
  39. package/src/builtins/@perf.js +1 -1
  40. package/src/builtins/@redirect.js +10 -1
  41. package/src/builtins/@regexParse.js +5 -0
  42. package/src/builtins/@regexParseFn.js +9 -0
  43. package/src/builtins/@rss.js +8 -4
  44. package/src/builtins/@sitemap.js +4 -4
  45. package/src/builtins/@slug.js +15 -0
  46. package/src/builtins/@sort.js +10 -7
  47. package/src/builtins/@sortFn.js +58 -0
  48. package/src/builtins/@take.js +3 -17
  49. package/src/builtins/@takeFn.js +21 -0
  50. package/src/builtins/@tree.js +2 -14
  51. package/src/builtins/@yaml.js +4 -0
  52. package/src/builtins/@yamlParse.js +10 -0
  53. package/src/builtins/map.d.ts +6 -7
  54. package/src/common/ExplorableSiteTransform.js +16 -10
  55. package/src/common/ShuffleTransform.js +3 -3
  56. package/src/common/{arrowFunctionsMap.js → arrowsMapFn.js} +3 -3
  57. package/src/common/documentObject.js +18 -9
  58. package/src/common/serialize.js +1 -10
  59. package/src/common/utilities.js +5 -2
  60. package/src/misc/OriCommandTransform.js +2 -7
  61. package/src/misc/explore.ori +7 -7
  62. package/src/server/constructResponse.js +3 -9
  63. package/src/server/server.js +5 -2
  64. package/src/builtins/@apply.js +0 -6
  65. package/src/builtins/@groupBy.js +0 -37
  66. package/src/builtins/@isAsyncTree.js +0 -17
  67. package/src/builtins/@mapDeep.js +0 -22
  68. package/src/builtins/@new.js +0 -6
  69. package/src/builtins/@parse/json.js +0 -6
  70. package/src/builtins/@parse/yaml.js +0 -8
  71. package/src/builtins/@sortBy.js +0 -37
  72. package/src/builtins/@with.js +0 -22
@@ -0,0 +1,20 @@
1
+ import getTreeArgument from "../misc/getTreeArgument.js";
2
+ import groupFn from "./@groupFn.js";
3
+
4
+ /**
5
+ * Map a tree to a new tree with the values from the original tree grouped by
6
+ * the given function.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ *
10
+ * @this {AsyncTree|null}
11
+ * @param {import("@weborigami/async-tree").Treelike} treelike
12
+ * @param {import("../../index.ts").Invocable} groupKey
13
+ */
14
+ export default async function groupBuiltin(treelike, groupKey) {
15
+ const tree = await getTreeArgument(this, arguments, treelike, "@sort");
16
+ return groupFn.call(this, groupKey)(tree);
17
+ }
18
+
19
+ groupBuiltin.usage = `@group <tree>, <fn>\tGroup a tree's values using the given function`;
20
+ groupBuiltin.documentation = "https://weborigami.org/builtins/@group.html";
@@ -0,0 +1,30 @@
1
+ import { groupFn } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+ import { toFunction } from "../common/utilities.js";
4
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
5
+
6
+ /**
7
+ * Return a function that maps a tree to a new tree with the values from the
8
+ * original tree grouped by the given function.
9
+ *
10
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
11
+ *
12
+ * @this {AsyncTree|null}
13
+ * @param {import("../../index.ts").Invocable} groupKey
14
+ */
15
+ export default function groupFnBuiltin(groupKey) {
16
+ assertScopeIsDefined(this);
17
+ const scope = this;
18
+ const groupKeyFn = toFunction(groupKey);
19
+ // @ts-ignore
20
+ const fn = groupFn(groupKeyFn);
21
+ return async (treelike) => {
22
+ const grouped = await fn(treelike);
23
+ const scoped = Scope.treeWithScope(grouped, scope);
24
+ return scoped;
25
+ };
26
+ }
27
+
28
+ groupFnBuiltin.usage = `@groupBy <tree>, [groupKeyFn]\tReturn a new tree with the original's values grouped`;
29
+ groupFnBuiltin.documentation =
30
+ "https://weborigami.org/cli/builtins.html#@group";
@@ -16,7 +16,8 @@ export default async function ifCommand(value, trueResult, falseResult) {
16
16
  condition = keys.length > 0;
17
17
  }
18
18
 
19
- let result = condition ? trueResult : falseResult;
19
+ // 0 is true, null/undefined/false is false
20
+ let result = condition || condition === 0 ? trueResult : falseResult;
20
21
  if (typeof result === "function") {
21
22
  result = await result.call(this);
22
23
  }
@@ -1,39 +1,18 @@
1
- import sharp from "sharp";
1
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
2
+ import imageFormatFn from "./formatFn.js";
2
3
 
3
4
  /**
4
5
  * Return the image in a different format.
5
6
  *
6
7
  * @this {import("@weborigami/types").AsyncTree|null}
7
8
  *
8
- * @typedef {import("sharp").ResizeOptions} ResizeOptions
9
- *
10
- * @overload
11
- * @param {any} param1
12
- * @param {any} param2
13
- * @returns {(buffer: Buffer) => Promise<Buffer>}
14
- *
15
- * @overload
16
- * @param {Buffer} param1
17
- * @param {any} param2
18
- * @param {any} param3
19
- * @returns {Promise<Buffer>}
9
+ * @this {import("@weborigami/types").AsyncTree|null}
10
+ * @param {Buffer} buffer
11
+ * @param {keyof import("sharp").FormatEnum|import("sharp").AvailableFormatInfo}
12
+ * format
13
+ * @param {any} options
20
14
  */
21
- export default function format(param1, param2, param3) {
22
- // Identify which overload was used.
23
- let buffer;
24
- let format;
25
- let options;
26
- if (param1 instanceof Buffer) {
27
- buffer = param1;
28
- format = param2;
29
- options = param3;
30
- } else {
31
- format = param1;
32
- options = param2;
33
- }
34
-
35
- const transform = (buffer) =>
36
- sharp(buffer).toFormat(format, options).toBuffer();
37
-
38
- return buffer ? transform(buffer) : transform;
15
+ export default async function imageFormat(buffer, format, options) {
16
+ assertScopeIsDefined(this, "image/format");
17
+ return imageFormatFn.call(this, format, options)(buffer);
39
18
  }
@@ -0,0 +1,15 @@
1
+ import sharp from "sharp";
2
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
3
+
4
+ /**
5
+ * Return a function that transforms an input image to a different format.
6
+ *
7
+ * @this {import("@weborigami/types").AsyncTree|null}
8
+ * @param {keyof import("sharp").FormatEnum|import("sharp").AvailableFormatInfo}
9
+ * format
10
+ * @param {any} options
11
+ */
12
+ export default function imageFormatFn(format, options) {
13
+ assertScopeIsDefined(this, "image/formatFn");
14
+ return (buffer) => sharp(buffer).toFormat(format, options).toBuffer();
15
+ }
@@ -1,35 +1,14 @@
1
- import sharp from "sharp";
1
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
2
+ import imageResizeFn from "./resizeFn.js";
2
3
 
3
4
  /**
4
5
  * Resize an image.
5
6
  *
6
7
  * @this {import("@weborigami/types").AsyncTree|null}
7
- *
8
- * @typedef {import("sharp").ResizeOptions} ResizeOptions
9
- *
10
- * @overload
11
- * @param {ResizeOptions} param1
12
- * @returns {(buffer: Buffer) => Promise<Buffer>}
13
- *
14
- * @overload
15
- * @param {Buffer} param1
16
- * @param {ResizeOptions} param2
17
- * @returns {Promise<Buffer>}
8
+ * @param {Buffer} buffer
9
+ * @param {import("sharp").ResizeOptions} options
18
10
  */
19
- export default function resize(param1, param2) {
20
- // Identify which overload was used.
21
- let buffer;
22
- let options;
23
- if (param2 === undefined) {
24
- options = param1;
25
- } else {
26
- buffer = param1;
27
- options = param2;
28
- }
29
-
30
- // Include `rotate()` to auto-rotate according to EXIF data.
31
- const transform = (buffer) =>
32
- sharp(buffer).rotate().resize(options).toBuffer();
33
-
34
- return buffer ? transform(buffer) : transform;
11
+ export default async function resize(buffer, options) {
12
+ assertScopeIsDefined(this, "image/resize");
13
+ return imageResizeFn.call(this, options)(buffer);
35
14
  }
@@ -0,0 +1,14 @@
1
+ import sharp from "sharp";
2
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
3
+
4
+ /**
5
+ * Return a function that resizes an image.
6
+ *
7
+ * @this {import("@weborigami/types").AsyncTree|null}
8
+ * @param {import("sharp").ResizeOptions} options
9
+ */
10
+ export default function imageResizeFn(options) {
11
+ assertScopeIsDefined(this, "image/resizeFn");
12
+ // Include `rotate()` to auto-rotate according to EXIF data.
13
+ return (buffer) => sharp(buffer).rotate().resize(options).toBuffer();
14
+ }
@@ -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.
@@ -3,7 +3,7 @@ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
3
3
  import builtins from "./@builtins.js";
4
4
 
5
5
  /**
6
- * Invoke the given function.
6
+ * Invoke the given text as an Origami function.
7
7
  *
8
8
  * This built-in exists to facilitate executing an Origami file as a script via
9
9
  * a [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) directive.
@@ -1,4 +1,5 @@
1
1
  /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
2
+ import { isUnpackable } from "@weborigami/async-tree";
2
3
  import * as serialize from "../common/serialize.js";
3
4
  import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
4
5
 
@@ -20,9 +21,12 @@ export default async function json(obj) {
20
21
  if (obj === undefined) {
21
22
  return undefined;
22
23
  }
24
+ if (isUnpackable(obj)) {
25
+ obj = await obj.unpack();
26
+ }
23
27
  const value = await serialize.toJsonValue(obj);
24
28
  return JSON.stringify(value, null, 2);
25
29
  }
26
30
 
27
31
  json.usage = "@json <obj>\tRender the object as text in JSON format";
28
- json.documentation = "https://weborigami.org/language/@json.html";
32
+ json.documentation = "https://weborigami.org/builtins/@json.html";
@@ -0,0 +1,9 @@
1
+ import { toString } from "../common/utilities.js";
2
+
3
+ export default async function jsonParse(input) {
4
+ const text = toString(input);
5
+ return text ? JSON.parse(text) : undefined;
6
+ }
7
+
8
+ jsonParse.usage = `@jsonParse <text>\tParse text as JSON`;
9
+ jsonParse.documentation = "https://weborigami.org/builtins/@jsonParse.html";
@@ -1,179 +1,19 @@
1
- import {
2
- cachedKeyFunctions,
3
- keyFunctionsForExtensions,
4
- map,
5
- } from "@weborigami/async-tree";
6
- import { Scope } from "@weborigami/language";
7
- import addValueKeyToScope from "../common/addValueKeyToScope.js";
8
- import { toFunction } from "../common/utilities.js";
1
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
2
+ import mapFn from "./@mapFn.js";
9
3
 
10
4
  /**
11
5
  * Map a hierarchical tree of keys and values to a new tree of keys and values.
12
6
  *
13
- * @typedef {import("@weborigami/async-tree").KeyFn} KeyFn
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
14
8
  * @typedef {import("@weborigami/async-tree").Treelike} Treelike
15
9
  * @typedef {import("@weborigami/async-tree").ValueKeyFn} ValueKeyFn
16
- * @typedef {import("@weborigami/async-tree").TreeTransform} TreeTransform
17
- * @typedef {import("../../index.ts").TreelikeTransform} TreelikeTransform
18
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
19
- *
20
- * @typedef {{ deep?: boolean, description?: string, extension?: string,
21
- * extensions?: string, inverseKey?: KeyFn, key?: ValueKeyFn, keyMap?:
22
- * ValueKeyFn, value?: ValueKeyFn, valueFn?: ValueKeyFn }} TreeMapOptions
23
- *
24
- * @this {import("@weborigami/types").AsyncTree|null}
25
- *
26
- * @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
37
- * @returns {AsyncTree}
38
- *
39
- * @overload
40
- * @param {Treelike} param1
41
- * @param {TreeMapOptions} param2
42
- * @returns {AsyncTree}
43
- */
44
- export default function treeMap(param1, param2) {
45
- // Identify whether the valueFn/options are the first parameter
46
- // or the second.
47
- let source;
48
- let options;
49
- if (arguments.length === 0) {
50
- throw new TypeError(
51
- `@map: You must give @map a function or a dictionary of options.`
52
- );
53
- } else if (!param1) {
54
- throw new TypeError(`@map: The first argument was undefined.`);
55
- } else if (arguments.length === 1) {
56
- options = param1;
57
- } else if (!param2) {
58
- throw new TypeError(`@map: The second argument was undefined.`);
59
- } else {
60
- source = param1;
61
- options = param2;
62
- }
63
-
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"
70
- ) {
71
- valueFn = options;
72
- options = {};
73
- } else if (!options) {
74
- throw new TypeError(
75
- `@map: You must specify a value function or options dictionary.`
76
- );
77
- } else {
78
- valueFn = options.value ?? options.valueMap;
79
- }
80
-
81
- let { deep, description, inverseKey, needsSourceValue } = options;
82
- let extension = options.extension ?? options.extensions;
83
- let keyFn = options.keyMap ?? options.key;
84
-
85
- description ??= `@map ${extension ?? ""}`;
86
-
87
- if (extension && (keyFn || inverseKey)) {
88
- throw new TypeError(
89
- `@map: You can't specify extensions and also a key or inverseKey function`
90
- );
91
- }
92
-
93
- const baseScope = Scope.getScope(this);
94
-
95
- // Extend the value function to include the value and key in scope.
96
- let extendedValueFn;
97
- if (valueFn) {
98
- const resolvedValueFn = toFunction(valueFn);
99
- extendedValueFn = function (sourceValue, sourceKey, tree) {
100
- const scope = addValueKeyToScope(baseScope, sourceValue, sourceKey);
101
- return resolvedValueFn.call(scope, sourceValue, sourceKey, tree);
102
- };
103
- }
104
-
105
- // Extend the key function to include the value and key in scope.
106
- let extendedKeyFn;
107
- let extendedInverseKeyFn;
108
- if (extension) {
109
- let { resultExtension, sourceExtension } = parseExtensions(extension);
110
- const keyFns = keyFunctionsForExtensions({
111
- resultExtension,
112
- sourceExtension,
113
- });
114
- extendedKeyFn = keyFns.key;
115
- extendedInverseKeyFn = keyFns.inverseKey;
116
- } else if (keyFn) {
117
- const resolvedKeyFn = toFunction(keyFn);
118
- async function scopedKeyFn(sourceKey, tree) {
119
- const sourceValue = await tree.get(sourceKey);
120
- const scope = addValueKeyToScope(baseScope, sourceValue, sourceKey);
121
- const resultKey = await resolvedKeyFn.call(
122
- scope,
123
- sourceValue,
124
- sourceKey,
125
- tree
126
- );
127
- return resultKey;
128
- }
129
- const keyFns = cachedKeyFunctions(scopedKeyFn);
130
- extendedKeyFn = keyFns.key;
131
- extendedInverseKeyFn = keyFns.inverseKey;
132
- } else {
133
- // Use sidecar keyFn/inverseKey functions if the valueFn defines them.
134
- extendedKeyFn = valueFn?.key;
135
- extendedInverseKeyFn = valueFn?.inverseKey;
136
- }
137
-
138
- const transform = function mapTreelike(treelike) {
139
- return map({
140
- deep,
141
- description,
142
- inverseKey: extendedInverseKeyFn,
143
- key: extendedKeyFn,
144
- needsSourceValue,
145
- value: extendedValueFn,
146
- })(treelike);
147
- };
148
-
149
- return source ? transform(source) : transform;
150
- }
151
-
152
- /**
153
- * Given a string specifying an extension or a mapping of one extension to another,
154
- * return the source and result extensions.
155
- *
156
- * Syntax:
157
- * foo
158
- * foo→bar Unicode Rightwards Arrow
159
- * foo->bar hyphen and greater-than sign
10
+ * @typedef {import("./map.d.ts").TreeMapOptions} TreeMapOptions
160
11
  *
161
- * @param {string} specifier
12
+ * @this {AsyncTree|null}
13
+ * @param {Treelike} source
14
+ * @param {ValueKeyFn|TreeMapOptions} operation
162
15
  */
163
- function parseExtensions(specifier) {
164
- const lowercase = specifier?.toLowerCase() ?? "";
165
- const extensionRegex =
166
- /^\.?(?<sourceExtension>\S*)(?:\s*(→|->)\s*)\.?(?<extension>\S+)$/;
167
- let resultExtension;
168
- let sourceExtension;
169
- const match = lowercase.match(extensionRegex);
170
- if (match?.groups) {
171
- // foo→bar
172
- ({ extension: resultExtension, sourceExtension } = match.groups);
173
- } else {
174
- // foo
175
- resultExtension = lowercase;
176
- sourceExtension = lowercase;
177
- }
178
- return { resultExtension, sourceExtension };
16
+ export default function map(source, operation) {
17
+ assertScopeIsDefined(this, "map");
18
+ return mapFn.call(this, operation)(source);
179
19
  }
@@ -0,0 +1,143 @@
1
+ import {
2
+ cachedKeyFunctions,
3
+ isPlainObject,
4
+ keyFunctionsForExtensions,
5
+ mapFn,
6
+ } from "@weborigami/async-tree";
7
+ import { Scope } from "@weborigami/language";
8
+ import { toFunction } from "../common/utilities.js";
9
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
10
+
11
+ /**
12
+ * Return a function that transforms a tree of keys and values to a new tree of
13
+ * keys and values.
14
+ *
15
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
16
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
17
+ * @typedef {import("@weborigami/async-tree").ValueKeyFn} ValueKeyFn
18
+ * @typedef {import("./map.d.ts").TreeMapOptions} TreeMapOptions
19
+ *
20
+ * @this {AsyncTree|null}
21
+ * @param {ValueKeyFn|TreeMapOptions} operation
22
+ */
23
+ export default function mapFnBuiltin(operation) {
24
+ assertScopeIsDefined(this, "map");
25
+ const scope = this;
26
+
27
+ // Identify whether the map instructions take the form of a value function or
28
+ // a dictionary of options.
29
+ /** @type {TreeMapOptions} */
30
+ let options;
31
+ /** @type {ValueKeyFn|undefined} */
32
+ let valueFn;
33
+ if (isPlainObject(operation)) {
34
+ // @ts-ignore
35
+ options = operation;
36
+ valueFn = options?.value;
37
+ } else if (
38
+ typeof operation === "function" ||
39
+ typeof (/** @type {any} */ (operation)?.unpack) === "function"
40
+ ) {
41
+ valueFn = operation;
42
+ options = {};
43
+ } else {
44
+ throw new TypeError(
45
+ `@mapFn: You must specify a value function or options dictionary as the first parameter.`
46
+ );
47
+ }
48
+
49
+ const { deep, extension, needsSourceValue } = options;
50
+ const description = options.description ?? `@mapFn ${extension ?? ""}`;
51
+ const keyFn = options.key;
52
+ const inverseKeyFn = options.inverseKey;
53
+
54
+ if (extension && (keyFn || inverseKeyFn)) {
55
+ throw new TypeError(
56
+ `@mapFn: You can't specify extensions and also a key or inverseKey function`
57
+ );
58
+ }
59
+
60
+ // Extend the value function to include the value and key in scope.
61
+ let extendedValueFn;
62
+ if (valueFn) {
63
+ const resolvedValueFn = toFunction(valueFn);
64
+ extendedValueFn = (sourceValue, sourceKey, tree) =>
65
+ resolvedValueFn.call(scope, sourceValue, sourceKey, tree);
66
+ }
67
+
68
+ // Extend the key function to include the value and key in scope.
69
+ let extendedKeyFn;
70
+ let extendedInverseKeyFn;
71
+ if (extension) {
72
+ let { resultExtension, sourceExtension } = parseExtensions(extension);
73
+ const keyFns = keyFunctionsForExtensions({
74
+ resultExtension,
75
+ sourceExtension,
76
+ });
77
+ extendedKeyFn = keyFns.key;
78
+ extendedInverseKeyFn = keyFns.inverseKey;
79
+ } else if (keyFn) {
80
+ const resolvedKeyFn = toFunction(keyFn);
81
+ async function scopedKeyFn(sourceKey, tree) {
82
+ const sourceValue = await tree.get(sourceKey);
83
+ const resultKey = await resolvedKeyFn.call(
84
+ scope,
85
+ sourceValue,
86
+ sourceKey,
87
+ tree
88
+ );
89
+ return resultKey;
90
+ }
91
+ const keyFns = cachedKeyFunctions(scopedKeyFn);
92
+ extendedKeyFn = keyFns.key;
93
+ extendedInverseKeyFn = keyFns.inverseKey;
94
+ } else {
95
+ // Use sidecar keyFn/inverseKey functions if the valueFn defines them.
96
+ extendedKeyFn = /** @type {any} */ (valueFn)?.key;
97
+ extendedInverseKeyFn = /** @type {any} */ (valueFn)?.inverseKey;
98
+ }
99
+
100
+ const fn = mapFn({
101
+ deep,
102
+ description,
103
+ inverseKey: extendedInverseKeyFn,
104
+ key: extendedKeyFn,
105
+ needsSourceValue,
106
+ value: extendedValueFn,
107
+ });
108
+
109
+ return (treelike) => {
110
+ const mapped = fn(treelike);
111
+ const scoped = Scope.treeWithScope(mapped, scope);
112
+ return scoped;
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Given a string specifying an extension or a mapping of one extension to another,
118
+ * return the source and result extensions.
119
+ *
120
+ * Syntax:
121
+ * foo
122
+ * foo→bar Unicode Rightwards Arrow
123
+ * foo->bar hyphen and greater-than sign
124
+ *
125
+ * @param {string} specifier
126
+ */
127
+ function parseExtensions(specifier) {
128
+ const lowercase = specifier?.toLowerCase() ?? "";
129
+ const extensionRegex =
130
+ /^\.?(?<sourceExtension>\S*)(?:\s*(→|->)\s*)\.?(?<extension>\S+)$/;
131
+ let resultExtension;
132
+ let sourceExtension;
133
+ const match = lowercase.match(extensionRegex);
134
+ if (match?.groups) {
135
+ // foo→bar
136
+ ({ extension: resultExtension, sourceExtension } = match.groups);
137
+ } else {
138
+ // foo
139
+ resultExtension = lowercase;
140
+ sourceExtension = lowercase;
141
+ }
142
+ return { resultExtension, sourceExtension };
143
+ }
@@ -26,6 +26,8 @@ marked.use(
26
26
  );
27
27
 
28
28
  /**
29
+ * Transform markdown to HTML.
30
+ *
29
31
  * @typedef {import("@weborigami/async-tree").StringLike} StringLike
30
32
  * @typedef {import("@weborigami/async-tree").Unpackable<StringLike>} UnpackableStringlike
31
33
  *
@@ -0,0 +1,69 @@
1
+ import { toString } from "../common/utilities.js";
2
+
3
+ /**
4
+ * Returns the tree structure of a markdown document.
5
+ *
6
+ * @typedef {import("@weborigami/async-tree").StringLike} StringLike
7
+ * @typedef {import("@weborigami/async-tree").Unpackable<StringLike>}
8
+ * UnpackableStringlike
9
+ *
10
+ * @this {import("@weborigami/types").AsyncTree|null|void}
11
+ * @param {StringLike|UnpackableStringlike} input
12
+ */
13
+ export default function mdStructure(input) {
14
+ const markdown = toString(input);
15
+ if (markdown === null) {
16
+ throw new Error("No markdown text provided.");
17
+ }
18
+
19
+ // The document acts as an entry for heading level zero. All level one
20
+ // headings will end up as its children.
21
+ const document = {};
22
+ const activeHeadings = [document];
23
+
24
+ // Split the text by lines that contain markdown headings.
25
+ const lines = markdown.split("\n");
26
+ lines.forEach((line) => {
27
+ const match = line.match(/^(?<levelMarkers>#{1,6})\s(?<heading>.*)$/);
28
+ if (!match?.groups) {
29
+ return;
30
+ }
31
+ const { levelMarkers, heading } = match.groups;
32
+ const level = levelMarkers.length;
33
+
34
+ // If we've gone up a level (or more), pop completed entries off the stack.
35
+ while (activeHeadings.length > level) {
36
+ activeHeadings.pop();
37
+ }
38
+
39
+ // If we've skipped a level (or more), add intermediate entries using
40
+ // symbols to avoid name collisions.
41
+ while (activeHeadings.length < level) {
42
+ const entry = {};
43
+ const parentEntry = activeHeadings[activeHeadings.length - 1];
44
+ parentEntry[Symbol()] = entry;
45
+ activeHeadings.push(entry);
46
+ }
47
+
48
+ // Add a new entry to the list of children under construction.
49
+ const entry = {};
50
+ const parentEntry = activeHeadings[activeHeadings.length - 1];
51
+ parentEntry[heading] = entry;
52
+ activeHeadings.push(entry);
53
+ });
54
+
55
+ return pruneEmptyObjects(document) ?? {};
56
+ }
57
+
58
+ // Replace empty objects in the tree with nulls.
59
+ function pruneEmptyObjects(tree) {
60
+ const keys = [...Object.keys(tree), ...Object.getOwnPropertySymbols(tree)];
61
+ if (keys.length === 0) {
62
+ return null;
63
+ }
64
+ const result = {};
65
+ for (const key of keys) {
66
+ result[key] = pruneEmptyObjects(tree[key]);
67
+ }
68
+ return result;
69
+ }
@@ -0,0 +1 @@
1
+ export { naturalOrder as default } from "@weborigami/async-tree";
@@ -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") {