diff --git a/docs/mode-reference.rst b/docs/mode-reference.rst index d8222b2d43..cfe2e9be26 100644 --- a/docs/mode-reference.rst +++ b/docs/mode-reference.rst @@ -177,8 +177,25 @@ begin Regular expression starting a mode. For example a single quote for strings or two forward slashes for C-style comments. If absent, ``begin`` defaults to a regexp that matches anything, so the mode starts immediately. +This may also be an array. See beginScope. -You can also pass an array when you need to individually highlight portions of the match using different scopes: +beginScope +^^^^^^^^^^ + +- **type**: scope +- **type**: numeric index of scopes (when ``begin`` is an array) + +This can be used to apply a scope to just the begin match portion. + +:: + + { + begin: /def/, + beginScope: "keyword" + } + +You can also use ``beginScope`` to individually highlight portions of the match +with different scopes by passing an array to ``begin``. :: @@ -188,7 +205,7 @@ You can also pass an array when you need to individually highlight portions of t /\s+/, hljs.IDENT_RE ], - scope: { + beginScope: { 1: "keyword", 3: "title" }, @@ -206,6 +223,22 @@ capture groups of their own yet.* If your regexes uses groups at all, they For more info see issue `#3095 `_. +endScope +^^^^^^^^ + +- **type**: scope +- **type**: numeric index of scopes (when ``end`` is an array) + +This has the same behavior as ``beginScope`` but applies to the content of the +``end`` match. + +:: + + { + begin: /FIRST/, + end: /LAST/, + endScope: "built_in" + } match diff --git a/src/highlight.js b/src/highlight.js index 35c6b4f85b..0aaf57b4ec 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -266,18 +266,19 @@ const HLJS = function(hljs) { * @param {CompiledMode} mode * @param {RegExpMatchArray} match */ - function emitMultiClass(mode, match) { + function emitMultiClass(scope, match) { let i = 1; // eslint-disable-next-line no-undefined while (match[i] !== undefined) { - if (!mode._emit[i]) { i++; continue; } - const klass = language.classNameAliases[mode.scope[i]] || mode.scope[i]; + if (!scope._emit[i]) { i++; continue; } + const klass = language.classNameAliases[scope[i]] || scope[i]; const text = match[i]; if (klass) { emitter.addKeyword(text, klass); } else { modeBuffer = text; processKeywords(); + modeBuffer = ""; } i++; } @@ -288,13 +289,21 @@ const HLJS = function(hljs) { * @param {RegExpMatchArray} match */ function startNewMode(mode, match) { - if (mode.isMultiClass) { - // at this point modeBuffer should just be the match - emitMultiClass(mode, match); - modeBuffer = ""; - } else if (mode.scope && typeof mode.scope === "string") { + if (mode.scope && typeof mode.scope === "string") { emitter.openNode(language.classNameAliases[mode.scope] || mode.scope); } + if (mode.beginScope) { + // beginScope just wraps the begin match itself in a scope + if (mode.beginScope._wrap) { + emitter.addKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap); + modeBuffer = ""; + } else if (mode.beginScope._multi) { + // at this point modeBuffer should just be the match + emitMultiClass(mode.beginScope, match); + modeBuffer = ""; + } + } + top = Object.create(mode, { parent: { value: top } }); return top; } @@ -395,7 +404,13 @@ const HLJS = function(hljs) { if (!endMode) { return NO_MATCH; } const origin = top; - if (origin.skip) { + if (top.endScope && top.endScope._wrap) { + processBuffer(); + emitter.addKeyword(lexeme, top.endScope._wrap); + } else if (top.endScope && top.endScope._multi) { + processBuffer(); + emitMultiClass(top.endScope, match); + } else if (origin.skip) { modeBuffer += lexeme; } else { if (!(origin.returnEnd || origin.excludeEnd)) { diff --git a/src/lib/ext/multi_class.js b/src/lib/ext/multi_class.js index 951b21d1f3..4f840e51f3 100644 --- a/src/lib/ext/multi_class.js +++ b/src/lib/ext/multi_class.js @@ -34,10 +34,11 @@ const MultiClassError = new Error(); * * @param {CompiledMode} mode * @param {Array} regexes + * @param {{key: "beginScope"|"endScope"}} opts */ -function remapScopeNames(mode, regexes) { +function remapScopeNames(mode, regexes, { key }) { let offset = 0; - const scopeNames = mode.scope; + const scopeNames = mode[key]; /** @type Record */ const emit = {}; /** @type Record */ @@ -50,28 +51,82 @@ function remapScopeNames(mode, regexes) { } // we use _emit to keep track of which match groups are "top-level" to avoid double // output from inside match groups - mode._emit = emit; - mode.scope = positions; + mode[key] = positions; + mode[key]._emit = emit; + mode[key]._multi = true; } /** * @param {CompiledMode} mode */ -export function MultiClass(mode) { +function beginMultiClass(mode) { if (!Array.isArray(mode.begin)) return; if (mode.skip || mode.excludeBegin || mode.returnBegin) { - logger.error("skip, excludeBegin, returnBegin not compatible with multi-class"); + logger.error("skip, excludeBegin, returnBegin not compatible with beginScope: {}"); throw MultiClassError; } - if (typeof mode.scope !== "object" || mode.scope == null) { - logger.error("scope/className must be object"); + if (typeof mode.beginScope !== "object" || mode.beginScope === null) { + logger.error("beginScope must be object"); throw MultiClassError; } - const matchers = mode.begin; - remapScopeNames(mode, matchers); + remapScopeNames(mode, mode.begin, {key: "beginScope"}); mode.begin = regex._rewriteBackreferences(mode.begin, { joinWith: "" }); - mode.isMultiClass = true; +} + +/** + * @param {CompiledMode} mode + */ +function endMultiClass(mode) { + if (!Array.isArray(mode.end)) return; + + if (mode.skip || mode.excludeEnd || mode.returnEnd) { + logger.error("skip, excludeEnd, returnEnd not compatible with endScope: {}"); + throw MultiClassError; + } + + if (typeof mode.endScope !== "object" || mode.endScope === null) { + logger.error("endScope must be object"); + throw MultiClassError; + } + + remapScopeNames(mode, mode.end, {key: "endScope"}); + mode.end = regex._rewriteBackreferences(mode.end, { joinWith: "" }); +} + +/** + * this exists only to allow `scope: {}` to be used beside `match:` + * Otherwise `beginScope` would necessary and that would look weird + + { + match: [ /def/, /\w+/ ] + scope: { 1: "keyword" , 2: "title" } + } + + * @param {CompiledMode} mode + */ +function scopeSugar(mode) { + if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) { + mode.beginScope = mode.scope; + delete mode.scope; + } +} + +/** + * @param {CompiledMode} mode + */ +export function MultiClass(mode) { + scopeSugar(mode) + + if (typeof mode.beginScope === "string") { + mode.beginScope = { _wrap: mode.beginScope }; + } + if (typeof mode.endScope === "string") { + mode.endScope = { _wrap: mode.endScope }; + } + + beginMultiClass(mode) + endMultiClass(mode) } diff --git a/test/parser/beginEndScope.js b/test/parser/beginEndScope.js new file mode 100644 index 0000000000..e2cf9af7b8 --- /dev/null +++ b/test/parser/beginEndScope.js @@ -0,0 +1,72 @@ +'use strict'; + +const hljs = require('../../build'); +hljs.debugMode(); + +describe('beginScope and endScope', () => { + before(() => { + const grammar = function() { + return { + contains: [ + { + begin: /xyz/, + end: /123/, + scope: "string", + beginScope: "red", + endScope: "green" + }, + { + begin: /123/, + end: [ /a/,/((b))/,/c/,/d/ ], + endScope: { 1: "apple", 2: "boy", 4: "delta" } + }, + { + begin: /dumb/, + end: /luck/, + beginScope: "red", + endScope: "green" + }, + { + begin: /abc/, + beginScope: "letters", + contains: [ + { match: /def/, scope: "more" } + ] + } + ] + } + }; + hljs.registerLanguage("test", grammar); + }); + after(() => { + hljs.unregisterLanguage("test"); + }); + it('should support multi-class', () => { + const code = "123 abcd"; + const result = hljs.highlight(code, { language: 'test' }); + + result.value.should.equal(`123 abcd`); + }) + it('should support an outer scope wrapper', () => { + const code = "xyz me 123"; + const result = hljs.highlight(code, { language: 'test' }); + + result.value.should.equal( + `` + + `xyz me 123` + + ``); + }) + it('should support textual beginScope & endScope pair', () => { + const code = "dumb really luck"; + const result = hljs.highlight(code, { language: 'test' }); + + result.value.should.equal(`dumb really luck`); + }); + it('should support textual beginScope', () => { + const code = "abcdef"; + const result = hljs.highlight(code, { language: 'test' }); + + result.value.should.equal(`abcdef`); + }); + +}); diff --git a/types/index.d.ts b/types/index.d.ts index b6b4424816..b4c9a4e262 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -202,6 +202,8 @@ declare module 'highlight.js' { isMultiClass?: boolean starts?: CompiledMode parent?: CompiledMode + beginScope?: Record & {_emit?: Record, _multi?: boolean, _wrap?: string} + endScope?: Record & {_emit?: Record, _multi?: boolean, _wrap?: string} } interface ModeDetails { @@ -211,6 +213,7 @@ declare module 'highlight.js' { className?: string _emit?: Record scope?: string | Record + beginScope?: string | Record contains?: ("self" | Mode)[] endsParent?: boolean endsWithParent?: boolean