diff --git a/package-lock.json b/package-lock.json
index 0bea353..19ce632 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,9 @@
"hast-util-from-html": "^1.0.1",
"hast-util-to-html": "^8.0.4",
"hastscript": "^7.2.0",
- "unist-util-visit": "^4.1.2"
+ "unist-util-visit": "^4.1.2",
+ "xast-util-from-xml": "^3.0.0",
+ "xast-util-to-xml": "^3.0.2"
},
"devDependencies": {
"@babel/preset-env": "^7.16.11",
@@ -2671,6 +2673,14 @@
"node": ">= 8"
}
},
+ "node_modules/@rgrove/parse-xml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@rgrove/parse-xml/-/parse-xml-4.1.0.tgz",
+ "integrity": "sha512-pBiltENdy8SfI0AeR1e5TRpS9/9Gl0eiOEt6ful2jQfzsgvZYWqsKiBWaOCLdocQuk0wS7KOHI37n0C1pnKqTw==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.25.24",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
@@ -2859,6 +2869,14 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
+ "node_modules/@types/xast": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@types/xast/-/xast-1.0.2.tgz",
+ "integrity": "sha512-eIuX49CmWaXTh0hDEQTFH5MbTV+NPm0FM5gB36nDcr9x8n75t9o7wy2guS1lakkQxtUwfVDYiokqvHk3x0CC4Q==",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "17.0.24",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
@@ -9100,6 +9118,35 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
+ "node_modules/xast-util-from-xml": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xast-util-from-xml/-/xast-util-from-xml-3.0.0.tgz",
+ "integrity": "sha512-zEfGxnue7vHbayZnRBAjJsjV1dQG6XWKLAzrsPxX2l4WHmqGZxM76XxRblaTHvcOYMFLlnVp3DFcUAl+Qk9SNQ==",
+ "dependencies": {
+ "@rgrove/parse-xml": "^4.1.0",
+ "@types/xast": "^1.0.0",
+ "vfile-location": "^4.0.0",
+ "vfile-message": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/xast-util-to-xml": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/xast-util-to-xml/-/xast-util-to-xml-3.0.2.tgz",
+ "integrity": "sha512-lbnJEVui7Lk9W/TydInkin5OnOyTAr2b4OpK5YjXo94qqoXaR6mFE/evXfOrBev9EnD91r0FOPxrLfTT0qLITQ==",
+ "dependencies": {
+ "@types/xast": "^1.0.0",
+ "ccount": "^2.0.0",
+ "stringify-entities": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/xmlbuilder": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.0.0.tgz",
diff --git a/package.json b/package.json
index 4400293..4e1c82f 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,8 @@
"hast-util-from-html": "^1.0.1",
"hast-util-to-html": "^8.0.4",
"hastscript": "^7.2.0",
- "unist-util-visit": "^4.1.2"
+ "unist-util-visit": "^4.1.2",
+ "xast-util-from-xml": "^3.0.0",
+ "xast-util-to-xml": "^3.0.2"
}
}
diff --git a/src/apply-modules.js b/src/apply-modules.js
new file mode 100644
index 0000000..dd48859
--- /dev/null
+++ b/src/apply-modules.js
@@ -0,0 +1,50 @@
+import {visit} from 'unist-util-visit';
+
+/**
+ * applyModules({ast,modules})
+ *
+ * Given an abstract syntax tree and a catalog of modules, apply the modules
+ * to the AST.
+ *
+ * @param {object} An object containing an abstract syntax tree (ast) and a
+ * catalog of modules (modules).
+ * @returns {ast, modules}
+ */
+export default async function applyModules ({ast, modules}) {
+ const result = {};
+
+ visit(
+ ast,
+ // A filter. True means visit this node, false means don't. Optional
+ // param.
+ node => {
+ if (!node?.children?.length) {
+ return false;
+ }
+
+ // Loop through this node's children. If any of them have a name that
+ // matches a defined module, we want to use this node (so we can
+ // replace the child with the component's content).
+ for (var i = 0; i < node.children.length; i++) {
+ if (Object.keys(modules).includes(node.children[i].name)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+ // This is the what-to-do-with-the-nodes function.
+ node => {
+ // Loop through each child. If the child's name matches a component's
+ // registered name, replace the child with the component's contents.
+ for (let c = 0; c < node.children.length; c++) {
+ if (Object.keys(modules).includes(node.children[c].name)) {
+ node.children.splice(c,1,...modules[node.children[c].name].children);
+ }
+ }
+ }
+ );
+
+ return {ast, modules};
+}
+
diff --git a/src/extract-modules.js b/src/extract-modules.js
new file mode 100644
index 0000000..d1972cf
--- /dev/null
+++ b/src/extract-modules.js
@@ -0,0 +1,47 @@
+import {visit} from 'unist-util-visit';
+
+/**
+ * extractModules()
+ *
+ * Given an abstract syntax tree, remove loom modules. Return the remaining
+ * AST and a catalog of modules.
+ *
+ * @param {ast} ast An abstract syntax tree from XML.
+ * @returns { modules, ast }
+ */
+export default async function extractModules (ast) {
+ const modules = {};
+
+ visit(
+ ast,
+ node => {
+ if (!node?.children?.length) {
+ return false;
+ }
+
+ for (var i = 0; i < node.children.length; i++) {
+ if (node.children[i].name === 'loom-module') {
+ return true;
+ }
+ }
+ },
+ node => {
+ node.children = node.children.reduce(
+ (list,item) => {
+ if (item.name === 'loom-module') {
+ if (item.attributes?.define) {
+ modules[item.attributes?.define] = item;
+ }
+ } else {
+ list.push(item);
+ }
+ return list;
+ },
+ []
+ );
+ }
+ );
+
+ return {modules, ast};
+}
+
diff --git a/src/file-to-ast.js b/src/file-to-ast.js
new file mode 100644
index 0000000..85084b4
--- /dev/null
+++ b/src/file-to-ast.js
@@ -0,0 +1,21 @@
+import fs from 'node:fs/promises';
+import {fromXml} from 'xast-util-from-xml';
+/**
+ * fileToAst(fn)
+ *
+ * Given a filename, parse it as XML to an abstract syntax tree.
+ *
+ * @param {string} The path to an XML file.
+ * @returns {ast} An abstract syntax tree of the XHTML file.
+ *
+ *
+ *
+ */
+export default async function fileToAst (fn) {
+ const html = await fs.readFile(fn);
+ const tree = fromXml(
+ `