Skip to content

Commit c2b8d73

Browse files
Improve the type system
So that authors defining a language shouldn’t add `!` everywhere they refer to `base`.
1 parent d1f28a5 commit c2b8d73

File tree

3 files changed

+54
-17
lines changed

3 files changed

+54
-17
lines changed

src/core/registry.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { kebabToCamelCase } from '../shared/util';
22
import { cloneGrammar } from '../util/extend';
33
import { forEach, toArray } from '../util/iterables';
44
import { extend } from '../util/language-util';
5-
import type { ComponentProto, Grammar, LanguageProto } from '../types';
5+
import type {
6+
ComponentProto,
7+
Grammar,
8+
GrammarOptions,
9+
GrammarOptionsWithBase,
10+
LanguageProto,
11+
} from '../types';
612
import type { Prism } from './prism';
713

814
interface Entry {
@@ -172,18 +178,27 @@ export class Registry {
172178
// We need this so that any code modifying the base grammar doesn't affect other instances
173179
const baseGrammar = base && cloneGrammar(required(base.id), base.id);
174180

175-
let evaluatedGrammar =
176-
typeof grammar === 'object'
177-
? grammar
178-
: grammar({
179-
base: baseGrammar,
180-
getLanguage: required,
181-
getOptionalLanguage: id => this.getLanguage(id),
182-
extend: (id, ref) => extend(required(id), id, ref),
183-
});
184-
185-
if (base) {
186-
evaluatedGrammar = extend(baseGrammar!, base.id, evaluatedGrammar);
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);
187202
}
188203

189204
return (entry.evaluatedGrammar = evaluatedGrammar);

src/types.d.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,43 @@ import type { Prism } from './core/prism';
33

44
export interface GrammarOptions {
55
readonly getLanguage: (id: string) => Grammar;
6-
readonly base?: Grammar;
76
readonly getOptionalLanguage: (id: string) => Grammar | undefined;
87
readonly extend: (id: string, ref: GrammarTokens) => Grammar;
98
}
9+
10+
// Overload for when base is required
11+
export interface GrammarOptionsWithBase extends GrammarOptions {
12+
readonly base: Grammar;
13+
}
14+
1015
export interface ComponentProtoBase<Id extends string = string> {
1116
id: Id;
1217
require?: ComponentProto | readonly ComponentProto[];
1318
optional?: string | readonly string[];
1419
alias?: string | readonly string[];
1520
effect?: (Prism: Prism & { plugins: Record<KebabToCamelCase<Id>, {}> }) => () => void;
1621
}
17-
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> {
1833
grammar: Grammar | ((options: GrammarOptions) => Grammar);
1934
plugin?: undefined;
20-
base?: LanguageProto;
35+
base?: never; // Explicitly no base allowed
2136
}
37+
38+
// Union type that allows TypeScript to discriminate
39+
export type LanguageProto<Id extends string = string> =
40+
| LanguageProtoWithBase<Id>
41+
| LanguageProtoWithoutBase<Id>;
42+
2243
type PluginType<Name extends string> = unknown;
2344
export interface PluginProto<Id extends string = string> extends ComponentProtoBase<Id> {
2445
grammar?: undefined;

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)