Skip to content

Commit

Permalink
Moved templating into Mustache.js templates
Browse files Browse the repository at this point in the history
  • Loading branch information
abashurov committed Sep 18, 2019
1 parent 2d3b307 commit 07e5f4c
Show file tree
Hide file tree
Showing 20 changed files with 920 additions and 188 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"presets": ["env"]
"presets": ["@babel/preset-env"]
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,8 @@
"webpack": "^4.16.0",
"webpack-cli": "^3.0.8"
},
"dependencies": {}
"dependencies": {
"@babel/preset-env": "^7.6.0",
"mustache": "^3.1.0"
}
}
17 changes: 17 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
html {
overflow: hidden;
}

.loader {
left: 50%;
position: absolute;
Expand All @@ -6,3 +10,16 @@
width: 40px;
}

.default_text {
line-height:2.5rem;
font-size:2rem;
}

.ruler {
margin:9px;
}

.error_wrapper {
white-space: pre-wrap;
display: flex;
}
46 changes: 23 additions & 23 deletions src/javascripts/lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* @return {Promise} will resolved after resize
*/
export function resizeContainer (client, max = Number.POSITIVE_INFINITY) {
const newHeight = Math.min(document.body.getBoundingClientRect().height, max)
return client.invoke('resize', { height: newHeight })
const newHeight = Math.min(document.body.getBoundingClientRect().height, max)
return client.invoke('resize', { height: newHeight })
}

/**
Expand All @@ -17,9 +17,9 @@ export function resizeContainer (client, max = Number.POSITIVE_INFINITY) {
* @return {String} final template
*/
export function templatingLoop (set, getTemplate, initialValue = '') {
return set.reduce((accumulator, item, index) => {
return `${accumulator}${getTemplate(item, index)}`
}, initialValue)
return set.reduce((accumulator, item, index) => {
return `${accumulator}${getTemplate(item, index)}`
}, initialValue)
}

/**
Expand All @@ -28,11 +28,11 @@ export function templatingLoop (set, getTemplate, initialValue = '') {
* @param {String} htmlString new html string to be rendered
*/
export function render (replacedNodeSelector, htmlString) {
const fragment = document.createRange().createContextualFragment(htmlString)
const replacedNode = document.querySelector(replacedNodeSelector)
if (replacedNode !== null) {
replacedNode.parentNode.replaceChild(fragment, replacedNode)
}
const fragment = document.createRange().createContextualFragment(htmlString)
const replacedNode = document.querySelector(replacedNodeSelector)
if (replacedNode !== null) {
replacedNode.parentNode.replaceChild(fragment, replacedNode)
}
}

/**
Expand All @@ -41,19 +41,19 @@ export function render (replacedNodeSelector, htmlString) {
* @return {String} escaped string
*/
export function escapeSpecialChars (str) {
if (typeof str !== 'string') {
throw new TypeError(`escapeSpecialChars function expects input in type String, received ${typeof(str)}`)
}
if (typeof str !== 'string') {
throw new TypeError(`escapeSpecialChars function expects input in type String, received ${typeof(str)}`)
}

const escape = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;'
}
const escape = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;'
}

return str.replace(/[&<>"'`=]/g, function (m) { return escape[m] })
return str.replace(/[&<>"'`=]/g, function (m) { return escape[m] })
}
82 changes: 41 additions & 41 deletions src/javascripts/lib/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,58 @@ let translations
* @return {String} formatted string
*/
function parsePlaceholders (str, context) {
const regex = /{{(.*?)}}/g
const matches = []
let match
const regex = /{{(.*?)}}/g
const matches = []
let match

do {
match = regex.exec(str)
if (match) matches.push(match)
} while (match)
do {
match = regex.exec(str)
if (match) matches.push(match)
} while (match)

return matches.reduce((str, match) => {
const newRegex = new RegExp(match[0], 'g')
return str.replace(newRegex, context[match[1]] || '')
}, str)
return matches.reduce((str, match) => {
const newRegex = new RegExp(match[0], 'g')
return str.replace(newRegex, context[match[1]] || '')
}, str)
}

class I18n {
constructor (locale) {
this.loadTranslations(locale)
}
constructor (locale) {
this.loadTranslations(locale)
}

tryRequire (locale) {
try {
return require(`../../translations/${locale}.json`)
} catch (e) {
return null
tryRequire (locale) {
try {
return require(`../../translations/${locale}.json`)
} catch (e) {
return null
}
}
}

/**
* Translate key with currently loaded translations,
* optional context to replace the placeholders in the translation
* @param {String} key
* @param {Object} context object contains placeholder/value pairs
* @return {String} tranlated string
*/
t (key, context) {
const keyType = typeof key
if (keyType !== 'string') throw new Error(`Translation key must be a string, got: ${keyType}`)
/**
* Translate key with currently loaded translations,
* optional context to replace the placeholders in the translation
* @param {String} key
* @param {Object} context object contains placeholder/value pairs
* @return {String} tranlated string
*/
t (key, context) {
const keyType = typeof key
if (keyType !== 'string') throw new Error(`Translation key must be a string, got: ${keyType}`)

const template = translations[key]
if (!template) throw new Error(`Missing translation: ${key}`)
if (typeof template !== 'string') throw new Error(`Invalid translation for key: ${key}`)
const template = translations[key]
if (!template) throw new Error(`Missing translation: ${key}`)
if (typeof template !== 'string') throw new Error(`Invalid translation for key: ${key}`)

return parsePlaceholders(template, context)
}
return parsePlaceholders(template, context)
}

loadTranslations (locale = 'en') {
translations = this.tryRequire(locale) ||
this.tryRequire(locale.replace(/-.+$/, '')) ||
translations ||
this.tryRequire('en')
}
loadTranslations (locale = 'en') {
translations = this.tryRequire(locale) ||
this.tryRequire(locale.replace(/-.+$/, '')) ||
translations ||
this.tryRequire('en')
}
}

export default new I18n(manifest.defaultLocale)
8 changes: 4 additions & 4 deletions src/javascripts/locations/organization_sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import tinApp from '../modules/app'
/* global ZAFClient */
var client = ZAFClient.init()

client.on('app.registered', _ => tinApp(client, true))
client.on('organization.tags.changed', _ => tinApp(client))
client.on('pane.activated', _ => tinApp(client))
client.on('app.activated', _ => tinApp(client))
client.on('app.registered', () => tinApp(client, true))
client.on('organization.tags.changed', () => tinApp(client))
client.on('pane.activated', () => tinApp(client))
client.on('app.activated', () => tinApp(client))
8 changes: 4 additions & 4 deletions src/javascripts/locations/ticket_sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import tinApp from '../modules/app'
/* global ZAFClient */
var client = ZAFClient.init()

client.on('app.registered', _ => tinApp(client, true))
client.on('*.changed', _ => tinApp(client))
client.on('pane.activated', _ => tinApp(client))
client.on('app.activated', _ => tinApp(client))
client.on('app.registered', () => tinApp(client, true))
client.on('*.changed', () => tinApp(client))
client.on('pane.activated', () => tinApp(client))
client.on('app.activated', () => tinApp(client))
8 changes: 4 additions & 4 deletions src/javascripts/locations/user_sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import tinApp from '../modules/app'
/* global ZAFClient */
var client = ZAFClient.init()

client.on('app.registered', _ => tinApp(client, true))
client.on('user.tags.changed', _ => tinApp(client))
client.on('pane.activated', _ => tinApp(client))
client.on('app.activated', _ => tinApp(client))
client.on('app.registered', () => tinApp(client, true))
client.on('user.tags.changed', () => tinApp(client))
client.on('pane.activated', () => tinApp(client))
client.on('app.activated', () => tinApp(client))
117 changes: 89 additions & 28 deletions src/javascripts/modules/app.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,107 @@
import * as Mustache from 'mustache'

import I18n from '../../javascripts/lib/i18n'
import { resizeContainer, render } from '../../javascripts/lib/helpers'
import getPreloadTemplate from '../../templates/preload'
import getDefaultTemplate from '../../templates/default'
import { preloadTemplate } from '../../templates/preload'
import {
defaultTemplate,
errorTemplate,
} from '../../templates/default'
import {
emptyWarning,
errorDetails,
linkedWarning,
plainWarning,
ruler,
} from '../../templates/partials'
import { extractTags } from './tags'
const MAX_HEIGHT = 1000

const tinApp = async (client, initial = false) => {
if (initial) {
await init(client).catch(e => handleError(e))
}
await fullRender(client)
if (initial) {
await init(client)
.catch(e => handleError(e, 'Could not initialize the application'))
}
await appRender(client)
}

const init = async (client) => {
const currentUser = (await client.get('currentUser')).currentUser
I18n.loadTranslations(currentUser.locale)
render('.loader', getPreloadTemplate())
await fullRender(client).catch(e => handleError(e))
const currentUser = (await client.get('currentUser')).currentUser
I18n.loadTranslations(currentUser.locale)
render('.loader', preloadTemplate)
await appRender(client)
}

const fullRender = async (client) => {
const tags = await extractTags(client).catch(e => handleError(e))
const settings = await client.metadata().catch(e => handleError(e))
if (Boolean(tags) && Boolean(settings)) {
render('.app', getDefaultTemplate({ "tags": tags, "settings": settings.settings }))
const appRender = async (client) => {
const tags = await extractTags(client)
.catch(e => handleError(e, I18n.t('runtime.errors.tagsLoading')))
const settings = await client
.metadata()
.catch(e => handleError(e, I18n.t('runtime.errors.settingsLoading')))
const view = await buildView(tags, settings.settings)
.catch(e => handleError(e, I18n.t('runtime.errors.viewLoading')))

if (view) {
render('.app', view)
}
return resizeContainer(client, MAX_HEIGHT)
}
}

const extractTags = async (client) => {
let currentLocation = (await client.context().catch(e => { throw new Error(e) })).location
currentLocation = currentLocation.replace('_sidebar', '.tags')
let tags = await client.get(currentLocation).catch(e => { throw new Error(e) })
if (Object.keys(tags['errors']).length > 0) {
let errorString = Object.keys(tags['errors']).reduce(key => `${key}: ${tags['errors'][key]}`, '')
throw new Error(`API returned errors: ${errorString}`)
} else {
return tags[currentLocation]
}
const buildView = async (tags, settings) => {
let config = {}
let content = ''
try {
config = {
tags: JSON.parse(settings.tags),
styles: JSON.parse(settings.styles),
defaults: JSON.parse(settings.defaults),
}
} catch (e) {
handleError(e, I18n.t('runtime.errors.configParsing'))
console.log(e)
return
}
console.log(config)

const objectTags = config.tags.filter(tag => tags.includes(tag.name))
if (!objectTags.length) {
content = Mustache.render(emptyWarning, {
description: config.defaults.desc || 'Coast is clear',
})
console.log(content)
} else {
content = objectTags.map(tag => {
let style = config.styles.find(style => style.name === tag.style)
style = style ? style.style : (config.defaults.style || '')
if (tag.link) {
return Mustache.render(linkedWarning, {
style: style,
description: tag.desc,
link: tag.link,
linkText: I18n.t('runtime.doclink'),
})
}
return Mustache.render(plainWarning, {
style: style,
description: tag.desc,
})
}).join(ruler)
console.log(content)
}

return Mustache.render(defaultTemplate, {
content
})
}

const handleError = (error) => {
console.warn('An error happened: ', error.message)
const handleError = (error, errorDescription) => {
console.log(error)
render('.app', Mustache.render(errorTemplate, {
errorDescription,
errorDetails: error.message,
}, {
errorDetails,
}))
}

export default tinApp
Loading

0 comments on commit 07e5f4c

Please sign in to comment.