[Vuejs]-How to safely render tokenized HTML in Vue?

0👍

So, this is what I came up with. I think it’s quite ugly, but it works.

<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>

0👍

Okay, I’m guessing you want us to do your homework and not learn anything and that’s not how it’s supposed to be, so I’ll try to help you without serving you everything pre-made and tested:

  1. You could start by tokenizing the text as shown here, splitting everything while treating the whitespace in between the words also as regular tokens. This will make it possible to get back the text’s overall structure once you have handled your links.

  2. Then you could loop over the tokens using a <template v-for="token in tokens">

  3. Finally, you decide what to render using a <template v-if=""> / <template v-else-if="">/ <template v-else> like this:

<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>

You see? No recursion at all! Just simple linear code. You might run into performance problems with large sets of persons/topics, but once you get there, you should know that searching something in arrays is the culprit here.

Leave a comment