diff --git a/src/assets.ts b/src/assets.ts index 9551011..6242ce3 100644 --- a/src/assets.ts +++ b/src/assets.ts @@ -2,9 +2,14 @@ import { createHash } from 'node:crypto' import { copyFile, readFile } from 'node:fs/promises' import { basename, extname, join, resolve } from 'node:path' import sharp from 'sharp' +import { visit } from 'unist-util-visit' import { getConfig } from './config' +import type { Element, Root as Hast } from 'hast' +import type { Root as Mdast, Node } from 'mdast' +import type { Plugin } from 'unified' + /** * Image object with metadata & blur image */ @@ -147,3 +152,90 @@ export const outputImage = async (ref: T, fromPath if (ref == null) return ref return output(ref, fromPath, true) as Promise } + +/** + * rehype plugin to copy linked files to public path and replace their urls with public urls + */ +export const rehypeCopyLinkedFiles: Plugin<[], Hast> = () => async (tree, file) => { + const links = new Map() + const linkedPropertyNames = ['href', 'src', 'poster'] + + visit(tree, 'element', node => { + linkedPropertyNames.forEach(name => { + const value = node.properties[name] + if (typeof value === 'string' && isValidatedStaticPath(value)) { + const elements = links.get(value) ?? [] + elements.push(node) + links.set(value, elements) + } + }) + }) + + await Promise.all( + Array.from(links.entries()).map(async ([url, elements]) => { + const publicUrl = await outputFile(url, file.path) + if (publicUrl == null || publicUrl === url) return + elements.forEach(node => { + linkedPropertyNames.forEach(name => { + if (name in node.properties) { + node.properties[name] = publicUrl + } + }) + }) + }) + ) +} + +/** + * remark plugin to copy linked files to public path and replace their urls with public urls + */ +export const remarkCopyLinkedFiles: Plugin<[], Mdast> = () => async (tree, file) => { + const links = new Map() + + visit(tree, ['link', 'image', 'definition'], (node: any) => { + if (isValidatedStaticPath(node.url)) { + const nodes = links.get(node.url) || [] + nodes.push(node) + links.set(node.url, nodes) + } + }) + + visit(tree, 'mdxJsxFlowElement', node => { + node.attributes.forEach((attr: any) => { + if (['href', 'src', 'poster'].includes(attr.name) && typeof attr.value === 'string' && isValidatedStaticPath(attr.value)) { + const nodes = links.get(attr.value) || [] + nodes.push(node) + links.set(attr.value, nodes) + } + }) + }) + + await Promise.all( + Array.from(links.entries()).map(async ([url, nodes]) => { + const publicUrl = await outputFile(url, file.path) + if (publicUrl == null || publicUrl === url) return + nodes.forEach((node: any) => { + if ('url' in node && node.url === url) { + node.url = publicUrl + return + } + if ('href' in node && node.href === url) { + node.href = publicUrl + return + } + + node.attributes.forEach((attr: any) => { + if (attr.name === 'src' && attr.value === url) { + attr.value = publicUrl + } + if (attr.name === 'href' && attr.value === url) { + attr.value = publicUrl + } + if (attr.name === 'poster' && attr.value === url) { + attr.value = publicUrl + } + }) + }) + }) + ) +} diff --git a/src/schemas/markdown.ts b/src/schemas/markdown.ts index 83033fa..058c391 100644 --- a/src/schemas/markdown.ts +++ b/src/schemas/markdown.ts @@ -7,10 +7,10 @@ import { unified } from 'unified' import { visit } from 'unist-util-visit' import { z } from 'zod' -import { isValidatedStaticPath, outputFile } from '../assets' +import { rehypeCopyLinkedFiles } from '../assets' import { getConfig } from '../config' -import type { Element, Root } from 'hast' +import type { Root } from 'hast' import type { PluggableList, Plugin } from 'unified' /** @@ -52,36 +52,6 @@ const remarkRemoveComments: Plugin<[], Root> = () => tree => { }) } -const rehypeCopyLinkedFiles: Plugin<[], Root> = () => async (tree, file) => { - const links = new Map() - const linkedPropertyNames = ['href', 'src', 'poster'] - - visit(tree, 'element', node => { - linkedPropertyNames.forEach(name => { - const value = node.properties[name] - if (typeof value === 'string' && isValidatedStaticPath(value)) { - const elements = links.get(value) ?? [] - elements.push(node) - links.set(value, elements) - } - }) - }) - - await Promise.all( - Array.from(links.entries()).map(async ([url, elements]) => { - const publicUrl = await outputFile(url, file.path) - if (publicUrl == null || publicUrl === url) return - elements.forEach(node => { - linkedPropertyNames.forEach(name => { - if (name in node.properties) { - node.properties[name] = publicUrl - } - }) - }) - }) - ) -} - export const markdown = (options: MarkdownOptions = {}) => z.string().transform(async (value, ctx) => { const { markdown = {} } = getConfig() diff --git a/src/schemas/mdx.ts b/src/schemas/mdx.ts index e091d63..8845f3d 100644 --- a/src/schemas/mdx.ts +++ b/src/schemas/mdx.ts @@ -3,11 +3,11 @@ import remarkGfm from 'remark-gfm' import { visit } from 'unist-util-visit' import { z } from 'zod' -import { isValidatedStaticPath, outputFile } from '../assets' +import { remarkCopyLinkedFiles } from '../assets' import { getConfig } from '../config' import type { CompileOptions } from '@mdx-js/mdx' -import type { Node, Root } from 'mdast' +import type { Root } from 'mdast' import type { Plugin } from 'unified' /** @@ -45,57 +45,6 @@ const remarkRemoveComments: Plugin<[], Root> = () => tree => { }) } -const remarkCopyLinkedFiles: Plugin<[], Root> = () => async (tree, file) => { - const links = new Map() - - visit(tree, ['link', 'image', 'definition'], (node: any) => { - if (isValidatedStaticPath(node.url)) { - const nodes = links.get(node.url) || [] - nodes.push(node) - links.set(node.url, nodes) - } - }) - - visit(tree, 'mdxJsxFlowElement', node => { - node.attributes.forEach((attr: any) => { - if (['href', 'src', 'poster'].includes(attr.name) && typeof attr.value === 'string' && isValidatedStaticPath(attr.value)) { - const nodes = links.get(attr.value) || [] - nodes.push(node) - links.set(attr.value, nodes) - } - }) - }) - - await Promise.all( - Array.from(links.entries()).map(async ([url, nodes]) => { - const publicUrl = await outputFile(url, file.path) - if (publicUrl == null || publicUrl === url) return - nodes.forEach((node: any) => { - if ('url' in node && node.url === url) { - node.url = publicUrl - return - } - if ('href' in node && node.href === url) { - node.href = publicUrl - return - } - - node.attributes.forEach((attr: any) => { - if (attr.name === 'src' && attr.value === url) { - attr.value = publicUrl - } - if (attr.name === 'href' && attr.value === url) { - attr.value = publicUrl - } - if (attr.name === 'poster' && attr.value === url) { - attr.value = publicUrl - } - }) - }) - }) - ) -} - export const mdx = (options: MdxOptions = {}) => z.string().transform(async (value, ctx) => { const { mdx = {} } = getConfig()