Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4061222
codemirror.mediawiki.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
51 KB
Referenced Files
None
Subscribers
None
codemirror.mediawiki.js
View Options
const
{
EditorView
,
HighlightStyle
,
LanguageSupport
,
StreamLanguage
,
StreamParser
,
StringStream
,
Tag
,
syntaxHighlighting
}
=
require
(
'ext.CodeMirror.v6.lib'
);
const
CodeMirrorMode
=
require
(
'../codemirror.mode.js'
);
const
mwModeConfig
=
require
(
'./codemirror.mediawiki.config.js'
);
const
bidiIsolationExtension
=
require
(
'./codemirror.mediawiki.bidiIsolation.js'
);
const
{
codeFoldingExtension
}
=
require
(
'./codemirror.mediawiki.codeFolding.js'
);
const
{
autocompleteExtension
,
completionSource
}
=
require
(
'./codemirror.mediawiki.autocomplete.js'
);
const
openLinksExtension
=
require
(
'./codemirror.mediawiki.openLinks.js'
);
const
mwKeymap
=
require
(
'./codemirror.mediawiki.keymap.js'
);
const
lintSource
=
require
(
'./codemirror.mediawiki.lint.js'
);
const
specialParserFunctions
=
{
ifexist
:
0
,
lst
:
0
,
lstx
:
0
,
lsth
:
0
,
filepath
:
6
,
int
:
8
,
invoke
:
828
},
nsIds
=
mw
.
config
.
get
(
'wgNamespaceIds'
),
fileNsRegex
=
new
RegExp
(
`^(?:
${
Object
.
entries
(
nsIds
).
filter
(
(
[
,
id
]
)
=>
id
===
6
).
map
(
(
[
ns
]
)
=>
ns
).
join
(
'|'
)
}
)\\s*:`
,
'i'
);
const
startState
=
(
tokenize
)
=>
(
{
tokenize
,
stack
:
[],
inHtmlTag
:
[],
extName
:
false
,
extMode
:
false
,
extState
:
false
,
nTemplate
:
0
,
nLink
:
0
,
nExtLink
:
0
,
nExt
:
0
,
nDt
:
0
,
bold
:
false
,
italic
:
false
,
sof
:
true
,
data
:
{
firstSingleLetterWord
:
null
,
firstMultiLetterWord
:
null
,
firstSpace
:
null
,
readyTokens
:
[],
oldToken
:
null
,
mark
:
null
}
}
);
const
copyState
=
(
state
)
=>
{
const
newState
=
{};
for
(
const
key
in
state
)
{
const
val
=
state
[
key
];
if
(
Array
.
isArray
(
val
)
)
{
newState
[
key
]
=
[
...
val
];
}
else
if
(
key
===
'extState'
)
{
newState
.
extState
=
(
state
.
extName
&&
state
.
extMode
&&
state
.
extMode
.
copyState
||
copyState
)(
val
);
}
else
{
newState
[
key
]
=
key
!==
'data'
&&
val
&&
typeof
val
===
'object'
?
Object
.
assign
(
{},
val
)
:
val
;
}
}
return
newState
;
};
/* eslint-disable max-len */
/**
* MediaWiki language support for CodeMirror 6.
* Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov.
* Exposed through the {@link module:ext.CodeMirror.v6.mode.mediawiki ext.CodeMirror.v6.mode.mediawiki} module.
*
* @class
* @extends CodeMirrorMode
*/
class
CodeMirrorMediaWiki
extends
CodeMirrorMode
{
/* eslint-enable max-len */
/**
* @param {Object} config MediaWiki configuration as generated by DataScript.php,
* along with additional properties documented below.
* @param {string[]} config.languageVariants List of language variants to support.
* @internal
* @hideconstructor
*/
constructor
(
config
)
{
super
(
'mediawiki'
);
this
.
config
=
config
;
this
.
urlProtocols
=
new
RegExp
(
`^(?:
${
config
.
urlProtocols
}
)(?=[^\\s\u00a0{[\\]<>~).,'])`
,
'i'
);
this
.
languageVariants
=
config
.
languageVariants
||
mw
.
config
.
get
(
'cmLanguageVariants'
,
[]
);
this
.
tokenTable
=
mwModeConfig
.
tokenTable
;
this
.
registerGroundTokens
();
for
(
const
k
in
specialParserFunctions
)
{
mwModeConfig
.
addFunction
(
specialParserFunctions
[
k
]
);
}
// Dynamically register any tags that aren't already in CodeMirrorMediaWikiConfig
Object
.
keys
(
config
.
tags
).
forEach
(
(
tag
)
=>
mwModeConfig
.
addTag
(
tag
)
);
this
.
functionSynonyms
=
[
...
Object
.
keys
(
config
.
functionSynonyms
[
0
]
)
.
map
(
(
label
)
=>
(
{
type
:
'function'
,
label
}
)
),
...
Object
.
keys
(
config
.
functionSynonyms
[
1
]
)
.
map
(
(
label
)
=>
(
{
type
:
'constant'
,
label
}
)
)
];
this
.
doubleUnderscore
=
[
...
Object
.
keys
(
config
.
doubleUnderscore
[
0
]
),
...
Object
.
keys
(
config
.
doubleUnderscore
[
1
]
)
].
map
(
(
label
)
=>
(
{
type
:
'constant'
,
label
}
)
);
const
extTags
=
Object
.
keys
(
config
.
tags
);
this
.
extTags
=
extTags
.
map
(
(
label
)
=>
(
{
type
:
'type'
,
label
}
)
);
this
.
htmlTags
=
Object
.
keys
(
mwModeConfig
.
permittedHtmlTags
)
.
filter
(
(
tag
)
=>
!
extTags
.
includes
(
tag
)
)
.
map
(
(
label
)
=>
(
{
type
:
'type'
,
label
}
)
);
this
.
protocols
=
config
.
urlProtocols
.
split
(
'|'
)
.
map
(
(
label
)
=>
(
{
type
:
'namespace'
,
label
:
label
.
replace
(
/\\([:/])/g
,
'$1'
)
}
)
);
this
.
redirectRegex
=
new
RegExp
(
`^\\s*(?:
${
config
.
redirection
.
join
(
'|'
)
}
)(\\s*:)?\\s*(?=\\[\\[)`
,
'i'
);
this
.
nsRegex
=
new
RegExp
(
`^(
${
Object
.
keys
(
nsIds
).
filter
(
Boolean
).
join
(
'|'
).
replace
(
/_/g
,
' '
)
}
)\\s*:\\s*`
,
'i'
);
const
img
=
Object
.
keys
(
config
.
imageKeywords
).
filter
(
(
word
)
=>
!
/\$1./
.
test
(
word
)
);
this
.
imgRegex
=
new
RegExp
(
`^(?:
${
img
.
filter
(
(
word
)
=>
word
.
endsWith
(
'$1'
)
).
map
(
(
word
)
=>
word
.
slice
(
0
,
-
2
)
).
join
(
'|'
)
}
|(?:
${
img
.
filter
(
(
word
)
=>
!
word
.
endsWith
(
'$1'
)
).
join
(
'|'
)
}
|(?:\\d+x?|\\d*x\\d+)\\s*(?:px)?px)\\s*(?=\\||\\]\\]|$))`
);
}
/** @inheritDoc */
get
language
()
{
return
StreamLanguage
.
define
(
this
.
parser
);
}
/** @inheritDoc */
get
lintSource
()
{
return
lintSource
;
}
/** @inheritDoc */
get
support
()
{
return
[
syntaxHighlighting
(
HighlightStyle
.
define
(
mwModeConfig
.
getTagStyles
(
this
.
parser
)
)
)
];
}
/**
* Register the ground tokens. These aren't referenced directly in the StreamParser, nor do
* they have a parent Tag, so we don't need them as constants like we do for other tokens.
* See this.makeLocalStyle() for how these tokens are used.
*
* @private
*/
registerGroundTokens
()
{
[
'mw-ext-ground'
,
'mw-ext-link-ground'
,
'mw-ext2-ground'
,
'mw-ext2-link-ground'
,
'mw-ext3-ground'
,
'mw-ext3-link-ground'
,
'mw-link-ground'
,
'mw-template-ext-ground'
,
'mw-template-ext-link-ground'
,
'mw-template-ext2-ground'
,
'mw-template-ext2-link-ground'
,
'mw-template-ext3-ground'
,
'mw-template-ext3-link-ground'
,
'mw-template-ground'
,
'mw-template-link-ground'
,
'mw-template2-ext-ground'
,
'mw-template2-ext-link-ground'
,
'mw-template2-ext2-ground'
,
'mw-template2-ext2-link-ground'
,
'mw-template2-ext3-ground'
,
'mw-template2-ext3-link-ground'
,
'mw-template2-ground'
,
'mw-template2-link-ground'
,
'mw-template3-ext-ground'
,
'mw-template3-ext-link-ground'
,
'mw-template3-ext2-ground'
,
'mw-template3-ext2-link-ground'
,
'mw-template3-ext3-ground'
,
'mw-template3-ext3-link-ground'
,
'mw-template3-ground'
,
'mw-template3-link-ground'
].
forEach
(
(
ground
)
=>
mwModeConfig
.
addToken
(
ground
)
);
}
eatHtmlEntity
(
stream
,
style
)
{
let
ok
;
if
(
stream
.
eat
(
'#'
)
)
{
if
(
stream
.
eat
(
'x'
)
)
{
ok
=
stream
.
eatWhile
(
/[a-fA-F\d]/
)
&&
stream
.
eat
(
';'
);
}
else
{
ok
=
stream
.
eatWhile
(
/[\d]/
)
&&
stream
.
eat
(
';'
);
}
}
else
{
ok
=
stream
.
eatWhile
(
/[\w.\-:]/
)
&&
stream
.
eat
(
';'
);
}
if
(
ok
)
{
return
mwModeConfig
.
tags
.
htmlEntity
;
}
return
style
;
}
isNested
(
state
)
{
return
state
.
nExt
>
0
||
state
.
nTemplate
>
0
||
state
.
nLink
>
0
||
state
.
nExtLink
>
0
;
}
makeFullStyle
(
style
,
state
)
{
return
(
typeof
style
===
'string'
?
style
:
`
${
style
[
0
]
}
${
state
.
bold
||
state
.
nDt
>
0
?
mwModeConfig
.
tags
.
strong
:
''
}
${
state
.
italic
?
mwModeConfig
.
tags
.
em
:
''
}
`
).
replace
(
/\s{2,}/g
,
' '
).
trim
()
||
' '
;
}
makeStyle
(
style
,
state
,
endGround
)
{
return
[
this
.
makeLocalStyle
(
style
,
state
,
endGround
)
];
}
makeLocalStyle
(
style
,
state
,
endGround
)
{
let
ground
=
''
;
switch
(
state
.
nTemplate
)
{
case
0
:
break
;
case
1
:
ground
+=
'-template'
;
break
;
case
2
:
ground
+=
'-template2'
;
break
;
default
:
ground
+=
'-template3'
;
break
;
}
switch
(
state
.
nExt
)
{
case
0
:
break
;
case
1
:
ground
+=
'-ext'
;
break
;
case
2
:
ground
+=
'-ext2'
;
break
;
default
:
ground
+=
'-ext3'
;
break
;
}
if
(
state
.
nLink
>
0
||
state
.
nExtLink
>
0
)
{
ground
+=
'-link'
;
}
if
(
ground
!==
''
)
{
style
=
`mw
${
ground
}
-ground
${
style
}
`
;
}
if
(
endGround
)
{
state
[
endGround
]
--
;
}
return
style
.
trim
();
}
eatBlock
(
style
,
terminator
,
consumeLast
)
{
return
(
stream
,
state
)
=>
{
if
(
stream
.
skipTo
(
terminator
)
)
{
if
(
consumeLast
!==
false
)
{
stream
.
match
(
terminator
);
}
state
.
tokenize
=
state
.
stack
.
pop
();
}
else
{
stream
.
skipToEnd
();
}
return
this
.
makeLocalStyle
(
style
,
state
);
};
}
eatEnd
(
style
)
{
return
(
stream
,
state
)
=>
{
stream
.
skipToEnd
();
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
style
,
state
);
};
}
eatChar
(
char
,
style
)
{
return
(
stream
,
state
)
=>
{
state
.
tokenize
=
state
.
stack
.
pop
();
if
(
stream
.
eat
(
char
)
)
{
return
this
.
makeLocalStyle
(
style
,
state
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
error
,
state
);
};
}
eatSectionHeader
(
count
)
{
return
(
stream
,
state
)
=>
{
if
(
stream
.
match
(
/^[^&<[{~-]+/
)
)
{
if
(
stream
.
eol
()
)
{
stream
.
backUp
(
count
);
state
.
tokenize
=
this
.
eatEnd
(
mwModeConfig
.
tags
.
sectionHeader
);
}
else
if
(
stream
.
match
(
/^<!--(?!.*?-->.*?=)/
,
false
)
)
{
// T171074: handle trailing comments
stream
.
backUp
(
count
);
state
.
tokenize
=
this
.
eatBlock
(
mwModeConfig
.
tags
.
sectionHeader
,
'<!--'
,
false
);
}
return
mwModeConfig
.
tags
.
section
;
// style is null
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
section
)(
stream
,
state
);
};
}
inVariable
(
stream
,
state
)
{
if
(
stream
.
match
(
/^[^{}|]+/
)
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableName
,
state
);
}
if
(
stream
.
eat
(
'|'
)
)
{
state
.
tokenize
=
this
.
inVariableDefault
.
bind
(
this
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableDelimiter
,
state
);
}
if
(
stream
.
match
(
'}}}'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableBracket
,
state
);
}
if
(
stream
.
match
(
'{{{'
)
)
{
state
.
stack
.
push
(
state
.
tokenize
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableBracket
,
state
);
}
stream
.
next
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableName
,
state
);
}
inVariableDefault
(
stream
,
state
)
{
if
(
stream
.
match
(
/^[^{}[<&~-]+/
)
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariable
,
state
);
}
if
(
stream
.
match
(
'}}}'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableBracket
,
state
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
templateVariable
)(
stream
,
state
);
}
inParserFunctionName
(
ns
)
{
return
(
stream
,
state
)
=>
{
// FIXME: {{#name}} and {{uc}} are wrong, must have ':'
const
mt
=
stream
.
match
(
/^#?[^:}{~]+/
);
if
(
mt
)
{
const
name
=
this
.
config
.
functionSynonyms
[
0
][
mt
[
0
].
trim
().
toLowerCase
()
];
if
(
name
in
specialParserFunctions
)
{
state
.
tokenize
=
this
.
inParserFunctionName
(
specialParserFunctions
[
name
]
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionName
,
state
);
}
if
(
stream
.
eat
(
':'
)
)
{
state
.
tokenize
=
this
.
inParserFunctionArguments
(
ns
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionDelimiter
,
state
);
}
if
(
stream
.
match
(
'}}'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionBracket
,
state
,
'nExt'
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
parserFunction
)(
stream
,
state
);
};
}
inParserFunctionArguments
(
ns
)
{
const
style
=
ns
===
undefined
?
mwModeConfig
.
tags
.
parserFunction
:
`
${
mwModeConfig
.
tags
.
parserFunction
}
${
mwModeConfig
.
tags
.
pageName
}
mw-function-
${
ns
}
`
;
return
(
stream
,
state
)
=>
{
if
(
stream
.
match
(
/^[^|}{[<&~-]+/
)
)
{
return
this
.
makeLocalStyle
(
style
,
state
);
}
else
if
(
stream
.
eat
(
'|'
)
)
{
if
(
ns
!==
undefined
)
{
state
.
tokenize
=
this
.
inParserFunctionArguments
();
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionDelimiter
,
state
);
}
else
if
(
stream
.
match
(
'}}'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionBracket
,
state
,
'nExt'
);
}
return
this
.
eatWikiText
(
style
)(
stream
,
state
);
};
}
eatTemplatePageName
(
haveAte
)
{
return
(
stream
,
state
)
=>
{
if
(
stream
.
match
(
/^[\s\u00a0]*\|[\s\u00a0]*/
)
)
{
state
.
tokenize
=
this
.
eatTemplateArgument
(
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateDelimiter
,
state
);
}
if
(
stream
.
match
(
/^[\s\u00a0]*\}\}/
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateBracket
,
state
,
'nTemplate'
);
}
if
(
stream
.
match
(
/^[\s\u00a0]*<!--.*?-->/
)
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
comment
,
state
);
}
if
(
haveAte
&&
stream
.
sol
()
)
{
// @todo error message
state
.
nTemplate
--
;
state
.
tokenize
=
state
.
stack
.
pop
();
return
''
;
}
if
(
stream
.
match
(
/^[\s\u00a0]*[^\s\u00a0|}<{&~]+/
)
)
{
state
.
tokenize
=
this
.
eatTemplatePageName
(
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateName
,
state
);
}
else
if
(
stream
.
eatSpace
()
)
{
if
(
stream
.
eol
()
===
true
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateName
,
state
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateName
,
state
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
templateName
)(
stream
,
state
);
};
}
eatTemplateArgument
(
expectArgName
)
{
return
(
stream
,
state
)
=>
{
if
(
expectArgName
&&
stream
.
match
(
/^(?:[^=|}{[<&~-]|-(?!\{))+/
)
)
{
if
(
stream
.
eat
(
'='
)
)
{
state
.
tokenize
=
this
.
eatTemplateArgument
(
false
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateArgumentName
,
state
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
template
,
state
);
}
else
if
(
stream
.
eatWhile
(
/[^|}{[<&~-]/
)
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
template
,
state
);
}
else
if
(
stream
.
eat
(
'|'
)
)
{
state
.
tokenize
=
this
.
eatTemplateArgument
(
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateDelimiter
,
state
);
}
else
if
(
stream
.
match
(
'}}'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateBracket
,
state
,
'nTemplate'
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
template
)(
stream
,
state
);
};
}
eatExternalLinkProtocol
(
chars
)
{
return
(
stream
,
state
)
=>
{
while
(
chars
>
0
)
{
chars
--
;
stream
.
next
();
}
if
(
stream
.
eol
()
)
{
state
.
nExtLink
--
;
// @todo error message
state
.
tokenize
=
state
.
stack
.
pop
();
}
else
{
state
.
tokenize
=
this
.
inExternalLink
.
bind
(
this
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
extLinkProtocol
,
state
);
};
}
inExternalLink
(
stream
,
state
)
{
if
(
stream
.
sol
()
)
{
state
.
nExtLink
--
;
// @todo error message
state
.
tokenize
=
state
.
stack
.
pop
();
return
''
;
}
if
(
stream
.
match
(
/^[\s\u00a0]*\]/
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
extLinkBracket
,
state
,
'nExtLink'
);
}
if
(
stream
.
eatSpace
()
)
{
state
.
tokenize
=
this
.
inExternalLinkText
.
bind
(
this
);
return
this
.
makeStyle
(
''
,
state
);
}
if
(
stream
.
match
(
/^[^\s\u00a0\]{&~']+/
)
||
stream
.
eatSpace
()
)
{
if
(
stream
.
peek
()
===
'\''
)
{
if
(
stream
.
match
(
'\'\''
,
false
)
)
{
state
.
tokenize
=
this
.
inExternalLinkText
.
bind
(
this
);
}
else
{
stream
.
next
();
}
}
return
this
.
makeStyle
(
mwModeConfig
.
tags
.
extLink
,
state
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
extLink
)(
stream
,
state
);
}
inExternalLinkText
(
stream
,
state
)
{
if
(
stream
.
sol
()
)
{
state
.
nExtLink
--
;
// @todo error message
state
.
tokenize
=
state
.
stack
.
pop
();
return
''
;
}
if
(
stream
.
eat
(
']'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
extLinkBracket
,
state
,
'nExtLink'
);
}
if
(
stream
.
match
(
/^[^'\]{&~<-]+/
)
)
{
return
this
.
makeStyle
(
mwModeConfig
.
tags
.
extLinkText
,
state
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
extLinkText
)(
stream
,
state
);
}
inGallery
(
stream
,
state
)
{
if
(
stream
.
match
(
/^[\s\u00a0]*\|[\s\u00a0]*/
)
)
{
state
.
tokenize
=
this
.
eatLinkText
(
true
,
true
);
this
.
toEatImageParameters
(
stream
,
state
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkDelimiter
,
state
);
}
if
(
stream
.
match
(
/^[\s\u00a0]*[^\s\u00a0|&{]+/
)
||
stream
.
eatSpace
()
)
{
return
this
.
makeStyle
(
`
${
mwModeConfig
.
tags
.
linkPageName
}
${
mwModeConfig
.
tags
.
pageName
}
`
,
state
);
}
return
this
.
eatWikiText
(
`
${
mwModeConfig
.
tags
.
linkPageName
}
${
mwModeConfig
.
tags
.
pageName
}
`
)(
stream
,
state
);
}
inLink
(
file
)
{
return
(
stream
,
state
)
=>
{
if
(
stream
.
sol
()
)
{
state
.
nLink
--
;
// @todo error message
state
.
tokenize
=
state
.
stack
.
pop
();
return
''
;
}
if
(
stream
.
match
(
/^[\s\u00a0]*#[\s\u00a0]*/
)
)
{
state
.
tokenize
=
this
.
inLinkToSection
.
bind
(
this
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
link
,
state
);
}
if
(
stream
.
match
(
/^[\s\u00a0]*\|[\s\u00a0]*/
)
)
{
state
.
tokenize
=
this
.
eatLinkText
(
file
);
if
(
file
)
{
this
.
toEatImageParameters
(
stream
,
state
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkDelimiter
,
state
);
}
if
(
stream
.
match
(
/^[\s\u00a0]*\]\]/
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkBracket
,
state
,
'nLink'
);
}
if
(
stream
.
match
(
/^[^#|\]&~{]+/
)
)
{
return
this
.
makeStyle
(
`
${
mwModeConfig
.
tags
.
linkPageName
}
${
mwModeConfig
.
tags
.
pageName
}
`
,
state
);
}
return
this
.
eatWikiText
(
`
${
mwModeConfig
.
tags
.
linkPageName
}
${
mwModeConfig
.
tags
.
pageName
}
`
)(
stream
,
state
);
};
}
inLinkToSection
(
stream
,
state
)
{
if
(
stream
.
sol
()
)
{
// @todo error message
state
.
nLink
--
;
state
.
tokenize
=
state
.
stack
.
pop
();
return
''
;
}
// FIXME '{{' breaks links, example: [[z{{page]]
if
(
stream
.
match
(
/^[^|\]&~{}]+/
)
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkToSection
,
state
);
}
if
(
stream
.
eat
(
'|'
)
)
{
state
.
tokenize
=
this
.
eatLinkText
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkDelimiter
,
state
);
}
if
(
stream
.
match
(
']]'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkBracket
,
state
,
'nLink'
);
}
return
this
.
eatWikiText
(
mwModeConfig
.
tags
.
linkToSection
)(
stream
,
state
);
}
eatLinkText
(
file
,
gallery
)
{
let
linkIsBold
,
linkIsItalic
;
return
(
stream
,
state
)
=>
{
let
tmpstyle
;
if
(
!
gallery
&&
stream
.
match
(
']]'
)
||
!
file
&&
stream
.
match
(
'[['
,
false
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkBracket
,
state
,
'nLink'
);
}
if
(
file
&&
stream
.
match
(
/^\|\s*/
)
)
{
this
.
toEatImageParameters
(
stream
,
state
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkDelimiter
,
state
);
}
if
(
stream
.
match
(
'\'\'\''
)
)
{
linkIsBold
=
!
linkIsBold
;
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
linkText
}
${
mwModeConfig
.
tags
.
apostrophes
}
`
,
state
);
}
if
(
stream
.
match
(
'\'\''
)
)
{
linkIsItalic
=
!
linkIsItalic
;
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
linkText
}
${
mwModeConfig
.
tags
.
apostrophes
}
`
,
state
);
}
tmpstyle
=
mwModeConfig
.
tags
.
linkText
;
if
(
linkIsBold
)
{
tmpstyle
+=
' '
+
mwModeConfig
.
tags
.
strong
;
}
if
(
linkIsItalic
)
{
tmpstyle
+=
' '
+
mwModeConfig
.
tags
.
em
;
}
if
(
stream
.
match
(
file
?
/^[^'\]{&~<[|-]+/
:
/^(?:[^'[\]{&~<-]|\[(?!\[))+/
)
)
{
return
this
.
makeStyle
(
tmpstyle
,
state
);
}
return
this
.
eatWikiText
(
tmpstyle
)(
stream
,
state
);
};
}
toEatImageParameters
(
stream
,
state
)
{
const
mt
=
stream
.
match
(
this
.
imgRegex
,
false
);
if
(
mt
)
{
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
(
stream2
,
state2
)
=>
{
stream2
.
pos
+=
mt
[
0
].
length
;
state2
.
tokenize
=
state2
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
imageParameter
,
state2
);
};
}
}
eatTagName
(
chars
,
isCloseTag
,
isHtmlTag
)
{
return
(
stream
,
state
)
=>
{
let
name
=
''
;
while
(
chars
>
0
)
{
chars
--
;
name
=
name
+
stream
.
next
();
}
stream
.
eatSpace
();
name
=
name
.
toLowerCase
();
if
(
isHtmlTag
)
{
if
(
isCloseTag
&&
!
mwModeConfig
.
implicitlyClosedHtmlTags
[
name
]
)
{
state
.
tokenize
=
this
.
eatChar
(
'>'
,
mwModeConfig
.
tags
.
htmlTagBracket
);
}
else
{
state
.
tokenize
=
this
.
eatHtmlTagAttribute
(
name
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
htmlTagName
,
state
);
}
// it is the extension tag
if
(
isCloseTag
)
{
state
.
tokenize
=
this
.
eatChar
(
'>'
,
`
${
mwModeConfig
.
tags
.
extTagBracket
}
mw-ext-
${
name
}
`
);
}
else
{
state
.
tokenize
=
this
.
eatExtTagAttribute
(
name
);
}
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
extTagName
}
mw-ext-
${
name
}
`
,
state
);
};
}
eatHtmlTagAttribute
(
name
,
quote
)
{
const
style
=
mwModeConfig
.
tags
[
quote
===
undefined
?
'htmlTagAttribute'
:
'htmlTagAttributeValue'
];
return
(
stream
,
state
)
=>
{
if
(
stream
.
eat
(
'>'
)
)
{
if
(
!
(
name
in
mwModeConfig
.
implicitlyClosedHtmlTags
)
)
{
state
.
inHtmlTag
.
push
(
name
);
}
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
htmlTagBracket
,
state
);
}
if
(
stream
.
match
(
'/>'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
htmlTagBracket
,
state
);
}
const
peek
=
stream
.
peek
();
if
(
peek
===
'<'
)
{
const
{
pos
}
=
stream
,
{
length
}
=
state
.
stack
,
// eat comment or extension tag
result
=
this
.
eatWikiText
(
''
)(
stream
,
state
);
if
(
typeof
result
!==
'string'
||
!
result
.
includes
(
mwModeConfig
.
tags
.
comment
)
&&
!
result
.
includes
(
mwModeConfig
.
tags
.
extTagBracket
)
)
{
state
.
stack
.
length
=
length
;
state
.
tokenize
=
state
.
stack
.
pop
();
stream
.
pos
=
pos
;
return
''
;
}
return
result
;
}
if
(
peek
===
'&'
||
peek
===
'{'
)
{
return
this
.
eatWikiText
(
style
)(
stream
,
state
);
}
if
(
quote
)
{
if
(
stream
.
eat
(
quote
[
0
]
)
)
{
state
.
tokenize
=
this
.
eatHtmlTagAttribute
(
name
,
quote
[
1
]
);
}
else
{
stream
.
match
(
new
RegExp
(
`^(?:[^<>&{/
${
quote
[
0
]
}
]|/(?!>))+`
)
);
}
return
this
.
makeLocalStyle
(
style
,
state
);
}
if
(
quote
===
''
)
{
if
(
stream
.
sol
()
||
/\s/
.
test
(
peek
)
)
{
state
.
tokenize
=
this
.
eatHtmlTagAttribute
(
name
);
return
''
;
}
stream
.
match
(
/^(?:[^\s<>&{/]|\/(?!>))+/
);
return
this
.
makeLocalStyle
(
style
,
state
);
}
if
(
stream
.
match
(
/^=\s*/
)
)
{
const
next
=
stream
.
peek
();
state
.
tokenize
=
this
.
eatHtmlTagAttribute
(
name
,
next
===
'"'
||
next
===
"'"
?
next
.
repeat
(
2
)
:
''
);
return
this
.
makeLocalStyle
(
style
,
state
);
}
stream
.
match
(
/^(?:[^<>&={/]|\/(?!>))+/
);
return
this
.
makeLocalStyle
(
style
,
state
);
};
}
eatNowiki
()
{
return
(
stream
)
=>
{
if
(
stream
.
match
(
/^[^&]+/
)
)
{
return
''
;
}
// eat &
stream
.
next
();
return
this
.
eatHtmlEntity
(
stream
,
''
);
};
}
eatExtTagAttribute
(
name
,
quote
,
isPage
)
{
const
style
=
`
${
mwModeConfig
.
tags
.
extTagAttribute
}
mw-ext-
${
name
}
`
,
valueStyle
=
`
${
mwModeConfig
.
tags
.
extTagAttributeValue
}
${
isPage
?
`
${
mwModeConfig
.
tags
.
pageName
}
mw-ext-
${
name
}
`
:
''
}
`
;
return
(
stream
,
state
)
=>
{
if
(
stream
.
eat
(
'>'
)
)
{
state
.
extName
=
name
;
// FIXME: remove nowiki and pre from TagModes in extension.json after CM6 upgrade
// leverage the tagModes system for <nowiki> and <pre>
if
(
name
===
'nowiki'
||
name
===
'pre'
)
{
// There's no actual processing within these tags (apart from HTML entities),
// so startState and copyState can be no-ops.
state
.
extMode
=
{
startState
:
()
=>
{},
copyState
:
()
=>
{},
token
:
this
.
eatNowiki
()
};
}
else
if
(
name
===
'gallery'
)
{
state
.
extMode
=
{
startState
:
()
=>
startState
(
this
.
inGallery
.
bind
(
this
)
),
copyState
,
token
:
(
stream2
,
state2
)
=>
{
if
(
stream2
.
sol
()
)
{
Object
.
assign
(
state2
,
startState
(
this
.
inGallery
.
bind
(
this
)
)
);
}
return
state2
.
tokenize
(
stream2
,
state2
);
}
};
state
.
extState
=
state
.
extMode
.
startState
();
}
else
if
(
name
in
this
.
config
.
tagModes
)
{
const
mode
=
this
.
config
.
tagModes
[
name
];
if
(
mode
===
'mediawiki'
||
mode
===
'text/mediawiki'
)
{
state
.
extMode
=
this
.
parser
;
state
.
extState
=
state
.
extMode
.
startState
();
}
}
state
.
tokenize
=
this
.
eatExtTagArea
(
name
);
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
extTagBracket
}
mw-ext-
${
name
}
`
,
state
);
}
if
(
stream
.
match
(
'/>'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
extTagBracket
}
mw-ext-
${
name
}
`
,
state
);
}
if
(
quote
)
{
if
(
stream
.
eat
(
quote
[
0
]
)
)
{
state
.
tokenize
=
this
.
eatExtTagAttribute
(
name
,
quote
[
1
],
quote
[
1
]
&&
isPage
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
extTagAttributeValue
,
state
);
}
stream
.
match
(
new
RegExp
(
`^(?:[^>/
${
quote
[
0
]
}
]|/(?!>))+`
)
);
return
this
.
makeLocalStyle
(
valueStyle
,
state
);
}
if
(
quote
===
''
)
{
if
(
stream
.
sol
()
||
/\s/
.
test
(
stream
.
peek
()
)
)
{
state
.
tokenize
=
this
.
eatExtTagAttribute
(
name
);
return
''
;
}
stream
.
match
(
/^(?:[^>/\s]|\/(?!>))+/
);
return
this
.
makeLocalStyle
(
valueStyle
,
state
);
}
if
(
stream
.
match
(
/^=\s*/
)
)
{
const
next
=
stream
.
peek
();
state
.
tokenize
=
this
.
eatExtTagAttribute
(
name
,
next
===
'"'
||
next
===
"'"
?
next
.
repeat
(
2
)
:
''
,
isPage
);
return
this
.
makeLocalStyle
(
style
,
state
);
}
const
mt
=
stream
.
match
(
/^(?:[^>/=]|\/(?!>))+/
);
if
(
stream
.
peek
()
===
'='
&&
name
===
'templatestyles'
&&
/(?:^|\s)src\s*$/i
.
test
(
mt
[
0
]
)
)
{
state
.
tokenize
=
this
.
eatExtTagAttribute
(
name
,
undefined
,
true
);
}
return
this
.
makeLocalStyle
(
style
,
state
);
};
}
eatExtTagArea
(
name
)
{
return
(
stream
,
state
)
=>
{
const
from
=
stream
.
pos
,
pattern
=
new
RegExp
(
`</
${
name
}
\\s*>`
,
'i'
),
m
=
pattern
.
exec
(
from
?
stream
.
string
.
slice
(
from
)
:
stream
.
string
);
let
origString
=
false
,
to
;
if
(
m
)
{
if
(
m
.
index
===
0
)
{
state
.
tokenize
=
this
.
eatExtCloseTag
(
name
);
state
.
extName
=
false
;
if
(
state
.
extMode
!==
false
)
{
state
.
extMode
=
false
;
state
.
extState
=
false
;
}
return
state
.
tokenize
(
stream
,
state
);
}
to
=
m
.
index
+
from
;
origString
=
stream
.
string
;
stream
.
string
=
origString
.
slice
(
0
,
to
);
}
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatExtTokens
(
origString
);
return
state
.
tokenize
(
stream
,
state
);
};
}
eatExtCloseTag
(
name
)
{
return
(
stream
,
state
)
=>
{
stream
.
next
();
// eat <
stream
.
next
();
// eat /
state
.
tokenize
=
this
.
eatTagName
(
name
.
length
,
true
,
false
);
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
extTagBracket
}
mw-ext-
${
name
}
`
,
state
);
};
}
eatExtTokens
(
origString
)
{
const
tokenize
=
(
stream
,
state
)
=>
{
let
ret
;
if
(
state
.
extMode
===
false
)
{
ret
=
mwModeConfig
.
tags
.
extTag
;
stream
.
skipToEnd
();
}
else
{
ret
=
`mw-tag-
${
state
.
extName
}
`
+
state
.
extMode
.
token
(
stream
,
state
.
extState
,
origString
===
false
);
}
if
(
stream
.
eol
()
)
{
if
(
origString
!==
false
)
{
stream
.
string
=
origString
;
}
state
.
tokenize
=
state
.
stack
.
pop
();
}
return
this
.
makeLocalStyle
(
ret
,
state
);
};
Object
.
defineProperty
(
tokenize
,
'name'
,
{
value
:
'eatExtTokens'
}
);
return
tokenize
;
}
eatStartTable
(
stream
,
state
)
{
stream
.
match
(
'{|'
);
stream
.
eatSpace
();
state
.
tokenize
=
this
.
inTableDefinition
();
return
mwModeConfig
.
tags
.
tableBracket
;
}
inTableDefinition
(
quote
)
{
const
style
=
mwModeConfig
.
tags
[
quote
===
undefined
?
'tableDefinition'
:
'tableDefinitionValue'
];
return
(
stream
,
state
)
=>
{
if
(
stream
.
sol
()
)
{
state
.
tokenize
=
this
.
inTable
.
bind
(
this
);
return
this
.
inTable
(
stream
,
state
);
}
else
if
(
stream
.
match
(
/^[&{<]/
,
false
)
)
{
return
this
.
eatWikiText
(
style
)(
stream
,
state
);
}
else
if
(
quote
)
{
if
(
stream
.
eat
(
quote
[
0
]
)
)
{
state
.
tokenize
=
this
.
inTableDefinition
(
quote
[
1
]
);
}
else
{
stream
.
match
(
new
RegExp
(
`^[^&{<
${
quote
[
0
]
}
]+`
)
);
}
return
this
.
makeLocalStyle
(
style
,
state
);
}
else
if
(
quote
===
''
)
{
if
(
/\s/
.
test
(
stream
.
peek
()
)
)
{
state
.
tokenize
=
this
.
inTableDefinition
();
return
''
;
}
stream
.
match
(
/^[^\s&{<]+/
);
return
this
.
makeLocalStyle
(
style
,
state
);
}
else
if
(
stream
.
match
(
/^=\s*/
)
)
{
const
next
=
stream
.
peek
();
state
.
tokenize
=
this
.
inTableDefinition
(
next
===
'"'
||
next
===
"'"
?
next
.
repeat
(
2
)
:
''
);
return
this
.
makeLocalStyle
(
style
,
state
);
}
stream
.
match
(
/^[^&{<=]+/
);
return
this
.
makeLocalStyle
(
style
,
state
);
};
}
inTable
(
stream
,
state
)
{
if
(
stream
.
sol
()
)
{
stream
.
eatSpace
();
if
(
stream
.
eat
(
'|'
)
)
{
if
(
stream
.
eat
(
'-'
)
)
{
stream
.
eatSpace
();
state
.
tokenize
=
this
.
inTableDefinition
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableDelimiter
,
state
);
}
if
(
stream
.
eat
(
'+'
)
)
{
stream
.
eatSpace
();
state
.
tokenize
=
this
.
eatTableRow
(
true
,
false
,
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableDelimiter
,
state
);
}
if
(
stream
.
eat
(
'}'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableBracket
,
state
);
}
stream
.
eatSpace
();
state
.
tokenize
=
this
.
eatTableRow
(
true
,
false
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableDelimiter
,
state
);
}
if
(
stream
.
eat
(
'!'
)
)
{
stream
.
eatSpace
();
state
.
tokenize
=
this
.
eatTableRow
(
true
,
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableDelimiter
,
state
);
}
}
return
this
.
eatWikiText
(
''
)(
stream
,
state
);
}
// isStart actually means whether there may be attributes */
eatTableRow
(
isStart
,
isHead
,
isCaption
)
{
let
tag
=
''
;
if
(
isCaption
)
{
tag
=
mwModeConfig
.
tags
.
tableCaption
;
}
else
if
(
isHead
)
{
tag
=
mwModeConfig
.
tags
.
strong
;
}
return
(
stream
,
state
)
=>
{
if
(
stream
.
sol
()
)
{
if
(
stream
.
match
(
/^[\s\u00a0]*[|!]/
,
false
)
)
{
state
.
tokenize
=
this
.
inTable
.
bind
(
this
);
return
this
.
inTable
(
stream
,
state
);
}
}
else
{
if
(
stream
.
match
(
/^[^'|{[<&~!-]+/
)
)
{
return
this
.
makeStyle
(
tag
,
state
);
}
if
(
stream
.
match
(
'||'
)
||
(
isHead
&&
stream
.
match
(
'!!'
)
)
)
{
state
.
bold
=
false
;
state
.
italic
=
false
;
state
.
tokenize
=
this
.
eatTableRow
(
true
,
isHead
,
isCaption
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableDelimiter
,
state
);
}
if
(
isStart
&&
stream
.
eat
(
'|'
)
)
{
state
.
bold
=
false
;
state
.
italic
=
false
;
state
.
tokenize
=
this
.
eatTableRow
(
false
,
isHead
,
isCaption
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
tableDelimiter
,
state
);
}
}
return
this
.
eatWikiText
(
tag
)(
stream
,
state
);
};
}
eatFreeExternalLinkProtocol
(
stream
,
state
)
{
stream
.
match
(
this
.
urlProtocols
);
state
.
tokenize
=
this
.
eatFreeExternalLink
.
bind
(
this
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLinkProtocol
,
state
);
}
eatFreeExternalLink
(
stream
,
state
)
{
if
(
stream
.
sol
()
)
{
// @todo error message
}
else
if
(
stream
.
match
(
/^[^\s\u00a0{[\]<>~).,']*/
)
)
{
if
(
stream
.
peek
()
===
'~'
)
{
if
(
!
stream
.
match
(
/^~~~+/
,
false
)
)
{
stream
.
match
(
/^~*/
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLink
,
state
);
}
}
else
if
(
stream
.
peek
()
===
'{'
)
{
if
(
!
stream
.
match
(
'{{'
,
false
)
)
{
stream
.
next
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLink
,
state
);
}
}
else
if
(
stream
.
peek
()
===
'\''
)
{
if
(
!
stream
.
match
(
'\'\''
,
false
)
)
{
stream
.
next
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLink
,
state
);
}
}
else
if
(
stream
.
match
(
/^[).,]+(?=[^\s\u00a0{[\]<>~).,])/
)
)
{
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLink
,
state
);
}
}
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLink
,
state
);
}
eatList
(
stream
,
state
)
{
// Just consume all nested list and indention syntax when there is more
const
mt
=
stream
.
match
(
/^[*#;:]*/
);
if
(
mt
&&
!
this
.
isNested
(
state
)
&&
mt
[
0
].
includes
(
';'
)
)
{
state
.
nDt
+=
mt
[
0
].
split
(
';'
).
length
-
1
;
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
list
,
state
);
}
inConversion
(
style
,
needFlag
,
needLang
)
{
const
variants
=
this
.
languageVariants
.
join
(
'|'
),
semicolon
=
new
RegExp
(
`^;\\s*(?=(?:[^;]*?=>\\s*)?(?:
${
variants
}
)\\s*:|(?:$|\\}-))`
,
'i'
),
lang
=
new
RegExp
(
`^(?:=>\\s*)?(?:
${
variants
}
)\\s*:`
,
'i'
);
return
(
stream
,
state
)
=>
{
const
space
=
stream
.
eatSpace
();
if
(
stream
.
match
(
'}-'
)
)
{
state
.
tokenize
=
state
.
stack
.
pop
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
conversionBracket
,
state
);
}
else
if
(
needFlag
&&
stream
.
match
(
/^(?:[^}|={[<_])*(?=\|)/
)
)
{
state
.
stack
.
push
(
this
.
inConversion
(
style
,
false
,
true
)
);
state
.
tokenize
=
this
.
eatChar
(
'|'
,
mwModeConfig
.
tags
.
conversionDelimiter
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
conversionFlag
,
state
);
}
else
if
(
stream
.
match
(
semicolon
)
)
{
if
(
needLang
||
!
needFlag
)
{
state
.
tokenize
=
this
.
inConversion
(
style
,
false
,
true
);
}
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
conversionDelimiter
,
state
);
}
else
if
(
needLang
&&
stream
.
match
(
lang
)
)
{
state
.
tokenize
=
this
.
inConversion
(
style
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
conversionLang
,
state
);
}
else
if
(
stream
.
match
(
/^(?:[^}|;&='{[<~_-])+/
)
||
space
)
{
return
this
.
makeStyle
(
style
,
state
);
}
return
this
.
eatWikiText
(
style
)(
stream
,
state
);
};
}
/**
* @param {string} style
* @return {string|Function}
* @private
*/
eatWikiText
(
style
)
{
return
(
stream
,
state
)
=>
{
if
(
stream
.
eol
()
)
{
return
''
;
}
let
ch
,
tmp
,
mt
,
name
,
isCloseTag
,
tagname
;
const
sol
=
stream
.
sol
();
function
chain
(
parser
)
{
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
parser
;
return
parser
(
stream
,
state
);
}
if
(
sol
)
{
if
(
state
.
sof
)
{
if
(
stream
.
match
(
/^\s+$/
)
)
{
return
''
;
}
state
.
sof
=
false
;
const
mtRedirect
=
stream
.
match
(
this
.
redirectRegex
);
if
(
mtRedirect
)
{
return
mwModeConfig
.
tags
.
redirect
;
}
}
// highlight free external links, see T108448
if
(
!
stream
.
match
(
'//'
,
false
)
&&
stream
.
match
(
this
.
urlProtocols
)
)
{
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatFreeExternalLink
.
bind
(
this
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
freeExtLinkProtocol
,
state
);
}
ch
=
stream
.
next
();
switch
(
ch
)
{
case
'-'
:
if
(
stream
.
match
(
/^---+/
)
)
{
return
mwModeConfig
.
tags
.
hr
;
}
break
;
case
'='
:
tmp
=
stream
.
match
(
/^(={0,5})(.+?(=\1\s*)(<!--(?!.*-->.*\S).*?)?)$/
);
// Title
if
(
tmp
)
{
stream
.
backUp
(
tmp
[
2
].
length
);
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatSectionHeader
(
tmp
[
3
].
length
);
return
mwModeConfig
.
tags
.
sectionHeader
+
' '
+
/**
* Tokens used here include:
* - cm-mw-section-1
* - cm-mw-section-2
* - cm-mw-section-3
* - cm-mw-section-4
* - cm-mw-section-5
* - cm-mw-section-6
*/
mwModeConfig
.
tags
[
`sectionHeader
${
tmp
[
1
].
length
+
1
}
`
];
}
break
;
case
';'
:
stream
.
backUp
(
1
);
// fall through
case
'*'
:
case
'#'
:
return
this
.
eatList
(
stream
,
state
);
case
':'
:
// Highlight indented tables :{|, bug T108454
if
(
stream
.
match
(
/^:*[\s\u00a0]*(?={\|)/
)
)
{
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatStartTable
.
bind
(
this
);
return
mwModeConfig
.
tags
.
indenting
;
}
return
this
.
eatList
(
stream
,
state
);
case
' '
:
// Leading spaces is valid syntax for tables, bug T108454
if
(
stream
.
match
(
/^[\s\u00a0]*(?::+[\s\u00a0]*)?{\|/
,
false
)
)
{
stream
.
eatSpace
();
if
(
stream
.
match
(
/^:+/
)
)
{
// ::{|
stream
.
eatSpace
();
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatStartTable
.
bind
(
this
);
return
mwModeConfig
.
tags
.
indenting
;
}
stream
.
eat
(
'{'
);
}
else
{
return
mwModeConfig
.
tags
.
skipFormatting
;
}
// break is not necessary here
// falls through
case
'{'
:
if
(
stream
.
eat
(
'|'
)
)
{
stream
.
eatSpace
();
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
inTableDefinition
();
return
mwModeConfig
.
tags
.
tableBracket
;
}
}
}
else
{
ch
=
stream
.
next
();
}
switch
(
ch
)
{
case
'&'
:
return
this
.
makeStyle
(
this
.
eatHtmlEntity
(
stream
,
style
),
state
);
case
'\''
:
// skip the irrelevant apostrophes ( >5 or =4 )
if
(
stream
.
match
(
/^'*(?=''''')/
)
||
stream
.
match
(
/^'''(?!')/
,
false
)
)
{
break
;
}
if
(
stream
.
match
(
'\'\''
)
)
{
// bold
if
(
!
(
state
.
data
.
firstSingleLetterWord
||
stream
.
match
(
'\'\''
,
false
)
)
)
{
this
.
prepareItalicForCorrection
(
stream
,
state
);
}
state
.
bold
=
!
state
.
bold
;
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
apostrophes
,
state
);
}
else
if
(
stream
.
eat
(
'\''
)
)
{
// italic
state
.
italic
=
!
state
.
italic
;
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
apostrophes
,
state
);
}
break
;
case
'['
:
if
(
stream
.
eat
(
'['
)
)
{
// Link Example: [[ Foo | Bar ]]
stream
.
eatSpace
();
if
(
/[^\]|[]/
.
test
(
stream
.
peek
()
)
)
{
state
.
nLink
++
;
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
inLink
(
stream
.
match
(
fileNsRegex
,
false
)
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
linkBracket
,
state
);
}
}
else
{
mt
=
stream
.
match
(
this
.
urlProtocols
);
if
(
mt
)
{
state
.
nExtLink
++
;
stream
.
backUp
(
mt
[
0
].
length
);
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatExternalLinkProtocol
(
mt
[
0
].
length
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
extLinkBracket
,
state
);
}
}
break
;
case
'{'
:
// Can't be a variable when it starts with more than 3 brackets (T108450) or
// a single { followed by a template. E.g. {{{!}} starts a table (T292967).
if
(
stream
.
match
(
/^{{(?!{|[^{}]*}}(?!}))/
)
)
{
stream
.
eatSpace
();
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
inVariable
.
bind
(
this
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateVariableBracket
,
state
);
}
else
if
(
stream
.
match
(
/^{(?!{(?!{))[\s\u00a0]*/
)
)
{
// Parser function
if
(
stream
.
peek
()
===
'#'
)
{
state
.
nExt
++
;
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
inParserFunctionName
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionBracket
,
state
);
}
// Check for parser function without '#'
name
=
stream
.
match
(
/^([^}[\]<{|:]+)(.)?/
,
false
);
if
(
name
)
{
const
[
,
f
,
delimiter
]
=
name
,
ff
=
delimiter
===
':'
?
f
:
f
.
trim
(),
ffLower
=
ff
.
toLowerCase
(),
{
config
:
{
functionSynonyms
,
variableIDs
}
}
=
this
,
insensitiveName
=
Object
.
prototype
.
hasOwnProperty
.
call
(
functionSynonyms
[
0
],
ffLower
)
&&
functionSynonyms
[
0
][
ffLower
],
sensitiveName
=
Object
.
prototype
.
hasOwnProperty
.
call
(
functionSynonyms
[
1
],
ff
)
&&
functionSynonyms
[
1
][
ff
],
canonicalName
=
insensitiveName
||
sensitiveName
;
if
(
(
!
delimiter
||
delimiter
===
':'
||
delimiter
===
'}'
)
&&
canonicalName
&&
(
delimiter
===
':'
||
variableIDs
.
includes
(
canonicalName
)
)
)
{
state
.
nExt
++
;
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
inParserFunctionName
();
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
parserFunctionBracket
,
state
);
}
}
// Template
state
.
nTemplate
++
;
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatTemplatePageName
(
false
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
templateBracket
,
state
);
}
break
;
case
'<'
:
if
(
stream
.
match
(
'!--'
)
)
{
// comment
return
chain
(
this
.
eatBlock
(
mwModeConfig
.
tags
.
comment
,
'-->'
)
);
}
isCloseTag
=
!!
stream
.
eat
(
'/'
);
tagname
=
stream
.
match
(
/^[a-z][^>/\s\u00a0]*/i
);
if
(
tagname
)
{
tagname
=
tagname
[
0
].
toLowerCase
();
if
(
tagname
in
this
.
config
.
tags
)
{
// Parser function
if
(
isCloseTag
===
true
)
{
return
mwModeConfig
.
tags
.
error
;
}
stream
.
backUp
(
tagname
.
length
);
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatTagName
(
tagname
.
length
,
isCloseTag
,
false
);
return
this
.
makeLocalStyle
(
`
${
mwModeConfig
.
tags
.
extTagBracket
}
mw-ext-
${
tagname
}
`
,
state
);
}
if
(
tagname
in
mwModeConfig
.
permittedHtmlTags
)
{
// Html tag
if
(
isCloseTag
===
true
&&
tagname
!==
state
.
inHtmlTag
.
pop
()
)
{
// Increment position so that the closing '>' gets highlighted red.
stream
.
pos
++
;
return
mwModeConfig
.
tags
.
error
;
}
if
(
isCloseTag
===
true
&&
tagname
in
mwModeConfig
.
implicitlyClosedHtmlTags
)
{
return
mwModeConfig
.
tags
.
error
;
}
stream
.
backUp
(
tagname
.
length
);
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatTagName
(
tagname
.
length
,
// Opening void tags should also be treated as the closing tag.
isCloseTag
||
(
tagname
in
mwModeConfig
.
implicitlyClosedHtmlTags
),
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
htmlTagBracket
,
state
);
}
stream
.
backUp
(
tagname
.
length
);
}
break
;
case
'~'
:
if
(
stream
.
match
(
/^~{2,4}/
)
)
{
return
mwModeConfig
.
tags
.
signature
;
}
break
;
// Maybe double underscored Magic Word such as __TOC__
case
'_'
:
tmp
=
1
;
// Optimize processing of many underscore symbols
while
(
stream
.
eat
(
'_'
)
)
{
tmp
++
;
}
// Many underscore symbols
if
(
tmp
>
2
)
{
if
(
!
stream
.
eol
()
)
{
// Leave last two underscore symbols for processing in next iteration
stream
.
backUp
(
2
);
}
// Optimization: skip regex function for EOL and backup-ed symbols
return
this
.
makeStyle
(
style
,
state
);
// Check on double underscore Magic Word
}
else
if
(
tmp
===
2
)
{
// The same as the end of function except '_' inside and '__' at the end.
name
=
stream
.
match
(
/^([^\s\u00a0>}[\]<{'|&:~]+?)__/
);
if
(
name
&&
name
[
0
]
)
{
if
(
'__'
+
name
[
0
].
toLowerCase
()
in
this
.
config
.
doubleUnderscore
[
0
]
||
'__'
+
name
[
0
]
in
this
.
config
.
doubleUnderscore
[
1
]
)
{
return
mwModeConfig
.
tags
.
doubleUnderscore
;
}
if
(
!
stream
.
eol
()
)
{
// Two underscore symbols at the end can be the
// beginning of another double underscored Magic Word
stream
.
backUp
(
2
);
}
// Optimization: skip regex for EOL and backup-ed symbols
return
this
.
makeStyle
(
style
,
state
);
}
}
break
;
case
':'
:
if
(
state
.
nDt
>
0
&&
!
this
.
isNested
(
state
)
)
{
state
.
nDt
--
;
return
mwModeConfig
.
tags
.
indenting
;
}
break
;
case
'-'
:
if
(
this
.
languageVariants
.
length
&&
stream
.
match
(
/^\{(?!\{)\s*/
)
)
{
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
inConversion
(
style
,
true
,
true
);
return
this
.
makeLocalStyle
(
mwModeConfig
.
tags
.
conversionBracket
,
state
);
}
break
;
default
:
if
(
/[\s\u00a0]/
.
test
(
ch
)
)
{
stream
.
eatSpace
();
// highlight free external links, bug T108448
if
(
stream
.
match
(
this
.
urlProtocols
,
false
)
&&
!
stream
.
match
(
'//'
)
)
{
state
.
stack
.
push
(
state
.
tokenize
);
state
.
tokenize
=
this
.
eatFreeExternalLinkProtocol
.
bind
(
this
);
return
this
.
makeStyle
(
style
,
state
);
}
}
break
;
}
stream
.
match
(
/^[^\s\u00a0_>}[\]<{'|&:~=-]+/
);
return
this
.
makeStyle
(
style
,
state
);
};
}
/**
* Remembers position and status for rollbacking.
* It is needed for changing from bold to italic with apostrophes before it, if required.
*
* @see https://phabricator.wikimedia.org/T108455
*
* @param {StringStream} stream
* @param {Object} state
* @private
*/
prepareItalicForCorrection
(
stream
,
state
)
{
// See Parser::doQuotes() in MediaWiki Core, it works similarly.
// firstSingleLetterWord has maximum priority
// firstMultiLetterWord has medium priority
// firstSpace has low priority
const
end
=
stream
.
pos
,
str
=
stream
.
string
.
slice
(
0
,
end
-
3
),
x1
=
str
.
slice
(
-
1
),
x2
=
str
.
slice
(
-
2
,
-
1
);
// firstSingleLetterWord always is undefined here
if
(
x1
===
' '
)
{
if
(
state
.
data
.
firstMultiLetterWord
||
state
.
data
.
firstSpace
)
{
return
;
}
state
.
data
.
firstSpace
=
end
;
}
else
if
(
x2
===
' '
)
{
state
.
data
.
firstSingleLetterWord
=
end
;
}
else
if
(
state
.
data
.
firstMultiLetterWord
)
{
return
;
}
else
{
state
.
data
.
firstMultiLetterWord
=
end
;
}
state
.
data
.
mark
=
end
;
}
/**
* @see https://codemirror.net/docs/ref/#language.StreamParser
* @type {StreamParser}
* @private
*/
get
parser
()
{
return
{
name
:
'mediawiki'
,
/**
* Initial State for the parser.
*
* @return {Object}
* @private
*/
startState
:
()
=>
startState
(
this
.
eatWikiText
(
''
)
),
/**
* Copies the given state.
*
* @param {Object} state
* @return {Object}
* @private
*/
copyState
,
/**
* Reads one token, advancing the stream past it,
* and returning a string indicating the token's style tag.
*
* @param {StringStream} stream
* @param {Object} state
* @return {string|null}
* @private
*/
token
:
(
stream
,
state
)
=>
{
const
{
data
}
=
state
,
{
readyTokens
}
=
data
;
let
{
oldToken
}
=
data
;
while
(
oldToken
&&
(
// If the start of PartialParse is after the current position
stream
.
pos
>
oldToken
.
pos
||
stream
.
pos
===
oldToken
.
pos
&&
state
.
tokenize
!==
oldToken
.
state
.
tokenize
)
)
{
oldToken
=
readyTokens
.
shift
();
}
if
(
// check the start
oldToken
&&
stream
.
pos
===
oldToken
.
pos
&&
stream
.
string
===
oldToken
.
string
)
{
const
{
pos
,
string
,
state
:
other
,
style
}
=
readyTokens
[
0
];
delete
other
.
bold
;
delete
other
.
italic
;
Object
.
assign
(
state
,
other
);
if
(
!
(
state
.
extName
&&
state
.
extMode
)
&&
state
.
nLink
===
0
&&
typeof
style
===
'string'
&&
style
.
includes
(
mwModeConfig
.
tags
.
apostrophes
)
)
{
if
(
data
.
mark
===
pos
)
{
// rollback
data
.
mark
=
null
;
// add one apostrophe, next token will be italic (two apostrophes)
stream
.
string
=
string
.
slice
(
0
,
pos
-
2
);
const
s
=
state
.
tokenize
(
stream
,
state
);
stream
.
string
=
string
;
oldToken
.
pos
++
;
data
.
oldToken
=
oldToken
;
return
this
.
makeFullStyle
(
s
,
state
);
}
const
length
=
pos
-
stream
.
pos
;
if
(
length
!==
3
)
{
state
.
italic
=
!
state
.
italic
;
}
if
(
length
!==
2
)
{
state
.
bold
=
!
state
.
bold
;
}
}
else
if
(
typeof
style
===
'string'
&&
style
.
includes
(
mwModeConfig
.
tags
.
tableDelimiter
)
)
{
state
.
bold
=
false
;
state
.
italic
=
false
;
}
// return first saved token
data
.
oldToken
=
readyTokens
.
shift
();
stream
.
pos
=
pos
;
stream
.
string
=
string
;
return
this
.
makeFullStyle
(
style
,
state
);
}
else
if
(
stream
.
sol
()
)
{
// reset bold and italic status in every new line
state
.
bold
=
false
;
state
.
italic
=
false
;
state
.
nDt
=
0
;
data
.
firstSingleLetterWord
=
null
;
data
.
firstMultiLetterWord
=
null
;
data
.
firstSpace
=
null
;
if
(
state
.
tokenize
.
name
===
'eatExtTokens'
)
{
state
.
stack
.
pop
();
// dispose eatExtTokens
state
.
tokenize
=
state
.
stack
.
pop
();
// dispose eatExtTagArea
state
.
extName
=
false
;
state
.
extMode
=
false
;
state
.
extState
=
false
;
}
}
readyTokens
.
length
=
0
;
data
.
mark
=
null
;
data
.
oldToken
=
{
pos
:
stream
.
pos
,
string
:
stream
.
string
,
state
:
copyState
(
state
),
style
:
''
};
const
{
start
}
=
stream
;
do
{
// get token style
stream
.
start
=
stream
.
pos
;
const
st
=
state
.
tokenize
(
stream
,
state
);
// save token
readyTokens
.
push
(
{
pos
:
stream
.
pos
,
string
:
stream
.
string
,
state
:
copyState
(
state
),
style
:
st
}
);
}
while
(
!
stream
.
eol
()
);
if
(
!
state
.
bold
||
!
state
.
italic
)
{
// no need to rollback
data
.
mark
=
null
;
}
stream
.
start
=
start
;
stream
.
pos
=
data
.
oldToken
.
pos
;
stream
.
string
=
data
.
oldToken
.
string
;
Object
.
assign
(
state
,
data
.
oldToken
.
state
);
return
''
;
},
/**
* @param {Object} state
* @private
*/
blankLine
:
(
state
)
=>
{
if
(
state
.
extMode
&&
state
.
extMode
.
blankLine
)
{
state
.
extMode
.
blankLine
(
state
.
extState
);
}
},
/**
* Extra tokens to use in this parser.
*
* @see CodeMirrorMediaWikiConfig.defaultTokenTable
* @return {Object<Tag>}
* @private
*/
tokenTable
:
this
.
tokenTable
,
/**
* @see https://codemirror.net/docs/ref/#language.StreamParser.languageData
* @return {Object}
* @private
*/
languageData
:
{
autocomplete
:
completionSource
(
this
),
closeBrackets
:
{
brackets
:
[
'('
,
'['
,
'{'
,
'"'
],
before
:
')]}>'
}
}
};
}
}
/* eslint-disable max-len */
/**
* @module ext.CodeMirror.v6.mode.mediawiki
* @description
* This module provides syntax highlighting for the MediaWiki mode in CodeMirror.
* @example
* const require = await mw.loader.using( [ 'ext.CodeMirror.v6', 'ext.CodeMirror.v6.mode.mediawiki' ] );
* const CodeMirror = require( 'ext.CodeMirror.v6' );
* const { mediawiki } = require( 'ext.CodeMirror.v6.mode.mediawiki' );
* const cm = new CodeMirror( myTextarea, mediawiki() );
* cm.initialize();
*/
/* eslint-enable max-len */
let
handler
;
/**
* Gets a LanguageSupport-like instance for the MediaWiki mode.
*
* @param {Object} [config] Configuration options for the MediaWiki mode.
* @param {boolean} [config.bidiIsolation=false] Enable bidi isolation around HTML tags.
* This should generally always be enabled on RTL pages, but it comes with a performance cost.
* @param {boolean} [config.codeFolding=true] Enable code folding.
* @param {boolean} [config.autocomplete=true] Enable autocompletion.
* @param {boolean} [config.openLinks=true] Enable opening of links.
* @param {boolean} [config.highlightRefs=true] Highlight references.
* @param {string[]} [config.languageVariants] Language variants that should be supported.
* @return {CodeMirrorMediaWiki|LanguageSupport}
* @stable to call
*/
const
mediawiki
=
(
config
=
{
bidiIsolation
:
false
}
)
=>
{
// Register MW-specific Extensions into CodeMirror preferences. Whether they are enabled
// or not is determined by the user's preferences and wiki configuration.
if
(
handler
)
{
mw
.
hook
(
'ext.CodeMirror.ready'
).
remove
(
handler
);
}
handler
=
(
cm
)
=>
{
// T380840
if
(
!
cm
.
view
)
{
return
;
}
// Register MW-specific keymaps.
mwKeymap
(
cm
);
if
(
config
.
codeFolding
!==
false
)
{
cm
.
preferences
.
registerExtension
(
'codeFolding'
,
codeFoldingExtension
,
cm
.
view
);
}
if
(
config
.
autocomplete
!==
false
)
{
cm
.
preferences
.
registerExtension
(
'autocomplete'
,
autocompleteExtension
,
cm
.
view
);
}
if
(
config
.
openLinks
!==
false
)
{
cm
.
preferences
.
registerExtension
(
'openLinks'
,
openLinksExtension
,
cm
.
view
);
}
if
(
config
.
bidiIsolation
)
{
cm
.
preferences
.
registerExtension
(
'bidiIsolation'
,
bidiIsolationExtension
,
cm
.
view
);
}
if
(
config
.
highlightRefs
!==
false
)
{
const
highlightRefsExtension
=
[
EditorView
.
theme
(
{
'.cm-mw-tag-ref'
:
{
backgroundColor
:
'var( --background-color-refs )'
}
}
)
];
cm
.
preferences
.
registerExtension
(
'highlightRefs'
,
highlightRefsExtension
,
cm
.
view
);
}
};
mw
.
hook
(
'ext.CodeMirror.ready'
).
add
(
handler
);
return
new
CodeMirrorMediaWiki
(
mw
.
config
.
get
(
'extCodeMirrorConfig'
)
);
};
module
.
exports
=
mediawiki
;
module
.
exports
.
mediawiki
=
mediawiki
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Aug 16 2026, 08:58 (12 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
67/93/84e1ed9f88ea9b5bb627cd70ce19
Default Alt Text
codemirror.mediawiki.js (51 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment