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 // Grammar extensions allow "syntactic sugar" to be added to the grammar modes // without requiring any underlying changes to the compiler internals. // `compileMatch` being the perfect small example of now allowing a grammar // author to write `match` when they desire to match a single expression rather // than being forced to use `begin`. The extension then just moves `match` into // `begin` when it runs. Ie, no features have been added, but we've just made // the experience of writing (and reading grammars) a little bit nicer. // ------ // TODO: We need negative look-behind support to do this properly /** * Skip a match if it has a preceding dot * * This is used for `beginKeywords` to prevent matching expressions such as * `bob.keyword.do()`. The mode compiler automatically wires this up as a * special _internal_ 'on:begin' callback for modes with `beginKeywords` * @param {RegExpMatchArray} match * @param {CallbackResponse} response */ function skipIfHasPrecedingDot(match, response) { const before = match.input[match.index - 1]; if (before === ".") { response.ignoreMatch(); } } /** * * @type {CompilerExt} */ export function scopeClassName(mode, _parent) { // eslint-disable-next-line no-undefined if (mode.className !== undefined) { mode.scope = mode.className; delete mode.className; } } /** * `beginKeywords` syntactic sugar * @type {CompilerExt} */ export function beginKeywords(mode, parent) { if (!parent) return; if (!mode.beginKeywords) return; // for languages with keywords that include non-word characters checking for // a word boundary is not sufficient, so instead we check for a word boundary // or whitespace - this does no harm in any case since our keyword engine // 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.keywords = mode.keywords || mode.beginKeywords; delete mode.beginKeywords; // prevents double relevance, the keywords themselves provide // relevance, the mode doesn't need to double it // eslint-disable-next-line no-undefined if (mode.relevance === undefined) mode.relevance = 0; } /** * Allow `illegal` to contain an array of illegal values * @type {CompilerExt} */ export function compileIllegal(mode, _parent) { if (!Array.isArray(mode.illegal)) return; mode.illegal = regex.either(...mode.illegal); } /** * `match` to match a single expression for readability * @type {CompilerExt} */ export function compileMatch(mode, _parent) { if (!mode.match) return; if (mode.begin || mode.end) throw new Error("begin & end are not supported with match"); mode.begin = mode.match; delete mode.match; } /** * provides the default 1 relevance to all modes * @type {CompilerExt} */ export function compileRelevance(mode, _parent) { // eslint-disable-next-line no-undefined if (mode.relevance === undefined) mode.relevance = 1; }