fix: use preEscapedText for inline JS script content
H.toHtml was escaping the JS content, causing the script tag to be empty. H.preEscapedText preserves the text as-is, which is correct for script tags.
This commit is contained in:
+117
-118
@@ -208,121 +208,122 @@ page title content =
|
|||||||
|
|
||||||
-- | Inline JavaScript for client-side search + sort rendering.
|
-- | Inline JavaScript for client-side search + sort rendering.
|
||||||
searchJs :: Text
|
searchJs :: Text
|
||||||
searchJs = T.unlines
|
searchJs =
|
||||||
[ "(function(){"
|
T.unlines
|
||||||
, "'use strict';"
|
[ "(function(){"
|
||||||
, ""
|
, "'use strict';"
|
||||||
, "const recipes = JSON.parse(document.getElementById('roux-recipe-data').textContent);"
|
, ""
|
||||||
, "const errors = JSON.parse(document.getElementById('roux-errors-data').textContent);"
|
, "const recipes = JSON.parse(document.getElementById('roux-recipe-data').textContent);"
|
||||||
, "const listEl = document.getElementById('roux-recipe-list');"
|
, "const errors = JSON.parse(document.getElementById('roux-errors-data').textContent);"
|
||||||
, "const searchEl = document.getElementById('roux-search');"
|
, "const listEl = document.getElementById('roux-recipe-list');"
|
||||||
, ""
|
, "const searchEl = document.getElementById('roux-search');"
|
||||||
, "function getSortMode() {"
|
, ""
|
||||||
, " const h = location.hash.slice(1);"
|
, "function getSortMode() {"
|
||||||
, " if (h === 'tags') return 'tags';"
|
, " const h = location.hash.slice(1);"
|
||||||
, " if (h === 'course') return 'course';"
|
, " if (h === 'tags') return 'tags';"
|
||||||
, " return 'alpha';"
|
, " if (h === 'course') return 'course';"
|
||||||
, "}"
|
, " return 'alpha';"
|
||||||
, ""
|
, "}"
|
||||||
, "function render() {"
|
, ""
|
||||||
, " const query = searchEl.value.toLowerCase().trim();"
|
, "function render() {"
|
||||||
, " const mode = getSortMode();"
|
, " const query = searchEl.value.toLowerCase().trim();"
|
||||||
, " let filtered = recipes;"
|
, " const mode = getSortMode();"
|
||||||
, " if (query) {"
|
, " let filtered = recipes;"
|
||||||
, " filtered = recipes.filter(r =>"
|
, " if (query) {"
|
||||||
, " r.title.toLowerCase().includes(query) ||"
|
, " filtered = recipes.filter(r =>"
|
||||||
, " r.filename.toLowerCase().includes(query)"
|
, " r.title.toLowerCase().includes(query) ||"
|
||||||
, " );"
|
, " r.filename.toLowerCase().includes(query)"
|
||||||
, " }"
|
, " );"
|
||||||
, " let html = '';"
|
, " }"
|
||||||
, " if (query || mode === 'alpha') {"
|
, " let html = '';"
|
||||||
, " const sorted = [...filtered].sort((a, b) => a.title.localeCompare(b.title));"
|
, " if (query || mode === 'alpha') {"
|
||||||
, " html = renderFlatList(sorted);"
|
, " const sorted = [...filtered].sort((a, b) => a.title.localeCompare(b.title));"
|
||||||
, " } else if (mode === 'tags') {"
|
, " html = renderFlatList(sorted);"
|
||||||
, " html = renderByTags(filtered);"
|
, " } else if (mode === 'tags') {"
|
||||||
, " } else if (mode === 'course') {"
|
, " html = renderByTags(filtered);"
|
||||||
, " html = renderByCourse(filtered);"
|
, " } else if (mode === 'course') {"
|
||||||
, " }"
|
, " html = renderByCourse(filtered);"
|
||||||
, " if (errors.length > 0) {"
|
, " }"
|
||||||
, " html += '<h3>Unparseable recipes</h3><ul>';"
|
, " if (errors.length > 0) {"
|
||||||
, " errors.forEach(e => { html += '<li>' + escapeHtml(e.title) + '</li>'; });"
|
, " html += '<h3>Unparseable recipes</h3><ul>';"
|
||||||
, " html += '</ul>';"
|
, " errors.forEach(e => { html += '<li>' + escapeHtml(e.title) + '</li>'; });"
|
||||||
, " }"
|
, " html += '</ul>';"
|
||||||
, " listEl.innerHTML = html;"
|
, " }"
|
||||||
, "}"
|
, " listEl.innerHTML = html;"
|
||||||
, ""
|
, "}"
|
||||||
, "function renderFlatList(items) {"
|
, ""
|
||||||
, " if (items.length === 0) return '<p>No recipes found.</p>';"
|
, "function renderFlatList(items) {"
|
||||||
, " let html = '<ul class=\'roux-recipes\'>';"
|
, " if (items.length === 0) return '<p>No recipes found.</p>';"
|
||||||
, " items.forEach(r => {"
|
, " let html = '<ul class=\'roux-recipes\'>';"
|
||||||
, " html += '<li><a href=\"/recipes/' + encodeURIComponent(r.filename) + '\">' + escapeHtml(r.title) + '</a></li>';"
|
, " items.forEach(r => {"
|
||||||
, " });"
|
, " html += '<li><a href=\"/recipes/' + encodeURIComponent(r.filename) + '\">' + escapeHtml(r.title) + '</a></li>';"
|
||||||
, " html += '</ul>';"
|
, " });"
|
||||||
, " return html;"
|
, " html += '</ul>';"
|
||||||
, "}"
|
, " return html;"
|
||||||
, ""
|
, "}"
|
||||||
, "function renderByTags(items) {"
|
, ""
|
||||||
, " const tagMap = {};"
|
, "function renderByTags(items) {"
|
||||||
, " items.forEach(r => {"
|
, " const tagMap = {};"
|
||||||
, " if (r.tags.length === 0) return;"
|
, " items.forEach(r => {"
|
||||||
, " r.tags.forEach(t => {"
|
, " if (r.tags.length === 0) return;"
|
||||||
, " if (!tagMap[t]) tagMap[t] = [];"
|
, " r.tags.forEach(t => {"
|
||||||
, " tagMap[t].push(r);"
|
, " if (!tagMap[t]) tagMap[t] = [];"
|
||||||
, " });"
|
, " tagMap[t].push(r);"
|
||||||
, " });"
|
, " });"
|
||||||
, " const tags = Object.keys(tagMap).sort((a,b) => a.localeCompare(b));"
|
, " });"
|
||||||
, " if (tags.length === 0) return '<p>No tagged recipes.</p>';"
|
, " const tags = Object.keys(tagMap).sort((a,b) => a.localeCompare(b));"
|
||||||
, " let html = '';"
|
, " if (tags.length === 0) return '<p>No tagged recipes.</p>';"
|
||||||
, " tags.forEach(t => {"
|
, " let html = '';"
|
||||||
, " const sorted = tagMap[t].sort((a,b) => a.title.localeCompare(b.title));"
|
, " tags.forEach(t => {"
|
||||||
, " html += '<h3>' + escapeHtml(t) + '</h3>';"
|
, " const sorted = tagMap[t].sort((a,b) => a.title.localeCompare(b.title));"
|
||||||
, " html += renderFlatList(sorted);"
|
, " html += '<h3>' + escapeHtml(t) + '</h3>';"
|
||||||
, " });"
|
, " html += renderFlatList(sorted);"
|
||||||
, " return html;"
|
, " });"
|
||||||
, "}"
|
, " return html;"
|
||||||
, ""
|
, "}"
|
||||||
, "function renderByCourse(items) {"
|
, ""
|
||||||
, " const courseMap = {};"
|
, "function renderByCourse(items) {"
|
||||||
, " items.forEach(r => {"
|
, " const courseMap = {};"
|
||||||
, " if (!r.course) return;"
|
, " items.forEach(r => {"
|
||||||
, " if (!courseMap[r.course]) courseMap[r.course] = [];"
|
, " if (!r.course) return;"
|
||||||
, " courseMap[r.course].push(r);"
|
, " if (!courseMap[r.course]) courseMap[r.course] = [];"
|
||||||
, " });"
|
, " courseMap[r.course].push(r);"
|
||||||
, " const courses = Object.keys(courseMap).sort((a,b) => a.localeCompare(b));"
|
, " });"
|
||||||
, " if (courses.length === 0) return '<p>No recipes with course metadata.</p>';"
|
, " const courses = Object.keys(courseMap).sort((a,b) => a.localeCompare(b));"
|
||||||
, " let html = '';"
|
, " if (courses.length === 0) return '<p>No recipes with course metadata.</p>';"
|
||||||
, " courses.forEach(c => {"
|
, " let html = '';"
|
||||||
, " const sorted = courseMap[c].sort((a,b) => a.title.localeCompare(b.title));"
|
, " courses.forEach(c => {"
|
||||||
, " html += '<h3>' + escapeHtml(c) + '</h3>';"
|
, " const sorted = courseMap[c].sort((a,b) => a.title.localeCompare(b.title));"
|
||||||
, " html += renderFlatList(sorted);"
|
, " html += '<h3>' + escapeHtml(c) + '</h3>';"
|
||||||
, " });"
|
, " html += renderFlatList(sorted);"
|
||||||
, " return html;"
|
, " });"
|
||||||
, "}"
|
, " return html;"
|
||||||
, ""
|
, "}"
|
||||||
, "function escapeHtml(s) {"
|
, ""
|
||||||
, " const d = document.createElement('div');"
|
, "function escapeHtml(s) {"
|
||||||
, " d.appendChild(document.createTextNode(s));"
|
, " const d = document.createElement('div');"
|
||||||
, " return d.innerHTML;"
|
, " d.appendChild(document.createTextNode(s));"
|
||||||
, "}"
|
, " return d.innerHTML;"
|
||||||
, ""
|
, "}"
|
||||||
, "searchEl.addEventListener('input', render);"
|
, ""
|
||||||
, "window.addEventListener('hashchange', render);"
|
, "searchEl.addEventListener('input', render);"
|
||||||
, ""
|
, "window.addEventListener('hashchange', render);"
|
||||||
, "// Set active tab style based on hash"
|
, ""
|
||||||
, "function updateActiveNav() {"
|
, "// Set active tab style based on hash"
|
||||||
, " const mode = getSortMode();"
|
, "function updateActiveNav() {"
|
||||||
, " document.querySelectorAll('.roux-navbar a').forEach(a => {"
|
, " const mode = getSortMode();"
|
||||||
, " a.classList.toggle('active', a.getAttribute('href') === '#' + mode);"
|
, " document.querySelectorAll('.roux-navbar a').forEach(a => {"
|
||||||
, " });"
|
, " a.classList.toggle('active', a.getAttribute('href') === '#' + mode);"
|
||||||
, "}"
|
, " });"
|
||||||
, "window.addEventListener('hashchange', updateActiveNav);"
|
, "}"
|
||||||
, ""
|
, "window.addEventListener('hashchange', updateActiveNav);"
|
||||||
, "// Initial render -- setting hash triggers hashchange"
|
, ""
|
||||||
, "if (!location.hash) location.hash = 'alpha';"
|
, "// Initial render -- setting hash triggers hashchange"
|
||||||
, "else { updateActiveNav(); render(); }"
|
, "if (!location.hash) location.hash = 'alpha';"
|
||||||
, ""
|
, "else { updateActiveNav(); render(); }"
|
||||||
, "})();"
|
, ""
|
||||||
]
|
, "})();"
|
||||||
|
]
|
||||||
|
|
||||||
-- | Render the recipe index page -- shell with embedded JSON + JS rendering.
|
-- | Render the recipe index page -- shell with embedded JSON + JS rendering.
|
||||||
indexPage :: SortMode -> [Idx.RecipeInfo] -> ByteString
|
indexPage :: SortMode -> [Idx.RecipeInfo] -> ByteString
|
||||||
@@ -349,7 +350,7 @@ indexPage _mode recipes =
|
|||||||
H.script ! A.id "roux-errors-data" ! A.type_ "application/json" $
|
H.script ! A.id "roux-errors-data" ! A.type_ "application/json" $
|
||||||
H.toHtml (decodeUtf8 (LB.toStrict (encode (Prelude.map Idx.toSearchEntry (filterBad recipes)))))
|
H.toHtml (decodeUtf8 (LB.toStrict (encode (Prelude.map Idx.toSearchEntry (filterBad recipes)))))
|
||||||
-- Inline JS
|
-- Inline JS
|
||||||
H.script ! A.type_ "text/javascript" $ H.toHtml searchJs
|
H.script ! A.type_ "text/javascript" $ H.preEscapedText searchJs
|
||||||
where
|
where
|
||||||
filterOk = filter (isRight . Idx.riRecipe)
|
filterOk = filter (isRight . Idx.riRecipe)
|
||||||
filterBad = filter (isLeft . Idx.riRecipe)
|
filterBad = filter (isLeft . Idx.riRecipe)
|
||||||
@@ -634,5 +635,3 @@ isLeft _ = False
|
|||||||
-- | Check if an 'Either' is 'Right'.
|
-- | Check if an 'Either' is 'Right'.
|
||||||
isRight :: Either a b -> Bool
|
isRight :: Either a b -> Bool
|
||||||
isRight = not . isLeft
|
isRight = not . isLeft
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user