diff --git a/packages/melonjs/src/loader/loader.js b/packages/melonjs/src/loader/loader.js index d241dd311..fd2cc46bc 100644 --- a/packages/melonjs/src/loader/loader.js +++ b/packages/melonjs/src/loader/loader.js @@ -522,6 +522,12 @@ export function load(asset, onload, onerror) { initParsers(); } + const parser = parsers.get(asset.type); + + if (typeof parser === "undefined") { + throw new Error("load : unknown or invalid resource type : " + asset.type); + } + // Resolve the effective src WITHOUT mutating the caller's asset // descriptor: load() used to write the transformed url back into // asset.src, so retrying the same object — loader.reload() after a @@ -531,20 +537,17 @@ export function load(asset, onload, onerror) { // caller's original src; only the parser sees the resolved one. let src = asset.src; - // strip url() wrapper for fontface assets so baseURL can be prepended to the raw path - if (asset.type === "fontface" && typeof src === "string") { - const urlMatch = src.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/); - if (urlMatch) { - src = urlMatch[1]; - } + // Let the parser normalize its source before applying the shared base URL. + if (typeof src === "string") { + src = parser.resolveSrc?.(src) ?? src; } - // transform the url if necessary (skip for local() font sources and data URIs) + // Data URIs and parser-specific sources do not need a base URL. if ( typeof baseURL[asset.type] !== "undefined" && typeof src === "string" && - !src.startsWith("local(") && - !src.startsWith("data:") + !src.startsWith("data:") && + !parser.skipBaseURL?.(src) ) { src = baseURL[asset.type] + src; } @@ -552,12 +555,6 @@ export function load(asset, onload, onerror) { const resource = src === asset.src ? asset : Object.assign({}, asset, { src }); - const parser = parsers.get(asset.type); - - if (typeof parser === "undefined") { - throw new Error("load : unknown or invalid resource type : " + asset.type); - } - const settings = { nocache: nocache, crossOrigin: crossOrigin, diff --git a/packages/melonjs/src/loader/parsers/fontface.js b/packages/melonjs/src/loader/parsers/fontface.js index 80f490fb7..04fdb7495 100644 --- a/packages/melonjs/src/loader/parsers/fontface.js +++ b/packages/melonjs/src/loader/parsers/fontface.js @@ -70,3 +70,26 @@ export function preloadFontFace(data, onload, onerror) { return 1; } + +/** + * Unwrap a CSS URL before the loader prefixes the font's base URL. + * @param {string} src - font source descriptor + * @returns {string} source path or unchanged descriptor + * @ignore + * @internal + */ +preloadFontFace.resolveSrc = (src) => { + const urlMatch = src.match(/^url\(\s*['"]?(.*?)['"]?\s*\)$/); + return urlMatch ? urlMatch[1] : src; +}; + +/** + * Installed font names are not paths relative to the asset base URL. + * @param {string} src - font source descriptor + * @returns {boolean} whether the base URL should be skipped + * @ignore + * @internal + */ +preloadFontFace.skipBaseURL = (src) => { + return src.startsWith("local("); +}; diff --git a/packages/melonjs/tests/loader-src-resolution.spec.js b/packages/melonjs/tests/loader-src-resolution.spec.js new file mode 100644 index 000000000..f4f434953 --- /dev/null +++ b/packages/melonjs/tests/loader-src-resolution.spec.js @@ -0,0 +1,47 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { boot, loader } from "../src/index.js"; +import { preloadFontFace } from "../src/loader/parsers/fontface.js"; + +describe("parser-owned source resolution", () => { + const type = "font-source-probe"; + let received; + + beforeAll(() => { + boot(); + const parser = (asset, onload) => { + received = asset.src; + onload(); + return 1; + }; + // Use the font parser's rules under another type, so resolution cannot + // depend on the loader recognizing the name "fontface". + Object.assign(parser, { + resolveSrc: preloadFontFace.resolveSrc, + skipBaseURL: preloadFontFace.skipBaseURL, + }); + loader.setParser(type, parser); + loader.setBaseURL(type, "fonts/"); + }); + + afterAll(() => { + loader.setBaseURL(type, ""); + }); + + it.each([ + ["test.woff2", "fonts/test.woff2"], + ["url(test.woff2)", "fonts/test.woff2"], + ["url('test.woff2')", "fonts/test.woff2"], + ['url( "Test Font.woff2" )', "fonts/Test Font.woff2"], + ["local('Test Font')", "local('Test Font')"], + ["data:font/woff2;base64,AA==", "data:font/woff2;base64,AA=="], + ["url('data:font/woff2;base64,AA==')", "data:font/woff2;base64,AA=="], + ])("resolves %s without changing the manifest", async (src, expected) => { + const asset = Object.freeze({ name: "source-probe", type, src }); + await loader.load(asset); + expect(received).toBe(expected); + expect(asset.src).toBe(src); + // Retrying the same entry must not prepend the base URL twice. + await loader.load(asset); + expect(received).toBe(expected); + }); +}); diff --git a/packages/melonjs/tests/loader.spec.js b/packages/melonjs/tests/loader.spec.js index 0db9b9535..b60412faf 100644 --- a/packages/melonjs/tests/loader.spec.js +++ b/packages/melonjs/tests/loader.spec.js @@ -1,6 +1,7 @@ import { beforeAll, describe, expect, it } from "vitest"; import { audio, boot, event, loader } from "../src/index.js"; import { fontList, videoList } from "../src/loader/cache.js"; +import { preloadFontFace } from "../src/loader/parsers/fontface.js"; describe("loader", () => { let audioURI; @@ -275,14 +276,15 @@ describe("loader", () => { loader.setBaseURL("fontface", "assets/"); const receivedSrc = []; - // stub fontface parser to capture the resolved src - loader.setParser("fontface", (data, onload) => { + // Capture the resolved src while retaining the font parser's rules. + const parser = (data, onload) => { receivedSrc.push(data.src); if (typeof onload === "function") { onload(); } return 1; - }); + }; + loader.setParser("fontface", Object.assign(parser, preloadFontFace)); // plain path loader.load( @@ -307,6 +309,7 @@ describe("loader", () => { // reset loader.setBaseURL("fontface", "./"); + loader.setParser("fontface", preloadFontFace); }); it("should not strip local() wrapper from fontface src", () => { @@ -314,13 +317,14 @@ describe("loader", () => { loader.setBaseURL("fontface", "./"); const receivedSrc = []; - loader.setParser("fontface", (data, onload) => { + const parser = (data, onload) => { receivedSrc.push(data.src); if (typeof onload === "function") { onload(); } return 1; - }); + }; + loader.setParser("fontface", Object.assign(parser, preloadFontFace)); loader.load( { name: "font4", type: "fontface", src: "local('My Font')" }, @@ -328,6 +332,7 @@ describe("loader", () => { ); expect(receivedSrc[0]).toBe("local('My Font')"); + loader.setParser("fontface", preloadFontFace); }); it("should configure loader options", () => {