Last active
February 16, 2020 07:52
-
-
Save BlakeHancock/8018422a3639d057e435c28d2078cf7e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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