Skip to content

Commit d2a6424

Browse files
Introduce the concept of base language from the simplify branch (#3990)
* Introduce the concept of base language Co-authored-by: Lea Verou <[email protected]> * Improve the type system So that authors defining a language shouldn’t add `!` everywhere they refer to `base`. --------- Co-authored-by: Lea Verou <[email protected]>
1 parent 31b467f commit d2a6424

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

src/core/registry.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { kebabToCamelCase } from '../shared/util';
2-
import { forEach } from '../util/iterables';
2+
import { cloneGrammar } from '../util/extend';
3+
import { forEach, toArray } from '../util/iterables';
34
import { extend } from '../util/language-util';
4-
import type { ComponentProto, Grammar } from '../types';
5+
import type {
6+
ComponentProto,
7+
Grammar,
8+
GrammarOptions,
9+
GrammarOptionsWithBase,
10+
LanguageProto,
11+
} from '../types';
512
import type { Prism } from './prism';
613

714
interface Entry {
@@ -60,6 +67,13 @@ export class Registry {
6067
// add aliases
6168
forEach(proto.alias, alias => this.aliasMap.set(alias, id));
6269

70+
if ((proto as LanguageProto).base) {
71+
proto.require = [
72+
(proto as LanguageProto).base as ComponentProto,
73+
...toArray(proto.require),
74+
];
75+
}
76+
6377
// dependencies
6478
forEach(proto.require, register);
6579

@@ -152,11 +166,6 @@ export class Registry {
152166
return entry.evaluatedGrammar;
153167
}
154168

155-
if (typeof grammar === 'object') {
156-
// the grammar is a simple object, so we don't need to evaluate it
157-
return (entry.evaluatedGrammar = grammar);
158-
}
159-
160169
const required = (id: string): Grammar => {
161170
const grammar = this.getLanguage(id);
162171
if (!grammar) {
@@ -165,10 +174,33 @@ export class Registry {
165174
return grammar;
166175
};
167176

168-
return (entry.evaluatedGrammar = grammar({
169-
getLanguage: required,
170-
getOptionalLanguage: id => this.getLanguage(id),
171-
extend: (id, ref) => extend(required(id), id, ref),
172-
}));
177+
const base = (entry?.proto as LanguageProto).base;
178+
// We need this so that any code modifying the base grammar doesn't affect other instances
179+
const baseGrammar = base && cloneGrammar(required(base.id), base.id);
180+
181+
let evaluatedGrammar: Grammar;
182+
if (typeof grammar === 'object') {
183+
// if the grammar is an object, we can use it directly
184+
evaluatedGrammar = grammar;
185+
}
186+
else {
187+
const options: GrammarOptions = {
188+
getLanguage: required,
189+
getOptionalLanguage: id => this.getLanguage(id),
190+
extend: (id, ref) => extend(required(id), id, ref),
191+
...(baseGrammar && { base: baseGrammar }),
192+
};
193+
194+
const grammarFn = grammar as (
195+
options: GrammarOptions | GrammarOptionsWithBase
196+
) => Grammar;
197+
evaluatedGrammar = grammarFn(options);
198+
}
199+
200+
if (baseGrammar) {
201+
evaluatedGrammar = extend(baseGrammar, base.id, evaluatedGrammar);
202+
}
203+
204+
return (entry.evaluatedGrammar = evaluatedGrammar);
173205
}
174206
}

src/types.d.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
1-
import type { Prism } from './core/prism';
21
import type { TokenStream } from './core/classes/token';
2+
import type { Prism } from './core/prism';
33

44
export interface GrammarOptions {
55
readonly getLanguage: (id: string) => Grammar;
66
readonly getOptionalLanguage: (id: string) => Grammar | undefined;
77
readonly extend: (id: string, ref: GrammarTokens) => Grammar;
88
}
9+
10+
// Overload for when base is required
11+
export interface GrammarOptionsWithBase extends GrammarOptions {
12+
readonly base: Grammar;
13+
}
14+
915
export interface ComponentProtoBase<Id extends string = string> {
1016
id: Id;
1117
require?: ComponentProto | readonly ComponentProto[];
1218
optional?: string | readonly string[];
1319
alias?: string | readonly string[];
1420
effect?: (Prism: Prism & { plugins: Record<KebabToCamelCase<Id>, {}> }) => () => void;
1521
}
16-
export interface LanguageProto<Id extends string = string> extends ComponentProtoBase<Id> {
22+
23+
// For languages that extend a base language
24+
export interface LanguageProtoWithBase<Id extends string = string> extends ComponentProtoBase<Id> {
25+
grammar: Grammar | ((options: GrammarOptionsWithBase) => Grammar);
26+
plugin?: undefined;
27+
base: LanguageProto; // Required base
28+
}
29+
30+
// For languages that don't extend a base language
31+
export interface LanguageProtoWithoutBase<Id extends string = string>
32+
extends ComponentProtoBase<Id> {
1733
grammar: Grammar | ((options: GrammarOptions) => Grammar);
1834
plugin?: undefined;
35+
base?: never; // Explicitly no base allowed
1936
}
37+
38+
// Union type that allows TypeScript to discriminate
39+
export type LanguageProto<Id extends string = string> =
40+
| LanguageProtoWithBase<Id>
41+
| LanguageProtoWithoutBase<Id>;
42+
2043
type PluginType<Name extends string> = unknown;
2144
export interface PluginProto<Id extends string = string> extends ComponentProtoBase<Id> {
2245
grammar?: undefined;

src/util/extend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function extend (grammar: Grammar, id: string, reDef: Grammar): Grammar {
3838
return lang;
3939
}
4040

41-
function cloneGrammar (grammar: Grammar, id: string): Grammar {
41+
export function cloneGrammar (grammar: Grammar, id: string): Grammar {
4242
const result: Grammar = {};
4343

4444
const visited = new Map<Grammar, Grammar>();

tests/core/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { assert } from 'chai';
22
import { Prism } from '../../src/core/prism';
3+
import type { GrammarOptions } from '../../src/types';
34

45
describe('Registry', () => {
56
it('should resolve aliases', () => {
@@ -26,7 +27,7 @@ describe('Registry', () => {
2627
components.add({
2728
id: 'c',
2829
optional: 'b',
29-
grammar ({ getOptionalLanguage }) {
30+
grammar ({ getOptionalLanguage }: GrammarOptions) {
3031
return getOptionalLanguage('b') ?? {};
3132
},
3233
});

0 commit comments

Comments
 (0)