@wsxjs/wsx-marked-components 0.0.18

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.
@@ -0,0 +1,269 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * WSX Markdown Component
4
+ *
5
+ * A reusable component that renders markdown into WSX marked components.
6
+ * Supports customization through custom token renderers.
7
+ *
8
+ * @example
9
+ * ```html
10
+ * <wsx-markdown markdown="# Hello World"></wsx-markdown>
11
+ * ```
12
+ */
13
+
14
+ import { LightComponent, autoRegister, state } from "@wsxjs/wsx-core";
15
+ import { createLogger } from "@wsxjs/wsx-logger";
16
+ import { marked } from "marked";
17
+ import type { Tokens } from "marked";
18
+ import styles from "./Markdown.css?inline";
19
+ import { renderInlineTokens } from "./marked-utils";
20
+ // Import WSX components so they're registered as custom elements
21
+ import "./Heading.wsx";
22
+ import "./Code.wsx";
23
+ import "./Blockquote.wsx";
24
+ import "./Paragraph.wsx";
25
+ import "./List.wsx";
26
+ import "./Error.wsx";
27
+
28
+ const logger = createLogger("Markdown");
29
+
30
+ /**
31
+ * Custom token renderer function type
32
+ * Return null to use default rendering, or return an HTMLElement to override
33
+ */
34
+ export type TokenRenderer = (
35
+ token: Tokens.Generic,
36
+ defaultRender: () => HTMLElement | null
37
+ ) => HTMLElement | null;
38
+
39
+ /**
40
+ * Custom renderers configuration
41
+ */
42
+ export interface CustomRenderers {
43
+ [tokenType: string]: TokenRenderer;
44
+ }
45
+
46
+ /**
47
+ * Markdown Component
48
+ *
49
+ * Renders markdown content into WSX marked components using marked.lexer()
50
+ * and manual JSX conversion.
51
+ */
52
+ @autoRegister({ tagName: "wsx-markdown" })
53
+ export default class Markdown extends LightComponent {
54
+ @state private markdown: string = "";
55
+ private customRenderers: CustomRenderers = {};
56
+
57
+ constructor() {
58
+ super({
59
+ styles,
60
+ styleName: "wsx-markdown",
61
+ lightDOM: true,
62
+ });
63
+ logger.info("Markdown initialized");
64
+ }
65
+
66
+ static get observedAttributes() {
67
+ return ["markdown"];
68
+ }
69
+
70
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
71
+ if (name === "markdown") {
72
+ this.markdown = newValue || "";
73
+ this.rerender();
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Set custom renderers for specific token types
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * markdown.setCustomRenderers({
83
+ * heading: (token, defaultRender) => {
84
+ * // Custom heading rendering
85
+ * return defaultRender();
86
+ * }
87
+ * });
88
+ * ```
89
+ */
90
+ setCustomRenderers(renderers: CustomRenderers): void {
91
+ this.customRenderers = { ...this.customRenderers, ...renderers };
92
+ this.rerender();
93
+ }
94
+
95
+ /**
96
+ * Get current custom renderers
97
+ */
98
+ getCustomRenderers(): CustomRenderers {
99
+ return { ...this.customRenderers };
100
+ }
101
+
102
+ render() {
103
+ if (!this.markdown) {
104
+ return <div class="marked-content"></div>;
105
+ }
106
+
107
+ try {
108
+ // Use marked.lexer to get tokens, then render with WSX JSX
109
+ const tokens = marked.lexer(this.markdown);
110
+ return <div class="marked-content">{this.renderTokens(tokens)}</div>;
111
+ } catch (error) {
112
+ logger.error("Failed to render markdown", error);
113
+ return (
114
+ <div class="marked-content">
115
+ <wsx-marked-error message={`Error: ${error}`} />
116
+ </div>
117
+ );
118
+ }
119
+ }
120
+
121
+ protected onConnected() {
122
+ // Get initial value from attribute
123
+ const markdownAttr = this.getAttribute("markdown");
124
+ if (markdownAttr) {
125
+ this.markdown = markdownAttr;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Convert marked tokens to WSX JSX elements
131
+ */
132
+ private renderTokens(tokens: Tokens.Generic[]): (HTMLElement | null)[] {
133
+ return tokens
134
+ .map((token) => this.renderToken(token))
135
+ .filter((el): el is HTMLElement => el !== null);
136
+ }
137
+
138
+ /**
139
+ * Render a single token to WSX JSX element
140
+ * Supports custom renderers for extensibility
141
+ */
142
+ private renderToken(token: Tokens.Generic): HTMLElement | null {
143
+ // Check for custom renderer first
144
+ const customRenderer = this.customRenderers[token.type];
145
+ if (customRenderer) {
146
+ const result = customRenderer(token, () => this.defaultRenderToken(token));
147
+ if (result !== null) {
148
+ return result;
149
+ }
150
+ // If custom renderer returns null, fall back to default
151
+ }
152
+
153
+ return this.defaultRenderToken(token);
154
+ }
155
+
156
+ /**
157
+ * Default token rendering logic
158
+ * This is the core rendering implementation
159
+ */
160
+ private defaultRenderToken(token: Tokens.Generic): HTMLElement | null {
161
+ switch (token.type) {
162
+ case "heading": {
163
+ const headingToken = token as Tokens.Heading;
164
+ return (
165
+ <wsx-marked-heading
166
+ level={headingToken.depth.toString()}
167
+ text={renderInlineTokens(headingToken.tokens)}
168
+ />
169
+ );
170
+ }
171
+
172
+ case "code": {
173
+ const codeToken = token as Tokens.Code;
174
+ return <wsx-marked-code code={codeToken.text} language={codeToken.lang || ""} />;
175
+ }
176
+
177
+ case "blockquote": {
178
+ const blockquoteToken = token as Tokens.Blockquote;
179
+ return (
180
+ <wsx-marked-blockquote>
181
+ {this.renderTokens(blockquoteToken.tokens)}
182
+ </wsx-marked-blockquote>
183
+ );
184
+ }
185
+
186
+ case "paragraph": {
187
+ const paraToken = token as Tokens.Paragraph;
188
+ return <wsx-marked-paragraph content={renderInlineTokens(paraToken.tokens)} />;
189
+ }
190
+
191
+ case "list": {
192
+ const listToken = token as Tokens.List;
193
+ // List items can contain block-level tokens (nested lists, blockquotes, etc.)
194
+ // So we need to use renderTokens() instead of renderInlineTokens()
195
+ const items = listToken.items.map((item) => {
196
+ // 防御性检查:确保 item.tokens 存在且是数组
197
+ if (!item.tokens || !Array.isArray(item.tokens)) {
198
+ logger.warn("List item has no tokens or tokens is not an array", { item });
199
+ return "";
200
+ }
201
+ // Render all tokens in the list item (both inline and block-level)
202
+ const renderedElements = this.renderTokens(item.tokens);
203
+ // 如果渲染结果为空,返回空字符串
204
+ if (renderedElements.length === 0) {
205
+ return "";
206
+ }
207
+ // Convert rendered elements to HTML string for the List component
208
+ // Create a temporary container to collect the HTML
209
+ const tempContainer = document.createElement("div");
210
+ renderedElements.forEach((el) => {
211
+ if (el) {
212
+ tempContainer.appendChild(el);
213
+ }
214
+ });
215
+ const html = tempContainer.innerHTML;
216
+ // 如果 innerHTML 为空,记录警告
217
+ if (!html) {
218
+ logger.warn("tempContainer.innerHTML is empty after appending elements", {
219
+ renderedElementsCount: renderedElements.length,
220
+ itemTokens: item.tokens,
221
+ });
222
+ }
223
+ return html;
224
+ });
225
+ return (
226
+ <wsx-marked-list
227
+ ordered={listToken.ordered ? "true" : "false"}
228
+ items={JSON.stringify(items)}
229
+ />
230
+ );
231
+ }
232
+
233
+ case "html": {
234
+ const htmlToken = token as Tokens.HTML;
235
+ return <div>{htmlToken.text}</div>;
236
+ }
237
+
238
+ case "hr": {
239
+ return <hr />;
240
+ }
241
+
242
+ case "space": {
243
+ // Ignore space tokens
244
+ return null;
245
+ }
246
+
247
+ case "text": {
248
+ // Text tokens are inline tokens, but if they appear as top-level tokens,
249
+ // render them directly as text nodes wrapped in a span
250
+ const textToken = token as Tokens.Text;
251
+ return <span>{textToken.text || ""}</span>;
252
+ }
253
+
254
+ default: {
255
+ // For other types, use default marked renderer
256
+ // Note: marked.Renderer() only handles block-level tokens, not inline tokens
257
+ const renderer = new marked.Renderer();
258
+ const renderMethod = (
259
+ renderer as unknown as Record<string, (token: unknown) => string>
260
+ )[token.type];
261
+ const html = renderMethod?.(token) || "";
262
+ if (html) {
263
+ return <div>{html}</div>;
264
+ }
265
+ return null;
266
+ }
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,36 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+ /**
3
+ * WSX Paragraph Component
4
+ * Custom paragraph component for markdown rendering
5
+ */
6
+
7
+ import { LightComponent, autoRegister, state } from "@wsxjs/wsx-core";
8
+
9
+ @autoRegister({ tagName: "wsx-marked-paragraph" })
10
+ export default class Paragraph extends LightComponent {
11
+ @state private content: string = "";
12
+
13
+ constructor() {
14
+ super({
15
+ styleName: "wsx-marked-paragraph",
16
+ });
17
+ }
18
+
19
+ static get observedAttributes() {
20
+ return ["content"];
21
+ }
22
+
23
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
24
+ if (name === "content") {
25
+ // HTML 属性值会被浏览器自动解码,所以这里得到的是原始的 HTML 字符串
26
+ this.content = newValue || "";
27
+ }
28
+ }
29
+
30
+ render() {
31
+ // content 是 HTML 字符串,需要解析为 DOM 节点
32
+ // JSX factory 会自动检测 HTML 字符串并转换
33
+ return <p class="marked-paragraph">{this.content}</p>;
34
+ }
35
+ }
36
+
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** @jsxImportSource @wsxjs/wsx-core */
2
+
3
+ // Export all marked components
4
+ export { default as Heading } from "./Heading.wsx";
5
+ export { default as Code } from "./Code.wsx";
6
+ export { default as Blockquote } from "./Blockquote.wsx";
7
+ export { default as Paragraph } from "./Paragraph.wsx";
8
+ export { default as List } from "./List.wsx";
9
+ export { default as Error } from "./Error.wsx";
10
+ export { default as Markdown } from "./Markdown.wsx";
11
+
12
+ // Export utilities
13
+ export * from "./marked-utils";
14
+
15
+ // Export types
16
+ export type { TokenRenderer, CustomRenderers, MarkdownOptions } from "./types";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Marked utilities for Pattern 1 and Pattern 2
3
+ * Shared utilities for extracting and rendering inline tokens
4
+ */
5
+
6
+ import type { Tokens } from "marked";
7
+
8
+ /**
9
+ * Helper to escape HTML for attributes
10
+ */
11
+ export function escapeHtml(text: string): string {
12
+ const div = document.createElement("div");
13
+ div.textContent = text;
14
+ // Framework utility function for HTML escaping - innerHTML is necessary here
15
+ return div.innerHTML;
16
+ }
17
+
18
+ /**
19
+ * Extract inline tokens from a list of tokens
20
+ * Handles paragraph tokens by extracting their inline tokens
21
+ */
22
+ export function extractInlineTokens(tokens: Tokens.Generic[] | undefined): Tokens.Generic[] {
23
+ // 防御性检查:确保 tokens 存在且是数组
24
+ if (!tokens || !Array.isArray(tokens)) {
25
+ return [];
26
+ }
27
+
28
+ const inlineTokens: Tokens.Generic[] = [];
29
+
30
+ tokens.forEach((token) => {
31
+ if (token.type === "paragraph") {
32
+ // If it's a paragraph, extract its inline tokens
33
+ const paraToken = token as Tokens.Paragraph;
34
+ // 防御性检查:确保 paraToken.tokens 存在
35
+ if (paraToken.tokens && Array.isArray(paraToken.tokens)) {
36
+ inlineTokens.push(...paraToken.tokens);
37
+ }
38
+ } else if (
39
+ token.type === "text" ||
40
+ token.type === "strong" ||
41
+ token.type === "em" ||
42
+ token.type === "link" ||
43
+ token.type === "code" ||
44
+ token.type === "br"
45
+ ) {
46
+ // If it's already an inline token, add it directly
47
+ inlineTokens.push(token);
48
+ }
49
+ // For other block-level tokens, we skip them
50
+ // If needed, they can be handled separately
51
+ });
52
+
53
+ return inlineTokens;
54
+ }
55
+
56
+ /**
57
+ * Render inline tokens to HTML string
58
+ * Used by both Pattern 1 and Pattern 2
59
+ */
60
+ export function renderInlineTokens(tokens: Tokens.Generic[] | undefined): string {
61
+ // 防御性检查:确保 tokens 存在且是数组
62
+ if (!tokens || !Array.isArray(tokens)) {
63
+ return "";
64
+ }
65
+
66
+ return tokens
67
+ .map((token) => {
68
+ switch (token.type) {
69
+ case "text": {
70
+ const textToken = token as Tokens.Text;
71
+ return textToken.text || "";
72
+ }
73
+ case "strong": {
74
+ const strongToken = token as Tokens.Strong;
75
+ // 防御性检查:确保 strongToken.tokens 存在
76
+ const nestedTokens =
77
+ strongToken.tokens && Array.isArray(strongToken.tokens)
78
+ ? strongToken.tokens
79
+ : [];
80
+ return `<strong>${renderInlineTokens(nestedTokens)}</strong>`;
81
+ }
82
+ case "em": {
83
+ const emToken = token as Tokens.Em;
84
+ // 防御性检查:确保 emToken.tokens 存在
85
+ const nestedTokens =
86
+ emToken.tokens && Array.isArray(emToken.tokens) ? emToken.tokens : [];
87
+ return `<em>${renderInlineTokens(nestedTokens)}</em>`;
88
+ }
89
+ case "link": {
90
+ const linkToken = token as Tokens.Link;
91
+ const title = linkToken.title ? ` title="${escapeHtml(linkToken.title)}"` : "";
92
+ // 防御性检查:确保 linkToken.tokens 存在
93
+ const nestedTokens =
94
+ linkToken.tokens && Array.isArray(linkToken.tokens) ? linkToken.tokens : [];
95
+ return `<a href="${linkToken.href || "#"}"${title}>${renderInlineTokens(nestedTokens)}</a>`;
96
+ }
97
+ case "code": {
98
+ const codeToken = token as Tokens.Code;
99
+ return `<code>${escapeHtml(codeToken.text || "")}</code>`;
100
+ }
101
+ case "br": {
102
+ return "<br>";
103
+ }
104
+ default: {
105
+ // For unknown token types, return empty string
106
+ return "";
107
+ }
108
+ }
109
+ })
110
+ .join("");
111
+ }
@@ -0,0 +1,5 @@
1
+ declare module "*.wsx" {
2
+ import { Component } from "@wsxjs/wsx-core";
3
+ const ComponentClass: typeof Component;
4
+ export default ComponentClass;
5
+ }
package/src/types.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Type definitions for @wsxjs/wsx-marked-components
3
+ */
4
+
5
+ import type { Tokens } from "marked";
6
+
7
+ /**
8
+ * Custom token renderer function type
9
+ *
10
+ * @param token - The token to render
11
+ * @param defaultRender - Function to call default rendering logic
12
+ * @returns HTMLElement or null (null means use default rendering)
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const customHeadingRenderer: TokenRenderer = (token, defaultRender) => {
17
+ * if (token.type === 'heading') {
18
+ * const headingToken = token as Tokens.Heading;
19
+ * // Custom logic here
20
+ * return defaultRender(); // Or return custom element
21
+ * }
22
+ * return null; // Use default for other types
23
+ * };
24
+ * ```
25
+ */
26
+ export type TokenRenderer = (
27
+ token: Tokens.Generic,
28
+ defaultRender: () => HTMLElement | null
29
+ ) => HTMLElement | null;
30
+
31
+ /**
32
+ * Custom renderers configuration
33
+ * Maps token types to custom renderer functions
34
+ */
35
+ export interface CustomRenderers {
36
+ [tokenType: string]: TokenRenderer;
37
+ }
38
+
39
+ /**
40
+ * Markdown component options
41
+ */
42
+ export interface MarkdownOptions {
43
+ /**
44
+ * Custom renderers for specific token types
45
+ */
46
+ customRenderers?: CustomRenderers;
47
+
48
+ /**
49
+ * Custom CSS class name for the content container
50
+ */
51
+ contentClass?: string;
52
+
53
+ /**
54
+ * Whether to enable debug logging
55
+ */
56
+ debug?: boolean;
57
+ }