Skip to content

Commit

Permalink
+ optional markers
Browse files Browse the repository at this point in the history
  • Loading branch information
Lcfvs committed Dec 14, 2020
1 parent 2dd3905 commit 6d44fe1
Show file tree
Hide file tree
Showing 19 changed files with 114 additions and 271 deletions.
165 changes: 98 additions & 67 deletions lib/dom.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const parts = /(^<!doctype [^>]+>|)([\s\S]*)$/i
const identifier = /({[a-z][a-z\d]*(?:\.[a-z][a-z\d]*)*})/gi
const identifier = /({\??[a-z][a-z\d]*(?:\.[a-z][a-z\d]*)*})/gi

export const symbols = {
doctype: Symbol('dom.doctype'),
Expand All @@ -15,6 +15,64 @@ const proto = {
[symbols.window]: null
}

function attribute (template, node) {
const rules = identify(node.nodeValue)
const value = resolve(template, rules)

if (value === null) {
node.ownerElement.removeAttribute(node.nodeName)
} else {
node.nodeValue = value
}
}

function content (template, node) {
const rules = identify(node.textContent)
const value = resolve(template, rules)

if (value === null) {
node.remove()
} else if (typeof value === 'object') {
const { ownerDocument } = node
const container = ownerDocument.createDocumentFragment()
const nodes = [value].flat()

nodes.forEach(child => container.appendChild(render(child)))
node.parentNode.replaceChild(container, node)
} else {
node.textContent = value
}
}

function dom ({ DOMParser, NodeFilter }, source) {
let [, doctype, contents] = source.match(parts)
const parser = new DOMParser()
const fragment = parse(parser, doctype, contents)
const { ownerDocument } = fragment
const walker = ownerDocument.createTreeWalker(fragment, NodeFilter.SHOW_TEXT)

while (true) {
const node = walker.nextNode()

if (!node || node.parentNode.nodeName === 'TITLE') {
break
}

const matches = node.nodeValue.match(identifier) || []

contents = matches.reduce(rewrite, contents)
}

if (!doctype.length) {
contents = wrap(contents)
}

return {
doctype,
node: tree(parse(parser, doctype, contents))
}
}

function filler (template, node) {
const { [symbols.window]: window } = template
const { Node } = window
Expand All @@ -32,19 +90,24 @@ function filler (template, node) {

function fill (template) {
const { [symbols.node]: node, [symbols.window]: window } = template
const { document, Node } = window
const value = node.nodeType === Node.TEXT_NODE
? resolve(node.textContent, template)
: node.nodeValue.replace(identifier, content => resolve(content, template))

if (typeof value === 'object') {
const container = document.createDocumentFragment()
const nodes = [value].flat()

nodes.forEach(child => container.appendChild(render(child)))
node.parentNode.replaceChild(container, node)
if (node.nodeType === window.Node.TEXT_NODE) {
content(template, node)
} else {
node.textContent = value
attribute(template, node)
}
}

function identify (identifier) {
const rule = identifier.slice(1, -1)
const [first] = rule
const optional = first === '?'
const name = optional ? rule.slice(1) : rule

return {
identifier,
optional,
name
}
}

Expand Down Expand Up @@ -80,6 +143,19 @@ function map (node) {
return from(this, node)
}

function parse (parser, doctype, contents) {
const document = parser.parseFromString(`${doctype}${contents}`, 'text/html')

if (doctype.length) {
document.write(contents)

return document.documentElement
} else {
return document.createRange()
.createContextualFragment(contents)
}
}

function replace (template) {
const { [symbols.node]: node, [symbols.window]: window } = template
const { document } = window
Expand All @@ -101,17 +177,14 @@ function replace (template) {
parentNode.replaceChild(container, node)
}

function resolve (content, template) {
return content.slice(1, -1).split('.')
.reduce((data, name) => {
const { [name]: value = null } = data
function resolve (template, { identifier, optional, name }) {
const { [name]: value = null } = template

if (value === null) {
throw new Error(`Missing ${content} in \`${template[symbols.source]}\``)
}
if (value === null && !optional) {
throw new Error(`Missing ${identifier} in \`${template[symbols.source]}\``)
}

return value
}, template)
return value
}

function render (template) {
Expand All @@ -122,8 +195,8 @@ function render (template) {
.reduce(filler, clone)[symbols.node]
}

function wrap (source) {
return `<template>${source}</template>`
function rewrite (source, match) {
return source.replace(match, wrap(match))
}

function tree (fragment) {
Expand All @@ -139,50 +212,8 @@ function tree (fragment) {
return fragment
}

function rewrite (source, match) {
return source.replace(match, wrap(match))
}

function parse (parser, doctype, contents) {
const document = parser.parseFromString(`${doctype}${contents}`, 'text/html')

if (doctype.length) {
document.write(contents)

return document.documentElement
} else {
return document.createRange()
.createContextualFragment(contents)
}
}

function dom ({ DOMParser, NodeFilter }, source) {
let [, doctype, contents] = source.match(parts)
const parser = new DOMParser()
const fragment = parse(parser, doctype, contents)
const { ownerDocument } = fragment
const walker = ownerDocument.createTreeWalker(fragment, NodeFilter.SHOW_TEXT)

while (true) {
const node = walker.nextNode()

if (!node || node.parentNode.nodeName === 'TITLE') {
break
}

const matches = node.nodeValue.match(identifier) || []

contents = matches.reduce(rewrite, contents)
}

if (!doctype.length) {
contents = wrap(contents)
}

return {
doctype,
node: tree(parse(parser, doctype, contents))
}
function wrap (source) {
return `<template>${source}</template>`
}

export function template (window, source = '', { ...data } = {}) {
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lcf.vs/dom-engine",
"version": "1.1.4",
"version": "2.0.0",
"description": "A composable DOM based template engine",
"type": "module",
"main": "lib/engine.js",
Expand Down
17 changes: 14 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,27 @@ A composable DOM based template engine

## Usage

### Markers

There is 2 type of markers:

* **Required**: `{name}`
* The value can't be nullish
* **Optional**: `{?name}`
* If a nullish value is provided for an attribute, that attribute is removed


### Create a fragment template

```js
import { template } from '@lcf.vs/dom-engine/backend.js'

const pTemplate = template(`
<p>{salutations} {name}</p>
<p class="{?classes}">{salutations} {name}</p>
`, {
salutations: null, // required
name: '' //optional
classes: null,
salutations: null,
name: null
})
```

Expand Down
27 changes: 0 additions & 27 deletions test.js

This file was deleted.

12 changes: 0 additions & 12 deletions test/builders/page.js

This file was deleted.

5 changes: 0 additions & 5 deletions test/builders/partial.js

This file was deleted.

15 changes: 0 additions & 15 deletions test/builders/response.js

This file was deleted.

3 changes: 0 additions & 3 deletions test/builders/site.js

This file was deleted.

21 changes: 0 additions & 21 deletions test/builders/views/home.js

This file was deleted.

9 changes: 0 additions & 9 deletions test/templates/footer.js

This file was deleted.

32 changes: 0 additions & 32 deletions test/templates/layout.js

This file was deleted.

Loading

0 comments on commit 6d44fe1

Please sign in to comment.