如何在 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.

好吧,我猜你想让我们做你的家庭作业而不是学习任何东西,这不是应该的样子,所以我会尽力帮助你,而不是为你提供所有预制和测试的东西:

  1. 您可以先将文本标记化,如图 here 所示,拆分所有内容,同时将单词之间的空格也视为常规标记。这将使您在处理完链接后恢复文本的整体结构成为可能。

  2. 然后你可以使用 <template v-for="token in tokens">

    遍历标记
  3. 最后,您决定使用 <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>