diff options
author | Chocobozzz <me@florianbigard.com> | 2022-02-04 10:31:54 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-02-04 10:38:32 +0100 |
commit | c68e2b2d223c57836e04e18105255cf0e10ae75b (patch) | |
tree | a40348363efc90464ff44306435d45079b0b7fca | |
parent | 457c83486ed2037a8cf0e55b06b1ae9370ed4d93 (diff) | |
download | PeerTube-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.ts | 4 | ||||
-rw-r--r-- | server/controllers/feeds.ts | 6 | ||||
-rw-r--r-- | server/helpers/markdown.ts | 68 | ||||
-rw-r--r-- | server/lib/client-html.ts | 8 | ||||
-rw-r--r-- | server/tests/helpers/index.ts | 1 | ||||
-rw-r--r-- | server/tests/helpers/markdown.ts | 34 | ||||
-rw-r--r-- | shared/core-utils/renderer/html.ts | 10 |
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 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { getCustomMarkupSanitizeOptions, getDefaultSanitizeOptions } from '@shared/core-utils/renderer/html' | ||
2 | import { LinkifierService } from './linkifier.service' | 3 | import { LinkifierService } from './linkifier.service' |
3 | import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html' | ||
4 | 4 | ||
5 | @Injectable() | 5 | @Injectable() |
6 | export class HtmlRendererService { | 6 | export 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import Feed from 'pfeed' | 2 | import Feed from 'pfeed' |
3 | import { mdToPlainText, toSafeHtml } from '@server/helpers/markdown' | 3 | import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' |
4 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
5 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' | 5 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' |
6 | import { VideoInclude } from '@shared/models' | 6 | import { 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 @@ | |||
1 | import { getSanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils' | 1 | import { getDefaultSanitizeOptions, getTextOnlySanitizeOptions, TEXT_WITH_HTML_RULES } from '@shared/core-utils' |
2 | 2 | ||
3 | const sanitizeOptions = getSanitizeOptions() | 3 | const defaultSanitizeOptions = getDefaultSanitizeOptions() |
4 | const textOnlySanitizeOptions = getTextOnlySanitizeOptions() | ||
4 | 5 | ||
5 | const sanitizeHtml = require('sanitize-html') | 6 | const sanitizeHtml = require('sanitize-html') |
6 | const markdownItEmoji = require('markdown-it-emoji/light') | 7 | const markdownItEmoji = require('markdown-it-emoji/light') |
7 | const MarkdownItClass = require('markdown-it') | 8 | const MarkdownItClass = require('markdown-it') |
8 | const markdownIt = new MarkdownItClass('default', { linkify: true, breaks: true, html: true }) | ||
9 | 9 | ||
10 | markdownIt.enable(TEXT_WITH_HTML_RULES) | 10 | const markdownItWithHTML = new MarkdownItClass('default', { linkify: true, breaks: true, html: true }) |
11 | markdownIt.use(markdownItEmoji) | 11 | const markdownItWithoutHTML = new MarkdownItClass('default', { linkify: true, breaks: true, html: false }) |
12 | 12 | ||
13 | const toSafeHtml = (text: string) => { | 13 | const 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 | ||
26 | const mdToPlainText = (text: string) => { | 28 | const 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 | ||
42 | export { | 41 | export { |
43 | toSafeHtml, | 42 | toSafeHtml, |
44 | mdToPlainText | 43 | mdToOneLinePlainText |
44 | } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | // Thanks: https://github.com/wavesheep/markdown-it-plain-text | ||
49 | function 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' | |||
12 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' | 12 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' |
13 | import { isTestInstance } from '../helpers/core-utils' | 13 | import { isTestInstance } from '../helpers/core-utils' |
14 | import { logger } from '../helpers/logger' | 14 | import { logger } from '../helpers/logger' |
15 | import { mdToPlainText } from '../helpers/markdown' | 15 | import { mdToOneLinePlainText } from '../helpers/markdown' |
16 | import { CONFIG } from '../initializers/config' | 16 | import { CONFIG } from '../initializers/config' |
17 | import { | 17 | import { |
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 @@ | |||
1 | import './image' | 1 | import './image' |
2 | import './core-utils' | 2 | import './core-utils' |
3 | import './comment-model' | 3 | import './comment-model' |
4 | import './markdown' | ||
4 | import './request' | 5 | import './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 | |||
3 | import 'mocha' | ||
4 | import { mdToOneLinePlainText } from '@server/helpers/markdown' | ||
5 | import { expect } from 'chai' | ||
6 | |||
7 | describe('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 @@ | |||
1 | export function getSanitizeOptions () { | 1 | export 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 | ||
26 | export function getTextOnlySanitizeOptions () { | ||
27 | return { | ||
28 | allowedTags: [] as string[] | ||
29 | } | ||
30 | } | ||
31 | |||
26 | export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) { | 32 | export function getCustomMarkupSanitizeOptions (additionalAllowedTags: string[] = []) { |
27 | const base = getSanitizeOptions() | 33 | const base = getDefaultSanitizeOptions() |
28 | 34 | ||
29 | return { | 35 | return { |
30 | allowedTags: [ | 36 | allowedTags: [ |