mirror of https://github.com/Squidex/squidex.git
Browse Source
* Escape HTML in markdown. * Escape variables for message history. * Rename escape method.pull/957/head
committed by
GitHub
23 changed files with 315 additions and 113 deletions
@ -0,0 +1,89 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { Renderer2 } from '@angular/core'; |
|||
import { IMock, It, Mock, Times } from 'typemoq'; |
|||
import { MarkdownDirective } from './markdown.directive'; |
|||
|
|||
describe('MarkdownDirective', () => { |
|||
let renderer: IMock<Renderer2>; |
|||
let markdownElement = {}; |
|||
let markdownDirective: MarkdownDirective; |
|||
|
|||
beforeEach(() => { |
|||
renderer = Mock.ofType<Renderer2>(); |
|||
|
|||
markdownElement = {}; |
|||
markdownDirective = new MarkdownDirective(markdownElement as any, renderer.object); |
|||
}); |
|||
|
|||
it('should render empty text as text', () => { |
|||
markdownDirective.markdown = ''; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyTextRender(''); |
|||
}); |
|||
|
|||
it('should render as text if result has no tags', () => { |
|||
markdownDirective.inline = true; |
|||
markdownDirective.markdown = 'markdown'; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyTextRender('markdown'); |
|||
}); |
|||
|
|||
it('should render as text if optional', () => { |
|||
markdownDirective.optional = true; |
|||
markdownDirective.markdown = '**bold**'; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyTextRender('**bold**'); |
|||
}); |
|||
|
|||
it('should render if optional with exclamation', () => { |
|||
markdownDirective.optional = true; |
|||
markdownDirective.markdown = '!**bold**'; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyHtmlRender('<strong>bold</strong>'); |
|||
}); |
|||
|
|||
it('should render as HTML if allowed', () => { |
|||
markdownDirective.inline = false; |
|||
markdownDirective.markdown = '**bold**'; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyHtmlRender('<p><strong>bold</strong></p>\n'); |
|||
}); |
|||
|
|||
it('should render as inline HTML if allowed', () => { |
|||
markdownDirective.markdown = '!**bold**'; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyHtmlRender('<strong>bold</strong>'); |
|||
}); |
|||
|
|||
it('should render HTML escaped', () => { |
|||
markdownDirective.inline = false; |
|||
markdownDirective.markdown = '<h1>Header</h1>'; |
|||
markdownDirective.ngOnChanges(); |
|||
|
|||
verifyHtmlRender('<p><h1>Header</h1></p>\n'); |
|||
}); |
|||
|
|||
function verifyTextRender(text: string) { |
|||
renderer.verify(x => x.setProperty(It.isAny(), 'textContent', text), Times.once()); |
|||
|
|||
expect().nothing(); |
|||
} |
|||
|
|||
function verifyHtmlRender(text: string) { |
|||
renderer.verify(x => x.setProperty(It.isAny(), 'innerHTML', text), Times.once()); |
|||
|
|||
expect().nothing(); |
|||
} |
|||
}); |
|||
@ -0,0 +1,61 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { marked } from 'marked'; |
|||
|
|||
function renderLink(href: string, _: string, text: string) { |
|||
if (href && href.startsWith('mailto')) { |
|||
return text; |
|||
} else { |
|||
return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`; |
|||
} |
|||
} |
|||
|
|||
function renderInlineParagraph(text: string) { |
|||
return text; |
|||
} |
|||
|
|||
const RENDERER_DEFAULT = new marked.Renderer(); |
|||
const RENDERER_INLINE = new marked.Renderer(); |
|||
|
|||
RENDERER_INLINE.paragraph = renderInlineParagraph; |
|||
RENDERER_INLINE.link = renderLink; |
|||
RENDERER_DEFAULT.link = renderLink; |
|||
|
|||
export function renderMarkdown(input: string | undefined | null, inline: boolean) { |
|||
if (!input) { |
|||
return ''; |
|||
} |
|||
|
|||
input = escapeHTML(input); |
|||
|
|||
if (inline) { |
|||
return marked(input, { renderer: RENDERER_INLINE }); |
|||
} else { |
|||
return marked(input, { renderer: RENDERER_DEFAULT }); |
|||
} |
|||
} |
|||
|
|||
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; |
|||
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); |
|||
const escapeReplacements = { |
|||
'&' : '&', |
|||
'<' : '<', |
|||
'>' : '>', |
|||
'"' : '"', |
|||
'\'': ''', |
|||
}; |
|||
|
|||
const getEscapeReplacement = (ch: string) => escapeReplacements[ch]; |
|||
|
|||
export function escapeHTML(html: string) { |
|||
if (escapeTestNoEncode.test(html)) { |
|||
return html.replace(escapeReplaceNoEncode, getEscapeReplacement); |
|||
} |
|||
|
|||
return html; |
|||
} |
|||
Loading…
Reference in new issue