@weborigami/origami 0.0.69 → 0.0.71-beta.1

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.
@@ -7,6 +7,7 @@ export { default as cache } from "../src/builtins/@cache.js";
7
7
  export { default as calendarTree } from "../src/builtins/@calendarTree.js";
8
8
  export { default as changes } from "../src/builtins/@changes.js";
9
9
  export { default as clean } from "../src/builtins/@clean.js";
10
+ export { default as code } from "../src/builtins/@code.js";
10
11
  export { default as concat } from "../src/builtins/@concat.js";
11
12
  export { default as config } from "../src/builtins/@config.js";
12
13
  export { default as copy } from "../src/builtins/@copy.js";
@@ -40,6 +41,7 @@ export { default as imageFormat } from "../src/builtins/@image/format.js";
40
41
  export { default as imageFormatFn } from "../src/builtins/@image/formatFn.js";
41
42
  export { default as imageResize } from "../src/builtins/@image/resize.js";
42
43
  export { default as imageResizeFn } from "../src/builtins/@image/resizeFn.js";
44
+ export { default as indent } from "../src/builtins/@indent.js";
43
45
  export { default as index } from "../src/builtins/@index.js";
44
46
  export { default as inherited } from "../src/builtins/@inherited.js";
45
47
  export { default as inline } from "../src/builtins/@inline.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/origami",
3
- "version": "0.0.69",
3
+ "version": "0.0.71-beta.1",
4
4
  "description": "Web Origami language, CLI, framework, and server",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,9 +17,9 @@
17
17
  "typescript": "5.6.2"
18
18
  },
19
19
  "dependencies": {
20
- "@weborigami/async-tree": "0.0.69",
21
- "@weborigami/language": "0.0.69",
22
- "@weborigami/types": "0.0.69",
20
+ "@weborigami/async-tree": "0.0.71-beta.1",
21
+ "@weborigami/language": "0.0.71-beta.1",
22
+ "@weborigami/types": "0.0.71-beta.1",
23
23
  "exif-parser": "0.1.12",
24
24
  "graphviz-wasm": "3.0.2",
25
25
  "highlight.js": "11.10.0",
@@ -0,0 +1,37 @@
1
+ import { symbols } from "@weborigami/language";
2
+ import getTreeArgument from "../misc/getTreeArgument.js";
3
+
4
+ /**
5
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
6
+ *
7
+ * @this {AsyncTree|null}
8
+ * @param {any} value
9
+ */
10
+ export default async function code(value) {
11
+ if (value === undefined) {
12
+ value = await getTreeArgument(this, arguments, value, "@clean");
13
+ }
14
+ if (value === undefined) {
15
+ return undefined;
16
+ }
17
+ const code = value.code ?? value[symbols.codeSymbol];
18
+ return code ? functionNames(code) : undefined;
19
+ }
20
+
21
+ function functionNames(code) {
22
+ if (!Array.isArray(code)) {
23
+ return code;
24
+ }
25
+ let [head, ...tail] = code;
26
+ if (typeof head === "function") {
27
+ const text = head.toString();
28
+ if (text.startsWith("«ops.")) {
29
+ head = text;
30
+ } else {
31
+ head = head.name;
32
+ }
33
+ } else {
34
+ head = functionNames(head);
35
+ }
36
+ return [head, ...tail.map(functionNames)];
37
+ }
@@ -0,0 +1,115 @@
1
+ const lastLineWhitespaceRegex = /\n(?<indent>[ \t]*)$/;
2
+
3
+ const mapStringsToModifications = new Map();
4
+
5
+ /**
6
+ * Normalize indentation in a tagged template string.
7
+ *
8
+ * @param {TemplateStringsArray} strings
9
+ * @param {...any} values
10
+ * @returns {string}
11
+ */
12
+ export default function indent(strings, ...values) {
13
+ let modified = mapStringsToModifications.get(strings);
14
+ if (!modified) {
15
+ modified = modifyStrings(strings);
16
+ mapStringsToModifications.set(strings, modified);
17
+ }
18
+ const { blockIndentations, strings: modifiedStrings } = modified;
19
+ return joinBlocks(modifiedStrings, values, blockIndentations);
20
+ }
21
+
22
+ // Join strings and values, applying the given block indentation to the lines of
23
+ // values for block placholders.
24
+ function joinBlocks(strings, values, blockIndentations) {
25
+ let result = strings[0];
26
+ for (let i = 0; i < values.length; i++) {
27
+ let text = values[i];
28
+ if (text) {
29
+ const blockIndentation = blockIndentations[i];
30
+ if (blockIndentation) {
31
+ const lines = text.split("\n");
32
+ text = "";
33
+ if (lines.at(-1) === "") {
34
+ // Drop empty last line
35
+ lines.pop();
36
+ }
37
+ for (let line of lines) {
38
+ text += blockIndentation + line + "\n";
39
+ }
40
+ }
41
+ result += text;
42
+ }
43
+ result += strings[i + 1];
44
+ }
45
+ return result;
46
+ }
47
+
48
+ // Given an array of template boilerplate strings, return an object { modified,
49
+ // blockIndentations } where `strings` is the array of strings with indentation
50
+ // removed, and `blockIndentations` is an array of indentation strings for each
51
+ // block placeholder.
52
+ function modifyStrings(strings) {
53
+ // Phase one: Identify the indentation based on the first real line of the
54
+ // first string (skipping the initial newline), and remove this indentation
55
+ // from all lines of all strings.
56
+ let indent;
57
+ if (strings.length > 0 && strings[0].startsWith("\n")) {
58
+ // Look for indenttation
59
+ const firstLineWhitespaceRegex = /^\n(?<indent>[ \t]*)/;
60
+ const match = strings[0].match(firstLineWhitespaceRegex);
61
+ indent = match?.groups.indent;
62
+ }
63
+
64
+ // Determine the modified strings. If this invoked as a JS tagged template
65
+ // literal, the `strings` argument will be an odd array-ish object that we'll
66
+ // want to convert to a real array.
67
+ let modified;
68
+ if (indent) {
69
+ // De-indent the strings.
70
+ const indentationRegex = new RegExp(`\n${indent}`, "g");
71
+ // The `replaceAll` also converts strings to a real array.
72
+ modified = strings.map((string) =>
73
+ string.replaceAll(indentationRegex, "\n")
74
+ );
75
+ // Remove indentation from last line of last string
76
+ modified[modified.length - 1] = modified
77
+ .at(-1)
78
+ .replace(lastLineWhitespaceRegex, "\n");
79
+ } else {
80
+ // No indentation; just copy the strings so we have a real array
81
+ modified = strings.slice();
82
+ }
83
+
84
+ // Phase two: Identify any block placholders, identify and remove their
85
+ // preceding indentation, and remove the following newline. Work backward from
86
+ // the end towards the start because we're modifying the strings in place and
87
+ // our pattern matching won't work going forward from start to end.
88
+ let blockIndentations = [];
89
+ for (let i = modified.length - 2; i >= 0; i--) {
90
+ // Get the modified before and after substitution with index `i`
91
+ const beforeString = modified[i];
92
+ const afterString = modified[i + 1];
93
+ const match = beforeString.match(lastLineWhitespaceRegex);
94
+ if (match && afterString.startsWith("\n")) {
95
+ // The substitution between these strings is a block substitution
96
+ let blockIndentation = match.groups.indent;
97
+ blockIndentations[i] = blockIndentation;
98
+ // Trim the before and after strings
99
+ if (blockIndentation) {
100
+ modified[i] = beforeString.slice(0, -blockIndentation.length);
101
+ }
102
+ modified[i + 1] = afterString.slice(1);
103
+ }
104
+ }
105
+
106
+ // Remove newline from start of first string *after* removing indentation.
107
+ if (modified[0].startsWith("\n")) {
108
+ modified[0] = modified[0].slice(1);
109
+ }
110
+
111
+ return {
112
+ blockIndentations,
113
+ strings: modified,
114
+ };
115
+ }
@@ -22,42 +22,40 @@ export default async function index(treelike) {
22
22
  for (const key of filtered) {
23
23
  const keyText = String(key);
24
24
  // Simple key.
25
- const link = `<li><a href="${keyText}">${keyText}</a></li>`;
25
+ const link = ` <li><a href="${keyText}">${keyText}</a></li>`;
26
26
  links.push(link);
27
27
  }
28
28
 
29
29
  const heading = tree[keySymbol] ?? "Index";
30
- const list = `
31
- <h1>${heading.trim()}</h1>
32
- <ul>\n${links.join("\n").trim()}\n</ul>
33
- `;
30
+ const list = ` <ul>\n${links.join("\n")}\n </ul>`;
34
31
 
35
- const html = `
36
- <!DOCTYPE html>
37
- <html lang="en">
38
- <head>
39
- <meta charset="utf-8" />
40
- <meta name="viewport" content="width=device-width,initial-scale=1" />
41
- <style>
42
- li {
43
- margin-bottom: 0.20em;
44
- }
32
+ const html = `<!DOCTYPE html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="utf-8">
36
+ <meta name="viewport" content="width=device-width,initial-scale=1">
37
+ <style>
38
+ li {
39
+ margin-bottom: 0.20em;
40
+ }
45
41
 
46
- a {
47
- text-decoration: none;
48
- }
49
- a:hover {
50
- text-decoration: revert;
51
- }
52
- </style>
53
- </head>
54
- <body>
55
- ${list.trim()}
56
- </body>
57
- </html>`;
42
+ a {
43
+ text-decoration: none;
44
+ }
45
+ a:hover {
46
+ text-decoration: revert;
47
+ }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <h1>${heading.trim()}</h1>
52
+ ${list}
53
+ </body>
54
+ </html>
55
+ `;
58
56
 
59
57
  /** @type {any} */
60
- const result = new String(html.trim());
58
+ const result = new String(html);
61
59
  result.unpack = () => tree;
62
60
  return result;
63
61
  }
@@ -1,4 +1,4 @@
1
- import { ObjectTree, Tree } from "@weborigami/async-tree";
1
+ import { Tree } from "@weborigami/async-tree";
2
2
  import assertTreeIsDefined from "../misc/assertTreeIsDefined.js";
3
3
 
4
4
  /**
@@ -12,8 +12,7 @@ import assertTreeIsDefined from "../misc/assertTreeIsDefined.js";
12
12
  * The pattern can also be a JavaScript regular expression.
13
13
  *
14
14
  * If a key is requested, match against the given pattern and, if matches,
15
- * incorporate the matched pattern's wildcard values into the scope and invoke
16
- * the indicated function to produce a result.
15
+ * invokes the given function with an object containing the matched values.
17
16
  *
18
17
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
19
18
  * @typedef {import("@weborigami/async-tree").Treelike} Treelike
@@ -57,23 +56,13 @@ export default function match(pattern, resultFn, keys = []) {
57
56
  return resultFn;
58
57
  }
59
58
 
60
- // If the pattern contained named wildcards, extend the scope. It appears
61
- // that the `groups` property of a match is *not* a real plain object, so
62
- // we have to make one.
63
- let target;
64
- if (keyMatch.groups) {
65
- target = new ObjectTree(
66
- Object.fromEntries(Object.entries(keyMatch.groups))
67
- );
68
- target.parent = tree;
69
- } else {
70
- target = tree;
71
- }
59
+ // Copy the `groups` property to a real object
60
+ const matches = { ...keyMatch.groups };
72
61
 
73
62
  // Invoke the result function with the extended scope.
74
63
  let value;
75
64
  if (typeof resultFn === "function") {
76
- value = await resultFn.call(target);
65
+ value = await resultFn.call(this, matches);
77
66
  } else {
78
67
  value = Object.create(resultFn);
79
68
  }
@@ -24,7 +24,11 @@ export default function processUnpackedContent(content, parent, attachedData) {
24
24
  } else {
25
25
  target = base;
26
26
  }
27
- return content.bind(target);
27
+ const result = content.bind(target);
28
+ if (content.code) {
29
+ result.code = content.code;
30
+ }
31
+ return result;
28
32
  } else if (Tree.isAsyncTree(content) && !content.parent) {
29
33
  const result = Object.create(content);
30
34
  result.parent = parent;
@@ -3,6 +3,7 @@ import {
3
3
  toString as asyncTreeToString,
4
4
  isPlainObject,
5
5
  isUnpackable,
6
+ trailingSlash,
6
7
  } from "@weborigami/async-tree";
7
8
 
8
9
  // Return true if the text appears to contain non-printable binary characters;
@@ -37,6 +38,8 @@ export const keySymbol = Symbol("key");
37
38
  * period), replace that extension with the result extension (which again should
38
39
  * generally include a period). Otherwise, return the key as is.
39
40
  *
41
+ * If the key ends in a trailing slash, that will be preserved in the result.
42
+ *
40
43
  * @param {string} key
41
44
  * @param {string} sourceExtension
42
45
  * @param {string} resultExtension
@@ -44,11 +47,16 @@ export const keySymbol = Symbol("key");
44
47
  export function replaceExtension(key, sourceExtension, resultExtension) {
45
48
  if (!key) {
46
49
  return undefined;
47
- } else if (!key.endsWith(sourceExtension)) {
48
- return key;
49
- } else {
50
- return key.slice(0, -sourceExtension.length) + resultExtension;
51
50
  }
51
+
52
+ const normalizedKey = trailingSlash.remove(key);
53
+ if (!normalizedKey.endsWith(sourceExtension)) {
54
+ return normalizedKey;
55
+ }
56
+
57
+ const replaced =
58
+ normalizedKey.slice(0, -sourceExtension.length) + resultExtension;
59
+ return trailingSlash.toggle(replaced, trailingSlash.has(key));
52
60
  }
53
61
 
54
62
  /**
@@ -64,13 +72,16 @@ export function toFunction(obj) {
64
72
  return obj;
65
73
  } else if (isUnpackable(obj)) {
66
74
  // Extract the contents of the object and convert that to a function.
67
- let fn;
75
+ let fnPromise;
68
76
  /** @this {any} */
69
77
  return async function (...args) {
70
- if (!fn) {
71
- const content = await obj.unpack();
72
- fn = toFunction(content);
78
+ if (!fnPromise) {
79
+ // unpack() may return a function or a promise for a function; normalize
80
+ // to a promise for a function
81
+ const unpackPromise = Promise.resolve(obj.unpack());
82
+ fnPromise = unpackPromise.then((content) => toFunction(content));
73
83
  }
84
+ const fn = await fnPromise;
74
85
  return fn.call(this, ...args);
75
86
  };
76
87
  } else if (Tree.isTreelike(obj)) {
@@ -27,9 +27,10 @@ export default async function* crawlResources(tree, baseUrl) {
27
27
 
28
28
  let errorPaths = [];
29
29
 
30
- // Seed the promise dictionary with robots.txt at the root and an empty path
31
- // indicating the current directory (relative to the baseUrl).
32
- const initialPaths = ["/robots.txt", ""];
30
+ // Seed the promise dictionary with robots.txt at the root, a sitemap.xml at
31
+ // the root, and an empty path indicating the current directory (relative to
32
+ // the baseUrl).
33
+ const initialPaths = ["/robots.txt", "/sitemap.xml", ""];
33
34
  initialPaths.forEach((path) => {
34
35
  promisesForPaths[path] = processPath(tree, path, baseUrl);
35
36
  });