tiptap-markdown-react 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CatsInJune
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # tiptap-markdown-react
2
+
3
+ A batteries-included, self-styled **Markdown WYSIWYG editor + reader** suite built on [Tiptap v3](https://tiptap.dev). Markdown in, markdown out — plus a table of contents, a client preview, and a server-side (RSC/SSR) renderer for SEO-friendly reading pages. No Ant Design, no icon library; themeable via CSS variables.
4
+
5
+ - **Markdown-first**: content goes in and comes out as markdown (`getMarkdown()`), with `getHTML()` / `getJSON()` also exposed.
6
+ - **Own opinionated UI**: toolbar, color palette, code block, and table of contents ship styled out of the box. Zero `antd`. Dropdowns/popovers use [Radix](https://www.radix-ui.com/) primitives; icons are inline SVG.
7
+ - **Editor + Preview + Static reader**: edit, live client-side preview, and a pure `renderReportHtml()` for server rendering (Next.js Server Components / ISR).
8
+ - **Table of contents**: stable, shareable slug anchors that match between the editor preview and the published reading page.
9
+ - **Themeable**: colors and fonts are exposed as `--tmr-*` CSS variables.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install tiptap-markdown-react
15
+ # or
16
+ pnpm add tiptap-markdown-react
17
+ ```
18
+
19
+ ### Peer dependencies
20
+
21
+ Install these alongside the package (kept as peers to avoid duplicate instances):
22
+
23
+ ```bash
24
+ npm install react react-dom lowlight \
25
+ @tiptap/react @tiptap/pm @tiptap/core @tiptap/starter-kit @tiptap/markdown \
26
+ @tiptap/static-renderer @tiptap/extension-code-block-lowlight \
27
+ @tiptap/extension-highlight @tiptap/extension-image @tiptap/extension-subscript \
28
+ @tiptap/extension-superscript @tiptap/extension-table @tiptap/extension-table-of-contents \
29
+ @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-text-style
30
+ ```
31
+
32
+ Then import the stylesheet once (e.g. in your root layout / entry):
33
+
34
+ ```ts
35
+ import 'tiptap-markdown-react/style.css';
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### 1. Editor + toolbar
41
+
42
+ The editor and toolbar are separate components so you can place the toolbar wherever you like (sticky header, etc.). Wire them together via `onEditorReady`.
43
+
44
+ ```tsx
45
+ 'use client';
46
+ import { useState, useRef } from 'react';
47
+ import type { Editor } from '@tiptap/react';
48
+ import {
49
+ MarkdownWysiwygEditor,
50
+ EditorToolbar,
51
+ type MarkdownWysiwygEditorHandle,
52
+ } from 'tiptap-markdown-react';
53
+ import 'tiptap-markdown-react/style.css';
54
+
55
+ export function Composer() {
56
+ const [editor, setEditor] = useState<Editor | null>(null);
57
+ const ref = useRef<MarkdownWysiwygEditorHandle>(null);
58
+
59
+ return (
60
+ <div>
61
+ {editor && (
62
+ <EditorToolbar
63
+ editor={editor}
64
+ onImageUpload={async (file) => {
65
+ const url = await uploadToYourStorage(file); // return a public URL
66
+ return url;
67
+ }}
68
+ onError={(err) => console.error(err)}
69
+ />
70
+ )}
71
+ <MarkdownWysiwygEditor
72
+ ref={ref}
73
+ initialMarkdown={'# Hello\n\nStart writing…'}
74
+ placeholder="Write something…"
75
+ onEditorReady={setEditor}
76
+ />
77
+ <button onClick={() => console.log(ref.current?.getMarkdown())}>
78
+ Save
79
+ </button>
80
+ </div>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ### 2. Client-side preview
86
+
87
+ ```tsx
88
+ 'use client';
89
+ import { MarkdownPreview } from 'tiptap-markdown-react';
90
+ import 'tiptap-markdown-react/style.css';
91
+
92
+ export function Preview({ markdown }: { markdown: string }) {
93
+ return <MarkdownPreview markdown={markdown} />;
94
+ }
95
+ ```
96
+
97
+ ### 3. Server-side / static reader (SEO)
98
+
99
+ Use the `./server` entry from a Server Component — it has no client code and no hard browser dependency. Render markdown to HTML at request/build time, then output it with `ReportContent` (which applies the same content styles as the editor).
100
+
101
+ ```tsx
102
+ // app/p/[slug]/page.tsx (React Server Component)
103
+ import { renderReportHtml, ReportContent } from 'tiptap-markdown-react/server';
104
+ import { TocPanel } from 'tiptap-markdown-react'; // client component, for the sidebar
105
+ import 'tiptap-markdown-react/style.css';
106
+
107
+ export default async function Page() {
108
+ const markdown = await loadMarkdown();
109
+ const { html, toc } = renderReportHtml(markdown);
110
+
111
+ return (
112
+ <article>
113
+ {/* toc can be passed to a client <TocPanel toc={toc} /> */}
114
+ <ReportContent html={html} />
115
+ </article>
116
+ );
117
+ }
118
+ ```
119
+
120
+ ## Theming
121
+
122
+ Override any of these CSS variables on an ancestor (e.g. `:root` or the editor container):
123
+
124
+ | Variable | Default | Purpose |
125
+ | --- | --- | --- |
126
+ | `--tmr-accent` | `#ff6719` | Brand/accent color (links, active states, quotes) |
127
+ | `--tmr-text` | `#363737` | Body text color |
128
+ | `--tmr-muted` | `#777777` | Muted text (blockquote, h6) |
129
+ | `--tmr-border` | `#e6e6e6` | Borders (tables, hr) |
130
+ | `--tmr-body-font` | Spectral, serif… | Content font family |
131
+ | `--tmr-font-size` | `19px` | Base content font size |
132
+ | `--tmr-line-height` | `1.7` | Content line height |
133
+ | `--tmr-min-height` | `420px` | Editor min height |
134
+ | `--tmr-code-bg` | `#f4f4f4` | Inline code background |
135
+ | `--tmr-table-header-bg` | `#f7f7f7` | Table header background |
136
+
137
+ ## API
138
+
139
+ ### `<MarkdownWysiwygEditor>` (client)
140
+
141
+ | Prop | Type | Description |
142
+ | --- | --- | --- |
143
+ | `initialMarkdown` | `string` | Initial content (markdown). |
144
+ | `placeholder` | `string` | Placeholder for the empty document. |
145
+ | `onEditorReady` | `(editor: Editor \| null) => void` | Get the Tiptap instance (for the toolbar). |
146
+ | `onTocChange` | `(items: TocItem[]) => void` | Fires when headings change. |
147
+ | `extraExtensions` | `AnyExtension[]` | Extra Tiptap extensions to register. |
148
+ | `codeBlockLabels` | `Partial<CodeBlockLabels>` | Localize the code block UI. |
149
+ | `className` | `string` | Class on the scroll container. |
150
+
151
+ Ref handle (`MarkdownWysiwygEditorHandle`): `getMarkdown()`, `getHTML()`, `getJSON()`, `getEditor()`.
152
+
153
+ ### `<EditorToolbar>` (client)
154
+
155
+ | Prop | Type | Description |
156
+ | --- | --- | --- |
157
+ | `editor` | `Editor` | The instance from `onEditorReady`. |
158
+ | `onImageUpload` | `(file: File) => Promise<string>` | Upload handler returning a URL. Omit to hide the image button. |
159
+ | `onError` | `(err: unknown) => void` | Side-effect error callback (e.g. failed upload). |
160
+ | `labels` | `Partial<ToolbarLabels>` | i18n labels. |
161
+ | `extraToolbarItems` | `ExtraToolbarItem[]` | Custom items appended to the "More" menu. |
162
+
163
+ ### `<MarkdownPreview>` (client)
164
+
165
+ | Prop | Type | Description |
166
+ | --- | --- | --- |
167
+ | `markdown` | `string` | Markdown to render read-only. |
168
+ | `className` | `string` | Class on the container. |
169
+
170
+ ### `<TocPanel>` (client)
171
+
172
+ | Prop | Type | Description |
173
+ | --- | --- | --- |
174
+ | `items` | `TocItem[]` | Table of contents items. |
175
+ | `activeId` | `string` | Currently highlighted anchor id. |
176
+ | `onItemClick` | `(item: TocItem) => void` | Click handler (locked items are skipped). |
177
+ | `labels` | `Partial<TocLabels>` | i18n labels. |
178
+
179
+ ### `tiptap-markdown-react/server` (RSC-safe)
180
+
181
+ | Export | Description |
182
+ | --- | --- |
183
+ | `renderReportHtml(markdown, lockedTitles?)` | `{ html, toc }` — pure markdown → HTML + TOC. |
184
+ | `ReportContent` | `<ReportContent html={...} />` static reader with editor content styles. |
185
+ | `extractToc`, `makeTocGetId` | TOC helpers. |
186
+ | `baseExtensions`, `pureCodeBlock`, `pureImage`, `lowlight` | Schema-level extensions. |
187
+
188
+ ## License
189
+
190
+ MIT © CatsInJune
@@ -0,0 +1,13 @@
1
+ import { HTMLAttributes } from 'react';
2
+ export interface ReportContentProps extends HTMLAttributes<HTMLDivElement> {
3
+ /** `renderReportHtml(...).html` 产出的正文 HTML 字符串。 */
4
+ html: string;
5
+ }
6
+ /**
7
+ * 只读静态正文渲染(RSC / SSR 安全,无 client 代码、无编辑器实例)。
8
+ *
9
+ * 把 `renderReportHtml` 产出的 HTML 注入容器,并套上与编辑器 / 只读预览**同一个
10
+ * 规范正文类**,保证「编辑所见 = 预览所见 = 发布后读者所见」。
11
+ * 用于阅读页首屏直出(SEO 可见),无需 JS 即可显示。
12
+ */
13
+ export declare function ReportContent({ html, className, ...rest }: ReportContentProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { NodeViewProps } from '@tiptap/react';
2
+ /**
3
+ * 代码块自定义 NodeView:右上角原生语言选择器 + 删除,可编辑代码内容。
4
+ * 文案(Auto-detect / 删除标题)从扩展 options.codeBlockLabels 读取,缺省用英文默认。
5
+ */
6
+ export declare function CodeBlockView({ node, updateAttributes, deleteNode, extension, }: NodeViewProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { ColorPaletteLabels } from '../labels';
2
+ export interface ColorPaletteProps {
3
+ /** 当前已应用的颜色(用于打勾),无则不选中。 */
4
+ value?: string;
5
+ /** 点击某色块。null 表示「None」(清除)。 */
6
+ onPick: (color: string | null) => void;
7
+ labels?: Partial<ColorPaletteLabels>;
8
+ }
9
+ /** 颜色面板:None 清除行 + 色板网格 + THEME 品牌色。 */
10
+ export declare function ColorPalette({ value, onPick, labels }: ColorPaletteProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { Editor } from '@tiptap/react';
2
+ import { ReactNode } from 'react';
3
+ import { ToolbarLabels } from '../labels';
4
+ /** More 菜单里注入的自定义项(宿主用它扩展工具栏,如「导入项目报告」)。 */
5
+ export interface ExtraToolbarItem {
6
+ key: string;
7
+ /** 左侧图标插槽(可选,文字或 SVG 皆可)。 */
8
+ icon?: ReactNode;
9
+ label: string;
10
+ disabled?: boolean;
11
+ /** 点击回调,拿到 editor 实例自行操作(如 insertContent)。 */
12
+ onSelect: (editor: Editor) => void;
13
+ }
14
+ export interface EditorToolbarProps {
15
+ editor: Editor;
16
+ /**
17
+ * 图片上传回调:收到文件,返回可访问的图片 URL。未提供则不显示图片按钮。
18
+ * 抛错会转交 onError。
19
+ */
20
+ onImageUpload?: (file: File) => Promise<string>;
21
+ /** 副作用错误回调(图片上传失败等),供宿主弹自己的提示。 */
22
+ onError?: (err: unknown) => void;
23
+ labels?: Partial<ToolbarLabels>;
24
+ /** 追加到 More 菜单末尾的自定义项。 */
25
+ extraToolbarItems?: ExtraToolbarItem[];
26
+ className?: string;
27
+ }
28
+ /**
29
+ * 顶部操作栏(antd-free,Radix Popover 菜单 + 内联 SVG 图标)。位置由父级控制,
30
+ * 这里只渲染按钮 + 反映 editor 激活态。不含业务耦合,扩展项经 extraToolbarItems 注入。
31
+ */
32
+ export declare function EditorToolbar({ editor, onImageUpload, onError, labels, extraToolbarItems, className, }: EditorToolbarProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ export interface MarkdownPreviewProps {
2
+ /** 要预览的 markdown 字符串。 */
3
+ markdown: string;
4
+ className?: string;
5
+ }
6
+ /**
7
+ * 轻量只读 markdown 预览(Tiptap + 官方 @tiptap/markdown,editable:false)。
8
+ * 与 MarkdownWysiwygEditor 共用同一套 base 扩展与正文样式类,渲染同源——
9
+ * 预览所见即编辑/插入后所得。用纯版 CodeBlock/Image(无 React 视图 / 删除快捷键)。
10
+ *
11
+ * 这是「客户端只读预览」;若需 SEO / 无 JS 静态渲染,请改用 server 入口的
12
+ * renderReportHtml + ReportContent。
13
+ */
14
+ export declare function MarkdownPreview({ markdown, className }: MarkdownPreviewProps): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { AnyExtension, Editor } from '@tiptap/react';
2
+ import { CodeBlockLabels } from '../labels';
3
+ import { TocItem } from '../toc/extractToc';
4
+ export interface MarkdownWysiwygEditorHandle {
5
+ /** 取当前正文的 markdown 字符串。 */
6
+ getMarkdown: () => string;
7
+ /** 取当前正文的 HTML 字符串。 */
8
+ getHTML: () => string;
9
+ /** 取当前正文的 Tiptap JSON。 */
10
+ getJSON: () => Record<string, unknown>;
11
+ /** 底层 Tiptap Editor 实例(可能为 null,未就绪时)。 */
12
+ getEditor: () => Editor | null;
13
+ }
14
+ export interface MarkdownWysiwygEditorProps {
15
+ /** 初始 markdown 内容。 */
16
+ initialMarkdown?: string;
17
+ placeholder?: string;
18
+ /** editor 实例就绪 / 销毁时回调,供外部工具栏使用。 */
19
+ onEditorReady?: (editor: Editor | null) => void;
20
+ /** 目录变化回调(正文标题增删改时),供侧边目录实时展示。 */
21
+ onTocChange?: (items: TocItem[]) => void;
22
+ /** 追加的 Tiptap 扩展(在内置扩展之后注册)。 */
23
+ extraExtensions?: AnyExtension[];
24
+ /** 代码块 NodeView 的本地化文案。 */
25
+ codeBlockLabels?: Partial<CodeBlockLabels>;
26
+ /** 附加到滚动容器的 class。 */
27
+ className?: string;
28
+ }
29
+ /**
30
+ * 所见即所得 Markdown 编辑器(基于 Tiptap v3 + 官方 @tiptap/markdown)。
31
+ * 内容进出均为 markdown 字符串。工具栏由外部渲染——通过 onEditorReady 拿到 editor 实例。
32
+ */
33
+ export declare const MarkdownWysiwygEditor: import('react').ForwardRefExoticComponent<MarkdownWysiwygEditorProps & import('react').RefAttributes<MarkdownWysiwygEditorHandle>>;
@@ -0,0 +1,20 @@
1
+ import { TocLabels } from '../labels';
2
+ import { TocItem } from '../toc/extractToc';
3
+ export interface TocPanelProps {
4
+ items: TocItem[];
5
+ /** 当前高亮的锚点 id(阅读页由 IntersectionObserver 算、编辑页由光标章节算)。 */
6
+ activeId?: string;
7
+ /** 点击目录项。锁住项不触发。默认行为由调用方决定(滚动 / 跳转)。 */
8
+ onItemClick?: (item: TocItem) => void;
9
+ labels?: Partial<TocLabels>;
10
+ }
11
+ /**
12
+ * 目录面板(阅读页 + 编辑页共用)。
13
+ *
14
+ * - 桌面:左侧粘性栏,可收起(«/»)。
15
+ * - 移动:顶部可折叠「目录」条,选中后自动收起。
16
+ * - 层级用左缩进;active accent 左边框 + 加粗;locked 置灰 + 锁图标不可点。
17
+ *
18
+ * 纯展示:滚动 / active 计算交给调用方,本组件只渲染 + 派发点击。
19
+ */
20
+ export declare function TocPanel({ items, activeId, onItemClick, labels }: TocPanelProps): import("react").JSX.Element | null;
@@ -0,0 +1,45 @@
1
+ import { AnyExtension } from '@tiptap/react';
2
+ /**
3
+ * 编辑器的「内容 schema 级」扩展集——纯节点/标记定义,不含任何 React NodeView
4
+ * 或键盘交互绑定。三处共用:
5
+ * 1. 编辑器(MarkdownWysiwygEditor):base + 交互增强版 CodeBlock/Image
6
+ * 2. 只读预览(MarkdownPreview)
7
+ * 3. server 端 markdown→HTML 渲染(renderReportHtml)
8
+ *
9
+ * 分层动机:交互增强依赖 @tiptap/react / 浏览器,会污染 server bundle(RSC)。
10
+ * base 保持纯 schema,server import 它只引入节点定义,无 React 代码。
11
+ *
12
+ * 关键边界:CodeBlock 与 Image「会被编辑器增强」,**不放进 base**——否则编辑器
13
+ * 追加增强版会与 base 的纯版同名冲突(Tiptap 报 "Duplicate extension names")。
14
+ * 改为:base 不含这两个节点,由调用方按需注入:
15
+ * - 编辑器注入「增强版」(React 视图 / 删除快捷键)
16
+ * - server / 预览注入「纯版」(pureCodeBlock / pureImage)
17
+ */
18
+ export declare const lowlight: {
19
+ highlight: (language: string, value: string, options?: Readonly<import('lowlight').Options> | null | undefined) => import('hast').Root;
20
+ highlightAuto: (value: string, options?: Readonly<import('lowlight').AutoOptions> | null | undefined) => import('hast').Root;
21
+ listLanguages: () => Array<string>;
22
+ register: {
23
+ (grammars: Readonly<Record<string, import('highlight.js').LanguageFn>>): undefined;
24
+ (name: string, grammar: import('highlight.js').LanguageFn): undefined;
25
+ };
26
+ registerAlias: {
27
+ (aliases: Readonly<Record<string, ReadonlyArray<string> | string>>): undefined;
28
+ (language: string, alias: ReadonlyArray<string> | string): undefined;
29
+ };
30
+ registered: (aliasOrName: string) => boolean;
31
+ };
32
+ /** 纯版代码块(无 React 视图):server / 预览用,产出带高亮 class 的静态结构。 */
33
+ export declare const pureCodeBlock: import('@tiptap/core').Node<import('@tiptap/extension-code-block-lowlight').CodeBlockLowlightOptions, any>;
34
+ /**
35
+ * 纯版块级图片(无删除快捷键):server / 预览用。
36
+ * inline:false(官方默认)——研报里图片均为大图独占一行,块级图片可直接作为 doc
37
+ * 顶层子节点,避免「inline image 裸挂 doc 顶层」被 generateTocIds 严格校验判非法。
38
+ */
39
+ export declare const pureImage: import('@tiptap/core').Node<import('@tiptap/extension-image').ImageOptions, any>;
40
+ /**
41
+ * 三处共用的纯 schema 扩展——**不含 CodeBlock / Image**(见上方边界说明)。
42
+ * 编辑器:`[...baseExtensions, 增强CodeBlock, 增强Image, Markdown]`
43
+ * server/预览:`[...baseExtensions, pureCodeBlock, pureImage, Markdown]`
44
+ */
45
+ export declare const baseExtensions: AnyExtension[];
@@ -0,0 +1,97 @@
1
+ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
2
+ import Highlight from "@tiptap/extension-highlight";
3
+ import Image from "@tiptap/extension-image";
4
+ import Subscript from "@tiptap/extension-subscript";
5
+ import Superscript from "@tiptap/extension-superscript";
6
+ import { TableKit } from "@tiptap/extension-table";
7
+ import TaskItem from "@tiptap/extension-task-item";
8
+ import TaskList from "@tiptap/extension-task-list";
9
+ import { TextStyle, Color, FontSize } from "@tiptap/extension-text-style";
10
+ import StarterKit from "@tiptap/starter-kit";
11
+ import { createLowlight, common } from "lowlight";
12
+ import { jsx } from "react/jsx-runtime";
13
+ const lowlight = createLowlight(common);
14
+ const pureCodeBlock = CodeBlockLowlight.configure({ lowlight });
15
+ const pureImage = Image.configure({ inline: false });
16
+ const baseExtensions = [
17
+ // 禁用 StarterKit 内置 codeBlock,统一改用带 lowlight 的代码块(由调用方注入)
18
+ StarterKit.configure({ codeBlock: false }),
19
+ TextStyle,
20
+ Color.configure({ types: [TextStyle.name] }),
21
+ // 正文字号:官方 FontSize(v3 起并入 text-style 包),以内联样式挂在 TextStyle
22
+ // 的 <span> 上(如 style="font-size: 16px"),与 Color 同基座,三端共用。
23
+ FontSize.configure({ types: [TextStyle.name] }),
24
+ Highlight.configure({ multicolor: true }),
25
+ Superscript,
26
+ Subscript,
27
+ TableKit.configure({ table: { resizable: true } }),
28
+ TaskList,
29
+ TaskItem.configure({ nested: true })
30
+ ];
31
+ const editorScroll = "_editorScroll_fahhq_4";
32
+ const editorContent = "_editorContent_fahhq_8";
33
+ const styles = {
34
+ editorScroll,
35
+ editorContent
36
+ };
37
+ function baseSlug(text) {
38
+ return text.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^\w一-龥-]/g, "").replace(/^-+|-+$/g, "") || "section";
39
+ }
40
+ function makeTocGetId() {
41
+ const seen = /* @__PURE__ */ new Map();
42
+ return (content) => {
43
+ const base = baseSlug(content);
44
+ const n = (seen.get(base) ?? 0) + 1;
45
+ seen.set(base, n);
46
+ return n === 1 ? base : `${base}-${n}`;
47
+ };
48
+ }
49
+ function ReportContent({ html, className, ...rest }) {
50
+ return /* @__PURE__ */ jsx(
51
+ "div",
52
+ {
53
+ ...rest,
54
+ className: className ? `${styles.editorContent} ${className}` : styles.editorContent,
55
+ dangerouslySetInnerHTML: { __html: html }
56
+ }
57
+ );
58
+ }
59
+ const MAX_LEVEL = 6;
60
+ function headingText(node) {
61
+ if (!node.content) return "";
62
+ return node.content.map((c) => c.type === "text" ? c.text ?? "" : "").join("").trim();
63
+ }
64
+ function extractToc(doc, lockedTitles = []) {
65
+ const locked = new Set(lockedTitles.map((t) => t.trim()));
66
+ const items = [];
67
+ const walk = (node) => {
68
+ var _a, _b, _c, _d;
69
+ const level = (_a = node.attrs) == null ? void 0 : _a.level;
70
+ if (node.type === "heading" && typeof level === "number" && level <= MAX_LEVEL) {
71
+ const text = headingText(node);
72
+ const id = ((_b = node.attrs) == null ? void 0 : _b["data-toc-id"]) ?? ((_c = node.attrs) == null ? void 0 : _c.id);
73
+ if (text && id) {
74
+ items.push({
75
+ id,
76
+ level,
77
+ text,
78
+ locked: locked.has(text) || void 0
79
+ });
80
+ }
81
+ }
82
+ (_d = node.content) == null ? void 0 : _d.forEach(walk);
83
+ };
84
+ walk(doc);
85
+ return items;
86
+ }
87
+ export {
88
+ ReportContent as R,
89
+ pureImage as a,
90
+ baseExtensions as b,
91
+ extractToc as e,
92
+ lowlight as l,
93
+ makeTocGetId as m,
94
+ pureCodeBlock as p,
95
+ styles as s
96
+ };
97
+ //# sourceMappingURL=extractToc-C7e1DuVc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractToc-C7e1DuVc.js","sources":["../src/extensions.ts","../src/toc/tocSlug.ts","../src/ReportContent.tsx","../src/toc/extractToc.ts"],"sourcesContent":["import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport { TableKit } from '@tiptap/extension-table';\nimport TaskItem from '@tiptap/extension-task-item';\nimport TaskList from '@tiptap/extension-task-list';\nimport { Color, FontSize, TextStyle } from '@tiptap/extension-text-style';\n// AnyExtension 从 @tiptap/react 取(它 re-export 自 core),避免新增 @tiptap/core 直接依赖\nimport type { AnyExtension } from '@tiptap/react';\nimport StarterKit from '@tiptap/starter-kit';\nimport { common, createLowlight } from 'lowlight';\n\n/**\n * 编辑器的「内容 schema 级」扩展集——纯节点/标记定义,不含任何 React NodeView\n * 或键盘交互绑定。三处共用:\n * 1. 编辑器(MarkdownWysiwygEditor):base + 交互增强版 CodeBlock/Image\n * 2. 只读预览(MarkdownPreview)\n * 3. server 端 markdown→HTML 渲染(renderReportHtml)\n *\n * 分层动机:交互增强依赖 @tiptap/react / 浏览器,会污染 server bundle(RSC)。\n * base 保持纯 schema,server import 它只引入节点定义,无 React 代码。\n *\n * 关键边界:CodeBlock 与 Image「会被编辑器增强」,**不放进 base**——否则编辑器\n * 追加增强版会与 base 的纯版同名冲突(Tiptap 报 \"Duplicate extension names\")。\n * 改为:base 不含这两个节点,由调用方按需注入:\n * - 编辑器注入「增强版」(React 视图 / 删除快捷键)\n * - server / 预览注入「纯版」(pureCodeBlock / pureImage)\n */\n\n// lowlight 实例:代码块语法高亮。导出供编辑器增强版与 server 纯版共用同一实例。\nexport const lowlight = createLowlight(common);\n\n/** 纯版代码块(无 React 视图):server / 预览用,产出带高亮 class 的静态结构。 */\nexport const pureCodeBlock = CodeBlockLowlight.configure({ lowlight });\n\n/**\n * 纯版块级图片(无删除快捷键):server / 预览用。\n * inline:false(官方默认)——研报里图片均为大图独占一行,块级图片可直接作为 doc\n * 顶层子节点,避免「inline image 裸挂 doc 顶层」被 generateTocIds 严格校验判非法。\n */\nexport const pureImage = Image.configure({ inline: false });\n\n/**\n * 三处共用的纯 schema 扩展——**不含 CodeBlock / Image**(见上方边界说明)。\n * 编辑器:`[...baseExtensions, 增强CodeBlock, 增强Image, Markdown]`\n * server/预览:`[...baseExtensions, pureCodeBlock, pureImage, Markdown]`\n */\nexport const baseExtensions: AnyExtension[] = [\n // 禁用 StarterKit 内置 codeBlock,统一改用带 lowlight 的代码块(由调用方注入)\n StarterKit.configure({ codeBlock: false }),\n TextStyle,\n Color.configure({ types: [TextStyle.name] }),\n // 正文字号:官方 FontSize(v3 起并入 text-style 包),以内联样式挂在 TextStyle\n // 的 <span> 上(如 style=\"font-size: 16px\"),与 Color 同基座,三端共用。\n FontSize.configure({ types: [TextStyle.name] }),\n Highlight.configure({ multicolor: true }),\n Superscript,\n Subscript,\n TableKit.configure({ table: { resizable: true } }),\n TaskList,\n TaskItem.configure({ nested: true }),\n];\n","/**\n * 目录锚点 id 生成——阅读页(SSR)与编辑页(editor)共用的**单一真源**,\n * 保证「创作预览的锚点 = 发布后阅读页的锚点」。\n *\n * 用 slug 而非官方默认 UUID:slug 跨 SSR / ISR revalidate 稳定(UUID 每次都变、\n * 分享的 #hash 会失效),且 `#introduction` 比 `#d4590f81-…` 可读、对 SEO 友好。\n *\n * slug 会重名(两个「细节」都 slug 成 `xi-jie`),故用闭包计数器去重:\n * 同名追加 -2 / -3(已实测:details / details-2 / details-3,HTML id 全唯一)。\n */\n\n/** 单个标题文本 → 基础 slug(保留中文,去标点,空白转连字符)。 */\nfunction baseSlug(text: string): string {\n return (\n text\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, '-')\n // 保留字母/数字/下划线/中文/连字符,其余剔除\n .replace(/[^\\w一-龥-]/g, '')\n .replace(/^-+|-+$/g, '') || 'section'\n );\n}\n\n/**\n * 造一个带去重的 `getId`。每次生成新实例(闭包各自计数),传给\n * `TableOfContents.configure({ getId })` 或 `generateTocIds` 的扩展配置。\n */\nexport function makeTocGetId(): (content: string) => string {\n const seen = new Map<string, number>();\n return (content: string) => {\n const base = baseSlug(content);\n const n = (seen.get(base) ?? 0) + 1;\n seen.set(base, n);\n return n === 1 ? base : `${base}-${n}`;\n };\n}\n","import type { HTMLAttributes } from 'react';\nimport styles from './styles/content.module.css';\n\nexport interface ReportContentProps extends HTMLAttributes<HTMLDivElement> {\n /** `renderReportHtml(...).html` 产出的正文 HTML 字符串。 */\n html: string;\n}\n\n/**\n * 只读静态正文渲染(RSC / SSR 安全,无 client 代码、无编辑器实例)。\n *\n * 把 `renderReportHtml` 产出的 HTML 注入容器,并套上与编辑器 / 只读预览**同一个\n * 规范正文类**,保证「编辑所见 = 预览所见 = 发布后读者所见」。\n * 用于阅读页首屏直出(SEO 可见),无需 JS 即可显示。\n */\nexport function ReportContent({ html, className, ...rest }: ReportContentProps) {\n return (\n <div\n {...rest}\n className={\n className ? `${styles.editorContent} ${className}` : styles.editorContent\n }\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n","/**\n * 最小 Tiptap JSON 节点结构(只取本模块用到的字段)。\n * 不从 `@tiptap/core` 引 `JSONContent`——core 是嵌套依赖、非直接依赖,\n * 直接 import 类型解析不到(与仓库既有做法一致,见 extensions.ts)。\n */\ninterface TocDocNode {\n type?: string;\n text?: string;\n attrs?: Record<string, unknown> | null;\n content?: TocDocNode[];\n}\n\n/** 目录项。 */\nexport interface TocItem {\n /** 锚点 id(= heading 的 data-toc-id / id,由 generateTocIds 注入)。 */\n id: string;\n /** 标题层级(1-6)。 */\n level: number;\n /** 标题纯文本。 */\n text: string;\n /** 付费墙锁住的章节(置灰不可点)。 */\n locked?: boolean;\n}\n\n/** 目录纳入的最大层级(h1-h6,全层级入目录)。 */\nconst MAX_LEVEL = 6;\n\n/** 取 heading 节点的纯文本(拼接其 text 子节点)。 */\nfunction headingText(node: TocDocNode): string {\n if (!node.content) return '';\n return node.content\n .map((c) => (c.type === 'text' ? c.text ?? '' : ''))\n .join('')\n .trim();\n}\n\n/**\n * 从已注入锚点 id 的 Tiptap JSON doc 提取 h1-h6 目录。\n *\n * 必须在 `generateTocIds` 之后调用(heading 的 attrs 里才有 `data-toc-id`)。\n * `lockedTitles`:被截断(锁住)章节标题,命中则标 `locked`——\n * 目录列全部章节,锁住项置灰不可点(付费墙钩子)。用 Set 便于去重命中。\n */\nexport function extractToc(\n doc: TocDocNode,\n lockedTitles: string[] = [],\n): TocItem[] {\n const locked = new Set(lockedTitles.map((t) => t.trim()));\n const items: TocItem[] = [];\n\n const walk = (node: TocDocNode) => {\n const level = node.attrs?.level;\n if (\n node.type === 'heading' &&\n typeof level === 'number' &&\n level <= MAX_LEVEL\n ) {\n const text = headingText(node);\n const id = (node.attrs?.['data-toc-id'] ?? node.attrs?.id) as\n | string\n | undefined;\n // 有文本 + 有锚点 id 才入目录(空标题 / 未注入 id 的跳过)。\n if (text && id) {\n items.push({\n id,\n level,\n text,\n locked: locked.has(text) || undefined,\n });\n }\n }\n node.content?.forEach(walk);\n };\n\n walk(doc);\n return items;\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAgCO,MAAM,WAAW,eAAe,MAAM;AAGtC,MAAM,gBAAgB,kBAAkB,UAAU,EAAE,UAAU;AAO9D,MAAM,YAAY,MAAM,UAAU,EAAE,QAAQ,OAAO;AAOnD,MAAM,iBAAiC;AAAA;AAAA,EAE5C,WAAW,UAAU,EAAE,WAAW,OAAO;AAAA,EACzC;AAAA,EACA,MAAM,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,GAAG;AAAA;AAAA;AAAA,EAG3C,SAAS,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,GAAG;AAAA,EAC9C,UAAU,UAAU,EAAE,YAAY,MAAM;AAAA,EACxC;AAAA,EACA;AAAA,EACA,SAAS,UAAU,EAAE,OAAO,EAAE,WAAW,KAAA,GAAQ;AAAA,EACjD;AAAA,EACA,SAAS,UAAU,EAAE,QAAQ,MAAM;AACrC;;;;;;;ACnDA,SAAS,SAAS,MAAsB;AACtC,SACE,KACG,YAAA,EACA,KAAA,EACA,QAAQ,QAAQ,GAAG,EAEnB,QAAQ,cAAc,EAAE,EACxB,QAAQ,YAAY,EAAE,KAAK;AAElC;AAMO,SAAS,eAA4C;AAC1D,QAAM,2BAAW,IAAA;AACjB,SAAO,CAAC,YAAoB;AAC1B,UAAM,OAAO,SAAS,OAAO;AAC7B,UAAM,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK;AAClC,SAAK,IAAI,MAAM,CAAC;AAChB,WAAO,MAAM,IAAI,OAAO,GAAG,IAAI,IAAI,CAAC;AAAA,EACtC;AACF;ACrBO,SAAS,cAAc,EAAE,MAAM,WAAW,GAAG,QAA4B;AAC9E,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACE,GAAG;AAAA,MACJ,WACE,YAAY,GAAG,OAAO,aAAa,IAAI,SAAS,KAAK,OAAO;AAAA,MAE9D,yBAAyB,EAAE,QAAQ,KAAA;AAAA,IAAK;AAAA,EAAA;AAG9C;ACAA,MAAM,YAAY;AAGlB,SAAS,YAAY,MAA0B;AAC7C,MAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,SAAO,KAAK,QACT,IAAI,CAAC,MAAO,EAAE,SAAS,SAAS,EAAE,QAAQ,KAAK,EAAG,EAClD,KAAK,EAAE,EACP,KAAA;AACL;AASO,SAAS,WACd,KACA,eAAyB,IACd;AACX,QAAM,SAAS,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,EAAE,KAAA,CAAM,CAAC;AACxD,QAAM,QAAmB,CAAA;AAEzB,QAAM,OAAO,CAAC,SAAqB;;AACjC,UAAM,SAAQ,UAAK,UAAL,mBAAY;AAC1B,QACE,KAAK,SAAS,aACd,OAAO,UAAU,YACjB,SAAS,WACT;AACA,YAAM,OAAO,YAAY,IAAI;AAC7B,YAAM,OAAM,UAAK,UAAL,mBAAa,qBAAkB,UAAK,UAAL,mBAAY;AAIvD,UAAI,QAAQ,IAAI;AACd,cAAM,KAAK;AAAA,UACT;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,OAAO,IAAI,IAAI,KAAK;AAAA,QAAA,CAC7B;AAAA,MACH;AAAA,IACF;AACA,eAAK,YAAL,mBAAc,QAAQ;AAAA,EACxB;AAEA,OAAK,GAAG;AACR,SAAO;AACT;"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 是否窄屏(默认 <768px)。取代原项目的 useDevice,内联为纯 matchMedia,
3
+ * 无第三方依赖。SSR 首帧返回 false,挂载后按实际视口更新。
4
+ */
5
+ export declare function useIsMobile(breakpoint?: number): boolean;
@@ -0,0 +1,30 @@
1
+ import { SVGProps } from 'react';
2
+ /**
3
+ * 内联 SVG 图标集(0 运行时依赖)。stroke 走 currentColor,尺寸默认 1em 跟随字号,
4
+ * 故颜色/大小随文本与主题自动变化。图形取自 Lucide(MIT)风格。
5
+ */
6
+ export interface IconProps extends SVGProps<SVGSVGElement> {
7
+ size?: number | string;
8
+ }
9
+ export declare const BoldIcon: (p: IconProps) => import("react").JSX.Element;
10
+ export declare const ItalicIcon: (p: IconProps) => import("react").JSX.Element;
11
+ export declare const UnderlineIcon: (p: IconProps) => import("react").JSX.Element;
12
+ export declare const StrikethroughIcon: (p: IconProps) => import("react").JSX.Element;
13
+ export declare const CodeIcon: (p: IconProps) => import("react").JSX.Element;
14
+ export declare const LinkIcon: (p: IconProps) => import("react").JSX.Element;
15
+ export declare const ImageIcon: (p: IconProps) => import("react").JSX.Element;
16
+ export declare const ListUnorderedIcon: (p: IconProps) => import("react").JSX.Element;
17
+ export declare const ListOrderedIcon: (p: IconProps) => import("react").JSX.Element;
18
+ export declare const ListChecksIcon: (p: IconProps) => import("react").JSX.Element;
19
+ export declare const UndoIcon: (p: IconProps) => import("react").JSX.Element;
20
+ export declare const RedoIcon: (p: IconProps) => import("react").JSX.Element;
21
+ export declare const ChevronDownIcon: (p: IconProps) => import("react").JSX.Element;
22
+ export declare const TrashIcon: (p: IconProps) => import("react").JSX.Element;
23
+ export declare const LockIcon: (p: IconProps) => import("react").JSX.Element;
24
+ export declare const MenuIcon: (p: IconProps) => import("react").JSX.Element;
25
+ export declare const ColumnBeforeIcon: (p: IconProps) => import("react").JSX.Element;
26
+ export declare const ColumnAfterIcon: (p: IconProps) => import("react").JSX.Element;
27
+ export declare const ColumnDeleteIcon: (p: IconProps) => import("react").JSX.Element;
28
+ export declare const RowBeforeIcon: (p: IconProps) => import("react").JSX.Element;
29
+ export declare const RowAfterIcon: (p: IconProps) => import("react").JSX.Element;
30
+ export declare const RowDeleteIcon: (p: IconProps) => import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ export { MarkdownWysiwygEditor, type MarkdownWysiwygEditorHandle, type MarkdownWysiwygEditorProps, } from './components/MarkdownWysiwygEditor';
2
+ export { MarkdownPreview, type MarkdownPreviewProps, } from './components/MarkdownPreview';
3
+ export { EditorToolbar, type EditorToolbarProps, type ExtraToolbarItem, } from './components/EditorToolbar';
4
+ export { ColorPalette, type ColorPaletteProps } from './components/ColorPalette';
5
+ export { TocPanel, type TocPanelProps } from './components/TocPanel';
6
+ export { CodeBlockView } from './components/CodeBlockView';
7
+ export { ReportContent, type ReportContentProps } from './ReportContent';
8
+ export { baseExtensions, lowlight, pureCodeBlock, pureImage, } from './extensions';
9
+ export { extractToc, type TocItem } from './toc/extractToc';
10
+ export { makeTocGetId } from './toc/tocSlug';
11
+ export { defaultCodeBlockLabels, defaultColorPaletteLabels, defaultTocLabels, defaultToolbarLabels, type CodeBlockLabels, type ColorPaletteLabels, type TocLabels, type ToolbarLabels, } from './labels';
12
+ export { useIsMobile } from './hooks/useIsMobile';
13
+ export * from './icons';