diff --git a/docs/content/3.rendering/3.vue.md b/docs/content/3.rendering/3.vue.md index 9ffbafb4..39eaf4c4 100644 --- a/docs/content/3.rendering/3.vue.md +++ b/docs/content/3.rendering/3.vue.md @@ -332,6 +332,8 @@ interface DefineComarkComponentOptions { autoClose?: boolean plugins?: ComarkPlugin[] components?: Record + /** Additional classes for the wrapper div */ + class?: string } ``` @@ -477,6 +479,8 @@ interface DefineComarkRendererOptions { extends?: ReturnType name?: string components?: Record + /** Additional classes for the wrapper div */ + class?: string } ``` diff --git a/docs/content/3.rendering/4.react.md b/docs/content/3.rendering/4.react.md index ceeed0d2..9007102f 100644 --- a/docs/content/3.rendering/4.react.md +++ b/docs/content/3.rendering/4.react.md @@ -329,6 +329,8 @@ interface DefineComarkComponentOptions { autoClose?: boolean plugins?: ComarkPlugin[] components?: Record> + /** Additional classes for the wrapper div */ + className?: string } ``` @@ -489,6 +491,8 @@ interface DefineComarkRendererOptions { extends?: React.FC name?: string components?: Record> + /** Additional classes for the wrapper div */ + className?: string } ``` diff --git a/packages/comark-react/src/index.ts b/packages/comark-react/src/index.ts index 5c51a836..b9056468 100644 --- a/packages/comark-react/src/index.ts +++ b/packages/comark-react/src/index.ts @@ -16,6 +16,10 @@ interface DefineComarkComponentOptions extends ParseOptions { /** Display name shown in React DevTools. */ name?: string components?: Record> + /** + * Additional classes for the wrapper div + */ + className?: string } /** @@ -42,7 +46,7 @@ interface DefineComarkComponentOptions extends ParseOptions { * ``` */ export function defineComarkComponent(config: DefineComarkComponentOptions = {}) { - const { name, components: configComponents = {}, extends: BaseComponent, ...parseOptions } = config + const { name, components: configComponents = {}, className: configClassName, extends: BaseComponent, ...parseOptions } = config const ComarkComponent: React.FC = (props) => { const mergedOptions: Exclude = { @@ -60,11 +64,14 @@ export function defineComarkComponent(config: DefineComarkComponentOptions = {}) ...props.components, } + const mergedClassName = [configClassName, props.className].filter(Boolean).join(' ') || undefined + return React.createElement(BaseComponent ?? Comark, { ...props, options: mergedOptions, plugins: mergedPlugins, components: mergedComponents, + className: mergedClassName, }) } @@ -79,6 +86,10 @@ interface DefineComarkRendererOptions { /** Display name shown in React DevTools. */ name?: string components?: Record> + /** + * Additional classes for the wrapper div + */ + className?: string } /** @@ -108,7 +119,7 @@ interface DefineComarkRendererOptions { * ``` */ export function defineComarkRendererComponent(config: DefineComarkRendererOptions = {}) { - const { name, components: configComponents = {}, extends: BaseComponent } = config + const { name, components: configComponents = {}, className: configClassName, extends: BaseComponent } = config const RendererComponent: React.FC = (props) => { const mergedComponents = { @@ -116,9 +127,12 @@ export function defineComarkRendererComponent(config: DefineComarkRendererOption ...props.components, } + const mergedClassName = [configClassName, props.className].filter(Boolean).join(' ') || undefined + return React.createElement(BaseComponent ?? ComarkRenderer, { ...props, components: mergedComponents, + className: mergedClassName, }) } diff --git a/packages/comark-react/test/define-component.test.tsx b/packages/comark-react/test/define-component.test.tsx index cefae5fa..db19e444 100644 --- a/packages/comark-react/test/define-component.test.tsx +++ b/packages/comark-react/test/define-component.test.tsx @@ -118,6 +118,37 @@ describe('defineComarkComponent — plugin inheritance via extends', () => { }) }) +// --------------------------------------------------------------------------- +// defineComarkComponent — className +// --------------------------------------------------------------------------- + +describe('defineComarkComponent — className via config', () => { + it('applies config className to wrapper div', async () => { + const Custom = defineComarkComponent({ name: 'WithClass', className: 'prose dark' }) + const html = await renderAsync() + expect(html).toContain('comark-content prose dark') + }) + + it('merges config className with prop className', async () => { + const Custom = defineComarkComponent({ name: 'WithClass', className: 'prose' }) + const html = await renderAsync() + expect(html).toContain('prose extra') + }) + + it('prop className works without config className', async () => { + const Custom = defineComarkComponent({ name: 'NoConfigClass' }) + const html = await renderAsync() + expect(html).toContain('comark-content only-prop') + }) + + it('inherited component preserves parent className', async () => { + const Base = defineComarkComponent({ name: 'Base', className: 'base-class' }) + const Child = defineComarkComponent({ name: 'Child', extends: Base }) + const html = await renderAsync() + expect(html).toContain('base-class') + }) +}) + // --------------------------------------------------------------------------- // defineComarkRendererComponent // --------------------------------------------------------------------------- @@ -190,3 +221,39 @@ describe('defineComarkRendererComponent — component inheritance via extends', expect(html).toContain('card-base') }) }) + +// --------------------------------------------------------------------------- +// defineComarkRendererComponent — className +// --------------------------------------------------------------------------- + +describe('defineComarkRendererComponent — className via config', () => { + it('applies config className to wrapper div', async () => { + const Renderer = defineComarkRendererComponent({ name: 'WithClass', className: 'prose dark' }) + const tree = await parse('hello') + const html = await renderAsync() + expect(html).toContain('comark-content prose dark') + }) + + it('merges config className with prop className', async () => { + const Renderer = defineComarkRendererComponent({ name: 'WithClass', className: 'prose' }) + const tree = await parse('hello') + const html = await renderAsync() + expect(html).toContain('prose extra') + }) + + it('prop className works without config className', async () => { + const Renderer = defineComarkRendererComponent({ name: 'NoConfigClass' }) + const tree = await parse('hello') + const html = await renderAsync() + expect(html).toContain('comark-content only-prop') + }) + + it('inherited renderer preserves parent className', async () => { + const Base = defineComarkRendererComponent({ name: 'BaseRenderer', className: 'base-class' }) + const Child = defineComarkRendererComponent({ name: 'ChildRenderer', extends: Base, className: 'child-class' }) + const tree = await parse('hello') + const html = await renderAsync() + expect(html).toContain('base-class') + expect(html).toContain('child-class') + }) +}) diff --git a/packages/comark-vue/src/index.ts b/packages/comark-vue/src/index.ts index 1469f073..a2341e9e 100644 --- a/packages/comark-vue/src/index.ts +++ b/packages/comark-vue/src/index.ts @@ -12,12 +12,20 @@ interface DefineComarkComponentOptions extends ParseOptions { extends?: typeof Comark name?: string components?: Record + /** + * Additional classes for the wrapper div + */ + class?: string } interface DefineComarkRendererOptions { extends?: typeof ComarkRenderer name?: string components?: Record + /** + * Additional classes for the wrapper div + */ + class?: string } export function defineComarkComponent(config: DefineComarkComponentOptions = {}): typeof Comark { @@ -120,6 +128,7 @@ export function defineComarkComponent(config: DefineComarkComponentOptions = {}) streaming: props.streaming, summary: props.summary, caret: props.caret, + class: config.class, }, { default: slots.default, }) @@ -189,6 +198,7 @@ export function defineComarkRendererComponent(config: DefineComarkRendererOption componentsManifest: props.componentsManifest, streaming: props.streaming, caret: props.caret, + class: config.class, }, { default: slots.default, }) diff --git a/packages/comark-vue/test/define-component.test.ts b/packages/comark-vue/test/define-component.test.ts index 9f433e5e..d6d616b2 100644 --- a/packages/comark-vue/test/define-component.test.ts +++ b/packages/comark-vue/test/define-component.test.ts @@ -128,6 +128,33 @@ describe('defineComarkComponent — plugin inheritance via extends', () => { }) }) +// --------------------------------------------------------------------------- +// defineComarkComponent — class +// --------------------------------------------------------------------------- + +describe('defineComarkComponent — class via config', () => { + it('applies config class to wrapper div', async () => { + const Custom = defineComarkComponent({ name: 'WithClass', class: 'prose dark' }) + const html = await renderComponent(Custom, { markdown: 'hello' }) + expect(html).toContain('prose dark') + expect(html).toContain('comark-content') + }) + + it('prop class works without config class', async () => { + const Custom = defineComarkComponent({ name: 'NoConfigClass' }) + const html = await renderComponent(Custom, { markdown: 'hello' }) + expect(html).toContain('comark-content') + }) + + it('inherited component preserves parent class', async () => { + const Base = defineComarkComponent({ name: 'Base', class: 'base-class' }) + const Child = defineComarkComponent({ name: 'Child', extends: Base, class: 'child-class' }) + const html = await renderComponent(Child, { markdown: 'hello' }) + expect(html).toContain('base-class') + expect(html).toContain('child-class') + }) +}) + // --------------------------------------------------------------------------- // defineComarkRendererComponent // --------------------------------------------------------------------------- @@ -201,3 +228,33 @@ describe('defineComarkRendererComponent — component inheritance via extends', expect(html).toContain('card-base') }) }) + +// --------------------------------------------------------------------------- +// defineComarkRendererComponent — class +// --------------------------------------------------------------------------- + +describe('defineComarkRendererComponent — class via config', () => { + it('applies config class to wrapper div', async () => { + const Renderer = defineComarkRendererComponent({ name: 'WithClass', class: 'prose dark' }) + const tree = await parse('hello') + const html = await renderComponent(Renderer, { tree }) + expect(html).toContain('prose dark') + expect(html).toContain('comark-content') + }) + + it('prop class works without config class', async () => { + const Renderer = defineComarkRendererComponent({ name: 'NoConfigClass' }) + const tree = await parse('hello') + const html = await renderComponent(Renderer, { tree }) + expect(html).toContain('comark-content') + }) + + it('inherited renderer preserves parent class', async () => { + const Base = defineComarkRendererComponent({ name: 'BaseRenderer', class: 'base-class' }) + const Child = defineComarkRendererComponent({ name: 'ChildRenderer', extends: Base, class: 'child-class' }) + const tree = await parse('hello') + const html = await renderComponent(Child, { tree }) + expect(html).toContain('base-class') + expect(html).toContain('child-class') + }) +})