@weborigami/origami 0.2.11 → 0.3.0
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/package.json +5 -4
- package/src/handlers/csv.handler.js +126 -0
- package/src/handlers/handlers.js +2 -0
- package/src/help/help.yaml +5 -2
- package/src/origami/csv.js +58 -0
- package/src/origami/origami.js +1 -0
- package/src/site/rss.js +2 -101
- package/src/tree/addNextPrevious.js +5 -46
- package/src/tree/filter.js +1 -1
- package/src/tree/map.js +2 -5
- package/src/tree/mask.js +19 -0
- package/src/tree/paginate.js +3 -39
- package/src/tree/tree.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/origami",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Web Origami language, CLI, framework, and server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"typescript": "5.8.2"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@weborigami/async-tree": "0.
|
|
21
|
-
"@weborigami/language": "0.
|
|
22
|
-
"@weborigami/
|
|
20
|
+
"@weborigami/async-tree": "0.3.0",
|
|
21
|
+
"@weborigami/language": "0.3.0",
|
|
22
|
+
"@weborigami/json-feed-to-rss": "1.0.0",
|
|
23
|
+
"@weborigami/types": "0.3.0",
|
|
23
24
|
"exif-parser": "0.1.12",
|
|
24
25
|
"graphviz-wasm": "3.0.2",
|
|
25
26
|
"highlight.js": "11.11.1",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { symbols, toString } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
mediaType: "text/csv",
|
|
5
|
+
|
|
6
|
+
unpack(packed, options = {}) {
|
|
7
|
+
const parent = options.parent ?? null;
|
|
8
|
+
const text = toString(packed);
|
|
9
|
+
const data = csvParse(text);
|
|
10
|
+
// Define `parent` as non-enumerable property
|
|
11
|
+
Object.defineProperty(data, symbols.parent, {
|
|
12
|
+
configurable: true,
|
|
13
|
+
enumerable: false,
|
|
14
|
+
value: parent,
|
|
15
|
+
writable: true,
|
|
16
|
+
});
|
|
17
|
+
return data;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse text as CSV following RFC 4180
|
|
23
|
+
*
|
|
24
|
+
* This assumes the presence of a header row, and accepts both CRLF and LF line
|
|
25
|
+
* endings.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} text
|
|
28
|
+
* @returns {any[]}
|
|
29
|
+
*/
|
|
30
|
+
function csvParse(text) {
|
|
31
|
+
const rows = [];
|
|
32
|
+
let currentRow = [];
|
|
33
|
+
let currentField = "";
|
|
34
|
+
|
|
35
|
+
const pushField = () => {
|
|
36
|
+
// Push the completed field and reset for the next field.
|
|
37
|
+
currentRow.push(currentField);
|
|
38
|
+
currentField = "";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const pushRow = () => {
|
|
42
|
+
// Push the row if there is at least one field (accounts for potential trailing newline)
|
|
43
|
+
rows.push(currentRow);
|
|
44
|
+
currentRow = [];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Main state machine
|
|
48
|
+
let i = 0;
|
|
49
|
+
let inQuotes = false;
|
|
50
|
+
while (i < text.length) {
|
|
51
|
+
const char = text[i];
|
|
52
|
+
|
|
53
|
+
if (inQuotes) {
|
|
54
|
+
// In a quoted field
|
|
55
|
+
if (char === '"') {
|
|
56
|
+
// Check if next character is also a quote
|
|
57
|
+
if (i + 1 < text.length && text[i + 1] === '"') {
|
|
58
|
+
// Append a literal double quote and skip the next character
|
|
59
|
+
currentField += '"';
|
|
60
|
+
i += 2;
|
|
61
|
+
continue;
|
|
62
|
+
} else {
|
|
63
|
+
// End of the quoted field
|
|
64
|
+
inQuotes = false;
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// All other characters within quotes are taken literally.
|
|
70
|
+
currentField += char;
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
} else if (char === '"') {
|
|
75
|
+
// Start of a quoted field
|
|
76
|
+
inQuotes = true;
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
} else if (char === ",") {
|
|
80
|
+
// End of field
|
|
81
|
+
pushField();
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
} else if (char === "\n" || (char === "\r" && text[i + 1] === "\n")) {
|
|
85
|
+
// End of row: push the last field, then row.
|
|
86
|
+
pushField();
|
|
87
|
+
pushRow();
|
|
88
|
+
if (char === "\r" && text[i + 1] === "\n") {
|
|
89
|
+
i++; // Handle CRLF line endings
|
|
90
|
+
}
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
} else {
|
|
94
|
+
// Regular character
|
|
95
|
+
currentField += char;
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle any remaining data after the loop.
|
|
102
|
+
// This will capture the last field/row if the text did not end with a newline.
|
|
103
|
+
if (inQuotes) {
|
|
104
|
+
// Mismatched quotes: you might choose to throw an error or handle it gracefully.
|
|
105
|
+
throw new Error("CSV parsing error: unmatched quote in the input.");
|
|
106
|
+
}
|
|
107
|
+
if (currentField !== "" || text.at(-1) === ",") {
|
|
108
|
+
pushField();
|
|
109
|
+
}
|
|
110
|
+
if (currentRow.length > 0) {
|
|
111
|
+
pushRow();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The first row is assumed to be the header.
|
|
115
|
+
if (rows.length === 0) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const header = rows.shift();
|
|
120
|
+
|
|
121
|
+
const data = rows.map((row) =>
|
|
122
|
+
Object.fromEntries(row.map((value, index) => [header[index], value]))
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return data;
|
|
126
|
+
}
|
package/src/handlers/handlers.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
yamlHandler,
|
|
7
7
|
} from "../internal.js";
|
|
8
8
|
import cssHandler from "./css.handler.js";
|
|
9
|
+
import csvHandler from "./csv.handler.js";
|
|
9
10
|
import htmHandler from "./htm.handler.js";
|
|
10
11
|
import htmlHandler from "./html.handler.js";
|
|
11
12
|
import jpegHandler from "./jpeg.handler.js";
|
|
@@ -19,6 +20,7 @@ import ymlHandler from "./yml.handler.js";
|
|
|
19
20
|
|
|
20
21
|
export default {
|
|
21
22
|
"css.handler": cssHandler,
|
|
23
|
+
"csv.handler": csvHandler,
|
|
22
24
|
"htm.handler": htmHandler,
|
|
23
25
|
"html.handler": htmlHandler,
|
|
24
26
|
"jpeg.handler": jpegHandler,
|
package/src/help/help.yaml
CHANGED
|
@@ -302,8 +302,8 @@ tree:
|
|
|
302
302
|
args: (tree)
|
|
303
303
|
description: The tree's [key, value] pairs
|
|
304
304
|
filter:
|
|
305
|
-
args: (source,
|
|
306
|
-
description: Filter the source tree
|
|
305
|
+
args: (source, options)
|
|
306
|
+
description: Filter the source tree
|
|
307
307
|
first:
|
|
308
308
|
args: (tree)
|
|
309
309
|
description: The first value in the tree
|
|
@@ -352,6 +352,9 @@ tree:
|
|
|
352
352
|
mapReduce:
|
|
353
353
|
args: (tree, valueFn, reduceFn)
|
|
354
354
|
description: Map values and reduce them
|
|
355
|
+
mask:
|
|
356
|
+
args: (source, mask)
|
|
357
|
+
description: Return the source tree with only the keys in the mask
|
|
355
358
|
match:
|
|
356
359
|
args: (pattern, fn, [keys])
|
|
357
360
|
description: Matches simple patterns or regular expressions
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isUnpackable, toPlainValue } from "@weborigami/async-tree";
|
|
2
|
+
import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render the object as text in CSV format.
|
|
6
|
+
*
|
|
7
|
+
* The object should a treelike object such as an array. The output will include
|
|
8
|
+
* a header row with field names taken from the first item in the tree/array.
|
|
9
|
+
*
|
|
10
|
+
* @this {import("@weborigami/types").AsyncTree|null}
|
|
11
|
+
* @param {any} [object]
|
|
12
|
+
*/
|
|
13
|
+
export default async function csv(object) {
|
|
14
|
+
assertTreeIsDefined(this, "origami:csv");
|
|
15
|
+
if (isUnpackable(object)) {
|
|
16
|
+
object = await object.unpack();
|
|
17
|
+
}
|
|
18
|
+
const value = await toPlainValue(object);
|
|
19
|
+
const text = formatCsv(value);
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatCsv(array) {
|
|
24
|
+
if (!array || array.length === 0) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper to quote field if necessary
|
|
29
|
+
const formatField = (value) => {
|
|
30
|
+
// Convert value to string.
|
|
31
|
+
let field = String(value);
|
|
32
|
+
|
|
33
|
+
// RFC 4180: Quote field if it contains a comma, a CR/LF, or a double quote
|
|
34
|
+
if (field.search(/("|,|\n|\r)/) !== -1) {
|
|
35
|
+
// Escape existing double quotes by replacing " with ""
|
|
36
|
+
field = field.replace(/"/g, '""');
|
|
37
|
+
// Surround the field with quotes
|
|
38
|
+
field = `"${field}"`;
|
|
39
|
+
}
|
|
40
|
+
return field;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Extract header fields from the first object.
|
|
44
|
+
const headerFields = Object.keys(array[0]);
|
|
45
|
+
|
|
46
|
+
// Generate the header row by formatting each header field.
|
|
47
|
+
const headerRow = headerFields.map(formatField).join(",");
|
|
48
|
+
|
|
49
|
+
// Map through each object and generate a CSV row.
|
|
50
|
+
const dataRows = array.map((row) => {
|
|
51
|
+
return headerFields
|
|
52
|
+
.map((field) => formatField(row[field] !== undefined ? row[field] : ""))
|
|
53
|
+
.join(",");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Concatenate header and data rows, joining and ending with CRLF.
|
|
57
|
+
return [headerRow, ...dataRows].join("\r\n") + "\r\n";
|
|
58
|
+
}
|
package/src/origami/origami.js
CHANGED
|
@@ -9,6 +9,7 @@ export { toFunction } from "../common/utilities.js";
|
|
|
9
9
|
export { default as help } from "../help/help.js"; // Alias
|
|
10
10
|
export { default as basename } from "./basename.js";
|
|
11
11
|
export { default as config } from "./config.js";
|
|
12
|
+
export { default as csv } from "./csv.js";
|
|
12
13
|
export { default as json } from "./json.js";
|
|
13
14
|
export { default as jsonParse } from "./jsonParse.js";
|
|
14
15
|
export { default as naturalOrder } from "./naturalOrder.js";
|
package/src/site/rss.js
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
1
|
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
import jsonFeedToRss from "@weborigami/json-feed-to-rss";
|
|
2
3
|
import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
|
|
3
4
|
|
|
4
|
-
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
5
|
-
const months = [
|
|
6
|
-
"Jan",
|
|
7
|
-
"Feb",
|
|
8
|
-
"Mar",
|
|
9
|
-
"Apr",
|
|
10
|
-
"May",
|
|
11
|
-
"Jun",
|
|
12
|
-
"Jul",
|
|
13
|
-
"Aug",
|
|
14
|
-
"Sep",
|
|
15
|
-
"Oct",
|
|
16
|
-
"Nov",
|
|
17
|
-
"Dec",
|
|
18
|
-
];
|
|
19
|
-
|
|
20
5
|
/**
|
|
21
6
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
22
7
|
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
@@ -27,89 +12,5 @@ const months = [
|
|
|
27
12
|
export default async function rss(jsonFeedTree, options = {}) {
|
|
28
13
|
assertTreeIsDefined(this, "site:rss");
|
|
29
14
|
const jsonFeed = await Tree.plain(jsonFeedTree);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
let { feed_url, language } = options;
|
|
33
|
-
if (!feed_url && jsonFeed.feed_url) {
|
|
34
|
-
// Presume that the RSS feed lives in same location as feed_url
|
|
35
|
-
// but with a .xml extension.
|
|
36
|
-
feed_url = jsonFeed.feed_url;
|
|
37
|
-
if (feed_url.endsWith(".json")) {
|
|
38
|
-
feed_url = feed_url.replace(".json", ".xml");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const itemsRss = items?.map((story) => itemRss(story)).join("") ?? [];
|
|
43
|
-
|
|
44
|
-
const titleElement = title ? ` <title>${escapeXml(title)}</title>\n` : "";
|
|
45
|
-
const descriptionElement = description
|
|
46
|
-
? ` <description>${escapeXml(description)}</description>\n`
|
|
47
|
-
: "";
|
|
48
|
-
const linkElement = home_page_url
|
|
49
|
-
? ` <link>${home_page_url}</link>\n`
|
|
50
|
-
: "";
|
|
51
|
-
const languageElement = language
|
|
52
|
-
? ` <language>${language}</language>\n`
|
|
53
|
-
: "";
|
|
54
|
-
const feedLinkElement = ` <atom:link href="${feed_url}" rel="self" type="application/rss+xml"/>\n`;
|
|
55
|
-
|
|
56
|
-
return `<?xml version="1.0" ?>
|
|
57
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
58
|
-
<channel>
|
|
59
|
-
${titleElement}${descriptionElement}${linkElement}${languageElement}${feedLinkElement}${itemsRss} </channel>
|
|
60
|
-
</rss>`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function itemRss(jsonFeedItem) {
|
|
64
|
-
const { content_html, id, summary, title, url } = jsonFeedItem;
|
|
65
|
-
let { date_published } = jsonFeedItem;
|
|
66
|
-
if (typeof date_published === "string") {
|
|
67
|
-
// Parse as ISO 8601 date.
|
|
68
|
-
date_published = new Date(date_published);
|
|
69
|
-
}
|
|
70
|
-
const date =
|
|
71
|
-
date_published instanceof Date
|
|
72
|
-
? toRFC822Date(date_published)
|
|
73
|
-
: date_published;
|
|
74
|
-
|
|
75
|
-
const dateElement = date ? ` <pubDate>${date}</pubDate>\n` : "";
|
|
76
|
-
const isPermaLink =
|
|
77
|
-
id !== undefined && !URL.canParse(id) ? ` isPermaLink="false"` : "";
|
|
78
|
-
const guidElement = id ? ` <guid${isPermaLink}>${id}</guid>\n` : "";
|
|
79
|
-
const descriptionElement = summary
|
|
80
|
-
? ` <description>${escapeXml(summary)}</description>\n`
|
|
81
|
-
: "";
|
|
82
|
-
const contentElement = content_html
|
|
83
|
-
? ` <content:encoded><![CDATA[${content_html}]]></content:encoded>\n`
|
|
84
|
-
: "";
|
|
85
|
-
const titleElement = title
|
|
86
|
-
? ` <title>${escapeXml(title)}</title>\n`
|
|
87
|
-
: "";
|
|
88
|
-
const linkElement = url ? ` <link>${url}</link>\n` : "";
|
|
89
|
-
|
|
90
|
-
return ` <item>
|
|
91
|
-
${dateElement}${titleElement}${linkElement}${guidElement}${descriptionElement}${contentElement} </item>
|
|
92
|
-
`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Escape XML entities for in the text.
|
|
96
|
-
function escapeXml(text) {
|
|
97
|
-
return text
|
|
98
|
-
.replace(/&/g, "&")
|
|
99
|
-
.replace(/</g, "<")
|
|
100
|
-
.replace(/>/g, ">")
|
|
101
|
-
.replace(/"/g, """)
|
|
102
|
-
.replace(/'/g, "'");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// RSS wants dates in RFC-822.
|
|
106
|
-
function toRFC822Date(date) {
|
|
107
|
-
const day = days[date.getUTCDay()];
|
|
108
|
-
const dayOfMonth = date.getUTCDate().toString().padStart(2, "0");
|
|
109
|
-
const month = months[date.getUTCMonth()];
|
|
110
|
-
const year = date.getUTCFullYear();
|
|
111
|
-
const hours = date.getUTCHours().toString().padStart(2, "0");
|
|
112
|
-
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
|
|
113
|
-
const seconds = date.getUTCSeconds().toString().padStart(2, "0");
|
|
114
|
-
return `${day}, ${dayOfMonth} ${month} ${year} ${hours}:${minutes}:${seconds} GMT`;
|
|
15
|
+
return jsonFeedToRss(jsonFeed, options);
|
|
115
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addNextPrevious, symbols } from "@weborigami/async-tree";
|
|
2
2
|
import getTreeArgument from "../common/getTreeArgument.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -9,55 +9,14 @@ import getTreeArgument from "../common/getTreeArgument.js";
|
|
|
9
9
|
* @this {AsyncTree|null}
|
|
10
10
|
* @param {import("@weborigami/async-tree").Treelike} treelike
|
|
11
11
|
*/
|
|
12
|
-
export default async function
|
|
12
|
+
export default async function addNextPreviousBuiltin(treelike) {
|
|
13
13
|
const tree = await getTreeArgument(
|
|
14
14
|
this,
|
|
15
15
|
arguments,
|
|
16
16
|
treelike,
|
|
17
17
|
"tree:addNextPrevious"
|
|
18
18
|
);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
value: async (key) => {
|
|
23
|
-
let value = await tree.get(key);
|
|
24
|
-
|
|
25
|
-
if (value === undefined) {
|
|
26
|
-
return undefined;
|
|
27
|
-
} else if (Tree.isTreelike(value)) {
|
|
28
|
-
value = await Tree.plain(value);
|
|
29
|
-
} else if (typeof value === "object") {
|
|
30
|
-
// Clone value to avoid modifying the original object.
|
|
31
|
-
value = { ...value };
|
|
32
|
-
} else if (typeof value === "string") {
|
|
33
|
-
// Upgrade text nodes to objects.
|
|
34
|
-
value = { "@text": value };
|
|
35
|
-
} else {
|
|
36
|
-
// Upgrade other scalar types to objects.
|
|
37
|
-
value = { "@data": value };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (keys === undefined) {
|
|
41
|
-
keys = Array.from(await tree.keys());
|
|
42
|
-
}
|
|
43
|
-
const index = keys.indexOf(key);
|
|
44
|
-
if (index === -1) {
|
|
45
|
-
// Key is supported but not published in `keys`
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Extend value with nextKey/previousKey properties.
|
|
50
|
-
const nextKey = keys[index + 1];
|
|
51
|
-
if (nextKey) {
|
|
52
|
-
value.nextKey = nextKey;
|
|
53
|
-
}
|
|
54
|
-
const previousKey = keys[index - 1];
|
|
55
|
-
if (previousKey) {
|
|
56
|
-
value.previousKey = previousKey;
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
},
|
|
60
|
-
writable: true,
|
|
61
|
-
},
|
|
62
|
-
});
|
|
19
|
+
const result = await addNextPrevious(tree);
|
|
20
|
+
result[symbols.parent] = this;
|
|
21
|
+
return result;
|
|
63
22
|
}
|
package/src/tree/filter.js
CHANGED
|
@@ -2,7 +2,7 @@ import { filter } from "@weborigami/async-tree";
|
|
|
2
2
|
import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Apply a filter to a tree
|
|
5
|
+
* Apply a filter to a tree
|
|
6
6
|
*
|
|
7
7
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
8
|
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
package/src/tree/map.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cachedKeyFunctions,
|
|
3
|
+
extensionKeyFunctions,
|
|
3
4
|
isPlainObject,
|
|
4
5
|
isUnpackable,
|
|
5
|
-
keyFunctionsForExtensions,
|
|
6
6
|
map as mapTransform,
|
|
7
7
|
} from "@weborigami/async-tree";
|
|
8
8
|
import getTreeArgument from "../common/getTreeArgument.js";
|
|
@@ -102,10 +102,7 @@ function extendedOptions(context, operation) {
|
|
|
102
102
|
if (extension) {
|
|
103
103
|
// Generate key/inverseKey functions from the extension
|
|
104
104
|
let { resultExtension, sourceExtension } = parseExtensions(extension);
|
|
105
|
-
const keyFns =
|
|
106
|
-
resultExtension,
|
|
107
|
-
sourceExtension,
|
|
108
|
-
});
|
|
105
|
+
const keyFns = extensionKeyFunctions(sourceExtension, resultExtension);
|
|
109
106
|
keyFn = keyFns.key;
|
|
110
107
|
inverseKeyFn = keyFns.inverseKey;
|
|
111
108
|
} else if (keyFn) {
|
package/src/tree/mask.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { mask } from "@weborigami/async-tree";
|
|
2
|
+
import assertTreeIsDefined from "../common/assertTreeIsDefined.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Apply a mask to a tree
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
9
|
+
*
|
|
10
|
+
* @this {AsyncTree|null}
|
|
11
|
+
* @param {Treelike} sourceTreelike
|
|
12
|
+
* @param {Treelike} maskTreelike
|
|
13
|
+
*/
|
|
14
|
+
export default async function maskBuiltin(sourceTreelike, maskTreelike) {
|
|
15
|
+
assertTreeIsDefined(this, "tree:mask");
|
|
16
|
+
const result = mask(sourceTreelike, maskTreelike);
|
|
17
|
+
result.parent = this;
|
|
18
|
+
return result;
|
|
19
|
+
}
|
package/src/tree/paginate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { paginate } from "@weborigami/async-tree";
|
|
1
2
|
import getTreeArgument from "../common/getTreeArgument.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -11,51 +12,14 @@ import getTreeArgument from "../common/getTreeArgument.js";
|
|
|
11
12
|
* @param {Treelike} [treelike]
|
|
12
13
|
* @param {number} [size=10]
|
|
13
14
|
*/
|
|
14
|
-
export default async function
|
|
15
|
+
export default async function paginateBuiltin(treelike, size = 10) {
|
|
15
16
|
const tree = await getTreeArgument(
|
|
16
17
|
this,
|
|
17
18
|
arguments,
|
|
18
19
|
treelike,
|
|
19
20
|
"tree:paginate"
|
|
20
21
|
);
|
|
21
|
-
|
|
22
|
-
const keys = Array.from(await tree.keys());
|
|
23
|
-
const pageCount = Math.ceil(keys.length / size);
|
|
24
|
-
|
|
25
|
-
const paginated = {
|
|
26
|
-
async get(pageKey) {
|
|
27
|
-
// Note: page numbers are 1-based.
|
|
28
|
-
const pageNumber = Number(pageKey);
|
|
29
|
-
if (Number.isNaN(pageNumber)) {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
const nextPage = pageNumber + 1 <= pageCount ? pageNumber + 1 : null;
|
|
33
|
-
const previousPage = pageNumber - 1 >= 1 ? pageNumber - 1 : null;
|
|
34
|
-
const items = {};
|
|
35
|
-
for (
|
|
36
|
-
let index = (pageNumber - 1) * size;
|
|
37
|
-
index < Math.min(keys.length, pageNumber * size);
|
|
38
|
-
index++
|
|
39
|
-
) {
|
|
40
|
-
const key = keys[index];
|
|
41
|
-
items[key] = await tree.get(keys[index]);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
items,
|
|
46
|
-
nextPage,
|
|
47
|
-
pageCount,
|
|
48
|
-
pageNumber,
|
|
49
|
-
previousPage,
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async keys() {
|
|
54
|
-
// Return an array from 1..totalPages
|
|
55
|
-
return Array.from({ length: pageCount }, (_, index) => index + 1);
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
22
|
+
const paginated = await paginate(tree, size);
|
|
59
23
|
paginated.parent = this;
|
|
60
24
|
return paginated;
|
|
61
25
|
}
|
package/src/tree/tree.js
CHANGED
|
@@ -21,6 +21,7 @@ export { default as inners } from "./inners.js";
|
|
|
21
21
|
export { default as keys } from "./keys.js";
|
|
22
22
|
export { default as length } from "./length.js";
|
|
23
23
|
export { default as map } from "./map.js";
|
|
24
|
+
export { default as mask } from "./mask.js";
|
|
24
25
|
export { default as match } from "./match.js";
|
|
25
26
|
export { default as merge } from "./merge.js";
|
|
26
27
|
export { default as paginate } from "./paginate.js";
|