diff --git a/CHANGELOG.md b/CHANGELOG.md index de56c18..7580f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ - Double brackets followed by a valid URL protocol should be an external link in the MediaWiki mode - Template argument names are now correctly highlighted in the MediaWiki mode - `<` breaks the wikilink syntax in the MediaWiki mode +- Table cell attributes are now correctly highlighted and [autocompleted](./README.md#autocompletion) in the MediaWiki mode **Changed** - MediaWiki tokenizer reworked to mimic the parser more closely +- New CSS class `cm-mw-table-delimiter2` for `|` delimiter within a table cell in the MediaWiki mode ## 2.12.6 diff --git a/mediawiki.css b/mediawiki.css index 85cbe6e..4b7565f 100644 --- a/mediawiki.css +++ b/mediawiki.css @@ -188,6 +188,7 @@ color: #d08; font-weight: bold; } +.cm-mw-table-delimiter2, .cm-mw-table-definition { color: #d08; font-weight: normal; diff --git a/src/config.ts b/src/config.ts index c889193..10bb35c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -148,6 +148,7 @@ const modeConfig = { tableCaption: 'mw-table-caption', tableDefinition: 'mw-table-definition', tableDelimiter: 'mw-table-delimiter', + tableDelimiter2: 'mw-table-delimiter2', template: 'mw-template', templateArgumentName: 'mw-template-argument-name', templateBracket: 'mw-template-bracket', diff --git a/src/mediawiki.ts b/src/mediawiki.ts index 85d8575..0162c26 100644 --- a/src/mediawiki.ts +++ b/src/mediawiki.ts @@ -30,13 +30,15 @@ declare type Style = string | [string]; declare type Tokenizer = (stream: StringStream, state: State) => T; declare type TagName = keyof typeof tokens; declare type NestCount = 'nTemplate' | 'nExt' | 'nVar' | 'nLink' | 'nExtLink'; -declare type Nesting = Record & {extName: string | false}; +declare interface Nesting extends Record { + extName: string | false; + extState: object | false; +} declare interface State extends Nesting { tokenize: Tokenizer; readonly stack: Tokenizer[]; readonly inHtmlTag: string[]; extMode: StreamParser | false; - extState: object | false; lbrack: boolean | undefined; bold: boolean; italic: boolean; @@ -45,7 +47,7 @@ declare interface State extends Nesting { } declare type ExtState = Omit & Partial>; declare interface Token { - readonly pos: number; + pos: number; readonly string: string; style: Style; readonly state: State; @@ -95,7 +97,8 @@ const cmpNesting = (a: Partial, b: Nesting): boolean => && a.nVar === b.nVar && a.nLink === b.nLink && a.nExtLink === b.nExtLink - && a.extName === b.extName; + && a.extName === b.extName + && (a.extName !== 'mediawiki' || cmpNesting(a.extState as Partial, b.extState as Nesting)); /** * 复制嵌套状态 @@ -109,6 +112,10 @@ const copyNesting = (a: Partial, b: Nesting): void => { a.nLink = b.nLink; a.nExtLink = b.nExtLink; a.extName = b.extName; + if (a.extName === 'mediawiki') { + a.extState = {}; + copyNesting(a.extState, b.extState as Nesting); + } }; /** @@ -200,12 +207,19 @@ const hasTag = (types: Set, names: string | string[]): boolean => /** Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov */ export class MediaWiki { + /** 已解析的节点 */ + declare readonly readyTokens: Token[]; + + /** 当前起始位置 */ + declare oldToken: Token | null; + + /** 可能需要回滚的`'''` */ + declare mark: number | null; + declare readonly config; declare firstSingleLetterWord: number | null; declare firstMultiLetterWord: number | null; declare firstSpace: number | null; - declare oldTokens: Token[]; - declare mark: number | null; declare readonly tokenTable; declare readonly hiddenTable: Record; declare readonly permittedHtmlTags; @@ -242,7 +256,8 @@ export class MediaWiki { this.firstSingleLetterWord = null; this.firstMultiLetterWord = null; this.firstSpace = null; - this.oldTokens = []; + this.readyTokens = []; + this.oldToken = null; this.mark = null; this.tokenTable = {...tokenTable}; this.hiddenTable = {}; @@ -312,7 +327,6 @@ export class MediaWiki { * @param token * @param hidden Whether the token is not highlighted * @param parent - * @internal */ addToken(token: string, hidden = false, parent?: Tag): void { (this[hidden ? 'hiddenTable' : 'tokenTable'][`mw-${token}`] as Tag | undefined) ||= Tag.define(parent); @@ -529,7 +543,7 @@ export class MediaWiki { // fall through case '{': if (stream.match(/^(?:\||\{\{\s*!\s*\}\})\s*/u)) { - chain(state, this.inTableDefinition); + chain(state, this.inTableDefinition()); return this.makeLocalTagStyle('tableBracket', state); } break; @@ -906,20 +920,21 @@ export class MediaWiki { get eatStartTable(): Tokenizer { return (stream, state) => { stream.match(/^(?:\{\||\{{3}\s*!\s*\}\})\s*/u); - state.tokenize = this.inTableDefinition; + state.tokenize = this.inTableDefinition(); return this.makeLocalTagStyle('tableBracket', state); }; } - get inTableDefinition(): Tokenizer { + inTableDefinition(tr?: boolean): Tokenizer { + const style = `${tokens.tableDefinition} mw-html-${tr ? 'tr' : 'table'}`; return (stream, state) => { if (stream.sol()) { state.tokenize = this.inTable; return ''; } return stream.eatWhile(/[^&{<]/u) - ? this.makeLocalTagStyle('tableDefinition', state) - : this.eatWikiText('tableDefinition')(stream, state); + ? this.makeLocalStyle(style, state) + : this.eatWikiText(style)(stream, state); }; } @@ -929,7 +944,7 @@ export class MediaWiki { stream.eatSpace(); if (stream.match(/^(?:\||\{\{\s*!\s*\}\})/u)) { if (stream.match(/^-+\s*/u)) { - state.tokenize = this.inTableDefinition; + state.tokenize = this.inTableDefinition(true); return this.makeLocalTagStyle('tableDelimiter', state); } else if (stream.match(/^\+\s*/u)) { state.tokenize = this.inTableCell(true, TableCell.Caption); @@ -982,7 +997,7 @@ export class MediaWiki { return this.makeLocalTagStyle('tableDelimiter', state); } else if (needAttr && stream.match(/^(?:\||\{\{\s*!\s*\}\})\s*/u)) { state.tokenize = this.inTableCell(false, type); - return this.makeLocalTagStyle('tableDelimiter', state); + return this.makeLocalTagStyle('tableDelimiter2', state); } } return this.eatWikiText(style)(stream, state); @@ -1349,8 +1364,26 @@ export class MediaWiki { copyState, token: (stream, state): string => { - if (this.oldTokens.length > 0) { - const {pos, string, state: {bold, italic, ...other}, style} = this.oldTokens[0]!; + const {readyTokens} = this; + let {oldToken} = this; + while ( + oldToken + && ( + // 如果 PartialParse 的起点位于当前位置之后 + stream.pos > oldToken.pos + || stream.pos === oldToken.pos && state.tokenize !== oldToken.state.tokenize + ) + ) { + oldToken = readyTokens.shift()!; + } + if ( + // 检查起点 + stream.pos === oldToken?.pos + && stream.string === oldToken.string + && cmpNesting(state, oldToken.state) + ) { + const {pos, string, state: {bold, italic, ...other}, style} = readyTokens[0]!; + // just send saved tokens till they exists Object.assign(state, other); if ( !state.extMode && state.nLink === 0 @@ -1359,9 +1392,12 @@ export class MediaWiki { if (this.mark === pos) { // rollback this.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++; + this.oldToken = oldToken; return this.makeFullStyle(s, state); } const length = pos - stream.pos; @@ -1372,7 +1408,8 @@ export class MediaWiki { state.bold = !state.bold; } } - this.oldTokens.shift(); + // return first saved token + this.oldToken = readyTokens.shift()!; stream.pos = pos; stream.string = string; return this.makeFullStyle(style, state); @@ -1385,14 +1422,16 @@ export class MediaWiki { this.firstMultiLetterWord = null; this.firstSpace = null; } - const {pos, string} = stream, - oldState = copyState(state); + readyTokens.length = 0; + this.mark = null; + this.oldToken = {pos: stream.pos, string: stream.string, state: copyState(state), style: ''}; let style: Style; do { + // get token style style = state.tokenize(stream, state); if (typeof style === 'string' && style.includes(tokens.templateArgumentName)) { - for (let i = this.oldTokens.length - 1; i >= 0; i--) { - const token = this.oldTokens[i]!; + for (let i = readyTokens.length - 1; i >= 0; i--) { + const token = readyTokens[i]!; if (cmpNesting(state, token.state)) { const types = typeof token.style === 'string' && token.style.split(' '), j = types && types.indexOf(tokens.template); @@ -1404,15 +1443,41 @@ export class MediaWiki { } } } + } else if (typeof style === 'string' && style.includes(tokens.tableDelimiter2)) { + for (let i = readyTokens.length - 1; i >= 0; i--) { + const token = readyTokens[i]!, + {state: st, style: s} = token; + if (cmpNesting(state, st)) { + const local = typeof s === 'string', + type = !local && s[0].split(' ').find(t => t && !t.endsWith('-ground')), + isCaption = type === tokens.tableCaption, + isTh = type === tokens.strong; + if (isCaption) { + token.style = `${ + s[0].replace(tokens.tableCaption, '') + } ${tokens.tableDefinition} mw-html-caption`; + } else if (isTh) { + token.style = `${ + s[0].replace(tokens.strong, '') + } ${tokens.tableDefinition} mw-html-th`; + } else if (type === undefined) { + token.style = `${s[0]} ${tokens.tableDefinition} mw-html-td`; + } else if (local && s.includes(tokens.tableDelimiter)) { + break; + } + } + } } - this.oldTokens.push({pos: stream.pos, string: stream.string, state: copyState(state), style}); + // save token + readyTokens.push({pos: stream.pos, string: stream.string, state: copyState(state), style}); } while (!stream.eol()); if (!state.bold || !state.italic) { + // no need to rollback this.mark = null; } - stream.pos = pos; - stream.string = string; - Object.assign(state, oldState); + stream.pos = this.oldToken.pos; + stream.string = this.oldToken.string; + Object.assign(state, this.oldToken.state); return ''; }, @@ -1589,8 +1654,8 @@ export class MediaWiki { } else if ( hasTag(types, ['htmlTagAttribute', 'tableDefinition', 'mw-ext-pre', 'mw-ext-gallery', 'mw-ext-poem']) ) { - const tagName = /mw-(?:ext|html)-([a-z]+)/u.exec(node.name)?.[1], - mt = context.matchBefore(tagName ? /\s[a-z]+$/iu : /[\s|-][a-z]+$/iu); + const [, tagName] = /mw-(?:ext|html)-([a-z]+)/u.exec(node.name) as string[] as [string, string], + mt = context.matchBefore(hasTag(types, 'tableDefinition') ? /[\s|-][a-z]+$/iu : /\s[a-z]+$/iu); if (mt) { return mt.from >= from && /^[|-]/u.test(mt.text) ? null diff --git a/test/parserTests.json b/test/parserTests.json index 231d30f..483734b 100644 --- a/test/parserTests.json +++ b/test/parserTests.json @@ -11632,7 +11632,7 @@ }, { "text": "testing ", - "name": "" + "name": "table-definition html-td" }, { "text": "[[", @@ -11656,11 +11656,11 @@ }, { "text": " ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "three", @@ -11680,11 +11680,11 @@ }, { "text": "testing one two ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "three", @@ -11704,7 +11704,7 @@ }, { "text": "testing=\"", - "name": "" + "name": "table-definition html-td" }, { "text": "[[", @@ -11728,11 +11728,11 @@ }, { "text": "\" ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "three", @@ -15832,7 +15832,7 @@ }, { "text": "class=\"123\" ", - "name": "template-ground table-definition" + "name": "template-ground table-definition html-table" }, { "text": "<", @@ -19852,7 +19852,7 @@ }, { "text": "title= id=", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|", @@ -19996,11 +19996,11 @@ }, { "text": "+ class=\"error\" class=\"awesome\"", - "name": "strong" + "name": "table-definition html-th" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "status", @@ -22214,7 +22214,7 @@ }, { "text": "+ style=\"", - "name": "strong" + "name": "table-definition html-th" }, { "text": "<", @@ -22246,11 +22246,11 @@ }, { "text": "\"", - "name": "strong" + "name": "table-definition html-th" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "status", @@ -22276,7 +22276,7 @@ }, { "text": "+ style=\"", - "name": "strong" + "name": "table-definition html-th" }, { "text": "<", @@ -22308,11 +22308,11 @@ }, { "text": "\"", - "name": "strong" + "name": "table-definition html-th" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "status", @@ -24154,11 +24154,11 @@ }, { "text": "http://a", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" } ] }, @@ -24210,7 +24210,7 @@ }, { "text": "STYLE=__TOC__", - "name": "table-definition" + "name": "table-definition html-table" } ] }, @@ -25334,11 +25334,11 @@ }, { "text": "colspan=\"1\" ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "foo ", @@ -29650,7 +29650,7 @@ }, { "text": "id=foo | style='color: red'", - "name": "table-definition" + "name": "table-definition html-tr" }, { "text": "|", @@ -30366,11 +30366,11 @@ }, { "text": "title=\"Hello world", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "Foo", @@ -30396,11 +30396,11 @@ }, { "text": "title=\"Hello world", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "Foo", @@ -30412,11 +30412,11 @@ }, { "text": "style=\"color:red", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "Bar", @@ -30442,20 +30442,24 @@ }, { "text": "align=\"center\" ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "foo", "name": "" }, { - "text": "|| |", + "text": "|| ", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "|}", "name": "table-bracket" @@ -30484,11 +30488,11 @@ }, { "text": "style=\"color: red\" ", - "name": "strong" + "name": "table-definition html-th" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "Bar", @@ -30509,17 +30513,25 @@ "name": "table-bracket" }, { - "text": "!|", + "text": "!", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "foo", "name": "strong" }, { - "text": "!!|", + "text": "!!", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "bar", "name": "strong" @@ -30560,7 +30572,7 @@ }, { "text": "class=\"foo\"", - "name": "table-definition" + "name": "table-definition html-tr" }, { "text": "|-", @@ -30586,7 +30598,7 @@ }, { "text": "[[foo]]", - "name": "table-definition" + "name": "table-definition html-tr" }, { "text": "|}", @@ -30632,7 +30644,7 @@ }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "foo", @@ -39306,9 +39318,13 @@ "name": "table-caption" }, { - "text": " ! |", + "text": " ! ", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "bar", "name": "strong" @@ -63395,7 +63411,7 @@ }, { "text": "|}", - "name": "table-definition" + "name": "table-definition html-table" } ] }, @@ -63487,7 +63503,7 @@ }, { "text": "style=\"color: red;\"|}", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "{|", @@ -63495,7 +63511,7 @@ }, { "text": "style=\"color: red;\" |}", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|", @@ -63511,7 +63527,7 @@ }, { "text": "style=\"color: red;\"|} id=\"foo\"", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|", @@ -63527,7 +63543,7 @@ }, { "text": "style=\"color: red;\" |} id=\"foo\"", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|", @@ -63579,11 +63595,11 @@ }, { "text": "style=\"color: red;\" ", - "name": "table-caption" + "name": "table-definition html-caption" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "caption1", @@ -63617,11 +63633,11 @@ }, { "text": "style=\"color: red;\"", - "name": "table-caption" + "name": "table-definition html-caption" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "caption2", @@ -63633,11 +63649,11 @@ }, { "text": "style=\"color: red;\"", - "name": "table-caption" + "name": "table-definition html-caption" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "caption3", @@ -63671,11 +63687,11 @@ }, { "text": "foo bar foo", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "baz", @@ -63703,11 +63719,11 @@ }, { "text": "style='color:red;'", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "baz", @@ -63821,7 +63837,7 @@ }, { "text": "border=\"1\" cellpadding=\"2\"", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|+", @@ -64258,17 +64274,25 @@ "name": "table-bracket" }, { - "text": "!|", + "text": "!", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "h1", "name": "strong" }, { - "text": "||", + "text": "|", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "a", "name": "" @@ -64293,11 +64317,11 @@ }, { "text": "!style=\"color:red\"", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "bar", @@ -64323,11 +64347,11 @@ }, { "text": "style='color:red;'", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "+1", @@ -64339,11 +64363,11 @@ }, { "text": "style='color:blue;'", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "-1", @@ -64429,7 +64453,7 @@ }, { "text": "border=1", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|", @@ -64445,11 +64469,11 @@ }, { "text": "rowspan=2", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "Cell 2, row 1 (and 2)", @@ -64495,7 +64519,7 @@ }, { "text": "border=1", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "| ", @@ -64515,7 +64539,7 @@ }, { "text": "bgcolor=#ABCDEF border=2", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": "|", @@ -64565,11 +64589,11 @@ }, { "text": "Cell:", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "broken", @@ -64595,11 +64619,11 @@ }, { "text": "title=\"foo\" ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "bar", @@ -64611,7 +64635,7 @@ }, { "text": "title=\"foo", - "name": "" + "name": "table-definition html-td" }, { "text": "<", @@ -64643,11 +64667,11 @@ }, { "text": "\" ", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "bar", @@ -64778,9 +64802,13 @@ "name": "table-bracket" }, { - "text": "| |", + "text": "| ", "name": "table-delimiter" }, + { + "text": "|", + "name": "table-delimiter2" + }, { "text": "[", "name": "link-ground extlink-bracket" @@ -64933,11 +64961,11 @@ }, { "text": "\"", - "name": "" + "name": "table-definition html-td" }, { "text": "|", - "name": "table-delimiter" + "name": "table-delimiter2" }, { "text": "foo", @@ -64971,7 +64999,7 @@ }, { "text": "boo", - "name": "table-definition" + "name": "table-definition html-table" }, { "text": " [", - "name": "table-definition" + "name": "table-definition html-tr" }, { "text": "|}",