Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

beginScope and endScope #3159

Merged
merged 4 commits into from May 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion .eslintrc.js
Expand Up @@ -43,7 +43,11 @@ module.exports = {
overrides: [
{
files: ["types/*.ts", "src/*.ts"],
parser: '@typescript-eslint/parser'
parser: '@typescript-eslint/parser',
rules: {
"import/no-duplicates": "off",
"import/extensions": "off"
}
},
{
files: ["src/**/*.js"],
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -43,6 +43,8 @@ Language Grammars:

Parser:

- enh(parser) add `beginScope` and `endScope` to allow separate scoping begin and end (#3159) [Josh Goebel][]
- enh(parsed) `endScope` now supports multi-class matchers as well (#3159) [Josh Goebel][]
- enh(parser) `highlightElement` now always tags blocks with a consistent `language-[name]` class [Josh Goebel][]
- subLanguage `span` tags now also always have the `language-` prefix added
- enh(parser) support multi-class matchers (#3081) [Josh Goebel][]
Expand Down
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
74 changes: 55 additions & 19 deletions src/highlight.js
Expand Up @@ -14,6 +14,27 @@ import { compileLanguage } from './lib/mode_compiler.js';
import * as packageJSON from '../package.json';
import * as logger from "./lib/logger.js";

/**
@typedef {import('highlight.js').Mode} Mode
@typedef {import('highlight.js').CompiledMode} CompiledMode
@typedef {import('highlight.js').Language} Language
@typedef {import('highlight.js').HLJSApi} HLJSApi
@typedef {import('highlight.js').HLJSPlugin} HLJSPlugin
@typedef {import('highlight.js').PluginEvent} PluginEvent
@typedef {import('highlight.js').HLJSOptions} HLJSOptions
@typedef {import('highlight.js').LanguageFn} LanguageFn
@typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement
@typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext
@typedef {import('highlight.js/private').MatchType} MatchType
@typedef {import('highlight.js/private').KeywordData} KeywordData
@typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch
@typedef {import('highlight.js/private').AnnotatedError} AnnotatedError
@typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult
@typedef {import('highlight.js').HighlightOptions} HighlightOptions
@typedef {import('highlight.js').HighlightResult} HighlightResult
*/


const escape = utils.escapeHTML;
const inherit = utils.inherit;
const NO_MATCH = Symbol("nomatch");
Expand Down Expand Up @@ -95,7 +116,7 @@ const HLJS = function(hljs) {
* NEW API
* highlight(code, {lang, ignoreIllegals})
*
* @param {string} codeOrlanguageName - the language to use for highlighting
* @param {string} codeOrLanguageName - the language to use for highlighting
* @param {string | HighlightOptions} optionsOrCode - the code to highlight
* @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail
* @param {CompiledMode} [continuation] - current continuation mode, if any
Expand All @@ -108,11 +129,11 @@ const HLJS = function(hljs) {
* @property {CompiledMode} top - top of the current mode stack
* @property {boolean} illegal - indicates whether any illegal matches were found
*/
function highlight(codeOrlanguageName, optionsOrCode, ignoreIllegals, continuation) {
function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals, continuation) {
let code = "";
let languageName = "";
if (typeof optionsOrCode === "object") {
code = codeOrlanguageName;
code = codeOrLanguageName;
ignoreIllegals = optionsOrCode.ignoreIllegals;
languageName = optionsOrCode.language;
// continuation not supported at all via the new API
Expand All @@ -122,7 +143,7 @@ const HLJS = function(hljs) {
// old API
logger.deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated.");
logger.deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277");
languageName = codeOrlanguageName;
languageName = codeOrLanguageName;
code = optionsOrCode;
}

Expand Down Expand Up @@ -246,18 +267,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 @@ -268,13 +290,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) {
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 @@ -316,7 +346,7 @@ const HLJS = function(hljs) {
*/
function doIgnore(lexeme) {
if (top.matcher.regexIndex === 0) {
// no more regexs to potentially match here, so we move the cursor forward one
// no more regexes to potentially match here, so we move the cursor forward one
// space
modeBuffer += lexeme[0];
return 1;
Expand Down Expand Up @@ -375,7 +405,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 Expand Up @@ -417,7 +453,7 @@ const HLJS = function(hljs) {
/**
* Process an individual match
*
* @param {string} textBeforeMatch - text preceeding the match (since the last match)
* @param {string} textBeforeMatch - text preceding the match (since the last match)
* @param {EnhancedMatch} [match] - the match itself
*/
function processLexeme(textBeforeMatch, match) {
Expand Down Expand Up @@ -499,7 +535,7 @@ const HLJS = function(hljs) {
throw new Error('Unknown language: "' + languageName + '"');
}

const md = compileLanguage(language, { plugins });
const md = compileLanguage(language);
let result = '';
/** @type {CompiledMode} */
let top = continuation || md;
Expand Down Expand Up @@ -701,12 +737,12 @@ const HLJS = function(hljs) {
language: result.language,
// TODO: remove with version 11.0
re: result.relevance,
relavance: result.relevance
relevance: result.relevance
};
if (result.secondBest) {
element.secondBest = {
language: result.secondBest.language,
relavance: result.secondBest.relevance
relevance: result.secondBest.relevance
};
}
}
Expand Down Expand Up @@ -928,7 +964,7 @@ const HLJS = function(hljs) {
}
}

// merge all the modes/regexs into our main object
// merge all the modes/regexes into our main object
Object.assign(hljs, MODES);

return hljs;
Expand Down
11 changes: 8 additions & 3 deletions src/lib/compiler_extensions.js
@@ -1,5 +1,10 @@
import * as regex from './regex.js';

/**
@typedef {import('highlight.js').CallbackResponse} CallbackResponse
@typedef {import('highlight.js').CompilerExt} CompilerExt
*/

// Grammar extensions / plugins
// See: https://github.com/highlightjs/highlight.js/issues/2833

Expand All @@ -24,7 +29,7 @@ import * as regex from './regex.js';
* @param {RegExpMatchArray} match
* @param {CallbackResponse} response
*/
function skipIfhasPrecedingDot(match, response) {
function skipIfHasPrecedingDot(match, response) {
const before = match.input[match.index - 1];
if (before === ".") {
response.ignoreMatch();
Expand All @@ -35,7 +40,7 @@ function skipIfhasPrecedingDot(match, response) {
*
* @type {CompilerExt}
*/
export function scopeClassName(mode, parent) {
export function scopeClassName(mode, _parent) {
// eslint-disable-next-line no-undefined
if (mode.className !== undefined) {
mode.scope = mode.className;
Expand All @@ -57,7 +62,7 @@ export function beginKeywords(mode, parent) {
// doesn't allow spaces in keywords anyways and we still check for the boundary
// first
mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)';
mode.__beforeBegin = skipIfhasPrecedingDot;
mode.__beforeBegin = skipIfHasPrecedingDot;
mode.keywords = mode.keywords || mode.beginKeywords;
delete mode.beginKeywords;

Expand Down