Skip to content
Open
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
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
node-version: [20.x, 22.x, 24.x]

steps:
- uses: actions/checkout@v3
Expand All @@ -31,7 +31,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
node-version: [20.x, 22.x, 24.x]

steps:
- uses: actions/checkout@v3
Expand All @@ -47,10 +47,10 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- run: npm ci
- run: npm run lint:ci

Expand All @@ -59,9 +59,9 @@ jobs:

steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
- run: npm ci
- run: npm run regex-coverage
2 changes: 1 addition & 1 deletion benchmark/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@

// file path
const hash = crypto.createHash('md5').update(uri).digest('hex');
const localPath = path.resolve(downloadDir, hash + '-' + /[-\w\.]*$/.exec(uri)[0]);

Check warning on line 327 in benchmark/benchmark.js

View workflow job for this annotation

GitHub Actions / lint

Unnecessary escape character: \.

if (!fs.existsSync(localPath)) {
// download file
Expand Down Expand Up @@ -422,7 +422,7 @@
function createTestFunction (Prism, mainLanguage, testFunction) {
if (testFunction === 'tokenize') {
return code => {
const grammar = Prism.components.getLanguage(mainLanguage);
const grammar = Prism.languageRegistry.getLanguage(mainLanguage)?.resolvedGrammar;
Prism.tokenize(code, grammar);
};
}
Expand Down
1 change: 1 addition & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
renderChunk (code) {
const str = new MagicString(code);
str.replace(
/\/((?:[^\n\r[\\\/]|\\.|\[(?:[^\n\r\\\]]|\\.)*\])+)\/\s*\.\s*source\b/g,

Check warning on line 253 in scripts/build.js

View workflow job for this annotation

GitHub Actions / lint

Unnecessary escape character: \/
(m, /** @type {string} */ source) => {
// escape backslashes
source = source.replace(
Expand Down Expand Up @@ -297,7 +297,7 @@
renderChunk (code) {
const str = new MagicString(code);
str.replace(
/^(?<indent>[ \t]+)grammar: (\{[\s\S]*?^\k<indent>\})/m,

Check warning on line 300 in scripts/build.js

View workflow job for this annotation

GitHub Actions / lint

The quantifier '[\s\S]*?' is always entered despite having a minimum of 0. This is because the assertion '^' contradicts with the element(s) after the quantifier. Either set the minimum to 1 (+?) or change the assertion
(m, _, /** @type {string} */ grammar) => `\tgrammar: () => (${grammar})`
);
return toRenderedChunk(str);
Expand Down Expand Up @@ -384,6 +384,7 @@
async function buildJS () {
const input = {
'index': path.join(SRC_DIR, 'index.js'),
'global': path.join(SRC_DIR, 'global.js'),
'prism': path.join(SRC_DIR, 'prism.global.js'),
'shared': path.join(SRC_DIR, 'shared.js'),
};
Expand Down
11 changes: 1 addition & 10 deletions src/auto-start.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import Prism from './global.js';
import autoloader from './plugins/autoloader/autoloader.js';
import { documentReady } from './util/async.js';

Prism.components.add(autoloader);

documentReady().then(() => {
if (!Prism.config.manual) {
Prism.highlightAll();
}
});
import './plugins/autoloader/autoloader.js';

export default Prism;
28 changes: 25 additions & 3 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const globalConfig = globalThis.Prism?.constructor?.name === 'Object' ? globalTh

/**
* @param {string} name
* @returns {string | boolean | null | undefined}
*/
function getGlobalSetting (name) {
// eslint-disable-next-line regexp/no-unused-capturing-group
Expand Down Expand Up @@ -43,16 +42,39 @@ function getGlobalBooleanSetting (name, defaultValue) {
return !(value === false || value === 'false');
}

/**
* @param {string} name
* @returns {string[]}
*/
function getGlobalArraySetting (name) {
const value = getGlobalSetting(name);
if (value === null || value === undefined || value === false || value === 'false') {
return [];
}
else if (typeof value === 'string') {
return value.split(',').map(s => s.trim());
}
else if (Array.isArray(value)) {
return value;
}

return [];
}

/**
* @type {PrismConfig}
*/
export const globalDefaults = {
manual: getGlobalBooleanSetting('manual', !hasDOM),
silent: getGlobalBooleanSetting('silent', false),
languages: getGlobalArraySetting('languages'),
plugins: getGlobalArraySetting('plugins'),
languagePath: /** @type {string} */ (getGlobalSetting('language-path') ?? './languages/'),
pluginPath: /** @type {string} */ (getGlobalSetting('plugin-path') ?? './plugins/'),
};

export default globalDefaults;

/**
* @typedef {import('./types.d.ts').PrismConfig} PrismConfig
* @typedef {import('./types.d.ts').GlobalConfig} GlobalConfig
* @import { PrismConfig, GlobalConfig } from './types.d.ts';
*/
237 changes: 237 additions & 0 deletions src/core/classes/component-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import { allSettled } from '../../util/async.js';

/**
* @template {ComponentProto} T
*/
export default class ComponentRegistry extends EventTarget {
static type = 'unknown';

/**
* All imported components.
*
* @type {Record<string, T>}
*/
cache = {};

/**
* All components that are currently being loaded.
*
* @type {Record<string, Promise<T>>}
*/
loading = {};

/**
* Same data as in loading, but as an array, used for aggregate promises.
* IMPORTANT: Do NOT overwrite this array, only modify its contents.
*
* @type {Promise<T>[]}
*/
#loadingList = [];

/**
* @type {Promise<T[]>}
*/
ready;

/**
* Path to the components, used for loading.
*
* @type {string}
*/
path;

/**
* A reference to the Prism instance.
*
* @type {Prism}
*/
prism;

/**
* @type {ComponentRegistryOptions}
*/
options;

/**
*
* @param {ComponentRegistryOptions} options
*/
constructor (options) {
super();

this.options = options;
let { path, preload, prism } = options;

this.prism = prism;

path = path.endsWith('/') ? path : path + '/';
this.path = path;

if (preload) {
void this.loadAll(preload);
}

this.ready = /** @type {Promise<T[]>} */ (allSettled(this.#loadingList));
}

/**
* Returns the component if it is already loaded or a promise that resolves when it is loaded,
* without triggering a load like `load()` would.
*
* @param {string} id
* @returns {Promise<T>}
*/
async whenDefined (id) {
if (this.cache[id]) {
// Already loaded
return this.cache[id];
}

if (this.loading[id] !== undefined) {
// Already loading
return this.loading[id];
}

const Self = /** @type {typeof ComponentRegistry} */ (this.constructor);
return new Promise(resolve => {
/**
* @param {CustomEvent<AddEventPayload<T>>} e
*/
const handler = e => {
if (e.detail.id === id) {
resolve(e.detail.component);
this.removeEventListener('add', /** @type {EventListener} */ (handler));
}
};
this.addEventListener('add' + Self.type, /** @type {EventListener} */ (handler));
});
}

/**
* Add a component to the registry.
*
* @param {T} def Component
* @param {string} [id=def.id] Component id
* @param {object} [options] Options
* @param {boolean} [options.force] Force add the component even if it is already present
* @returns {boolean} true if the component was added, false if it was already present
*/
add (def, id = def.id, options) {
const Self = /** @type {typeof ComponentRegistry} */ (this.constructor);

if (typeof this.loading[id] !== 'undefined') {
// If it was loading, remove it from the loading list
const index = this.#loadingList.indexOf(this.loading[id]);
if (index > -1) {
this.#loadingList.splice(index, 1);
}

delete this.loading[id];
}

if (!this.cache[id] || options?.force) {
this.cache[id] = def;

this.dispatchEvent(
/** @type {CustomEvent<AddEventPayload<T>>} */
new CustomEvent('add', {
detail: { id, type: Self.type, component: def },
})
);

this.dispatchEvent(
/** @type {CustomEvent<AddEventPayload<T>>} */
new CustomEvent('add' + Self.type, {
detail: { id, component: def },
})
);

return true;
}

return false;
}

/**
*
* @param {string} id
* @returns {boolean}
*/
has (id) {
return this.cache[id] !== undefined;
}

/**
*
* @param {string} id
* @returns {T | null}
*/
get (id) {
return this.cache[id] ?? null;
}

/**
*
* @param {string} id
* @returns {T | Promise<T | null>}
*/
load (id) {
if (this.cache[id]) {
return this.cache[id];
}

if (this.loading[id] !== undefined) {
// Already loading
return this.loading[id];
}

const loadingComponent = import(this.path + id + '.js')
.then(m => {
/** @type {T} */
const component = m.default ?? m;
this.add(component, id);
return component;
})
.catch(error => {
console.error(error);
return null;
});

this.loading[id] = /** @type {Promise<T>} */ (loadingComponent);
this.#loadingList.push(/** @type {Promise<T>} */ (loadingComponent));
return loadingComponent;
}

/**
*
* @param {string[]} ids
* @returns {(T | Promise<T | null>)[]}
*/
loadAll (ids) {
if (!Array.isArray(ids)) {
ids = [ids];
}

return ids.map(id => this.load(id));
}
}

/**
* @import {Prism} from '../prism.js'
* @import {ComponentProto} from '../../types.d.ts'
*/

/**
* @typedef {object} ComponentRegistryOptions
* @property {string} path Path to the components
* @property {string[]} [preload] List of component ids to preload
* @property {Prism} prism A reference to the Prism instance
*/

/**
* @template {ComponentProto} T
* @typedef {object} AddEventPayload
* @property {string} id
* @property {string} [type]
* @property {T} component
*/
Loading
Loading