This template contains no teddy tags. Just HTML.
diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc149b..bcfee4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ ## Next version +- Put your changes here... + +## 0.6.14 + +- Finsihed work on `cheerioPolyfill.js` which makes it possible for Teddy to execute in client-side contexts without using `cheerio`, allowing for a very small bundle size for client-side Teddy (17kb minified). - Fixed client-side tests to test Teddy in the browser properly. - Refactored tests to improve maintainability. -- Did further work on `cheerioPolyfill.js`. It's more than half finished, but it isn't fully done yet. - Updated various dependencies. ## 0.6.13 diff --git a/cheerioPolyfill.js b/cheerioPolyfill.js index 0fbe9ee..dc0b469 100644 --- a/cheerioPolyfill.js +++ b/cheerioPolyfill.js @@ -1,29 +1,10 @@ -// extend HTMLElement prototype to include cheerio properties -Object.defineProperty(window.HTMLElement.prototype, 'name', { - get: function () { - if (this.__name === undefined) this.__name = this.nodeName.toLowerCase() - return this.__name - } -}) - -Object.defineProperty(window.HTMLElement.prototype, 'parent', { - get: function () { - if (this.__parent === undefined) this.__parent = this.parentNode - return this.__parent - } -}) - -// TODO: write more HTMLElement property extensions to polyfill cheerio: this will require reading teddy.js line by line to see what properties from cheerio each method uses and adapt them like above one property at a time -// TODO: nextSibling, children? - // create a native DOMParser const parser = new window.DOMParser() // stub out cheerio using native dom methods for frontend so we don't have to bundle cheerio on the frontend export function load (html) { - console.log('Loading cheerio polyfill... TODO: This is unfinished! Please use teddy.mjs or teddy.cjs via a bundle for now.') const doc = parser.parseFromString(html, 'text/html') - console.log('doc:', doc.body.innerHTML) + doc.body.innerHTML = doc.head.innerHTML + doc.body.innerHTML // return a querySelector function with function chains // e.g. dom('include') or dom(el) from teddy @@ -31,7 +12,6 @@ export function load (html) { // if query is a string, we need to create a dom object from the string: an object with elements in it, e.g. a list of include tag objects if (typeof query === 'string') { const els = doc.querySelectorAll(query) - console.log('cheerio polyfill: dom(query)', els) return els // return the object collection } @@ -41,46 +21,42 @@ export function load (html) { // e.g. dom(el).children() from teddy children: function () { - console.log('cheerio polyfill: children()', el.children) - return el.children + return el.childNodes }, // e.g. dom(arg).html() from teddy html: function () { - console.log('cheerio polyfill: html()', el.innerHTML) return el.innerHTML }, // e.g. dom(el).attr('teddy_deferred_dynamic_include', 'true') from teddy attr: function (attr, val) { - console.log('cheerio polyfill: attr()', attr, val) return el.setAttribute(attr, val) }, // dom(el).removeAttr(attr) from teddy removeAttr: function (attr) { - console.log('cheerio polyfill: removeAttr()', attr) return el.removeAttribute(attr) }, // e.g. dom(el).replaceWith(localDom.html()) from teddy replaceWith: function (html) { // can either be a string or an array of elements - console.log('replaceWith doc:', doc.body.innerHTML) if (typeof html === 'object') { let newHtml = '' - for (const el of html) newHtml += el.outerHTML + for (const el of html) { + if (el.nodeType === window.Node.COMMENT_NODE) newHtml += '' + else newHtml += el.outerHTML || el.textContent + } html = newHtml } const temp = document.createElement('div') temp.innerHTML = html - el.replaceWith(...temp.children) - console.log('replaceWith doc:', doc.body.innerHTML) + el.replaceWith(...temp.childNodes) }, // e.g. dom(el).remove() from teddy remove: function () { - console.log('cheerio polyfill: remove()', el) return el.remove() } } @@ -88,7 +64,6 @@ export function load (html) { // e.g. dom.html() from teddy $.html = function () { - console.log('cheerio polyfill: dom.html()', doc.body.innerHTML) return doc.body.innerHTML } diff --git a/package-lock.json b/package-lock.json index fdef3f0..914e18d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "teddy", - "version": "0.6.13", + "version": "0.6.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "teddy", - "version": "0.6.13", + "version": "0.6.14", "license": "CC-BY-4.0", "dependencies": { "cheerio": "1.0.0" @@ -554,9 +554,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", - "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "version": "22.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", + "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 82730ed..d63d393 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "https://github.com/rooseveltframework/teddy/graphs/contributors" } ], - "version": "0.6.13", + "version": "0.6.14", "files": [ "dist" ], diff --git a/teddy.js b/teddy.js index bd5ae1c..7861328 100644 --- a/teddy.js +++ b/teddy.js @@ -108,7 +108,7 @@ function replaceCacheElements (dom, model) { const now = Date.now() // if max age is not set, then there is no max age and the cache content is still valid // or if last accessed + max age > now then the cache is not stale and the cache is still valid - if (!cache.maxAge || cache.entries[keyVal].lastAccessed + cache.maxAge > now) { + if (!(cache.maxAge && !cache.maxage) || cache.entries[keyVal].lastAccessed + (cache.maxAge || cache.maxage) > now) { const cacheContent = cache.entries[keyVal].markup cache.entries[keyVal].lastAccessed = now dom(el).replaceWith(cacheContent) @@ -160,16 +160,16 @@ function parseIncludes (dom, model, dynamic) { // ensure this isn't the child of a no parse block let foundBody = false let next = false - let parent = el.parent + let parent = el.parent || el.parentNode while (!foundBody) { let parentName if (!parent) parentName = 'body' - else parentName = parent.name + else parentName = parent.name || parent.nodeName?.toLowerCase() if (parentName === 'noparse' || parentName === 'noteddy') { next = true break } else if (parentName === 'body') foundBody = true - else parent = parent.parent + else parent = parent.parent || parent.parentNode } if (next) continue // get attributes @@ -187,6 +187,7 @@ function parseIncludes (dom, model, dynamic) { const contents = templates[src] const localModel = Object.assign({}, model) for (const arg of dom(el).children()) { + if (browser) arg.name = arg.nodeName?.toLowerCase() if (arg.name === 'arg') { if (browser) arg.attribs = getAttribs(arg) const argval = Object.keys(arg.attribs)[0] @@ -217,16 +218,16 @@ function parseConditionals (dom, model) { // ensure this isn't the child of a loop or a no parse block let foundBody = false let next = false - let parent = el.parent + let parent = el.parent || el.parentNode while (!foundBody) { let parentName if (!parent) parentName = 'body' - else parentName = parent.name + else parentName = parent.name || parent.nodeName?.toLowerCase() if (parentName === 'loop' || parentName === 'noparse' || parentName === 'noteddy') { next = true break } else if (parentName === 'body') foundBody = true - else parent = parent.parent + else parent = parent.parent || parent.parentNode } if (next) continue // get conditions @@ -239,6 +240,7 @@ function parseConditionals (dom, model) { } // check if it's an if tag and not an unless tag let isIf = true + if (browser) el.name = el.nodeName?.toLowerCase() if (el.name === 'unless') isIf = false // evaluate conditional const condResult = evaluateConditional(args, model) @@ -247,6 +249,7 @@ function parseConditionals (dom, model) { let nextSibling = el.nextSibling const removeStack = [] while (nextSibling) { + if (browser) nextSibling.name = nextSibling.nodeName?.toLowerCase() switch (nextSibling.name) { case 'elseif': case 'elseunless': @@ -263,12 +266,13 @@ function parseConditionals (dom, model) { } } for (const element of removeStack) dom(element).replaceWith('') - dom(el).replaceWith(el.children) + dom(el).replaceWith(el.childNodes || el.children) parsedTags++ } else { // true block is false; find the next elseif, elseunless, or else tag to evaluate let nextSibling = el.nextSibling while (nextSibling) { + if (browser) nextSibling.name = nextSibling.nodeName?.toLowerCase() switch (nextSibling.name) { case 'elseif': // get conditions @@ -282,10 +286,11 @@ function parseConditionals (dom, model) { if (evaluateConditional(args, model)) { // render the true block and discard the elseif, elseunless, and else blocks const replaceSibling = nextSibling - dom(replaceSibling).replaceWith(replaceSibling.children) + dom(replaceSibling).replaceWith(replaceSibling.childNodes || replaceSibling.children) nextSibling = el.nextSibling const removeStack = [] while (nextSibling) { + if (browser) nextSibling.name = nextSibling.nodeName?.toLowerCase() switch (nextSibling.name) { case 'elseif': case 'elseunless': @@ -323,10 +328,11 @@ function parseConditionals (dom, model) { if (!evaluateConditional(args, model)) { // render the true block and discard the elseif, elseunless, and else blocks const replaceSibling = nextSibling - dom(replaceSibling).replaceWith(replaceSibling.children) + dom(replaceSibling).replaceWith(replaceSibling.childNodes || replaceSibling.children) nextSibling = el.nextSibling const removeStack = [] while (nextSibling) { + if (browser) nextSibling.name = nextSibling.nodeName?.toLowerCase() switch (nextSibling.name) { case 'elseif': case 'elseunless': @@ -354,7 +360,7 @@ function parseConditionals (dom, model) { break case 'else': // else is always true, so if we've gotten here, then there's nothing to evaluate and we've reached the end of the conditional blocks - dom(nextSibling).replaceWith(nextSibling.children) + dom(nextSibling).replaceWith(nextSibling.childNodes || nextSibling.children) nextSibling = false parsedTags++ break @@ -482,16 +488,16 @@ function parseOneLineConditionals (dom, model) { // ensure this isn't the child of a loop or a no parse block let foundBody = false let next = false - let parent = el.parent + let parent = el.parent || el.parentNode while (!foundBody) { let parentName if (!parent) parentName = 'body' - else parentName = parent.name + else parentName = parent.name || parent.nodeName?.toLowerCase() if (parentName === 'loop' || parentName === 'noparse' || parentName === 'noteddy') { next = true break } else if (parentName === 'body') foundBody = true - else parent = parent.parent + else parent = parent.parent || parent.parentNode } if (next) continue // get conditions @@ -673,8 +679,8 @@ function defineNewCaches (dom, model) { if (browser) el.attribs = getAttribs(el) const name = el.attribs.name const key = el.attribs.key || 'none' - const maxAge = parseInt(el.attribs.maxAge) || 0 - const maxCaches = parseInt(el.attribs.maxCaches) || 1000 + const maxAge = parseInt(el.attribs.maxAge || el.attribs.maxage) || 0 + const maxCaches = parseInt(el.attribs.maxCaches || el.attribs.maxcaches) || 1000 const timestamp = Date.now() const markup = dom(el).html() if (!caches[name]) { @@ -711,6 +717,7 @@ function cleanupStrayTeddyTags (dom) { const tags = dom('[teddy_deferred_one_line_conditional], include, arg, if, unless, elseif, elseunless, else, loop, cache') if (tags.length > 0) { for (const el of tags) { + if (browser) el.name = el.nodeName?.toLowerCase() if (el.name === 'include' || el.name === 'arg' || el.name === 'if' || el.name === 'unless' || el.name === 'elseif' || el.name === 'elseunless' || el.name === 'else' || el.name === 'loop' || el.name === 'cache') { dom(el).remove() } @@ -832,10 +839,7 @@ function getOrSetObjectByDotNotation (obj, dotNotation, value) { } } -// #endregion - -// #region cheerio polyfills - +// cheerio polyfill function getAttribs (element) { const attributes = element.attributes const attributesObject = {} @@ -918,13 +922,13 @@ function setCache (params) { if (!templateCaches[params.template]) templateCaches[params.template] = {} if (params.key) { templateCaches[params.template][params.key] = { - maxAge: params.maxAge, - maxCaches: params.maxCaches || 1000, + maxAge: params.maxAge || params.maxage, + maxCaches: (params.maxCaches || params.maxcaches) || 1000, entries: {} } } else { templateCaches[params.template].none = { - maxAge: params.maxAge, + maxAge: params.maxAge || params.maxage, markup: null, created: null } @@ -981,11 +985,11 @@ function render (template, model, callback) { if (singletonCache) { // check if the timestamp exceeds max age if (!singletonCache.created) cacheKey = 'none' - else if (!singletonCache.maxAge) { + else if (!singletonCache.maxAge && singletonCache.maxage) { // if no max age is set, then this cache doesn't expire if (typeof callback === 'function') return callback(null, singletonCache.markup) else return singletonCache.markup - } else if (singletonCache.created + singletonCache.maxAge < Date.now()) cacheKey = 'none' // if yes re-render the template and cache it again + } else if (singletonCache.created + (singletonCache.maxAge || singletonCache.maxage) < Date.now()) cacheKey = 'none' // if yes re-render the template and cache it again else { // if no return the cached markup and skip the template render if (typeof callback === 'function') return callback(null, singletonCache.markup) @@ -1004,11 +1008,11 @@ function render (template, model, callback) { if (entryKey === cacheKeyModelVal) { // check if the timestamp exceeds max age const entry = templateCacheAtThisKey.entries[entryKey] - if (!templateCacheAtThisKey.maxAge) { + if (!templateCacheAtThisKey.maxAge && !templateCacheAtThisKey.maxage) { // if no max age is set, then this cache doesn't expire if (typeof callback === 'function') return callback(null, entry.markup) else return entry.markup - } else if (entry.created + templateCacheAtThisKey.maxAge < Date.now()) { + } else if (entry.created + (templateCacheAtThisKey.maxAge || templateCacheAtThisKey.maxage) < Date.now()) { // if yes re-render the template and cache it again cacheKey = key break diff --git a/test/loaders/mocha.js b/test/loaders/mocha.js index df275fa..bb8aa7b 100644 --- a/test/loaders/mocha.js +++ b/test/loaders/mocha.js @@ -13,7 +13,7 @@ for (const testGroup of testsToRun) { before(() => { teddy.setTemplateRoot('test/templates') model = makeModel() - if (process.env.NODE_ENV === 'test') teddy.setVerbosity(0) + if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'cover') teddy.setVerbosity(0) }) for (const test of testGroup.tests) { @@ -26,8 +26,16 @@ for (const testGroup of testsToRun) { } function teddyAssert (result, expected = true) { + result = ignoreSpaces(result) if (typeof expected === 'string') expected = ignoreSpaces(expected) - assert.equal(ignoreSpaces(result), expected) + if (Array.isArray(expected)) { + let match = false + for (let acceptable of expected) { + acceptable = ignoreSpaces(acceptable) + if (result === acceptable) match = true + } + assert.equal(match, true) + } else assert.equal(result, expected) } function ignoreSpaces (str) { diff --git a/test/loaders/playwright.js b/test/loaders/playwright.js index 6082ef3..3035f43 100644 --- a/test/loaders/playwright.js +++ b/test/loaders/playwright.js @@ -8,7 +8,6 @@ import testGroups from '../tests.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -const teddyPath = path.resolve(__dirname, '../../dist/teddy.js') const testsToRun = loadTests(testGroups) // pre-register teddy templates @@ -40,65 +39,77 @@ function registerTemplates (dir) { } const templates = registerTemplates('test/templates') -for (const testGroup of testsToRun) { - playwrightTest.describe(testGroup.describe, () => { - for (const test of testGroup.tests) { - if (test.skip) continue - if (test.runPlaywright) test.run = test.runPlaywright - if (!test.run) continue - else { - playwrightTest(test.message, async ({ page }) => { - // to debug, uncomment this: - // page.on('console', (msg) => console.log(msg)) - // for deeper debugging: export DEBUG=pw:browser - - const model = makeModel() - - // set an initial DOM - await page.setContent(` - -
- - - -The variable 'doesntexist' is not present
-The variable 'something' is present
-The variable 'doesntexist' is present
-The variable 'doesntexist' is not present
+The variable 'something' is present
+The variable 'doesntexist' is present
+The variable 'doesntexist' is present
-The variable 'doesntexist' is not present
-The variable 'doesntexist' is present
+The variable 'doesntexist' is not present
+The variable 'doesntexist' is present
-The variable 'doesntexist' is present
+The variable 'doesntexist' is not present
-The variable 'doesntexist' is not present
+The variable 'doesntexist' is present
-The variable 'doesntexist' is present
+The variable 'doesntexist' is not present
-The variable 'doesntexist' is not present
+One line if.
One line if.
-One line if.
- - +One line if.
+ + diff --git a/test/templates/looping/nestedObjectLoopLookup.html b/test/templates/looping/nestedObjectLoopLookup.html index 7430fac..d5605a0 100644 --- a/test/templates/looping/nestedObjectLoopLookup.html +++ b/test/templates/looping/nestedObjectLoopLookup.html @@ -4,5 +4,5 @@{child.num}
- +The variable \'something\' is present
' + expected: 'The variable \'something\' is present
One line if.
One line if.
One line if.
' + expected: ['One line if.
One line if.
One line if.
', 'One line if.
One line if.
One line if.
'] }, { message: 'should evaluate one line if "if-something=\'\'" as false (conditionals/oneLineEmpty.html)', @@ -417,19 +418,19 @@ export default [ message: 'should evaluateThe variable \'doesntexist\' is not present
' + expected: 'The variable \'doesntexist\' is not present
The variable \'doesntexist\' is not present
' + expected: 'The variable \'doesntexist\' is not present
The variable \'doesntexist\' is not present
' + expected: 'The variable \'doesntexist\' is not present
1
2
3
' + expected: ['1
2
3
', '1
2
3
'] }, { message: 'should parse nested loops correctly (looping/nestedLoopsObjectWithArrayOfObjects.html)', @@ -929,7 +930,7 @@ export default [ { message: 'should render plain HTML with no teddy tags with no changes (misc/plainHTML.html)', template: 'misc/plainHTML', - run: async (teddy, template, model, assert, expected) => { + runMocha: async (teddy, template, model, assert, expected) => { const teddyTemplate = teddy.render(template, model) assert(teddyTemplate, 'This template contains no teddy tags. Just HTML.