Skip to content

Commit

Permalink
Merge pull request #703 from kethinov/0.6.16
Browse files Browse the repository at this point in the history
0.6.16
  • Loading branch information
kethinov authored Nov 14, 2024
2 parents e8940d0 + a74c272 commit 2f6ab15
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 153 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

- Put your changes here...

## 0.6.16

- Fixed a bug which caused client-side Teddy to fail in some situations like putting a `<loop>` in a `<select>` element.
- Deprecated a test that tests for passing numeric arguments to include tags, since this violates HTML grammar and never should've worked to begin with. It may still work with `cheerio`-driven Teddy because `cheerio`'s parser is more forgiving than a standards-compliant one unless and until `cheerio` deprecates support for that itself. Client-side Teddy will not support it, so for consistency the test has been removed.
- Updated various dependencies.

## 0.6.15

- Fixed a bug which caused the `cheerio`-driven modules to not work client-side if you choose to use them there.
Expand Down
125 changes: 117 additions & 8 deletions cheerioPolyfill.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// stub out cheerio using native dom methods for frontend so we don't have to bundle cheerio on the frontend
export function load (html) {
// create a native DOMParser
const parser = new window.DOMParser()
const doc = parser.parseFromString(html, 'text/html')
doc.body.innerHTML = doc.head.innerHTML + doc.body.innerHTML
const doc = parseTeddyDOMFromString(html) // create a DOM

// return a querySelector function with function chains
// e.g. dom('include') or dom(el) from teddy
Expand All @@ -25,10 +22,10 @@ export function load (html) {

// e.g. dom(arg).html() from teddy
html: function () {
return el.innerHTML
return getTeddyDOMInnerHTML(el)
},

// e.g. dom(el).attr('teddy_deferred_dynamic_include', 'true') from teddy
// e.g. dom(el).attr('teddydeferreddynamicinclude', 'true') from teddy
attr: function (attr, val) {
return el.setAttribute(attr, val)
},
Expand All @@ -44,7 +41,7 @@ export function load (html) {
if (typeof html === 'object') {
let newHtml = ''
for (const el of html) {
if (el.nodeType === window.Node.COMMENT_NODE) newHtml += '<!-- ' + el.textContent + ' -->'
if (el.nodeType === window.Node.COMMENT_NODE) newHtml += '<!--' + el.textContent + '-->'
else newHtml += el.outerHTML || el.textContent
}
html = newHtml
Expand All @@ -63,10 +60,122 @@ export function load (html) {

// e.g. dom.html() from teddy
$.html = function () {
return doc.body.innerHTML
return getTeddyDOMInnerHTML(doc)
}

return $
}

load.isCheerioPolyfill = true

// DOM parser function like DOMParser's parseFromString but allows Teddy elements to exist in places where they otherwise wouldn't be allowed, like inside of <select> elements
function parseTeddyDOMFromString (html) {
const selfClosingTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'])
const root = document.createElement('body')
const dom = [root]
const tagAndCommentRegex = /<\/?([a-zA-Z0-9]+)([^>]*)>|<!--([\s\S]*?)-->/g
const attrRegex = /([a-zA-Z0-9-:._]+)(?:=(["'])(.*?)\2)?/g
let lastIndex = 0
let match

// loop through each match and build a DOM
while ((match = tagAndCommentRegex.exec(html)) !== null) {
if (!dom[dom.length - 1]) throw new Error('Error parsing your template. There may be a coding mistake in your HTML. Look for extra closing </tags> and other common mistakes.')
const textBeforeMatch = html.slice(lastIndex, match.index)

// append text nodes
if (textBeforeMatch.trim()) {
const textNode = document.createTextNode(textBeforeMatch)
dom[dom.length - 1].appendChild(textNode)
}

if (match[0].startsWith('<!--')) {
// handle comments
const commentNode = document.createComment(match[3])
dom[dom.length - 1].appendChild(commentNode)
} else {
// handle tags
const [fullMatch, tagName, attrString] = match
const isClosingTag = fullMatch.startsWith('</')
if (isClosingTag) dom.pop() // pop the list if it's a closing tag
else {
// create a new element
const element = document.createElement(tagName)

// set attributes
let attrMatch
const attrMap = new Map()
while ((attrMatch = attrRegex.exec(attrString)) !== null) {
const attrName = attrMatch[1]
const attrValue = attrMatch[3]

// handle duplicate attributes for special tags
if (attrMap.has(attrName)) {
let count = 1
let newAttrName
do {
newAttrName = `${attrName}-teddyduplicate${count}`
count++
} while (attrMap.has(newAttrName))
attrMap.set(newAttrName, attrValue)
} else attrMap.set(attrName, attrValue)
}

// apply attributes to the element
for (const [name, value] of attrMap) element.setAttribute(name, value || '')

// append the new element to the current parent
dom[dom.length - 1].appendChild(element)

// push the new element to the dom if it's not self-closing
if (!selfClosingTags.has(tagName.toLowerCase()) && !fullMatch.endsWith('/>')) dom.push(element)
}
}

lastIndex = tagAndCommentRegex.lastIndex
}

// append any remaining text after the last match
if (lastIndex < html.length) {
const remainingText = html.slice(lastIndex)
if (remainingText.trim()) {
const textNode = document.createTextNode(remainingText)
dom[dom.length - 1].appendChild(textNode)
}
}

return root
}

// custom function to get inner HTML without escaping various things to prevent teddy from infinitely escaping them
function getTeddyDOMInnerHTML (node) {
const doublyEncodedEntities = {
'&amp;amp;': '&amp;',
'&amp;lt;': '&lt;',
'&amp;gt;': '&gt;',
'&amp;quot;': '&quot;',
'&amp;#39;': '&#39;',
'&amp;#x2F;': '&#x2F;'
}
const entityEntries = Object.entries(doublyEncodedEntities)

// build html string
let html = ''
for (const child of node.childNodes) {
if (child.nodeType === window.Node.ELEMENT_NODE) {
let outerHTML = child.outerHTML
for (const [doublyEncoded, singleEncoded] of entityEntries) outerHTML = outerHTML.replace(new RegExp(doublyEncoded, 'g'), singleEncoded)
html += outerHTML
} else if (child.nodeType === window.Node.TEXT_NODE) {
let textContent = child.textContent
for (const [doublyEncoded, singleEncoded] of entityEntries) textContent = textContent.replace(new RegExp(doublyEncoded, 'g'), singleEncoded)
html += textContent
} else if (child.nodeType === window.Node.COMMENT_NODE) {
let commentContent = child.textContent
for (const [doublyEncoded, singleEncoded] of entityEntries) commentContent = commentContent.replace(new RegExp(doublyEncoded, 'g'), singleEncoded)
html += `<!--${commentContent}-->`
}
}

return html
}
Loading

0 comments on commit 2f6ab15

Please sign in to comment.