Home > Blockchain >  How to safely render tokenized HTML in Vue?
How to safely render tokenized HTML in Vue?

Time:01-07

I have a text that contains certain tokens that need to be replaced into links. For example:

@peter and @samantha went on a date in #Paris to see the movie #HouseOfGucci

The result should be:

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

I have a list of tokens to be replaced(not all will be replaced) and how(users vs topics). This has been solved in here

The problem now is that I am using Vue3 and I can render the html as simple <div v-html="text"></div> but this creates a problem because the links will cause full page reload of the Vue front-end, which is SPA.

The correct solution is to have a component that will tokenize the entire text and render each token appropriately via if condition(see below). Not only that, but also break lines need to be taken into account.

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

So I am trying to figure out how to process the input string and properly tokenize it and am a bit stuck on how to recursively cycle through all the tokens properly so they are properly replaced/appended/inserted into the final array to be rendered.


Here is a Go code of the desired end-result. I will try to implement it in JS, but if someone has better one, please do post it as answer. Also it does not handle the regex properly for prefix and suffix so foo@samanthabar will match @samantha.

CodePudding user response:

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.

CodePudding user response:

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>
  •  Tags:  
  • Related