如何在 Vue 中安全地呈现标记化 HTML?
How to safely render tokenized HTML in Vue?
我有一段文本包含某些需要替换为链接的标记。例如:
@peter and @samantha went on a date in #Paris to see the movie #HouseOfGucci
结果应该是:
<a href="/user/peter">@peter</a> and <a href="/user/samantha">@samantha</a> went on a date in <a href="/topic/paris">#Paris</a> to see the movie <a href="/topic/houseofgucci">#HouseOfGucci</>
我有一个要替换的令牌列表(不是所有都会被替换)以及如何替换(用户与主题)。已解决
现在的问题是我正在使用 Vue3,我可以将 html 呈现为简单 <div v-html="text"></div>
但这会产生问题,因为链接会导致 Vue 前端的整个页面重新加载, 即 SPA。
正确的解决方案是使用一个组件来标记整个文本并通过 if 条件适当地呈现每个标记(见下文)。不仅如此,还需要考虑断线。
<template>
<template v-for="(item, index) in lines" :key="index">
<router-link :to="item.route" v-if="'route' in item">{{ item.value }}</router-link>
<br v-else-if="'break' in item">
<template v-else>{{ item.value }}</template>
</template>
</template>
所以我试图弄清楚如何处理输入字符串并正确地对其进行标记化,并且有点卡在如何正确地递归循环所有标记以便它们正确地 replaced/appended/inserted 进入最终数组待渲染。
Here is a Go code of the desired end-result。我会尝试在 JS 中实现它,但如果有人有更好的,请做 post 作为答案。此外,它无法正确处理前缀和后缀的正则表达式,因此 foo@samanthabar
将匹配 @samantha
.
好吧,我猜你想让我们做你的家庭作业而不是学习任何东西,这不是应该的样子,所以我会尽力帮助你,而不是为你提供所有预制和测试的东西:
您可以先将文本标记化,如图 here 所示,拆分所有内容,同时将单词之间的空格也视为常规标记。这将使您在处理完链接后恢复文本的整体结构成为可能。
然后你可以使用 <template v-for="token in tokens">
遍历标记
最后,您决定使用 <template v-if="">
/ <template v-else-if="">
/ <template v-else>
来渲染什么:
<template v-if="token.charAt(0) === '@' && persons.includes(token.substring(1, token.length))">
<router-link :to="`/persons/${token.substring(1, token.length)}`">
{{ token }}
</router-link>
</template>
<template v-else-if="token.charAt(0) === '#' && topics.includes(token.substring(1, token.length))">
<router-link :to="`/topics/${token.substring(1, token.length)}`">
{{ token }}
</router-link>
</template>
<template v-else>
{{ token }}
</template>
看到了吗?根本没有递归!只是简单的线性代码。您可能 运行 遇到大量 persons/topics 的性能问题,但是一旦您到达那里,您应该知道在数组中搜索某些内容是这里的罪魁祸首。
所以,这就是我想出的。我认为它很丑陋,但它确实有效。
<template>
<template v-for="(item, index) in lines" :key="index">
<router-link :to="item.Route" v-if="item.Type === 'route'">{{ item.Value }}</router-link>
<br v-else-if="item.Type === 'break'">
<template v-else>{{ item.Value }}</template>
</template>
</template>
<script>
import { defineComponent, h } from 'vue';
export default defineComponent({
name: 'TokenizedText',
props: {
text: {
type: String,
required: true,
},
symbols: {
type: Array,
default: () => []
},
handles: {
type: Array,
default: () => []
},
topics: {
type: Array,
default: () => []
},
},
computed: {
lines() {
return new Token("str", this.text).Tokenize(this.symbols || [], this.handles || [], this.topics || []).Build()
}
},
})
function Token(type, value) {
this.Type = type
this.Value = value
this.Route = {}
this.Children = []
}
Token.prototype.Build = function() {
let out = []
if (this.Value.length > 0 ||this.Type === 'break') {
out.push(this)
}
for (let i in this.Children) {
out.push.apply(out, this.Children[i].Build())
}
return out
}
Token.prototype.Tokenize = function(symbols, handles, topics) {
if (this.Type === "str") {
let breakLines = this.Value.split("\n")
if (breakLines.length > 1) {
this.Value = ""
for (let key in breakLines) {
this.Children.push(new Token("str", breakLines[key]).Tokenize(symbols, handles, topics))
if (key < breakLines.length - 1) {
this.Children.push(new Token("break", ""))
}
}
return this
}
}
handles.sort((a, b) => a.length - b.length)
for (let h in handles) {
if (this.Value.toLowerCase() === "@"+handles[h].toLowerCase()) {
this.Type = "route"
this.Route = {name: "user.view", params: {handle: handles[h]}}
return this
}
let handleLines = this.Value.split(new RegExp(`@\b${handles[h]}\b`, 'gmi'))
let handleOriginals = this.Value.match(new RegExp(`@\b${handles[h]}\b`, 'gmi')) || []
if (handleLines.length > 1) {
this.Value = ""
for (let l in handleLines) {
if (handleLines[l].length > 0) {
this.Children.push(new Token("str", handleLines[l]).Tokenize(symbols, handles, topics))
}
if (l < handleLines.length-1) {
let line = handleOriginals[l] || "@"+handles[h]
console.log(line)
this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
}
}
return this
}
}
topics.sort((a, b) => a.length - b.length)
for (let h in topics) {
if (this.Value.toLowerCase() === "#"+topics[h].toLowerCase()) {
this.Type = "route"
this.Route= {name: "topic.view", params: {tag: topics[h]}}
return this
}
let topicLines = this.Value.split(new RegExp(`#\b${topics[h]}\b`, 'gmi'))
let topicOriginals = this.Value.match(new RegExp(`#\b${topics[h]}\b`, 'gmi')) || []
if (topicLines.length > 1) {
this.Value = ""
for (let l in topicLines) {
if (topicLines[l].length > 0) {
this.Children.push(new Token("str", topicLines[l]).Tokenize(symbols, handles, topics))
}
if (l < topicLines.length-1) {
let line = topicOriginals[l] || "#"+topics[h]
this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
}
}
return this
}
}
symbols.sort((a, b) => a.length - b.length)
for (let h in symbols) {
if (this.Value.toLowerCase() === "$"+symbols[h].toLowerCase()) {
this.Type = "route"
this.Route= {name: "symbol.view", params: {symbol: symbols[h]}}
return this
}
let symbolLines = this.Value.split(new RegExp(`\$\b${symbols[h]}\b`, 'gmi'))
let symbolOriginals = this.Value.match(new RegExp(`\$\b${symbols[h]}\b`, 'gmi')) || []
if (symbolLines.length > 1) {
this.Value = ""
for (let l in symbolLines) {
if (symbolLines[l].length > 0) {
this.Children.push(new Token("str", symbolLines[l]).Tokenize(symbols, handles, topics))
}
if (l < symbolLines.length-1) {
let line = symbolOriginals[l] || "$"+symbols[h]
this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
}
}
return this
}
}
return this
}
</script>
我有一段文本包含某些需要替换为链接的标记。例如:
@peter and @samantha went on a date in #Paris to see the movie #HouseOfGucci
结果应该是:
<a href="/user/peter">@peter</a> and <a href="/user/samantha">@samantha</a> went on a date in <a href="/topic/paris">#Paris</a> to see the movie <a href="/topic/houseofgucci">#HouseOfGucci</>
我有一个要替换的令牌列表(不是所有都会被替换)以及如何替换(用户与主题)。已解决
现在的问题是我正在使用 Vue3,我可以将 html 呈现为简单 <div v-html="text"></div>
但这会产生问题,因为链接会导致 Vue 前端的整个页面重新加载, 即 SPA。
正确的解决方案是使用一个组件来标记整个文本并通过 if 条件适当地呈现每个标记(见下文)。不仅如此,还需要考虑断线。
<template>
<template v-for="(item, index) in lines" :key="index">
<router-link :to="item.route" v-if="'route' in item">{{ item.value }}</router-link>
<br v-else-if="'break' in item">
<template v-else>{{ item.value }}</template>
</template>
</template>
所以我试图弄清楚如何处理输入字符串并正确地对其进行标记化,并且有点卡在如何正确地递归循环所有标记以便它们正确地 replaced/appended/inserted 进入最终数组待渲染。
Here is a Go code of the desired end-result。我会尝试在 JS 中实现它,但如果有人有更好的,请做 post 作为答案。此外,它无法正确处理前缀和后缀的正则表达式,因此 foo@samanthabar
将匹配 @samantha
.
好吧,我猜你想让我们做你的家庭作业而不是学习任何东西,这不是应该的样子,所以我会尽力帮助你,而不是为你提供所有预制和测试的东西:
您可以先将文本标记化,如图 here 所示,拆分所有内容,同时将单词之间的空格也视为常规标记。这将使您在处理完链接后恢复文本的整体结构成为可能。
然后你可以使用
遍历标记<template v-for="token in tokens">
最后,您决定使用
<template v-if="">
/<template v-else-if="">
/<template v-else>
来渲染什么:
<template v-if="token.charAt(0) === '@' && persons.includes(token.substring(1, token.length))">
<router-link :to="`/persons/${token.substring(1, token.length)}`">
{{ token }}
</router-link>
</template>
<template v-else-if="token.charAt(0) === '#' && topics.includes(token.substring(1, token.length))">
<router-link :to="`/topics/${token.substring(1, token.length)}`">
{{ token }}
</router-link>
</template>
<template v-else>
{{ token }}
</template>
看到了吗?根本没有递归!只是简单的线性代码。您可能 运行 遇到大量 persons/topics 的性能问题,但是一旦您到达那里,您应该知道在数组中搜索某些内容是这里的罪魁祸首。
所以,这就是我想出的。我认为它很丑陋,但它确实有效。
<template>
<template v-for="(item, index) in lines" :key="index">
<router-link :to="item.Route" v-if="item.Type === 'route'">{{ item.Value }}</router-link>
<br v-else-if="item.Type === 'break'">
<template v-else>{{ item.Value }}</template>
</template>
</template>
<script>
import { defineComponent, h } from 'vue';
export default defineComponent({
name: 'TokenizedText',
props: {
text: {
type: String,
required: true,
},
symbols: {
type: Array,
default: () => []
},
handles: {
type: Array,
default: () => []
},
topics: {
type: Array,
default: () => []
},
},
computed: {
lines() {
return new Token("str", this.text).Tokenize(this.symbols || [], this.handles || [], this.topics || []).Build()
}
},
})
function Token(type, value) {
this.Type = type
this.Value = value
this.Route = {}
this.Children = []
}
Token.prototype.Build = function() {
let out = []
if (this.Value.length > 0 ||this.Type === 'break') {
out.push(this)
}
for (let i in this.Children) {
out.push.apply(out, this.Children[i].Build())
}
return out
}
Token.prototype.Tokenize = function(symbols, handles, topics) {
if (this.Type === "str") {
let breakLines = this.Value.split("\n")
if (breakLines.length > 1) {
this.Value = ""
for (let key in breakLines) {
this.Children.push(new Token("str", breakLines[key]).Tokenize(symbols, handles, topics))
if (key < breakLines.length - 1) {
this.Children.push(new Token("break", ""))
}
}
return this
}
}
handles.sort((a, b) => a.length - b.length)
for (let h in handles) {
if (this.Value.toLowerCase() === "@"+handles[h].toLowerCase()) {
this.Type = "route"
this.Route = {name: "user.view", params: {handle: handles[h]}}
return this
}
let handleLines = this.Value.split(new RegExp(`@\b${handles[h]}\b`, 'gmi'))
let handleOriginals = this.Value.match(new RegExp(`@\b${handles[h]}\b`, 'gmi')) || []
if (handleLines.length > 1) {
this.Value = ""
for (let l in handleLines) {
if (handleLines[l].length > 0) {
this.Children.push(new Token("str", handleLines[l]).Tokenize(symbols, handles, topics))
}
if (l < handleLines.length-1) {
let line = handleOriginals[l] || "@"+handles[h]
console.log(line)
this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
}
}
return this
}
}
topics.sort((a, b) => a.length - b.length)
for (let h in topics) {
if (this.Value.toLowerCase() === "#"+topics[h].toLowerCase()) {
this.Type = "route"
this.Route= {name: "topic.view", params: {tag: topics[h]}}
return this
}
let topicLines = this.Value.split(new RegExp(`#\b${topics[h]}\b`, 'gmi'))
let topicOriginals = this.Value.match(new RegExp(`#\b${topics[h]}\b`, 'gmi')) || []
if (topicLines.length > 1) {
this.Value = ""
for (let l in topicLines) {
if (topicLines[l].length > 0) {
this.Children.push(new Token("str", topicLines[l]).Tokenize(symbols, handles, topics))
}
if (l < topicLines.length-1) {
let line = topicOriginals[l] || "#"+topics[h]
this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
}
}
return this
}
}
symbols.sort((a, b) => a.length - b.length)
for (let h in symbols) {
if (this.Value.toLowerCase() === "$"+symbols[h].toLowerCase()) {
this.Type = "route"
this.Route= {name: "symbol.view", params: {symbol: symbols[h]}}
return this
}
let symbolLines = this.Value.split(new RegExp(`\$\b${symbols[h]}\b`, 'gmi'))
let symbolOriginals = this.Value.match(new RegExp(`\$\b${symbols[h]}\b`, 'gmi')) || []
if (symbolLines.length > 1) {
this.Value = ""
for (let l in symbolLines) {
if (symbolLines[l].length > 0) {
this.Children.push(new Token("str", symbolLines[l]).Tokenize(symbols, handles, topics))
}
if (l < symbolLines.length-1) {
let line = symbolOriginals[l] || "$"+symbols[h]
this.Children.push(new Token("route", line).Tokenize(symbols, handles, topics))
}
}
return this
}
}
return this
}
</script>