@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.
- package/exports/buildExports.js +2 -2
- package/exports/exports.js +29 -13
- package/index.ts +0 -3
- package/package.json +10 -10
- package/src/builtins/@addNextPrevious.js +58 -0
- 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/@debug.js +17 -19
- 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/@document.js +1 -2
- 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/@inline.js +8 -2
- 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 +10 -170
- 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/@ori.js +1 -1
- package/src/builtins/@paginate.js +18 -0
- package/src/builtins/@paginateFn.js +61 -0
- package/src/builtins/@perf.js +1 -1
- package/src/builtins/@redirect.js +10 -1
- package/src/builtins/@regexParse.js +5 -0
- package/src/builtins/@regexParseFn.js +9 -0
- package/src/builtins/@rss.js +8 -4
- package/src/builtins/@sitemap.js +4 -4
- package/src/builtins/@slug.js +15 -0
- 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 +6 -7
- package/src/common/ExplorableSiteTransform.js +16 -10
- package/src/common/ShuffleTransform.js +3 -3
- package/src/common/{arrowFunctionsMap.js → arrowsMapFn.js} +3 -3
- package/src/common/documentObject.js +18 -9
- package/src/common/serialize.js +1 -10
- package/src/common/utilities.js +5 -2
- package/src/misc/OriCommandTransform.js +2 -7
- package/src/misc/explore.ori +7 -7
- package/src/server/constructResponse.js +3 -9
- package/src/server/server.js +5 -2
- 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 -22
- package/src/builtins/@new.js +0 -6
- package/src/builtins/@parse/json.js +0 -6
- package/src/builtins/@parse/yaml.js +0 -8
- package/src/builtins/@sortBy.js +0 -37
- 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";
|
package/src/builtins/@if.js
CHANGED
|
@@ -16,7 +16,8 @@ export default async function ifCommand(value, trueResult, falseResult) {
|
|
|
16
16
|
condition = keys.length > 0;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
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
|
|
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
|
-
* @
|
|
9
|
-
*
|
|
10
|
-
* @
|
|
11
|
-
*
|
|
12
|
-
* @param {any}
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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/@inline.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
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,179 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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/
|
|
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("
|
|
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
|
-
* @
|
|
12
|
+
* @this {AsyncTree|null}
|
|
13
|
+
* @param {Treelike} source
|
|
14
|
+
* @param {ValueKeyFn|TreeMapOptions} operation
|
|
162
15
|
*/
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
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";
|
package/src/builtins/@ori.js
CHANGED
|
@@ -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)
|
|
65
|
+
result.toString !== getRealmObjectPrototype(result)?.toString)
|
|
66
66
|
) {
|
|
67
67
|
text = result.toString();
|
|
68
68
|
} else if (typeof result === "object") {
|