aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/renderer
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 14:09:23 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 14:09:23 +0100
commitb718fd22374d64534bcfe69932cf562894abed6a (patch)
tree311d3c67e2a4d1f33ebdd1dc163527de9d33d0f7 /client/src/app/shared/renderer
parentadb115f5522bea4d52456a9fc5eb4140bb064476 (diff)
parent501e961199578129629cf0567033d13efced9904 (diff)
downloadPeerTube-b718fd22374d64534bcfe69932cf562894abed6a.tar.gz
PeerTube-b718fd22374d64534bcfe69932cf562894abed6a.tar.zst
PeerTube-b718fd22374d64534bcfe69932cf562894abed6a.zip
Merge branch 'develop' into pr/1285
Diffstat (limited to 'client/src/app/shared/renderer')
-rw-r--r--client/src/app/shared/renderer/html-renderer.service.ts35
-rw-r--r--client/src/app/shared/renderer/index.ts3
-rw-r--r--client/src/app/shared/renderer/linkifier.service.ts115
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts79
4 files changed, 232 insertions, 0 deletions
diff --git a/client/src/app/shared/renderer/html-renderer.service.ts b/client/src/app/shared/renderer/html-renderer.service.ts
new file mode 100644
index 000000000..d49df9b6d
--- /dev/null
+++ b/client/src/app/shared/renderer/html-renderer.service.ts
@@ -0,0 +1,35 @@
1import { Injectable } from '@angular/core'
2import { LinkifierService } from '@app/shared/renderer/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html'
4
5@Injectable()
6export class HtmlRendererService {
7
8 constructor (private linkifier: LinkifierService) {
9
10 }
11
12 toSafeHtml (text: string) {
13 // Convert possible markdown to html
14 const html = this.linkifier.linkify(text)
15
16 return sanitizeHtml(html, {
17 allowedTags: [ 'a', 'p', 'span', 'br' ],
18 allowedSchemes: [ 'http', 'https' ],
19 allowedAttributes: {
20 'a': [ 'href', 'class', 'target' ]
21 },
22 transformTags: {
23 a: (tagName, attribs) => {
24 return {
25 tagName,
26 attribs: Object.assign(attribs, {
27 target: '_blank',
28 rel: 'noopener noreferrer'
29 })
30 }
31 }
32 }
33 })
34 }
35}
diff --git a/client/src/app/shared/renderer/index.ts b/client/src/app/shared/renderer/index.ts
new file mode 100644
index 000000000..39202b385
--- /dev/null
+++ b/client/src/app/shared/renderer/index.ts
@@ -0,0 +1,3 @@
1export * from './html-renderer.service'
2export * from './linkifier.service'
3export * from './markdown.service'
diff --git a/client/src/app/shared/renderer/linkifier.service.ts b/client/src/app/shared/renderer/linkifier.service.ts
new file mode 100644
index 000000000..2529c9eaf
--- /dev/null
+++ b/client/src/app/shared/renderer/linkifier.service.ts
@@ -0,0 +1,115 @@
1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
3// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged?
4const linkify = require('linkifyjs')
5const linkifyHtml = require('linkifyjs/html')
6
7@Injectable()
8export class LinkifierService {
9
10 static CLASSNAME = 'linkified'
11
12 private linkifyOptions = {
13 className: {
14 mention: LinkifierService.CLASSNAME + '-mention',
15 url: LinkifierService.CLASSNAME + '-url'
16 }
17 }
18
19 constructor () {
20 // Apply plugin
21 this.mentionWithDomainPlugin(linkify)
22 }
23
24 linkify (text: string) {
25 return linkifyHtml(text, this.linkifyOptions)
26 }
27
28 private mentionWithDomainPlugin (linkify: any) {
29 const TT = linkify.scanner.TOKENS // Text tokens
30 const { TOKENS: MT, State } = linkify.parser // Multi tokens, state
31 const MultiToken = MT.Base
32 const S_START = linkify.parser.start
33
34 const TT_AT = TT.AT
35 const TT_DOMAIN = TT.DOMAIN
36 const TT_LOCALHOST = TT.LOCALHOST
37 const TT_NUM = TT.NUM
38 const TT_COLON = TT.COLON
39 const TT_SLASH = TT.SLASH
40 const TT_TLD = TT.TLD
41 const TT_UNDERSCORE = TT.UNDERSCORE
42 const TT_DOT = TT.DOT
43
44 function MENTION (this: any, value: any) {
45 this.v = value
46 }
47
48 linkify.inherits(MultiToken, MENTION, {
49 type: 'mentionWithDomain',
50 isLink: true,
51 toHref () {
52 return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1)
53 }
54 })
55
56 const S_AT = S_START.jump(TT_AT) // @
57 const S_AT_SYMS = new State()
58 const S_MENTION = new State(MENTION)
59 const S_MENTION_DIVIDER = new State()
60 const S_MENTION_DIVIDER_SYMS = new State()
61
62 // @_,
63 S_AT.on(TT_UNDERSCORE, S_AT_SYMS)
64
65 // @_*
66 S_AT_SYMS
67 .on(TT_UNDERSCORE, S_AT_SYMS)
68 .on(TT_DOT, S_AT_SYMS)
69
70 // Valid mention (not made up entirely of symbols)
71 S_AT
72 .on(TT_DOMAIN, S_MENTION)
73 .on(TT_LOCALHOST, S_MENTION)
74 .on(TT_TLD, S_MENTION)
75 .on(TT_NUM, S_MENTION)
76
77 S_AT_SYMS
78 .on(TT_DOMAIN, S_MENTION)
79 .on(TT_LOCALHOST, S_MENTION)
80 .on(TT_TLD, S_MENTION)
81 .on(TT_NUM, S_MENTION)
82
83 // More valid mentions
84 S_MENTION
85 .on(TT_DOMAIN, S_MENTION)
86 .on(TT_LOCALHOST, S_MENTION)
87 .on(TT_TLD, S_MENTION)
88 .on(TT_COLON, S_MENTION)
89 .on(TT_NUM, S_MENTION)
90 .on(TT_UNDERSCORE, S_MENTION)
91
92 // Mention with a divider
93 S_MENTION
94 .on(TT_AT, S_MENTION_DIVIDER)
95 .on(TT_SLASH, S_MENTION_DIVIDER)
96 .on(TT_DOT, S_MENTION_DIVIDER)
97
98 // Mention _ trailing stash plus syms
99 S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
100 S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
101
102 // Once we get a word token, mentions can start up again
103 S_MENTION_DIVIDER
104 .on(TT_DOMAIN, S_MENTION)
105 .on(TT_LOCALHOST, S_MENTION)
106 .on(TT_TLD, S_MENTION)
107 .on(TT_NUM, S_MENTION)
108
109 S_MENTION_DIVIDER_SYMS
110 .on(TT_DOMAIN, S_MENTION)
111 .on(TT_LOCALHOST, S_MENTION)
112 .on(TT_TLD, S_MENTION)
113 .on(TT_NUM, S_MENTION)
114 }
115}
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
new file mode 100644
index 000000000..07017eca5
--- /dev/null
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -0,0 +1,79 @@
1import { Injectable } from '@angular/core'
2
3import * as MarkdownIt from 'markdown-it'
4
5@Injectable()
6export class MarkdownService {
7 static TEXT_RULES = [
8 'linkify',
9 'autolink',
10 'emphasis',
11 'link',
12 'newline',
13 'list'
14 ]
15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
16
17 private textMarkdownIt: MarkdownIt.MarkdownIt
18 private enhancedMarkdownIt: MarkdownIt.MarkdownIt
19
20 constructor () {
21 this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES)
22 this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES)
23 }
24
25 textMarkdownToHTML (markdown: string) {
26 if (!markdown) return ''
27
28 const html = this.textMarkdownIt.render(markdown)
29 return this.avoidTruncatedTags(html)
30 }
31
32 enhancedMarkdownToHTML (markdown: string) {
33 if (!markdown) return ''
34
35 const html = this.enhancedMarkdownIt.render(markdown)
36 return this.avoidTruncatedTags(html)
37 }
38
39 private createMarkdownIt (rules: string[]) {
40 const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true })
41
42 for (let rule of rules) {
43 markdownIt.enable(rule)
44 }
45
46 this.setTargetToLinks(markdownIt)
47
48 return markdownIt
49 }
50
51 private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) {
52 // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
53 const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
54 return self.renderToken(tokens, idx, options)
55 }
56
57 markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
58 const token = tokens[index]
59
60 const targetIndex = token.attrIndex('target')
61 if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
62 else token.attrs[targetIndex][1] = '_blank'
63
64 const relIndex = token.attrIndex('rel')
65 if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
66 else token.attrs[relIndex][1] = 'noopener noreferrer'
67
68 // pass token to default renderer.
69 return defaultRender(tokens, index, options, env, self)
70 }
71 }
72
73 private avoidTruncatedTags (html: string) {
74 return html.replace(/\*\*?([^*]+)$/, '$1')
75 .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
76 .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
77
78 }
79}