diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..61c8f38
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,72 @@
+module.exports = {
+ env: {
+ node: true,
+ jest: true,
+ },
+ extends: [
+ 'eslint:recommended',
+ ],
+ overrides: [
+ {
+ env: {
+ node: true,
+ },
+ files: ['.eslintrc.{js,cjs}'],
+ parserOptions: {
+ sourceType: 'script',
+ },
+ },
+ ],
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ plugins: [
+ 'jest',
+ ],
+ rules: {
+ 'no-console': [
+ 'warn',
+ ],
+ 'no-unused-vars': [
+ 'warn',
+ ],
+ indent: [
+ 'error',
+ 'tab',
+ ],
+ 'no-tabs': ['off'],
+ 'linebreak-style': [
+ 'error',
+ 'unix',
+ ],
+ quotes: [
+ 'error',
+ 'single',
+ ],
+ semi: [
+ 'error',
+ 'always',
+ ],
+ 'max-len': [
+ 'error',
+ {
+ code: 90,
+ comments: 80,
+ ignoreUrls: true,
+ ignoreRegExpLiterals: true,
+ },
+ ],
+ 'object-curly-spacing': ['off'],
+ 'comma-dangle': [
+ 'error',
+ {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'ignore',
+ },
+ ],
+ },
+};
diff --git a/.vimrc b/.vimrc
new file mode 100644
index 0000000..91566d2
--- /dev/null
+++ b/.vimrc
@@ -0,0 +1,29 @@
+"""
+" To get your own vim to read this .vimrc file from any subdirectory of this
+" repository, add the following to your own ~/.vimrc file:
+"
+" " Look for .vimrc in the current directory or parent directories
+" let s:local_vimrc = findfile('.vimrc', expand('%:p:h').';')
+"
+" " Source the file if it exists and is not the global ~/.vimrc
+" if !empty(s:local_vimrc) && s:local_vimrc != expand('~/.vimrc')
+" execute 'source' s:local_vimrc
+" endif
+"
+" You should also install the [ale plugin](https://github.com/dense-analysis/ale)
+
+"""
+" Leave the rest of this unchanged.
+
+" -- Prefer tabs to spaces
+set noexpandtab
+set tabstop=2
+set shiftwidth=2
+set softtabstop=2
+set colorcolumn=80
+
+let g:ale_fixers = {
+\ 'javascript': ['eslint'],
+\}
+" -- automatically fix files on save
+let g:ale_fix_on_save=1
diff --git a/README.md b/README.md
index d50d307..a986277 100644
--- a/README.md
+++ b/README.md
@@ -1,172 +1,88 @@
+# @thefarce/loom
-## Rationale
-Modern implementations of web renderers seem to make incremental changes,
-working from the previous ideas rather than a desired ideal state.
+## What is this?
-This means seemingly needless constraints like Svelte's (SvelteKit's)
-absolute reliance on files.
+Loom is a tool for weaving form data with content data in nodejs. It does
+this by using elements of the Unified.js toolkit for parsing text into
+abstract syntax trees (AST).
-We do not want a framework, we want a toolkit from which appropriate
-frameworks can arise ephemerally to meet various needs. The core engine
-needs to do one thing: weave arbitrary content with arbitrary form.
+The fundamental idea of Loom is that presentation and content are orthogonal
+dimensions of data. A communication has values in these two dimensions
+integrated into a coherent whole.
-## Requirements
+## When should I use this?
-Here are the requirements:
+When you want to blend form (such as HTML) with content (such as JSON) into
+a single document. Unlike other systems, Loom does not require writing to
+or reading from files.
-The fundamental concept is that both form (for example, templates) and
-content (such as from a database) are considered data. This project, then,
-is to be a toolkit that allows you to create data flows.
+## Installation
-We want to be absolutely agnostic to platform, architecture, etc. We are
-just taking in data and transforming it repeatedly to desired outputs.
+`$` `npm install @thefarce/loom`
-Let's say we have a set of data in the form of:
+## Usage Example
- * HTML templates (including fragments)
- * Markdown templates
- * json data
- * csv data
+For more thorough usage examples, see the `examples` directory and refer to
+the [documentation](./docs/usage.md).
-We want to create a pipeline of transformers that do whatever is necessary
-to merge those in desirable ways. Here are some (overly-)simplified
-examples:
+```js
+import {
+ interpolate, // interpolate data into the AST
+ htmlToAst, // parse HTML to an AST
+ astToHtml, // render HTML from an AST
+ transform, // transform an AST according to plugins
+} from '@thefarce/loom'
-```
-# feed.json
-{
- "title": "How to do things",
- "link": "url.com",
- "description": "Learn how to do various things."
-}
+console.log(
+ renderHtml(
+ interpolate(
+ transform(
+ parseHtml('
Hello, {name}!
'),
+ [],
+ ),
+ { name: 'Othello' },
+ )
+ )
+);
```
-```
-# rss.csv
-title,link,date
-"How to see","url.com/story/how-to-see","2023-01-01"
-"How to eat","url.com/story/how-to-eat","2023-01-02"
-"How to run","url.com/story/how-to-run","2023-01-03"
+Running this script produces the following output:
+
+```html
+
Hello, Othello!
```
-```
-# Item.component.xml
-
- {item.title}
- https://{item.link}
- {item.description}
-
-```
+## API
-```
-# rss.template.xml
-
-
-
- {feed.title}
- https://{feed.link}
- {feed.description}
-
-
-
-
-
-```
+### renderHtml(ast)
-The toolkit will make it easy to weave these bits of data (both form data
-and content data) together.
+### parseHtml(html)
-Here are some flows and dreamcode.
+### interpolate(ast, data)
-## Flows
+### transform(ast, plugins)
-Possible flows:
+Transform the AST according to plugins.
-***Fragment-first***
+#### ast
-```
- 1. read Item.component.html
- 2. read rss.csv
- 3. transform rss.csv into a desired format
- 4. produce output XML with all item data fixed.
+The AST to be transformed. This can be any AST that conforms to the
+Unified.js specification, especially the
+[hast](https://github.com/syntax-tree/hast) AST.
- 5. read rss.template.xml
- 6. read feed.json
- 7. transform feed.json into a desired format
- 8. produce output XML with values fixed.
+#### plugins
- 9. integrate the two outputs for the final xml
- 10. optimize/minify
-```
+Plugins are pairs of functions.
-***Compilation-first***
+The *selector* function
-```
- 1. read Item.component.html
- 2. read rss.template.xml
- 3. compile a master template
+The *transformer* function
- 4. read rss.csv
- 5. read feed.json
- 6. compile a master data source
+## Development
- 7. produce an output XML with values fixed.
- 8. optimize/minify
-```
+### VIM
-## Dreamcode
-
-The key here is that components don't have to come from files. Key
-features:
- * Modules can come from any string, stream, buffer, or whatever.
- * Multiple modules can be defined in the same source.
- * Once a module is loaded, it's available to import with an ES6-like
- syntax.
-
-### Compiler / Renderer
-The target style is to use pure functions. If someone wants something like
-OO, they can wrap the functions.
-
-```
-// functional style
-(sanitizeXml
- (compileRenderedComponents
- (renderXmlWithJson
- (readXmlTemplate 'rss.template.xml')
- (readJson 'feed.json'))
-
- (renderXmlWithJson
- (readXmlTemplate 'Item.template.xml')
- (csvToJson (readCsv 'rss.csv')))
- )
-)
-```
-
-```
-// The same in a JavaScript-style syntax
-const xml = minifyXML(
- sanitizeXML(
- compileRenderedComponents(
- await renderXmlWithJson(
- readXmlTemplate('rss.template.xml'),
- readJson('feed.json')
- ),
-
- await renderXmlWithJson(
- readXmlTemplate('Item.template.xml'),
- csvToJson(readCsv('rss.csv'))
- )
- )
- )
-)
-```
-
-### Markup
-
-See [the Markup documentation](./docs/markup.md).
-
-### Components
-
-See [the Components documentation](./docs/components.md).
+This project contains a `.vimrc` file to help with development and
+maintainance. See that file for more information and instructions for use.
diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml
new file mode 100644
index 0000000..5b50087
--- /dev/null
+++ b/bitbucket-pipelines.yml
@@ -0,0 +1,47 @@
+---
+image: node:18.18.2
+
+definitions:
+
+ caches:
+ npm: $HOME/.npm
+
+ steps:
+ - step: &lint
+ name: Check code syntax
+ caches:
+ - npm
+ script:
+ - npm ci
+ - npm run lint
+
+ - step: &test
+ name: Run tests
+ caches:
+ - npm
+ script:
+ - npm ci
+ - npm run test
+
+ - step: &build
+ image: node:18-alpine
+ name: build the package for npm distribution
+ caches:
+ - npm
+ script:
+ - NODE_ENV=production npm ci
+ - npm run build
+
+pipelines:
+ default:
+ - parallel:
+ - step: *lint
+ - step: *test
+ branches:
+ master:
+ - parallel:
+ fail-fast: true
+ steps:
+ - step: *lint
+ - step: *test
+ - step: *build
diff --git a/config/eslint.config.cjs b/config/eslint.config.cjs
deleted file mode 100644
index f4d3a4c..0000000
--- a/config/eslint.config.cjs
+++ /dev/null
@@ -1,64 +0,0 @@
-module.exports = {
- env: {
- es2020: true,
- node: true,
- },
- extends: [
- 'airbnb-base',
- ],
- plugins: [
- 'eslint-plugin-import',
- ],
- parserOptions: {
- ecmaVersion: 11,
- sourceType: 'module',
- },
- rules: {
- // I'm not convinced this is the right way to handle this, but here we
- // are for now.
- "import/extensions": 0,
-
- // This is generally a code smell, but I don't think it is this time.
- // An alternate approach may be to make it a static method and use it
- // directly from the base class that way.
- "class-methods-use-this": [
- "error",
- {
- exceptMethods: [
- 'normalizeFlagAndValue'
- ]
- }
- ],
- "no-plusplus": [
- "error",
- {
- "allowForLoopAfterthoughts": true
- }
- ],
- // I think I would rather this be turned on, but eslint doesn't seem to
- // allow stroustrup with comments, which is important to me.
- "brace-style": 0,
- "space-before-function-paren": 0,
- "import/no-named-as-default": 0,
- "key-spacing": [
- "error",
- {
- "align": {
- "on" : "colon",
- "beforeColon" : true,
- "afterColon" : true,
- "mode" : "strict",
- },
- }
- ],
- "no-multi-spaces": [
- "error",
- {
- exceptions: {
- ImportDeclaration: true,
- VariableDeclarator: true,
- }
- }
- ]
- },
-};
diff --git a/config/jest.spec.config.json b/config/jest.spec.config.json
deleted file mode 100644
index 49f4e29..0000000
--- a/config/jest.spec.config.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "rootDir": "../",
- "reporters": [
- "default",
- [
- "./node_modules/jest-html-reporter", {
- "boilerplate": "./test/spec/report-template.html",
- "outputPath": "./reports/spec/index.html",
- "pageTitle": "Specification Progress Report",
- "sort": "titleAsc"
- }
- ]
- ]
-}
diff --git a/config/jest.test.config.json b/config/jest.test.config.json
deleted file mode 100644
index 1bd937d..0000000
--- a/config/jest.test.config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "rootDir": "../",
- "collectCoverageFrom": [
- "./src/**/*.js"
- ],
- "coverageReporters": [
- "json",
- "json-summary",
- "lcov",
- "text",
- "clover"
- ],
- "coverageThreshold": {
- "global": {
- "branches" : 100,
- "functions" : 100,
- "lines" : 100,
- "statements" : 100
- }
- }
-}
diff --git a/config/jsdoc.config.json b/config/jsdoc.config.json
deleted file mode 100644
index b8e0ec2..0000000
--- a/config/jsdoc.config.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "opts": {
- "destination": "./docs",
- "recurse": true
- },
- "plugins": [
- "plugins/markdown"
- ],
- "recurseDepth": 10,
- "source": {
- "includePattern": ".+\\.js(doc|x)?$",
- "excludePattern": "(^|\\/|\\\\)_"
- },
- "sourceType": "module",
- "tags": {
- "allowUnknownTags": true,
- "dictionaries": [
- "jsdoc",
- "closure"
- ]
- },
- "templates": {
- "systemName" : "The Farce: Commando",
- "footer" : "Produced by The Farce",
- "copyright" : "Copyright The Farce",
- "includeDate" : true,
- "navType" : "vertical",
- "theme" : "cerulean",
- "linenums" : true,
- "collapseSymbols" : true,
- "inverseNav" : true,
- "syntaxTheme" : "dark",
- "sort" : "longname, version, since",
- "search" : true
- }
-}
diff --git a/coverage/clover.xml b/coverage/clover.xml
deleted file mode 100644
index 3d00ace..0000000
--- a/coverage/clover.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/coverage/coverage-final.json b/coverage/coverage-final.json
deleted file mode 100644
index 0967ef4..0000000
--- a/coverage/coverage-final.json
+++ /dev/null
@@ -1 +0,0 @@
-{}
diff --git a/coverage/coverage-summary.json b/coverage/coverage-summary.json
deleted file mode 100644
index ef1ae37..0000000
--- a/coverage/coverage-summary.json
+++ /dev/null
@@ -1,2 +0,0 @@
-{"total": {"lines":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"statements":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"functions":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"branches":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}}
-}
diff --git a/coverage/lcov-report/base.css b/coverage/lcov-report/base.css
deleted file mode 100644
index f418035..0000000
--- a/coverage/lcov-report/base.css
+++ /dev/null
@@ -1,224 +0,0 @@
-body, html {
- margin:0; padding: 0;
- height: 100%;
-}
-body {
- font-family: Helvetica Neue, Helvetica, Arial;
- font-size: 14px;
- color:#333;
-}
-.small { font-size: 12px; }
-*, *:after, *:before {
- -webkit-box-sizing:border-box;
- -moz-box-sizing:border-box;
- box-sizing:border-box;
- }
-h1 { font-size: 20px; margin: 0;}
-h2 { font-size: 14px; }
-pre {
- font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
- margin: 0;
- padding: 0;
- -moz-tab-size: 2;
- -o-tab-size: 2;
- tab-size: 2;
-}
-a { color:#0074D9; text-decoration:none; }
-a:hover { text-decoration:underline; }
-.strong { font-weight: bold; }
-.space-top1 { padding: 10px 0 0 0; }
-.pad2y { padding: 20px 0; }
-.pad1y { padding: 10px 0; }
-.pad2x { padding: 0 20px; }
-.pad2 { padding: 20px; }
-.pad1 { padding: 10px; }
-.space-left2 { padding-left:55px; }
-.space-right2 { padding-right:20px; }
-.center { text-align:center; }
-.clearfix { display:block; }
-.clearfix:after {
- content:'';
- display:block;
- height:0;
- clear:both;
- visibility:hidden;
- }
-.fl { float: left; }
-@media only screen and (max-width:640px) {
- .col3 { width:100%; max-width:100%; }
- .hide-mobile { display:none!important; }
-}
-
-.quiet {
- color: #7f7f7f;
- color: rgba(0,0,0,0.5);
-}
-.quiet a { opacity: 0.7; }
-
-.fraction {
- font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
- font-size: 10px;
- color: #555;
- background: #E8E8E8;
- padding: 4px 5px;
- border-radius: 3px;
- vertical-align: middle;
-}
-
-div.path a:link, div.path a:visited { color: #333; }
-table.coverage {
- border-collapse: collapse;
- margin: 10px 0 0 0;
- padding: 0;
-}
-
-table.coverage td {
- margin: 0;
- padding: 0;
- vertical-align: top;
-}
-table.coverage td.line-count {
- text-align: right;
- padding: 0 5px 0 20px;
-}
-table.coverage td.line-coverage {
- text-align: right;
- padding-right: 10px;
- min-width:20px;
-}
-
-table.coverage td span.cline-any {
- display: inline-block;
- padding: 0 5px;
- width: 100%;
-}
-.missing-if-branch {
- display: inline-block;
- margin-right: 5px;
- border-radius: 3px;
- position: relative;
- padding: 0 4px;
- background: #333;
- color: yellow;
-}
-
-.skip-if-branch {
- display: none;
- margin-right: 10px;
- position: relative;
- padding: 0 4px;
- background: #ccc;
- color: white;
-}
-.missing-if-branch .typ, .skip-if-branch .typ {
- color: inherit !important;
-}
-.coverage-summary {
- border-collapse: collapse;
- width: 100%;
-}
-.coverage-summary tr { border-bottom: 1px solid #bbb; }
-.keyline-all { border: 1px solid #ddd; }
-.coverage-summary td, .coverage-summary th { padding: 10px; }
-.coverage-summary tbody { border: 1px solid #bbb; }
-.coverage-summary td { border-right: 1px solid #bbb; }
-.coverage-summary td:last-child { border-right: none; }
-.coverage-summary th {
- text-align: left;
- font-weight: normal;
- white-space: nowrap;
-}
-.coverage-summary th.file { border-right: none !important; }
-.coverage-summary th.pct { }
-.coverage-summary th.pic,
-.coverage-summary th.abs,
-.coverage-summary td.pct,
-.coverage-summary td.abs { text-align: right; }
-.coverage-summary td.file { white-space: nowrap; }
-.coverage-summary td.pic { min-width: 120px !important; }
-.coverage-summary tfoot td { }
-
-.coverage-summary .sorter {
- height: 10px;
- width: 7px;
- display: inline-block;
- margin-left: 0.5em;
- background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
-}
-.coverage-summary .sorted .sorter {
- background-position: 0 -20px;
-}
-.coverage-summary .sorted-desc .sorter {
- background-position: 0 -10px;
-}
-.status-line { height: 10px; }
-/* yellow */
-.cbranch-no { background: yellow !important; color: #111; }
-/* dark red */
-.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
-.low .chart { border:1px solid #C21F39 }
-.highlighted,
-.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
- background: #C21F39 !important;
-}
-/* medium red */
-.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
-/* light red */
-.low, .cline-no { background:#FCE1E5 }
-/* light green */
-.high, .cline-yes { background:rgb(230,245,208) }
-/* medium green */
-.cstat-yes { background:rgb(161,215,106) }
-/* dark green */
-.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
-.high .chart { border:1px solid rgb(77,146,33) }
-/* dark yellow (gold) */
-.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
-.medium .chart { border:1px solid #f9cd0b; }
-/* light yellow */
-.medium { background: #fff4c2; }
-
-.cstat-skip { background: #ddd; color: #111; }
-.fstat-skip { background: #ddd; color: #111 !important; }
-.cbranch-skip { background: #ddd !important; color: #111; }
-
-span.cline-neutral { background: #eaeaea; }
-
-.coverage-summary td.empty {
- opacity: .5;
- padding-top: 4px;
- padding-bottom: 4px;
- line-height: 1;
- color: #888;
-}
-
-.cover-fill, .cover-empty {
- display:inline-block;
- height: 12px;
-}
-.chart {
- line-height: 0;
-}
-.cover-empty {
- background: white;
-}
-.cover-full {
- border-right: none !important;
-}
-pre.prettyprint {
- border: none !important;
- padding: 0 !important;
- margin: 0 !important;
-}
-.com { color: #999 !important; }
-.ignore-none { color: #999; font-weight: normal; }
-
-.wrapper {
- min-height: 100%;
- height: auto !important;
- height: 100%;
- margin: 0 auto -48px;
-}
-.footer, .push {
- height: 48px;
-}
diff --git a/coverage/lcov-report/block-navigation.js b/coverage/lcov-report/block-navigation.js
deleted file mode 100644
index cc12130..0000000
--- a/coverage/lcov-report/block-navigation.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/* eslint-disable */
-var jumpToCode = (function init() {
- // Classes of code we would like to highlight in the file view
- var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
-
- // Elements to highlight in the file listing view
- var fileListingElements = ['td.pct.low'];
-
- // We don't want to select elements that are direct descendants of another match
- var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
-
- // Selecter that finds elements on the page to which we can jump
- var selector =
- fileListingElements.join(', ') +
- ', ' +
- notSelector +
- missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
-
- // The NodeList of matching elements
- var missingCoverageElements = document.querySelectorAll(selector);
-
- var currentIndex;
-
- function toggleClass(index) {
- missingCoverageElements
- .item(currentIndex)
- .classList.remove('highlighted');
- missingCoverageElements.item(index).classList.add('highlighted');
- }
-
- function makeCurrent(index) {
- toggleClass(index);
- currentIndex = index;
- missingCoverageElements.item(index).scrollIntoView({
- behavior: 'smooth',
- block: 'center',
- inline: 'center'
- });
- }
-
- function goToPrevious() {
- var nextIndex = 0;
- if (typeof currentIndex !== 'number' || currentIndex === 0) {
- nextIndex = missingCoverageElements.length - 1;
- } else if (missingCoverageElements.length > 1) {
- nextIndex = currentIndex - 1;
- }
-
- makeCurrent(nextIndex);
- }
-
- function goToNext() {
- var nextIndex = 0;
-
- if (
- typeof currentIndex === 'number' &&
- currentIndex < missingCoverageElements.length - 1
- ) {
- nextIndex = currentIndex + 1;
- }
-
- makeCurrent(nextIndex);
- }
-
- return function jump(event) {
- if (
- document.getElementById('fileSearch') === document.activeElement &&
- document.activeElement != null
- ) {
- // if we're currently focused on the search input, we don't want to navigate
- return;
- }
-
- switch (event.which) {
- case 78: // n
- case 74: // j
- goToNext();
- break;
- case 66: // b
- case 75: // k
- case 80: // p
- goToPrevious();
- break;
- }
- };
-})();
-window.addEventListener('keydown', jumpToCode);
diff --git a/coverage/lcov-report/favicon.png b/coverage/lcov-report/favicon.png
deleted file mode 100644
index c1525b8..0000000
Binary files a/coverage/lcov-report/favicon.png and /dev/null differ
diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html
deleted file mode 100644
index b5ed4f5..0000000
--- a/coverage/lcov-report/index.html
+++ /dev/null
@@ -1,101 +0,0 @@
-
-
-
-
-
- Code coverage report for All files
-
-
-
-
-
-
-
-
-
-
-
-
All files
-
-
-
- Unknown%
- Statements
- 0/0
-
-
-
-
- Unknown%
- Branches
- 0/0
-
-
-
-
- Unknown%
- Functions
- 0/0
-
-
-
-
- Unknown%
- Lines
- 0/0
-
-
-
-
-
- Press n or j to go to the next uncovered block, b, p or k for the previous block.
-