@object-ui/plugin-markdown 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/MarkdownImpl-Dp8rFxgw.js +9096 -0
- package/dist/MarkdownImpl.d.ts +22 -0
- package/dist/MarkdownImpl.d.ts.map +1 -0
- package/dist/index-DDihmVdn.js +656 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.umd.cjs +48 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/MarkdownImpl.tsx +64 -0
- package/src/index.test.ts +55 -0
- package/src/index.tsx +62 -0
- package/src/types.ts +28 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import ReactMarkdown from "react-markdown"
|
|
3
|
+
import remarkGfm from "remark-gfm"
|
|
4
|
+
import rehypeSanitize from "rehype-sanitize"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props for the Markdown component implementation.
|
|
8
|
+
*
|
|
9
|
+
* This component renders markdown content using react-markdown with GitHub Flavored Markdown support.
|
|
10
|
+
* All content is sanitized to prevent XSS attacks.
|
|
11
|
+
*/
|
|
12
|
+
export interface MarkdownImplProps {
|
|
13
|
+
/**
|
|
14
|
+
* The markdown content to render.
|
|
15
|
+
*/
|
|
16
|
+
content: string
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Optional CSS class name to apply custom styling to the markdown container.
|
|
20
|
+
*/
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Internal Markdown implementation component.
|
|
26
|
+
* This contains the actual react-markdown import (heavy ~100-200 KB).
|
|
27
|
+
*/
|
|
28
|
+
export default function MarkdownImpl({ content, className }: MarkdownImplProps) {
|
|
29
|
+
// Utility function to merge class names (inline to avoid external dependency)
|
|
30
|
+
const cn = (...classes: (string | undefined)[]) => classes.filter(Boolean).join(' ')
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="markdown"
|
|
35
|
+
className={cn(
|
|
36
|
+
"prose prose-sm dark:prose-invert max-w-none",
|
|
37
|
+
"prose-headings:font-semibold prose-headings:tracking-tight",
|
|
38
|
+
"prose-h1:text-3xl prose-h2:text-2xl prose-h3:text-xl",
|
|
39
|
+
"prose-p:leading-relaxed prose-p:text-foreground",
|
|
40
|
+
"prose-a:text-primary prose-a:no-underline hover:prose-a:underline",
|
|
41
|
+
"prose-code:text-foreground prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none",
|
|
42
|
+
"prose-pre:bg-muted prose-pre:text-foreground prose-pre:border",
|
|
43
|
+
"prose-blockquote:border-l-primary prose-blockquote:text-muted-foreground",
|
|
44
|
+
"prose-strong:text-foreground prose-strong:font-semibold",
|
|
45
|
+
"prose-ul:list-disc prose-ol:list-decimal",
|
|
46
|
+
"prose-li:text-foreground prose-li:marker:text-muted-foreground",
|
|
47
|
+
"prose-table:border prose-th:border prose-th:bg-muted prose-td:border",
|
|
48
|
+
"prose-img:rounded-md prose-img:border",
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<ReactMarkdown
|
|
53
|
+
remarkPlugins={[remarkGfm]}
|
|
54
|
+
rehypePlugins={[rehypeSanitize]}
|
|
55
|
+
// Additional security: only allow safe elements
|
|
56
|
+
// This provides defense-in-depth beyond rehype-sanitize
|
|
57
|
+
disallowedElements={['script', 'style', 'iframe', 'object', 'embed']}
|
|
58
|
+
unwrapDisallowed={true}
|
|
59
|
+
>
|
|
60
|
+
{content}
|
|
61
|
+
</ReactMarkdown>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { ComponentRegistry } from '@object-ui/core';
|
|
3
|
+
|
|
4
|
+
describe('Plugin Markdown', () => {
|
|
5
|
+
// Import all renderers to register them
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
await import('./index');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('markdown component', () => {
|
|
11
|
+
it('should be registered in ComponentRegistry', () => {
|
|
12
|
+
const markdownRenderer = ComponentRegistry.get('markdown');
|
|
13
|
+
expect(markdownRenderer).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have proper metadata', () => {
|
|
17
|
+
const config = ComponentRegistry.getConfig('markdown');
|
|
18
|
+
expect(config).toBeDefined();
|
|
19
|
+
expect(config?.label).toBe('Markdown');
|
|
20
|
+
expect(config?.category).toBe('plugin');
|
|
21
|
+
expect(config?.inputs).toBeDefined();
|
|
22
|
+
expect(config?.defaultProps).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should have expected inputs', () => {
|
|
26
|
+
const config = ComponentRegistry.getConfig('markdown');
|
|
27
|
+
const inputNames = config?.inputs?.map((input: any) => input.name) || [];
|
|
28
|
+
|
|
29
|
+
expect(inputNames).toContain('content');
|
|
30
|
+
expect(inputNames).toContain('className');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have content as required input', () => {
|
|
34
|
+
const config = ComponentRegistry.getConfig('markdown');
|
|
35
|
+
const contentInput = config?.inputs?.find((input: any) => input.name === 'content');
|
|
36
|
+
|
|
37
|
+
expect(contentInput).toBeDefined();
|
|
38
|
+
expect(contentInput?.required).toBe(true);
|
|
39
|
+
expect(contentInput?.type).toBe('string');
|
|
40
|
+
expect(contentInput?.inputType).toBe('textarea');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should have sensible default props', () => {
|
|
44
|
+
const config = ComponentRegistry.getConfig('markdown');
|
|
45
|
+
const defaults = config?.defaultProps;
|
|
46
|
+
|
|
47
|
+
expect(defaults).toBeDefined();
|
|
48
|
+
expect(defaults?.content).toBeDefined();
|
|
49
|
+
expect(typeof defaults?.content).toBe('string');
|
|
50
|
+
expect(defaults?.content.length).toBeGreaterThan(0);
|
|
51
|
+
// Verify it contains markdown syntax
|
|
52
|
+
expect(defaults?.content).toContain('#');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { Suspense } from 'react';
|
|
2
|
+
import { ComponentRegistry } from '@object-ui/core';
|
|
3
|
+
import { Skeleton } from '@object-ui/components';
|
|
4
|
+
|
|
5
|
+
// Export types for external use
|
|
6
|
+
export type { MarkdownSchema } from './types';
|
|
7
|
+
|
|
8
|
+
// 🚀 Lazy load the implementation file
|
|
9
|
+
// This ensures react-markdown is only loaded when the component is actually rendered
|
|
10
|
+
const LazyMarkdown = React.lazy(() => import('./MarkdownImpl'));
|
|
11
|
+
|
|
12
|
+
export interface MarkdownRendererProps {
|
|
13
|
+
schema: {
|
|
14
|
+
type: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
content?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MarkdownRenderer - The public API for the markdown component
|
|
23
|
+
* This wrapper handles lazy loading internally using React.Suspense
|
|
24
|
+
*/
|
|
25
|
+
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ schema }) => {
|
|
26
|
+
return (
|
|
27
|
+
<Suspense fallback={<Skeleton className="w-full h-[200px]" />}>
|
|
28
|
+
<LazyMarkdown
|
|
29
|
+
content={schema.content || ''}
|
|
30
|
+
className={schema.className}
|
|
31
|
+
/>
|
|
32
|
+
</Suspense>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Register the component with the ComponentRegistry
|
|
37
|
+
ComponentRegistry.register(
|
|
38
|
+
'markdown',
|
|
39
|
+
MarkdownRenderer,
|
|
40
|
+
{
|
|
41
|
+
label: 'Markdown',
|
|
42
|
+
category: 'plugin',
|
|
43
|
+
inputs: [
|
|
44
|
+
{
|
|
45
|
+
name: 'content',
|
|
46
|
+
type: 'string',
|
|
47
|
+
label: 'Markdown Content',
|
|
48
|
+
required: true,
|
|
49
|
+
inputType: 'textarea'
|
|
50
|
+
},
|
|
51
|
+
{ name: 'className', type: 'string', label: 'CSS Class' }
|
|
52
|
+
],
|
|
53
|
+
defaultProps: {
|
|
54
|
+
content: '# Hello World\n\nThis is a **markdown** component with *formatting* support.\n\n- Item 1\n- Item 2\n- Item 3',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Standard Export Protocol - for manual integration
|
|
60
|
+
export const markdownComponents = {
|
|
61
|
+
'markdown': MarkdownRenderer,
|
|
62
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Markdown component schema.
|
|
5
|
+
* Renders markdown content with GitHub Flavored Markdown support.
|
|
6
|
+
*/
|
|
7
|
+
export interface MarkdownSchema extends BaseSchema {
|
|
8
|
+
type: 'markdown';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The markdown content to render. Supports GitHub Flavored Markdown including:
|
|
12
|
+
* - Headers (H1-H6)
|
|
13
|
+
* - Bold, italic, and inline code
|
|
14
|
+
* - Links and images
|
|
15
|
+
* - Lists (ordered, unordered, and nested)
|
|
16
|
+
* - Tables
|
|
17
|
+
* - Blockquotes
|
|
18
|
+
* - Code blocks
|
|
19
|
+
* - Strikethrough
|
|
20
|
+
* - Task lists
|
|
21
|
+
*/
|
|
22
|
+
content?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional CSS class name to apply custom styling to the markdown container.
|
|
26
|
+
*/
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"declarationMap": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
|
|
11
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import dts from 'vite-plugin-dts';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
react(),
|
|
9
|
+
dts({
|
|
10
|
+
insertTypesEntry: true,
|
|
11
|
+
include: ['src'],
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: {
|
|
16
|
+
'@': resolve(__dirname, './src'),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
build: {
|
|
20
|
+
lib: {
|
|
21
|
+
entry: resolve(__dirname, 'src/index.tsx'),
|
|
22
|
+
name: 'ObjectUIPluginMarkdown',
|
|
23
|
+
fileName: 'index',
|
|
24
|
+
},
|
|
25
|
+
rollupOptions: {
|
|
26
|
+
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react'],
|
|
27
|
+
output: {
|
|
28
|
+
globals: {
|
|
29
|
+
react: 'React',
|
|
30
|
+
'react-dom': 'ReactDOM',
|
|
31
|
+
'@object-ui/components': 'ObjectUIComponents',
|
|
32
|
+
'@object-ui/core': 'ObjectUICore',
|
|
33
|
+
'@object-ui/react': 'ObjectUIReact',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|