Skip to content

Commit 9579fc1

Browse files
Introduce the concept of languages from the simplify branch (#4016)
It allows referencing required languages inside grammar definitions via the `language` property of an options object passed to the `grammar()` function. So, we don't need to expose the `getLanguage()` function to grammar authors. - Don't pass `getLanguage()` to the `grammar()` function - Don't mutate the language proto when the base grammar is specified (firstly, it's a bug, secondly, we don't want to define the base language as a property of the `languages` object—it's for the required languages only) - Add new utility functions for working with objects (authored by @LeaVerou) - Improve the type system to support the updated signature of `grammar()` - Update definitions of languages previously relied on `getLanguage()` Co-authored-by: Lea Verou <[email protected]>
1 parent 6af1a48 commit 9579fc1

File tree

8 files changed

+107
-35
lines changed

8 files changed

+107
-35
lines changed

src/core/registry.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { kebabToCamelCase } from '../shared/util.js';
22
import { cloneGrammar } from '../util/extend.js';
33
import { forEach, toArray } from '../util/iterables.js';
44
import { extend } from '../util/language-util.js';
5+
import { defineLazyProperty } from '../util/objects.js';
56

67
/**
78
* TODO: docs
@@ -79,15 +80,18 @@ export class Registry {
7980
// @ts-ignore - alias is always there
8081
forEach(proto.alias, alias => this.aliasMap.set(alias, id));
8182

83+
// @ts-ignore
84+
const required = [...toArray(proto.require)]; // don't mutate the original array
85+
8286
// @ts-ignore
8387
if (proto.base) {
8488
// @ts-ignore
85-
proto.require = [proto.base, ...toArray(proto.require)];
89+
required.unshift(proto.base);
8690
}
8791

8892
// dependencies
8993
// @ts-ignore
90-
forEach(proto.require, register);
94+
forEach(required, register);
9195

9296
// add plugin namespace
9397
if (proto.plugin) {
@@ -219,6 +223,14 @@ export class Registry {
219223
// We need this so that any code modifying the base grammar doesn't affect other instances
220224
const baseGrammar = base && cloneGrammar(required(base.id), base.id);
221225

226+
const requiredLanguages = toArray(
227+
/** @type {LanguageProto | LanguageProto[] | undefined} */ (entry?.proto.require)
228+
);
229+
const languages = /** @type {Record<string, Grammar>} */ ({});
230+
for (const lang of requiredLanguages) {
231+
defineLazyProperty(languages, lang.id, () => required(lang.id));
232+
}
233+
222234
/** @type {Grammar} */
223235
let evaluatedGrammar;
224236
if (typeof grammar === 'object') {
@@ -227,10 +239,10 @@ export class Registry {
227239
}
228240
else {
229241
const options = {
230-
getLanguage: required,
231242
getOptionalLanguage: id => this.getLanguage(id),
232243
extend: (id, ref) => extend(required(id), id, ref),
233244
...(baseGrammar && { base: baseGrammar }),
245+
...(requiredLanguages.length && { languages }),
234246
};
235247

236248
evaluatedGrammar = grammar(/** @type {any} */ (options));
@@ -253,6 +265,7 @@ export class Registry {
253265

254266
/**
255267
* @typedef {import('../types.d.ts').ComponentProto} ComponentProto
268+
* @typedef {import('../types.d.ts').LanguageProto} LanguageProto
256269
* @typedef {import('../types.d.ts').Grammar} Grammar
257270
* @typedef {import('./prism.js').Prism} Prism
258271
*/

src/languages/apex.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import sql from './sql.js';
55
export default {
66
id: 'apex',
77
require: [clike, sql],
8-
grammar ({ getLanguage }) {
8+
grammar ({ languages }) {
99
const keywords =
1010
/\b(?:abstract|activate|(?:after|before)(?=\s+[a-z])|and|any|array|as|asc|autonomous|begin|bigdecimal|blob|boolean|break|bulk|by|byte|case|cast|catch|char|class|collect|commit|const|continue|currency|date|datetime|decimal|default|delete|desc|do|double|else|end|enum|exception|exit|export|extends|final|finally|float|for|from|get(?=\s*[{};])|global|goto|group|having|hint|if|implements|import|in|inner|insert|instanceof|int|integer|interface|into|join|like|limit|list|long|loop|map|merge|new|not|null|nulls|number|object|of|on|or|outer|override|package|parallel|pragma|private|protected|public|retrieve|return|rollback|select|set|short|sObject|sort|static|string|super|switch|synchronized|system|testmethod|then|this|throw|time|transaction|transient|trigger|try|undelete|update|upsert|using|virtual|void|webservice|when|where|while|(?:inherited|with|without)\s+sharing)\b/i;
1111

@@ -31,7 +31,7 @@ export default {
3131
'punctuation': /[()\[\]{};,:.<>]/,
3232
};
3333

34-
const clike = getLanguage('clike');
34+
const clike = languages.clike;
3535

3636
return {
3737
'comment': clike.comment,

src/languages/chaiscript.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ export default {
88
id: 'chaiscript',
99
base: clike,
1010
require: cpp,
11-
grammar ({ base, getLanguage }) {
12-
const cpp = getLanguage('cpp');
13-
11+
grammar ({ base, languages }) {
1412
insertBefore(base, 'operator', {
1513
'parameter-type': {
1614
// e.g. def foo(int x, Vector y) {...}
@@ -68,7 +66,7 @@ export default {
6866
],
6967
'keyword':
7068
/\b(?:attr|auto|break|case|catch|class|continue|def|default|else|finally|for|fun|global|if|return|switch|this|try|var|while)\b/,
71-
'number': [...toArray(cpp.number), /\b(?:Infinity|NaN)\b/],
69+
'number': [...toArray(languages.cpp.number), /\b(?:Infinity|NaN)\b/],
7270
'operator': />>=?|<<=?|\|\||&&|:[:=]?|--|\+\+|[=!<>+\-*/%|&^]=?|[?~]|`[^`\r\n]{1,4}`/,
7371
};
7472
},

src/languages/javadoc.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ export default {
88
id: 'javadoc',
99
base: javadoclike,
1010
require: [markup, java],
11-
grammar ({ base, getLanguage }) {
12-
const java = getLanguage('java');
13-
const { tag, entity } = getLanguage('markup');
11+
grammar ({ base, languages }) {
12+
const { tag, entity } = languages.markup;
1413

1514
const codeLinePattern = /(^(?:[\t ]*(?:\*\s*)*))[^*\s].*$/m;
1615

@@ -45,7 +44,7 @@ export default {
4544
},
4645
},
4746
'class-name': /\b[A-Z]\w*/,
48-
'keyword': java.keyword,
47+
'keyword': languages.java.keyword,
4948
'punctuation': /[#()[\],.]/,
5049
},
5150
},

src/languages/jsdoc.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ export default {
88
id: 'jsdoc',
99
base: javadoclike,
1010
require: [javascript, typescript],
11-
grammar ({ base, getLanguage }) {
12-
const javascript = getLanguage('javascript');
13-
const typescript = getLanguage('typescript');
11+
grammar ({ base, languages }) {
12+
const { javascript, typescript } = languages;
1413

1514
const type = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})+\}/.source;
1615
const parameterPrefix = '(@(?:arg|argument|param|property)\\s+(?:' + type + '\\s+)?)';

src/languages/xml-doc.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import markup from './markup.js';
44
export default {
55
id: 'xml-doc',
66
require: markup,
7-
grammar ({ getLanguage }) {
8-
const tag = getLanguage('markup').tag;
7+
grammar ({ languages }) {
8+
const tag = languages.markup.tag;
99

1010
return {
1111
'slash': {

src/types.d.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,10 @@ export type HooksRemove = <Name extends keyof HookEnv>(
3232
export type HooksRun = <Name extends keyof HookEnv>(name: Name, env: HookEnv[Name]) => void;
3333

3434
export interface GrammarOptions {
35-
readonly getLanguage: (id: string) => Grammar;
3635
readonly getOptionalLanguage: (id: string) => Grammar | undefined;
3736
readonly extend: (id: string, ref: GrammarTokens) => Grammar;
3837
}
3938

40-
// Overload for when base is required
41-
export interface GrammarOptionsWithBase extends GrammarOptions {
42-
readonly base: Grammar;
43-
}
44-
4539
export interface ComponentProtoBase<Id extends string = string> {
4640
id: Id;
4741
require?: ComponentProto | readonly ComponentProto[];
@@ -50,25 +44,49 @@ export interface ComponentProtoBase<Id extends string = string> {
5044
effect?: (Prism: Prism & { plugins: Record<KebabToCamelCase<Id>, {}> }) => () => void;
5145
}
5246

53-
// For languages that extend a base language
54-
export interface LanguageProtoWithBase<Id extends string = string> extends ComponentProtoBase<Id> {
55-
grammar: Grammar | ((options: GrammarOptionsWithBase) => Grammar);
47+
export type LanguageProto<Id extends string = string> =
48+
| LanguageProtoPlain<Id>
49+
| LanguageProtoWithBase<Id>
50+
| LanguageProtoWithRequire<Id>
51+
| LanguageProtoWithBaseAndRequire<Id>;
52+
53+
interface LanguageProtoPlain<Id extends string = string> extends ComponentProtoBase<Id> {
54+
grammar: Grammar | ((options: GrammarOptions) => Grammar);
55+
plugin?: undefined;
56+
base?: never; // Explicitly no base allowed
57+
require?: never; // Explicitly no require allowed
58+
}
59+
60+
interface LanguageProtoWithBase<Id extends string = string> extends ComponentProtoBase<Id> {
61+
grammar: Grammar | ((options: GrammarOptions & { readonly base: Grammar }) => Grammar);
5662
plugin?: undefined;
5763
base: LanguageProto; // Required base
64+
require?: never; // Explicitly no require allowed
5865
}
5966

60-
// For languages that don't extend a base language
61-
export interface LanguageProtoWithoutBase<Id extends string = string>
62-
extends ComponentProtoBase<Id> {
63-
grammar: Grammar | ((options: GrammarOptions) => Grammar);
67+
interface LanguageProtoWithRequire<Id extends string = string> extends ComponentProtoBase<Id> {
68+
grammar:
69+
| Grammar
70+
| ((options: GrammarOptions & { readonly languages: Record<string, Grammar> }) => Grammar);
6471
plugin?: undefined;
6572
base?: never; // Explicitly no base allowed
73+
require: ComponentProto | readonly ComponentProto[]; // Required require
6674
}
6775

68-
// Union type that allows TypeScript to discriminate
69-
export type LanguageProto<Id extends string = string> =
70-
| LanguageProtoWithBase<Id>
71-
| LanguageProtoWithoutBase<Id>;
76+
interface LanguageProtoWithBaseAndRequire<Id extends string = string>
77+
extends ComponentProtoBase<Id> {
78+
grammar:
79+
| Grammar
80+
| ((
81+
options: GrammarOptions & {
82+
readonly base: Grammar;
83+
readonly languages: Record<string, Grammar>;
84+
}
85+
) => Grammar);
86+
plugin?: undefined;
87+
base: LanguageProto; // Required base
88+
require: ComponentProto | readonly ComponentProto[]; // Required require
89+
}
7290

7391
type PluginType<Name extends string> = unknown;
7492
export interface PluginProto<Id extends string = string> extends ComponentProtoBase<Id> {

src/util/objects.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @template {Record<string, any>} T
3+
* @template {keyof T} K
4+
* @param {T} obj
5+
* @param {K} key
6+
* @param {() => T[K]} getter
7+
* @param {any} [waitFor]
8+
* @returns {Promise<void>}
9+
*/
10+
export async function defineLazyProperty (obj, key, getter, waitFor) {
11+
if (waitFor) {
12+
await waitFor;
13+
}
14+
15+
Object.defineProperty(obj, key, {
16+
enumerable: true,
17+
configurable: true,
18+
get () {
19+
const value = getter.call(this);
20+
// Replace the getter with a writable property
21+
defineSimpleProperty(this, key, value);
22+
return value;
23+
},
24+
set (value) {
25+
defineSimpleProperty(this, key, value);
26+
},
27+
});
28+
}
29+
30+
/**
31+
* @template {Record<string, any>} T
32+
* @template {keyof T} K
33+
* @param {T} obj
34+
* @param {K} key
35+
* @param {T[K]} value
36+
* @returns {void}
37+
*/
38+
export function defineSimpleProperty (obj, key, value) {
39+
Object.defineProperty(obj, key, {
40+
value,
41+
writable: true,
42+
enumerable: true,
43+
configurable: false,
44+
});
45+
}

0 commit comments

Comments
 (0)