|
| 1 | +import { betterAssign, deepClone } from './objects.js'; |
| 2 | + |
1 | 3 | /** |
2 | 4 | * Creates a deep copy of the language with the given id and appends the given tokens. |
3 | 5 | * |
|
13 | 15 | * Therefore, it is encouraged to order overwriting tokens according to the positions of the overwritten tokens. |
14 | 16 | * Furthermore, all non-overwriting tokens should be placed after the overwriting ones. |
15 | 17 | * |
16 | | - * @param {Grammar} grammar The grammar of the language to extend. |
17 | | - * @param {string} id The id of the language to extend. |
18 | | - * @param {Grammar} reDef The new tokens to append. |
| 18 | + * @param {Grammar} base The grammar of the language to extend. |
| 19 | + * @param {Grammar} grammar The new tokens to append. |
19 | 20 | * @returns {Grammar} The new language created. |
20 | 21 | * @example |
21 | 22 | * Prism.languages['css-with-colors'] = Prism.languages.extend('css', { |
|
26 | 27 | * 'color': /\b(?:red|green|blue)\b/ |
27 | 28 | * }); |
28 | 29 | */ |
29 | | -export function extend (grammar, id, reDef) { |
30 | | - const lang = cloneGrammar(grammar, id); |
| 30 | +export function extend (base, grammar) { |
| 31 | + const lang = deepClone(base); |
| 32 | + |
| 33 | + for (const key in grammar) { |
| 34 | + if (typeof key !== 'string' || key.startsWith('$')) { |
| 35 | + // ignore special keys |
| 36 | + continue; |
| 37 | + } |
31 | 38 |
|
32 | | - for (const key in reDef) { |
33 | | - lang[key] = reDef[key]; |
| 39 | + lang[key] = grammar[key]; |
34 | 40 | } |
35 | 41 |
|
36 | | - return lang; |
37 | | -} |
| 42 | + if (grammar.$insertBefore) { |
| 43 | + lang.$insertBefore = betterAssign(lang.$insertBefore ?? {}, grammar.$insertBefore); |
| 44 | + } |
38 | 45 |
|
39 | | -/** |
40 | | - * @param {Grammar} grammar |
41 | | - * @param {string} id |
42 | | - * @returns {Grammar} |
43 | | - */ |
44 | | -export function cloneGrammar (grammar, id) { |
45 | | - /** @type {Grammar} */ |
46 | | - const result = {}; |
| 46 | + if (grammar.$insertAfter) { |
| 47 | + lang.$insertAfter = betterAssign(lang.$insertAfter ?? {}, grammar.$insertAfter); |
| 48 | + } |
47 | 49 |
|
48 | | - /** @type {Map<Grammar, Grammar>} */ |
49 | | - const visited = new Map(); |
| 50 | + if (grammar.$insert) { |
| 51 | + // Syntactic sugar for $insertBefore/$insertAfter |
| 52 | + for (const tokenName in grammar.$insert) { |
| 53 | + const def = grammar.$insert[tokenName]; |
| 54 | + const { $before, $after, ...token } = def; |
| 55 | + const relToken = $before || $after; |
| 56 | + const all = $before ? '$insertBefore' : '$insertAfter'; |
| 57 | + lang[all] ??= {}; |
50 | 58 |
|
51 | | - /** |
52 | | - * @param {GrammarToken | RegExpLike} value |
53 | | - */ |
54 | | - function cloneToken (value) { |
55 | | - if (!value.pattern) { |
56 | | - return value; |
57 | | - } |
58 | | - else { |
59 | | - /** @type {GrammarToken} */ |
60 | | - const copy = { pattern: value.pattern }; |
61 | | - if (value.lookbehind) { |
62 | | - copy.lookbehind = value.lookbehind; |
| 59 | + if (Array.isArray(relToken)) { |
| 60 | + // Insert in multiple places |
| 61 | + for (const t of relToken) { |
| 62 | + lang[all][t][tokenName] = token; |
| 63 | + } |
63 | 64 | } |
64 | | - if (value.greedy) { |
65 | | - copy.greedy = value.greedy; |
| 65 | + else if (relToken) { |
| 66 | + (lang[all][relToken] ??= {})[tokenName] = token; |
66 | 67 | } |
67 | | - if (value.alias) { |
68 | | - copy.alias = Array.isArray(value.alias) ? [...value.alias] : value.alias; |
| 68 | + else { |
| 69 | + lang[tokenName] = token; |
69 | 70 | } |
70 | | - if (value.inside) { |
71 | | - copy.inside = cloneRef(value.inside); |
72 | | - } |
73 | | - return copy; |
74 | 71 | } |
75 | 72 | } |
76 | 73 |
|
77 | | - /** |
78 | | - * @param {GrammarTokens['string']} value |
79 | | - */ |
80 | | - function cloneTokens (value) { |
81 | | - if (!value) { |
82 | | - return undefined; |
83 | | - } |
84 | | - else if (Array.isArray(value)) { |
85 | | - return value.map(cloneToken); |
| 74 | + if (grammar.$delete) { |
| 75 | + if (lang.$delete) { |
| 76 | + // base also had $delete |
| 77 | + lang.$delete.push(...grammar.$delete); |
86 | 78 | } |
87 | 79 | else { |
88 | | - return cloneToken(value); |
| 80 | + lang.$delete = [...grammar.$delete]; |
89 | 81 | } |
90 | 82 | } |
91 | 83 |
|
92 | | - /** |
93 | | - * @param {string | Grammar} ref |
94 | | - */ |
95 | | - function cloneRef (ref) { |
96 | | - if (ref === id) { |
97 | | - // self ref |
98 | | - return result; |
99 | | - } |
100 | | - else if (typeof ref === 'string') { |
101 | | - return ref; |
102 | | - } |
103 | | - else { |
104 | | - return clone(ref); |
105 | | - } |
| 84 | + if (grammar.$merge) { |
| 85 | + lang.$merge = betterAssign(lang.$merge ?? {}, grammar.$merge); |
106 | 86 | } |
107 | 87 |
|
108 | | - /** |
109 | | - * @param {Grammar} value |
110 | | - */ |
111 | | - function clone (value) { |
112 | | - let mapped = visited.get(value); |
113 | | - if (mapped === undefined) { |
114 | | - mapped = value === grammar ? result : {}; |
115 | | - visited.set(value, mapped); |
116 | | - |
117 | | - // tokens |
118 | | - for (const [key, tokens] of Object.entries(value)) { |
119 | | - mapped[key] = cloneTokens(/** @type {GrammarToken[]} */ (tokens)); |
120 | | - } |
121 | | - |
122 | | - // rest |
123 | | - const r = value.$rest; |
124 | | - if (r != null) { |
125 | | - mapped.$rest = cloneRef(r); |
126 | | - } |
127 | | - |
128 | | - // tokenize |
129 | | - const t = value.$tokenize; |
130 | | - if (t) { |
131 | | - mapped.$tokenize = t; |
132 | | - } |
133 | | - } |
134 | | - return mapped; |
135 | | - } |
136 | | - |
137 | | - return clone(grammar); |
| 88 | + return lang; |
138 | 89 | } |
139 | 90 |
|
140 | 91 | /** |
141 | 92 | * @typedef {import('../types.d.ts').Grammar} Grammar |
142 | | - * @typedef {import('../types.d.ts').GrammarToken} GrammarToken |
143 | | - * @typedef {import('../types.d.ts').GrammarTokens} GrammarTokens |
144 | | - * @typedef {import('../types.d.ts').RegExpLike} RegExpLike |
145 | 93 | */ |
0 commit comments