/** * Copyright © 2016-2019 Threema GmbH (https://threema.ch/). * * This file is part of Threema Web. * * Threema Web is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero * General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Threema Web. If not, see . */ // tslint:disable:max-line-length import {markify, parse, tokenize, TokenType} from '../../src/markup_parser'; describe('Markup Parser', () => { describe('tokenizer', () => { it('simple', function() { const text = 'hello *there*!'; const tokens = tokenize(text); expect(tokens).toEqual([ { kind: TokenType.Text, value: 'hello ' }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: 'there' }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: '!' }, ]); }); it('nested', function() { const text = 'this is *_nested_*!'; const tokens = tokenize(text); expect(tokens).toEqual([ { kind: TokenType.Text, value: 'this is ' }, { kind: TokenType.Asterisk }, { kind: TokenType.Underscore }, { kind: TokenType.Text, value: 'nested' }, { kind: TokenType.Underscore }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: '!' }, ]); }); it('ignore if not along boundary', function() { const text = 'this*is_not~at-boundary'; const tokens = tokenize(text); expect(tokens).toEqual([ { kind: TokenType.Text, value: 'this*is_not~at-boundary' }, ]); }); it('ignore markup in URLs', function() { const text = 'ignore if *in* a link: https://example.com/pic_-_a.jpg'; const tokens = tokenize(text); expect(tokens).toEqual([ { kind: TokenType.Text, value: 'ignore if ' }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: 'in' }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: ' a link: https://example.com/pic_-_a.jpg' }, ]); }); it('with newlines', function() { const text = 'hello\n*world*\n'; const tokens = tokenize(text); expect(tokens).toEqual([ { kind: TokenType.Text, value: 'hello' }, { kind: TokenType.Newline }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: 'world' }, { kind: TokenType.Asterisk }, { kind: TokenType.Newline }, ]); }); }); describe('parser', () => { it('simple text without formatting', () => { const tokens = [{ kind: TokenType.Text, value: 'hello world' }]; const html = parse(tokens); expect(html).toEqual('hello world'); }); it('simple bold text', () => { const tokens = [ { kind: TokenType.Text, value: 'hello ' }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: 'bold' }, { kind: TokenType.Asterisk }, ]; const html = parse(tokens); expect(html).toEqual('hello bold'); }); it('simple italic text', () => { const tokens = [ { kind: TokenType.Text, value: 'hello ' }, { kind: TokenType.Underscore }, { kind: TokenType.Text, value: 'italic' }, { kind: TokenType.Underscore }, ]; const html = parse(tokens); expect(html).toEqual('hello italic'); }); it('simple strikethrough text', () => { const tokens = [ { kind: TokenType.Text, value: 'hello ' }, { kind: TokenType.Tilde }, { kind: TokenType.Text, value: 'strikethrough' }, { kind: TokenType.Tilde }, ]; const html = parse(tokens); expect(html).toEqual('hello strikethrough'); }); it('correct nesting', () => { const tokens = [ { kind: TokenType.Text, value: 'hello ' }, { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: 'bold and ' }, { kind: TokenType.Underscore }, { kind: TokenType.Text, value: 'italic' }, { kind: TokenType.Underscore }, { kind: TokenType.Asterisk }, ]; const html = parse(tokens); expect(html).toEqual('hello bold and italic'); }); it('incorrect nesting', () => { const tokens = [ { kind: TokenType.Asterisk }, { kind: TokenType.Text, value: 'hi ' }, { kind: TokenType.Underscore }, { kind: TokenType.Text, value: 'there' }, { kind: TokenType.Asterisk }, { kind: TokenType.Underscore }, ]; const html = parse(tokens); expect(html).toEqual('hi _there_'); }); }); function testPatterns(cases) { for (const testcase of cases) { const input = testcase[0]; const expected = testcase[1]; expect(markify(input)).toEqual(expected); } } describe('markify', () => { it('detects bold text', () => { testPatterns([ ['*bold text (not italic)*', 'bold text (not italic)'], ]); }); it('detects italic text', () => { testPatterns([ ['This text is not italic.', 'This text is not italic.'], ['_This text is italic._', 'This text is italic.'], ['This text is _partially_ italic', 'This text is partially italic'], ['This text has _two_ _italic_ bits', 'This text has two italic bits'], ]); }); it('detects strikethrough text', () => { testPatterns([ ['so ~strikethrough~', 'so strikethrough'], ]); }); it('detects mixed markup', () => { testPatterns([ ['*bold text with _italic_ *', 'bold text with italic '], ['*part bold,* _part italic_', 'part bold, part italic'], ['_italic text with *bold* _', 'italic text with bold '], ]); }); it('is applied on word boundaries', () => { testPatterns([ ['(*bold*)', '(bold)'], ['¡*Threema* es fantástico!', '¡Threema es fantástico!'], ['«_great_ service»', '«great service»'], ['"_great_" service', '"great" service'], ['*bold*…', 'bold…'], ['_Threema_', 'Threema'], ]); }); it('is only applied on word boundaries', () => { testPatterns([ ['so not_really_italic', 'so not_really_italic'], ['invalid*bold*stuff', 'invalid*bold*stuff'], ['no~strike~through', 'no~strike~through'], ['*bold_but_no~strike~through*', 'bold_but_no~strike~through'], ['<_< >_>', '<_< >_>'], ['_Threema_', '_Threema_'], ]); }); it('does not break URLs', () => { testPatterns([ ['https://en.wikipedia.org/wiki/Java_class_file *nice*', 'https://en.wikipedia.org/wiki/Java_class_file nice'], ['https://example.com/_output_/', 'https://example.com/_output_/'], ['https://example.com/*output*/', 'https://example.com/*output*/'], ['https://example.com?_twitter_impression=true', 'https://example.com?_twitter_impression=true'], ['https://example.com?__twitter_impression=true', 'https://example.com?__twitter_impression=true'], ['https://example.com?___twitter_impression=true', 'https://example.com?___twitter_impression=true'], ['https://example.com/image_-_1.jpg', 'https://example.com/image_-_1.jpg'], ]); }); it('ignores invalid markup', () => { testPatterns([ ['*invalid markup (do not parse)_', '*invalid markup (do not parse)_'], ['random *asterisk', 'random *asterisk'], ['***three asterisks', '***three asterisks'], ['***three asterisks*', '**three asterisks'], ]); }); it('ignores markup with \\n (newline)', () => { testPatterns([ ['*First line\n and a new one. (do not parse)*', '*First line\n and a new one. (do not parse)*'], ['*\nbegins with linebreak. (do not parse)*', '*\nbegins with linebreak. (do not parse)*'], ['*Just some text. But it ends with newline (do not parse)\n*', '*Just some text. But it ends with newline (do not parse)\n*'], ]); }); }); });