Add markdown support to video description
authorChocobozzz <florian.bigard@gmail.com>
Thu, 26 Oct 2017 13:01:47 +0000 (15:01 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Thu, 26 Oct 2017 13:01:47 +0000 (15:01 +0200)
client/package.json
client/src/app/videos/+video-edit/video-update.component.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/+video-watch/video-watch.module.ts
client/src/app/videos/shared/index.ts
client/src/app/videos/shared/markdown.service.ts [new file with mode: 0644]
client/src/app/videos/shared/video-edit.model.ts
client/src/app/videos/shared/video.service.ts
client/yarn.lock

index 8b949ef80965b0ab236f3038f25d24f7884f04fd..8d42e0c8766125eb849f796eb8f0352eb7a978ac 100644 (file)
@@ -35,6 +35,7 @@
     "@angularclass/hmr-loader": "^3.0.2",
     "@ngx-meta/core": "^4.0.1",
     "@types/core-js": "^0.9.28",
+    "@types/markdown-it": "^0.0.4",
     "@types/node": "^8.0.33",
     "@types/source-map": "^0.5.1",
     "@types/uglify-js": "^2.0.27",
@@ -66,6 +67,7 @@
     "inline-manifest-webpack-plugin": "^3.0.1",
     "intl": "^1.2.4",
     "json-loader": "^0.5.4",
+    "markdown-it": "^8.4.0",
     "ng-router-loader": "^2.0.0",
     "ngc-webpack": "3.2.2",
     "ngx-bootstrap": "1.9.3",
index 70cb334fddde57e1192e0b89160d7e9cf8e97a55..30390ac058ef23836a9a2f420e6c0bdaa407d383 100644 (file)
@@ -87,7 +87,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
     this.videoService.getVideo(uuid)
                      .subscribe(
                        video => {
-                         this.video = video
+                         this.video = new VideoEdit(video)
 
                          this.hydrateFormFromVideo()
                        },
index 5d582734462c0ef9d48cc90b2b6fd4fe83dd18e8..6e502aae28f0d7c2f45e2f6f6ae9e4f220384e30 100644 (file)
         Published on {{ video.createdAt | date:'short' }}
       </div>
 
-      <div class="video-details-description">
-        {{ video.description }}
-      </div>
+      <div class="video-details-description" [innerHTML]="videoHTMLDescription"></div>
     </div>
 
     <div class="video-details-attributes col-xs-4 col-md-3">
index 529e2e84f5d1ce3e6f1d3d2d9efcffe9705bb2ae..2e1adb0434a757caa18a0d35da77ca8813f58909 100644 (file)
@@ -13,7 +13,7 @@ import { AuthService, ConfirmService } from '../../core'
 import { VideoDownloadComponent } from './video-download.component'
 import { VideoShareComponent } from './video-share.component'
 import { VideoReportComponent } from './video-report.component'
-import { VideoDetails, VideoService } from '../shared'
+import { VideoDetails, VideoService, MarkdownService } from '../shared'
 import { VideoBlacklistService } from '../../shared'
 import { UserVideoRateType, VideoRateType } from '../../../../../shared'
 
@@ -38,6 +38,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   video: VideoDetails = null
   videoPlayerLoaded = false
   videoNotFound = false
+  videoHTMLDescription = ''
 
   private paramsSub: Subscription
 
@@ -50,7 +51,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     private confirmService: ConfirmService,
     private metaService: MetaService,
     private authService: AuthService,
-    private notificationsService: NotificationsService
+    private notificationsService: NotificationsService,
+    private markdownService: MarkdownService
   ) {}
 
   ngOnInit () {
@@ -259,6 +261,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
           })
         })
 
+        this.videoHTMLDescription = this.markdownService.markdownToHTML(this.video.description)
+
         this.setOpenGraphTags()
         this.checkUserRating()
       }
index c6c1344ce27521c7b9e902b5d4b1175cbecf21b2..1b983200db3824dd307aa7516bdca98fdcb18834 100644 (file)
@@ -1,7 +1,7 @@
 import { NgModule } from '@angular/core'
 
 import { VideoWatchRoutingModule } from './video-watch-routing.module'
-import { VideoService } from '../shared'
+import { VideoService, MarkdownService } from '../shared'
 import { SharedModule } from '../../shared'
 
 import { VideoWatchComponent } from './video-watch.component'
@@ -28,6 +28,7 @@ import { VideoDownloadComponent } from './video-download.component'
   ],
 
   providers: [
+    MarkdownService,
     VideoService
   ]
 })
index dcaa4e0903a5f471a1fe6f2b6b754c4f922b72ed..09d961dd3ea74b40ac4c62572fad8a8ca6f19cc6 100644 (file)
@@ -1,4 +1,5 @@
 export * from './sort-field.type'
+export * from './markdown.service'
 export * from './video.model'
 export * from './video-details.model'
 export * from './video-edit.model'
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts
new file mode 100644 (file)
index 0000000..d8b5b76
--- /dev/null
@@ -0,0 +1,40 @@
+import { Injectable } from '@angular/core'
+
+import * as MarkdownIt from 'markdown-it'
+
+@Injectable()
+export class MarkdownService {
+  private markdownIt: MarkdownIt.MarkdownIt
+
+  constructor () {
+    this.markdownIt = new MarkdownIt('zero', { linkify: true })
+      .enable('linkify')
+      .enable('autolink')
+      .enable('emphasis')
+      .enable('link')
+      .enable('newline')
+
+    // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
+    const defaultRender = this.markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
+      return self.renderToken(tokens, idx, options)
+    }
+
+    this.markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) {
+      // If you are sure other plugins can't add `target` - drop check below
+      const aIndex = tokens[idx].attrIndex('target')
+
+      if (aIndex < 0) {
+        tokens[idx].attrPush(['target', '_blank']) // add new attribute
+      } else {
+        tokens[idx].attrs[aIndex][1] = '_blank'    // replace value of existing attr
+      }
+
+      // pass token to default renderer.
+      return defaultRender(tokens, idx, options, env, self)
+    }
+  }
+
+  markdownToHTML (markdown: string) {
+    return this.markdownIt.render(markdown)
+  }
+}
index f30d8febaf2d4366575a4d4b49146847f38ca7eb..e0b7bf130c4cbba00a3c1901ec9ebcd4d131bffc 100644 (file)
@@ -1,3 +1,5 @@
+import { VideoDetails } from './video-details.model'
+
 export class VideoEdit {
   category: number
   licence: number
@@ -10,6 +12,19 @@ export class VideoEdit {
   uuid?: string
   id?: number
 
+  constructor (videoDetails: VideoDetails) {
+    this.id = videoDetails.id
+    this.uuid = videoDetails.uuid
+    this.category = videoDetails.category
+    this.licence = videoDetails.licence
+    this.language = videoDetails.language
+    this.description = videoDetails.description
+    this.name = videoDetails.name
+    this.tags = videoDetails.tags
+    this.nsfw = videoDetails.nsfw
+    this.channel = videoDetails.channel.id
+  }
+
   patch (values: Object) {
     Object.keys(values).forEach((key) => {
       this[key] = values[key]
index 06fb3313ea7d52127b8a1cfedff649ba8036bfee..8fdc1f213fae0e55aeb205ba02a9a2df7243d895 100644 (file)
@@ -36,7 +36,7 @@ export class VideoService {
     private restService: RestService
   ) {}
 
-  getVideo (uuid: string) {
+  getVideo (uuid: string): Observable<VideoDetails> {
     return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid)
                         .map(videoHash => new VideoDetails(videoHash))
                         .catch((res) => this.restExtractor.handleError(res))
index b63c76e2fca0603fafde6fb49db0b1d763ce6477..23ab3a3df8b24bf7e86028af6cf2c8b76892a290 100644 (file)
   dependencies:
     "@types/node" "*"
 
+"@types/markdown-it@^0.0.4":
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.4.tgz#c5f67365916044b342dae8d702724788ba0b5b74"
+
 "@types/node@*":
   version "8.0.25"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.25.tgz#66ecaf4df93f5281b48427ee96fbcdfc4f0cdce1"
@@ -3842,6 +3846,12 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+linkify-it@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f"
+  dependencies:
+    uc.micro "^1.0.1"
+
 load-ip-set@^1.2.7:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-1.3.1.tgz#cfd050c6916e7ba0ca85d0b566e7854713eb495e"
@@ -4169,6 +4179,16 @@ map-visit@^0.1.5:
     lazy-cache "^2.0.1"
     object-visit "^0.3.4"
 
+markdown-it@^8.4.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.0.tgz#e2400881bf171f7018ed1bd9da441dac8af6306d"
+  dependencies:
+    argparse "^1.0.7"
+    entities "~1.1.1"
+    linkify-it "^2.0.0"
+    mdurl "^1.0.1"
+    uc.micro "^1.0.3"
+
 marked-terminal@^1.6.2:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
@@ -4194,6 +4214,10 @@ md5.js@^1.3.4:
     hash-base "^3.0.0"
     inherits "^2.0.1"
 
+mdurl@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6824,6 +6848,10 @@ typescript@^2.5.2:
   version "2.5.3"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d"
 
+uc.micro@^1.0.1, uc.micro@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
+
 uglify-js@3.0.x, uglify-js@^3.0.6:
   version "3.0.28"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.0.28.tgz#96b8495f0272944787b5843a1679aa326640d5f7"