diff --git a/content/templates.xqm b/content/templates.xqm index e284161..3c7184e 100644 --- a/content/templates.xqm +++ b/content/templates.xqm @@ -1,5 +1,6 @@ xquery version "3.1"; + (:~ : HTML templating module : @@ -9,8 +10,7 @@ xquery version "3.1"; : @contributor Joe Wicentowski :) module namespace templates="http://exist-db.org/xquery/html-templating"; - -import module namespace inspect="http://exist-db.org/xquery/inspection"; +import module namespace inspect="http://exist-db.org/xquery/inspection" at "java:org.exist.xquery.functions.inspect.InspectionModule"; import module namespace map="http://www.w3.org/2005/xpath-functions/map"; import module namespace request="http://exist-db.org/xquery/request"; import module namespace session="http://exist-db.org/xquery/session"; @@ -24,6 +24,7 @@ declare variable $templates:CONFIG_ROOT := "root"; declare variable $templates:CONFIG_FN_RESOLVER := "fn-resolver"; declare variable $templates:CONFIG_PARAM_RESOLVER := "param-resolver"; declare variable $templates:CONFIG_FILTER_ATTRIBUTES := "filter-atributes"; +declare variable $templates:CONFIG_USE_CLASS_SYNTAX := "class-lookup"; declare variable $templates:CONFIGURATION := "configuration"; declare variable $templates:CONFIGURATION_ERROR := QName("http://exist-db.org/xquery/html-templating", "ConfigurationError"); @@ -34,6 +35,7 @@ declare variable $templates:TYPE_ERROR := QName("http://exist-db.org/xquery/html declare variable $templates:MAX_ARITY := 20; declare variable $templates:ATTR_DATA_TEMPLATE := "data-template"; +declare variable $templates:SEARCH_IN_CLASS := true(); (:~ : Start processing the provided content. Template functions are looked up by calling the @@ -136,7 +138,7 @@ declare function templates:process($nodes as node()*, $model as map(*)) { return if ($dataAttr) then templates:call($dataAttr, $node, $model) - else + else if (($model($templates:CONFIGURATION)($templates:CONFIG_USE_CLASS_SYNTAX), $templates:SEARCH_IN_CLASS)[1]) then let $instructions := templates:get-instructions($node/@class) return if ($instructions) then @@ -147,6 +149,7 @@ declare function templates:process($nodes as node()*, $model as map(*)) { element { node-name($node) } { $node/@*, for $child in $node/node() return templates:process($child, $model) } + else $node default return $node }; @@ -576,7 +579,7 @@ declare function templates:form-control($node as node(), $model as map(*)) as no return if (exists($value)) then switch ($type) - case "checkbox" + case "checkbox" case "radio" return element { node-name($node) } { $node/@* except $node/@checked, diff --git a/package.json b/package.json index 85899f6..5eaaf7b 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,56 @@ { - "name": "templating", - "version": "1.1.0", - "description": "eXist-db HTML Templating Library", - "homepage": "https://github.com/eXist-db/templating#readme", - "bugs": "https://github.com/eXist-db/templating/issues", - "keywords": [ - "exist", - "exist-db", - "xml", - "xql", - "xquery" - ], - "devDependencies": { - "@existdb/gulp-exist": "^4.3.2", - "@existdb/gulp-replace-tmpl": "^1.0.4", - "axios": "^0.21.1", - "chai": "^4.2.0", - "chai-xml": "^0.4.0", - "chokidar": "^3.5.3", - "delete": "^1.1.0", - "fs-extra": "^9.1.0", - "glob-stream": "^7.0.0", - "gulp": "^4.0.2", - "gulp-rename": "^2.0.0", - "gulp-zip": "^5.0.2", - "jsdom": "^16.4.0", - "mocha": "^10.1.0", - "xmldoc": "^1.1.2", - "yeoman-assert": "^3.1.1" - }, - "author": { - "name": "The eXist-db Authors" - }, - "license": "LGPL-2.1", - "scripts": { - "start": "npm install && npm run build", - "test": "gulp install:all && mocha test/mocha --recursive --exit && node test/xqs/xqSuite.js", - "test:watch": "mocha test/mocha --recursive --watch", - "build": "gulp build", - "build:all": "gulp build:all" - }, - "repository": { - "type": "git", - "url": "https://github.com/eXist-db/templating", - "license": "LGPL-2.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/existdb" - }, - "overrides": { - "chokidar": "$chokidar", - "glob-stream": "$glob-stream" - } + "name": "templating", + "version": "1.2.0", + "description": "eXist-db HTML Templating Library", + "homepage": "https://github.com/eXist-db/templating#readme", + "bugs": "https://github.com/eXist-db/templating/issues", + "keywords": [ + "exist", + "exist-db", + "xml", + "xql", + "xquery" + ], + "devDependencies": { + "@existdb/gulp-exist": "^4.3.2", + "@existdb/gulp-replace-tmpl": "^1.0.4", + "axios": "^0.21.1", + "chai": "^4.2.0", + "chai-xml": "^0.4.0", + "chokidar": "^3.5.3", + "delete": "^1.1.0", + "fs-extra": "^9.1.0", + "glob-stream": "^7.0.0", + "gulp": "^4.0.2", + "gulp-rename": "^2.0.0", + "gulp-zip": "^5.0.2", + "jsdom": "^16.4.0", + "mocha": "^10.1.0", + "xmldoc": "^1.1.2", + "yeoman-assert": "^3.1.1" + }, + "author": { + "name": "The eXist-db Authors" + }, + "license": "LGPL-2.1", + "scripts": { + "start": "npm install && npm run build", + "test": "gulp install:all && mocha test/mocha --recursive --exit && node test/xqs/xqSuite.js", + "test:watch": "mocha test/mocha --recursive --watch", + "build": "gulp build", + "build:all": "gulp build:all" + }, + "repository": { + "type": "git", + "url": "https://github.com/eXist-db/templating", + "license": "LGPL-2.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/existdb" + }, + "overrides": { + "chokidar": "$chokidar", + "glob-stream": "$glob-stream" + } } diff --git a/test/app/call-from-class.html b/test/app/call-from-class.html new file mode 100644 index 0000000..1fe6a5b --- /dev/null +++ b/test/app/call-from-class.html @@ -0,0 +1,11 @@ + + + + Test calling Templates from class + + + +

+ + + diff --git a/test/app/modules/view.xql b/test/app/modules/view.xql index 7140182..5dfe14e 100644 --- a/test/app/modules/view.xql +++ b/test/app/modules/view.xql @@ -5,6 +5,7 @@ declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; import module namespace templates="http://exist-db.org/xquery/html-templating"; import module namespace lib="http://exist-db.org/xquery/html-templating/lib"; +import module namespace console="http://exist-db.org/xquery/console"; declare option output:method "html5"; declare option output:media-type "text/html"; @@ -24,7 +25,7 @@ declare variable $test:app-root := substring-before($modulePath, "/modules") ; -declare +declare %templates:wrap function test:init-data($node as node(), $model as map(*)) { let $addresses := ( @@ -50,23 +51,23 @@ function test:init-data($node as node(), $model as map(*)) { } }; -declare +declare %templates:wrap function test:print-name($node as node(), $model as map(*)) { $model("address")?name }; -declare +declare %templates:wrap function test:print-city($node as node(), $model as map(*)) { $model("address")?city }; -declare +declare %templates:wrap function test:print-street($node as node(), $model as map(*)) { $model("address")?street }; -declare +declare %templates:wrap %templates:default("language", "en") function test:hello($node as node(), $model as map(*), $language as xs:string) { @@ -79,26 +80,26 @@ function test:hello($node as node(), $model as map(*), $language as xs:string) { "Welcome" }; -declare +declare %templates:wrap %templates:default("defaultParam", "fallback") function test:default($node as node(), $model as map(*), $defaultParam as xs:string) { $defaultParam }; -declare +declare %templates:wrap function test:numbers($node as node(), $model as map(*), $n1 as xs:integer, $n2 as xs:double) { ($n1 treat as xs:integer) + ($n2 treat as xs:double) }; -declare +declare %templates:wrap function test:date($node as node(), $model as map(*), $date as xs:date) { day-from-date($date) }; -declare +declare %templates:wrap function test:boolean($node as node(), $model as map(*), $boolean as xs:boolean) { if ($boolean instance of xs:boolean) then @@ -111,10 +112,17 @@ declare function test:custom-model($node as node(), $model as map(*)) { $model?('my-model-item') }; +declare + %templates:wrap +function test:print-from-class($node as node(), $model as map(*)) { + 'print-from-class' +}; let $config := map { $templates:CONFIG_APP_ROOT : $test:app-root, - $templates:CONFIG_STOP_ON_ERROR: true() + $templates:CONFIG_STOP_ON_ERROR: true(), + $templates:CONFIG_USE_CLASS_SYNTAX: xs:boolean(request:get-parameter('classLookup', $templates:SEARCH_IN_CLASS)) } + let $lookup := function($name as xs:string, $arity as xs:integer) { try { function-lookup(xs:QName($name), $arity) @@ -128,4 +136,4 @@ let $lookup := function($name as xs:string, $arity as xs:integer) { :) let $content := request:get-data() return - templates:apply($content, $lookup, map { "my-model-item": 'xxx' }, $config) \ No newline at end of file + templates:apply($content, $lookup, map { "my-model-item": 'xxx' }, $config) diff --git a/test/mocha/rest_spec.js b/test/mocha/rest_spec.js index e0587ff..02f9788 100644 --- a/test/mocha/rest_spec.js +++ b/test/mocha/rest_spec.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; const axios = require('axios'); const expect = require('chai').expect; @@ -11,17 +11,17 @@ const { origin } = new URL(serverInfo.server); const app = `${origin}/exist/apps/templating-test`; const axiosInstance = axios.create({ - baseURL: app + baseURL: app, }); describe('expand HTML template index.html', function () { - let document, res + let document, res; before(async function () { - res = await axiosInstance.get('index.html'); - const {window} = new JSDOM(res.data); - document = window.document - }) + res = await axiosInstance.get('index.html'); + const { window } = new JSDOM(res.data); + document = window.document; + }); it('returns status ok', async function () { expect(res.status).to.equal(200); @@ -35,32 +35,38 @@ describe('expand HTML template index.html', function () { }); it('handles static parameters', async function () { - // statically defined parameter + // statically defined parameter expect(document.querySelector('h1.static-lang')).to.exist; - expect(document.querySelector('h1.static-lang').innerHTML).to.equal('Witam'); + expect(document.querySelector('h1.static-lang').innerHTML).to.equal( + 'Witam' + ); }); it('handles custom model items', async function () { - expect(document.querySelector('.custom').innerHTML).to.equal('Custom model item: xxx'); + expect(document.querySelector('.custom').innerHTML).to.equal( + 'Custom model item: xxx' + ); }); it('handles fallbacks', async function () { - expect(document.querySelector('.default-param').innerHTML).to.equal('fallback'); + expect(document.querySelector('.default-param').innerHTML).to.equal( + 'fallback' + ); }); }); describe('expand HTML template index.html with language parameter set', function () { - let document, res + let document, res; before(async function () { res = await axiosInstance.get('index.html', { params: { - language: 'de' - } + language: 'de', + }, }); const { window } = new JSDOM(res.data); - document = window.document - }) + document = window.document; + }); it('request returns with status 200', async function () { expect(res.status).to.equal(200); @@ -70,18 +76,22 @@ describe('expand HTML template index.html with language parameter set', function it('request parameter overwrites default', async function () { // default parameter value applies expect(document.querySelector('h1.no-lang')).to.exist; - expect(document.querySelector('h1.no-lang').innerHTML).to.equal('Willkommen'); + expect(document.querySelector('h1.no-lang').innerHTML).to.equal( + 'Willkommen' + ); }); it('request parameter overwrites static', async function () { - // statically defined parameter + // statically defined parameter expect(document.querySelector('h1.static-lang')).to.exist; - expect(document.querySelector('h1.static-lang').innerHTML).to.equal('Willkommen'); + expect(document.querySelector('h1.static-lang').innerHTML).to.equal( + 'Willkommen' + ); }); }); describe('expand HTML template types.html', function () { - let document, res + let document, res; before(async function () { res = await axiosInstance.get('types.html', { @@ -89,13 +99,13 @@ describe('expand HTML template types.html', function () { n1: 20, n2: 30.25, date: '2021-02-07+01:00', - boolean: 'true' - } + boolean: 'true', + }, }); const { window } = new JSDOM(res.data); - document = window.document - }) - + document = window.document; + }); + it('returns with status OK', async function () { expect(res.status).to.equal(200); expect(document).to.be.ok; @@ -117,56 +127,61 @@ describe('expand HTML template types.html', function () { }); describe('expand HTML template types-fail.html', function () { - it('rejects wrong parameter type', function () { - return axiosInstance.get('types-fail.html', { - params: { - n1: 'abc', - n2: 30.25, - date: '2021-02-07+01:00', - boolean: 'true' - } - }) - .catch(error => { + return axiosInstance + .get('types-fail.html', { + params: { + n1: 'abc', + n2: 30.25, + date: '2021-02-07+01:00', + boolean: 'true', + }, + }) + .catch((error) => { expect(error.response.status).to.be.oneOf([400, 500]); expect(error.response.data).to.contain('templates:TypeError'); }); }); - }); describe('expand HTML template missing-tmpl.html', function () { - - it("reports missing template functions", async function () { - return axiosInstance.get("missing-tmpl.html") - .catch((error) => { - expect(error.response.status).to.be.oneOf([400, 500]); - expect(error.response.data).to.contain("templates:NotFound"); - }); + it('reports missing template functions', async function () { + return axiosInstance.get('missing-tmpl.html').catch((error) => { + expect(error.response.status).to.be.oneOf([400, 500]); + expect(error.response.data).to.contain('templates:NotFound'); + }); }); }); -describe('Supports template nesting', function() { - it('handles nested templates', async function() { - const res = await axiosInstance.get('nesting.html'); +describe('Supports template nesting', function () { + it('handles nested templates', async function () { + const res = await axiosInstance.get('nesting.html'); expect(res.status).to.equal(200); const { window } = new JSDOM(res.data); - expect(window.document.querySelector('tr:nth-child(1) td[data-template="test:print-name"]').innerHTML).to.equal('Berta Muh'); - expect(window.document.querySelector('tr:nth-child(2) td[data-template="test:print-street"]').innerHTML).to.equal('Am Zoo 45'); + expect( + window.document.querySelector( + 'tr:nth-child(1) td[data-template="test:print-name"]' + ).innerHTML + ).to.equal('Berta Muh'); + expect( + window.document.querySelector( + 'tr:nth-child(2) td[data-template="test:print-street"]' + ).innerHTML + ).to.equal('Am Zoo 45'); }); }); -describe('Supports form fields', function() { - it('injects form field values', async function() { +describe('Supports form fields', function () { + it('injects form field values', async function () { const res = await axiosInstance.get('forms.html', { params: { param1: 'xxx', param2: 'value2', param3: true, param4: 'checkbox2', - param5: 'radio2' - } + param5: 'radio2', + }, }); expect(res.status).to.equal(200); const { window } = new JSDOM(res.data); @@ -188,7 +203,7 @@ describe('Supports form fields', function() { expect(control4).to.have.length(2); expect(control4[0].checked).to.be.false; expect(control4[1].checked).to.be.true; - + const control5 = window.document.querySelectorAll('input[name="param5"]'); expect(control5).to.have.length(2); expect(control5[0].checked).to.be.false; @@ -196,132 +211,126 @@ describe('Supports form fields', function() { }); }); -describe('Supports set and unset param', function() { - it('supports set and unset with multiple params of the same name', async function() { - const res = await axiosInstance.get('set-unset-params.html?foo=bar&foo=baz' - // if URL parameters are supplied via params object, mocha will only send one param - // of a given name, so we must include params in the query string +describe('Supports set and unset param', function () { + it('supports set and unset with multiple params of the same name', async function () { + const res = await axiosInstance.get( + 'set-unset-params.html?foo=bar&foo=baz' + // if URL parameters are supplied via params object, mocha will only send one param + // of a given name, so we must include params in the query string ); expect(res.status).to.equal(200); const { window } = new JSDOM(res.data); - + expect(window.document.querySelector('p#set')).to.exist; - }); + }); }); -describe("Supports parsing parameters", function () { - it("supports parsing parameters in attributes and text", async function () { - const res = await axiosInstance.get( - "parse-params.html", - { - params: { - description: 'my title', - link: 'foo' - } - } - ); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); - - const link = window.document.querySelector("a"); +describe('Supports parsing parameters', function () { + it('supports parsing parameters in attributes and text', async function () { + const res = await axiosInstance.get('parse-params.html', { + params: { + description: 'my title', + link: 'foo', + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + + const link = window.document.querySelector('a'); expect(link).to.exist; - expect(link.title).to.equal('Link: my title'); + expect(link.title).to.equal('Link: my title'); expect(link.href).to.equal('/api/foo/'); - }); - - it("supports expanding from model", async function () { - const res = await axiosInstance.get("parse-params.html", { - params: { - description: "my title", - link: "foo", - }, - }); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); - - const para = window.document.getElementById('nested'); - expect(para).to.exist; - expect(para.innerHTML).to.equal("Out: TEST2"); + }); + + it('supports expanding from model', async function () { + const res = await axiosInstance.get('parse-params.html', { + params: { + description: 'my title', + link: 'foo', + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + + const para = window.document.getElementById('nested'); + expect(para).to.exist; + expect(para.innerHTML).to.equal('Out: TEST2'); const li = window.document.querySelectorAll('li'); expect(li).to.have.lengthOf(2); - expect(li[0].innerHTML).to.equal("Berta Muh, Kuhweide"); - expect(li[1].innerHTML).to.equal("Rudi Rüssel, Tierheim"); + expect(li[0].innerHTML).to.equal('Berta Muh, Kuhweide'); + expect(li[1].innerHTML).to.equal('Rudi Rüssel, Tierheim'); }); - it("fails gracefully", async function () { - const res = await axiosInstance.get("parse-params.html", { - params: { - description: "my title", - link: "foo", - }, - }); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); + it('fails gracefully', async function () { + const res = await axiosInstance.get('parse-params.html', { + params: { + description: 'my title', + link: 'foo', + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); const para = window.document.getElementById('default'); expect(para).to.exist; - expect(para.innerHTML).to.equal("not found;not found;"); + expect(para.innerHTML).to.equal('not found;not found;'); }); - it("serializes maps and arrays to JSON", async function () { - const res = await axiosInstance.get("parse-params.html", { - params: { - description: "my title", - link: "foo", - }, - }); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); - - const para = window.document.getElementById("map"); - expect(para).to.exist; - expect(para.innerHTML).to.equal('{"test":"TEST2"}'); + it('serializes maps and arrays to JSON', async function () { + const res = await axiosInstance.get('parse-params.html', { + params: { + description: 'my title', + link: 'foo', + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + + const para = window.document.getElementById('map'); + expect(para).to.exist; + expect(para.innerHTML).to.equal('{"test":"TEST2"}'); }); - it("handles different delimiters", async function () { - const res = await axiosInstance.get("parse-params.html", { - params: { - description: "my title" - }, - }); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); - - let para = window.document.getElementById("delimiters1"); - expect(para).to.exist; - expect(para.innerHTML).to.equal('my title'); - - para = window.document.getElementById("delimiters2"); - expect(para).to.exist; - expect(para.innerHTML).to.equal("TITLE: my title"); + it('handles different delimiters', async function () { + const res = await axiosInstance.get('parse-params.html', { + params: { + description: 'my title', + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + + let para = window.document.getElementById('delimiters1'); + expect(para).to.exist; + expect(para.innerHTML).to.equal('my title'); + + para = window.document.getElementById('delimiters2'); + expect(para).to.exist; + expect(para.innerHTML).to.equal('TITLE: my title'); }); }); -describe('Fail if template is missing', function() { +describe('Fail if template is missing', function () { it('fails if template could not be found', function () { - return axiosInstance.get('template-missing.html') - .catch(error => { - expect(error.response.status).to.be.oneOf([400, 500]); - expect(error.response.data).to.contain('templates:NotFound'); - }); + return axiosInstance.get('template-missing.html').catch((error) => { + expect(error.response.status).to.be.oneOf([400, 500]); + expect(error.response.data).to.contain('templates:NotFound'); + }); }); }); -describe("Supports including another file", function () { - it("replaces target blocks in included file", async function () { - const res = await axiosInstance.get( - "includes.html", - { - params: { - title: 'my title' - } - } - ); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); - - const items = window.document.querySelectorAll("li"); +describe('Supports including another file', function () { + it('replaces target blocks in included file', async function () { + const res = await axiosInstance.get('includes.html', { + params: { + title: 'my title', + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + + const items = window.document.querySelectorAll('li'); expect(items).to.have.lengthOf(4); expect(items[0].getAttribute('title')).to.equal('my title'); expect(items[0].innerHTML).to.equal('Block inserted at "start"'); @@ -330,16 +339,42 @@ describe("Supports including another file", function () { }); }); -describe("Supports resolving app location", function() { +describe('Supports resolving app location', function () { this.timeout(10000); - it("replaces variable with app URL", async function () { - const res = await axiosInstance.get("resolve-apps.html"); - expect(res.status).to.equal(200); - const { window } = new JSDOM(res.data); + it('replaces variable with app URL', async function () { + const res = await axiosInstance.get('resolve-apps.html'); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); let para = window.document.getElementById('test1'); - expect(para.innerHTML).to.equal("/exist/apps/templating-test"); + expect(para.innerHTML).to.equal('/exist/apps/templating-test'); - para = window.document.getElementById("test2"); - expect(para.innerHTML).to.equal("/exist/404.html#"); + para = window.document.getElementById('test2'); + expect(para.innerHTML).to.equal('/exist/404.html#'); + }); +}); + +describe('Templates can be called from class', function () { + it('and will be expanded when $templates:CONFIG_USE_CLASS_SYNTAX is true()', async function () { + const res = await axiosInstance.get('call-from-class.html', { + params: { + classLookup: true, + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + expect(window.document.querySelector('p').innerHTML).to.equal( + 'print-from-class' + ); + }); + + it('and will not be expanded when $templates:CONFIG_USE_CLASS_SYNTAX is false()', async function () { + const res = await axiosInstance.get('call-from-class.html', { + params: { + classLookup: false, + }, + }); + expect(res.status).to.equal(200); + const { window } = new JSDOM(res.data); + expect(window.document.querySelector('p').innerHTML).to.equal(''); }); });