import { StreamParser } from '@codemirror/language'; import { Tag } from '@lezer/highlight'; import CodeMirror from '../../src/codemirror.js'; import mediaWikiLang from '../../src/codemirror.mode.mediawiki.js'; import { mwModeConfig } from '../../src/codemirror.mode.mediawiki.config.js'; // NOTE: each test case should have a space before the closing // This is to avoid interactive UI components from showing up in the test output. const testCases = [ { title: 'p tags, extra closing tag', input: 'this is

content

', output: '
this is <p><div>content</p></p>
' }, { title: 'HTML tag attributes', input: '', output: '
<span title="a<b"><b title="a>b"></b></span>
' }, { title: 'ref tag attributes', input: '', output: '
<ref name="a<b"/>
' }, { title: 'indented table with caption and inline headings', input: ' ::{| class="wikitable"\n |+ Caption\n |-\n ! Uno !! Dos\n |-\n | Foo || Bar\n |}', output: '
::{| class="wikitable"
|+ Caption
|-
! Uno !! Dos
|-
| Foo || Bar
|}
' }, { title: 'apostrophe before italic', input: 'plain l\'\'\'italic\'\'plain', output: '
plain l\'\'\'italic\'\'plain
' }, { title: 'free external links', input: 'https://wikimedia.org [ftp://foo.bar FOO] //archive.org', output: '
https://wikimedia.org [ftp://foo.bar FOO] //archive.org
' }, { title: 'not free external links', input: 'news: foo news:bar [news: baz]', output: '
news: foo news:bar [news: baz]
' }, { title: 'void tags', input: 'a
b
c a
b
c
d', output: '
a<br>b</br>c a<div>b<br>c</div>d
' }, { title: 'magic words', input: '__NOTOC__', output: '
__NOTOC__
' }, { title: 'nowiki', input: '{{foo}}

{{{\n
\n\n {{bar}}
', output: '
<nowiki>{{foo}}<p> </div> {{{</nowiki>
<nowiki/><pre class="foo">

{{bar}}</pre>
' }, { title: 'ref tag with cite web, extraneous curly braces', input: '{{cite web|2=foo}}}}', output: '
<ref>{{cite web|2=foo}}}}</ref>
' }, { title: 'template with params and parser function', input: '{{foo|1=bar|2={{{param|blah}}}|{{#if:{{{3|}}}|yes|no}}}}', output: '
{{foo|1=bar|2={{{param|blah}}}|{{#if:{{{3|}}}|yes|no}}}}
' }, { title: 'T277767: newlines and comments in template names', input: '{{#if: | {{some template\n }} }}', output: '
{{#if: | {{some template
<!-- comment --> }} }}
' }, { title: 'T108450: template transclusion where the template name is a parameter', input: '{{{{{1}}}|…}}', output: '
{{{{{1}}}|}}
' }, { title: 'T292967: table syntax where all | are escaped with the {{!}} parser function', input: '{{{!}} class="wikitable"\n! header\n{{!}}-\n{{!}} cell\n{{!}}}', output: '
{{{!}} class="wikitable"
! header
{{!}}-
{{!}} cell
{{!}}}
' }, { title: 'T324374: table cell attributes', input: '{|\n|+ class="z" | Z\n! class="a" | A !! class="b" | B\n|-\n! class="c" | C || class="d" | D\n|-\n| class="e" | E || class="f" | F\n|}', output: '
{|
|+ class="z" | Z
! class="a" | A !! class="b" | B
|-
! class="c" | C || class="d" | D
|-
| class="e" | E || class="f" | F
|}
' }, { title: 'section headings', input: '== My section ==\nFoo bar\n=== Blah ===\nBaz\n= { =\nText', output: '
== My section ==
Foo bar
=== Blah ===
Baz
= { =
Text
' }, { title: 'section headings with trailing comments', input: '== My section == \nFoo bar\n=== Blah ===\nBaz\n== a == ', output: '
== My section == <!-- comment -->
Foo bar
=== Blah ===<!--comment-->
Baz
== <i>a</i> <!-- comment --> == <!--comment-->
' }, { title: 'bullets and numbering, with invalid leading spacing', input: '* bullet A\n* bullet B\n# one\n # two', output: '
* bullet A
* bullet B
# one
# two
' }, { title: 'nested ordered, unordered and definition lists', input: '*#;: item A\n#;:* item B\n;:*# item C\n:*#; item D', output: '
*#;: item A
#;:* item B
;:*# item C
:*#; item D
' }, { title: 'link with bold text', input: '[[Link title|\'\'\'bold link\'\'\']]', output: '
[[Link title|\'\'\'bold link\'\'\']]
' }, { title: 'horizontal rule', input: 'One\n----\nTwo', output: '
One
----
Two
' }, { title: 'comments', input: '', output: '
<!-- foo [[bar]] {{{param}}} -->
' }, { title: 'signatures', input: 'my sig ~~~ ~~~~ ~~~~~~~', output: '
my sig ~~~ ~~~~ ~~~~~~~
' }, { title: 'capitalization of tags', input: '', output: '
<ref></Ref>
' }, { title: 'multi-line tag', input: 'bar', output: '
<div
id="foo"
>bar</div>
' }, { title: 'HTML entities', input: '—\n[[/dev/null]]', output: '
&#x2014;
[[&#47;dev/null]]
' }, { title: 'Extension tag with no TagMode', input: 'foo\nbar\nbaz', output: '
<myextension>foo
bar
baz</myextension>
' }, { title: 'Special characters', input: 'Soft­hyphen\nzero-width​space\nnon-breaking space\nnarrow nbsp', // i18n messages are the keys because we don't stub mw.msg() in this test. output: '
Softhyphen
zero-widthspace
non-breaking·space
narrow·nbsp
' }, { title: 'Nested template calls', input: '{{foo|{{bar|[[Test]]|{{baz|[[Test2]]}}}}}}', output: '
{{foo|{{bar|[[Test]]|{{baz|[[Test2]]}}}}}}
' } ]; // Setup CodeMirror instance. const textarea = document.createElement( 'textarea' ); document.body.appendChild( textarea ); const cm = new CodeMirror( textarea ); // Stub the config normally provided by mw.config.get('extCodeMirrorConfig') const mwLang = mediaWikiLang( {}, { urlProtocols: 'ftp://|https://|news:', doubleUnderscore: [ { __notoc__: 'notoc' } ], functionSynonyms: [ {}, { '!': '!' } ], tags: { nowiki: true, pre: true, ref: true, references: true, // Made-up tag, for testing when a corresponding TagMode is not configured. myextension: true }, tagModes: { ref: 'mediawiki', references: 'mediawiki' } } ); cm.initialize( [ ...cm.defaultExtensions, mwLang ] ); describe( 'CodeMirrorModeMediaWiki', () => { it.each( testCases )( 'syntax highlighting ($title)', ( { input, output } ) => { cm.view.dispatch( { changes: { from: 0, to: cm.view.state.doc.length, insert: input + ' ' }, // Above we add an extra space to the end, and here we've move the cursor there. // This is to avoid bracket matching and other interactive UI components // from showing up in the test output. selection: { anchor: input.length + 1 } } ); cm.$textarea.textSelection = jest.fn().mockReturnValue( input ); expect( cm.view.dom.querySelector( '.cm-content' ).innerHTML ).toStrictEqual( output ); } ); it( 'configuration contains all expected tokens', () => { expect( Object.keys( mwModeConfig.tags ) ).toStrictEqual( [ 'apostrophes', 'apostrophesBold', 'apostrophesItalic', 'comment', 'doubleUnderscore', 'extLink', 'extLinkBracket', 'extLinkProtocol', 'extLinkText', 'hr', 'htmlTagAttribute', 'htmlTagBracket', 'htmlTagName', 'indenting', 'linkBracket', 'linkDelimiter', 'linkText', 'linkToSection', 'list', 'parserFunction', 'parserFunctionBracket', 'parserFunctionDelimiter', 'parserFunctionName', 'sectionHeader', 'sectionHeader1', 'sectionHeader2', 'sectionHeader3', 'sectionHeader4', 'sectionHeader5', 'sectionHeader6', 'signature', 'tableBracket', 'tableDefinition', 'tableDelimiter', 'template', 'templateArgumentName', 'templateBracket', 'templateDelimiter', 'templateName', 'templateVariable', 'templateVariableBracket', 'templateVariableName', // Custom tags 'em', 'error', 'extNowiki', 'extPre', 'extTag', 'extTagAttribute', 'extTagBracket', 'extTagName', 'freeExtLink', 'freeExtLinkProtocol', 'htmlEntity', 'link', 'linkPageName', 'nowiki', 'pageName', 'pre', 'section', 'skipFormatting', 'strong', 'tableCaption', 'templateVariableDelimiter' ] ); } ); it( 'configuration has a TagStyle for all expected CSS classes', () => { /** @type {StreamParser} */ const mockContext = { tokenTable: jest.fn().mockReturnValue( Tag.define() ) }; const cssClasses = mwModeConfig.getTagStyles( mockContext ) .map( ( tagStyle ) => tagStyle.class ); expect( cssClasses ).toStrictEqual( [ 'cm-mw-apostrophes', 'cm-mw-apostrophes-bold', 'cm-mw-apostrophes-italic', 'cm-mw-comment', 'cm-mw-double-underscore', 'cm-mw-extlink', 'cm-mw-extlink-bracket', 'cm-mw-extlink-protocol', 'cm-mw-extlink-text', 'cm-mw-hr', 'cm-mw-htmltag-attribute', 'cm-mw-htmltag-bracket', 'cm-mw-htmltag-name', 'cm-mw-indenting', 'cm-mw-link-bracket', 'cm-mw-link-delimiter', 'cm-mw-link-text', 'cm-mw-link-tosection', 'cm-mw-list', 'cm-mw-parserfunction', 'cm-mw-parserfunction-bracket', 'cm-mw-parserfunction-delimiter', 'cm-mw-parserfunction-name', 'cm-mw-section-header', 'cm-mw-section-1', 'cm-mw-section-2', 'cm-mw-section-3', 'cm-mw-section-4', 'cm-mw-section-5', 'cm-mw-section-6', 'cm-mw-signature', 'cm-mw-table-bracket', 'cm-mw-table-definition', 'cm-mw-table-delimiter', 'cm-mw-template', 'cm-mw-template-argument-name', 'cm-mw-template-bracket', 'cm-mw-template-delimiter', 'cm-mw-pagename cm-mw-template-name', 'cm-mw-templatevariable', 'cm-mw-templatevariable-bracket', 'cm-mw-templatevariable-name', // Custom tags 'cm-mw-em', 'cm-mw-error', 'cm-mw-ext-nowiki', 'cm-mw-ext-pre', 'cm-mw-exttag-bracket', 'cm-mw-exttag', 'cm-mw-exttag-attribute', 'cm-mw-exttag-name', 'cm-mw-free-extlink', 'cm-mw-free-extlink-protocol', 'cm-mw-html-entity', 'cm-mw-link', 'cm-mw-link-pagename', 'cm-mw-tag-nowiki', 'cm-mw-pagename', 'cm-mw-tag-pre', 'cm-mw-section', 'cm-mw-skipformatting', 'cm-mw-strong', 'cm-mw-table-caption', 'cm-mw-templatevariable-delimiter', // Dynamically generated tags 'cm-mw-ext-ground', 'cm-mw-ext-link-ground', 'cm-mw-ext2-ground', 'cm-mw-ext2-link-ground', 'cm-mw-ext3-ground', 'cm-mw-ext3-link-ground', 'cm-mw-link-ground', 'cm-mw-template-ext-ground', 'cm-mw-template-ext-link-ground', 'cm-mw-template-ext2-ground', 'cm-mw-template-ext2-link-ground', 'cm-mw-template-ext3-ground', 'cm-mw-template-ext3-link-ground', 'cm-mw-template-ground', 'cm-mw-template-link-ground', 'cm-mw-template2-ext-ground', 'cm-mw-template2-ext-link-ground', 'cm-mw-template2-ext2-ground', 'cm-mw-template2-ext2-link-ground', 'cm-mw-template2-ext3-ground', 'cm-mw-template2-ext3-link-ground', 'cm-mw-template2-ground', 'cm-mw-template2-link-ground', 'cm-mw-template3-ext-ground', 'cm-mw-template3-ext-link-ground', 'cm-mw-template3-ext2-ground', 'cm-mw-template3-ext2-link-ground', 'cm-mw-template3-ext3-ground', 'cm-mw-template3-ext3-link-ground', 'cm-mw-template3-ground', 'cm-mw-template3-link-ground', /** Added by the MW config stub above {@link mwLang} */ 'cm-mw-tag-ref', 'cm-mw-ext-ref', 'cm-mw-tag-references', 'cm-mw-ext-references', 'cm-mw-tag-myextension', 'cm-mw-ext-myextension' ] ); } ); } );