@weborigami/origami 0.6.15 → 0.6.17

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.
@@ -0,0 +1,93 @@
1
+ const ELEMENT_NODE = 1;
2
+ const TEXT_NODE = 3;
3
+ const CDATA_SECTION_NODE = 4;
4
+ const DOCUMENT_NODE = 9;
5
+ const DOCUMENT_FRAGMENT_NODE = 11;
6
+
7
+ export default function domNodeToObject(node) {
8
+ switch (node.nodeType) {
9
+ case DOCUMENT_NODE:
10
+ return {
11
+ name: "#document",
12
+ children: [...node.childNodes]
13
+ .filter((child) => !isWhitespaceOnly(child))
14
+ .map(domNodeToObject),
15
+ };
16
+
17
+ case DOCUMENT_FRAGMENT_NODE:
18
+ return {
19
+ name: "#document-fragment",
20
+ children: [...node.childNodes]
21
+ .filter((child) => !isWhitespaceOnly(child))
22
+ .map(domNodeToObject),
23
+ };
24
+
25
+ case ELEMENT_NODE: {
26
+ const attributes = Object.fromEntries(
27
+ [...node.attributes].map((attr) => [attr.name, attr.value]),
28
+ );
29
+
30
+ const relevantChildren = [...node.childNodes].filter(
31
+ (child) =>
32
+ (child.nodeType === ELEMENT_NODE ||
33
+ child.nodeType === TEXT_NODE ||
34
+ child.nodeType === CDATA_SECTION_NODE) &&
35
+ !isWhitespaceOnly(child),
36
+ );
37
+
38
+ const onlyText = relevantChildren.every(
39
+ (child) =>
40
+ child.nodeType === TEXT_NODE || child.nodeType === CDATA_SECTION_NODE,
41
+ );
42
+
43
+ const result = {
44
+ name: node.localName,
45
+ };
46
+ if (Object.keys(attributes).length > 0) {
47
+ result.attributes = attributes;
48
+ }
49
+ if (onlyText) {
50
+ const text = relevantChildren
51
+ .map((child) => collapseWhitespace(child.nodeValue ?? ""))
52
+ .join("")
53
+ .trim();
54
+ if (text.length > 0) {
55
+ result.text = text;
56
+ }
57
+ } else if (relevantChildren.length > 0) {
58
+ result.children = relevantChildren.map(domNodeToObject);
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ case TEXT_NODE:
65
+ return {
66
+ name: "#text",
67
+ text: collapseWhitespace(node.nodeValue ?? ""),
68
+ };
69
+
70
+ case CDATA_SECTION_NODE:
71
+ return {
72
+ name: "#cdata-section",
73
+ text: collapseWhitespace(node.nodeValue ?? ""),
74
+ };
75
+
76
+ default:
77
+ return {
78
+ name: `#node-${node.nodeType}`,
79
+ };
80
+ }
81
+ }
82
+
83
+ // Collapse leading or trailing whitespace characters to a single space
84
+ function collapseWhitespace(str) {
85
+ return str.replace(/^\s+/, " ").replace(/\s+$/, " ");
86
+ }
87
+
88
+ function isWhitespaceOnly(node) {
89
+ return (
90
+ (node.nodeType === TEXT_NODE || node.nodeType === CDATA_SECTION_NODE) &&
91
+ node.nodeValue.trim() === ""
92
+ );
93
+ }
@@ -0,0 +1,17 @@
1
+ import hashBytes from "../common/hashBytes.js";
2
+
3
+ /**
4
+ * Given string or Uint8Array data, return a hex-encoded hash of that data.
5
+ *
6
+ * @typedef {import("@weborigami/async-tree").Stringlike} Stringlike
7
+ *
8
+ * @param {Uint8Array|Stringlike} data
9
+ */
10
+ export default function hash(data) {
11
+ const bytes = hashBytes(data);
12
+ const text = Array.from(bytes)
13
+ .slice(0, 20) // Limit to first 20 bytes (160 bits) for a shorter hash string
14
+ .map((byte) => byte.toString(16).padStart(2, "0"))
15
+ .join("");
16
+ return text;
17
+ }
@@ -0,0 +1,22 @@
1
+ import { args } from "@weborigami/async-tree";
2
+ import loadJsDom from "../common/loadJsDom.js";
3
+ import domNodeToObject from "./domNodeToObject.js";
4
+
5
+ /**
6
+ * Return the DOM structure for the given HTML as a plain object.
7
+ *
8
+ * @param {import("@weborigami/async-tree").Stringlike} html
9
+ */
10
+ export default async function htmlParse(html) {
11
+ 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;
22
+ }
@@ -55,4 +55,5 @@ export default async function mdHtml(input) {
55
55
 
56
56
  mdHtml.key = (sourceValue, sourceKey) =>
57
57
  extension.replace(sourceKey, ".md", ".html");
58
+ mdHtml.key.needsSourceValue = false;
58
59
  mdHtml.inverseKey = (resultKey) => extension.replace(resultKey, ".html", ".md");
@@ -22,18 +22,29 @@ export default async function mdOutline(input) {
22
22
  const outline = {};
23
23
  const stack = [];
24
24
  let sectionText = "";
25
+ let sectionTextTrimmed = null;
25
26
  /** @type {any} */
26
27
  let current = outline;
27
28
  for (const token of tokens) {
28
29
  if (token.type === "heading") {
29
30
  // Current section text gets added as content for the current node.
30
- if (sectionText) {
31
- current._text = sectionText.trim();
31
+ sectionTextTrimmed = sectionText.trim();
32
+ if (sectionTextTrimmed) {
33
+ current._text = sectionTextTrimmed;
32
34
  sectionText = "";
33
35
  }
34
36
 
35
- // Pop the stack to find the right level for this heading
36
37
  const { depth, text: headingText } = token;
38
+
39
+ // Did we skip a heading level? If so, create `_skip<n>` nodes
40
+ while (stack.length < depth - 1) {
41
+ const skipNode = {};
42
+ current[`_skip${stack.length + 1}`] = skipNode;
43
+ stack.push(current);
44
+ current = skipNode;
45
+ }
46
+
47
+ // Pop the stack to find the right level for this heading
37
48
  while (stack.length >= depth) {
38
49
  current = stack.pop();
39
50
  consolidateText(current);
@@ -51,8 +62,9 @@ export default async function mdOutline(input) {
51
62
  }
52
63
 
53
64
  // Any remaining section text gets added as content for the current node.
54
- if (sectionText) {
55
- current._text = sectionText.trim();
65
+ sectionTextTrimmed = sectionText.trim();
66
+ if (sectionTextTrimmed) {
67
+ current._text = sectionTextTrimmed;
56
68
  current = stack.pop();
57
69
  if (current) {
58
70
  consolidateText(current);
@@ -4,8 +4,9 @@ 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
6
  export { default as fetch } from "./fetch.js";
7
- export { default as htmlDom } from "./htmlDom.js";
7
+ export { default as hash } from "./hash.js";
8
8
  export { default as htmlEscape } from "./htmlEscape.js";
9
+ export { default as htmlParse } from "./htmlParse.js";
9
10
  export { default as format } from "./image/format.js";
10
11
  export * as image from "./image/image.js";
11
12
  export { default as resize } from "./image/resize.js";
@@ -23,6 +24,8 @@ export { default as pack } from "./pack.js";
23
24
  export { default as post } from "./post.js";
24
25
  export { default as project } from "./project.js";
25
26
  export { default as projectRoot } from "./projectRoot.js";
27
+ export { default as randomFrom } from "./randomFrom.js";
28
+ export { default as randomsFrom } from "./randomsFrom.js";
26
29
  export { default as redirect } from "./redirect.js";
27
30
  export { default as repeat } from "./repeat.js";
28
31
  export { default as rss } from "./rss.js";
@@ -34,5 +37,6 @@ export { default as static } from "./static.js";
34
37
  export { default as string } from "./string.js";
35
38
  export { default as tsv } from "./tsv.js";
36
39
  export { default as unpack } from "./unpack.js";
40
+ export { default as xmlParse } from "./xmlParse.js";
37
41
  export { default as yaml } from "./yaml.js";
38
42
  export { default as yamlParse } from "./yamlParse.js";
@@ -0,0 +1,15 @@
1
+ import hashBytes from "../common/hashBytes.js";
2
+
3
+ /**
4
+ * Given a block of seed data, derive a pseudo-random 32-bit integer.
5
+ *
6
+ * @typedef {import("@weborigami/async-tree").Stringlike} Stringlike
7
+ *
8
+ * @param {Uint8Array|Stringlike} data
9
+ * @return {number}
10
+ */
11
+ export default function randomFrom(data) {
12
+ // Extract the first 32-bit integer from the hash to use as the random number
13
+ const hash = hashBytes(data);
14
+ return hash.readUInt32LE(0);
15
+ }
@@ -0,0 +1,65 @@
1
+ import hashBytes from "../common/hashBytes.js";
2
+
3
+ /**
4
+ * Given a block of seed data, return a function that produces a sequence of
5
+ * pseudo-random 32-bit integers.
6
+ *
7
+ * @typedef {import("@weborigami/async-tree").Stringlike} Stringlike
8
+ *
9
+ * @param {Uint8Array|Stringlike} data
10
+ * @return {function(): number}
11
+ */
12
+ export default function randomsFrom(data) {
13
+ const hash = hashBytes(data);
14
+
15
+ // Extract four 32-bit integers from the hash to use as the initial state of
16
+ // the pseudo-random number generator
17
+ const a = hash.readUInt32LE(0);
18
+ const b = hash.readUInt32LE(4);
19
+ const c = hash.readUInt32LE(8);
20
+ const d = hash.readUInt32LE(12);
21
+
22
+ const prng = xoshiro128ss(a, b, c, d);
23
+ return prng;
24
+ }
25
+
26
+ // Rotate left (circular left shift) for 32-bit integers
27
+ function rotl(x, k) {
28
+ return ((x << k) | (x >>> (32 - k))) >>> 0;
29
+ }
30
+
31
+ /**
32
+ * Pseudo-random number generator based on the xoshiro128** algorithm. See
33
+ * the 256-bit variant: https://en.wikipedia.org/wiki/Xorshift#xoshiro256**
34
+ *
35
+ * Unlike a typical implementation, this directly returns a 32-bit integer
36
+ * instead of a float.
37
+ *
38
+ * @param {number} a
39
+ * @param {number} b
40
+ * @param {number} c
41
+ * @param {number} d
42
+ * @returns {function(): number}
43
+ */
44
+ function xoshiro128ss(a, b, c, d) {
45
+ let s0 = a >>> 0;
46
+ let s1 = b >>> 0;
47
+ let s2 = c >>> 0;
48
+ let s3 = d >>> 0;
49
+
50
+ return function () {
51
+ const result = (rotl(Math.imul(s1, 5), 7) * 9) >>> 0;
52
+
53
+ const t = (s1 << 9) >>> 0;
54
+
55
+ s2 ^= s0;
56
+ s3 ^= s1;
57
+ s1 ^= s2;
58
+ s0 ^= s3;
59
+
60
+ s2 ^= t;
61
+ s3 = rotl(s3, 11);
62
+
63
+ return result;
64
+ };
65
+ }
@@ -0,0 +1,33 @@
1
+ import { args } from "@weborigami/async-tree";
2
+ import loadJsDom from "../common/loadJsDom.js";
3
+ import domNodeToObject from "./domNodeToObject.js";
4
+
5
+ let parser;
6
+
7
+ /**
8
+ * Return the DOM for the given XML as a plain object.
9
+ *
10
+ * @param {import("@weborigami/async-tree").Stringlike} xml
11
+ */
12
+ 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
+ }
25
+
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;
33
+ }
@@ -88,7 +88,14 @@ export async function handleRequest(request, response, map) {
88
88
 
89
89
  export function keysFromUrl(url) {
90
90
  const encodedKeys = keysFromPath(url.pathname);
91
- const keys = encodedKeys.map((key) => decodeURIComponent(key));
91
+ // Decode the keys, but stop decoding if we encounter an Origami debugger command
92
+ let foundCommand = false;
93
+ const keys = encodedKeys.map((key) => {
94
+ if (key.startsWith("!")) {
95
+ foundCommand = true;
96
+ }
97
+ return foundCommand ? key : decodeURIComponent(key);
98
+ });
92
99
 
93
100
  // If the keys array is empty (the path was just a trailing slash) or if the
94
101
  // path ended with a slash, add "index.html" to the end of the keys.
@@ -104,12 +111,17 @@ export function keysFromUrl(url) {
104
111
  * https.createServer calls, letting you serve an async tree as a set of pages.
105
112
  *
106
113
  * @typedef {import("@weborigami/async-tree").Maplike} Maplike
114
+ * @param {object} options
115
+ * @param {boolean} [options.quiet] If true, suppresses logging of incoming requests.
107
116
  * @param {Maplike} maplike
108
117
  */
109
- export function requestListener(maplike) {
118
+ export function requestListener(maplike, options = {}) {
119
+ const quiet = options.quiet ?? false;
110
120
  const tree = Tree.from(maplike);
111
121
  return async function (request, response) {
112
- console.log(decodeURI(request.url));
122
+ if (!quiet) {
123
+ console.log(decodeURI(request.url));
124
+ }
113
125
  const handled = await handleRequest(request, response, tree);
114
126
  if (!handled) {
115
127
  // Not found, return a 404.
@@ -1,14 +0,0 @@
1
- import { args } from "@weborigami/async-tree";
2
- import loadJsDom from "../common/loadJsDom.js";
3
-
4
- /**
5
- * Return the DOM for the given HTML string.
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
- const dom = JSDOM.fragment(html);
13
- return dom;
14
- }