Skip to content

Commit

Permalink
enh(parser) beginScope, endScope
Browse files Browse the repository at this point in the history
- also adds multi-match support to endScope
  • Loading branch information
joshgoebel committed Apr 22, 2021
1 parent 00c54bd commit d4bc4ec
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 22 deletions.
37 changes: 35 additions & 2 deletions docs/mode-reference.rst
Expand Up @@ -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``.

::

Expand All @@ -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"
},
Expand All @@ -206,6 +223,22 @@ capture groups of their own yet.* If your regexes uses groups at all, they
For more info see issue `#3095 <https://github.com/highlightjs/highlight.js/issues/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
Expand Down
33 changes: 24 additions & 9 deletions src/highlight.js
Expand Up @@ -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++;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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)) {
Expand Down
77 changes: 66 additions & 11 deletions src/lib/ext/multi_class.js
Expand Up @@ -34,10 +34,11 @@ const MultiClassError = new Error();
*
* @param {CompiledMode} mode
* @param {Array<RegExp>} 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<number,boolean> */
const emit = {};
/** @type Record<number,string> */
Expand All @@ -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)
}
72 changes: 72 additions & 0 deletions 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 <span class="hljs-apple">a</span><span class="hljs-boy">b</span>c<span class="hljs-delta">d</span>`);
})
it('should support an outer scope wrapper', () => {
const code = "xyz me 123";
const result = hljs.highlight(code, { language: 'test' });

result.value.should.equal(
`<span class="hljs-string">` +
`<span class="hljs-red">xyz</span> me <span class="hljs-green">123</span>` +
`</span>`);
})
it('should support textual beginScope & endScope pair', () => {
const code = "dumb really luck";
const result = hljs.highlight(code, { language: 'test' });

result.value.should.equal(`<span class="hljs-red">dumb</span> really <span class="hljs-green">luck</span>`);
});
it('should support textual beginScope', () => {
const code = "abcdef";
const result = hljs.highlight(code, { language: 'test' });

result.value.should.equal(`<span class="hljs-letters">abc</span><span class="hljs-more">def</span>`);
});

});
3 changes: 3 additions & 0 deletions types/index.d.ts
Expand Up @@ -202,6 +202,8 @@ declare module 'highlight.js' {
isMultiClass?: boolean
starts?: CompiledMode
parent?: CompiledMode
beginScope?: Record<number, string> & {_emit?: Record<number,boolean>, _multi?: boolean, _wrap?: string}
endScope?: Record<number, string> & {_emit?: Record<number,boolean>, _multi?: boolean, _wrap?: string}
}

interface ModeDetails {
Expand All @@ -211,6 +213,7 @@ declare module 'highlight.js' {
className?: string
_emit?: Record<number, boolean>
scope?: string | Record<number, string>
beginScope?: string | Record<number, string>
contains?: ("self" | Mode)[]
endsParent?: boolean
endsWithParent?: boolean
Expand Down

0 comments on commit d4bc4ec

Please sign in to comment.