aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-04 10:31:54 +0100
committerChocobozzz <me@florianbigard.com>2022-02-04 10:38:32 +0100
commitc68e2b2d223c57836e04e18105255cf0e10ae75b (patch)
treea40348363efc90464ff44306435d45079b0b7fca
parent457c83486ed2037a8cf0e55b06b1ae9370ed4d93 (diff)
downloadPeerTube-c68e2b2d223c57836e04e18105255cf0e10ae75b.tar.gz
PeerTube-c68e2b2d223c57836e04e18105255cf0e10ae75b.tar.zst
PeerTube-c68e2b2d223c57836e04e18105255cf0e10ae75b.zip
Fix plaintext markdown converter
-rw-r--r--client/src/app/core/renderer/html-renderer.service.ts4
-rw-r--r--server/controllers/feeds.ts6
-rw-r--r--server/helpers/markdown.ts68
-rw-r--r--server/lib/client-html.ts8
-rw-r--r--server/tests/helpers/index.ts1
-rw-r--r--server/tests/helpers/markdown.ts34
-rw-r--r--shared/core-utils/renderer/html.ts10
7 files changed, 104 insertions, 27 deletions
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts
index 418d8603e..d158519f8 100644
--- a/client/src/app/core/renderer/html-renderer.service.ts
+++ b/client/src/app/core/renderer/html-renderer.service.ts
@@ -1,6 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { getCustomMarkupSanitizeOptions, getDefaultSanitizeOptions } from '@shared/core-utils/renderer/html'
2import { LinkifierService } from './linkifier.service' 3import { LinkifierService } from './linkifier.service'
3import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
4 4
5@Injectable() 5@Injectable()
6export class HtmlRendererService { 6export class HtmlRendererService {
@@ -30,7 +30,7 @@ export class HtmlRendererService {
30 30
31 const options = additionalAllowedTags.length !== 0 31 const options = additionalAllowedTags.length !== 0
32 ? getCustomMarkupSanitizeOptions(additionalAllowedTags) 32 ? getCustomMarkupSanitizeOptions(additionalAllowedTags)
33 : getSanitizeOptions() 33 : getDefaultSanitizeOptions()
34 34
35 return this.sanitizeHtml(html, options) 35 return this.sanitizeHtml(html, options)
36 } 36 }
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 3c8680ca4..e6cdaf94b 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,6 +1,6 @@
1import express from 'express' 1import express from 'express'
2import Feed from 'pfeed' 2import Feed from 'pfeed'
3import { mdToPlainText, toSafeHtml } from '@server/helpers/markdown' 3import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown'
4import { getServerActor } from '@server/models/application/application' 4import { getServerActor } from '@server/models/application/application'
5import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 5import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
6import { VideoInclude } from '@shared/models' 6import { VideoInclude } from '@shared/models'
@@ -236,7 +236,7 @@ function initFeed (parameters: {
236 236
237 return new Feed({ 237 return new Feed({
238 title: name, 238 title: name,
239 description: mdToPlainText(description), 239 description: mdToOneLinePlainText(description),
240 // updated: TODO: somehowGetLatestUpdate, // optional, default = today 240 // updated: TODO: somehowGetLatestUpdate, // optional, default = today
241 id: webserverUrl, 241 id: webserverUrl,
242 link: webserverUrl, 242 link: webserverUrl,
@@ -299,7 +299,7 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
299 title: video.name, 299 title: video.name,
300 id: video.url, 300 id: video.url,
301 link: WEBSERVER.URL + video.getWatchStaticPath(), 301 link: WEBSERVER.URL + video.getWatchStaticPath(),
302 description: mdToPlainText(video.getTruncatedDescription()), 302 description: mdToOneLinePlainText(video.getTruncatedDescription()),
303 content: toSafeHtml(video.description), 303 content: toSafeHtml(video.description),
304 author: [ 304 author: [
305 { 305 {
diff --git a/server/helpers/markdown.ts b/server/helpers/markdown.ts
index 0b8c2fabc..25685ec6d 100644
--- a/server/helpers/markdown.ts
+++ b/server/helpers/markdown.ts
@@ -1,14 +1,14 @@
1import { getSanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils' 1import { getDefaultSanitizeOptions, getTextOnlySanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
2 2
3const sanitizeOptions = getSanitizeOptions() 3const defaultSanitizeOptions = getDefaultSanitizeOptions()
4const textOnlySanitizeOptions = getTextOnlySanitizeOptions()
4 5
5const sanitizeHtml = require('sanitize-html') 6const sanitizeHtml = require('sanitize-html')
6const markdownItEmoji = require('markdown-it-emoji/light') 7const markdownItEmoji = require('markdown-it-emoji/light')
7const MarkdownItClass = require('markdown-it') 8const MarkdownItClass = require('markdown-it')
8const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true })
9 9
10markdownIt.enable(TEXT_WITH_HTML_RULES) 10const markdownItWithHTML = new MarkdownItClass('default', { linkify: true, breaks: true, html: true })
11markdownIt.use(markdownItEmoji) 11const markdownItWithoutHTML = new MarkdownItClass('default', { linkify: true, breaks: true, html: false })
12 12
13const toSafeHtml = (text: string) => { 13const toSafeHtml = (text: string) => {
14 if (!text) return '' 14 if (!text) return ''
@@ -17,29 +17,65 @@ const toSafeHtml = (text: string) => {
17 const textWithLineFeed = text.replace(/<br.?\/?>/g, '\r\n') 17 const textWithLineFeed = text.replace(/<br.?\/?>/g, '\r\n')
18 18
19 // Convert possible markdown (emojis, emphasis and lists) to html 19 // Convert possible markdown (emojis, emphasis and lists) to html
20 const html = markdownIt.render(textWithLineFeed) 20 const html = markdownItWithHTML.enable(TEXT_WITH_HTML_RULES)
21 .use(markdownItEmoji)
22 .render(textWithLineFeed)
21 23
22 // Convert to safe Html 24 // Convert to safe Html
23 return sanitizeHtml(html, sanitizeOptions) 25 return sanitizeHtml(html, defaultSanitizeOptions)
24} 26}
25 27
26const mdToPlainText = (text: string) => { 28const mdToOneLinePlainText = (text: string) => {
27 if (!text) return '' 29 if (!text) return ''
28 30
29 // Convert possible markdown (emojis, emphasis and lists) to html 31 markdownItWithoutHTML.use(markdownItEmoji)
30 const html = markdownIt.render(text) 32 .use(plainTextPlugin)
33 .render(text)
31 34
32 // Convert to safe Html 35 // Convert to safe Html
33 const safeHtml = sanitizeHtml(html, sanitizeOptions) 36 return sanitizeHtml(markdownItWithoutHTML.plainText, textOnlySanitizeOptions)
34
35 return safeHtml.replace(/<[^>]+>/g, '')
36 .replace(/\n$/, '')
37 .replace(/\n/g, ', ')
38} 37}
39 38
40// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
41 40
42export { 41export {
43 toSafeHtml, 42 toSafeHtml,
44 mdToPlainText 43 mdToOneLinePlainText
44}
45
46// ---------------------------------------------------------------------------
47
48// Thanks: https://github.com/wavesheep/markdown-it-plain-text
49function plainTextPlugin (markdownIt: any) {
50 let lastSeparator = ''
51
52 function plainTextRule (state: any) {
53 const text = scan(state.tokens)
54
55 markdownIt.plainText = text.replace(/\s+/g, ' ')
56 }
57
58 function scan (tokens: any[]) {
59 let text = ''
60
61 for (const token of tokens) {
62 if (token.children !== null) {
63 text += scan(token.children)
64 continue
65 }
66
67 if (token.type === 'list_item_close') {
68 lastSeparator = ', '
69 } else if (/[a-zA-Z]+_close/.test(token.type)) {
70 lastSeparator = ' '
71 } else if (token.content) {
72 text += lastSeparator
73 text += token.content
74 }
75 }
76
77 return text
78 }
79
80 markdownIt.core.ruler.push('plainText', plainTextRule)
45} 81}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 74788af52..19354ab70 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -12,7 +12,7 @@ import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
12import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' 12import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
13import { isTestInstance } from '../helpers/core-utils' 13import { isTestInstance } from '../helpers/core-utils'
14import { logger } from '../helpers/logger' 14import { logger } from '../helpers/logger'
15import { mdToPlainText } from '../helpers/markdown' 15import { mdToOneLinePlainText } from '../helpers/markdown'
16import { CONFIG } from '../initializers/config' 16import { CONFIG } from '../initializers/config'
17import { 17import {
18 ACCEPT_HEADERS, 18 ACCEPT_HEADERS,
@@ -103,7 +103,7 @@ class ClientHtml {
103 res.status(HttpStatusCode.NOT_FOUND_404) 103 res.status(HttpStatusCode.NOT_FOUND_404)
104 return html 104 return html
105 } 105 }
106 const description = mdToPlainText(video.description) 106 const description = mdToOneLinePlainText(video.description)
107 107
108 let customHtml = ClientHtml.addTitleTag(html, video.name) 108 let customHtml = ClientHtml.addTitleTag(html, video.name)
109 customHtml = ClientHtml.addDescriptionTag(customHtml, description) 109 customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -164,7 +164,7 @@ class ClientHtml {
164 return html 164 return html
165 } 165 }
166 166
167 const description = mdToPlainText(videoPlaylist.description) 167 const description = mdToOneLinePlainText(videoPlaylist.description)
168 168
169 let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name) 169 let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name)
170 customHtml = ClientHtml.addDescriptionTag(customHtml, description) 170 customHtml = ClientHtml.addDescriptionTag(customHtml, description)
@@ -263,7 +263,7 @@ class ClientHtml {
263 return ClientHtml.getIndexHTML(req, res) 263 return ClientHtml.getIndexHTML(req, res)
264 } 264 }
265 265
266 const description = mdToPlainText(entity.description) 266 const description = mdToOneLinePlainText(entity.description)
267 267
268 let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName()) 268 let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName())
269 customHtml = ClientHtml.addDescriptionTag(customHtml, description) 269 customHtml = ClientHtml.addDescriptionTag(customHtml, description)
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts
index 66db93c99..91d11e25d 100644
--- a/server/tests/helpers/index.ts
+++ b/server/tests/helpers/index.ts
@@ -1,4 +1,5 @@
1import './image' 1import './image'
2import './core-utils' 2import './core-utils'
3import './comment-model' 3import './comment-model'
4import './markdown'
4import './request' 5import './request'
diff --git a/server/tests/helpers/markdown.ts b/server/tests/helpers/markdown.ts
new file mode 100644
index 000000000..0488a1a05
--- /dev/null
+++ b/server/tests/helpers/markdown.ts
@@ -0,0 +1,34 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { mdToOneLinePlainText } from '@server/helpers/markdown'
5import { expect } from 'chai'
6
7describe('Markdown helpers', function () {
8
9 describe('Plain text', function () {
10
11 it('Should convert a list to plain text', function () {
12 const result = mdToOneLinePlainText(`* list 1
13* list 2
14* list 3`)
15
16 expect(result).to.equal('list 1, list 2, list 3')
17 })
18
19 it('Should convert a list with indentation to plain text', function () {
20 const result = mdToOneLinePlainText(`Hello:
21 * list 1
22 * list 2
23 * list 3`)
24
25 expect(result).to.equal('Hello: list 1, list 2, list 3')
26 })
27
28 it('Should convert HTML to plain text', function () {
29 const result = mdToOneLinePlainText(`**Hello** <strong>coucou</strong>`)
30
31 expect(result).to.equal('Hello coucou')
32 })
33 })
34})
diff --git a/shared/core-utils/renderer/html.ts b/shared/core-utils/renderer/html.ts
index c9757be85..502308979 100644
--- a/shared/core-utils/renderer/html.ts
+++ b/shared/core-utils/renderer/html.ts
@@ -1,4 +1,4 @@
1export function getSanitizeOptions () { 1export function getDefaultSanitizeOptions () {
2 return { 2 return {
3 allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ], 3 allowedTags: [ 'a', 'p', 'span', 'br', 'strong', 'em', 'ul', 'ol', 'li' ],
4 allowedSchemes: [ 'http', 'https' ], 4 allowedSchemes: [ 'http', 'https' ],
@@ -23,8 +23,14 @@ export function getSanitizeOptions () {
23 } 23 }
24} 24}
25 25
26export function getTextOnlySanitizeOptions () {
27 return {
28 allowedTags: [] as string[]
29 }
30}
31
26export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) { 32export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) {
27 const base = getSanitizeOptions() 33 const base = getDefaultSanitizeOptions()
28 34
29 return { 35 return {
30 allowedTags: [ 36 allowedTags: [