aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/videos')
-rw-r--r--client/src/app/videos/+video-watch/comment/linkifier.service.ts115
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts27
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-support.component.ts3
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts4
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/shared/markdown.service.ts79
8 files changed, 8 insertions, 229 deletions
diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/videos/+video-watch/comment/linkifier.service.ts
deleted file mode 100644
index 2529c9eaf..000000000
--- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts
+++ /dev/null
@@ -1,115 +0,0 @@
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/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index 00f0460a1..aba7f9d1c 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
@@ -1,11 +1,10 @@
1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html'
4import { UserRight } from '../../../../../../shared/models/users' 2import { UserRight } from '../../../../../../shared/models/users'
5import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' 3import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
6import { AuthService } from '../../../core/auth' 4import { AuthService } from '../../../core/auth'
7import { Video } from '../../../shared/video/video.model' 5import { Video } from '../../../shared/video/video.model'
8import { VideoComment } from './video-comment.model' 6import { VideoComment } from './video-comment.model'
7import { HtmlRendererService } from '@app/shared/renderer'
9 8
10@Component({ 9@Component({
11 selector: 'my-video-comment', 10 selector: 'my-video-comment',
@@ -29,7 +28,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
29 newParentComments: VideoComment[] = [] 28 newParentComments: VideoComment[] = []
30 29
31 constructor ( 30 constructor (
32 private linkifierService: LinkifierService, 31 private htmlRenderer: HtmlRendererService,
33 private authService: AuthService 32 private authService: AuthService
34 ) {} 33 ) {}
35 34
@@ -87,27 +86,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
87 } 86 }
88 87
89 private init () { 88 private init () {
90 // Convert possible markdown to html 89 this.sanitizedCommentHTML = this.htmlRenderer.toSafeHtml(this.comment.text)
91 const html = this.linkifierService.linkify(this.comment.text)
92
93 this.sanitizedCommentHTML = sanitizeHtml(html, {
94 allowedTags: [ 'a', 'p', 'span', 'br' ],
95 allowedSchemes: [ 'http', 'https' ],
96 allowedAttributes: {
97 'a': [ 'href', 'class', 'target' ]
98 },
99 transformTags: {
100 a: (tagName, attribs) => {
101 return {
102 tagName,
103 attribs: Object.assign(attribs, {
104 target: '_blank',
105 rel: 'noopener noreferrer'
106 })
107 }
108 }
109 }
110 })
111 90
112 this.newParentComments = this.parentComments.concat([ this.comment ]) 91 this.newParentComments = this.parentComments.concat([ this.comment ])
113 } 92 }
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
index 921447d5b..b8e5878c5 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts
@@ -1,7 +1,7 @@
1import { catchError, map } from 'rxjs/operators' 1import { catchError, map } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { lineFeedToHtml } from '@app/shared/misc/utils' 4import { objectLineFeedToHtml } from '@app/shared/misc/utils'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { ResultList, FeedFormat } from '../../../../../../shared/models' 6import { ResultList, FeedFormat } from '../../../../../../shared/models'
7import { 7import {
@@ -28,7 +28,7 @@ export class VideoCommentService {
28 28
29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) { 29 addCommentThread (videoId: number | string, comment: VideoCommentCreate) {
30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' 30 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
31 const normalizedComment = lineFeedToHtml(comment, 'text') 31 const normalizedComment = objectLineFeedToHtml(comment, 'text')
32 32
33 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) 33 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
34 .pipe( 34 .pipe(
@@ -39,7 +39,7 @@ export class VideoCommentService {
39 39
40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { 40 addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) {
41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId 41 const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
42 const normalizedComment = lineFeedToHtml(comment, 'text') 42 const normalizedComment = objectLineFeedToHtml(comment, 'text')
43 43
44 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) 44 return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
45 .pipe( 45 .pipe(
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts
index 154002120..deb8fbc67 100644
--- a/client/src/app/videos/+video-watch/modal/video-support.component.ts
+++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts
@@ -1,8 +1,7 @@
1import { Component, Input, ViewChild } from '@angular/core' 1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared'
3
4import { VideoDetails } from '../../../shared/video/video-details.model' 2import { VideoDetails } from '../../../shared/video/video-details.model'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { MarkdownService } from '@app/shared/renderer'
6 5
7@Component({ 6@Component({
8 selector: 'my-video-support', 7 selector: 'my-video-support',
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 67c5254b3..ee504bc58 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -19,7 +19,6 @@ import { AuthService, ConfirmService } from '../../core'
19import { RestExtractor, VideoBlacklistService } from '../../shared' 19import { RestExtractor, VideoBlacklistService } from '../../shared'
20import { VideoDetails } from '../../shared/video/video-details.model' 20import { VideoDetails } from '../../shared/video/video-details.model'
21import { VideoService } from '../../shared/video/video.service' 21import { VideoService } from '../../shared/video/video.service'
22import { MarkdownService } from '../shared'
23import { VideoDownloadComponent } from './modal/video-download.component' 22import { VideoDownloadComponent } from './modal/video-download.component'
24import { VideoReportComponent } from './modal/video-report.component' 23import { VideoReportComponent } from './modal/video-report.component'
25import { VideoShareComponent } from './modal/video-share.component' 24import { VideoShareComponent } from './modal/video-share.component'
@@ -30,6 +29,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
30import { environment } from '../../../environments/environment' 29import { environment } from '../../../environments/environment'
31import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 30import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
32import { VideoCaptionService } from '@app/shared/video-caption' 31import { VideoCaptionService } from '@app/shared/video-caption'
32import { MarkdownService } from '@app/shared/renderer'
33 33
34@Component({ 34@Component({
35 selector: 'my-video-watch', 35 selector: 'my-video-watch',
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 54a12c126..2f448db78 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -1,9 +1,7 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 2import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
4import { ClipboardModule } from 'ngx-clipboard' 3import { ClipboardModule } from 'ngx-clipboard'
5import { SharedModule } from '../../shared' 4import { SharedModule } from '../../shared'
6import { MarkdownService } from '../shared'
7import { VideoCommentAddComponent } from './comment/video-comment-add.component' 5import { VideoCommentAddComponent } from './comment/video-comment-add.component'
8import { VideoCommentComponent } from './comment/video-comment.component' 6import { VideoCommentComponent } from './comment/video-comment.component'
9import { VideoCommentService } from './comment/video-comment.service' 7import { VideoCommentService } from './comment/video-comment.service'
@@ -46,8 +44,6 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
46 ], 44 ],
47 45
48 providers: [ 46 providers: [
49 MarkdownService,
50 LinkifierService,
51 VideoCommentService 47 VideoCommentService
52 ] 48 ]
53}) 49})
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
deleted file mode 100644
index 7a66944b9..000000000
--- a/client/src/app/videos/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './markdown.service'
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts
deleted file mode 100644
index 07017eca5..000000000
--- a/client/src/app/videos/shared/markdown.service.ts
+++ /dev/null
@@ -1,79 +0,0 @@
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}