diff --git a/third_party/muya/src/utils/prism/__tests__/loadLanguageDependencyOrder.spec.ts b/third_party/muya/src/utils/prism/__tests__/loadLanguageDependencyOrder.spec.ts new file mode 100644 index 0000000..3a11563 --- /dev/null +++ b/third_party/muya/src/utils/prism/__tests__/loadLanguageDependencyOrder.spec.ts @@ -0,0 +1,37 @@ +// @vitest-environment happy-dom + +import { afterEach, describe, expect, it, vi } from 'vitest'; +import prism, { loadLanguage } from '../index'; +import { loadedLanguages } from '../loadLanguage'; + +function resetCppState() { + for (const lang of ['c', 'cpp']) { + delete (prism.languages as Record)[lang]; + loadedLanguages.delete(lang); + } +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('loadLanguage dependency order', () => { + it('loads a dependency before the dependent grammar that extends it', async () => { + resetCppState(); + + const addOrder: string[] = []; + const realAdd = loadedLanguages.add.bind(loadedLanguages); + vi.spyOn(loadedLanguages, 'add').mockImplementation((lang: string) => { + addOrder.push(lang); + return realAdd(lang); + }); + + await expect(loadLanguage('cpp')).resolves.toBeDefined(); + + expect(addOrder).toContain('c'); + expect(addOrder).toContain('cpp'); + expect(addOrder.indexOf('c')).toBeLessThan(addOrder.indexOf('cpp')); + expect(prism.languages.cpp).toBeTruthy(); + expect(() => prism.tokenize('int main(){}', prism.languages.cpp)).not.toThrow(); + }); +}); diff --git a/third_party/muya/src/utils/prism/loadLanguage.ts b/third_party/muya/src/utils/prism/loadLanguage.ts index bfcbc59..739acbd 100644 --- a/third_party/muya/src/utils/prism/loadLanguage.ts +++ b/third_party/muya/src/utils/prism/loadLanguage.ts @@ -1,6 +1,5 @@ import components from 'prismjs/components.js'; import getLoader from 'prismjs/dependencies'; -import { getDefer } from '../index'; interface ILangLoadStatus { lang: string; @@ -19,10 +18,28 @@ export const loadedLanguages = new Set([ ]); const { languages } = components; -const languageModules = import.meta.glob([ - '../../../../../prismjs/components/prism-*.js', - '!../../../../../prismjs/components/prism-*.min.js', -]) as Record Promise>; +// Two glob shapes on purpose: the importer path differs between Muya's own +// vitest/build context (three levels up to third_party/muya/node_modules) and +// the Android app build, where this file compiles from the installed +// @muyajs/core copy and prismjs resolves five levels up beside it in the +// pnpm store. Each shape matches nothing in the other context, so keep them +// as separate globs (one array poisons resolution for both) and merge. +const languageModules = { + ...import.meta.glob([ + '../../../../../prismjs/components/prism-*.js', + '!../../../../../prismjs/components/prism-*.min.js', + ]), + ...import.meta.glob([ + '../../../node_modules/prismjs/components/prism-*.js', + '!../../../node_modules/prismjs/components/prism-*.min.js', + ]), +} as Record Promise>; + +function getLanguageModule(lang: string) { + const modulePath = `../../../node_modules/prismjs/components/prism-${lang}.js`; + return languageModules[modulePath] + ?? Object.entries(languageModules).find(([path]) => path.endsWith(`/prism-${lang}.js`))?.[1]; +} // Look for the origin language by alias export function transformAliasToOrigin(langs: string[]) { @@ -81,43 +98,38 @@ function initLoadLanguage(Prism: IPrismLike) { if (!Array.isArray(langs)) langs = [langs]; - const promises: Promise[] = []; + const statuses: ILangLoadStatus[] = []; // The user might have loaded languages via some other way or used `prism.js` which already includes some // We don't need to validate the ids because `getLoader` will ignore invalid ones const loaded = [...loadedLanguages, ...Object.keys(Prism.languages)]; - getLoader(components, langs, loaded).load(async (lang: string) => { - const defer = getDefer(); - promises.push(defer.promise); + const loadComponent = async (lang: string): Promise => { if (!(lang in components.languages)) { - defer.resolve({ - lang, - status: 'noexist', - }); + statuses.push({ lang, status: 'noexist' }); + return; } - else if (loadedLanguages.has(lang)) { - defer.resolve({ - lang, - status: 'cached', - }); + if (loadedLanguages.has(lang)) { + statuses.push({ lang, status: 'cached' }); + return; } - else { - delete Prism.languages[lang]; - const modulePath = `../../../../../prismjs/components/prism-${lang}.js`; - const loadLanguageModule = languageModules[modulePath]; - if (!loadLanguageModule) { - throw new Error(`Prism language module not found: ${lang}`); - } - await loadLanguageModule(); - defer.resolve({ - lang, - status: 'loaded', - }); - loadedLanguages.add(lang); + delete Prism.languages[lang]; + const loadLanguageModule = getLanguageModule(lang); + if (!loadLanguageModule) { + throw new Error(`Prism language module not found: ${lang}`); } + + await loadLanguageModule(); + loadedLanguages.add(lang); + statuses.push({ lang, status: 'loaded' }); + }; + + await getLoader(components, langs, loaded).load(loadComponent, { + series: (before: Promise, after: () => Promise) => + before.then(after), + parallel: (values: Promise[]) => Promise.all(values), }); - return Promise.all(promises); + return statuses; }; }