├── .gitignore ├── logo.png ├── tests ├── extra │ ├── listchange.html │ ├── listchange.js │ └── roller.html └── testcases │ ├── test.css │ ├── test.js │ ├── jquery.template.js │ └── index.html ├── LICENSE ├── javascript ├── template.css ├── extra │ ├── zcall.roller.css │ └── zcall.roller.js ├── bin │ └── build ├── tokenize.js ├── prepare.js ├── template.bundle.module.min.js ├── template.bundle.min.js ├── template.js └── template.bundle.module.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdevelopers-eu/z-template/HEAD/logo.png -------------------------------------------------------------------------------- /tests/extra/listchange.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Z Template Tests 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/extra/listchange.js: -------------------------------------------------------------------------------- 1 | import { zTemplate as zTemplate } from '../../javascript/template.js'; 2 | 3 | const genVar = test(); 4 | document 5 | .addEventListener('click', (ev) => { 6 | if (ev.target.localName == 'button') { 7 | zTemplate(document.querySelector('#test'), genVar.next().value || {}); 8 | } 9 | }); 10 | 11 | function *test() { 12 | const list = [ 13 | {"id": 10, "value": "test1"}, 14 | {"id": 20, "value": "test2"}, 15 | {"id": 30, "value": "test3"} 16 | ]; 17 | yield {list}; 18 | 19 | list.shift(); 20 | list.push({"id": 40, "value": "test4"}); 21 | yield {list}; 22 | 23 | list.splice(1, 0, {"id": 50, "value": "test5"}); 24 | yield {list}; 25 | 26 | list.pop(); 27 | yield {list}; 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daniel Ševčík, www.webdevelopers.eu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/extra/roller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Z Template Tests 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

A

13 |

Z

14 |

Some Name Comes Here

15 |

Name

16 |

17 |

From Something to Nothing1 18 |

19 |

0.000

20 |

0.000

21 |
22 | 23 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /javascript/template.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --z-anim-speed: 0.2s; 3 | } 4 | 5 | .z-no-anim { 6 | --z-anim-speed: 0s; 7 | } 8 | 9 | .z-template-hidden, 10 | .dna-template-hidden, 11 | [template] { 12 | display: none !important; 13 | } 14 | 15 | /* Nonessential */ 16 | /* Using :where() because of 0 specificity */ 17 | *:where([z-content-rev], .z-fade-in, .dna-template-visible), 18 | *:where([template-clone], [z-removed]) { 19 | animation-timing-function: ease-in; 20 | animation-duration: var(--z-anim-speed, 0.32s); 21 | animation-fill-mode: forwards; 22 | transform-origin: center center; 23 | } 24 | 25 | *:where([z-content-rev], .z-fade-in, .dna-template-visible) { 26 | animation-name: z-fade-in; 27 | } 28 | 29 | *:where([template-clone]), .z-slide-in { 30 | animation-name: z-slide-in; 31 | } 32 | 33 | *:where([z-removed]), .z-slide-out { 34 | animation-name: z-slide-out; 35 | } 36 | 37 | @keyframes z-fade-in { 38 | 0% { 39 | opacity: 0; 40 | } 41 | 100% { 42 | opacity: 1; 43 | } 44 | } 45 | 46 | @keyframes z-slide-in { 47 | 0% { 48 | opacity: 0; 49 | transform: translateY(1em); 50 | } 51 | 100% { 52 | opacity: 1; 53 | transform: translateY(0px); 54 | } 55 | } 56 | 57 | @keyframes z-slide-out { 58 | 0% { 59 | opacity: 1; 60 | transform: translateX(0%); 61 | } 62 | 100% { 63 | opacity: 0; 64 | transform: translateX(100%); 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /javascript/extra/zcall.roller.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Z Template 3 | * @author Daniel Sevcik 4 | * @copyright 2023 Daniel Sevcik 5 | * @license MIT License 6 | */ 7 | /* element if it has .z-roller child */ 8 | .z-roller-rolling:has(span) { 9 | overflow: hidden; 10 | display: run-in block; 11 | } 12 | 13 | .z-roller { 14 | clip-path: inset(0 0 0 0); 15 | vertical-align: bottom; 16 | display: inline-block; 17 | position: relative; 18 | } 19 | 20 | .z-roller.z-roller-animate > .z-roller-letter { 21 | visibility: hidden; 22 | } 23 | 24 | .z-roller:before { 25 | content: attr(data-z-face); 26 | display: block; 27 | white-space: pre; 28 | line-height: inherit; 29 | animation-iteration-count: 1; 30 | animation-fill-mode: forwards; 31 | animation-timing-function: ease-in-out; 32 | animation-duration: var(--z-roller-speed, 1000ms); 33 | animation-delay: var(--z-roller-delay, 0ms); 34 | position: absolute; 35 | line-height: var(--z-roller-line-height, 1.2em); 36 | } 37 | 38 | .z-roller:not(.z-roller-animate):before { 39 | visibility: hidden; 40 | } 41 | 42 | .z-roller-up:before { 43 | transform: translateY(0); 44 | top: 0; 45 | } 46 | 47 | .z-roller-down:before { 48 | transform: translateY(0%); 49 | bottom: 0; 50 | } 51 | 52 | .z-roller-animate.z-roller-up:before { 53 | animation-name: z-roller-up; 54 | } 55 | 56 | .z-roller-animate.z-roller-down:before { 57 | animation-name: z-roller-down; 58 | } 59 | 60 | @keyframes z-roller-down { 61 | 0% { 62 | transform: translateY(0%); 63 | } 64 | 100% { 65 | transform: translateY(calc(100% - var(--z-roller-line-height, 1.2em))); 66 | } 67 | } 68 | 69 | @keyframes z-roller-up { 70 | 0% { 71 | transform: translateY(0%); 72 | } 73 | 100% { 74 | transform: translateY(calc(-100% + var(--z-roller-line-height, 1.2em))); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /javascript/bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used to build one template.bundle.min.js that contains all the templates. 4 | pushd `dirname "$0"` > /dev/null 2>&1; 5 | trap "popd > /dev/null" EXIT; 6 | 7 | BUNDLE=( 8 | "../tokenize.js" 9 | "../prepare.js" 10 | "../template.js" 11 | ); 12 | 13 | # Print all up to a line having keyword @INSERT 14 | echo "Generating template.bundle.js..."; 15 | ( 16 | echo "/*! Z Template | (c) Daniel Sevcik | MIT License | https://github.com/webdevelopers-eu/z-template | build `date -u -Iseconds` */"; 17 | echo "window.zTemplate = (function() {"; 18 | cat "${BUNDLE[@]}" | grep -vE "^\s*(import|export|module)\s"; 19 | echo " return zTemplate;})();"; 20 | ) > ../template.bundle.js 21 | 22 | 23 | # Print all up to a line having keyword @INSERT 24 | echo "Generating template.bundle.module.js..."; 25 | ( 26 | echo "/*! Z Template | (c) Daniel Sevcik | MIT License | https://github.com/webdevelopers-eu/z-template | build `date -u -Iseconds` */"; 27 | cat "${BUNDLE[@]}" | grep -vE "^\s*import|export\s"; 28 | echo -n "export default zTemplate;"; 29 | ) > ../template.bundle.module.js 30 | 31 | #cat ../template.bundle.js | uglifyjs --source-map "url='template.bundle.min.js.map'" --comments "/^!/" -o ../template.bundle.min.js --compress --mangle --toplevel --mangle-props reserved=['zTemplate']; 32 | echo "Minifying template.bundle.js..."; 33 | babel-minify ../template.bundle.js --mangle --simplify --booleans --memberExpressions --mergeVars --numericLiterals --propertyLiterals -o ../template.bundle.min.js; 34 | 35 | echo "Minifying template.bundle.module.js..."; 36 | babel-minify ../template.bundle.module.js --mangle --simplify --booleans --memberExpressions --mergeVars --numericLiterals --propertyLiterals --sourceType module -o ../template.bundle.module.min.js; 37 | 38 | ls -lah `readlink -f ../template.bundle.min.js`; 39 | ls -lah `readlink -f ../template.bundle.module.min.js`; 40 | 41 | popd > /dev/null; 42 | trap "" EXIT; 43 | -------------------------------------------------------------------------------- /tests/testcases/test.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: 'Roboto', Arial, sans-serif; 3 | } 4 | 5 | .result { 6 | font-size: 1.5em; 7 | } 8 | 9 | .result:before { 10 | content: 'Result: '; 11 | font-weight: bold; 12 | } 13 | 14 | #tests { 15 | margin: 2em 0em; 16 | padding: 0em; 17 | list-style-type: none; 18 | list-style: none; 19 | position: relative; 20 | } 21 | 22 | #tests > li { 23 | display: flex; 24 | flex-direction: row; 25 | flex-wrap: wrap; 26 | align-items: stretch; 27 | counter-increment: test; 28 | } 29 | 30 | #tests > li:before { 31 | content: "Test #" counter(test); 32 | display: block; 33 | background: #eee; 34 | padding: 0.5em; 35 | color: #333; 36 | cursor: pointer; 37 | } 38 | 39 | #tests > li.pass:before { 40 | content: "✅ Test #" counter(test); 41 | } 42 | 43 | #tests > li.fail:before { 44 | content: "❌ Test #" counter(test); 45 | } 46 | 47 | #tests > li > * { 48 | border: 1px dotted black; 49 | padding: 1em 1em; 50 | flex: 1; 51 | min-width: 360px; 52 | } 53 | 54 | #tests > li > *:first-child { 55 | border-right: 1px dotted black 56 | } 57 | 58 | #tests > li > *:before { 59 | display: block; 60 | border-bottom: 1px dotted black; 61 | } 62 | 63 | #tests .actions { 64 | flex: 0; 65 | min-width: auto; 66 | white-space: nowrap; 67 | } 68 | 69 | #tests > li > article:first-of-type:before { 70 | content: "Result"; 71 | } 72 | 73 | #tests > li > article:last-of-type:before { 74 | content: "Expected"; 75 | } 76 | 77 | 78 | #tests > li.fail:before, 79 | #tests > li.fail > * { 80 | background: #FCC; 81 | } 82 | 83 | #tests > li.pass:before, 84 | #tests > li.pass > * { 85 | background: #CFC; 86 | } 87 | 88 | #tests > li.pass:not(.active) > * { 89 | display: none; 90 | } 91 | 92 | #tests > li:hover:before { 93 | background: #FFC; 94 | } 95 | 96 | #tests > li > *[data-source]::after { 97 | content: attr(data-source); 98 | display: block; 99 | font-family: monospace; 100 | font-size: 0.8em; 101 | background: #eee; 102 | margin-top: 1em; 103 | padding: 0.5em; 104 | color: #333; 105 | white-space: pre; 106 | overflow: auto; 107 | } 108 | 109 | /* Center message box in the middle of the screen */ 110 | .message { 111 | position: fixed; 112 | top: 50%; 113 | left: 50%; 114 | transform: translate(-50%, -50%); 115 | background: #eee; 116 | padding: 1em; 117 | border: 1px solid black; 118 | border-radius: 0.5em; 119 | text-align: center; 120 | } 121 | -------------------------------------------------------------------------------- /javascript/extra/zcall.roller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Z Template 3 | * @author Daniel Sevcik 4 | * @copyright 2023 Daniel Sevcik 5 | * @license MIT 6 | * 7 | * This is a JavaScript callback function for the Z-Template plugin 8 | * that creates a rolling letters effect, transforming one text into 9 | * another by replacing each character one-by-one with a set speed and 10 | * delay. 11 | * 12 | * It will register the "roller" as global default callback function. 13 | * 14 | * Using it like this: 15 | * 16 | * Original Value 17 | * Original Value 18 | * Original Value 19 | * 20 | * You must include also zcall.roller.css in your page. 21 | */ 22 | zTemplate.callbacks 23 | .set('roller', function(element, detail) { 24 | const document = element.ownerDocument; 25 | const speed = detail.arguments[0] || 1000; 26 | const delay = detail.arguments[1] || 100; 27 | // Convert value into string 28 | const sourceText = element.textContent + ''; 29 | const targetText = detail.value + ''; 30 | const len = Math.max(sourceText.length, targetText.length); 31 | const frag = document.createDocumentFragment(); 32 | const height = element.getBoundingClientRect().height; 33 | 34 | if (sourceText === targetText || sourceText.length === 0) { 35 | element.textContent = targetText; 36 | return; 37 | } 38 | 39 | // Set css variable --z-roller-speed 40 | element.classList.add('z-roller-rolling'); 41 | 42 | for (let i = 0; i < len; i++) { 43 | const sourceChar = sourceText[i] || ''; 44 | const targetChar = targetText[i] || ''; 45 | 46 | const div = frag.appendChild(document.createElement('div')); 47 | div.setAttribute('data-target', targetChar); 48 | const {direction, chars} = generateRollerChars(sourceChar, targetChar); 49 | if (direction === 'up') { 50 | div.classList.add('z-roller-up', 'z-roller'); 51 | } else { 52 | div.classList.add('z-roller-down', 'z-roller'); 53 | } 54 | 55 | const charSpan = div.appendChild(document.createElement('span')); 56 | charSpan.classList.add('z-roller-letter'); 57 | charSpan.textContent = sourceChar; 58 | 59 | // We use :before to avoid multiplying the textContents 60 | // when multiple callbacks are applied in short succession 61 | div.style.setProperty('--z-roller-speed', speed + 'ms'); 62 | div.style.setProperty('--z-roller-line-height', height + 'px'); 63 | div.setAttribute('data-z-face', chars.join("\n")); 64 | 65 | } 66 | 67 | element.replaceChildren(frag); 68 | for (i = 0; i < element.childElementCount; i++) { 69 | // Get i-th child element 70 | const child = element.children[i]; 71 | const charDelay = delay * i; 72 | child.style.setProperty('--z-roller-delay', charDelay + 'ms'); 73 | child.classList.add('z-roller-animate'); 74 | setTimeout(() => child.replaceWith(child.getAttribute('data-target')), charDelay + speed); 75 | } 76 | 77 | // Generate array of characters between two characters 78 | function generateRollerChars(fromChar, toChar) { 79 | let fromCode = (fromChar || ' ').charCodeAt(0); 80 | let toCode = (toChar || ' ').charCodeAt(0); 81 | const len = Math.abs(fromCode - toCode); 82 | const chars = []; 83 | 84 | const direction = fromCode < toCode ? 'up' : 'down'; 85 | if (direction === 'down') { 86 | [fromCode, toCode] = [toCode, fromCode]; 87 | } 88 | 89 | for (let i = 0; i <= len; i++) { 90 | chars.push(String.fromCharCode(fromCode + i)); 91 | } 92 | return {direction, chars}; 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /javascript/tokenize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * @module ZTemplate 5 | * @author Daniel Sevcik 6 | * @copyright 2023 Daniel Sevcik 7 | * @since 2023-01-23 22:01:46 UTC 8 | * @access public 9 | */ 10 | class Tokenizer extends Array { 11 | #operatorChars = ["!", "=", "<", ">", "~", "|", "&" ]; 12 | #quoteChars = ["'", '"']; 13 | #blockChars = {"{": "}", "[": "]", "(": ")"}; 14 | #hardSeparatorChars = [","]; 15 | #softSeparatorChars = [" ", "\t", "\r", "\n"]; 16 | #input; 17 | #pointer = 0; 18 | 19 | constructor(input) { 20 | super(); 21 | this.#input = (input || "") + ""; // convert all into string 22 | } 23 | 24 | 25 | tokenize() { 26 | // Cycle parse() and add values until it returns null 27 | while (!this.#endOfInput()) { 28 | const command = this.#tokenizeUntil(); 29 | if (command === null) { 30 | break; 31 | } 32 | this.push(command); 33 | } 34 | } 35 | 36 | #endOfInput() { 37 | return this.#pointer >= this.#input.length; 38 | } 39 | 40 | #next() { 41 | if (this.#endOfInput()) { 42 | return null; 43 | } 44 | return this.#input[this.#pointer++]; 45 | } 46 | 47 | #prev() { 48 | if (this.#pointer <= 0) { 49 | return null; 50 | } 51 | return this.#input[--this.#pointer]; 52 | } 53 | 54 | #tokenizeUntil(until=",") { 55 | const result = new Tokenizer(this.#input); 56 | let current = {type: "generic", value: ""}; 57 | let escaped = false; 58 | 59 | for (let value = this.#next(); value !== null; value = this.#next()) { 60 | if (escaped || (current.type == "text" && value != current.delimiter)) { // Inside string or escaped current 61 | current.value += value; 62 | escaped = false; 63 | } else if (value === '\\') { // Escape next 64 | escaped = true; 65 | } else if (current.type == "text" && value == current.delimiter) { // End of string 66 | this.#pushSmart(result, current); 67 | current = {type: "generic", value: ""}; 68 | } else if (value === until) { // End of subrequest 69 | break; 70 | } else if (value in this.#blockChars) {// Statement start 71 | this.#pushSmart(result, current); 72 | result.push({"type": "block", "value": this.#tokenizeUntil(this.#blockChars[value]), "start": value, "end": this.#blockChars[value]}); 73 | current = {type: "generic", value: ""}; 74 | } else if (this.#quoteChars.includes(value)) { // String start 75 | this.#pushSmart(result, current); 76 | current = {type: "text", value: "", delimiter: value}; 77 | } else if (current.type == 'generic' && this.#hardSeparatorChars.includes(value)) { // Significant separator 78 | this.#pushSmart(result, current); 79 | this.#pushSmart(result, {type: "separator", value: value}); 80 | current = {"type": 'generic', "value": ""}; 81 | } else if (current.type == 'generic' && this.#softSeparatorChars.includes(value)) { // Insignificant separator 82 | this.#pushSmart(result, current); 83 | current = {"type": 'generic', "value": ""}; 84 | } else if (current.type !== 'operator' && this.#operatorChars.includes(value)) { // Operator 85 | this.#pushSmart(result, current); 86 | current = {"type": 'operator', "value": value}; 87 | } else if (current.type === 'operator' && !this.#operatorChars.includes(value)) { // End of operator 88 | this.#pushSmart(result, current); 89 | current = {"type": "generic", "value": ""}; 90 | this.#prev(); 91 | } else { 92 | current.value += value; 93 | } 94 | } 95 | this.#pushSmart(result, current); 96 | return result; 97 | } 98 | 99 | #pushSmart(result, current) { 100 | if (current.type == 'generic') { 101 | current.value = current.value.trim(); 102 | if (!current.value.length) { 103 | return; 104 | } 105 | // If numeric then it is not a generic but 'text' 106 | if (!isNaN(current.value)) { 107 | current.type = 'text'; 108 | current.value = new Number(current.value); 109 | } 110 | } 111 | 112 | result.push(current); 113 | } 114 | } 115 | 116 | function tokenize(input) { 117 | const result = new Tokenizer(input); 118 | result.tokenize(); 119 | return result; 120 | } 121 | 122 | export { tokenize }; 123 | -------------------------------------------------------------------------------- /tests/testcases/test.js: -------------------------------------------------------------------------------- 1 | // Import zTemplate into zTemplateImport variable 2 | import { zTemplate as zTemplateModule } from '../../javascript/template.js'; 3 | 4 | const headerVars = { 5 | "bundle": location.href.match(/bundle/) ? true : false, 6 | "pass": true, 7 | }; 8 | 9 | if (!headerVars.bundle) { 10 | window.zTemplate = zTemplateModule; 11 | } 12 | 13 | const callbacks = { 14 | onTest1: (element, detail) => { 15 | console.log('onTest1', element, detail); 16 | element.setAttribute('data-test1', JSON.stringify(detail)); 17 | }, 18 | onTest2: (element, detail) => { 19 | console.log('onTest2', element, detail); 20 | element.setAttribute('data-test2', JSON.stringify(detail)); 21 | }, 22 | cbTest2: (element, detail) => { // this should overwrite the global default 23 | console.log('cbTest2', element, detail); 24 | $(element).text('Local callback cbTest2: ' + JSON.stringify(detail)); 25 | }, 26 | }; 27 | 28 | zTemplate.callbacks.set('cbTest1', (element, detail) => { 29 | console.log('cbTest1', element, detail); 30 | $(element).text('Default callback cbTest1: ' + JSON.stringify(detail)); 31 | }); 32 | zTemplate.callbacks.set('cbTest2', (element, detail) => { 33 | console.log('cbTest1', element, detail); 34 | $(element).text('Default callback cbTest2: ' + JSON.stringify(detail)); 35 | }); 36 | 37 | 38 | // Listen on #tests for a custom new Event('test1') event dispatched on the child element. 39 | // When the event is dispatched, the callback will be called with the element and the event detail. 40 | $('#tests') 41 | .on('printargs', function(event) { 42 | console.log('printargs', event, event.originalEvent.detail); 43 | $(event.target).text(JSON.stringify(event.originalEvent.detail)); 44 | }) 45 | .on('click', '> li', function(event) { 46 | $(this).toggleClass('active'); 47 | }) 48 | .on('click', 'article', (event) => { 49 | // copy the innerHTML of the clicked element to the clipboard 50 | const text = event.currentTarget.innerHTML; 51 | const input = document.createElement('textarea'); 52 | input.textContent = text; 53 | document.body.appendChild(input); 54 | input.select(); 55 | document.execCommand('copy'); 56 | document.body.removeChild(input); 57 | 58 | // Show quick message 59 | const message = document.createElement('div'); 60 | message.classList.add('message'); 61 | message.innerHTML = 'Copied to clipboard'; 62 | document.body.appendChild(message); 63 | setTimeout(() => { 64 | message.remove(); 65 | }, 1000); 66 | return false; 67 | }) 68 | .on('test1 test2', (event) => { 69 | console.log('EVENT jQuery test1', event); 70 | }); 71 | 72 | document.querySelector('#tests') 73 | .addEventListener('test1', (event) => { 74 | console.log('EVENT Native test1', event); 75 | event.target.setAttribute('data-event-test1', JSON.stringify(event.detail)); 76 | }, false); 77 | 78 | document.querySelector('#tests') 79 | .addEventListener('test2', (event) => { 80 | console.log('EVENT Native test2', event); 81 | event.target.setAttribute('data-event-test2', JSON.stringify(event.detail)); 82 | }, false); 83 | 84 | const onlyTest = location.href.match(/test=(?\d+)/)?.groups?.idx; 85 | document.querySelectorAll('#tests > li > *:first-child') 86 | .forEach((test1, idx) => { 87 | if (onlyTest != null) { 88 | if (onlyTest != idx) { 89 | test1.parentElement.remove(); 90 | } else { 91 | $(test1.parentElement).append(``); 92 | } 93 | } else { 94 | $(test1.parentElement).append(``); 95 | } 96 | const data = JSON.parse(test1.parentNode.getAttribute('data') || '{}'); 97 | // get following
element if any 98 | let test2 = test1.nextElementSibling?.localName === 'article' ? test1.nextElementSibling : null; 99 | 100 | if (!test2) { // Result not created yet, use old Z Template 1.0 to generate it 101 | test2 = test1.parentNode.appendChild(test1.cloneNode(true)); 102 | $(test2).template(data); 103 | } 104 | 105 | for (let i = 0; i < 2; i++) { // try twice 106 | console.log('Test round %s for %o with data %o', i + 1, test1.parentNode, data); 107 | zTemplate(test1, data, callbacks); 108 | 109 | // the z-content-rev attribute is added after the template is rendered to trigger CSS anim restart. 110 | // so we wait with the result evaluation until the attribute is added. 111 | setTimeout(() => { 112 | const html1 = serialize(test1); 113 | const html2 = serialize(test2); 114 | test1.setAttribute('data-source', html1); 115 | test2.setAttribute('data-source', html2); 116 | 117 | 118 | if (html1 !== html2) { 119 | headerVars.pass = false; 120 | test1.parentNode.classList.add('fail'); 121 | test1.parentNode.classList.remove('pass'); 122 | console.warn('Fail:', test1.parentNode, data); 123 | console.warn('Produced:', html1); 124 | console.warn('Expected:', html2); 125 | } else { 126 | test1.parentNode.classList.add('pass'); 127 | } 128 | }, 10); 129 | } 130 | 131 | function serialize(el) { 132 | const div = document.createElement('div'); 133 | const clone = el.cloneNode(true); 134 | clone.removeAttribute('data-source'); 135 | div.appendChild(clone); 136 | 137 | const attrs = div.ownerDocument.evaluate('//@*[starts-with(name(), "z-var-content")]', div, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); 138 | for (let i = 0; i < attrs.snapshotLength; i++) { 139 | const attr = attrs.snapshotItem(i); 140 | attr.ownerElement.removeAttribute(attr.nodeName); 141 | } 142 | return div.innerHTML.replace(/^\s*\n/gm, "").replace(/^\s+/gm, ""); 143 | }; 144 | 145 | }); 146 | 147 | 148 | zTemplate(document.querySelector('#header'), headerVars); 149 | -------------------------------------------------------------------------------- /javascript/prepare.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * @module ZTemplate 5 | * @author Daniel Sevcik 6 | * @copyright 2023 Daniel Sevcik 7 | * @since 2023-01-23 22:02:02 UTC 8 | * @access public 9 | */ 10 | class Preparator { 11 | #vars; 12 | #tokens; 13 | #paramShortcuts = { 14 | "@*": "attr", 15 | ":*": "event", 16 | "**": "call", 17 | ".*": "class", 18 | "+": "html", 19 | ".": "text", 20 | "=": "value", 21 | "?": "toggle", 22 | "!": "remove", 23 | "`": "debugger" 24 | }; 25 | 26 | #operatorsCompare = [ 27 | '==', 28 | '!=', 29 | '>', 30 | '>=', 31 | '<', 32 | '<=', 33 | ]; 34 | 35 | #operatorsBoolean = [ 36 | '!', 37 | '&&', 38 | '||' 39 | ]; 40 | 41 | #data = { 42 | "negateValue": 0, // int how many '!' operators are in front of the expression 43 | "variable": null, // variable name 44 | "value": null, // evaluated value 45 | "valueBool": null, // evaluated value 46 | "action": "", // string 47 | "param": null, // string 48 | "arguments": [], // array of optional arguments 49 | "condition": null, // evaluated {} condition 50 | } 51 | 52 | constructor(tokens, vars) { 53 | if (typeof vars !== 'object') { 54 | throw new Error(`The variables must be an object. Current argument: ${typeof vars}.`); 55 | } 56 | this.#vars = vars; 57 | this.#tokens = tokens; 58 | this.#normalize(); 59 | } 60 | 61 | /** 62 | * Normalize this.#tokens so it has fixed elements that reflect the syntax like this: 63 | * 64 | * The syntax is: 65 | * 66 | * VALUE ACTION CONDITION 67 | * 68 | * VALUE = [![!]] ( VARIABLE_NAME | { EXPRESSION } ) 69 | * ACTION = ACTION_NAME [ ? PARAM [ ? (...ARGUMENTS) ] ] 70 | * CONDITION = { EXPRESSION } 71 | * 72 | * @access private 73 | * @return void 74 | */ 75 | #normalize() { 76 | const tokens = Array.from(this.#tokens); 77 | let token = this.#nextToken(tokens, ["operator", "generic", "block", "text"]); 78 | 79 | // negate 80 | if (token.type === 'operator' && ["!", "!!"].includes(token.value)) { 81 | this.#data.negateValue = token.value.length; 82 | token = this.#nextToken(tokens, ["generic", "block", "text"]); 83 | } 84 | 85 | // value 86 | if (token.type === 'generic') { 87 | this.#data.variable = token.value; 88 | this.#data.value = this.#getVariableValue(token.value); 89 | } else if (token.type === 'block') { 90 | this.#data.value = this.#prepareBlock(token.value); 91 | } else { 92 | this.#data.value = token.value; 93 | } 94 | token = this.#nextToken(tokens, ["generic", "operator"]); 95 | 96 | // action shortcut 97 | const shortcutType = token.value.substr(0, 1) + (token.value.length > 1 ? '*' : ''); 98 | if (typeof this.#paramShortcuts[shortcutType] != 'undefined') { // shortcut 99 | this.#data.action = this.#paramShortcuts[shortcutType]; 100 | tokens.unshift({"type": "text", "value": token.value.substr(1), "info": "Extracted from shortcut"}); 101 | } else if (Object.values(this.#paramShortcuts).includes(token.value)) { 102 | this.#data.action = token.value; 103 | } else { 104 | throw new Error(`Invalid action: ${token.value} . Supported actions: ${Object.keys(this.#paramShortcuts).join(', ')}`); 105 | } 106 | token = this.#nextToken(tokens, ["text", "generic", null], ["block", "operator"]); // param is optional, we may encounter next "block" or "operator" instead. 107 | 108 | // param 109 | this.#data.param = token?.value; 110 | token = this.#nextToken(tokens, ["block", "operator", null]); 111 | 112 | // arguments 113 | if (token?.type === 'block' && token.start == '(') { // Enclosed in '(' and ')' => arguments 114 | this.#data.arguments = this.#prepareArguments(token.value); 115 | token = this.#nextToken(tokens, ["block", "operator", null]); 116 | } 117 | 118 | // negate 119 | let negateCondition = 0; 120 | if (token?.type === 'operator' && ["!", "!!"].includes(token.value)) { 121 | negateCondition = token.value.length; 122 | token = this.#nextToken(tokens, ["block", null]); 123 | } 124 | 125 | // condition 126 | if (token?.type === 'block' && token.start == '{') { 127 | this.#data.condition = this.#prepareBlock(token.value); 128 | token = this.#nextToken(tokens, ["block", null]); 129 | } else { 130 | this.#data.condition = true; 131 | } 132 | this.#data.condition = this.#negate(this.#data.condition, negateCondition); 133 | 134 | if (this.#data.action === 'debugger' && this.#data.valueBool) { 135 | debugger; 136 | } 137 | } 138 | 139 | #getTokenValue(token) { 140 | if (token.type === 'generic') { 141 | return this.#getVariableValue(token.value); 142 | } else if (token.type === 'block') { 143 | return this.#prepareBlock(token.value); 144 | } else { 145 | return token.value; 146 | } 147 | } 148 | 149 | #getVariableValue(variable) { 150 | const parts = variable.split('.'); 151 | 152 | // reserved keywords 153 | switch(parts[0]) { 154 | case 'true': 155 | case 'always': 156 | return true; 157 | case 'false': 158 | case 'never': 159 | return false; 160 | case 'null': 161 | case 'none': 162 | return null; 163 | case 'undefined': 164 | case 'z': 165 | return undefined; 166 | } 167 | 168 | // Split the variable into parts separated by dot and get the corresponding value from this.#vars object. 169 | // Example: "user.name" => this.#vars.user.name 170 | let value = this.#vars; 171 | for (let i = 0; i < parts.length; i++) { 172 | if (typeof value[parts[i]] === 'undefined') { 173 | console.warn("Can't find variable " + variable + " in data source %o", this.#vars); 174 | return null; 175 | } 176 | value = value[parts[i]]; 177 | } 178 | return value; 179 | } 180 | 181 | prepare() { 182 | const result = {...this.#data}; 183 | 184 | // Before resolving conditions 185 | if (result.condition.type === 'special') { 186 | result.action = result.condition.value; 187 | result.condition = true; 188 | return result; 189 | } else if (result.value === null && result.condition.type == "generic") { 190 | result.value = this.#toValue(result.condition, result.negateValue); 191 | } else if (result.value !== null && typeof result.value == 'object') { 192 | result.value = this.#toValue(result.value, result.negateValue); 193 | } else { 194 | result.value = this.#negate(result.value, result.negateValue); 195 | } 196 | result.valueBool = this.#toBool(result.value); 197 | 198 | // Resolve conditions 199 | switch(result.condition.type) { 200 | case "block": 201 | result.condition = !!this.#prepareBlock(result.condition.value); 202 | break; 203 | case "generic": 204 | result.condition = !!this.#prepareVariable(result.condition.value); 205 | break; 206 | } 207 | 208 | return result; 209 | } 210 | 211 | #prepareArguments(tokens) { 212 | let args = []; 213 | let expr = []; 214 | 215 | for (let i = 0; i < tokens.length; i++) { 216 | const token = tokens[i]; 217 | if (token.type === 'separator') { 218 | args.push(expr); 219 | expr = []; 220 | } else { 221 | expr.push(token); 222 | } 223 | } 224 | args.push(expr); 225 | 226 | args = args.map(arg => arg.length == 1 ? this.#getTokenValue(arg[0]) : this.#getTokenValue({"type": "block", "value": arg})); 227 | return args; 228 | } 229 | 230 | #prepareVariable(varName) { 231 | const val = this.#getVariableValue(varName); 232 | if (val === null) { 233 | return null; 234 | } 235 | return this.#toBool(val); 236 | } 237 | 238 | #toBool(val) { 239 | switch (typeof val) { 240 | case "string": 241 | return val.length !== 0; 242 | case "object": 243 | return val === null ? false : Object.keys(val).length !== 0; 244 | case "number": 245 | return val !== 0; 246 | case "boolean": 247 | return val; 248 | default: 249 | return false; 250 | } 251 | } 252 | 253 | #prepareBlock(tokens) { 254 | const expression = this.#mkExpression(tokens); 255 | // console.log("expression", expression); 256 | return eval(expression); 257 | } 258 | 259 | #mkExpression(tokens) { 260 | // We could do eval of the whole expression with vars but it's not 261 | // safe and it's not needed and we want it to be portable to other langs. 262 | let result = ''; 263 | while (true) { 264 | const token = this.#nextToken(tokens, ["generic", "block", "text", "operator", null]); 265 | if (!token) { 266 | break; 267 | } 268 | 269 | // Arithmetic operators look ahead 270 | if (tokens.length > 1 && tokens[0].type === 'operator' && this.#operatorsCompare.includes(tokens[0].value)) { 271 | const operator = this.#nextToken(tokens, ["operator"]); 272 | const token2 = this.#nextToken(tokens, ["generic", "text"]); 273 | result += this.#compare(token, operator, token2) ? 1 : 0; 274 | continue; 275 | } 276 | 277 | switch (token.type) { 278 | case "operator": 279 | if (this.#operatorsBoolean.includes(token.value)) { 280 | result += token.value; 281 | } else { 282 | throw new Error(`Invalid operator: ${token.value}. Supported operators: ${this.#operatorsBoolean.join(', ')}`); 283 | } 284 | break; 285 | case "block": 286 | result += this.#mkExpression(token.value); 287 | break; 288 | case "generic": 289 | result += this.#prepareVariable(token.value) ? 1 : 0; 290 | break; 291 | case "text": 292 | result += this.#toBool(token.value) ? 1 : 0; 293 | break; 294 | } 295 | } 296 | return "(" + (result || '0') + ")"; 297 | } 298 | 299 | #toValue(token, negate = 0) { 300 | let value = token; 301 | 302 | if (typeof token?.type !== 'undefined' && typeof token?.value !== 'undefined') { // @todo we should use Token class instead of Object 303 | switch (token.type) { 304 | case "generic": 305 | value = this.#getVariableValue(token.value); 306 | break; 307 | case "text": 308 | value = token.value; 309 | break; 310 | case "block": 311 | value = this.#prepareBlock(token.value); 312 | break; 313 | default: 314 | throw new Error(`Invalid token type: ${token.type} (value: ${JSON.stringify(token)}).`); 315 | } 316 | } 317 | 318 | if (value === null) { 319 | return value; // Number(null) === 0 320 | } else if (value instanceof Array) { 321 | value = value.length; 322 | } else if (value instanceof Number) { 323 | value = value.valueOf(); 324 | } else if (typeof value === 'object') { 325 | value = Object.keys(value).length; 326 | } 327 | 328 | const num = Number(value); 329 | let ret = isNaN(num) ? value : num.valueOf(); 330 | 331 | ret = this.#negate(ret, negate); 332 | return ret; 333 | } 334 | 335 | #compare(token1, operator, token2) { 336 | const val1 = this.#toValue(token1); 337 | const val2 = this.#toValue(token2); 338 | 339 | switch (operator.value) { 340 | case '==': 341 | return val1 === val2 ? 1 : 0; 342 | case '!=': 343 | return val1 !== val2 ? 1 : 0; 344 | case '<': 345 | return val1 < val2 ? 1 : 0; 346 | case '<=': 347 | return val1 <= val2 ? 1 : 0; 348 | case '>': 349 | return val1 > val2 ? 1 : 0; 350 | case '>=': 351 | return val1 >= val2 ? 1 : 0; 352 | default: 353 | throw new Error(`Invalid operator: ${operator.value} . Supported operators: ${this.#operatorsCompare.join(', ')}`); 354 | } 355 | } 356 | 357 | #nextToken(tokens, expectType = [], skipType = []) { 358 | const token = tokens.shift(); 359 | if (skipType.includes(token?.type)) { 360 | tokens.unshift(token); 361 | return null; 362 | } 363 | if (!expectType.includes(token ? token.type : null)) { 364 | throw new Error(`Invalid z-var value: (${token && token.type}) ${JSON.stringify(token)}. Expected: ${JSON.stringify(expectType)}`); 365 | } 366 | return token; 367 | } 368 | 369 | #negate(value, negate = 0) { 370 | for (let c = negate; c; c--) { 371 | value = !value; 372 | } 373 | return value; 374 | } 375 | } 376 | 377 | function prepare(tokens, vars) { 378 | return (new Preparator(tokens, vars)).prepare(); 379 | } 380 | 381 | 382 | // exports.prepare = prepare; 383 | export { prepare }; 384 | 385 | -------------------------------------------------------------------------------- /tests/testcases/jquery.template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JQUERY DNA TEMPLATE 3 | * https://github.com/webdevelopers-eu/jquery-template 4 | * 5 | * author Daniel Sevcik 6 | * copyright 2016 Daniel Sevcik 7 | * since 2016-07-11 08:57:48 UTC 8 | * 9 | * Replace variables according to rules specified in @z-var 10 | * attribute. Optionaly clone the element before replacing variables. 11 | * Removes attribute 'template'. 12 | * 13 | * $(templ).template(VARS); 14 | * 15 | * VARS - object or array of objects or multidimensional object 16 | * 17 | * Syntax: 18 | * ...... 20 | * ... 21 | * ... 22 | * ... 23 | * 24 | * The HTML