/
multi_class.js
132 lines (114 loc) · 3.45 KB
/
multi_class.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/* eslint-disable no-throw-literal */
import * as logger from "../../lib/logger.js";
import * as regex from "../regex.js";
/**
@typedef {import('highlight.js').CompiledMode} CompiledMode
*/
const MultiClassError = new Error();
/**
* Renumbers labeled scope names to account for additional inner match
* groups that otherwise would break everything.
*
* Lets say we 3 match scopes:
*
* { 1 => ..., 2 => ..., 3 => ... }
*
* So what we need is a clean match like this:
*
* (a)(b)(c) => [ "a", "b", "c" ]
*
* But this falls apart with inner match groups:
*
* (a)(((b)))(c) => ["a", "b", "b", "b", "c" ]
*
* Our scopes are now "out of alignment" and we're repeating `b` 3 times.
* What needs to happen is the numbers are remapped:
*
* { 1 => ..., 2 => ..., 5 => ... }
*
* We also need to know that the ONLY groups that should be output
* are 1, 2, and 5. This function handles this behavior.
*
* @param {CompiledMode} mode
* @param {Array<RegExp>} regexes
* @param {{key: "beginScope"|"endScope"}} opts
*/
function remapScopeNames(mode, regexes, { key }) {
let offset = 0;
const scopeNames = mode[key];
/** @type Record<number,boolean> */
const emit = {};
/** @type Record<number,string> */
const positions = {};
for (let i = 1; i <= regexes.length; i++) {
positions[i + offset] = scopeNames[i];
emit[i + offset] = true;
offset += regex.countMatchGroups(regexes[i - 1]);
}
// we use _emit to keep track of which match groups are "top-level" to avoid double
// output from inside match groups
mode[key] = positions;
mode[key]._emit = emit;
mode[key]._multi = true;
}
/**
* @param {CompiledMode} 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 beginScope: {}");
throw MultiClassError;
}
if (typeof mode.beginScope !== "object" || mode.beginScope === null) {
logger.error("beginScope must be object");
throw MultiClassError;
}
remapScopeNames(mode, mode.begin, {key: "beginScope"});
mode.begin = regex._rewriteBackreferences(mode.begin, { joinWith: "" });
}
/**
* @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)
}