@weborigami/origami 0.0.49 → 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 (58) hide show
  1. package/exports/buildExports.js +2 -2
  2. package/exports/exports.js +27 -14
  3. package/package.json +4 -4
  4. package/src/builtins/@addNextPrevious.js +18 -7
  5. package/src/builtins/{@arrows.js → @arrowsMap.js} +7 -7
  6. package/src/builtins/@changes.js +46 -0
  7. package/src/builtins/@clean.js +19 -0
  8. package/src/builtins/@constructor.js +17 -0
  9. package/src/builtins/@crawl.js +2 -2
  10. package/src/builtins/@deepMap.js +19 -0
  11. package/src/builtins/@deepMapFn.js +25 -0
  12. package/src/builtins/{@mergeDeep.js → @deepMerge.js} +7 -7
  13. package/src/builtins/@deepTake.js +21 -0
  14. package/src/builtins/@deepTakeFn.js +22 -0
  15. package/src/builtins/{@valuesDeep.js → @deepValues.js} +6 -5
  16. package/src/builtins/@files.js +14 -1
  17. package/src/builtins/@group.js +20 -0
  18. package/src/builtins/@groupFn.js +30 -0
  19. package/src/builtins/@if.js +2 -1
  20. package/src/builtins/@image/format.js +10 -31
  21. package/src/builtins/@image/formatFn.js +15 -0
  22. package/src/builtins/@image/resize.js +7 -28
  23. package/src/builtins/@image/resizeFn.js +14 -0
  24. package/src/builtins/@invoke.js +1 -1
  25. package/src/builtins/@json.js +5 -1
  26. package/src/builtins/@jsonParse.js +9 -0
  27. package/src/builtins/@map.js +7 -178
  28. package/src/builtins/@mapFn.js +143 -0
  29. package/src/builtins/@mdHtml.js +2 -0
  30. package/src/builtins/@mdTree.js +69 -0
  31. package/src/builtins/@naturalOrder.js +1 -0
  32. package/src/builtins/@paginate.js +18 -0
  33. package/src/builtins/@paginateFn.js +61 -0
  34. package/src/builtins/@redirect.js +10 -1
  35. package/src/builtins/@regexParse.js +5 -0
  36. package/src/builtins/{@makeParser.js → @regexParseFn.js} +1 -1
  37. package/src/builtins/@sitemap.js +4 -4
  38. package/src/builtins/@sort.js +10 -7
  39. package/src/builtins/@sortFn.js +58 -0
  40. package/src/builtins/@take.js +3 -17
  41. package/src/builtins/@takeFn.js +21 -0
  42. package/src/builtins/@tree.js +2 -14
  43. package/src/builtins/@yaml.js +4 -0
  44. package/src/builtins/@yamlParse.js +10 -0
  45. package/src/builtins/map.d.ts +1 -1
  46. package/src/common/ShuffleTransform.js +3 -3
  47. package/src/common/{arrowFunctionsMap.js → arrowsMapFn.js} +3 -3
  48. package/src/common/serialize.js +1 -10
  49. package/src/misc/explore.ori +7 -7
  50. package/src/builtins/@apply.js +0 -6
  51. package/src/builtins/@groupBy.js +0 -37
  52. package/src/builtins/@isAsyncTree.js +0 -17
  53. package/src/builtins/@mapDeep.js +0 -71
  54. package/src/builtins/@new.js +0 -6
  55. package/src/builtins/@parse/json.js +0 -9
  56. package/src/builtins/@parse/yaml.js +0 -10
  57. package/src/builtins/@sortBy.js +0 -37
  58. package/src/builtins/@with.js +0 -22
@@ -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
+ }
@@ -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,190 +1,19 @@
1
- import {
2
- cachedKeyFunctions,
3
- isPlainObject,
4
- keyFunctionsForExtensions,
5
- map,
6
- } from "@weborigami/async-tree";
7
- import { Scope } from "@weborigami/language";
8
- import addValueKeyToScope from "../common/addValueKeyToScope.js";
9
- import { toFunction } from "../common/utilities.js";
10
1
  import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
2
+ import mapFn from "./@mapFn.js";
11
3
 
12
4
  /**
13
5
  * Map a hierarchical tree of keys and values to a new tree of keys and values.
14
6
  *
15
- * @typedef {import("@weborigami/async-tree").KeyFn} KeyFn
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
16
8
  * @typedef {import("@weborigami/async-tree").Treelike} Treelike
17
9
  * @typedef {import("@weborigami/async-tree").ValueKeyFn} ValueKeyFn
18
- * @typedef {import("@weborigami/async-tree").TreeTransform} TreeTransform
19
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
20
- *
21
- * @typedef {{ deep?: boolean, description?: string, extension?: string,
22
- * extensions?: string, inverseKey?: KeyFn, key?: ValueKeyFn, keyMap?:
23
- * ValueKeyFn, needsSourceValue?: boolean, value?: ValueKeyFn, valueMap?:
24
- * ValueKeyFn }} MapOptionsDictionary
25
- *
26
- * @typedef {ValueKeyFn|MapOptionsDictionary} OptionsOrValueFn
27
- *
28
- * @overload
29
- * @param {Treelike} source
30
- * @param {OptionsOrValueFn} instructions
31
- * @returns {AsyncTree}
32
- *
33
- * @overload
34
- * @param {OptionsOrValueFn} instructions
35
- * @returns {TreeTransform}
10
+ * @typedef {import("./map.d.ts").TreeMapOptions} TreeMapOptions
36
11
  *
37
12
  * @this {AsyncTree|null}
38
- * @param {Treelike|OptionsOrValueFn} param1
39
- * @param {OptionsOrValueFn} [param2]
13
+ * @param {Treelike} source
14
+ * @param {ValueKeyFn|TreeMapOptions} operation
40
15
  */
41
- export default function treeMap(param1, param2) {
16
+ export default function map(source, operation) {
42
17
  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} */
48
- let source;
49
- /** @type {OptionsOrValueFn} */
50
- let instructions;
51
- /** @type {ValueKeyFn|undefined} */
52
- let valueFn;
53
-
54
- if (arguments.length === 0) {
55
- throw new TypeError(
56
- `@map: You must give @map a function or a dictionary of options.`
57
- );
58
- } else if (!param1) {
59
- throw new TypeError(`@map: The first argument was undefined.`);
60
- } else if (arguments.length === 1) {
61
- // One argument
62
- // @ts-ignore
63
- instructions = param1;
64
- } else if (param2 === undefined) {
65
- throw new TypeError(`@map: The second argument was undefined.`);
66
- } else {
67
- // Two arguments
68
- source = param1;
69
- instructions = param2;
70
- }
71
-
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"
83
- ) {
84
- valueFn = instructions;
85
- options = {};
86
- } else {
87
- throw new TypeError(
88
- `@map: You must specify a value function or options dictionary.`
89
- );
90
- }
91
-
92
- let { deep, description, inverseKey, needsSourceValue } = options;
93
- let extension = options.extension ?? options.extensions;
94
- let keyFn = options.keyMap ?? options.key;
95
-
96
- description ??= `@map ${extension ?? ""}`;
97
-
98
- if (extension && (keyFn || inverseKey)) {
99
- throw new TypeError(
100
- `@map: You can't specify extensions and also a key or inverseKey function`
101
- );
102
- }
103
-
104
- const baseScope = Scope.getScope(this);
105
-
106
- // Extend the value function to include the value and key in scope.
107
- let extendedValueFn;
108
- if (valueFn) {
109
- const resolvedValueFn = toFunction(valueFn);
110
- extendedValueFn = function (sourceValue, sourceKey, tree) {
111
- const scope = addValueKeyToScope(baseScope, sourceValue, sourceKey);
112
- return resolvedValueFn.call(scope, sourceValue, sourceKey, tree);
113
- };
114
- }
115
-
116
- // Extend the key function to include the value and key in scope.
117
- let extendedKeyFn;
118
- let extendedInverseKeyFn;
119
- if (extension) {
120
- let { resultExtension, sourceExtension } = parseExtensions(extension);
121
- const keyFns = keyFunctionsForExtensions({
122
- resultExtension,
123
- sourceExtension,
124
- });
125
- extendedKeyFn = keyFns.key;
126
- extendedInverseKeyFn = keyFns.inverseKey;
127
- } else if (keyFn) {
128
- const resolvedKeyFn = toFunction(keyFn);
129
- async function scopedKeyFn(sourceKey, tree) {
130
- const sourceValue = await tree.get(sourceKey);
131
- const scope = addValueKeyToScope(baseScope, sourceValue, sourceKey);
132
- const resultKey = await resolvedKeyFn.call(
133
- scope,
134
- sourceValue,
135
- sourceKey,
136
- tree
137
- );
138
- return resultKey;
139
- }
140
- const keyFns = cachedKeyFunctions(scopedKeyFn);
141
- extendedKeyFn = keyFns.key;
142
- extendedInverseKeyFn = keyFns.inverseKey;
143
- } else {
144
- // Use sidecar keyFn/inverseKey functions if the valueFn defines them.
145
- extendedKeyFn = /** @type {any} */ (valueFn)?.key;
146
- extendedInverseKeyFn = /** @type {any} */ (valueFn)?.inverseKey;
147
- }
148
-
149
- const transform = function mapTreelike(treelike) {
150
- return map({
151
- deep,
152
- description,
153
- inverseKey: extendedInverseKeyFn,
154
- key: extendedKeyFn,
155
- needsSourceValue,
156
- value: extendedValueFn,
157
- })(treelike);
158
- };
159
-
160
- return source ? transform(source) : transform;
161
- }
162
-
163
- /**
164
- * Given a string specifying an extension or a mapping of one extension to another,
165
- * return the source and result extensions.
166
- *
167
- * Syntax:
168
- * foo
169
- * foo→bar Unicode Rightwards Arrow
170
- * foo->bar hyphen and greater-than sign
171
- *
172
- * @param {string} specifier
173
- */
174
- function parseExtensions(specifier) {
175
- const lowercase = specifier?.toLowerCase() ?? "";
176
- const extensionRegex =
177
- /^\.?(?<sourceExtension>\S*)(?:\s*(→|->)\s*)\.?(?<extension>\S+)$/;
178
- let resultExtension;
179
- let sourceExtension;
180
- const match = lowercase.match(extensionRegex);
181
- if (match?.groups) {
182
- // foo→bar
183
- ({ extension: resultExtension, sourceExtension } = match.groups);
184
- } else {
185
- // foo
186
- resultExtension = lowercase;
187
- sourceExtension = lowercase;
188
- }
189
- return { resultExtension, sourceExtension };
18
+ return mapFn.call(this, operation)(source);
190
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";
@@ -0,0 +1,18 @@
1
+ import getTreeArgument from "../misc/getTreeArgument.js";
2
+ import paginateFn from "./@paginateFn.js";
3
+
4
+ /**
5
+ * Return a new grouping of the treelike's values into chunks of the specified
6
+ * size.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
10
+ *
11
+ * @this {AsyncTree|null}
12
+ * @param {Treelike} [treelike]
13
+ * @param {number} [size=10]
14
+ */
15
+ export default async function paginate(treelike, size = 10) {
16
+ const tree = await getTreeArgument(this, arguments, treelike, "@count");
17
+ return paginateFn.call(this, size)(tree);
18
+ }
@@ -0,0 +1,61 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+
4
+ /**
5
+ * Return a new grouping of the treelike's values into "pages" of the specified
6
+ * size.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
10
+ *
11
+ * @this {AsyncTree|null}
12
+ * @param {number} [size=10]
13
+ */
14
+ export default function paginateFn(size = 10) {
15
+ const scope = this;
16
+ /**
17
+ * @param {Treelike} [treelike]
18
+ */
19
+ return async function (treelike) {
20
+ const tree = Tree.from(treelike);
21
+ const keys = Array.from(await tree.keys());
22
+ const pageCount = Math.ceil(keys.length / size);
23
+
24
+ const result = {
25
+ async get(pageKey) {
26
+ // Note: page numbers are 1-based.
27
+ const pageNumber = Number(pageKey);
28
+ if (Number.isNaN(pageNumber)) {
29
+ return undefined;
30
+ }
31
+ const nextPage = pageNumber + 1 <= pageCount ? pageNumber + 1 : null;
32
+ const previousPage = pageNumber - 1 >= 1 ? pageNumber - 1 : null;
33
+ const items = {};
34
+ for (
35
+ let index = (pageNumber - 1) * size;
36
+ index < Math.min(keys.length, pageNumber * size);
37
+ index++
38
+ ) {
39
+ const key = keys[index];
40
+ items[key] = await tree.get(keys[index]);
41
+ }
42
+
43
+ return {
44
+ items,
45
+ nextPage,
46
+ pageCount,
47
+ pageNumber,
48
+ previousPage,
49
+ };
50
+ },
51
+
52
+ async keys() {
53
+ // Return an array from 1..totalPages
54
+ return Array.from({ length: pageCount }, (_, index) => index + 1);
55
+ },
56
+ };
57
+
58
+ const scoped = Scope.treeWithScope(result, scope);
59
+ return scoped;
60
+ };
61
+ }
@@ -1,8 +1,17 @@
1
1
  export default function redirect(url, options = { permanent: false }) {
2
- return new Response("ok", {
2
+ const response = new Response("ok", {
3
3
  headers: {
4
4
  Location: url,
5
5
  },
6
6
  status: options.permanent ? 301 : 307,
7
7
  });
8
+ /** @type {any} */ (response).pack = () => `<!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ <meta charset="utf-8" />
12
+ <meta http-equiv="refresh" content="0;url=${url}" />
13
+ </head>
14
+ </html>
15
+ `;
16
+ return response;
8
17
  }
@@ -0,0 +1,5 @@
1
+ import regexParser from "./@regexParseFn.js";
2
+
3
+ export default function regexParse(text, regex) {
4
+ return regexParser(regex)(text);
5
+ }
@@ -1,6 +1,6 @@
1
1
  const parsers = {};
2
2
 
3
- export default function makeParser(text) {
3
+ export default function regexParseFn(text) {
4
4
  if (!parsers[text]) {
5
5
  const regexp = new RegExp(text);
6
6
  parsers[text] = (input) => input.match(regexp)?.groups;
@@ -3,13 +3,13 @@ import builtins from "./@builtins.js";
3
3
  import paths from "./@paths.js";
4
4
  import fileTypeOrigami from "./ori_handler.js";
5
5
 
6
- const templateText = `=\`<?xml version="1.0" encoding="UTF-8"?>
6
+ const templateText = `(urls) => \`<?xml version="1.0" encoding="UTF-8"?>
7
7
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
8
- \${ @map(=\`
8
+ \${ @map(urls, (url) => \`
9
9
  <url>
10
- <loc>\${ _ }</loc>
10
+ <loc>\${ url }</loc>
11
11
  </url>
12
- \`)(_) }
12
+ \`) }
13
13
  </urlset>
14
14
  \`
15
15
  `;