|
1 | 1 | <template> |
2 | 2 | <div class="d-flex flex-row mt-4 min-height-40"> |
3 | | - <span |
4 | | - class="d-flex align-baseline width-15-percent-flex" |
5 | | - > |
| 3 | + <span class="d-flex align-baseline width-15-percent-flex"> |
6 | 4 | <v-tooltip bottom> |
7 | 5 | <template #activator="{ on }"> |
8 | | - <v-icon |
9 | | - class="mr-2" |
10 | | - size="15" |
11 | | - v-on="on" |
12 | | - > |
13 | | - fa-question-circle |
14 | | - </v-icon> |
| 6 | + <v-icon class="mr-2" size="15" v-on="on"> fa-question-circle </v-icon> |
15 | 7 | </template> |
16 | | - {{ recordTooltips['description'] }} |
| 8 | + {{ recordTooltips.description }} |
17 | 9 | </v-tooltip> |
18 | 10 | <b>Description</b> |
19 | 11 | </span> |
| 12 | + |
| 13 | + <!-- use v-html to inject sanitized HTML --> |
20 | 14 | <p |
21 | 15 | class="ma-0 full-width ml-md-12 ml-8" |
22 | | - :class="{'text-end' : $vuetify.breakpoint.smAndDown}" |
23 | | - > |
24 | | - {{ getField('description') | capitalize }} |
25 | | - </p> |
| 16 | + :class="{ 'text-end': $vuetify.breakpoint.smAndDown }" |
| 17 | + v-html="descriptionHtml" |
| 18 | + /> |
26 | 19 | </div> |
27 | 20 | </template> |
28 | 21 |
|
29 | 22 | <script> |
30 | | -import {mapGetters, mapState} from "vuex"; |
| 23 | +import DOMPurify from "dompurify"; |
| 24 | +import MarkdownIt from "markdown-it"; |
| 25 | +import { mapGetters, mapState } from "vuex"; |
31 | 26 |
|
32 | 27 | import stringUtils from "@/utils/stringUtils"; |
| 28 | +
|
| 29 | +const md = new MarkdownIt({ |
| 30 | + html: true, // allow inline HTML in Markdown input (we will sanitize below) |
| 31 | + linkify: true, |
| 32 | + typographer: true, |
| 33 | +}); |
| 34 | +
|
33 | 35 | export default { |
34 | 36 | name: "Description", |
35 | 37 | mixins: [stringUtils], |
36 | 38 | computed: { |
37 | 39 | ...mapGetters("record", ["getField"]), |
38 | 40 | ...mapState("editor", ["recordTooltips"]), |
39 | | - } |
40 | | -} |
| 41 | +
|
| 42 | + // raw description string from store (fallback to empty string) |
| 43 | + descriptionRaw() { |
| 44 | + const d = this.getField("description"); |
| 45 | + return d == null ? "" : d; |
| 46 | + }, |
| 47 | +
|
| 48 | + // capitalise using stringutils capitalize method, then render => sanitize |
| 49 | + descriptionHtml() { |
| 50 | + // if your mixin exposes `capitalize`, call it; otherwise remove the call |
| 51 | + const capitalized = |
| 52 | + typeof this.capitalize === "function" |
| 53 | + ? this.capitalize(this.descriptionRaw) |
| 54 | + : this.descriptionRaw; |
| 55 | +
|
| 56 | + const unsafe = md.render(capitalized || ""); |
| 57 | + // Sanitize the generated HTML to avoid XSS. |
| 58 | + return DOMPurify.sanitize(unsafe, { |
| 59 | + // ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i |
| 60 | + }); |
| 61 | + }, |
| 62 | + }, |
| 63 | +}; |
41 | 64 | </script> |
0 commit comments