spyne
Version:
Reactive Real-DOM Framework for Advanced Javascript applications
223 lines (176 loc) • 6.56 kB
JavaScript
import { includes, __, ifElse, path, prop, reject, is, isNil, isEmpty } from 'ramda'
import sanitizeHTML from '../utils/sanitize-html.js'
import { SpyneAppProperties } from '../utils/spyne-app-properties.js'
export class DomElementTemplate {
constructor(template, data = {}, opts = {}) {
this.template = this.formatTemplate(template)
this.isProxyData = data.__cms__isProxy === true
this.testMode = opts?.testMode
if (this.isProxyData === true) {
if (SpyneAppProperties.enableCMSProxies === true) {
this.template = SpyneAppProperties.formatTemplateForProxyData(this.template)
}
}
const checkForArrayData = () => {
if (is(Array, data) === true) {
data = { spyneData:data }
this.template = this.template.replace('{{/}}', '{{/spyneData}}')
this.template = this.template.replace('{{#}}', '{{#spyneData}}')
}
}
checkForArrayData()
this.templateData = data
const strArr = DomElementTemplate.getStringArray(this.template)
let strMatches = this.template.match(DomElementTemplate.findTmplLoopsRE())
strMatches = strMatches === null ? [] : strMatches
const parseTmplLoopsRE = DomElementTemplate.parseTmplLoopsRE()
const parseTmplLoopFn = this.parseTheTmplLoop.bind(this)
const mapTmplLoop = (str, data) => {
return str.replace(parseTmplLoopsRE, parseTmplLoopFn)
}
const findTmplLoopsPred = includes(__, strMatches)
const checkForMatches = ifElse(
findTmplLoopsPred,
mapTmplLoop,
this.addParams.bind(this))
this.finalArr = strArr.map(checkForMatches)
}
static isPrimitiveTag(str) {
return /({{\.\*?}})/.test(str)
}
// FIND CORRECT NESTED DATA
static getNestedDataReducer(data = {}, param = '') {
const dataReducer = (nestedData, str) => {
if (nestedData[str]) {
return nestedData[str]
}
return typeof nestedData === 'string' ? nestedData : ''
}
return /(\.)/gm.test(String(param)) ? String(param).split('.').reduce(dataReducer, data) : data[param] ?? ''
}
static getStringArray(template) {
const strArr = template.split(DomElementTemplate.findTmplLoopsRE())
const emptyRE = /^([\\n\s\W]+)$/
const filterOutEmptyStrings = s => s.match(emptyRE)
const finalStr = reject(filterOutEmptyStrings, strArr)
return finalStr
}
static findTmplLoopsRE() {
return /({{#[\w.]+}}[\w\n\s\W]+?{{\/[\w.]+}})/gm
}
static parseTmplLoopsRE() {
return /({{#([\w.]+)}})([\w\n\s\W]+?)({{\/\2}})/gm
}
static swapParamsForTagsRE() {
return /({{)(.*?)(}})/gm
}
removeThis() {
if (this !== undefined) {
this.finalArr = undefined
this.templateData = undefined
this.template = undefined
}
}
/**
*
* @desc Returns a document fragment generated from the template and any added data.
*/
renderDocFrag() {
let html = DomElementTemplate.replaceImgPath(this.finalArr.join(''))
if (this.testMode !== true) {
html = sanitizeHTML(html)
}
const isTableSubTag = /^([^>]*?)(<){1}(\b)(thead|col|colgroup|tbody|td|tfoot|tr|th)(\b)([^\0]*)$/.test(html)
const el = isTableSubTag ? html : document.createRange().createContextualFragment(html)
window.setTimeout(this.removeThis, 2)
return el
}
renderToString() {
let html = this.finalArr.join('')
html = DomElementTemplate.replaceImgPath(html)
window.setTimeout(this.removeThis, 2)
return html
}
getTemplateString() {
return this.finalArr.join('')
}
formatTemplate(template) {
return ['SCRIPT', 'TEMPLATE'].includes(prop('nodeName', template)) === true ? template.innerHTML : template
}
getDataValFromPathStr(pathStr, dataFile) {
const pathArr = String(pathStr).split('.')
const pathData = path(pathArr, dataFile)
return pathData || ''
}
addParams(str) {
const replaceTags = (str, p1, p2, p3) => {
return DomElementTemplate.getNestedDataReducer(this.templateData, p2)
}
return str.replace(DomElementTemplate.swapParamsForTagsRE(), replaceTags)
}
static replaceImgPath(templateStr) {
const { IMG_PATH } = SpyneAppProperties
if (IMG_PATH !== undefined) {
templateStr = templateStr.replaceAll(/src\s*=\s*(['"])imgs\//g, `src=$1${IMG_PATH}`)
return templateStr.replaceAll(/url\(\s*(['"]?)imgs\//g, `url($1${IMG_PATH}`)
}
return templateStr
}
parseTheTmplLoop(str, p1, p2, p3) {
const reDot = /(\.)/gm
const subStr = p3
let elData = DomElementTemplate.getNestedDataReducer(this.templateData, p2)
const arrayStringToObjAdapter = (d, str, i) => {
if (DomElementTemplate.isPrimitiveTag(str)) {
return parseString(d, str, i)
}
const createDataObj = () => {
const spyneLoopKey = d
const loopIndex = i
const loopNum = i + 1
if (this.isProxyData) {
const __cms__dataId = elData.__cms__dataId
const keyIdStr = `__cms__keyFor_${d}`
const origKey = elData[keyIdStr]
return { spyneLoopKey, __cms__dataId, origKey, loopIndex, loopNum, d }
}
return { spyneLoopKey, loopIndex, loopNum }
}
return parseObject(createDataObj(), str, i)
}
const parseString = (item, str, index, origIndex) => {
item = item.replace(/\$/g, '$$$$') // $ → $$
return str.replace(DomElementTemplate.swapParamsForTagsRE(), item)
}
// PARSING ARRAYS AND OBJECTS
const parseObject = (obj, str, i) => {
/// LOOP NUMBER VALUES AUTO ADDED
// const loopIndex = i;
// const loopNum = i+1;
const loopObj = (str, p1, p2) => {
// DOT SYNTAX CHECK
const hash = {
loopIndex: i,
loopNum: i + 1
}
// IF {{.}}
if (reDot.test(p2) === false && obj[p2] !== undefined) {
return hash[p2] !== undefined ? hash[p2] : obj[p2]
}
const dataReducedVal = this.getDataValFromPathStr(p2, obj)
return hash[p2] !== undefined ? hash[p2] : dataReducedVal
}
return str.replace(DomElementTemplate.swapParamsForTagsRE(), loopObj)
}
const mapStringData = (d, i) => typeof (d) === 'string' ? arrayStringToObjAdapter(d, subStr, i) : parseObject(d, subStr, i)
if (isNil(elData) === true || isEmpty(elData)) {
return ''
}
if (elData.length === undefined) {
elData = [elData]
}
// convert to array if is string
elData = Array.isArray(elData) ? elData : [elData]
return elData.map(mapStringData).join('')
}
}