@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.
- package/exports/buildExports.js +2 -2
- package/exports/exports.js +27 -14
- package/package.json +4 -4
- package/src/builtins/@addNextPrevious.js +18 -7
- package/src/builtins/{@arrows.js → @arrowsMap.js} +7 -7
- package/src/builtins/@changes.js +46 -0
- package/src/builtins/@clean.js +19 -0
- package/src/builtins/@constructor.js +17 -0
- package/src/builtins/@crawl.js +2 -2
- package/src/builtins/@deepMap.js +19 -0
- package/src/builtins/@deepMapFn.js +25 -0
- package/src/builtins/{@mergeDeep.js → @deepMerge.js} +7 -7
- package/src/builtins/@deepTake.js +21 -0
- package/src/builtins/@deepTakeFn.js +22 -0
- package/src/builtins/{@valuesDeep.js → @deepValues.js} +6 -5
- package/src/builtins/@files.js +14 -1
- package/src/builtins/@group.js +20 -0
- package/src/builtins/@groupFn.js +30 -0
- package/src/builtins/@if.js +2 -1
- package/src/builtins/@image/format.js +10 -31
- package/src/builtins/@image/formatFn.js +15 -0
- package/src/builtins/@image/resize.js +7 -28
- package/src/builtins/@image/resizeFn.js +14 -0
- package/src/builtins/@invoke.js +1 -1
- package/src/builtins/@json.js +5 -1
- package/src/builtins/@jsonParse.js +9 -0
- package/src/builtins/@map.js +7 -178
- package/src/builtins/@mapFn.js +143 -0
- package/src/builtins/@mdHtml.js +2 -0
- package/src/builtins/@mdTree.js +69 -0
- package/src/builtins/@naturalOrder.js +1 -0
- package/src/builtins/@paginate.js +18 -0
- package/src/builtins/@paginateFn.js +61 -0
- package/src/builtins/@redirect.js +10 -1
- package/src/builtins/@regexParse.js +5 -0
- package/src/builtins/{@makeParser.js → @regexParseFn.js} +1 -1
- package/src/builtins/@sitemap.js +4 -4
- package/src/builtins/@sort.js +10 -7
- package/src/builtins/@sortFn.js +58 -0
- package/src/builtins/@take.js +3 -17
- package/src/builtins/@takeFn.js +21 -0
- package/src/builtins/@tree.js +2 -14
- package/src/builtins/@yaml.js +4 -0
- package/src/builtins/@yamlParse.js +10 -0
- package/src/builtins/map.d.ts +1 -1
- package/src/common/ShuffleTransform.js +3 -3
- package/src/common/{arrowFunctionsMap.js → arrowsMapFn.js} +3 -3
- package/src/common/serialize.js +1 -10
- package/src/misc/explore.ori +7 -7
- package/src/builtins/@apply.js +0 -6
- package/src/builtins/@groupBy.js +0 -37
- package/src/builtins/@isAsyncTree.js +0 -17
- package/src/builtins/@mapDeep.js +0 -71
- package/src/builtins/@new.js +0 -6
- package/src/builtins/@parse/json.js +0 -9
- package/src/builtins/@parse/yaml.js +0 -10
- package/src/builtins/@sortBy.js +0 -37
- 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
|
|
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
|
-
* @
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
}
|
package/src/builtins/@invoke.js
CHANGED
|
@@ -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.
|
package/src/builtins/@json.js
CHANGED
|
@@ -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/
|
|
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";
|
package/src/builtins/@map.js
CHANGED
|
@@ -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/
|
|
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("
|
|
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
|
|
39
|
-
* @param {
|
|
13
|
+
* @param {Treelike} source
|
|
14
|
+
* @param {ValueKeyFn|TreeMapOptions} operation
|
|
40
15
|
*/
|
|
41
|
-
export default function
|
|
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
|
+
}
|
package/src/builtins/@mdHtml.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/builtins/@sitemap.js
CHANGED
|
@@ -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 =
|
|
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>\${
|
|
10
|
+
<loc>\${ url }</loc>
|
|
11
11
|
</url>
|
|
12
|
-
\`)
|
|
12
|
+
\`) }
|
|
13
13
|
</urlset>
|
|
14
14
|
\`
|
|
15
15
|
`;
|