Core commit for loom with hast.

master
Sir Robert Burbridge 2023-11-15 09:47:20 -05:00
parent 0ad6124c29
commit 0e6cd5e54b
35 changed files with 5145 additions and 6842 deletions

72
.eslintrc.cjs 100644
View File

@ -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',
},
],
},
};

29
.vimrc 100644
View File

@ -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

218
README.md
View File

@ -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."
}
```
```
# 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"
```
```
# Item.component.xml
<item>
<title>{item.title}</title>
<link>https://{item.link}</link>
<description>{item.description}</description>
</item>
```
```
# rss.template.xml
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>{feed.title}</title>
<link>https://{feed.link}</link>
<description>{feed.description}</description>
<Item data={feed.items[0]}/>
<Item data={feed.items[1]}/>
<Item data={feed.items[2]}/>
</channel>
</rss>
```
The toolkit will make it easy to weave these bits of data (both form data
and content data) together.
Here are some flows and dreamcode.
## Flows
Possible flows:
***Fragment-first***
```
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.
5. read rss.template.xml
6. read feed.json
7. transform feed.json into a desired format
8. produce output XML with values fixed.
9. integrate the two outputs for the final xml
10. optimize/minify
```
***Compilation-first***
```
1. read Item.component.html
2. read rss.template.xml
3. compile a master template
4. read rss.csv
5. read feed.json
6. compile a master data source
7. produce an output XML with values fixed.
8. optimize/minify
```
## 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')
console.log(
renderHtml(
interpolate(
transform(
parseHtml('<p>Hello, {name}!</p>'),
[],
),
await renderXmlWithJson(
readXmlTemplate('Item.template.xml'),
csvToJson(readCsv('rss.csv'))
)
)
{ name: 'Othello' },
)
)
);
```
### Markup
Running this script produces the following output:
See [the Markup documentation](./docs/markup.md).
```html
<p>Hello, Othello!</p>
```
### Components
## API
See [the Components documentation](./docs/components.md).
### renderHtml(ast)
### parseHtml(html)
### interpolate(ast, data)
### transform(ast, plugins)
Transform the AST according to plugins.
#### ast
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.
#### plugins
Plugins are pairs of functions.
The *selector* function
The *transformer* function
## Development
### VIM
This project contains a `.vimrc` file to help with development and
maintainance. See that file for more information and instructions for use.

View File

@ -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

View File

@ -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,
}
}
]
},
};

View File

@ -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"
}
]
]
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1680986409102" clover="3.2.0">
<project timestamp="1680986409102" name="All files">
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="0" coveredelements="0" complexity="0" loc="0" ncloc="0" packages="0" files="0" classes="0"/>
</project>
</coverage>

View File

@ -1 +0,0 @@
{}

View File

@ -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"}}
}

View File

@ -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;
}

View File

@ -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);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 B

View File

@ -1,101 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for All files</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="prettify.css" />
<link rel="stylesheet" href="base.css" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type='text/css'>
.coverage-summary .sorter {
background-image: url(sort-arrow-sprite.png);
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>All files</h1>
<div class='clearfix'>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Statements</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Branches</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Functions</span>
<span class='fraction'>0/0</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">Unknown% </span>
<span class="quiet">Lines</span>
<span class='fraction'>0/0</span>
</div>
</div>
<p class="quiet">
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
</p>
<template id="filterTemplate">
<div class="quiet">
Filter:
<input oninput="onInput()" type="search" id="fileSearch">
</div>
</template>
</div>
<div class='status-line medium'></div>
<div class="pad1">
<table class="coverage-summary">
<thead>
<tr>
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2023-04-08T20:40:09.092Z
</div>
<script src="prettify.js"></script>
<script>
window.onload = function () {
prettyPrint();
};
</script>
<script src="sorter.js"></script>
<script src="block-navigation.js"></script>
</body>
</html>

View File

@ -1 +0,0 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

View File

@ -1,196 +0,0 @@
/* eslint-disable */
var addSorting = (function() {
'use strict';
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() {
return document.querySelector('.coverage-summary');
}
// returns the thead element of the summary table
function getTableHeader() {
return getTable().querySelector('thead tr');
}
// returns the tbody element of the summary table
function getTableBody() {
return getTable().querySelector('tbody');
}
// returns the th element for nth column
function getNthColumn(n) {
return getTableHeader().querySelectorAll('th')[n];
}
function onFilterInput() {
const searchValue = document.getElementById('fileSearch').value;
const rows = document.getElementsByTagName('tbody')[0].children;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (
row.textContent
.toLowerCase()
.includes(searchValue.toLowerCase())
) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
}
// loads the search box
function addSearchBox() {
var template = document.getElementById('filterTemplate');
var templateClone = template.content.cloneNode(true);
templateClone.getElementById('fileSearch').oninput = onFilterInput;
template.parentElement.appendChild(templateClone);
}
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML =
colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function(a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function(a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc
? ' sorted-desc'
: ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function() {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i = 0; i < cols.length; i += 1) {
if (cols[i].sortable) {
// add the click event handler on the th so users
// dont have to click on those tiny arrows
el = getNthColumn(i).querySelector('.sorter').parentElement;
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function() {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData();
addSearchBox();
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

View File

@ -1,3 +1,5 @@
const module = "@thefarce/loom";
import htmlToAst from './src/html-to-ast.mjs';
export default module;
export default {
htmlToAst,
};

8
jest.config.js 100644
View File

@ -0,0 +1,8 @@
export default {
// The glob patterns Jest uses to detect test files
testMatch: [
'**/test/**/*.test.(mjs|js)',
],
};

10504
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +1,22 @@
{
"name": "@thefarce/loom",
"version": "0.0.1",
"description": "Weaving together data from many sources",
"version": "0.0.2",
"description": "A module for weaving form data with content data",
"type": "module",
"homepage": "https://bitbucket.org/thefarce/loom",
"main": "./index.mjs",
"main": "index.mjs",
"scripts": {
"clean": "rimraf ./reports",
"docs": "npx jsdoc --package ./package.json -c ./config/jsdoc.config.json -R README.md -t ./node_modules/ink-docstrap/template ./src",
"report:view": "xdg-open http://localhost:8000/reports && python3 -m http.server",
"test": "npm run test:lint; npm run test:unit; npm run test:spec; npm run test:coverage",
"test:lint": "npx eslint src --config ./config/eslint.config.cjs -f ./node_modules/eslint-html-reporter/reporter.js -o ./reports/style/index.html",
"test:lint.fix": "npx eslint src --config ./config/eslint.config.cjs -f ./node_modules/eslint-html-reporter/reporter.js -o ./reports/style/index.html --fix",
"test:unit": "cross-env NODE_ENV=test jest --testTimeout=10000 --config=./config/jest.test.config.json ./test ./spec",
"test:coverage": "cross-env NODE_ENV=test jest --coverage --config=./config/jest.test.config.json ./test ./spec && rm -rf ./reports/test-coverage && mv ./coverage ./reports/test-coverage",
"test:spec": "cross-env NODE_ENV=test jest --testTimeout=10000 --config=./config/jest.spec.config.json ./test/spec",
"validate": "npm run test:coverage"
"lint": "eslint '**/*.?js'",
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"repository": {
"type": "git",
"url": "git@bitbucket.com:thefarce/loom.git"
},
"keywords": [
"template",
"templating",
"website"
],
"author": "Sir Robert Burbridge",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@babel/preset-env": "^7.16.11",
"cross-env": "^7.0.3",
"eslint": "^8.13.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-html-reporter": "^0.7.4",
"eslint-plugin-import": "^2.26.0",
"ink-docstrap": "^1.3.2",
"jest": "^29.3.1",
"jest-html-reporter": "^3.4.2",
"jsdoc": "^3.6.10",
"lehre": "^1.5.0",
"rimraf": "^3.0.2"
"eslint": "^8.32.0",
"eslint-plugin-jest": "^27.2.1",
"jest": "^29.7.0"
},
"dependencies": {
"hast-util-from-html": "^1.0.1",
"hast-util-to-html": "^8.0.4",
"hastscript": "^7.2.0",
"unist-util-visit": "^4.1.2",
"xast-util-from-xml": "^3.0.0",
"xast-util-to-xml": "^3.0.2"
"hast-util-from-parse5": "^8.0.1",
"parse5": "^7.1.2",
"unist-util-visit-parents": "^6.0.1"
}
}

View File

@ -1,50 +0,0 @@
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};
}

View File

@ -1,47 +0,0 @@
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};
}

View File

@ -1,21 +0,0 @@
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(
`<LOOM-FRAGMENT>${html}</LOOM-FRAGMENT>`,
{depth: null}
);
return tree;
}

View File

@ -0,0 +1,9 @@
import { fromParse5 } from 'hast-util-from-parse5'
import { parse } from 'parse5'
export function parseHtml(source) {
const p5ast = parse(source, { sourceCodeLocationInfo: true })
return fromParse5(p5ast)
}
export default parseHtml;

View File

@ -0,0 +1,24 @@
import { visitParents } from 'unist-util-visit-parents';
export function replaceNode (tree, test, transformer) {
visitParents(tree, test, (node, ancestors) => {
const parent = ancestors[ancestors.length - 1]
if (!parent?.children?.length) {
return;
}
let position = -1;
parent.children.forEach((child, index) => {
if (child === node) {
position = index;
}
})
if (position) {
parent.children.splice(position, 1, transformer(node));
}
});
}
export default replaceNode;

13
test.js
View File

@ -1,13 +0,0 @@
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));
});

View File

@ -0,0 +1,9 @@
/**
* A Jest test suite for testing '../src/html-to-ast.mjs'
*/
describe("html-to-ast", () => {
it("should pass", () => {
expect(true).toBe(true);
});
});

View File

@ -1,3 +0,0 @@
<node>

View File

@ -1,16 +0,0 @@
<loom-module define="Component">
<span>component</span>
foo
</loom-module>
<div>
<h1>Title</h1>
<span class="foo" id="bar">testing</span>
<Component/>
</div>
<div>
<h1>Title</h1>
<span class="foo" id="bar">testing</span>
<Component/>
</div>

View File

@ -1,101 +0,0 @@
import * as fs from 'fs/promises'
import {fromXml} from 'xast-util-from-xml'
import { toXml } from 'xast-util-to-xml'
import { visit } from 'unist-util-visit'
async function readHtml (fn) {
return await fs.readFile(fn);
}
async function fileToAst () {
const html = await readHtml('./demo.html');
const tree = fromXml(`<LOOM-FRAGMENT>${html}</LOOM-FRAGMENT>`, {depth: null})
return tree;
}
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};
}
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(extractModules)
.then(applyModules)
.then(({ast,modules}) => {
console.log(toXml(ast.children[0].children))
})

View File

@ -1,16 +0,0 @@
import {h, s} from 'hastscript'
import {fromXml} from 'xast-util-from-xml';
const xml = `
<album id="123">
<name>Born in the U.S.A.</name>
<artist>Bruce Springsteen</artist>
<releasedate>1984-04-06</releasedate>
</album>
`
const tree = fromXml(xml);
console.dir(tree, {depth:null});