diff --git a/__snapshots__/mod_test.ts.snap b/__snapshots__/mod_test.ts.snap index 37a3f94..c71ab9b 100644 --- a/__snapshots__/mod_test.ts.snap +++ b/__snapshots__/mod_test.ts.snap @@ -8,3 +8,12 @@ snapshot[`should archive a markdown file with all images 1`] = ` Google Logo 2006 ' `; + +snapshot[`should use a fallback image if downloading the image failed 1`] = ` +'# Testing + +![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAADN0lEQVR4nO2Y3U9SYRzH+T/qstq6sYv+gmKkKaAmaLalazJNjdNoikAo0PSyteWFF3mRlk0Kp64ZDGsElq/LeLtpxYbrwoZ4lq04cG5+7XnwIfDEi43ToTy/7bs9nPNs5/N5Xsb2k0jEEkssscQqdzVq9cebKNMzldb0TUWZQNBoMcPMJcp8snR4rXFXcHAqN4hJrTMdKyqAV54ygW10HCLRLaBpWtBEoltgHX1AROxFBcixqQR4OksC7wJl+lpcYH/LhIamD4RwHV2B0y8SfzW0KED/Zzug8CVA4WPgvIeBKtfvP4qeo/dortzHgLTAXJTrKwH4EhuG7Z1h6FwJ8itQv8RkQuCQkNzLwDkPA2fdaejseWRulZMLf8aVgFh8GGBPjxOLj+TM41XgsKnzpkUOysazBHbiI+kF8aV3sKIE8sXmD2IJBI/G2e94FVB6PoNsSgOyJxo85kOO5k3Al4AL9j6QPqzGaXDcAksoCcYAC+1ryRyIlmUGdO9TMBRiYSiUAt1mCprfCiRgCLBgCbPQ7p7OwJNoXk2DLcyCNcxiyMFQCsxBBM3i59lBIh3rSRjwp8X0gRS0rf4Sb1tl8LuyC6CP6zc+gWxSwRGQTcqhf/0DB/YwsYRZvEjkd9kFLMEfoLR3ceBJFHYNDIW+c8Bu+7dBvdALqoUbeFyqEF1ugTbnWF54knbnWC5IKAVXXGaQzzbgtLqMYA0nhRGQTtQUFUBzdMsbGYjuJUcGnqT7jYN7LwIx6PXYoMdzB4/5ESgGv5/aqVYY9O+CYTMCynk1R0A5p4aBdx9zdknrvQediz04Wu9dvEuCCaA0z1tBvXCTA0+C7oNl/770r7ky8CR9ay5hBVBqniryCqBoXo+D2R+FrpcURwA9E1wA3Ye6mfr8EnON0LE4wIEnEV4A/T88vlhwF+rnmkDj7q5cAXyUpuUFJVTPL1e2gHSiGmodyoISV53XKlgAHaVHNSCfzX8flOg+uLv4FRA6qiMj0EQZ9/711uIMmowaqpUgEYlugeV+urmLGs9FBVp6DadUlCnOd7v8z9rrhhOSUgr14VErmxwnQcEp4x5a+ZLhxRJLLLHEkhyifgJu5rlgZTGQ8gAAAABJRU5ErkJggg==) + + +' +`; diff --git a/deno.json b/deno.json index 671293c..775905f 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "name": "@openformation/markdown-archiver", "description": "Library for creating self-contained markdown files by embedding images.", - "version": "1.0.3", + "version": "1.0.4", "exports": "./mod.ts", "imports": { "@std/encoding": "jsr:@std/encoding@^0.224.2", @@ -21,4 +21,4 @@ "__snapshots__" ] } -} +} \ No newline at end of file diff --git a/lib/image.ts b/lib/image.ts index eba6858..7f20bb9 100644 --- a/lib/image.ts +++ b/lib/image.ts @@ -28,9 +28,7 @@ const ImageDataUri = Brand.refined( export function fetchImage(url: string) { return Effect.gen(function* () { const response = yield* Effect.tryPromise({ - try: () => { - return fetch(url); - }, + try: () => fetch(url), catch: (cause: unknown) => new FetchImageError(`Failed to fetch image at ${url}: ${cause}`, { cause, @@ -38,6 +36,10 @@ export function fetchImage(url: string) { }); if (!response.ok) { + if (response.body) { + yield* Effect.promise(response.body.cancel.bind(response.body)); + } + return yield* Effect.fail( new FetchImageError( `Failed to fetch image at ${url} as the server responded with a status code of ${response.status}.`, @@ -76,10 +78,22 @@ export class ImageService extends Context.Tag("ImageService")< readonly fetch: ( url: string, ) => Effect.Effect; + readonly getFallbackImageDataUri: () => Effect.Effect< + ImageDataUri + >; } >() {} +function getFallbackImageDataUri() { + return Effect.succeed( + ImageDataUri( + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAADN0lEQVR4nO2Y3U9SYRzH+T/qstq6sYv+gmKkKaAmaLalazJNjdNoikAo0PSyteWFF3mRlk0Kp64ZDGsElq/LeLtpxYbrwoZ4lq04cG5+7XnwIfDEi43ToTy/7bs9nPNs5/N5Xsb2k0jEEkssscQqdzVq9cebKNMzldb0TUWZQNBoMcPMJcp8snR4rXFXcHAqN4hJrTMdKyqAV54ygW10HCLRLaBpWtBEoltgHX1AROxFBcixqQR4OksC7wJl+lpcYH/LhIamD4RwHV2B0y8SfzW0KED/Zzug8CVA4WPgvIeBKtfvP4qeo/dortzHgLTAXJTrKwH4EhuG7Z1h6FwJ8itQv8RkQuCQkNzLwDkPA2fdaejseWRulZMLf8aVgFh8GGBPjxOLj+TM41XgsKnzpkUOysazBHbiI+kF8aV3sKIE8sXmD2IJBI/G2e94FVB6PoNsSgOyJxo85kOO5k3Al4AL9j6QPqzGaXDcAksoCcYAC+1ryRyIlmUGdO9TMBRiYSiUAt1mCprfCiRgCLBgCbPQ7p7OwJNoXk2DLcyCNcxiyMFQCsxBBM3i59lBIh3rSRjwp8X0gRS0rf4Sb1tl8LuyC6CP6zc+gWxSwRGQTcqhf/0DB/YwsYRZvEjkd9kFLMEfoLR3ceBJFHYNDIW+c8Bu+7dBvdALqoUbeFyqEF1ugTbnWF54knbnWC5IKAVXXGaQzzbgtLqMYA0nhRGQTtQUFUBzdMsbGYjuJUcGnqT7jYN7LwIx6PXYoMdzB4/5ESgGv5/aqVYY9O+CYTMCynk1R0A5p4aBdx9zdknrvQediz04Wu9dvEuCCaA0z1tBvXCTA0+C7oNl/770r7ky8CR9ay5hBVBqniryCqBoXo+D2R+FrpcURwA9E1wA3Ye6mfr8EnON0LE4wIEnEV4A/T88vlhwF+rnmkDj7q5cAXyUpuUFJVTPL1e2gHSiGmodyoISV53XKlgAHaVHNSCfzX8flOg+uLv4FRA6qiMj0EQZ9/711uIMmowaqpUgEYlugeV+urmLGs9FBVp6DadUlCnOd7v8z9rrhhOSUgr14VErmxwnQcEp4x5a+ZLhxRJLLLHEkhyifgJu5rlgZTGQ8gAAAABJRU5ErkJggg==", + ), + ); +} + const ImageServiceServer = ImageService.of({ + getFallbackImageDataUri, fetch(url: string) { return Effect.gen(function* () { const { buffer, mimeType } = yield* fetchImage(url); @@ -92,6 +106,7 @@ const ImageServiceServer = ImageService.of({ }); const ImageServiceBrowser = ImageService.of({ + getFallbackImageDataUri, fetch(url: string) { return Effect.gen(function* () { const { buffer } = yield* fetchImage(url); diff --git a/lib/mod.ts b/lib/mod.ts index 23babb3..a3a86cf 100644 --- a/lib/mod.ts +++ b/lib/mod.ts @@ -20,19 +20,15 @@ function makeServices() { return Layer.mergeAll(imageService, markdownService, transformerService); } -/** - * The archiver result type. The effect contains either a `string` with - * the archived markdown or a `FetchImageError` if fetching an image failed. - */ -export type Archiver = Effect.Effect; - /** * Takes source markdown contents and makes it self-contained by embedding images. * * @param markdown {string} The markdown to archive. * @returns {string} The markdown with embedded images. */ -export function archive(markdown: string): Archiver { +export function archive( + markdown: string, +): Effect.Effect { const run = Effect.gen(function* () { const markdownService = yield* MarkdownService; diff --git a/lib/transformer.ts b/lib/transformer.ts index 564567a..04d2f22 100644 --- a/lib/transformer.ts +++ b/lib/transformer.ts @@ -1,5 +1,4 @@ import type { Html, Image, Root } from "mdast"; -import type { FetchImageError } from "./image.ts"; import { Context, Effect, Layer } from "effect"; import { visit } from "unist-util-visit"; @@ -11,7 +10,7 @@ export class TransformerService extends Context.Tag("TransformerService")< { readonly embedImages: ( ast: Root, - ) => Effect.Effect; + ) => Effect.Effect; } >() {} @@ -19,7 +18,10 @@ function processImage(image: Image) { return Effect.gen(function* () { const imageService = yield* ImageService; - const dataUri = yield* imageService.fetch(image.url); + const dataUri = yield* Effect.orElse( + imageService.fetch(image.url), + () => imageService.getFallbackImageDataUri(), + ); image.url = dataUri; }); @@ -37,7 +39,10 @@ function processHtmlImage({ if (match) { const url = match[1]; - const dataUri = yield* imageService.fetch(url); + const dataUri = yield* Effect.orElse( + imageService.fetch(url), + () => imageService.getFallbackImageDataUri(), + ); node.value = node.value.replace(url, dataUri); } diff --git a/mod.ts b/mod.ts index 5a076bd..1dfdb5a 100644 --- a/mod.ts +++ b/mod.ts @@ -1,4 +1 @@ -export type { FetchImageError } from "./lib/image.ts"; -export type { Archiver } from "./lib/mod.ts"; - export { archive } from "./lib/mod.ts"; diff --git a/mod_test.ts b/mod_test.ts index f5dd446..ce70df8 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -15,3 +15,17 @@ Deno.test("should archive a markdown file with all images", async (t) => { await assertSnapshot(t, result); }); + +Deno.test("should use a fallback image if downloading the image failed", async (t) => { + const program = archive(` +# Testing + +![](https://example.com/not-found.jpg) + + +`); + + const result = await Effect.runPromise(program); + + await assertSnapshot(t, result); +});