tgweb
Version:
Teamgenik Website Builder Offline Tool
1,574 lines (1,279 loc) • 60.6 kB
JavaScript
import * as PATH from "path"
import fs from "fs"
import { escape } from "html-escaper"
import render from "dom-serializer"
import getType from "./get_type.mjs"
import { parseDocument, DomUtils } from "htmlparser2"
import { getDocumentProperties } from "./get_document_properties.mjs"
import { getTitle } from "./get_title.mjs"
import { getWrapper } from "./get_wrapper.mjs"
import { getLayout } from "./get_layout.mjs"
import { filterArticles } from "./filter_articles.mjs"
import { sortArticles } from "./sort_articles.mjs"
import { inspectDom } from "../utils/inspect_dom.mjs"
import { mergeProperties } from "./merge_properties.mjs"
if (inspectDom === undefined) { inspectDom() }
const __dirname = import.meta.dirname
const dataPath = PATH.resolve(PATH.join(__dirname, "..", "..", "resources", "material_symbols.txt"))
const symbolsData = fs.readFileSync(dataPath)
const symbolNameCodepointMapping = {}
symbolsData.toString().split("\n").map(line => {
const a = line.split(" ")
symbolNameCodepointMapping[a[0]] = a[1]
})
const renderWebPage = (path, siteData) => {
const type = getType(path)
if (type === "page") return renderPage(path, siteData)
else if (type === "article") return renderArticle(path, siteData)
}
const renderPage = (path, siteData) => {
const relPath = path.replace(/^src\//, "")
const page = siteData.pages.find(page => page.path == relPath)
const state =
{
path,
container: undefined,
innerContent: [],
inserts: [],
referencedComponentNames: [],
referencedSegmentNames: [],
hookName: undefined,
itemIndex: 0
}
if (page === undefined) {
console.log(`Page '${relPath}' is not found.`)
return
}
const wrapper = getWrapper(siteData, page.path)
const layout = getLayout(siteData, page, wrapper)
const documentProperties = getDocumentProperties(page, wrapper, layout, siteData.properties)
documentProperties["title"] = getTitle(documentProperties, page.dom)
if (wrapper) {
return applyWrapper(page, wrapper, layout, siteData, documentProperties, state)
}
else {
if (layout) {
return applyLayout(page, layout, siteData, documentProperties, mergeState(state, {container: layout}))
}
else {
return doRenderPage(page, siteData, documentProperties, state)
}
}
}
const renderArticle = (path, siteData) => {
const relPath = path.replace(/^src\//, "")
const article = siteData.articles.find(article => article.path == relPath)
if (article === undefined) {
console.log(`Article '${relPath}' is not found.`)
return
}
const mainSection =
typeof article.frontMatter.main === "object" ? article.frontMatter.main : {}
if (mainSection["embedded-only"] === true) return
const state = {path, container: undefined, innerContent: [], inserts: []}
const wrapper = getWrapper(siteData, article.path)
const layout = getLayout(siteData, article, wrapper)
const documentProperties = getDocumentProperties(article, wrapper, layout, siteData.properties)
documentProperties["title"] = getTitle(documentProperties, article.dom)
if (wrapper) {
return applyWrapper(article, wrapper, layout, siteData, documentProperties, state)
}
else {
if (layout) {
return applyLayout(article, layout, siteData, documentProperties, mergeState(state, {container: layout}))
}
else {
return doRenderPage(article, siteData, documentProperties, state)
}
}
}
const applyLayout = (page, layout, siteData, documentProperties, state) => {
const doc = parseDocument("<html><head></head><body></body></html>")
const head = renderHead(documentProperties, siteData)
if (documentProperties.main["html-class"] !== undefined) {
doc.children[0].attribs = {class: documentProperties.main["html-class"]}
}
doc.children[0].children[0].children = head.children
const pageState = mergeState(state, {container: page})
const pageContent =
page.dom.children
.map(child => renderNode(child, siteData, documentProperties, pageState))
.flat()
const localState =
getLocalState(state, layout, pageContent, page.inserts)
const rendered =
layout.dom.children
.map(child => renderNode(child, siteData, documentProperties, localState))
.flat()
.map(c => postprocess(c, {}))
.flat()
doc.children[0].children.pop()
rendered.forEach(child => doc.children[0].children.push(child))
return doc
}
const applyWrapper = (page, wrapper, layout, siteData, documentProperties, state) => {
const doc = parseDocument("<html><head></head><body></body></html>")
const head = renderHead(documentProperties, siteData)
if (documentProperties.main["html-class"] !== undefined) {
doc.children[0].attribs = {class: documentProperties.main["html-class"]}
}
doc.children[0].children[0].children = head.children
const pageState = mergeState(state, {container: page})
const pageContent =
page.dom.children
.map(child => renderNode(child, siteData, documentProperties, pageState))
.flat()
const localState = getLocalState(state, wrapper, pageContent, page.inserts)
const renderedWrapper =
wrapper.dom.children
.map(child => renderNode(child, siteData, documentProperties, localState))
.flat()
if (layout) {
const localState = getLocalState(state, layout, renderedWrapper)
const rendered =
layout.dom.children
.map(child => renderNode(child, siteData, documentProperties, localState))
.flat()
.map(c => postprocess(c, {}))
.flat()
doc.children[0].children.pop()
rendered.forEach(child => doc.children[0].children.push(child))
return doc
}
else {
doc.children[0].children[1].children = renderedWrapper
return doc
}
}
const doRenderPage = (page, siteData, documentProperties, state) => {
const doc = parseDocument("<html><head></head><body></body></html>")
const head = renderHead(documentProperties, siteData)
if (documentProperties.main["html-class"] !== undefined) {
doc.children[0].attribs = {class: documentProperties.main["html-class"]}
}
doc.children[0].children[0].children = head.children
const localState = getLocalState(state, page, undefined)
const renderedPage =
page.dom.children
.map(child => renderNode(child, siteData, documentProperties, localState))
.flat()
.map(c => postprocess(c, {}))
.flat()
doc.children[0].children[1].children = renderedPage
return doc
}
const renderNode = (node, siteData, documentProperties, state) => {
if (node == undefined) return err("undefined")
const klass = node.constructor.name
if (klass === "Document") {
console.log("renderNode() does not accept a Document as the first argument.")
}
else if (klass === "Element") {
if (node.name === "tg:content") {
if (state.innerContent !== undefined) return state.innerContent
return err(render(node))
}
else if (node.name === "tg:segment") {
return renderSegment(node, siteData, documentProperties, state)
}
else if (node.name === "tg:component") {
return renderComponent(node, siteData, documentProperties, state)
}
else if (node.name === "tg:shared-component") {
return renderSharedComponent(node, siteData, documentProperties, state)
}
else if (node.name === "tg:slot") {
return renderSlot(node, siteData, documentProperties, state)
}
else if (node.name === "tg:prop") {
return renderProp(node, siteData, documentProperties, state)
}
else if (node.name === "tg:data") {
return renderData(node, siteData, documentProperties, state)
}
else if (node.name === "tg:if-complete") {
return renderIfComplete(node, siteData, documentProperties, state)
}
else if (node.name === "tg:article") {
return renderEmbeddedArticle(node, siteData, state)
}
else if (node.name === "tg:articles") {
return renderEmbeddedArticleList(node, siteData, state)
}
else if (node.name === "tg:if-embedded") {
return renderIfEmbedded(node, documentProperties, siteData, state)
}
else if (node.name === "tg:unless-embedded") {
return renderUnlessEmbedded(node, documentProperties, siteData, state)
}
else if (node.name === "tg:link") {
return renderLink(node, documentProperties, siteData, state)
}
else if (node.name === "tg:links") {
return renderLinks(node, documentProperties, siteData, state)
}
else if (node.name === "tg:label") {
return renderLabel(node, state)
}
else if (node.name === "tg:animation") {
return renderAnimation(node)
}
else if (node.name === "tg:symbol") {
return renderSymbol(node)
}
else if (node.name === "tg:plugin") {
return node
}
else if (node.name === "tg:app") {
return renderAppPlaceholder(node, siteData)
}
else if (node.name === "a") {
return renderAnchor(node, siteData, documentProperties, state)
}
else {
return renderElement(node, siteData, documentProperties, state)
}
}
else {
return node
}
}
const renderSegment = (node, siteData, documentProperties, state) => {
const segmentName = node.attribs.name
if (state.referencedSegmentNames && state.referencedSegmentNames.includes(segmentName))
return err(render(node))
const allowedTypes = ["page", "layout", "segment", "wrapper"]
if (state.container && allowedTypes.includes(state.container.type)) {
const segment = siteData.segments.find(c => c.path == `segments/${segmentName}.html`)
if (segment === undefined) console.log({notFound: segmentName})
if (segment === undefined) return err(render(node))
let properties = Object.assign({}, documentProperties)
properties = mergeProperties(documentProperties, segment.frontMatter)
Object.keys(node.attribs).forEach(key => {
if (key.startsWith("data-")) {
const propName = toKebabCase(key.slice(5))
properties.data[propName] = node.attribs[key]
}
})
const inserts = getInserts(node)
const innerContent = removeInserts(node)
const localState = getLocalState(state, segment, innerContent, inserts)
if (localState.referencedSegmentNames) localState.referencedSegmentNames.push(segmentName)
return segment.dom.children
.map(child => renderNode(child, siteData, properties, localState))
.flat()
}
else {
return err(render(node))
}
}
const renderComponent = (node, siteData, documentProperties, state) => {
const componentName = node.attribs.name
if (state.referencedComponentNames && state.referencedComponentNames.includes(componentName))
return err(render(node))
if (state.referencedComponentNames &&
state.referencedComponentNames.some(name => name.startsWith("shared_components/")))
return err(render(node))
const component = siteData.components.find(c => c.path == `components/${componentName}.html`)
if (component === undefined) return err(render(node))
let properties = Object.assign({}, documentProperties)
properties = mergeProperties(documentProperties, component.frontMatter)
if (properties.data === undefined) properties.data = {}
Object.keys(node.attribs).forEach(key => {
if (key.startsWith("data-")) {
const propName = toKebabCase(key.slice(5))
properties.data[propName] = node.attribs[key]
}
})
const inserts = getInserts(node)
const innerContent = removeInserts(node)
const localState = getLocalState(state, component, innerContent, inserts)
if (localState.referencedComponentNames) localState.referencedComponentNames.push(componentName)
return component.dom.children
.map(child => renderNode(child, siteData, properties, localState))
.flat()
}
const renderSharedComponent = (node, siteData, documentProperties, state) => {
const componentName = "shared_components/" + node.attribs.name
if (state.referencedComponentNames && state.referencedComponentNames.includes(componentName))
return err(render(node))
const component =
siteData.sharedComponents.find(c => c.path == `${componentName}.html`)
if (component === undefined) return err(render(node))
let properties = Object.assign({}, documentProperties)
properties = mergeProperties(documentProperties, component.frontMatter)
if (properties.data === undefined) properties.data = {}
Object.keys(node.attribs).forEach(key => {
if (key.startsWith("data-")) {
const propName = toKebabCase(key.slice(5))
properties.data[propName] = node.attribs[key]
}
})
const inserts = getInserts(node)
const innerContent = removeInserts(node)
const localState = getLocalState(state, component, innerContent, inserts)
if (localState.referencedComponentNames) localState.referencedComponentNames.push(componentName)
return component.dom.children
.map(child => renderNode(child, siteData, properties, localState))
.flat()
}
const getInserts = (node) => {
const inserts = {}
node.children
.filter(child => child.constructor.name === "Element" && child.name === "tg:insert")
.forEach(child => {
const name = child.attribs.name
if (name) inserts[name] = child
})
return inserts
}
const removeInserts = (node) =>
node.children.filter(child =>
child.constructor.name !== "Element" || child.name !== "tg:insert"
)
const renderSlot = (node, siteData, documentProperties, state) => {
const insert = state.inserts && state.inserts[node.attribs.name] || node
return insert.children
.map(child => renderNode(child, siteData, documentProperties, state))
.flat()
}
const renderProp = (node, siteData, documentProperties, state) => {
const value = documentProperties.main[node.attribs.name]
if (value) {
const textNode = parseDocument("\n").children[0]
textNode.data = escape(value)
return textNode
}
else {
return node.children
.map(child => renderNode(child, siteData, documentProperties, state))
.flat()
}
}
const renderData = (node, siteData, documentProperties, state) => {
if (typeof documentProperties.data !== "object") return node
const value = documentProperties.data[node.attribs.name]
if (value) {
const textNode = parseDocument("\n").children[0]
if (value.constructor.name === "LocalDate") {
const str = value + ""
const parts = str.match(/(\d{4})(\d{2})(\d{2})/)
textNode.data = parts.slice(1).join("-")
}
else if (value.constructor.name === "OffsetDateTime") {
const dt_value = parseInt(value + "")
const dt = new Date(dt_value)
textNode.data = dt.toISOString()
}
else if (value.constructor.name === "LocalTime") {
const str = value + ""
const parts = str.match(/(\d{2})(\d{2})(\d{2})/)
textNode.data = parts.slice(1).join(":")
}
else {
textNode.data = escape(value)
}
return textNode
}
else {
return node.children
.map(child => renderNode(child, siteData, documentProperties, state))
.flat()
}
}
const renderIfComplete = (node, siteData, documentProperties, state) => {
const placeholders =
DomUtils.find(n => {
if (n.constructor.name === "Element") {
if (n.name === "tg:prep") return true
if (n.name === "tg:data") return true
if (n.name === "tg:slot") return true
return false
}
}, node.children, true)
if (placeholders.every(p => {
if (p.name === "tg:prep") return documentProperties[p.attribs.name] !== undefined
if (p.name === "tg:data" && documentProperties.data === undefined) return false
if (p.name === "tg:data") return documentProperties.data[p.attribs.name] !== undefined
if (p.name === "tg:slot") return state.inserts[p.attribs.name] !== undefined
return false
})) {
return node.children
.map(child => renderNode(child, siteData, documentProperties, state))
.flat()
}
else {
return []
}
}
const renderEmbeddedArticle = (node, siteData, state) => {
const articleName = node.attribs.name
if (state.container && (
state.container.type === "page" ||
state.container.type === "segment" ||
state.container.type === "wrapper" ||
state.container.type === "layout")) {
const article = siteData.articles.find(a => a.path == `articles/${articleName}.html`)
if (article === undefined) return err(render(node))
if (siteData.options.buildDrafts !== true && article.frontMatter.main &&
article.frontMatter.main.draft === true) return []
return doRenderEmbeddedArticle(article, node, siteData, state)
}
else {
return err(render(node))
}
}
const renderEmbeddedArticleList = (node, siteData, state) => {
const pattern = node.attribs.pattern
const tag = getTag(node.attribs.filter)
const orderBy = node.attribs["order-by"]
if (state.container && (
state.container.type === "page" ||
state.container.type === "segment" ||
state.container.type === "wrapper" ||
state.container.type === "layout")) {
let articles = filterArticles(siteData.articles, pattern, tag)
sortArticles(articles, orderBy)
if (siteData.options.buildDrafts !== true) {
articles =
articles.filter(a =>
a.frontMatter.main === undefined || a.frontMatter.main.draft !== true
)
}
return articles.map(article => doRenderEmbeddedArticle(article, node, siteData, state)).flat()
}
else {
return err(render(node))
}
}
const doRenderEmbeddedArticle = (article, parent, siteData, state) => {
const inserts = getInserts(parent)
const innerContent = removeInserts(parent)
const localState = getLocalState(state, article, innerContent, inserts)
const wrapper = getWrapper(siteData, article.path)
const properties = getDocumentProperties(article, wrapper, undefined, siteData.properties)
Object.keys(parent.attribs).forEach(key => {
if (key.startsWith("data-")) {
const propName = toKebabCase(key.slice(5))
properties.data[propName] = parent.attribs[key]
}
})
const articleContent =
article.dom.children
.map(child => renderNode(child, siteData, properties, localState))
.flat()
state.itemIndex = localState.itemIndex
if (wrapper) {
const localState2 = getLocalState(state, wrapper, articleContent, article.inserts)
const content =
wrapper.dom.children
.map(child => renderNode(child, siteData, properties, localState2))
.flat()
state.itemIndex = localState2.itemIndex
return content
}
else {
return articleContent
}
}
const renderIfEmbedded = (node, properties, siteData, state) => {
if (state.container.path.startsWith("articles")) {
if (state.path.startsWith("src/articles/")) {
return []
}
else {
return node.children
.map(child => renderNode(child, siteData, properties, state))
.flat()
}
}
else {
return err(render(node))
}
}
const renderUnlessEmbedded = (node, properties, siteData, state) => {
if (state.container.path.startsWith("articles")) {
if (state.path.startsWith("src/articles/")) {
return node.children
.map(child => renderNode(child, siteData, properties, state))
.flat()
}
else {
return []
}
}
else {
return err(render(node))
}
}
const renderLink = (node, properties, siteData, state) => {
const localState =
mergeState(state, {container: node, targetPath: node.attribs.href, label: node.attribs.label})
let targetPath = localState.targetPath
if (targetPath.startsWith("/articles/")) {
const article = siteData.articles.find(a => `/${a.path}` === targetPath)
if (siteData.options.buildDrafts !== true && article && article.frontMatter.main &&
article.frontMatter.main.draft === true) return []
}
else {
targetPath = targetPath === "/" ? "index.html" : targetPath
const page = siteData.pages.find(p => `/${p.path}` === targetPath)
if (siteData.options.buildDrafts !== true && page && page.frontMatter.main &&
page.frontMatter.main.draft === true) return []
}
let children
if (node.attribs.component !== undefined) {
const component =
siteData.components.find(c => c.path === `components/${node.attribs.component}.html`)
if (component) {
children = component.dom.children
}
else {
children = node.children
}
}
else if (node.attribs.sharedComponent !== undefined) {
const component =
siteData.sharedComponents.find(c =>
c.path == `shared_components/${node.attribs.component}.html`
)
if (component) {
children = component.dom.children
}
else {
children = node.children
}
}
else {
children = node.children
}
const href = state.path.replace(/^src\//, "").replace(/^pages/, "").replace(/\bindex.html$/, "")
if (localState.targetPath === href) {
const fallback =
DomUtils.findOne(
n => n.constructor.name === "Element" && n.name === "tg:if-current",
children,
true
)
if (fallback)
return fallback.children
.map(child => renderNode(child, siteData, properties, localState))
.flat()
else
return []
}
else {
return children.map(child => {
if (child.constructor.name === "Element" && child.name === "tg:if-current") return []
return renderNode(child, siteData, properties, localState)
})
.flat()
}
}
const renderLinks = (node, documentProperties, siteData, state) => {
const pattern = node.attribs.pattern
const tag = getTag(node.attribs.filter)
const orderBy = node.attribs["order-by"]
if (state.container && (state.container.type !== "links")) {
let articles = filterArticles(siteData.articles, pattern, tag)
if (orderBy !== undefined) sortArticles(articles, orderBy)
if (siteData.options.buildDrafts !== true) {
articles =
articles.filter(a =>
a.frontMatter.main === undefined || a.frontMatter.main.draft !== true
)
}
return articles
.map(article => renderArticleLink(node, article, siteData, state))
.flat()
.filter(node => !Array.isArray(node))
}
else {
return err(render(node))
}
}
const renderArticleLink = (node, article, siteData, state) => {
const href = `/${article.path}`.replace(/\/index.html$/, "/")
const localState = mergeState(state, {targetPath: href, inserts: article.inserts})
let children
if (node.attribs.component !== undefined) {
const component =
siteData.components.find(c => c.path === `components/${node.attribs.component}.html`)
if (component) {
children = component.dom.children
}
else {
children = node.children
}
}
else if (node.attribs.sharedComponent !== undefined) {
const component =
siteData.sharedComponents.find(c =>
c.path === `shared_components/${node.attribs.component}.html`
)
if (component) {
children = component.dom.children
}
else {
children = node.children
}
}
else {
children = node.children
}
if (`src/${article.path}` === state.path) {
const fallback =
DomUtils.findOne(
n => n.constructor.name === "Element" && n.name === "tg:if-current",
children,
true
)
if (fallback)
return fallback.children
.map(child => renderNode(child, siteData, article.frontMatter, localState))
.flat()
else
return []
}
else {
return children.map(child => {
if (child.constructor.name === "Element" && child.name === "tg:if-current") return []
return renderNode(child, siteData, article.frontMatter, localState)
})
}
}
const renderLabel = (node, state) => {
if (state.container.name === "tg:link" || state.container.name === "tg:links") {
if (state.label !== "") {
const textNode = parseDocument("\n").children[0]
textNode.data = escape(state.label)
return textNode
}
}
else {
return err(render(node))
}
}
const renderAnimation = (node) => {
const canvas = parseDocument(`<canvas data-animation="lottie"></canvas>`).children[0]
if (node.attribs["src"] && node.attribs["src"] !== "") {
canvas.attribs["data-src"] = node.attribs["src"]
if (["false", "true"].includes(node.attribs["autoplay"]))
canvas.attribs["data-autoplay"] = node.attribs["autoplay"]
if (["false", "true"].includes(node.attribs["loop"]))
canvas.attribs["data-loop"] = node.attribs["loop"]
if (["false", "true"].includes(node.attribs["hover"]))
canvas.attribs["data-hover"] = node.attribs["hover"]
if (["false", "true"].includes(node.attribs["click"]))
canvas.attribs["data-click"] = node.attribs["click"]
if (node.attribs["class"]) canvas.attribs["class"] = node.attribs["class"]
if (node.attribs["width"]) canvas.attribs["width"] = node.attribs["width"]
if (node.attribs["height"]) canvas.attribs["height"] = node.attribs["height"]
return canvas
}
else {
return err(render(node))
}
}
const renderSymbol = (node) => {
const name = node.attribs["name"]
const codepoint = symbolNameCodepointMapping[name]
if (codepoint) {
const symbolStyle = node.attribs["symbol-style"] || "outlined"
const span = parseDocument(`<span>&#x${codepoint};</span>`).children[0]
const classTokens = []
const styleNameParts = symbolStyle.split("-")
if (styleNameParts.length === 1) {
classTokens.push(`material-symbols-${symbolStyle}`)
}
else {
const styleName = styleNameParts[0]
const variant = styleNameParts.slice(1).join("-")
classTokens.push(`material-symbols-${styleName}`)
classTokens.push(`material-symbols-${styleName}-${variant}`)
}
span.attribs["class"] = classTokens.join(" ")
const settings = []
if (["0", "1"].includes(node.attribs["fill"])) settings.push(`'FILL' ${node.attribs["fill"]}`)
if (["100", "200", "300", "400", "500", "600", "700"].includes(node.attribs["wght"]))
settings.push(`'wght' ${node.attribs["wght"]}`)
if (["-25", "0", "200"].includes(node.attribs["grad"]))
settings.push(`'GRAD' ${node.attribs["grad"]}`)
if (["20", "24", "40", "48"].includes(node.attribs["opsz"]))
settings.push(`'opsz' ${node.attribs["opsz"]}`)
if (settings.length > 0) {
span.attribs["style"] = `font-variation-settings: ${settings.join(", ")}`
}
return span
}
else {
return err(render(node))
}
}
const renderAppPlaceholder = (node, siteData) => {
const appName = node.attribs.name
if (appName && Array.isArray(siteData.properties.apps)) {
const appConfig = siteData.properties.apps.find(app => app.name === appName)
if (appConfig) {
const displayName = appConfig["display-name"] || appName
const outerNode = parseDocument(`<div></div>`).children[0]
const innerNode = parseDocument(`<div>${displayName}</div>`).children[0]
outerNode.attribs = {
style: `width: 100%; height: 300px; padding: 16px; backdrop-filter: invert(50%);`
}
innerNode.attribs = {
style:
"width: 100%; height: 100%; background-color: white; opacity: 50%; " +
"display: flex; justify-content: center; align-items: center; font-size: 1.5rem;"
}
outerNode.children = [innerNode]
return outerNode
}
else {
return err(render(node))
}
}
else {
return err(render(node))
}
}
const renderAnchor = (node, siteData, documentProperties, state) => {
if (node.attribs.href === "#" && state.targetPath !== undefined) {
const newNode = parseDocument("<a></a>").children[0]
newNode.attribs = Object.assign({}, node.attribs)
newNode.attribs.href = state.targetPath
newNode.children =
node.children
.map(child => renderNode(child, siteData, documentProperties, state))
.flat()
return newNode
}
else {
return renderElement(node, siteData, documentProperties, state)
}
}
const renderElement = (node, siteData, documentProperties, state) => {
const newNode = parseDocument("<div></div>").children[0]
const newState = mergeState(state, {})
newNode.name = node.name
newNode.attribs = Object.assign({}, node.attribs)
convertAttribs(newNode.attribs, documentProperties)
purgeAttribs(newNode.attribs)
if (newNode.attribs["tg:toggler"] !== undefined && state.hookName === undefined)
addTogglerHook(newNode, newState)
if (newNode.attribs["tg:switcher"] !== undefined && state.hookName === undefined)
addSwitcherHook(node, newNode, newState)
if (newNode.attribs["tg:rotator"] !== undefined && state.hookName === undefined)
addRotatorHook(node, newNode, newState)
if (newNode.attribs["tg:carousel"] !== undefined && state.hookName === undefined)
addCarouselHook(node, newNode, newState)
if (newNode.attribs["tg:modal"] !== undefined && state.hookName === undefined)
addModalHook(newNode, newState)
if (newNode.attribs["tg:scheduler"] !== undefined && state.hookName === undefined)
addSchedulerHook(newNode, newState)
if (newNode.attribs["tg:tram"] !== undefined && state.hookName === undefined)
addTramHook(newNode, newState)
if (state.hookName === "toggler") addTogglerSubhooks(newNode)
else if (state.hookName === "switcher") addSwitcherSubhooks(newNode, state)
else if (state.hookName === "rotator") addRotatorSubhooks(newNode, state)
else if (state.hookName === "carousel") addCarouselSubhooks(newNode)
else if (state.hookName === "modal") addModalSubhooks(newNode)
else if (state.hookName === "tram") addTramSubhooks(newNode)
newNode.children =
node.children
.map(child => renderNode(child, siteData, documentProperties, newState))
.flat()
return newNode
}
const convertAttribs = (attribs, documentProperties) => {
Object.keys(attribs).forEach(key => {
attribs[key] = expandCustomProperties(attribs[key], documentProperties)
})
}
const expandCustomProperties = (value, documentProperties) =>
value.replaceAll(/\$\{(\w+(?:-\w+)*)\}/g, (_, propName) => {
if (documentProperties.data === undefined) return `\${${propName}}`
else if (documentProperties.data[propName] !== undefined)
return documentProperties.data[propName]
else return `\${${propName}}`
})
// Toggler
const addTogglerHook = (newNode, newState) => {
newNode.attribs["x-data"] = `{ f: false }`
newNode.attribs["x-on:click"] = `f = false`
newNode.attribs["x-on:click.outside"] = `f = false`
newState.hookName = "toggler"
}
const addTogglerSubhooks = (newNode) => {
const enebledClass = (newNode.attribs["tg:enabled-class"] || "").replace(/'/, "\\'")
const disabledClass = (newNode.attribs["tg:disabled-class"] || "").replace(/'/, "\\'")
if (newNode.attribs["tg:when"] === "on") {
newNode.attribs["x-show"] = `f === true`
newNode.attribs["x-cloak"] = `x-cloak`
}
else if (newNode.attribs["tg:when"] === "off") newNode.attribs["x-show"] = `f === false`
if (newNode.attribs["tg:toggle"] === "on") {
newNode.attribs["x-on:click.stop"] = "f = true"
newNode.attribs["x-bind:class"] = `f === true ? '${disabledClass}' : '${enebledClass}'`
}
else if (newNode.attribs["tg:toggle"] === "off") {
newNode.attribs["x-on:click.stop"] = "f = false"
newNode.attribs["x-bind:class"] = `f === false ? '${disabledClass}' : '${enebledClass}'`
}
else if (newNode.attribs["tg:toggle"] === "") {
newNode.attribs["x-on:click.stop"] = "f = !f"
newNode.attribs["x-bind:class"] = `'${enebledClass}'`
}
}
// Switcher
const addSwitcherHook = (node, newNode, newState) => {
newState.hookName = "switcher"
newState.itemIndex = 0
let transitionDuration
if (newNode.attribs["tg:transition-duration"] !== undefined) {
transitionDuration = parseInt(newNode.attribs["tg:transition-duration"], 10)
}
if (! Number.isNaN(transitionDuration)) {
newState.transitionDuration = transitionDuration
}
else {
newState.transitionDuration = 0
}
}
const addSwitcherSubhooks = (newNode, state) => {
const enebledClass = (newNode.attribs["tg:enabled-class"] || "").replace(/'/, "\\'")
const disabledClass = (newNode.attribs["tg:disabled-class"] || "").replace(/'/, "\\'")
const currentClass = (newNode.attribs["tg:current-class"] || "").replace(/'/, "\\'")
const normalClass = (newNode.attribs["tg:normal-class"] || "").replace(/'/, "\\'")
if (newNode.attribs["tg:body"] !== undefined) {
newNode.attribs["data-switcher-body"] = ""
}
else if (newNode.attribs["tg:item"] !== undefined) {
newNode.attribs["data-item-index"] = String(state.itemIndex)
addTransitionEffect(newNode, state)
state.itemIndex = state.itemIndex + 1
}
if (newNode.attribs["tg:first"] !== undefined) {
newNode.attribs["x-on:click"] = "first()"
newNode.attribs["x-bind:class"] = `i === 0 ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:prev"] !== undefined) {
newNode.attribs["x-on:click"] = "prev()"
newNode.attribs["x-bind:class"] = `i === 0 ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:next"] !== undefined) {
newNode.attribs["x-on:click"] = "next()"
newNode.attribs["x-bind:class"] = `i === len - 1 ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:last"] !== undefined) {
newNode.attribs["x-on:click"] = "last()"
newNode.attribs["x-bind:class"] = `i === len - 1 ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:choose"] !== undefined) {
const n = parseInt(newNode.attribs["tg:choose"], 10)
if (!Number.isNaN(n)) {
newNode.attribs["x-on:click"] = `choose(${n})`
newNode.attribs["x-bind:class"] = `i == ${n} ? '${currentClass}' : '${normalClass}'`
}
}
}
// Rotator
const addRotatorHook = (node, newNode, newState) => {
newState.hookName = "rotator"
newState.itemIndex = 0
let transitionDuration
if (newNode.attribs["tg:transition-duration"] !== undefined) {
transitionDuration = parseInt(newNode.attribs["tg:transition-duration"], 10)
}
if (! Number.isNaN(transitionDuration)) {
newState.transitionDuration = transitionDuration
}
else {
newState.transitionDuration = 0
}
}
const addRotatorSubhooks = (newNode, state) => {
const enebledClass = (newNode.attribs["tg:enabled-class"] || "").replace(/'/, "\\'")
const disabledClass = (newNode.attribs["tg:disabled-class"] || "").replace(/'/, "\\'")
const currentClass = (newNode.attribs["tg:current-class"] || "").replace(/'/, "\\'")
const normalClass = (newNode.attribs["tg:normal-class"] || "").replace(/'/, "\\'")
if (newNode.attribs["tg:body"] !== undefined) {
newNode.attribs["data-rotator-body"] = ""
}
else if (newNode.attribs["tg:item"] !== undefined) {
newNode.attribs["data-item-index"] = String(state.itemIndex)
addTransitionEffect(newNode, state)
state.itemIndex = state.itemIndex + 1
}
if (newNode.attribs["tg:first"] !== undefined) {
newNode.attribs["x-on:click"] = "first()"
newNode.attribs["x-bind:class"] = `i === 0 ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:prev"] !== undefined) {
newNode.attribs["x-on:click"] = "prev()"
newNode.attribs["x-bind:class"] = `'${enebledClass}'`
}
if (newNode.attribs["tg:next"] !== undefined) {
newNode.attribs["x-on:click"] = "next()"
newNode.attribs["x-bind:class"] = `'${enebledClass}'`
}
if (newNode.attribs["tg:last"] !== undefined) {
newNode.attribs["x-on:click"] = "last()"
newNode.attribs["x-bind:class"] = `i === len - 1 ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:choose"] !== undefined) {
const n = parseInt(newNode.attribs["tg:choose"], 10)
if (!Number.isNaN(n)) {
newNode.attribs["x-on:click"] = `choose(${n})`
newNode.attribs["x-bind:class"] = `i == ${n} ? '${currentClass}' : '${normalClass}'`
}
}
}
const addTransitionEffect = (newNode, state) => {
if (state.transitionDuration !== undefined) {
const transitionStyle = `transition: opacity ${state.transitionDuration}ms`
const style00 = `position: absolute; opacity: 1; order: 0`
const style01 = `position: absolute; opacity: 0; order: -1`
const style10 = `position: absolute; opacity: 1; order: 0; ${transitionStyle}`
const style11 = `position: absolute; opacity: 0; order: -1; ${transitionStyle}`
const expr0 = `($el.dataset.itemIndex === String(i) ? '${style00}' : '${style01}')`
const expr1 = `($el.dataset.itemIndex === String(i) ? '${style10}' : '${style11}')`
newNode.attribs["x-bind:style"] = `initial ? ${expr0} : ${expr1}`
}
else {
newNode.attribs["x-show"] = `$el.dataset.itemIndex === String(i)`
}
}
// Carousel
const addCarouselHook = (node, newNode, newState) => {
newState.hookName = "carousel"
}
const addCarouselSubhooks = (newNode) => {
const enebledClass = (newNode.attribs["tg:enabled-class"] || "").replace(/'/, "\\'")
const disabledClass = (newNode.attribs["tg:disabled-class"] || "").replace(/'/, "\\'")
const currentClass = (newNode.attribs["tg:current-class"] || "").replace(/'/, "\\'")
const normalClass = (newNode.attribs["tg:normal-class"] || "").replace(/'/, "\\'")
if (newNode.attribs["tg:frame"] !== undefined) {
newNode.attribs["data-carousel-frame"] = ""
}
else if (newNode.attribs["tg:body"] !== undefined) {
newNode.attribs["data-carousel-body"] = ""
}
else if (newNode.attribs["tg:item"] !== undefined) {
newNode.attribs["data-carousel-item"] = ""
}
if (newNode.attribs["tg:prev"] !== undefined) {
newNode.attribs["x-on:click"] = "prev()"
newNode.attribs["x-bind:class"] = `inTransition ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:next"] !== undefined) {
newNode.attribs["x-on:click"] = "next()"
newNode.attribs["x-bind:class"] = `inTransition ? '${disabledClass}' : '${enebledClass}'`
}
if (newNode.attribs["tg:choose"] !== undefined) {
const n = parseInt(newNode.attribs["tg:choose"], 10)
if (!Number.isNaN(n)) {
newNode.attribs["x-on:click"] = `choose(${n})`
const script = `
i % len === ${n} ?
'${currentClass}' :
(inTransition ? '${disabledClass}' : '${normalClass}')
`
newNode.attribs["x-bind:class"] = script.trim().replaceAll(/\s+/g, " ")
}
}
}
// Modal
const addModalHook = (newNode, newState) => {
newNode.attribs["x-data"] = "{ body: undefined, open: false }"
newNode.attribs["x-init"] = "body = $el.querySelector('dialog')"
newState.hookName = "modal"
if (newNode.attribs["tg:open"] !== undefined) {
newNode.attribs["x-on:click.stop"] = "if (body && !open) body.showModal(); open = true"
}
}
const addModalSubhooks = (newNode) => {
if (newNode.attribs["tg:open"] !== undefined) {
newNode.attribs["x-on:click.stop"] = "if (body && !open) body.showModal(); open = true"
}
else if (newNode.attribs["tg:close"] !== undefined) {
newNode.attribs["x-on:click.stop"] = "if (body && open) body.close(); open = false"
}
}
// Scheduler
const addSchedulerHook = (newNode, newState) => {
newState.hookName = "scheduler"
newNode.attribs["x-data"] = `window.tgweb.scheduler($el)`
addSchedulerSubhooks(newNode)
}
const addSchedulerSubhooks = (newNode) => {
if (newNode.attribs["tg:init"] !== undefined)
newNode.attribs["data-scheduler-init"] = newNode.attribs["tg:init"]
Object.keys(newNode.attribs).forEach(attrName => {
const md = attrName.match(/^tg:(\d+)$/)
if (md === null) return
const time = md[1]
newNode.attribs[`data-scheduler-${time}`] = newNode.attribs[attrName]
})
}
// Tram
const addTramHook = (newNode, newState) => {
newState.hookName = "tram"
newNode.attribs["x-data"] = `window.tgweb.tram($el)`
addTramSubhooks(newNode)
}
const addTramSubhooks = (newNode) => {
if (newNode.attribs["class"] !== undefined)
newNode.attribs["data-tram-base-class"] = newNode.attribs["class"]
else
newNode.attribs["data-tram-base-class"] = ""
let classTokens = []
if (newNode.attribs["class"] !== undefined)
classTokens = newNode.attribs["class"].split(" ")
if (newNode.attribs["tg:init"] !== undefined)
classTokens = classTokens.concat(newNode.attribs["tg:init"].split(" "))
if (classTokens.length > 0) newNode.attribs["class"] = classTokens.join(" ")
const found =
Object.keys(newNode.attribs).some(attrName =>
attrName.startsWith("tg:forward-") || attrName.startsWith("tg:backward-")
)
if (!found) return
newNode.attribs["data-tram-trigger"] = ""
Object.keys(newNode.attribs).forEach(attrName => {
const md = attrName.match(/^tg:(forward|backward)-(\d{1,3})(|%|vh|px)(|\+|-)$/)
if (md === null) return
const direction = md[1]
const distance = parseInt(md[2], 10)
const unit = md[3]
const suffix = md[4]
newNode.attribs[`data-tram-${direction}-${distance}${unit}${suffix}`] =
newNode.attribs[attrName]
})
}
// Postprocess
const postprocess = (node, state) => {
const newState = Object.assign({}, state)
const klass = node.constructor.name
if (klass === "Element") {
if (node.attribs["tg:carousel"] !== undefined) {
newState.hookName = "carousel"
return postprocessHook(node, newState)
}
else if (state.hookName === "carousel") {
if (node.attribs["tg:body"] !== undefined)
return postprocessCarouselBody(node, newState)
else if (node.attribs["tg:paginator"] !== undefined)
return postprocessPaginator(node, newState)
else {
node.children = node.children.map(c => postprocess(c, newState)).flat()
removeTgAttribs(node.attribs)
return node
}
}
else if (node.attribs["tg:switcher"] !== undefined) {
newState.hookName = "switcher"
return postprocessHook(node, newState)
}
else if (node.attribs["tg:rotator"] !== undefined) {
newState.hookName = "rotator"
return postprocessHook(node, newState)
}
else if (state.hookName === "switcher" || state.hookName === "rotator") {
if (node.attribs["tg:paginator"] !== undefined)
return postprocessPaginator(node, newState)
else {
node.children = node.children.map(c => postprocess(c, newState)).flat()
removeTgAttribs(node.attribs)
return node
}
}
else if (node.name === "tg:plugin" && node.attribs.name == "hubspot") {
const script0 = "<script charset='utf-8' type='text/javascript' src='//js.hsforms.net/forms/embed/v2.js'></script>"
const portalId = node.attribs["portal-id"]
const formId = node.attribs["form-id"]
const scriptBody = `hbspt.forms.create({portalId: "${portalId}", formId: "${formId}"});`
return parseDocument(`${script0}<script>${scriptBody}</script>`)
}
else {
node.children = node.children.map(c => postprocess(c, newState)).flat()
removeTgAttribs(node.attribs)
return node
}
}
else {
return node
}
}
const postprocessHook = (node, newState) => {
const items =
DomUtils.findAll(elem => elem.attribs["tg:item"] !== undefined, node.children)
let interval = 0
if (node.attribs["tg:interval"] !== undefined) {
interval = parseInt(node.attribs["tg:interval"], 10)
if (Number.isNaN(interval)) interval = 0
if (interval < 0) interval = 0
}
let transitionDuration = parseInt(node.attribs["tg:transition-duration"], 10)
if (Number.isNaN(transitionDuration)) transitionDuration = 0
newState.itemCount = items.length
if (newState.hookName === "carousel") {
newState.repeatCount = 3
node.attribs["x-data"] =
`window.tgweb.carousel($el, ${items.length}, ${newState.repeatCount}, ${interval}, ${transitionDuration})`
}
else {
node.attribs["x-data"] =
`window.tgweb.${newState.hookName}($el, ${interval}, ${transitionDuration})`
}
node.children = node.children.map(c => postprocess(c, newState)).flat()
removeTgAttribs(node.attribs)
return node
}
const postprocessCarouselBody = (node, newState) => {
const carouselItems =
DomUtils.findAll(elem => elem.attribs["tg:item"] !== undefined, node.children)
const children = []
for (let n = 0; n < newState.repeatCount; n++) {
carouselItems.forEach(item => {
children.push(item.cloneNode(true))
})
}
node.children = children
return node
}
const postprocessPaginator = (node, newState) => {
const disabledClass = (node.attribs["tg:disabled-class"] || "").replace(/'/, "\\'")
const currentClass = (node.attribs["tg:current-class"] || "").replace(/'/, "\\'")
const normalClass = (node.attribs["tg:normal-class"] || "").replace(/'/, "\\'")
const choosers = []
removeTgAttribs(node.attribs)
for (let n = 0; n < newState.itemCount; n++) {
const newNode = parseDocument("<div></div>").children[0]
newNode.name = node.name
newNode.children = node.children
newNode.attribs = Object.assign({}, node.attribs)
newNode.attribs["x-on:click"] = `choose(${n})`
const script = `
i % len === ${n} ?
'${currentClass}' :
(inTransition ? '${disabledClass}' : '${normalClass}')
`
newNode.attribs["x-bind:class"] = script.trim().replaceAll(/\s+/g, " ")
delete newNode.attribs.id
choosers.push(newNode)
}
return choosers
}
const purgeAttribs = (attribs) => {
const keys = Object.keys(attribs).filter(key => key.match(/^(on|x-|:|@)/))
keys.forEach(key => delete attribs[key])
}
const removeTgAttribs = (attribs) => {
const keys = Object.keys(attribs).filter(key => key.match(/^tg:/))
keys.forEach(key => delete attribs[key])
}
const renderHead = (documentProperties, siteData) => {
const head = parseDocument("<head></head>")
const children = []
children.push(parseDocument("<meta charset='utf-8'>").children[0])
if (documentProperties["title"] !== undefined) {
const title = escape(documentProperties["title"])
const doc = parseDocument(`<title>${title}</title>`)
children.push(doc.children[0])
}
if (typeof documentProperties.meta === "object") {
if (typeof documentProperties.meta.name === "object") {
Object.keys(documentProperties.meta.name).forEach(name => {
if (name.match(/"/) !== null) return
const content = documentProperties.meta.name[name]
if (typeof content === "string") {
const doc = parseDocument(`<meta name="${name}" content="${content}">`)
children.push(doc.children[0])
}
else if (Array.isArray(content)) {
content.forEach(e => {
const doc = parseDocument(`<meta name="${name}" content="${e}">`)
children.push(doc.children[0])
})
}
})
}
if (typeof documentProperties.meta["http-equiv"] === "object") {
Object.keys(documentProperties.meta["http-equiv"]).forEach(name => {
if (name.match(/"/) !== null) return
const content = documentProperties.meta["http-equiv"][name]
if (typeof content !== "string") return
const doc = parseDocument(`<meta http-equiv="${name}" content="${content}">`)
children.push(doc.children[0])
})
}
if (typeof documentProperties.meta["property"] === "object") {
Object.keys(documentProperties.meta["property"]).forEach(name => {
if (name.match(/"/) !== null) return
const content = documentProperties.meta["property"][name]
if (typeof content !== "string") return
let converted = content.replaceAll(/\$\{([^}]+)\}/g, (_, propName) => {
const parts = propName.split(".")
if (parts.length === 1) {
const value = documentProperties.main[propName]
if (typeof value === "string") {
return value
}
else {
return `\${${propName}}`
}
}
else if (parts.length === 2) {
const p1 = parts[0]
const p2 = parts[1]
if (typeof documentProperties.main[p1] === "object") {
const value = documentProperties.main[p1][p2]
if (typeof value === "string") {
return value
}
else {
return `\${${propName}}`
}
}
else {
return `\${${propName}}`
}
}
else if (parts.length === 3) {
const p1 = parts[0]
const p2 = parts[1]
const p3 = parts[2]
if (typeof documentProperties[p1] === "object"
&& typeof documentProperties[p1][p2] === "object") {
const value = documentProperties[p1][p2][p3]
if (typeof value === "string") {
return value
}
else {
return `\${${propName}}`
}
}
else {
return `\${${propName}}`
}
}
})
converted = converted.replaceAll(/%\{([^}]+)\}/g, (_, path) => {
const rootUrl = documentProperties.main["root-url"]
return rootUrl + path.replace(/^\//, "")
}).replace(/"/g, """)
const doc = parseDocument(`<meta property="${name}" content="${converted}">`)
children.push(doc.children[0])
})
}
}
if (typeof documentProperties.link === "object") {
Object.keys(documentProperties.link).forEach(rel => {
if (rel == "stylesheet") return
if (rel.match(/^[a-z]+$/) === null) return
const href = documentProperties.link[rel]
const converted = href.replaceAll(/%\{([^}]+)\}/g, (_, path) => {
const rootUrl = documentProperties.main["root-url"]
return rootUrl + path.replace(/^\//, "")
}).replace(/"/g, """)
const d