diff --git a/src/markdown.ts b/src/markdown.ts index c582277b8..4325104db 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -316,22 +316,32 @@ function renderIntoPieces(renderer: Renderer, root: string, sourcePath: string): } let result = ""; for (const piece of context.pieces) { - result += piece.html = normalizePieceHtml(piece.html, root, sourcePath, context); + result += piece.html = normalizePieceHtml(piece.html, sourcePath, context); } return result; }; } -function normalizePieceHtml(html: string, root: string, sourcePath: string, context: ParseContext): string { +const SUPPORTED_PROPERTIES: readonly {query: string; src: string}[] = Object.freeze([ + {query: "img[src]", src: "src"}, + {query: "video[src]", src: "src"}, + {query: "video source[src]", src: "src"}, + {query: "audio[src]", src: "src"}, + {query: "audio source[src]", src: "src"}, + {query: "link[href]", src: "href"} +]); +export function normalizePieceHtml(html: string, sourcePath: string, context: ParseContext): string { const {document} = parseHTML(html); // Extracting references to files (such as from linked stylesheets). - for (const element of document.querySelectorAll("link[href]") as any as Iterable) { - const href = element.getAttribute("href")!; - const path = getLocalPath(sourcePath, href); - if (path) { - context.files.push(fileReference(href, sourcePath)); - element.setAttribute("href", relativeUrl(sourcePath, join("_file", path))); + for (const {query, src} of SUPPORTED_PROPERTIES) { + for (const element of document.querySelectorAll(query) as any as Iterable) { + const relativePath = element.getAttribute(src)!; + const path = getLocalPath(sourcePath, relativePath); + if (path) { + context.files.push(fileReference(relativePath, sourcePath)); + element.setAttribute(src!, relativeUrl(sourcePath, join("_file", path))); + } } } diff --git a/test/promote-file-attachment-test.ts b/test/promote-file-attachment-test.ts new file mode 100644 index 000000000..d83ae5d02 --- /dev/null +++ b/test/promote-file-attachment-test.ts @@ -0,0 +1,79 @@ +import assert from "node:assert"; +import {normalizePieceHtml} from "../src/markdown.js"; +import {resolvePath} from "../src/url.js"; + +const html = (strings, ...values) => String.raw({raw: strings}, ...values); +const mockContext = () => ({files: [], imports: [], pieces: [], startLine: 0, currentLine: 0}); + +describe("file attachments", () => { + describe("success", () => { + const sourcePath = "/attachments.md"; + + it("img[src]", () => { + const htmlStr = html``; + const expected = html``; + const context = mockContext(); + const actual = normalizePieceHtml(htmlStr, sourcePath, context); + + assert.equal(expected, actual); + assert.deepEqual(context.files, [ + { + mimeType: "image/png", + name: "./test.png", + path: "./_file/test.png" + } + ]); + }); + + it("video[src]", () => { + const htmlStr = html``; + const expected = html``; + const context = mockContext(); + const actual = normalizePieceHtml(htmlStr, sourcePath, context); + + assert.equal(expected, actual); + assert.deepEqual(context.files, [ + { + mimeType: "video/quicktime", + name: "observable.mov", + path: "./_file/observable.mov" + } + ]); + }); + + it("video source[src]", () => { + const htmlStr = html``; + + const expected = html``; + + const context = mockContext(); + const actual = normalizePieceHtml(htmlStr, sourcePath, context); + + assert.equal(expected, actual); + assert.deepEqual(context.files, [ + { + mimeType: "video/mp4", + name: "observable.mp4", + path: "./_file/observable.mp4" + }, + { + mimeType: "video/quicktime", + name: "observable.mov", + path: "./_file/observable.mov" + } + ]); + }); + }); +});