@weborigami/origami 0.6.17 → 0.7.0-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.
@@ -1,5 +1,5 @@
1
1
  import { trailingSlash, Tree } from "@weborigami/async-tree";
2
- import { evaluate, projectGlobals } from "@weborigami/language";
2
+ import { evaluate, getGlobalsForTree } from "@weborigami/language";
3
3
  import debugTransform from "./debugTransform.js";
4
4
 
5
5
  const mapParentToResult = new WeakMap();
@@ -16,7 +16,7 @@ const mapParentToResult = new WeakMap();
16
16
  * counter to force reevaluation of the same expression.
17
17
  */
18
18
  export default async function oriEval(parent) {
19
- const globals = await projectGlobals(parent);
19
+ const globals = getGlobalsForTree(parent);
20
20
  return async (key) => {
21
21
  const normalizedKey = trailingSlash.remove(key);
22
22
  let result = mapParentToResult.get(parent);
package/src/dev/dev.js CHANGED
@@ -6,7 +6,6 @@ export { default as indexPage } from "../origami/indexPage.js";
6
6
  export { default as yaml } from "../origami/yaml.js";
7
7
  export { default as breakpoint } from "./breakpoint.js";
8
8
  export { default as changes } from "./changes.js";
9
- export { default as changes2 } from "./changes2.js";
10
9
  export { default as code } from "./code.js";
11
10
  export { default as copy } from "./copy.js";
12
11
  export { default as audit } from "./crawler/audit.js";
@@ -19,5 +18,6 @@ export { default as log } from "./log.js";
19
18
  export { default as serve } from "./serve.js";
20
19
  export { default as stdin } from "./stdin.js";
21
20
  export { default as svg } from "./svg.js";
21
+ export { default as syscache } from "./syscache.js";
22
22
  export { default as version } from "./version.js";
23
23
  export { default as watch } from "./watch.js";
@@ -44,6 +44,7 @@ async function getScopeData(scope) {
44
44
  async function loadTemplate() {
45
45
  const folderPath = path.resolve(fileURLToPath(import.meta.url), "..");
46
46
  const folder = new OrigamiFileMap(folderPath);
47
+ await folder.initializeGlobals();
47
48
  const templateFile = await folder.get("explore.ori");
48
49
  const template = await Handlers.ori_handler.unpack(templateFile, {
49
50
  parent: folder,
package/src/dev/help.yaml CHANGED
@@ -13,12 +13,12 @@ Dev:
13
13
  clear:
14
14
  args: (tree)
15
15
  description: Remove all values from the tree (alias of Tree.clear)
16
- crawl:
17
- args: (tree, base)
18
- description: A tree of a site's discoverable resources
19
16
  copy:
20
17
  args: (source, target)
21
18
  description: Copy the source tree to the target
19
+ crawl:
20
+ args: (tree, base)
21
+ description: A tree of a site's discoverable resources
22
22
  debug:
23
23
  args: (tree)
24
24
  description: Add debug features to the tree
@@ -63,6 +63,9 @@ Origami:
63
63
  document:
64
64
  args: (text, [data])
65
65
  description: Create a document object with the text and data
66
+ domObject:
67
+ args: (dom)
68
+ description: Convert a DOM tree to a plain JavaScript object
66
69
  extension:
67
70
  description: Helpers for working with file extensions
68
71
  fetch:
@@ -71,12 +74,12 @@ Origami:
71
74
  hash:
72
75
  args: (data)
73
76
  description: A hex string hash of the data
77
+ htmlDom:
78
+ args: (html)
79
+ description: Parse HTML into a DOM tree
74
80
  htmlEscape:
75
81
  args: (text)
76
82
  description: Escape HTML entities in the text
77
- htmlParse:
78
- args: (html)
79
- description: Parse HTML into a plain JavaScript object
80
83
  image:
81
84
  description: Collection of functions for working with images
82
85
  indexPage:
@@ -102,9 +105,9 @@ Origami:
102
105
  description: The outline structure of the markdown document
103
106
  naturalOrder:
104
107
  description: A comparison function for natural sort order
105
- once:
106
- args: (fn)
107
- description: Run the function only once, return the same result
108
+ # once:
109
+ # args: (fn)
110
+ # description: Run the function only once, return the same result
108
111
  ori:
109
112
  args: (text)
110
113
  description: Evaluate the text as an Origami expression
@@ -148,12 +151,15 @@ Origami:
148
151
  toFunction:
149
152
  args: (obj)
150
153
  description: Coerce a tree or packed function definition to a function
154
+ tsv:
155
+ args: (tree)
156
+ description: Render the tree as a TSV file
151
157
  unpack:
152
158
  args: (buffer)
153
159
  description: Unpack the buffer into a usable form
154
- xmlParse:
160
+ xmlDom:
155
161
  args: (xml)
156
- description: Parse XML into a plain JavaScript object
162
+ description: Parse XML into a DOM tree
157
163
  yaml:
158
164
  args: (obj)
159
165
  description: Render the object in YAML format
@@ -0,0 +1,56 @@
1
+ import { activeProjectRoot, systemCache, volatile } from "@weborigami/language";
2
+ import path from "node:path";
3
+ import * as YAMLModule from "yaml";
4
+
5
+ // The "yaml" package doesn't seem to provide a default export that the browser can
6
+ // recognize, so we have to handle two ways to accommodate Node and the browser.
7
+ // @ts-ignore
8
+ const YAML = YAMLModule.default ?? YAMLModule.YAML;
9
+
10
+ export default function syscache() {
11
+ const projectRoot = activeProjectRoot.get();
12
+
13
+ /** @type {any} */
14
+ const entries = [...systemCache.entries()].map(([path, entry]) => {
15
+ const result = {};
16
+ if (entry.downstreams) {
17
+ result.downstreams = preferRelativePaths(projectRoot, entry.downstreams);
18
+ }
19
+ if (entry.upstreams) {
20
+ result.upstreams = preferRelativePaths(projectRoot, entry.upstreams);
21
+ }
22
+ return [preferRelativePath(projectRoot, path), result];
23
+ });
24
+
25
+ // Sort the entries by key
26
+ entries.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
27
+
28
+ const result = volatile(new Map(entries));
29
+
30
+ // When served, render as YAML and preserve trailing slashes
31
+ Object.defineProperty(result, "pack", {
32
+ configurable: true,
33
+ enumerable: false,
34
+ get() {
35
+ return () => volatile(YAML.stringify(result));
36
+ },
37
+ });
38
+
39
+ return result;
40
+ }
41
+
42
+ function preferRelativePath(projectRoot, inputPath) {
43
+ if (!path.isAbsolute(inputPath)) {
44
+ return inputPath;
45
+ }
46
+ const prefix = `${projectRoot.path}${path.sep}`;
47
+ if (!inputPath.startsWith(prefix)) {
48
+ return inputPath;
49
+ }
50
+ const relativePath = inputPath.slice(prefix.length);
51
+ return relativePath;
52
+ }
53
+
54
+ function preferRelativePaths(projectRoot, paths) {
55
+ return Array.from(paths).map((path) => preferRelativePath(projectRoot, path));
56
+ }
@@ -1,5 +1,5 @@
1
1
  import { symbols, toString } from "@weborigami/async-tree";
2
- import xmlParse from "../origami/xmlParse.js";
2
+ import xmlParse from "../origami/xmlDom.js";
3
3
 
4
4
  export default {
5
5
  mediaType: "application/xml",
@@ -1,17 +1,29 @@
1
+ import { isUnpackable } from "@weborigami/async-tree";
2
+
1
3
  const ELEMENT_NODE = 1;
2
4
  const TEXT_NODE = 3;
3
5
  const CDATA_SECTION_NODE = 4;
4
6
  const DOCUMENT_NODE = 9;
5
7
  const DOCUMENT_FRAGMENT_NODE = 11;
6
8
 
7
- export default function domNodeToObject(node) {
9
+ /**
10
+ * Given a DOM node, return a plain object representation of it.
11
+ */
12
+ export default async function domObject(input) {
13
+ if (isUnpackable(input)) {
14
+ input = await input.unpack();
15
+ }
16
+ return nodeObject(input);
17
+ }
18
+
19
+ function nodeObject(node) {
8
20
  switch (node.nodeType) {
9
21
  case DOCUMENT_NODE:
10
22
  return {
11
23
  name: "#document",
12
24
  children: [...node.childNodes]
13
25
  .filter((child) => !isWhitespaceOnly(child))
14
- .map(domNodeToObject),
26
+ .map(nodeObject),
15
27
  };
16
28
 
17
29
  case DOCUMENT_FRAGMENT_NODE:
@@ -19,7 +31,7 @@ export default function domNodeToObject(node) {
19
31
  name: "#document-fragment",
20
32
  children: [...node.childNodes]
21
33
  .filter((child) => !isWhitespaceOnly(child))
22
- .map(domNodeToObject),
34
+ .map(nodeObject),
23
35
  };
24
36
 
25
37
  case ELEMENT_NODE: {
@@ -55,7 +67,7 @@ export default function domNodeToObject(node) {
55
67
  result.text = text;
56
68
  }
57
69
  } else if (relevantChildren.length > 0) {
58
- result.children = relevantChildren.map(domNodeToObject);
70
+ result.children = relevantChildren.map(nodeObject);
59
71
  }
60
72
 
61
73
  return result;
@@ -1,22 +1 @@
1
- import { args } from "@weborigami/async-tree";
2
- import { handleExtension } from "@weborigami/language";
3
-
4
- /**
5
- * Extend the JavaScript `fetch` function to implicity return an ArrayBuffer
6
- * with an unpack() method if the resource has a known file extension.
7
- */
8
- export default async function fetchBuiltin(resource, options, state) {
9
- resource = args.string(resource, "Origami.fetch");
10
- const response = await fetch(resource, options);
11
- if (!response.ok) {
12
- return undefined;
13
- }
14
-
15
- const value = await response.arrayBuffer();
16
-
17
- const url = new URL(resource);
18
- const key = url.pathname;
19
-
20
- return handleExtension(value, key, state?.parent);
21
- }
22
- fetchBuiltin.needsState = true;
1
+ export { fetchAndHandleExtension as default } from "@weborigami/language";
@@ -0,0 +1,21 @@
1
+ import { args } from "@weborigami/async-tree";
2
+ import loadJsDom from "../common/loadJsDom.js";
3
+
4
+ /**
5
+ * Return the DOM structure for the given HTML.
6
+ *
7
+ * @param {import("@weborigami/async-tree").Stringlike} html
8
+ */
9
+ export default async function htmlDom(html) {
10
+ html = args.stringlike(html, "Origami.htmlDom");
11
+ const { JSDOM } = await loadJsDom();
12
+ let dom = JSDOM.fragment(html);
13
+ if (
14
+ (dom.nodeType === 9 || dom.nodeType === 11) &&
15
+ dom.children.length === 1
16
+ ) {
17
+ // Document or DocumentFragment with a single child: return the child
18
+ dom = dom.children[0];
19
+ }
20
+ return dom;
21
+ }
@@ -1,22 +1,15 @@
1
1
  import { args } from "@weborigami/async-tree";
2
- import loadJsDom from "../common/loadJsDom.js";
3
- import domNodeToObject from "./domNodeToObject.js";
2
+ import htmlDom from "./htmlDom.js";
4
3
 
5
4
  /**
6
- * Return the DOM structure for the given HTML as a plain object.
5
+ * Return the DOM structure for the given HTML.
7
6
  *
8
7
  * @param {import("@weborigami/async-tree").Stringlike} html
9
8
  */
10
9
  export default async function htmlParse(html) {
10
+ console.warn("Origami.htmlParse is deprecated. Use Origami.htmlDom instead.");
11
+
11
12
  html = args.stringlike(html, "Origami.htmlParse");
12
- const { JSDOM } = await loadJsDom();
13
- const dom = JSDOM.fragment(html);
14
- let object = domNodeToObject(dom);
15
- if (
16
- (object.name === "#document" || object.name === "#document-fragment") &&
17
- object.children.length === 1
18
- ) {
19
- object = object.children[0];
20
- }
21
- return object;
13
+ const dom = await htmlDom(html);
14
+ return dom;
22
15
  }
@@ -1,4 +1,4 @@
1
- import { args } from "@weborigami/async-tree";
1
+ import { args, interop } from "@weborigami/async-tree";
2
2
 
3
3
  const fnPromiseMap = new Map();
4
4
  const codePromiseMap = new Map();
@@ -9,6 +9,9 @@ const codePromiseMap = new Map();
9
9
  * @param {Function} fn
10
10
  */
11
11
  export default async function once(fn) {
12
+ interop.warn(
13
+ `Now that Origami caches most results, Origami.once is no longer needed and will be removed in a future release. If you believe you have a need for it, please mention your use case in the Origami chat.`,
14
+ );
12
15
  fn = args.fn(fn, "Origami.once");
13
16
  const code = /** @type {any} */ (fn).code;
14
17
  if (code) {
@@ -1,5 +1,10 @@
1
- import { Tree, args, getRealmObjectPrototype } from "@weborigami/async-tree";
2
- import { compile, projectGlobals } from "@weborigami/language";
1
+ import {
2
+ args,
3
+ assignPropertyDescriptors,
4
+ getRealmObjectPrototype,
5
+ Tree,
6
+ } from "@weborigami/async-tree";
7
+ import { compile, getGlobalsForTree } from "@weborigami/language";
3
8
  import { toYaml } from "../common/serialize.js";
4
9
  import * as dev from "../dev/dev.js";
5
10
 
@@ -17,18 +22,12 @@ export default async function ori(expression, options = {}) {
17
22
 
18
23
  expression = args.stringlike(expression, "Origami.ori");
19
24
 
20
- // Add Dev builtins as top-level globals
21
- const globals = {
22
- ...(await projectGlobals(parent)),
23
- ...dev,
24
- };
25
+ // Add Dev builtins as top-level globals; avoid invoking getters
26
+ const globals = assignPropertyDescriptors({}, getGlobalsForTree(parent), dev);
25
27
 
26
- // Compile the expression. Avoid caching scope references so that, e.g.,
27
- // passing a function to the `watch` builtin will always look the current
28
- // value of things in scope.
28
+ // Compile the expression
29
29
  const fn = compile.expression(expression, {
30
30
  globals,
31
- enableCaching: false,
32
31
  mode: "shell",
33
32
  parent,
34
33
  });
@@ -3,8 +3,10 @@ export { default as help } from "../dev/help.js"; // Alias
3
3
  export { default as basename } from "./basename.js";
4
4
  export { default as csv } from "./csv.js";
5
5
  export { default as document } from "./document.js";
6
+ export { default as domObject } from "./domObject.js";
6
7
  export { default as fetch } from "./fetch.js";
7
8
  export { default as hash } from "./hash.js";
9
+ export { default as htmlDom } from "./htmlDom.js";
8
10
  export { default as htmlEscape } from "./htmlEscape.js";
9
11
  export { default as htmlParse } from "./htmlParse.js";
10
12
  export { default as format } from "./image/format.js";
@@ -22,7 +24,6 @@ export { default as once } from "./once.js";
22
24
  export { default as ori } from "./ori.js";
23
25
  export { default as pack } from "./pack.js";
24
26
  export { default as post } from "./post.js";
25
- export { default as project } from "./project.js";
26
27
  export { default as projectRoot } from "./projectRoot.js";
27
28
  export { default as randomFrom } from "./randomFrom.js";
28
29
  export { default as randomsFrom } from "./randomsFrom.js";
@@ -37,6 +38,8 @@ export { default as static } from "./static.js";
37
38
  export { default as string } from "./string.js";
38
39
  export { default as tsv } from "./tsv.js";
39
40
  export { default as unpack } from "./unpack.js";
41
+ export { default as volatile } from "./volatile.js";
42
+ export { default as xmlDom } from "./xmlDom.js";
40
43
  export { default as xmlParse } from "./xmlParse.js";
41
44
  export { default as yaml } from "./yaml.js";
42
45
  export { default as yamlParse } from "./yamlParse.js";
@@ -0,0 +1 @@
1
+ export { volatile as default } from "@weborigami/language";
@@ -0,0 +1,32 @@
1
+ import { args } from "@weborigami/async-tree";
2
+ import loadJsDom from "../common/loadJsDom.js";
3
+
4
+ let parser;
5
+
6
+ /**
7
+ * Return the DOM for the given XML.
8
+ *
9
+ * @param {import("@weborigami/async-tree").Stringlike} xml
10
+ */
11
+ export default async function xmlDom(xml) {
12
+ xml = args.stringlike(xml, "Origami.xmlDom");
13
+ const parser = await getParser();
14
+ let dom = parser.parseFromString(xml, "application/xml");
15
+ if (
16
+ (dom.nodeType === 9 || dom.nodeType === 11) &&
17
+ dom.children.length === 1
18
+ ) {
19
+ // Document or DocumentFragment with a single child: return the child
20
+ dom = dom.children[0];
21
+ }
22
+ return dom;
23
+ }
24
+
25
+ async function getParser() {
26
+ if (!parser) {
27
+ const { JSDOM } = await loadJsDom();
28
+ const dom = new JSDOM();
29
+ parser = new dom.window.DOMParser();
30
+ }
31
+ return parser;
32
+ }
@@ -1,33 +1,15 @@
1
1
  import { args } from "@weborigami/async-tree";
2
- import loadJsDom from "../common/loadJsDom.js";
3
- import domNodeToObject from "./domNodeToObject.js";
4
-
5
- let parser;
2
+ import xmlDom from "./xmlDom.js";
6
3
 
7
4
  /**
8
- * Return the DOM for the given XML as a plain object.
5
+ * Return the DOM structure for the given XML.
9
6
  *
10
7
  * @param {import("@weborigami/async-tree").Stringlike} xml
11
8
  */
12
9
  export default async function xmlParse(xml) {
13
- xml = args.stringlike(xml, "Origami.xmlParse");
14
- const parser = await getParser();
15
- const dom = parser.parseFromString(xml, "application/xml");
16
- let object = domNodeToObject(dom);
17
- if (
18
- (object.name === "#document" || object.name === "#document-fragment") &&
19
- object.children.length === 1
20
- ) {
21
- object = object.children[0];
22
- }
23
- return object;
24
- }
10
+ console.warn("Origami.xmlParse is deprecated. Use Origami.xmlDom instead.");
25
11
 
26
- async function getParser() {
27
- if (!parser) {
28
- const { JSDOM } = await loadJsDom();
29
- const dom = new JSDOM();
30
- parser = new dom.window.DOMParser();
31
- }
32
- return parser;
12
+ xml = args.stringlike(xml, "Origami.xmlParse");
13
+ const dom = await xmlDom(xml);
14
+ return dom;
33
15
  }
@@ -1,4 +1,6 @@
1
1
  import { extension, isPacked, toString, Tree } from "@weborigami/async-tree";
2
+ import { symbols } from "@weborigami/language";
3
+ import { createHash } from "node:crypto";
2
4
  import { computedMIMEType } from "whatwg-mimetype";
3
5
  import { mediaTypeForExtension } from "./mediaTypes.js";
4
6
 
@@ -19,6 +21,16 @@ export default async function constructResponse(request, resource) {
19
21
  // Determine media type, what data we'll send, and encoding.
20
22
  const url = new URL(request?.url ?? "", `https://${request?.headers.host}`);
21
23
 
24
+ if (!isPacked(resource) && typeof resource.pack === "function") {
25
+ resource = await resource.pack();
26
+ if (typeof resource === "function") {
27
+ resource = await resource();
28
+ }
29
+ if (resource instanceof Response) {
30
+ return resource;
31
+ }
32
+ }
33
+
22
34
  if (!url.pathname.endsWith("/") && Tree.isMaplike(resource)) {
23
35
  // Maplike resource: redirect to its index page.
24
36
  const Location = `${url.pathname}/`;
@@ -30,16 +42,6 @@ export default async function constructResponse(request, resource) {
30
42
  });
31
43
  }
32
44
 
33
- if (!isPacked(resource) && typeof resource.pack === "function") {
34
- resource = await resource.pack();
35
- if (typeof resource === "function") {
36
- resource = await resource();
37
- }
38
- if (resource instanceof Response) {
39
- return resource;
40
- }
41
- }
42
-
43
45
  let body = resource;
44
46
  if (!isPacked(resource)) {
45
47
  // Can we treat it as text?
@@ -50,6 +52,7 @@ export default async function constructResponse(request, resource) {
50
52
  }
51
53
 
52
54
  // Determine MIME type
55
+ /** @type {string | undefined} */
53
56
  let mediaType;
54
57
  if (resource.mediaType) {
55
58
  // Resource indicates its own media type.
@@ -77,6 +80,10 @@ export default async function constructResponse(request, resource) {
77
80
  } else {
78
81
  mediaType = sniffedType.toString();
79
82
  }
83
+ if (mediaType === "text/plain") {
84
+ // Prefer UTF-8 encoding for text/plain
85
+ mediaType += "; charset=utf-8";
86
+ }
80
87
  }
81
88
  }
82
89
  }
@@ -91,7 +98,35 @@ export default async function constructResponse(request, resource) {
91
98
  );
92
99
  }
93
100
 
94
- const options = mediaType ? { headers: { "Content-Type": mediaType } } : {};
95
- const response = new Response(body, options);
101
+ // Compute ETag from the body content.
102
+ let etag;
103
+ if (!resource[symbols.volatileSymbol]) {
104
+ const hash = createHash("sha1");
105
+ if (typeof body === "string" || body instanceof String) {
106
+ hash.update(String(body), "utf8");
107
+ } else {
108
+ hash.update(body);
109
+ }
110
+ const digest = hash.digest("hex");
111
+ // Store ETag with quotes in cache to match If-None-Match header
112
+ etag = `"${digest}"`;
113
+ }
114
+
115
+ /** @type {Record<string, string>} */
116
+ const headers = {};
117
+ if (mediaType) {
118
+ headers["Content-Type"] = mediaType;
119
+ }
120
+ if (etag) {
121
+ headers["Cache-Control"] = "no-cache";
122
+ headers.ETag = etag;
123
+ }
124
+
125
+ const response = new Response(body, { headers });
126
+
127
+ if (resource[symbols.volatileSymbol]) {
128
+ response[symbols.volatileSymbol] = true;
129
+ }
130
+
96
131
  return response;
97
132
  }