Skip to content

Instantly share code, notes, and snippets.

@BlakeHancock
Last active February 16, 2020 07:52
Show Gist options
  • Save BlakeHancock/8018422a3639d057e435c28d2078cf7e to your computer and use it in GitHub Desktop.
Save BlakeHancock/8018422a3639d057e435c28d2078cf7e to your computer and use it in GitHub Desktop.
<script>
import appAnchor from './Anchor'
// Whitelist of elements
const elements = ['a', 'img', 'strong', 'em', 'br', 'hr']
// Whitelist of element attributes
const attributes = {
a: ['href'],
img: ['src', 'alt']
}
// Identifiers used to surround translation placeholders
// vuex-i18n plugin config doesn't appear to be accessible
const identifiers = ['{{', '}}']
// Suffix at end of translation alias to indicate it is an html string
const htmlSuffix = '__html'
function vnodesFromHtml (h, html, slots) {
let element = new DOMParser().parseFromString(html, 'text/html')
element = element.childNodes[0].childNodes[1] // Body element
return vnodesFromElement(h, element, slots)
}
function vnodesFromElement (h, element, slots) {
let vnodes = []
element.childNodes.forEach(child => {
if (child.nodeType === Node.TEXT_NODE) {
vnodes.push(...vnodesFromText(child.nodeValue, slots))
} else if (child.nodeType === Node.ELEMENT_NODE) {
let tag = child.nodeName.toLowerCase(),
attrs = {}
// Ensure tag is in whitelist
if (elements.indexOf(tag) === -1) {
return
}
// Add any whitelisted attributes
if (attributes[tag]) {
attributes[tag].forEach(attribute => {
attribute = child.attributes[attribute]
if (!attribute) {
return
}
// Prevent javascript in a[href]
if (tag === 'a' && attribute.nodeName === 'href') {
if (attribute.nodeValue.toLowerCase().startsWith('javascript:')) {
return
}
}
// TODO: Some way of supporting slot args in attributes?
attrs[attribute.nodeName] = attribute.nodeValue
})
}
// Replace anchors with our custom override to handle
// opening links on non web platforms properly
// Since this is a functional component, we use it directly
// instead of using its name
if (tag === 'a') {
tag = appAnchor
}
vnodes.push(
h(tag, {attrs}, vnodesFromElement(h, child, slots))
)
}
})
return vnodes
}
function vnodesFromText (text, slots) {
// Add @@@ before and after arg identifiers in order split
// using an unlikely to be used string
let parts = text.replace(
new RegExp(identifiers[0], 'g'),
'@@@' + identifiers[0]
).replace(
new RegExp(identifiers[1], 'g'),
identifiers[1] + '@@@'
).split('@@@')
return parts.reduce((vnodes, value) => {
let key = value.substring(2, value.length - 2)
if (value.startsWith(identifiers[0]) && key in slots) {
if (Array.isArray(slots[key])) {
vnodes.push(...slots[key])
} else {
vnodes.push(slots[key])
}
} else {
vnodes.push(value)
}
return vnodes
}, [])
}
function escapeText (text) {
return document.createElement('div')
.appendChild(document.createTextNode(text))
.parentNode.innerHTML
}
function translate (h, $i18n, module, alias, args, count) {
let slots = {},
type,
translateArgs = {}
if (!alias.startsWith('/')) {
if (module) {
alias = module + '/' + alias
} else {
alias = '/app/' + alias
}
}
if ($i18n.keyExists(alias + htmlSuffix)) {
type = 'html'
} else {
type = 'text'
}
Object.keys(args).forEach(key => {
let payloads = args[key],
payloadArgs = [],
hasHtmlPayloadArgs = false
if (typeof payloads === 'string') {
translateArgs[key] = payloads
return
}
if (!Array.isArray(payloads)) {
payloads = [payloads]
}
payloads.forEach((payload, index) => {
if (typeof payload === 'object') {
let arg = translate(
h,
$i18n,
module,
payload.alias,
payload.args,
payload.count
)
payloadArgs.push(arg)
if (arg.type === 'html') {
hasHtmlPayloadArgs = true
}
} else {
payloadArgs.push(payload)
}
})
if (hasHtmlPayloadArgs) {
// We can't insert html into plaintext, so store it as
// fake slots and pass to final output. We replace the key
// so that it doesn't interfere with translations that
// have the same key
translateArgs[key] = identifiers[0] + alias + '/' + key + identifiers[1]
slots[alias + '/' + key] = []
} else {
translateArgs[key] = ''
}
payloadArgs.forEach(payloadArg => {
if (typeof payloadArg === 'object') {
if (payloadArg.type === 'text') {
if (hasHtmlPayloadArgs) {
slots[identifiers[0] + alias + '/' + key + identifiers[1]].push(
payloadArg.value
)
} else {
if (type === 'html') {
translateArgs[key] += escapeText(payloadArg.value)
} else {
translateArgs[key] += payloadArg.value
}
}
slots = {...slots, ...payloadArg.slots}
} else {
slots[alias + '/' + key].push(
...vnodesFromHtml(h, payloadArg.value, payloadArg.slots)
)
}
} else if (hasHtmlPayloadArgs) {
slots[alias + '/' + key].push(
payloadArg
)
} else {
if (type === 'html') {
translateArgs[key] += escapeText(payloadArg.value)
} else {
translateArgs[key] += payloadArg.value
}
}
})
})
if (type === 'html') {
alias += htmlSuffix
}
return {
value: $i18n.translate(alias, translateArgs, count),
type,
slots
}
}
export default {
functional: true,
props: {
tag: String,
module: String,
alias: {
type: [String, Object, Array, Error],
required: true
},
args: Object,
count: Number
},
render (h, {data, props, slots, parent}) {
let payloads,
vnodes = []
const tag = props.tag || 'span'
// Slots potentially hold translation args
let slotsObject = slots()
if (Array.isArray(props.alias)) {
payloads = props.alias
} else {
payloads = [props.alias]
}
payloads.forEach(payload => {
let alias,
args = {},
count
if (props.args) {
// Let slot args take precedence over prop args
// TODO: Maybe some way of letting slots use prop args
Object.keys(props.args).forEach(key => {
if (key in slotsObject) {
return
}
args[key] = props.args[key]
})
}
if (payload instanceof Error) {
if (payload.translationPayload) {
payload = payload.translationPayload
} else {
vnodes.push(vnodesFromText(payload.message, slotsObject))
return
}
}
if (typeof payload === 'object') {
if ('count' in payload) {
count = payload.count
} else {
count = props.count
}
if ('args' in payload && payload.args !== null) {
// Let slot args take precedence over param args
Object.keys(payload.args).forEach(key => {
if (key in slotsObject) {
return
}
args[key] = payload.args[key]
})
}
alias = payload.alias
} else {
alias = payload
count = props.count
}
// Suppress warnings by ensuring slot placeholders are filled
Object.keys(slotsObject).forEach(key => {
if (!(key in args)) {
args[key] = identifiers[0] + key + identifiers[1]
}
})
const translation = translate(
h,
parent.$i18n,
props.module,
alias,
args,
count
)
const payloadSlots = {...slotsObject, ...translation.slots}
if (translation.type === 'html') {
vnodes.push(vnodesFromHtml(h, translation.value, payloadSlots))
} else {
vnodes.push(vnodesFromText(translation.value, payloadSlots))
}
})
return h(tag, data, vnodes)
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment