Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/core/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { kebabToCamelCase } from '../shared/util.js';
import { cloneGrammar } from '../util/extend.js';
import { forEach, toArray } from '../util/iterables.js';
import { extend } from '../util/language-util.js';
import { defineLazyProperty } from '../util/objects.js';

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

// @ts-ignore
const required = [...toArray(proto.require)]; // don't mutate the original array

// @ts-ignore
if (proto.base) {
// @ts-ignore
proto.require = [proto.base, ...toArray(proto.require)];
required.unshift(proto.base);
}

// dependencies
// @ts-ignore
forEach(proto.require, register);
forEach(required, register);

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

const requiredLanguages = toArray(
/** @type {LanguageProto | LanguageProto[] | undefined} */ (entry?.proto.require)
);
const languages = /** @type {Record<string, Grammar>} */ ({});
for (const lang of requiredLanguages) {
defineLazyProperty(languages, lang.id, () => required(lang.id));
}

/** @type {Grammar} */
let evaluatedGrammar;
if (typeof grammar === 'object') {
Expand All @@ -227,10 +239,10 @@ export class Registry {
}
else {
const options = {
getLanguage: required,
getOptionalLanguage: id => this.getLanguage(id),
extend: (id, ref) => extend(required(id), id, ref),
...(baseGrammar && { base: baseGrammar }),
...(requiredLanguages.length && { languages }),
};

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

/**
* @typedef {import('../types.d.ts').ComponentProto} ComponentProto
* @typedef {import('../types.d.ts').LanguageProto} LanguageProto
* @typedef {import('../types.d.ts').Grammar} Grammar
* @typedef {import('./prism.js').Prism} Prism
*/
4 changes: 2 additions & 2 deletions src/languages/apex.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sql from './sql.js';
export default {
id: 'apex',
require: [clike, sql],
grammar ({ getLanguage }) {
grammar ({ languages }) {
const keywords =
/\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;

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

const clike = getLanguage('clike');
const clike = languages.clike;

return {
'comment': clike.comment,
Expand Down
6 changes: 2 additions & 4 deletions src/languages/chaiscript.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ export default {
id: 'chaiscript',
base: clike,
require: cpp,
grammar ({ base, getLanguage }) {
const cpp = getLanguage('cpp');

grammar ({ base, languages }) {
insertBefore(base, 'operator', {
'parameter-type': {
// e.g. def foo(int x, Vector y) {...}
Expand Down Expand Up @@ -68,7 +66,7 @@ export default {
],
'keyword':
/\b(?:attr|auto|break|case|catch|class|continue|def|default|else|finally|for|fun|global|if|return|switch|this|try|var|while)\b/,
'number': [...toArray(cpp.number), /\b(?:Infinity|NaN)\b/],
'number': [...toArray(languages.cpp.number), /\b(?:Infinity|NaN)\b/],
'operator': />>=?|<<=?|\|\||&&|:[:=]?|--|\+\+|[=!<>+\-*/%|&^]=?|[?~]|`[^`\r\n]{1,4}`/,
};
},
Expand Down
7 changes: 3 additions & 4 deletions src/languages/javadoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ export default {
id: 'javadoc',
base: javadoclike,
require: [markup, java],
grammar ({ base, getLanguage }) {
const java = getLanguage('java');
const { tag, entity } = getLanguage('markup');
grammar ({ base, languages }) {
const { tag, entity } = languages.markup;

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

Expand Down Expand Up @@ -45,7 +44,7 @@ export default {
},
},
'class-name': /\b[A-Z]\w*/,
'keyword': java.keyword,
'keyword': languages.java.keyword,
'punctuation': /[#()[\],.]/,
},
},
Expand Down
5 changes: 2 additions & 3 deletions src/languages/jsdoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ export default {
id: 'jsdoc',
base: javadoclike,
require: [javascript, typescript],
grammar ({ base, getLanguage }) {
const javascript = getLanguage('javascript');
const typescript = getLanguage('typescript');
grammar ({ base, languages }) {
const { javascript, typescript } = languages;

const type = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})+\}/.source;
const parameterPrefix = '(@(?:arg|argument|param|property)\\s+(?:' + type + '\\s+)?)';
Expand Down
4 changes: 2 additions & 2 deletions src/languages/xml-doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import markup from './markup.js';
export default {
id: 'xml-doc',
require: markup,
grammar ({ getLanguage }) {
const tag = getLanguage('markup').tag;
grammar ({ languages }) {
const tag = languages.markup.tag;

return {
'slash': {
Expand Down
52 changes: 35 additions & 17 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,10 @@ export type HooksRemove = <Name extends keyof HookEnv>(
export type HooksRun = <Name extends keyof HookEnv>(name: Name, env: HookEnv[Name]) => void;

export interface GrammarOptions {
readonly getLanguage: (id: string) => Grammar;
readonly getOptionalLanguage: (id: string) => Grammar | undefined;
readonly extend: (id: string, ref: GrammarTokens) => Grammar;
}

// Overload for when base is required
export interface GrammarOptionsWithBase extends GrammarOptions {
readonly base: Grammar;
}

export interface ComponentProtoBase<Id extends string = string> {
id: Id;
require?: ComponentProto | readonly ComponentProto[];
Expand All @@ -50,25 +44,49 @@ export interface ComponentProtoBase<Id extends string = string> {
effect?: (Prism: Prism & { plugins: Record<KebabToCamelCase<Id>, {}> }) => () => void;
}

// For languages that extend a base language
export interface LanguageProtoWithBase<Id extends string = string> extends ComponentProtoBase<Id> {
grammar: Grammar | ((options: GrammarOptionsWithBase) => Grammar);
export type LanguageProto<Id extends string = string> =
| LanguageProtoPlain<Id>
| LanguageProtoWithBase<Id>
| LanguageProtoWithRequire<Id>
| LanguageProtoWithBaseAndRequire<Id>;

interface LanguageProtoPlain<Id extends string = string> extends ComponentProtoBase<Id> {
grammar: Grammar | ((options: GrammarOptions) => Grammar);
plugin?: undefined;
base?: never; // Explicitly no base allowed
require?: never; // Explicitly no require allowed
}

interface LanguageProtoWithBase<Id extends string = string> extends ComponentProtoBase<Id> {
grammar: Grammar | ((options: GrammarOptions & { readonly base: Grammar }) => Grammar);
plugin?: undefined;
base: LanguageProto; // Required base
require?: never; // Explicitly no require allowed
}

// For languages that don't extend a base language
export interface LanguageProtoWithoutBase<Id extends string = string>
extends ComponentProtoBase<Id> {
grammar: Grammar | ((options: GrammarOptions) => Grammar);
interface LanguageProtoWithRequire<Id extends string = string> extends ComponentProtoBase<Id> {
grammar:
| Grammar
| ((options: GrammarOptions & { readonly languages: Record<string, Grammar> }) => Grammar);
plugin?: undefined;
base?: never; // Explicitly no base allowed
require: ComponentProto | readonly ComponentProto[]; // Required require
}

// Union type that allows TypeScript to discriminate
export type LanguageProto<Id extends string = string> =
| LanguageProtoWithBase<Id>
| LanguageProtoWithoutBase<Id>;
interface LanguageProtoWithBaseAndRequire<Id extends string = string>
extends ComponentProtoBase<Id> {
grammar:
| Grammar
| ((
options: GrammarOptions & {
readonly base: Grammar;
readonly languages: Record<string, Grammar>;
}
) => Grammar);
plugin?: undefined;
base: LanguageProto; // Required base
require: ComponentProto | readonly ComponentProto[]; // Required require
}

type PluginType<Name extends string> = unknown;
export interface PluginProto<Id extends string = string> extends ComponentProtoBase<Id> {
Expand Down
45 changes: 45 additions & 0 deletions src/util/objects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @template {Record<string, any>} T
* @template {keyof T} K
* @param {T} obj
* @param {K} key
* @param {() => T[K]} getter
* @param {any} [waitFor]
* @returns {Promise<void>}
*/
export async function defineLazyProperty (obj, key, getter, waitFor) {
if (waitFor) {
await waitFor;
}

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
const value = getter.call(this);
// Replace the getter with a writable property
defineSimpleProperty(this, key, value);
return value;
},
set (value) {
defineSimpleProperty(this, key, value);
},
});
}

/**
* @template {Record<string, any>} T
* @template {keyof T} K
* @param {T} obj
* @param {K} key
* @param {T[K]} value
* @returns {void}
*/
export function defineSimpleProperty (obj, key, value) {
Object.defineProperty(obj, key, {
value,
writable: true,
enumerable: true,
configurable: false,
});
}
Loading