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( + `${html}`, + {depth: null} + ); + return tree; +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..1f7f50b --- /dev/null +++ b/test.js @@ -0,0 +1,13 @@ +import extractModules from './src/extract-modules.js'; +import applyModules from './src/apply-modules.js'; +import fileToAst from './src/file-to-ast.js'; +import {toXml} from 'xast-util-to-xml'; + +fileToAst('./tmp/demo.html') + .then(extractModules) + .then(applyModules) + .then(({ast,modules}) => { + console.log(toXml(ast.children[0].children)); + }); + + diff --git a/tmp/demo.html b/tmp/demo.html index ed52ee4..156f6cf 100644 --- a/tmp/demo.html +++ b/tmp/demo.html @@ -1,5 +1,6 @@ component + foo
@@ -7,3 +8,9 @@ testing
+ +
+

Title

+ testing + +
diff --git a/tmp/read-demo.js b/tmp/read-demo.js index e1bca0e..79905b4 100644 --- a/tmp/read-demo.js +++ b/tmp/read-demo.js @@ -1,6 +1,7 @@ import * as fs from 'fs/promises' -import { fromHtml } from 'hast-util-from-html' -import { toHtml } from 'hast-util-to-html' +import {fromXml} from 'xast-util-from-xml' + +import { toXml } from 'xast-util-to-xml' import { visit } from 'unist-util-visit' @@ -10,12 +11,12 @@ async function readHtml (fn) { async function fileToAst () { const html = await readHtml('./demo.html'); - const tree = fromHtml(html, {fragment: true}) + const tree = fromXml(`${html}`, {depth: null}) return tree; } async function extractModules (ast) { - const modules = []; + const modules = {}; visit( ast, @@ -25,7 +26,7 @@ async function extractModules (ast) { } for (var i = 0; i < node.children.length; i++) { - if (node.children[i].tagName === 'loom-module') { + if (node.children[i].name === 'loom-module') { return true; } } @@ -33,9 +34,10 @@ async function extractModules (ast) { node => { node.children = node.children.reduce( (list,item) => { - if (item.tagName === 'loom-module') { - console.log("foo") - modules.push(item); + if (item.name === 'loom-module') { + if (item.attributes?.define) { + modules[item.attributes?.define] = item; + } } else { list.push(item); } @@ -49,15 +51,50 @@ async function extractModules (ast) { return {modules, ast}; } +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}; +} + + fileToAst() - .then(ast => { - return ast; - }) .then(extractModules) + .then(applyModules) .then(({ast,modules}) => { - console.log(JSON.stringify(ast,undefined,2)); - console.log(toHtml(ast)) - console.log(modules); + console.log(toXml(ast.children[0].children)) }) diff --git a/tmp/test.js b/tmp/test.js index 35a7caa..1498120 100644 --- a/tmp/test.js +++ b/tmp/test.js @@ -1,19 +1,16 @@ import {h, s} from 'hastscript' -console.log( - h('.foo#some-id', [ - h('span', 'some text'), - h('input', {type: 'text', value: 'foo'}), - h('a.alpha', {class: 'bravo charlie', download: 'download'}, [ - 'delta', - 'echo' - ]) - ]) -) +import {fromXml} from 'xast-util-from-xml'; + +const xml = ` + + Born in the U.S.A. + Bruce Springsteen + 1984-04-06 + +` + +const tree = fromXml(xml); + +console.dir(tree, {depth:null}); -console.log( - s('svg', {xmlns: 'http://www.w3.org/2000/svg', viewbox: '0 0 500 500'}, [ - s('title', 'SVG `` element'), - s('circle', {cx: 120, cy: 120, r: 100}) - ]) -)