From 4a9de8b036d1dde8ef377fbaadc16b4e5fcf2414 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 16:20:49 -0800 Subject: [PATCH 01/17] feat: Add custom component to markdown-remark integration - Add a new custom component called "Custom" to the markdown-remark integration in Astro. - The custom component is defined in the file "packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_comps/Custom.astro". - This component will be used when processing markdown content. --- packages/markdown-remark/README.md | 24 +++- packages/markdown-remark/package.json | 2 + .../markdown-remark/src/integration/index.ts | 31 +++- .../src/integration/markdown.ts | 69 ++++++--- .../markdown-remark/src/integration/utils.ts | 136 ++++++++++++++++++ packages/markdown-remark/src/v.d.ts | 10 ++ .../tests/astro-integration.test.ts | 14 +- .../tests/fixture/astro/astro.config.ts | 10 +- .../custom-components/_comps/Custom.astro | 7 + .../src/pages/custom-components/_md/md.txt | 5 + .../src/pages/custom-components/index.astro | 8 ++ .../tests/fixture/astro/src/pages/index.astro | 1 + .../tests/fixture/astro/v.d.ts | 4 + pnpm-lock.yaml | 12 ++ 14 files changed, 302 insertions(+), 31 deletions(-) create mode 100644 packages/markdown-remark/src/integration/utils.ts create mode 100644 packages/markdown-remark/src/v.d.ts create mode 100644 packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_comps/Custom.astro create mode 100644 packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_md/md.txt create mode 100644 packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/index.astro diff --git a/packages/markdown-remark/README.md b/packages/markdown-remark/README.md index 4005c10..7a10828 100644 --- a/packages/markdown-remark/README.md +++ b/packages/markdown-remark/README.md @@ -44,7 +44,22 @@ export default defineConfig({ * https://docs.astro.build/en/reference/configuration-reference/#markdown-options */ }, - integrations: [markdownRemark()], + integrations: [markdownRemark({ + // Used for injecting CSS for Headings and Callouts + injectCSS: true, + // Custom Markdown config + markdown: { + // Configure the available callout themes + callouts: { + theme: 'obsidian' // Can also be 'github' or 'vitepress' + }, + // User defined components that will be used when processing markdown + components: { + // Example of a custom defined component + custom: "./src/components/Custom.astro", + } + } + })], }); ``` @@ -55,6 +70,7 @@ export default defineConfig({ ```astro --- import { Markdown } from 'studiocms:markdown-remark'; +import Custom from '../components/Custom.astro'; --- @@ -63,7 +79,7 @@ import { Markdown } from 'studiocms:markdown-remark'; Example - + ``` @@ -73,8 +89,10 @@ OR ```astro --- import { render } from 'studiocms:markdown-remark'; +import Custom from '../components/Custom.astro'; -const { html } = render('# Hello World!') +// @ts-ignore +const { html } = render('# Hello World!', {}, { $$result, {custom: Custom} }) --- diff --git a/packages/markdown-remark/package.json b/packages/markdown-remark/package.json index 6665e68..587f86c 100644 --- a/packages/markdown-remark/package.json +++ b/packages/markdown-remark/package.json @@ -46,6 +46,7 @@ "dependencies": { "@astrojs/prism": "^3.2.0", "astro-integration-kit": "^0.18.0", + "entities": "^6.0.0", "github-slugger": "^2.0.0", "hastscript": "^9.0.0", "hast-util-from-html": "^2.0.3", @@ -64,6 +65,7 @@ "remark-rehype": "^11.1.1", "remark-smartypants": "^3.0.2", "shiki": "^1.23.1", + "ultrahtml": "^1.5.3", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 2cc2782..4bee199 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -6,7 +6,7 @@ import { shared } from './shared.js'; const MarkdownRemarkOptionsSchema = z .object({ /** - * Inject CSS for Rehype autolink headings styles. + * Inject CSS for the Markdown processor. */ injectCSS: z.boolean().optional().default(true), @@ -30,6 +30,11 @@ const MarkdownRemarkOptionsSchema = z }) .optional() .default({}), + + /** + * Configures the user defined components for the Markdown processor. + */ + components: z.record(z.string(), z.string()).optional().default({}), }) .optional() .default({}), @@ -57,6 +62,19 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { name: '@studiocms/markdown-remark', hooks: { 'astro:config:setup'(params) { + const { resolve: astroRootResolve } = createResolver(params.config.root.pathname); + + const resolvedComponents: Record = {}; + + for (const [name, path] of Object.entries(markdown.components)) { + resolvedComponents[name] = astroRootResolve(path); + } + + const componentNames = Object.keys(resolvedComponents); + const componentExports = Object.entries(resolvedComponents).map((component) => { + return `export { default as ${component[0]} } from '${component[1]}';`; + }); + addVirtualImports(params, { name: '@studiocms/markdown-remark', imports: { @@ -65,6 +83,13 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { import '${resolve('../../assets/headings.css')}'; import '${resolvedCalloutTheme}'; `, + 'studiocms:markdown-remark/user-components': ` + export const componentMap = ${JSON.stringify(resolvedComponents)}; + + export const componentKeys = ${JSON.stringify(componentNames)}; + + ${componentExports.join('\n')} + `, }, }); @@ -84,6 +109,10 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { export type Props = import('${resolve('./markdown.js')}').Props; export type RenderResponse = import('${resolve('./markdown.js')}').RenderResponse; } + + declare module 'studiocms:markdown-remark/user-components' { + export const componentMap: Record; + } `, }); }, diff --git a/packages/markdown-remark/src/integration/markdown.ts b/packages/markdown-remark/src/integration/markdown.ts index 32cc780..c5009cd 100644 --- a/packages/markdown-remark/src/integration/markdown.ts +++ b/packages/markdown-remark/src/integration/markdown.ts @@ -1,14 +1,18 @@ +import { componentKeys } from 'studiocms:markdown-remark/user-components'; import type { SSRResult } from 'astro'; import { renderSlot } from 'astro/runtime/server/index.js'; import type { RenderTemplateResult } from 'astro/runtime/server/render/astro/render-template.js'; import type { ComponentSlotValue } from 'astro/runtime/server/render/slot.js'; -import type { HTMLString } from '../processor/HTMLString.js'; +import { transform } from 'ultrahtml'; +import swap from 'ultrahtml/transformers/swap'; +import { HTMLString } from '../processor/HTMLString.js'; import { type MarkdownHeading, type MarkdownProcessorRenderOptions, createMarkdownProcessor, } from '../processor/index.js'; import { shared } from './shared.js'; +import { createComponentProxy, dedent, importComponentsKeys, mergeRecords } from './utils.js'; const processor = await createMarkdownProcessor({ ...shared.markdownConfig, @@ -17,6 +21,8 @@ const processor = await createMarkdownProcessor({ }, }); +const predefinedComponents = await importComponentsKeys(componentKeys); + /** * Represents the response from rendering a markdown document. */ @@ -52,19 +58,6 @@ export interface RenderResponse { }; } -/** - * Interface representing the properties for a markdown component. - * - * @property content - The markdown content as a string. - * @property [name: string] - An index signature allowing additional properties with string keys and values of any type. - */ -export interface Props { - content: string; - - // biome-ignore lint/suspicious/noExplicitAny: - [name: string]: any; -} - /** * Renders the given markdown content using the specified options. * @@ -74,16 +67,41 @@ export interface Props { */ export async function render( content: string, - options?: MarkdownProcessorRenderOptions + options?: MarkdownProcessorRenderOptions, + _components?: { + // biome-ignore lint/suspicious/noExplicitAny: + $$result: any; + // biome-ignore lint/suspicious/noExplicitAny: + components?: Record; + } ): Promise { + const allComponents = mergeRecords(predefinedComponents, _components?.components ?? {}); + + const componentsRendered = createComponentProxy(_components?.$$result, allComponents); + const result = await processor.render(content, options); + const html = await transform(dedent(result.astroHTML.toString()), [swap(componentsRendered)]); return { - html: result.astroHTML, + html: new HTMLString(html), meta: result.metadata, }; } +/** + * Interface representing the properties for a markdown component. + * + * @property content - The markdown content as a string. + * @property [name: string] - An index signature allowing additional properties with string keys and values of any type. + */ +export interface Props { + content: string; + // biome-ignore lint/suspicious/noExplicitAny: + components?: Record; + // biome-ignore lint/suspicious/noExplicitAny: + [name: string]: any; +} + /** * A factory function for creating an Astro component that renders Markdown content. * @@ -113,8 +131,9 @@ export async function render( // biome-ignore lint/suspicious/noExplicitAny: export const Markdown: (props: Props) => any = Object.assign( function Markdown( - result: SSRResult, - attributes: { content: string }, + $$result: SSRResult, + // biome-ignore lint/suspicious/noExplicitAny: + attributes: { content: string; components?: Record }, slots: { default: ComponentSlotValue | RenderTemplateResult } ) { return { @@ -125,13 +144,17 @@ export const Markdown: (props: Props) => any = Object.assign( const mdl = attributes.content; if (typeof mdl === 'string') { - yield ( - await render(mdl, { + const content = await render( + mdl, + { fileURL: new URL(import.meta.url), - }) - ).html; + }, + { $$result, components: attributes.components } + ); + + yield content.html; } else { - yield renderSlot(result, slots.default); + yield renderSlot($$result, slots.default); } }, }; diff --git a/packages/markdown-remark/src/integration/utils.ts b/packages/markdown-remark/src/integration/utils.ts new file mode 100644 index 0000000..76ea8dd --- /dev/null +++ b/packages/markdown-remark/src/integration/utils.ts @@ -0,0 +1,136 @@ +import { AstroError } from 'astro/errors'; +import { jsx as h } from 'astro/jsx-runtime'; +import { renderJSX } from 'astro/runtime/server/jsx.js'; +import * as entities from 'entities'; +import { __unsafeHTML } from 'ultrahtml'; + +export function createComponentProxy( + // biome-ignore lint/suspicious/noExplicitAny: + result: any, + // biome-ignore lint/suspicious/noExplicitAny: + _components: Record = {} +) { + // biome-ignore lint/suspicious/noExplicitAny: + const components: Record = {}; + for (const [key, value] of Object.entries(_components)) { + if (typeof value === 'string') { + components[key] = value; + } else { + components[key] = async ( + // biome-ignore lint/suspicious/noExplicitAny: + props: Record, + // biome-ignore lint/suspicious/noExplicitAny: + children: { value: any } + ) => { + if (key === 'CodeBlock' || key === 'CodeSpan') { + props.code = entities.decode(JSON.parse(`"${props.code}"`)); + } + const output = await renderJSX(result, h(value, { ...props, 'set:html': children.value })); + return __unsafeHTML(output); + }; + } + } + return components; +} + +function getIndent(ln: string): string { + if (ln.trimStart() === ln) return ''; + return ln.slice(0, ln.length - ln.trimStart().length); +} + +export function dedent(str: string): string { + const lns = str.replace(/^[\r\n]+/, '').split('\n'); + let indent = getIndent(lns[0]); + if (indent.length === 0 && lns.length > 1) { + indent = getIndent(lns[1]); + } + if (indent.length === 0) return lns.join('\n'); + return lns.map((ln) => (ln.startsWith(indent) ? ln.slice(indent.length) : ln)).join('\n'); +} + +// biome-ignore lint/suspicious/noExplicitAny: +export function mergeRecords(...records: Record[]): Record { + // biome-ignore lint/suspicious/noExplicitAny: + const result: Record = {}; + for (const record of records) { + for (const [key, value] of Object.entries(record)) { + result[key] = value; + } + } + return result; +} + +export class MarkdownRemarkError extends AstroError { + name = 'StudioCMS Markdown Remark Error'; +} + +// biome-ignore lint/suspicious/noExplicitAny: +export function prefixError(err: any, prefix: string): any { + // If the error is an object with a `message` property, attempt to prefix the message + if (err?.message) { + try { + err.message = `${prefix}:\n${err.message}`; + return err; + } catch { + // Any errors here are ok, there's fallback code below + } + } + + // If that failed, create a new error with the desired message and attempt to keep the stack + const wrappedError = new Error(`${prefix}${err ? `: ${err}` : ''}`); + try { + wrappedError.stack = err.stack; + wrappedError.cause = err; + } catch { + // It's ok if we could not set the stack or cause - the message is the most important part + } + + return wrappedError; +} + +export async function importComponents(components?: Record) { + // biome-ignore lint/suspicious/noExplicitAny: + const PredefinedComponents: Record = {}; + + for (const [name, path] of Object.entries(components ?? {})) { + try { + PredefinedComponents[name] = (await import(/* @vite-ignore */ path)).default; + } catch (e) { + if (e instanceof Error) { + const newErr = prefixError(e, `Failed to import component "${name}" from "${path}"`); + console.error(newErr); + throw new MarkdownRemarkError(newErr.message, newErr.stack); + } + const newErr = prefixError( + new Error('Unknown error'), + `Failed to import component "${name}" from "${path}"` + ); + console.error(newErr); + throw new MarkdownRemarkError(newErr.message, newErr.stack); + } + } + + return PredefinedComponents; +} + +export async function importComponentsKeys(keys: string[]) { + // biome-ignore lint/suspicious/noExplicitAny: + const predefinedComponents: Record = {}; + + for (const key of keys) { + try { + predefinedComponents[key] = (await import('studiocms:markdown-remark/user-components'))[key]; + } catch (e) { + if (e instanceof Error) { + const newErr = prefixError(e, `Failed to import component "${key}"`); + console.error(newErr); + throw new MarkdownRemarkError(newErr.message, newErr.stack); + } + const newErr = prefixError(new Error('Unknown error'), `Failed to import component "${key}"`); + console.error(newErr); + throw new MarkdownRemarkError(newErr.message, newErr.stack); + } + } + + return predefinedComponents; +} diff --git a/packages/markdown-remark/src/v.d.ts b/packages/markdown-remark/src/v.d.ts new file mode 100644 index 0000000..ff31209 --- /dev/null +++ b/packages/markdown-remark/src/v.d.ts @@ -0,0 +1,10 @@ +declare module 'studiocms:markdown-remark' { + export const Markdown: typeof import('../../../src/integration/markdown').Markdown; + export const render: typeof import('../../../src/integration/markdown').render; +} + +declare module 'studiocms:markdown-remark/user-components' { + export const componentMap: Record; + + export const componentKeys: string[]; +} diff --git a/packages/markdown-remark/tests/astro-integration.test.ts b/packages/markdown-remark/tests/astro-integration.test.ts index 38aede7..9c017bb 100644 --- a/packages/markdown-remark/tests/astro-integration.test.ts +++ b/packages/markdown-remark/tests/astro-integration.test.ts @@ -10,9 +10,9 @@ describe('Markdown-Remark Astro Integration Tests', () => { await fixture.build({}); }); - afterAll(async () => { - await fixture.clean(); - }); + // afterAll(async () => { + // await fixture.clean(); + // }); test('Basic Component Tests', async () => { const content = await fixture.readFile('basic/index.html'); @@ -41,6 +41,14 @@ describe('Markdown-Remark Astro Integration Tests', () => {
This is a collapsible callout

Some content shown after opening!

`); }); + test('Custom Components test', async () => { + const content = await fixture.readFile('custom-components/index.html'); + + expect(content).toContain( + '

Custom Component

This is a custom component

' + ); + }); + test('Direct Markdown Processor Tests', async () => { const content = await fixture.readFile('direct/index.html'); diff --git a/packages/markdown-remark/tests/fixture/astro/astro.config.ts b/packages/markdown-remark/tests/fixture/astro/astro.config.ts index ccebb4b..d8b5684 100644 --- a/packages/markdown-remark/tests/fixture/astro/astro.config.ts +++ b/packages/markdown-remark/tests/fixture/astro/astro.config.ts @@ -2,5 +2,13 @@ import markdownRemark from '@studiocms/markdown-remark'; import { defineConfig } from 'astro/config'; export default defineConfig({ - integrations: [markdownRemark()], + integrations: [ + markdownRemark({ + markdown: { + components: { + custom: './src/pages/custom-components/_comps/Custom.astro', + }, + }, + }), + ], }); diff --git a/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_comps/Custom.astro b/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_comps/Custom.astro new file mode 100644 index 0000000..d1401d9 --- /dev/null +++ b/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_comps/Custom.astro @@ -0,0 +1,7 @@ +--- +--- + +
+

Custom Component

+

This is a custom component

+
\ No newline at end of file diff --git a/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_md/md.txt b/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_md/md.txt new file mode 100644 index 0000000..b47f8f9 --- /dev/null +++ b/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/_md/md.txt @@ -0,0 +1,5 @@ +# Hello World! + + + +Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/index.astro b/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/index.astro new file mode 100644 index 0000000..64247a5 --- /dev/null +++ b/packages/markdown-remark/tests/fixture/astro/src/pages/custom-components/index.astro @@ -0,0 +1,8 @@ +--- +import { Markdown } from 'studiocms:markdown-remark'; +import Layout from '../../layouts/layout.astro'; +import content from './_md/md.txt?raw'; +--- + + + \ No newline at end of file diff --git a/packages/markdown-remark/tests/fixture/astro/src/pages/index.astro b/packages/markdown-remark/tests/fixture/astro/src/pages/index.astro index 44124ed..c993d32 100644 --- a/packages/markdown-remark/tests/fixture/astro/src/pages/index.astro +++ b/packages/markdown-remark/tests/fixture/astro/src/pages/index.astro @@ -6,6 +6,7 @@ import Layout from '../layouts/layout.astro';
  • Basic
  • Basic Render
  • Callouts
  • +
  • Custom Components
  • Direct
  • Syntax Test
  • diff --git a/packages/markdown-remark/tests/fixture/astro/v.d.ts b/packages/markdown-remark/tests/fixture/astro/v.d.ts index e659f2b..6d1f170 100644 --- a/packages/markdown-remark/tests/fixture/astro/v.d.ts +++ b/packages/markdown-remark/tests/fixture/astro/v.d.ts @@ -2,3 +2,7 @@ declare module 'studiocms:markdown-remark' { export const Markdown: typeof import('../../../src/integration/markdown').Markdown; export const render: typeof import('../../../src/integration/markdown').render; } + +declare module 'studiocms:markdown-remark/user-components' { + export const componentMap: Record; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1110e96..e98c2f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: astro-integration-kit: specifier: ^0.18.0 version: 0.18.0(astro@5.1.2(@types/node@22.10.5)(rollup@4.29.1)(typescript@5.7.2)(yaml@2.7.0)) + entities: + specifier: ^6.0.0 + version: 6.0.0 github-slugger: specifier: ^2.0.0 version: 2.0.0 @@ -107,6 +110,9 @@ importers: shiki: specifier: ^1.23.1 version: 1.25.1 + ultrahtml: + specifier: ^1.5.3 + version: 1.5.3 unified: specifier: ^11.0.5 version: 11.0.5 @@ -1427,6 +1433,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.0: + resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} + engines: {node: '>=0.12'} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -4164,6 +4174,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.0: {} + es-module-lexer@1.6.0: {} esbuild-plugin-copy@2.1.1(esbuild@0.24.2): From 906c18895ef1677bf50671cffd2556605e05ce8f Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 16:22:37 -0800 Subject: [PATCH 02/17] Refactor markdown-remark integration to remove unused code --- packages/markdown-remark/src/integration/index.ts | 4 +--- packages/markdown-remark/src/v.d.ts | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 4bee199..c2b4d45 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -84,8 +84,6 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { import '${resolvedCalloutTheme}'; `, 'studiocms:markdown-remark/user-components': ` - export const componentMap = ${JSON.stringify(resolvedComponents)}; - export const componentKeys = ${JSON.stringify(componentNames)}; ${componentExports.join('\n')} @@ -111,7 +109,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { } declare module 'studiocms:markdown-remark/user-components' { - export const componentMap: Record; + export const componentKeys: string[]; } `, }); diff --git a/packages/markdown-remark/src/v.d.ts b/packages/markdown-remark/src/v.d.ts index ff31209..ea552fc 100644 --- a/packages/markdown-remark/src/v.d.ts +++ b/packages/markdown-remark/src/v.d.ts @@ -4,7 +4,5 @@ declare module 'studiocms:markdown-remark' { } declare module 'studiocms:markdown-remark/user-components' { - export const componentMap: Record; - export const componentKeys: string[]; } From 81ba48cf7db037f8d274d98ca836cc5c55c78cad Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 16:51:47 -0800 Subject: [PATCH 03/17] Refactor markdown-remark integration to remove unused code --- packages/markdown-remark/README.md | 6 +++-- .../markdown-remark/src/integration/utils.ts | 25 ------------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/packages/markdown-remark/README.md b/packages/markdown-remark/README.md index 7a10828..2f1c9e6 100644 --- a/packages/markdown-remark/README.md +++ b/packages/markdown-remark/README.md @@ -29,6 +29,8 @@ pnpm add @studiocms/markdown-remark ### As an Astro Integration +With the Astro integration enabled, you can either pass in custom components into your astro config, or manually for the specific render your trying to do shown in the following methods. + #### Setup the integration **`astro.config.mjs`** @@ -79,7 +81,7 @@ import Custom from '../components/Custom.astro'; Example - + `} components={{ custom: Custom }} /> ``` @@ -92,7 +94,7 @@ import { render } from 'studiocms:markdown-remark'; import Custom from '../components/Custom.astro'; // @ts-ignore -const { html } = render('# Hello World!', {}, { $$result, {custom: Custom} }) +const { html } = render('# Hello World! ', {}, { $$result, {custom: Custom} }) --- diff --git a/packages/markdown-remark/src/integration/utils.ts b/packages/markdown-remark/src/integration/utils.ts index 76ea8dd..1660d6b 100644 --- a/packages/markdown-remark/src/integration/utils.ts +++ b/packages/markdown-remark/src/integration/utils.ts @@ -88,31 +88,6 @@ export function prefixError(err: any, prefix: string): any { return wrappedError; } -export async function importComponents(components?: Record) { - // biome-ignore lint/suspicious/noExplicitAny: - const PredefinedComponents: Record = {}; - - for (const [name, path] of Object.entries(components ?? {})) { - try { - PredefinedComponents[name] = (await import(/* @vite-ignore */ path)).default; - } catch (e) { - if (e instanceof Error) { - const newErr = prefixError(e, `Failed to import component "${name}" from "${path}"`); - console.error(newErr); - throw new MarkdownRemarkError(newErr.message, newErr.stack); - } - const newErr = prefixError( - new Error('Unknown error'), - `Failed to import component "${name}" from "${path}"` - ); - console.error(newErr); - throw new MarkdownRemarkError(newErr.message, newErr.stack); - } - } - - return PredefinedComponents; -} - export async function importComponentsKeys(keys: string[]) { // biome-ignore lint/suspicious/noExplicitAny: const predefinedComponents: Record = {}; From 9b9bf9948f87a486b55b7bc46f55cbf61335bfbf Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 16:54:50 -0800 Subject: [PATCH 04/17] Refactor markdown-remark integration to clean up code --- packages/markdown-remark/tests/astro-integration.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/markdown-remark/tests/astro-integration.test.ts b/packages/markdown-remark/tests/astro-integration.test.ts index 9c017bb..a5a10c4 100644 --- a/packages/markdown-remark/tests/astro-integration.test.ts +++ b/packages/markdown-remark/tests/astro-integration.test.ts @@ -10,9 +10,9 @@ describe('Markdown-Remark Astro Integration Tests', () => { await fixture.build({}); }); - // afterAll(async () => { - // await fixture.clean(); - // }); + afterAll(async () => { + await fixture.clean(); + }); test('Basic Component Tests', async () => { const content = await fixture.readFile('basic/index.html'); From a1da582e2e154089a0c1876029a2317c6e2030e8 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 17:01:51 -0800 Subject: [PATCH 05/17] Refactor markdown-remark integration to remove unused code and optimize component exports --- packages/markdown-remark/src/integration/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index c2b4d45..1fd2ef2 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -70,11 +70,6 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { resolvedComponents[name] = astroRootResolve(path); } - const componentNames = Object.keys(resolvedComponents); - const componentExports = Object.entries(resolvedComponents).map((component) => { - return `export { default as ${component[0]} } from '${component[1]}';`; - }); - addVirtualImports(params, { name: '@studiocms/markdown-remark', imports: { @@ -84,9 +79,11 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { import '${resolvedCalloutTheme}'; `, 'studiocms:markdown-remark/user-components': ` - export const componentKeys = ${JSON.stringify(componentNames)}; + export const componentKeys = ${JSON.stringify(Object.keys(resolvedComponents))}; - ${componentExports.join('\n')} + ${Object.entries(resolvedComponents) + .map(([name, path]) => `export { default as ${name} } from '${path}';`) + .join('\n')} `, }, }); From 2f76ef6fd8383f17081866e38e776e07f55194c2 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 17:04:09 -0800 Subject: [PATCH 06/17] Refactor markdown-remark integration to optimize component exports --- packages/markdown-remark/src/integration/index.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 1fd2ef2..421817c 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -64,12 +64,6 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { 'astro:config:setup'(params) { const { resolve: astroRootResolve } = createResolver(params.config.root.pathname); - const resolvedComponents: Record = {}; - - for (const [name, path] of Object.entries(markdown.components)) { - resolvedComponents[name] = astroRootResolve(path); - } - addVirtualImports(params, { name: '@studiocms/markdown-remark', imports: { @@ -79,10 +73,13 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { import '${resolvedCalloutTheme}'; `, 'studiocms:markdown-remark/user-components': ` - export const componentKeys = ${JSON.stringify(Object.keys(resolvedComponents))}; + export const componentKeys = ${JSON.stringify(Object.keys(markdown.components))}; - ${Object.entries(resolvedComponents) - .map(([name, path]) => `export { default as ${name} } from '${path}';`) + ${Object.entries(markdown.components) + .map( + ([name, path]) => + `export { default as ${name} } from '${astroRootResolve(path)}';` + ) .join('\n')} `, }, From bd0ee285bfcce90b0a422d311dc8b3a238fc96e8 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 17:08:16 -0800 Subject: [PATCH 07/17] Refactor markdown-remark integration to optimize component exports and remove unused code --- .../markdown-remark/src/integration/index.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 421817c..28351cf 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -51,12 +51,13 @@ export type MarkdownRemarkOptions = typeof MarkdownRemarkOptionsSchema._input; * @returns Astro integration. */ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { - const { injectCSS, markdown } = MarkdownRemarkOptionsSchema.parse(opts); + const { + injectCSS, + markdown: { callouts, components }, + } = MarkdownRemarkOptionsSchema.parse(opts); const { resolve } = createResolver(import.meta.url); - const resolvedCalloutTheme = resolve( - `../../assets/callout-themes/${markdown.callouts.theme}.css` - ); + const resolvedCalloutTheme = resolve(`../../assets/callout-themes/${callouts.theme}.css`); return { name: '@studiocms/markdown-remark', @@ -73,9 +74,9 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { import '${resolvedCalloutTheme}'; `, 'studiocms:markdown-remark/user-components': ` - export const componentKeys = ${JSON.stringify(Object.keys(markdown.components))}; + export const componentKeys = ${JSON.stringify(Object.keys(components))}; - ${Object.entries(markdown.components) + ${Object.entries(components) .map( ([name, path]) => `export { default as ${name} } from '${astroRootResolve(path)}';` @@ -90,6 +91,8 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { } }, 'astro:config:done'({ injectTypes, config }) { + const { resolve: astroRootResolve } = createResolver(config.root.pathname); + shared.markdownConfig = config.markdown; injectTypes({ filename: 'render.d.ts', @@ -104,6 +107,13 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { declare module 'studiocms:markdown-remark/user-components' { export const componentKeys: string[]; + + ${Object.entries(components) + .map( + ([name, path]) => + `export const ${name}: typeof import('${astroRootResolve(path)}').default;` + ) + .join('\n')} } `, }); From 5cb8b0feac1bbf45cf8e9f93414361823fc1e032 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 17:17:14 -0800 Subject: [PATCH 08/17] Refactor markdown-remark integration to optimize component exports and update type definition --- packages/markdown-remark/src/index.ts | 2 +- .../markdown-remark/src/integration/index.ts | 45 ++------------- .../markdown-remark/src/integration/schema.ts | 55 +++++++++++++++++++ 3 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 packages/markdown-remark/src/integration/schema.ts diff --git a/packages/markdown-remark/src/index.ts b/packages/markdown-remark/src/index.ts index 52f6c16..18d4a0e 100644 --- a/packages/markdown-remark/src/index.ts +++ b/packages/markdown-remark/src/index.ts @@ -1,2 +1,2 @@ export * from './processor/index.js'; -export { markdownRemark, markdownRemark as default } from './integration/index.js'; +export { markdownRemark, default, type MarkdownRemarkOptions } from './integration/index.js'; diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 28351cf..831feb6 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -1,48 +1,9 @@ import type { AstroIntegration } from 'astro'; import { addVirtualImports, createResolver } from 'astro-integration-kit'; -import { z } from 'astro/zod'; +import { type MarkdownRemarkOptions, MarkdownRemarkOptionsSchema } from './schema.js'; import { shared } from './shared.js'; -const MarkdownRemarkOptionsSchema = z - .object({ - /** - * Inject CSS for the Markdown processor. - */ - injectCSS: z.boolean().optional().default(true), - - /** - * Options for the Markdown processor. - */ - markdown: z - .object({ - /** - * Configures the callouts theme. - */ - callouts: z - .object({ - /** - * The theme to use for callouts. - */ - theme: z - .union([z.literal('github'), z.literal('obsidian'), z.literal('vitepress')]) - .optional() - .default('obsidian'), - }) - .optional() - .default({}), - - /** - * Configures the user defined components for the Markdown processor. - */ - components: z.record(z.string(), z.string()).optional().default({}), - }) - .optional() - .default({}), - }) - .optional() - .default({}); - -export type MarkdownRemarkOptions = typeof MarkdownRemarkOptionsSchema._input; +export type { MarkdownRemarkOptions } from './schema.js'; /** * Integrates the Markdown Remark processor into Astro available as `studiocms:markdown-remark`. @@ -121,3 +82,5 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { }, }; } + +export default markdownRemark; diff --git a/packages/markdown-remark/src/integration/schema.ts b/packages/markdown-remark/src/integration/schema.ts new file mode 100644 index 0000000..1fa836b --- /dev/null +++ b/packages/markdown-remark/src/integration/schema.ts @@ -0,0 +1,55 @@ +import { z } from 'astro/zod'; + +/** + * Options for the Markdown Callouts. + */ +const CalloutsSchema = z + .object({ + /** + * The theme to use for callouts. + */ + theme: z + .union([z.literal('github'), z.literal('obsidian'), z.literal('vitepress')]) + .optional() + .default('obsidian'), + }) + .optional() + .default({}); + +/** + * Extended options for the Astro Integration for Markdown Remark. Used to control how Markdown is processed. + */ +const MarkdownSchema = z + .object({ + /** + * Configures the callouts theme. + */ + callouts: CalloutsSchema, + + /** + * Configures the user defined components for the Markdown processor. + */ + components: z.record(z.string(), z.string()).optional().default({}), + }) + .optional() + .default({}); + +/** + * Options for the Markdown Remark processor. + */ +export const MarkdownRemarkOptionsSchema = z + .object({ + /** + * Inject CSS for the Markdown processor. + */ + injectCSS: z.boolean().optional().default(true), + + /** + * Extended options for the Astro Integration for Markdown Remark. Used to control how Markdown is processed. + */ + markdown: MarkdownSchema, + }) + .optional() + .default({}); + +export type MarkdownRemarkOptions = typeof MarkdownRemarkOptionsSchema._input; From 6fb40fdac2d989616fae4e13710d55d9e2b60a99 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 17:34:33 -0800 Subject: [PATCH 09/17] Refactor markdown-remark integration to optimize component exports and remove unused code --- .../markdown-remark/src/integration/index.ts | 14 ++++++++ .../src/integration/markdown.ts | 2 ++ .../markdown-remark/src/integration/schema.ts | 3 ++ .../markdown-remark/src/integration/utils.ts | 35 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 831feb6..78611aa 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -12,28 +12,37 @@ export type { MarkdownRemarkOptions } from './schema.js'; * @returns Astro integration. */ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { + // Parse the options const { injectCSS, markdown: { callouts, components }, } = MarkdownRemarkOptionsSchema.parse(opts); + + // Create a resolver for the current file const { resolve } = createResolver(import.meta.url); + // Resolve the callout theme based on the user's configuration const resolvedCalloutTheme = resolve(`../../assets/callout-themes/${callouts.theme}.css`); return { name: '@studiocms/markdown-remark', hooks: { 'astro:config:setup'(params) { + // Create a resolver for the Astro project root const { resolve: astroRootResolve } = createResolver(params.config.root.pathname); + // Add virtual imports for the Markdown Remark processor addVirtualImports(params, { name: '@studiocms/markdown-remark', imports: { + // The main Markdown Remark processor 'studiocms:markdown-remark': `export * from '${resolve('./markdown.js')}';`, + // Styles for the Markdown Remark processor 'studiocms:markdown-remark/css': ` import '${resolve('../../assets/headings.css')}'; import '${resolvedCalloutTheme}'; `, + // User defined components for the Markdown processor 'studiocms:markdown-remark/user-components': ` export const componentKeys = ${JSON.stringify(Object.keys(components))}; @@ -47,14 +56,19 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { }, }); + // Inject the CSS for the Markdown processor if enabled if (injectCSS) { params.injectScript('page-ssr', 'import "studiocms:markdown-remark/css";'); } }, 'astro:config:done'({ injectTypes, config }) { + // Create a resolver for the Astro project root const { resolve: astroRootResolve } = createResolver(config.root.pathname); + // Inject the Markdown configuration into the shared state shared.markdownConfig = config.markdown; + + // Inject types for the Markdown Remark processor injectTypes({ filename: 'render.d.ts', content: `// This file is generated by @studiocms/markdown-remark diff --git a/packages/markdown-remark/src/integration/markdown.ts b/packages/markdown-remark/src/integration/markdown.ts index c5009cd..17f410d 100644 --- a/packages/markdown-remark/src/integration/markdown.ts +++ b/packages/markdown-remark/src/integration/markdown.ts @@ -3,6 +3,7 @@ import type { SSRResult } from 'astro'; import { renderSlot } from 'astro/runtime/server/index.js'; import type { RenderTemplateResult } from 'astro/runtime/server/render/astro/render-template.js'; import type { ComponentSlotValue } from 'astro/runtime/server/render/slot.js'; +import type { HTMLAttributes } from 'astro/types'; import { transform } from 'ultrahtml'; import swap from 'ultrahtml/transformers/swap'; import { HTMLString } from '../processor/HTMLString.js'; @@ -92,6 +93,7 @@ export async function render( * Interface representing the properties for a markdown component. * * @property content - The markdown content as a string. + * @property [components] - An object containing the components to be used in the markdown content. * @property [name: string] - An index signature allowing additional properties with string keys and values of any type. */ export interface Props { diff --git a/packages/markdown-remark/src/integration/schema.ts b/packages/markdown-remark/src/integration/schema.ts index 1fa836b..7b6c023 100644 --- a/packages/markdown-remark/src/integration/schema.ts +++ b/packages/markdown-remark/src/integration/schema.ts @@ -52,4 +52,7 @@ export const MarkdownRemarkOptionsSchema = z .optional() .default({}); +/** + * Options for the Markdown Remark processor. + */ export type MarkdownRemarkOptions = typeof MarkdownRemarkOptionsSchema._input; diff --git a/packages/markdown-remark/src/integration/utils.ts b/packages/markdown-remark/src/integration/utils.ts index 1660d6b..6bccdfe 100644 --- a/packages/markdown-remark/src/integration/utils.ts +++ b/packages/markdown-remark/src/integration/utils.ts @@ -4,6 +4,16 @@ import { renderJSX } from 'astro/runtime/server/jsx.js'; import * as entities from 'entities'; import { __unsafeHTML } from 'ultrahtml'; +/** + * Creates a proxy for components that can either be strings or functions. + * If the component is a string, it is directly assigned to the proxy. + * If the component is a function, it is wrapped in an async function that + * processes the props and children before rendering. + * + * @param result - The result object used for rendering JSX. + * @param _components - An optional record of components to be proxied. Defaults to an empty object. + * @returns A record of proxied components. + */ export function createComponentProxy( // biome-ignore lint/suspicious/noExplicitAny: result: any, @@ -33,11 +43,23 @@ export function createComponentProxy( return components; } +/** + * Determines the indentation of a given line of text. + * + * @param ln - The line of text to analyze. + * @returns The leading whitespace characters of the line, or an empty string if there is no indentation. + */ function getIndent(ln: string): string { if (ln.trimStart() === ln) return ''; return ln.slice(0, ln.length - ln.trimStart().length); } +/** + * Removes leading indentation from a multi-line string. + * + * @param str - The string from which to remove leading indentation. + * @returns The dedented string. + */ export function dedent(str: string): string { const lns = str.replace(/^[\r\n]+/, '').split('\n'); let indent = getIndent(lns[0]); @@ -48,6 +70,12 @@ export function dedent(str: string): string { return lns.map((ln) => (ln.startsWith(indent) ? ln.slice(indent.length) : ln)).join('\n'); } +/** + * Merges multiple records into a single record. If there are duplicate keys, the value from the last record with that key will be used. + * + * @param {...Record[]} records - The records to merge. + * @returns {Record} - The merged record. + */ // biome-ignore lint/suspicious/noExplicitAny: export function mergeRecords(...records: Record[]): Record { // biome-ignore lint/suspicious/noExplicitAny: @@ -88,6 +116,13 @@ export function prefixError(err: any, prefix: string): any { return wrappedError; } +/** + * Imports components by their keys from the 'studiocms:markdown-remark/user-components' module. + * + * @param keys - An array of strings representing the keys of the components to import. + * @returns A promise that resolves to an object containing the imported components. + * @throws {MarkdownRemarkError} If any component fails to import, an error is thrown with a prefixed message. + */ export async function importComponentsKeys(keys: string[]) { // biome-ignore lint/suspicious/noExplicitAny: const predefinedComponents: Record = {}; From b2ef01df68e6d8bdbc5b89026d94f1e2b12421a4 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 17:51:46 -0800 Subject: [PATCH 10/17] add changeset --- .changeset/wicked-bikes-raise.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .changeset/wicked-bikes-raise.md diff --git a/.changeset/wicked-bikes-raise.md b/.changeset/wicked-bikes-raise.md new file mode 100644 index 0000000..4cafc0a --- /dev/null +++ b/.changeset/wicked-bikes-raise.md @@ -0,0 +1,27 @@ +--- +"@studiocms/markdown-remark": minor +--- + +Introduce custom User-Defined component handling. + +This update includes significant enhancements to the Markdown Remark processor Astro Integration, allowing for more flexible and powerful Markdown rendering with custom components. + +### New Features: + +- Added custom components support in the Markdown Remark processor Astro Integration. +- Introduced utility functions in `utils.ts` for component proxy creation, indentation handling, dedenting strings, and merging records. +- Moved zod schema to separate `schema.ts` file. + +### Integration Updates: + +- Enhanced Astro integration to support custom components configuration via `astro.config.mjs`. +- Updated `markdown.ts` to include custom components during Markdown rendering. +- Extended `index.ts` to utilize the new schema and utilities. + +### Documentation: + +- Updated `README.md` with instructions for setting up custom components in Astro integration. + +### Dependencies: + +- Added `entities` and `ultrahtml` as new dependencies. \ No newline at end of file From f37a095ac5a9902315ca3b12668defc7e4ec0d58 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 19:22:28 -0800 Subject: [PATCH 11/17] Refactor markdown-remark integration to include callouts in shared state --- .../markdown-remark/src/integration/index.ts | 1 + .../src/integration/markdown.ts | 118 ++++++------------ .../markdown-remark/src/integration/schema.ts | 1 + .../markdown-remark/src/integration/shared.ts | 12 +- .../markdown-remark/src/integration/types.ts | 75 +++++++++++ .../markdown-remark/src/integration/utils.ts | 13 +- 6 files changed, 125 insertions(+), 95 deletions(-) create mode 100644 packages/markdown-remark/src/integration/types.ts diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 78611aa..e1de5cf 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -67,6 +67,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { // Inject the Markdown configuration into the shared state shared.markdownConfig = config.markdown; + shared.callouts = callouts; // Inject types for the Markdown Remark processor injectTypes({ diff --git a/packages/markdown-remark/src/integration/markdown.ts b/packages/markdown-remark/src/integration/markdown.ts index 17f410d..517b6da 100644 --- a/packages/markdown-remark/src/integration/markdown.ts +++ b/packages/markdown-remark/src/integration/markdown.ts @@ -1,64 +1,36 @@ import { componentKeys } from 'studiocms:markdown-remark/user-components'; import type { SSRResult } from 'astro'; import { renderSlot } from 'astro/runtime/server/index.js'; -import type { RenderTemplateResult } from 'astro/runtime/server/render/astro/render-template.js'; -import type { ComponentSlotValue } from 'astro/runtime/server/render/slot.js'; -import type { HTMLAttributes } from 'astro/types'; -import { transform } from 'ultrahtml'; -import swap from 'ultrahtml/transformers/swap'; +import type { SanitizeOptions } from 'ultrahtml/transformers/sanitize'; import { HTMLString } from '../processor/HTMLString.js'; import { - type MarkdownHeading, type MarkdownProcessorRenderOptions, createMarkdownProcessor, } from '../processor/index.js'; import { shared } from './shared.js'; -import { createComponentProxy, dedent, importComponentsKeys, mergeRecords } from './utils.js'; +import type { + ComponentSlots, + MarkdownComponentAttributes, + Props, + RenderComponents, + RenderResponse, +} from './types.js'; +import { + createComponentProxy, + importComponentsKeys, + mergeRecords, + transformHTML, +} from './utils.js'; + +export type { Props, RenderResponse } from './types.js'; const processor = await createMarkdownProcessor({ ...shared.markdownConfig, - callouts: { - theme: 'obsidian', - }, + ...shared.callouts, }); const predefinedComponents = await importComponentsKeys(componentKeys); -/** - * Represents the response from rendering a markdown document. - */ -export interface RenderResponse { - /** - * The rendered HTML content as a string. - */ - html: HTMLString; - - /** - * Metadata extracted from the markdown document. - */ - meta: { - /** - * An array of headings found in the markdown document. - */ - headings: MarkdownHeading[]; - - /** - * An array of image paths found in the markdown document. - */ - imagePaths: string[]; - - /** - * The frontmatter data extracted from the markdown document. - * - * @remarks - * The frontmatter is represented as a record with string keys and values of any type. - */ - - // biome-ignore lint/suspicious/noExplicitAny: - frontmatter: Record; - }; -} - /** * Renders the given markdown content using the specified options. * @@ -69,41 +41,24 @@ export interface RenderResponse { export async function render( content: string, options?: MarkdownProcessorRenderOptions, - _components?: { - // biome-ignore lint/suspicious/noExplicitAny: - $$result: any; - // biome-ignore lint/suspicious/noExplicitAny: - components?: Record; - } + componentProxy?: RenderComponents, + sanitizeOpts?: SanitizeOptions ): Promise { - const allComponents = mergeRecords(predefinedComponents, _components?.components ?? {}); + const componentsRendered = createComponentProxy( + componentProxy?.$$result, + mergeRecords(predefinedComponents, componentProxy?.components ?? {}) + ); - const componentsRendered = createComponentProxy(_components?.$$result, allComponents); + const { code, metadata } = await processor.render(content, options); - const result = await processor.render(content, options); + const html = await transformHTML(code, componentsRendered, sanitizeOpts); - const html = await transform(dedent(result.astroHTML.toString()), [swap(componentsRendered)]); return { html: new HTMLString(html), - meta: result.metadata, + meta: metadata, }; } -/** - * Interface representing the properties for a markdown component. - * - * @property content - The markdown content as a string. - * @property [components] - An object containing the components to be used in the markdown content. - * @property [name: string] - An index signature allowing additional properties with string keys and values of any type. - */ -export interface Props { - content: string; - // biome-ignore lint/suspicious/noExplicitAny: - components?: Record; - // biome-ignore lint/suspicious/noExplicitAny: - [name: string]: any; -} - /** * A factory function for creating an Astro component that renders Markdown content. * @@ -134,29 +89,26 @@ export interface Props { export const Markdown: (props: Props) => any = Object.assign( function Markdown( $$result: SSRResult, - // biome-ignore lint/suspicious/noExplicitAny: - attributes: { content: string; components?: Record }, - slots: { default: ComponentSlotValue | RenderTemplateResult } + { content, components, sanitizeOpts }: MarkdownComponentAttributes, + { default: slotted }: ComponentSlots ) { return { get [Symbol.toStringTag]() { return 'AstroComponent'; }, async *[Symbol.asyncIterator]() { - const mdl = attributes.content; - - if (typeof mdl === 'string') { - const content = await render( - mdl, + if (typeof content === 'string') { + const { html } = await render( + content, { fileURL: new URL(import.meta.url), }, - { $$result, components: attributes.components } + { $$result, components }, + sanitizeOpts ); - - yield content.html; + yield html; } else { - yield renderSlot($$result, slots.default); + yield renderSlot($$result, slotted); } }, }; diff --git a/packages/markdown-remark/src/integration/schema.ts b/packages/markdown-remark/src/integration/schema.ts index 7b6c023..140bfd7 100644 --- a/packages/markdown-remark/src/integration/schema.ts +++ b/packages/markdown-remark/src/integration/schema.ts @@ -56,3 +56,4 @@ export const MarkdownRemarkOptionsSchema = z * Options for the Markdown Remark processor. */ export type MarkdownRemarkOptions = typeof MarkdownRemarkOptionsSchema._input; +export type CalloutConfig = typeof CalloutsSchema._output; diff --git a/packages/markdown-remark/src/integration/shared.ts b/packages/markdown-remark/src/integration/shared.ts index faae39b..414113d 100644 --- a/packages/markdown-remark/src/integration/shared.ts +++ b/packages/markdown-remark/src/integration/shared.ts @@ -1,17 +1,7 @@ -import type { AstroConfig } from 'astro'; +import type { Shared } from './types.js'; export const symbol: symbol = Symbol.for('@studiocms/markdown-remark'); -/** - * Interface representing shared configuration for markdown. - * - * @interface Shared - * @property {AstroConfig['markdown']} markdownConfig - The markdown configuration from AstroConfig. - */ -export interface Shared { - markdownConfig: AstroConfig['markdown']; -} - /** * A shared object that is either retrieved from the global scope using a symbol or * initialized as a new object with a `markdownConfig` property. diff --git a/packages/markdown-remark/src/integration/types.ts b/packages/markdown-remark/src/integration/types.ts new file mode 100644 index 0000000..ba2b673 --- /dev/null +++ b/packages/markdown-remark/src/integration/types.ts @@ -0,0 +1,75 @@ +import type { AstroConfig } from 'astro'; +import type { RenderTemplateResult } from 'astro/runtime/server/render/astro/render-template.js'; +import type { ComponentSlotValue } from 'astro/runtime/server/render/slot.js'; +import type { SanitizeOptions } from 'ultrahtml/transformers/sanitize'; +import type { HTMLString } from '../processor/HTMLString.js'; +import type { MarkdownHeading } from '../processor/index.js'; +import type { CalloutConfig } from './schema.js'; + +/** + * Represents the response from rendering a markdown document. + */ +export interface RenderResponse { + /** + * The rendered HTML content as a string. + */ + html: HTMLString; + + /** + * Metadata extracted from the markdown document. + */ + meta: { + /** + * An array of headings found in the markdown document. + */ + headings: MarkdownHeading[]; + + /** + * An array of image paths found in the markdown document. + */ + imagePaths: string[]; + + /** + * The frontmatter data extracted from the markdown document. + * + * @remarks + * The frontmatter is represented as a record with string keys and values of any type. + */ + + // biome-ignore lint/suspicious/noExplicitAny: + frontmatter: Record; + }; +} + +export interface RenderComponents { + // biome-ignore lint/suspicious/noExplicitAny: + $$result?: any; + // biome-ignore lint/suspicious/noExplicitAny: + components?: Record; +} + +export interface MarkdownComponentAttributes { + content: string; + // biome-ignore lint/suspicious/noExplicitAny: + components?: Record; + sanitizeOpts?: SanitizeOptions; +} + +export interface Props extends MarkdownComponentAttributes { + // biome-ignore lint/suspicious/noExplicitAny: + [name: string]: any; +} + +export interface ComponentSlots { + default: ComponentSlotValue | RenderTemplateResult; +} +/** + * Interface representing shared configuration for markdown. + * + * @interface Shared + * @property {AstroConfig['markdown']} markdownConfig - The markdown configuration from AstroConfig. + */ +export interface Shared { + markdownConfig: AstroConfig['markdown']; + callouts: CalloutConfig; +} diff --git a/packages/markdown-remark/src/integration/utils.ts b/packages/markdown-remark/src/integration/utils.ts index 6bccdfe..618a9e3 100644 --- a/packages/markdown-remark/src/integration/utils.ts +++ b/packages/markdown-remark/src/integration/utils.ts @@ -2,7 +2,9 @@ import { AstroError } from 'astro/errors'; import { jsx as h } from 'astro/jsx-runtime'; import { renderJSX } from 'astro/runtime/server/jsx.js'; import * as entities from 'entities'; -import { __unsafeHTML } from 'ultrahtml'; +import { __unsafeHTML, transform } from 'ultrahtml'; +import sanitize, { type SanitizeOptions } from 'ultrahtml/transformers/sanitize'; +import swap from 'ultrahtml/transformers/swap'; /** * Creates a proxy for components that can either be strings or functions. @@ -144,3 +146,12 @@ export async function importComponentsKeys(keys: string[]) { return predefinedComponents; } + +export async function transformHTML( + html: string, + // biome-ignore lint/suspicious/noExplicitAny: + components: Record, + sanitizeOpts?: SanitizeOptions +): Promise { + return await transform(dedent(html), [sanitize(sanitizeOpts), swap(components)]); +} From 9a32164375afb9d8627e357a0d52a3d39f99a69f Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 19:49:04 -0800 Subject: [PATCH 12/17] ensure all components are auto-converted to lowercase so they meet HTML spec --- packages/markdown-remark/src/integration/index.ts | 6 +++--- packages/markdown-remark/src/integration/utils.ts | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index e1de5cf..94afd6a 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -44,12 +44,12 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { `, // User defined components for the Markdown processor 'studiocms:markdown-remark/user-components': ` - export const componentKeys = ${JSON.stringify(Object.keys(components))}; + export const componentKeys = ${JSON.stringify(Object.keys(components).map((name) => name.toLowerCase()))}; ${Object.entries(components) .map( ([name, path]) => - `export { default as ${name} } from '${astroRootResolve(path)}';` + `export { default as ${name.toLowerCase()} } from '${astroRootResolve(path)}';` ) .join('\n')} `, @@ -87,7 +87,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { ${Object.entries(components) .map( ([name, path]) => - `export const ${name}: typeof import('${astroRootResolve(path)}').default;` + `export const ${name.toLowerCase()}: typeof import('${astroRootResolve(path)}').default;` ) .join('\n')} } diff --git a/packages/markdown-remark/src/integration/utils.ts b/packages/markdown-remark/src/integration/utils.ts index 618a9e3..2f2fe09 100644 --- a/packages/markdown-remark/src/integration/utils.ts +++ b/packages/markdown-remark/src/integration/utils.ts @@ -26,15 +26,15 @@ export function createComponentProxy( const components: Record = {}; for (const [key, value] of Object.entries(_components)) { if (typeof value === 'string') { - components[key] = value; + components[key.toLowerCase()] = value; } else { - components[key] = async ( + components[key.toLowerCase()] = async ( // biome-ignore lint/suspicious/noExplicitAny: props: Record, // biome-ignore lint/suspicious/noExplicitAny: children: { value: any } ) => { - if (key === 'CodeBlock' || key === 'CodeSpan') { + if (key === 'codeblock' || key === 'codespan') { props.code = entities.decode(JSON.parse(`"${props.code}"`)); } const output = await renderJSX(result, h(value, { ...props, 'set:html': children.value })); @@ -84,7 +84,7 @@ export function mergeRecords(...records: Record[]): Record = {}; for (const record of records) { for (const [key, value] of Object.entries(record)) { - result[key] = value; + result[key.toLowerCase()] = value; } } return result; @@ -131,7 +131,9 @@ export async function importComponentsKeys(keys: string[]) { for (const key of keys) { try { - predefinedComponents[key] = (await import('studiocms:markdown-remark/user-components'))[key]; + predefinedComponents[key.toLowerCase()] = ( + await import('studiocms:markdown-remark/user-components') + )[key.toLowerCase()]; } catch (e) { if (e instanceof Error) { const newErr = prefixError(e, `Failed to import component "${key}"`); From 19dddd174c44c04a679cdcdb5d89c1adc0da0edd Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 20:44:25 -0800 Subject: [PATCH 13/17] refactor config --- .../markdown-remark/src/integration/index.ts | 4 +-- .../src/integration/markdown.ts | 2 +- .../markdown-remark/src/integration/schema.ts | 5 +++ .../markdown-remark/src/integration/types.ts | 5 ++- .../markdown-remark/src/processor/index.ts | 34 ++++++++++++++++--- .../markdown-remark/src/processor/types.ts | 13 +++++-- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 94afd6a..e887e85 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -15,7 +15,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { // Parse the options const { injectCSS, - markdown: { callouts, components }, + markdown: { callouts, components, autolink }, } = MarkdownRemarkOptionsSchema.parse(opts); // Create a resolver for the current file @@ -67,7 +67,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { // Inject the Markdown configuration into the shared state shared.markdownConfig = config.markdown; - shared.callouts = callouts; + shared.studiocms = { callouts, autolink }; // Inject types for the Markdown Remark processor injectTypes({ diff --git a/packages/markdown-remark/src/integration/markdown.ts b/packages/markdown-remark/src/integration/markdown.ts index 517b6da..f4cbebc 100644 --- a/packages/markdown-remark/src/integration/markdown.ts +++ b/packages/markdown-remark/src/integration/markdown.ts @@ -26,7 +26,7 @@ export type { Props, RenderResponse } from './types.js'; const processor = await createMarkdownProcessor({ ...shared.markdownConfig, - ...shared.callouts, + studiocms: shared.studiocms, }); const predefinedComponents = await importComponentsKeys(componentKeys); diff --git a/packages/markdown-remark/src/integration/schema.ts b/packages/markdown-remark/src/integration/schema.ts index 140bfd7..271242b 100644 --- a/packages/markdown-remark/src/integration/schema.ts +++ b/packages/markdown-remark/src/integration/schema.ts @@ -26,6 +26,11 @@ const MarkdownSchema = z */ callouts: CalloutsSchema, + /** + * Enables autolinking of headings. + */ + autolink: z.boolean().optional().default(true), + /** * Configures the user defined components for the Markdown processor. */ diff --git a/packages/markdown-remark/src/integration/types.ts b/packages/markdown-remark/src/integration/types.ts index ba2b673..841f5b2 100644 --- a/packages/markdown-remark/src/integration/types.ts +++ b/packages/markdown-remark/src/integration/types.ts @@ -71,5 +71,8 @@ export interface ComponentSlots { */ export interface Shared { markdownConfig: AstroConfig['markdown']; - callouts: CalloutConfig; + studiocms: { + callouts: CalloutConfig; + autolink: boolean; + }; } diff --git a/packages/markdown-remark/src/processor/index.ts b/packages/markdown-remark/src/processor/index.ts index 8590a22..fceeede 100644 --- a/packages/markdown-remark/src/processor/index.ts +++ b/packages/markdown-remark/src/processor/index.ts @@ -65,8 +65,11 @@ export const markdownConfigDefaults: Required = { remarkRehype: {}, gfm: true, smartypants: true, - callouts: { - theme: 'obsidian', + studiocms: { + callouts: { + theme: 'obsidian', + }, + autolink: true, }, }; @@ -104,7 +107,7 @@ export async function createMarkdownProcessor( remarkRehype: remarkRehypeOptions = markdownConfigDefaults.remarkRehype, gfm = markdownConfigDefaults.gfm, smartypants = markdownConfigDefaults.smartypants, - callouts = markdownConfigDefaults.callouts, + studiocms = markdownConfigDefaults.studiocms, } = opts ?? {}; const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins)); @@ -156,11 +159,32 @@ export async function createMarkdownProcessor( // Headings parser.use(rehypeHeadingIds); + let autolink = true; + let calloutsEnabled = true; + let calloutsConfig: { + theme?: 'github' | 'obsidian' | 'vitepress'; + } = { theme: 'obsidian' }; + + if (typeof studiocms === 'boolean') { + autolink = studiocms; + calloutsEnabled = studiocms; + } else if (typeof studiocms === 'object') { + autolink = studiocms.autolink ?? autolink; + calloutsEnabled = studiocms.callouts !== false; + if (typeof studiocms.callouts === 'object') { + calloutsConfig = studiocms.callouts; + } + } + // Autolink headings - parser.use(rehypeAutoLink, rehypeAutolinkOptions); + if (autolink) { + parser.use(rehypeAutoLink, rehypeAutolinkOptions); + } // Callouts - parser.use(rehypeCallouts, callouts); + if (calloutsEnabled) { + parser.use(rehypeCallouts, calloutsConfig); + } // Stringify to HTML parser.use(rehypeRaw).use(rehypeStringify, { allowDangerousHtml: true }); diff --git a/packages/markdown-remark/src/processor/types.ts b/packages/markdown-remark/src/processor/types.ts index 6449155..2515ef7 100644 --- a/packages/markdown-remark/src/processor/types.ts +++ b/packages/markdown-remark/src/processor/types.ts @@ -56,9 +56,16 @@ export interface AstroMarkdownOptions { } export interface StudioCMSMarkdownOptions extends AstroMarkdownOptions { - callouts?: { - theme?: 'github' | 'obsidian' | 'vitepress'; - }; + studiocms?: + | { + callouts?: + | { + theme?: 'github' | 'obsidian' | 'vitepress'; + } + | false; + autolink?: boolean; + } + | false; } export interface MarkdownProcessor { From bcc73372be8adaeac80257460f76721d1e72cdd4 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 22:23:57 -0800 Subject: [PATCH 14/17] Refactor markdown-remark integration to optimize config --- packages/markdown-remark/src/index.ts | 6 +- .../markdown-remark/src/integration/index.ts | 25 ++-- .../src/integration/markdown.ts | 11 +- .../markdown-remark/src/integration/schema.ts | 121 +++++++++++------- .../markdown-remark/src/integration/types.ts | 7 +- .../markdown-remark/src/processor/index.ts | 33 +++-- .../markdown-remark/src/processor/types.ts | 20 +-- .../tests/fixture/astro/astro.config.ts | 6 +- packages/markdown-remark/tsconfig.json | 2 +- packages/markdown-remark/{src => }/v.d.ts | 0 10 files changed, 134 insertions(+), 97 deletions(-) rename packages/markdown-remark/{src => }/v.d.ts (100%) diff --git a/packages/markdown-remark/src/index.ts b/packages/markdown-remark/src/index.ts index 18d4a0e..4db0445 100644 --- a/packages/markdown-remark/src/index.ts +++ b/packages/markdown-remark/src/index.ts @@ -1,2 +1,6 @@ export * from './processor/index.js'; -export { markdownRemark, default, type MarkdownRemarkOptions } from './integration/index.js'; +export { + markdownRemark, + default, + type StudioCMSMarkdownRemarkOptions, +} from './integration/index.js'; diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index e887e85..8baf974 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -1,28 +1,31 @@ import type { AstroIntegration } from 'astro'; import { addVirtualImports, createResolver } from 'astro-integration-kit'; -import { type MarkdownRemarkOptions, MarkdownRemarkOptionsSchema } from './schema.js'; +import { + type StudioCMSMarkdownRemarkOptions, + StudioCMSMarkdownRemarkOptionsSchema, +} from './schema.js'; import { shared } from './shared.js'; -export type { MarkdownRemarkOptions } from './schema.js'; +export type { StudioCMSMarkdownRemarkOptions } from './schema.js'; /** * Integrates the Markdown Remark processor into Astro available as `studiocms:markdown-remark`. * - * @param {MarkdownRemarkOptions} opts Options for the Markdown Remark processor. + * @param {StudioCMSMarkdownRemarkOptions} opts Options for the Markdown Remark processor. * @returns Astro integration. */ -export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { +export function markdownRemark(opts?: StudioCMSMarkdownRemarkOptions): AstroIntegration { // Parse the options - const { - injectCSS, - markdown: { callouts, components, autolink }, - } = MarkdownRemarkOptionsSchema.parse(opts); + const { injectCSS, components, markdownExtended } = + StudioCMSMarkdownRemarkOptionsSchema.parse(opts); // Create a resolver for the current file const { resolve } = createResolver(import.meta.url); // Resolve the callout theme based on the user's configuration - const resolvedCalloutTheme = resolve(`../../assets/callout-themes/${callouts.theme}.css`); + const resolvedCalloutTheme = resolve( + `../../assets/callout-themes/${markdownExtended.callouts.theme}.css` + ); return { name: '@studiocms/markdown-remark', @@ -40,7 +43,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { // Styles for the Markdown Remark processor 'studiocms:markdown-remark/css': ` import '${resolve('../../assets/headings.css')}'; - import '${resolvedCalloutTheme}'; + ${markdownExtended.callouts.enabled ? `import '${resolvedCalloutTheme}';` : ''} `, // User defined components for the Markdown processor 'studiocms:markdown-remark/user-components': ` @@ -67,7 +70,7 @@ export function markdownRemark(opts?: MarkdownRemarkOptions): AstroIntegration { // Inject the Markdown configuration into the shared state shared.markdownConfig = config.markdown; - shared.studiocms = { callouts, autolink }; + shared.studiocms = markdownExtended; // Inject types for the Markdown Remark processor injectTypes({ diff --git a/packages/markdown-remark/src/integration/markdown.ts b/packages/markdown-remark/src/integration/markdown.ts index f4cbebc..728c372 100644 --- a/packages/markdown-remark/src/integration/markdown.ts +++ b/packages/markdown-remark/src/integration/markdown.ts @@ -7,6 +7,7 @@ import { type MarkdownProcessorRenderOptions, createMarkdownProcessor, } from '../processor/index.js'; +import { TransformToProcessor } from './schema.js'; import { shared } from './shared.js'; import type { ComponentSlots, @@ -24,9 +25,11 @@ import { export type { Props, RenderResponse } from './types.js'; +const studiocmsMarkdownExtended = TransformToProcessor.parse(shared.studiocms); + const processor = await createMarkdownProcessor({ ...shared.markdownConfig, - studiocms: shared.studiocms, + ...studiocmsMarkdownExtended, }); const predefinedComponents = await importComponentsKeys(componentKeys); @@ -51,7 +54,11 @@ export async function render( const { code, metadata } = await processor.render(content, options); - const html = await transformHTML(code, componentsRendered, sanitizeOpts); + const html = await transformHTML( + code, + componentsRendered, + sanitizeOpts ?? shared.studiocms.sanitize + ); return { html: new HTMLString(html), diff --git a/packages/markdown-remark/src/integration/schema.ts b/packages/markdown-remark/src/integration/schema.ts index 271242b..f26cef7 100644 --- a/packages/markdown-remark/src/integration/schema.ts +++ b/packages/markdown-remark/src/integration/schema.ts @@ -1,64 +1,93 @@ import { z } from 'astro/zod'; +import type { StudioCMSConfigOptions } from '../processor/types.js'; -/** - * Options for the Markdown Callouts. - */ -const CalloutsSchema = z +export const StudioCMSSanitizeOptionsSchema = z .object({ - /** - * The theme to use for callouts. - */ - theme: z - .union([z.literal('github'), z.literal('obsidian'), z.literal('vitepress')]) - .optional() - .default('obsidian'), + /** An Array of strings indicating elements that the sanitizer should not remove. All elements not in the array will be dropped. */ + allowElements: z.array(z.string()).optional(), + /** An Array of strings indicating elements that the sanitizer should remove, but keeping their child elements. */ + blockElements: z.array(z.string()).optional(), + /** An Array of strings indicating elements (including nested elements) that the sanitizer should remove. */ + dropElements: z.array(z.string()).optional(), + /** An Object where each key is the attribute name and the value is an Array of allowed tag names. Matching attributes will not be removed. All attributes that are not in the array will be dropped. */ + allowAttributes: z.record(z.array(z.string())).optional(), + /** An Object where each key is the attribute name and the value is an Array of dropped tag names. Matching attributes will be removed. */ + dropAttributes: z.record(z.array(z.string())).optional(), + /** A Boolean value set to false (default) to remove components and their children. If set to true, components will be subject to built-in and custom configuration checks (and will be retained or dropped based on those checks). */ + allowComponents: z.boolean().optional(), + /** A Boolean value set to false (default) to remove custom elements and their children. If set to true, custom elements will be subject to built-in and custom configuration checks (and will be retained or dropped based on those checks). */ + allowCustomElements: z.boolean().optional(), + /** A Boolean value set to false (default) to remove HTML comments. Set to true in order to keep comments. */ + allowComments: z.boolean().optional(), }) - .optional() - .default({}); + .optional(); -/** - * Extended options for the Astro Integration for Markdown Remark. Used to control how Markdown is processed. - */ -const MarkdownSchema = z - .object({ - /** - * Configures the callouts theme. - */ - callouts: CalloutsSchema, +export const StudioCMSMarkdownExtendedSchema = z + .union([ + z.literal(false), + z.object({ + callouts: z + .union([ + z.literal(false), + z.object({ + theme: z + .union([z.literal('github'), z.literal('obsidian'), z.literal('vitepress')]) + .optional() + .default('obsidian'), + }), + ]) + .optional() + .default({}) + .transform((value) => { + if (value === false) { + return { theme: 'obsidian' as const, enabled: false }; + } + return { ...value, enabled: true }; + }), - /** - * Enables autolinking of headings. - */ - autolink: z.boolean().optional().default(true), + autoLinkHeadings: z.boolean().optional().default(true), - /** - * Configures the user defined components for the Markdown processor. - */ - components: z.record(z.string(), z.string()).optional().default({}), - }) + sanitize: StudioCMSSanitizeOptionsSchema, + }), + ]) .optional() - .default({}); + .default({}) + .transform((value) => { + if (value === false) { + return { + callouts: { enabled: false, theme: 'obsidian' as const }, + autoLinkHeadings: false, + }; + } + return value; + }); -/** - * Options for the Markdown Remark processor. - */ -export const MarkdownRemarkOptionsSchema = z +export const StudioCMSMarkdownRemarkOptionsSchema = z .object({ /** - * Inject CSS for the Markdown processor. + * Inject CSS for Rendering Markdown content. */ injectCSS: z.boolean().optional().default(true), - /** - * Extended options for the Astro Integration for Markdown Remark. Used to control how Markdown is processed. - */ - markdown: MarkdownSchema, + components: z.record(z.string(), z.string()).optional().default({}), + + markdownExtended: StudioCMSMarkdownExtendedSchema, }) .optional() .default({}); -/** - * Options for the Markdown Remark processor. - */ -export type MarkdownRemarkOptions = typeof MarkdownRemarkOptionsSchema._input; -export type CalloutConfig = typeof CalloutsSchema._output; +export const TransformToProcessor = StudioCMSMarkdownExtendedSchema.transform( + ({ autoLinkHeadings, callouts }) => { + return { + studiocms: { + callouts: callouts.enabled ? { theme: callouts.theme } : false, + autolink: autoLinkHeadings, + }, + } as { studiocms: StudioCMSConfigOptions }; + } +); + +export type StudioCMSMarkdownExtendedOptions = typeof StudioCMSMarkdownExtendedSchema._input; +export type StudioCMSMarkdownExtendedConfig = z.infer; +export type StudioCMSMarkdownRemarkOptions = typeof StudioCMSMarkdownRemarkOptionsSchema._input; +export type StudioCMSMarkdownRemarkConfig = z.infer; diff --git a/packages/markdown-remark/src/integration/types.ts b/packages/markdown-remark/src/integration/types.ts index 841f5b2..4b9f25c 100644 --- a/packages/markdown-remark/src/integration/types.ts +++ b/packages/markdown-remark/src/integration/types.ts @@ -4,7 +4,7 @@ import type { ComponentSlotValue } from 'astro/runtime/server/render/slot.js'; import type { SanitizeOptions } from 'ultrahtml/transformers/sanitize'; import type { HTMLString } from '../processor/HTMLString.js'; import type { MarkdownHeading } from '../processor/index.js'; -import type { CalloutConfig } from './schema.js'; +import type { StudioCMSMarkdownExtendedConfig } from './schema.js'; /** * Represents the response from rendering a markdown document. @@ -71,8 +71,5 @@ export interface ComponentSlots { */ export interface Shared { markdownConfig: AstroConfig['markdown']; - studiocms: { - callouts: CalloutConfig; - autolink: boolean; - }; + studiocms: StudioCMSMarkdownExtendedConfig; } diff --git a/packages/markdown-remark/src/processor/index.ts b/packages/markdown-remark/src/processor/index.ts index fceeede..f7ffc6c 100644 --- a/packages/markdown-remark/src/processor/index.ts +++ b/packages/markdown-remark/src/processor/index.ts @@ -19,6 +19,7 @@ import { remarkCollectImages } from './remark-collect-images.js'; import type { MarkdownProcessor, MarkdownProcessorRenderResult, + StudioCMSCalloutOptions, StudioCMSMarkdownOptions, } from './types.js'; @@ -110,6 +111,21 @@ export async function createMarkdownProcessor( studiocms = markdownConfigDefaults.studiocms, } = opts ?? {}; + let autolink = true; + let calloutsEnabled = true; + let calloutsConfig: StudioCMSCalloutOptions = { theme: 'obsidian' }; + + if (typeof studiocms === 'boolean') { + autolink = studiocms; + calloutsEnabled = studiocms; + } else if (typeof studiocms === 'object') { + autolink = studiocms.autolink ?? autolink; + calloutsEnabled = studiocms.callouts !== false; + if (typeof studiocms.callouts === 'object') { + calloutsConfig = studiocms.callouts; + } + } + const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins)); const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins)); @@ -159,23 +175,6 @@ export async function createMarkdownProcessor( // Headings parser.use(rehypeHeadingIds); - let autolink = true; - let calloutsEnabled = true; - let calloutsConfig: { - theme?: 'github' | 'obsidian' | 'vitepress'; - } = { theme: 'obsidian' }; - - if (typeof studiocms === 'boolean') { - autolink = studiocms; - calloutsEnabled = studiocms; - } else if (typeof studiocms === 'object') { - autolink = studiocms.autolink ?? autolink; - calloutsEnabled = studiocms.callouts !== false; - if (typeof studiocms.callouts === 'object') { - calloutsConfig = studiocms.callouts; - } - } - // Autolink headings if (autolink) { parser.use(rehypeAutoLink, rehypeAutolinkOptions); diff --git a/packages/markdown-remark/src/processor/types.ts b/packages/markdown-remark/src/processor/types.ts index 2515ef7..c148075 100644 --- a/packages/markdown-remark/src/processor/types.ts +++ b/packages/markdown-remark/src/processor/types.ts @@ -55,17 +55,17 @@ export interface AstroMarkdownOptions { smartypants?: boolean; } +export interface StudioCMSCalloutOptions { + theme?: 'github' | 'obsidian' | 'vitepress'; +} + +export interface StudioCMSConfigOptions { + callouts?: StudioCMSCalloutOptions | false; + autolink?: boolean; +} + export interface StudioCMSMarkdownOptions extends AstroMarkdownOptions { - studiocms?: - | { - callouts?: - | { - theme?: 'github' | 'obsidian' | 'vitepress'; - } - | false; - autolink?: boolean; - } - | false; + studiocms?: StudioCMSConfigOptions | false; } export interface MarkdownProcessor { diff --git a/packages/markdown-remark/tests/fixture/astro/astro.config.ts b/packages/markdown-remark/tests/fixture/astro/astro.config.ts index d8b5684..affc995 100644 --- a/packages/markdown-remark/tests/fixture/astro/astro.config.ts +++ b/packages/markdown-remark/tests/fixture/astro/astro.config.ts @@ -4,10 +4,8 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ integrations: [ markdownRemark({ - markdown: { - components: { - custom: './src/pages/custom-components/_comps/Custom.astro', - }, + components: { + custom: './src/pages/custom-components/_comps/Custom.astro', }, }), ], diff --git a/packages/markdown-remark/tsconfig.json b/packages/markdown-remark/tsconfig.json index 8f97fc1..8881889 100644 --- a/packages/markdown-remark/tsconfig.json +++ b/packages/markdown-remark/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"], + "include": ["src", "v.d.ts"], "compilerOptions": { "outDir": "./dist", "rootDir": "./src" diff --git a/packages/markdown-remark/src/v.d.ts b/packages/markdown-remark/v.d.ts similarity index 100% rename from packages/markdown-remark/src/v.d.ts rename to packages/markdown-remark/v.d.ts From 2b0ae426c87f81b553c4cb31428795584b4160d7 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 22:27:44 -0800 Subject: [PATCH 15/17] Refactor markdown-remark integration to update import paths --- packages/markdown-remark/v.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/markdown-remark/v.d.ts b/packages/markdown-remark/v.d.ts index ea552fc..e39b685 100644 --- a/packages/markdown-remark/v.d.ts +++ b/packages/markdown-remark/v.d.ts @@ -1,6 +1,6 @@ declare module 'studiocms:markdown-remark' { - export const Markdown: typeof import('../../../src/integration/markdown').Markdown; - export const render: typeof import('../../../src/integration/markdown').render; + export const Markdown: typeof import('./src/integration/markdown.js').Markdown; + export const render: typeof import('./src/integration/markdown.js').render; } declare module 'studiocms:markdown-remark/user-components' { From 7125e52442d47e885b9d3331a06296805cc77178 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Sun, 5 Jan 2025 22:32:00 -0800 Subject: [PATCH 16/17] Update readme to match new schema --- packages/markdown-remark/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/markdown-remark/README.md b/packages/markdown-remark/README.md index 2f1c9e6..04108cb 100644 --- a/packages/markdown-remark/README.md +++ b/packages/markdown-remark/README.md @@ -49,17 +49,19 @@ export default defineConfig({ integrations: [markdownRemark({ // Used for injecting CSS for Headings and Callouts injectCSS: true, + // User defined components that will be used when processing markdown + components: { + // Example of a custom defined component + custom: "./src/components/Custom.astro", + }, // Custom Markdown config markdown: { // Configure the available callout themes callouts: { theme: 'obsidian' // Can also be 'github' or 'vitepress' }, - // User defined components that will be used when processing markdown - components: { - // Example of a custom defined component - custom: "./src/components/Custom.astro", - } + autoLinkHeadings: true, + sanitize: {} // see https://github.com/natemoo-re/ultrahtml?tab=readme-ov-file#sanitization for full options } })], }); From ccfbf942237df1a66b8a0eb6e63b2953d3b0b8d2 Mon Sep 17 00:00:00 2001 From: Adam Matthiesen Date: Mon, 6 Jan 2025 00:57:57 -0800 Subject: [PATCH 17/17] Refactor markdown-remark integration to update import paths and optimize config --- packages/markdown-remark/package.json | 11 +++++------ packages/markdown-remark/src/integration/index.ts | 4 ++-- .../integration/styles}/callout-themes/github.css | 0 .../integration/styles}/callout-themes/obsidian.css | 0 .../integration/styles}/callout-themes/vitepress.css | 0 .../{assets => src/integration/styles}/headings.css | 0 6 files changed, 7 insertions(+), 8 deletions(-) rename packages/markdown-remark/{assets => src/integration/styles}/callout-themes/github.css (100%) rename packages/markdown-remark/{assets => src/integration/styles}/callout-themes/obsidian.css (100%) rename packages/markdown-remark/{assets => src/integration/styles}/callout-themes/vitepress.css (100%) rename packages/markdown-remark/{assets => src/integration/styles}/headings.css (100%) diff --git a/packages/markdown-remark/package.json b/packages/markdown-remark/package.json index 587f86c..6cd3fda 100644 --- a/packages/markdown-remark/package.json +++ b/packages/markdown-remark/package.json @@ -21,7 +21,7 @@ "types": "./dist/processor/index.d.ts", "default": "./dist/processor/index.js" }, - "./assets/*": "./assets/*" + "./styles/*": "./dist/integration/styles/*" }, "imports": { "#import-plugin": { @@ -30,14 +30,13 @@ } }, "files": [ - "dist", - "assets" + "dist" ], "scripts": { "prepublish": "pnpm build", - "build": "run-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json", - "build:ci": "run-scripts build \"src/**/*.ts\"", - "dev": "run-scripts dev \"src/**/*.ts\"", + "build": "run-scripts build \"src/**/*.{ts,css}\" && tsc -p tsconfig.json", + "build:ci": "run-scripts build \"src/**/*.{ts,css}\"", + "dev": "run-scripts dev \"src/**/*.{ts,css}\"", "test": "pnpm build && vitest run" }, "peerDependencies": { diff --git a/packages/markdown-remark/src/integration/index.ts b/packages/markdown-remark/src/integration/index.ts index 8baf974..ae3661f 100644 --- a/packages/markdown-remark/src/integration/index.ts +++ b/packages/markdown-remark/src/integration/index.ts @@ -24,7 +24,7 @@ export function markdownRemark(opts?: StudioCMSMarkdownRemarkOptions): AstroInte // Resolve the callout theme based on the user's configuration const resolvedCalloutTheme = resolve( - `../../assets/callout-themes/${markdownExtended.callouts.theme}.css` + `./styles/callout-themes/${markdownExtended.callouts.theme}.css` ); return { @@ -42,7 +42,7 @@ export function markdownRemark(opts?: StudioCMSMarkdownRemarkOptions): AstroInte 'studiocms:markdown-remark': `export * from '${resolve('./markdown.js')}';`, // Styles for the Markdown Remark processor 'studiocms:markdown-remark/css': ` - import '${resolve('../../assets/headings.css')}'; + import '${resolve('./styles/headings.css')}'; ${markdownExtended.callouts.enabled ? `import '${resolvedCalloutTheme}';` : ''} `, // User defined components for the Markdown processor diff --git a/packages/markdown-remark/assets/callout-themes/github.css b/packages/markdown-remark/src/integration/styles/callout-themes/github.css similarity index 100% rename from packages/markdown-remark/assets/callout-themes/github.css rename to packages/markdown-remark/src/integration/styles/callout-themes/github.css diff --git a/packages/markdown-remark/assets/callout-themes/obsidian.css b/packages/markdown-remark/src/integration/styles/callout-themes/obsidian.css similarity index 100% rename from packages/markdown-remark/assets/callout-themes/obsidian.css rename to packages/markdown-remark/src/integration/styles/callout-themes/obsidian.css diff --git a/packages/markdown-remark/assets/callout-themes/vitepress.css b/packages/markdown-remark/src/integration/styles/callout-themes/vitepress.css similarity index 100% rename from packages/markdown-remark/assets/callout-themes/vitepress.css rename to packages/markdown-remark/src/integration/styles/callout-themes/vitepress.css diff --git a/packages/markdown-remark/assets/headings.css b/packages/markdown-remark/src/integration/styles/headings.css similarity index 100% rename from packages/markdown-remark/assets/headings.css rename to packages/markdown-remark/src/integration/styles/headings.css