@comitor/ui 3.5.0 → 3.6.1

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.
Files changed (51) hide show
  1. package/README.md +4 -2
  2. package/dist/data-grid/data-grid-cell-variants.js +6 -1
  3. package/dist/data-grid/data-grid-cell-variants.js.map +1 -1
  4. package/dist/data-grid/types.d.ts +10 -0
  5. package/dist/data-grid/use-data-grid.d.ts +2 -2
  6. package/dist/data-grid/use-data-grid.js +30 -13
  7. package/dist/data-grid/use-data-grid.js.map +1 -1
  8. package/dist/editor/bubble-toolbar.d.ts +20 -1
  9. package/dist/editor/bubble-toolbar.js +12 -172
  10. package/dist/editor/bubble-toolbar.js.map +1 -1
  11. package/dist/editor/editor-core.d.ts +28 -0
  12. package/dist/editor/editor-core.js +354 -0
  13. package/dist/editor/editor-core.js.map +1 -0
  14. package/dist/editor/editor-types.d.ts +3 -0
  15. package/dist/editor/editor-types.js +1 -0
  16. package/dist/editor/editor-types.js.map +1 -0
  17. package/dist/editor/fixed-toolbar.d.ts +19 -0
  18. package/dist/editor/fixed-toolbar.js +106 -0
  19. package/dist/editor/fixed-toolbar.js.map +1 -0
  20. package/dist/editor/image-upload.d.ts +3 -57
  21. package/dist/editor/image-upload.js +3 -2
  22. package/dist/editor/image-upload.js.map +1 -1
  23. package/dist/editor/index.d.ts +3 -2
  24. package/dist/editor/index.js +4 -4
  25. package/dist/editor/index.js.map +1 -1
  26. package/dist/editor/markdown-editor-classes.js +6 -4
  27. package/dist/editor/markdown-editor-classes.js.map +1 -1
  28. package/dist/editor/markdown-editor-constants.d.ts +8 -2
  29. package/dist/editor/markdown-editor-constants.js.map +1 -1
  30. package/dist/editor/markdown-editor.d.ts +14 -91
  31. package/dist/editor/markdown-editor.js +23 -315
  32. package/dist/editor/markdown-editor.js.map +1 -1
  33. package/dist/editor/markdown-extensions.d.ts +40 -15
  34. package/dist/editor/markdown-extensions.js +143 -24
  35. package/dist/editor/markdown-extensions.js.map +1 -1
  36. package/dist/editor/rich-text-editor.d.ts +48 -0
  37. package/dist/editor/rich-text-editor.js +33 -0
  38. package/dist/editor/rich-text-editor.js.map +1 -0
  39. package/dist/editor/slash-commands.d.ts +4 -1
  40. package/dist/editor/slash-commands.js +7 -2
  41. package/dist/editor/slash-commands.js.map +1 -1
  42. package/dist/editor/toolbar-actions.d.ts +44 -0
  43. package/dist/editor/toolbar-actions.js +173 -0
  44. package/dist/editor/toolbar-actions.js.map +1 -0
  45. package/dist/editor/toolbar-button.d.ts +15 -0
  46. package/dist/editor/toolbar-button.js +47 -0
  47. package/dist/editor/toolbar-button.js.map +1 -0
  48. package/dist/editor-types-BMST7-VR.d.ts +171 -0
  49. package/dist/i18n/namespaces/markdown-editor.js +6 -0
  50. package/dist/i18n/namespaces/markdown-editor.js.map +1 -1
  51. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { Extension } from "@tiptap/core";
1
+ import { Extension, InputRule, mergeAttributes } from "@tiptap/core";
2
2
  import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
3
3
  import { Image } from "@tiptap/extension-image";
4
4
  import { TaskItem, TaskList } from "@tiptap/extension-list";
@@ -7,6 +7,8 @@ import { Paragraph } from "@tiptap/extension-paragraph";
7
7
  import { renderTableToMarkdown, Table, TableCell, TableHeader, TableKit } from "@tiptap/extension-table";
8
8
  import { Placeholder } from "@tiptap/extensions";
9
9
  import { Markdown } from "@tiptap/markdown";
10
+ import { Fragment, Slice } from "@tiptap/pm/model";
11
+ import { Plugin } from "@tiptap/pm/state";
10
12
  import { StarterKit } from "@tiptap/starter-kit";
11
13
  import { common, createLowlight } from "lowlight";
12
14
  import { Marked } from "marked";
@@ -44,16 +46,104 @@ const ComitorTable = Table.extend({
44
46
  });
45
47
  const ComitorTableCell = TableCell.extend({ content: "paragraph" });
46
48
  const ComitorTableHeader = TableHeader.extend({ content: "paragraph" });
49
+ const HORIZONTAL_RULE_INPUT = /^(?:---|—-|___\s|\*\*\*\s)$/;
50
+ const MarkdownTableCellGuard = Extension.create({
51
+ name: "comitorMarkdownTableCellGuard",
52
+ // Cao hơn StarterKit (100): mỗi extension một plugin luật nhập, xếp theo `priority` — luật dưới đây
53
+ // phải được hỏi TRƯỚC luật đường kẻ, không thì đường kẻ đã chèn xong.
54
+ priority: 1e3,
55
+ addInputRules() {
56
+ return [
57
+ new InputRule({
58
+ find: HORIZONTAL_RULE_INPUT,
59
+ undoable: false,
60
+ handler: ({ state, range, match }) => {
61
+ if (!isInTableCell(state.doc.resolve(range.from))) return null;
62
+ state.tr.insertText(match[0], range.from, range.to);
63
+ }
64
+ })
65
+ ];
66
+ },
67
+ addProseMirrorPlugins() {
68
+ let pastedTable = false;
69
+ return [
70
+ new Plugin({
71
+ props: {
72
+ transformPastedHTML: (html) => {
73
+ pastedTable = /<(?:table|tr|td|th)[\s>/]/i.test(html);
74
+ return html;
75
+ },
76
+ transformPastedText: (text) => {
77
+ pastedTable = false;
78
+ return text;
79
+ },
80
+ transformPasted: (slice, view) => {
81
+ const keepTables = pastedTable;
82
+ pastedTable = false;
83
+ return isInTableCell(view.state.selection.$from) ? flattenSliceToLine(slice, keepTables) : slice;
84
+ }
85
+ },
86
+ filterTransaction: (tr, state) => {
87
+ if (!tr.docChanged || tr.getMeta("history$") || !isInTableCell(state.selection.$from)) return true;
88
+ if (replacesWholeDocument(tr.steps, state.doc.content.size)) return true;
89
+ return countTables(tr.doc) <= countTables(state.doc);
90
+ }
91
+ })
92
+ ];
93
+ }
94
+ });
95
+ function flattenSliceToLine(slice, keepTables = false) {
96
+ const first = slice.content.firstChild;
97
+ if (!first || first.isInline) return slice;
98
+ if (keepTables) {
99
+ let tabular = false;
100
+ slice.content.descendants((node) => {
101
+ if (node.type.spec.tableRole) tabular = true;
102
+ return !tabular;
103
+ });
104
+ if (tabular) return slice;
105
+ }
106
+ const schema = first.type.schema;
107
+ const line = [];
108
+ slice.content.descendants((node) => {
109
+ if (!node.isTextblock) return true;
110
+ if (node.content.size === 0) return false;
111
+ if (line.length > 0) line.push(schema.text(" "));
112
+ node.content.forEach((child) => {
113
+ if (child.type.name === "hardBreak") line.push(schema.text(" "));
114
+ else if (child.isText && child.text?.includes("\n"))
115
+ line.push(schema.text(child.text.replace(/\s*\n\s*/g, " "), child.marks));
116
+ else line.push(child);
117
+ });
118
+ return false;
119
+ });
120
+ return new Slice(Fragment.from(line), 0, 0);
121
+ }
122
+ function countTables(doc) {
123
+ let count = 0;
124
+ doc.descendants((node) => {
125
+ if (node.type.name === "table") count++;
126
+ return node.type.name !== "table";
127
+ });
128
+ return count;
129
+ }
130
+ function replacesWholeDocument(steps, size) {
131
+ return steps.some((step) => {
132
+ const { from, to } = step;
133
+ return from === 0 && to === size;
134
+ });
135
+ }
47
136
  function createMarkedInstance() {
48
137
  return new Marked();
49
138
  }
50
- function createMarkdownSchemaExtensions(options = {}) {
139
+ function createSchemaExtensions(options = {}) {
51
140
  const runtime = options.runtime ?? { current: DEFAULT_RUNTIME };
141
+ const markdown = (options.format ?? "markdown") === "markdown";
52
142
  return [
53
143
  StarterKit.configure({
54
144
  paragraph: false,
55
145
  codeBlock: false,
56
- underline: false,
146
+ ...markdown ? { underline: false } : {},
57
147
  link: {
58
148
  // Đang sửa thì bấm vào liên kết là đặt con trỏ, không phải mở tab mới (thanh định dạng có nút
59
149
  // Mở). Chỉ đọc thì Tiptap không chặn cú bấm — thẻ `<a target="_blank">` tự mở như thường.
@@ -66,7 +156,7 @@ function createMarkdownSchemaExtensions(options = {}) {
66
156
  HTMLAttributes: { target: "_blank", rel: "noopener noreferrer nofollow" }
67
157
  }
68
158
  }),
69
- ComitorParagraph,
159
+ markdown ? ComitorParagraph : Paragraph,
70
160
  ComitorCodeBlock.configure({
71
161
  lowlight,
72
162
  defaultLanguage: null,
@@ -82,24 +172,32 @@ function createMarkdownSchemaExtensions(options = {}) {
82
172
  // liền chữ của mọi mục con ("Gửi mã qua emailGiới hạn 5 lần/giờ") — đã gặp.
83
173
  a11y: { checkboxLabel: (node) => runtime.current.taskCheckbox(node.firstChild?.textContent ?? "") }
84
174
  }),
85
- TableKit.configure({ table: false, tableCell: false, tableHeader: false }),
86
- ComitorTable.configure({ resizable: false }),
87
- ComitorTableCell,
88
- ComitorTableHeader,
175
+ ...markdown ? [
176
+ TableKit.configure({ table: false, tableCell: false, tableHeader: false }),
177
+ ComitorTable.configure({ resizable: false }),
178
+ ComitorTableCell,
179
+ ComitorTableHeader,
180
+ MarkdownTableCellGuard
181
+ ] : [TableKit.configure({ table: { resizable: false } })],
89
182
  createImageExtension(runtime),
90
183
  createMentionExtension(runtime, options.mentionSuggestion),
91
184
  Placeholder.configure({ placeholder: () => runtime.current.placeholder }),
92
- Markdown.configure({ marked: createMarkedInstance(), indentation: { style: "space", size: 2 } })
185
+ ...markdown ? [Markdown.configure({ marked: createMarkedInstance(), indentation: { style: "space", size: 2 } })] : []
93
186
  ];
94
187
  }
188
+ function createMarkdownSchemaExtensions(options = {}) {
189
+ return createSchemaExtensions({ ...options, format: "markdown" });
190
+ }
95
191
  function createImageExtension(runtime) {
96
192
  return Image.extend({
97
- renderHTML({ HTMLAttributes }) {
98
- const src = typeof HTMLAttributes.src === "string" ? HTMLAttributes.src : "";
99
- return [
100
- "img",
101
- { ...this.options.HTMLAttributes, ...HTMLAttributes, src: runtime.current.resolveImageSrc?.(src) ?? src }
102
- ];
193
+ addNodeView() {
194
+ return ({ node, HTMLAttributes }) => {
195
+ const image = document.createElement("img");
196
+ setAttributes(image, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ["src"]);
197
+ const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
198
+ if (src) image.setAttribute("src", runtime.current.resolveImageSrc?.(src) ?? src);
199
+ return { dom: image, update: (updated) => sameNode(updated, node) };
200
+ };
103
201
  },
104
202
  renderMarkdown(node) {
105
203
  return imageMarkdown(String(node.attrs?.src ?? ""), node.attrs?.alt, node.attrs?.title);
@@ -107,11 +205,11 @@ function createImageExtension(runtime) {
107
205
  }).configure({ allowBase64: false });
108
206
  }
109
207
  function createMentionExtension(runtime, suggestion) {
110
- const labelOf = (attrs) => {
111
- const id = typeof attrs.id === "string" ? attrs.id : "";
208
+ const storedLabelOf = (attrs) => {
112
209
  const stored = typeof attrs.label === "string" ? attrs.label : "";
113
- return runtime.current.resolveMentionLabel?.(id) ?? (stored || id);
210
+ return stored || (typeof attrs.id === "string" ? attrs.id : "");
114
211
  };
212
+ const labelOf = (attrs) => runtime.current.resolveMentionLabel?.(typeof attrs.id === "string" ? attrs.id : "") ?? storedLabelOf(attrs);
115
213
  return Mention.extend({
116
214
  markdownTokenName: "mention",
117
215
  markdownTokenizer: {
@@ -124,15 +222,24 @@ function createMentionExtension(runtime, suggestion) {
124
222
  }
125
223
  },
126
224
  parseMarkdown: (token, helpers) => helpers.createNode("mention", { id: token.id, label: token.label }),
127
- renderMarkdown: (node) => renderMentionMarkdown(String(node.attrs?.id ?? ""), node.attrs?.label)
225
+ renderMarkdown: (node) => renderMentionMarkdown(String(node.attrs?.id ?? ""), node.attrs?.label),
226
+ addNodeView() {
227
+ return ({ node, HTMLAttributes }) => {
228
+ const dom = document.createElement("span");
229
+ setAttributes(dom, mergeAttributes({ "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes));
230
+ dom.textContent = `@${labelOf(node.attrs)}`;
231
+ return { dom, update: (updated) => sameNode(updated, node) };
232
+ };
233
+ }
128
234
  }).configure({
129
235
  renderText: ({ node }) => `@${labelOf(node.attrs)}`,
130
236
  // `options.HTMLAttributes` ở đây là bản Tiptap đã trộn sẵn (`data-type`, `data-id`, `data-label`).
131
- renderHTML: ({ options, node }) => ["span", options.HTMLAttributes, `@${labelOf(node.attrs)}`],
237
+ renderHTML: ({ options, node }) => ["span", options.HTMLAttributes, `@${storedLabelOf(node.attrs)}`],
132
238
  suggestion: { char: "@", allow: () => false, ...suggestion }
133
239
  });
134
240
  }
135
- function createEditorKeymap(handlers) {
241
+ function createEditorKeymap(handlers, format = "markdown") {
242
+ const swallowInCell = (editor) => format === "markdown" && isInTableCell(editor.state.selection.$from);
136
243
  return Extension.create({
137
244
  name: "comitorEditorKeymap",
138
245
  priority: 1e3,
@@ -151,9 +258,9 @@ function createEditorKeymap(handlers) {
151
258
  return false;
152
259
  });
153
260
  if (toggled || handlers.current.onSubmitShortcut()) return true;
154
- return isInTableCell(editor.state.selection.$from);
261
+ return swallowInCell(editor);
155
262
  },
156
- "Shift-Enter": ({ editor }) => isInTableCell(editor.state.selection.$from)
263
+ "Shift-Enter": ({ editor }) => swallowInCell(editor)
157
264
  };
158
265
  }
159
266
  });
@@ -165,8 +272,20 @@ function isInTableCell($pos) {
165
272
  }
166
273
  return false;
167
274
  }
275
+ function setAttributes(element, attributes, skip = []) {
276
+ for (const [key, value] of Object.entries(attributes)) {
277
+ if (skip.includes(key) || value === null || value === void 0 || value === false) continue;
278
+ element.setAttribute(key, String(value));
279
+ }
280
+ }
281
+ function sameNode(updated, current) {
282
+ if (updated.type !== current.type) return false;
283
+ return Object.keys(current.attrs).every((key) => updated.attrs[key] === current.attrs[key]);
284
+ }
168
285
  export {
169
286
  createEditorKeymap,
170
- createMarkdownSchemaExtensions
287
+ createMarkdownSchemaExtensions,
288
+ createSchemaExtensions,
289
+ flattenSliceToLine
171
290
  };
172
291
  //# sourceMappingURL=markdown-extensions.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/markdown-extensions.ts"],"sourcesContent":["/**\n * LƯỢC ĐỒ + MARKDOWN của `MarkdownEditor` — danh sách extension Tiptap, không React, không DOM.\n *\n * Mọi thứ quyết định \"tài liệu này lưu xuống thành chuỗi markdown nào, và đọc lên lại thành tài liệu\n * nào\" nằm ở đây, và CHỈ ở đây: trình soạn dùng nó, cổng `test:editor-markdown` dựng\n * `MarkdownManager` từ chính hàm này trên `dist/` rồi đo vòng tài liệu → markdown → tài liệu. Hai\n * nơi mà dùng hai danh sách là hai lược đồ, và cổng xanh trên một lược đồ không ai dùng.\n *\n * Phần GIAO DIỆN (menu `/`, menu `@`, tải ảnh, phím tắt) gắn vào qua tham số — xem\n * `MarkdownSchemaOptions`. Gọi không tham số là ra đúng lược đồ, đủ để đọc/ghi markdown.\n */\n\nimport { type AnyExtension, Extension, type JSONContent } from \"@tiptap/core\";\nimport { CodeBlockLowlight } from \"@tiptap/extension-code-block-lowlight\";\nimport { Image } from \"@tiptap/extension-image\";\nimport { TaskItem, TaskList } from \"@tiptap/extension-list\";\nimport { Mention, type MentionOptions } from \"@tiptap/extension-mention\";\nimport { Paragraph } from \"@tiptap/extension-paragraph\";\nimport { renderTableToMarkdown, Table, TableCell, TableHeader, TableKit } from \"@tiptap/extension-table\";\nimport { Placeholder } from \"@tiptap/extensions\";\nimport { Markdown } from \"@tiptap/markdown\";\nimport type { ResolvedPos } from \"@tiptap/pm/model\";\nimport { StarterKit } from \"@tiptap/starter-kit\";\nimport { common, createLowlight } from \"lowlight\";\nimport { Marked, type marked } from \"marked\";\nimport {\n codeFence,\n escapeParagraphMarkdown,\n imageMarkdown,\n parseMentionMarkdown,\n renderMentionMarkdown\n} from \"./markdown-syntax.js\";\n\n/**\n * Giá trị đổi được SAU KHI trình soạn đã dựng. Extension đọc chúng lúc CẦN (lúc vẽ chữ mờ, lúc vẽ một\n * lần nhắc tên…), không chép lúc dựng — vì đổi danh sách extension là dựng lại cả trình soạn: mất\n * lịch sử hoàn tác, mất con trỏ. Component giữ object này trong một ref và ghi đè từng lượt render.\n */\nexport interface MarkdownEditorRuntime {\n placeholder: string;\n taskCheckbox: (text: string) => string;\n /** Đổi `src` ĐÃ LƯU thành địa chỉ HIỂN THỊ được (vd URL đã ký). Markdown vẫn giữ `src` gốc. */\n resolveImageSrc?: (src: string) => string;\n /** Tên SỐNG của một người theo id. `undefined` thì rơi về tên chụp lúc nhắc. */\n resolveMentionLabel?: (id: string) => string | undefined;\n}\n\nconst DEFAULT_RUNTIME: MarkdownEditorRuntime = {\n placeholder: \"\",\n taskCheckbox: (text) => text\n};\n\nexport interface MarkdownSchemaOptions {\n /** Đọc lười — xem `MarkdownEditorRuntime`. Bỏ trống ở nơi chỉ cần đọc/ghi markdown (cổng, máy chủ). */\n runtime?: { readonly current: MarkdownEditorRuntime };\n /**\n * Cấu hình gợi ý `@` của `Mention` (danh sách, cách vẽ, `allow`). Bỏ trống = node `mention` vẫn có\n * trong lược đồ — để tài liệu CÓ nhắc tên vẫn đọc ra đúng ở chế độ chỉ đọc — nhưng gõ `@` không\n * mở gì.\n */\n mentionSuggestion?: Partial<MentionOptions[\"suggestion\"]>;\n}\n\n/*\n * Ngôn ngữ tô màu: bộ `common` của highlight.js (37 ngôn ngữ, ~53KB gzip — đo bằng esbuild trên\n * lowlight 3.3.0). Dựng MỘT lần cho cả module: bảng ngôn ngữ chỉ đọc, chia được giữa các trình soạn.\n */\nconst lowlight = createLowlight(common);\n\n/**\n * Đoạn văn — thêm phép thoát cú pháp khối (ca 1 ở đầu `markdown-syntax.ts`). Mọi hành vi khác giữ\n * nguyên bản gốc, kể cả `&nbsp;` cho đoạn rỗng: đó là cách CỐ Ý của Tiptap để giữ dòng trống người\n * dùng gõ — bỏ nó đi là nội dung \"co\" lại sau mỗi lần lưu.\n */\nconst ComitorParagraph = Paragraph.extend({\n renderMarkdown(node, helpers, context) {\n return escapeParagraphMarkdown(this.parent?.(node, helpers, context) ?? \"\");\n }\n});\n\n/** Khối mã — hàng rào dài hơn mọi dãy backtick bên trong (ca 2 ở đầu `markdown-syntax.ts`). */\nconst ComitorCodeBlock = CodeBlockLowlight.extend({\n renderMarkdown(node: JSONContent) {\n const language = typeof node.attrs?.language === \"string\" ? node.attrs.language : \"\";\n const text = (node.content ?? []).map((child) => child.text ?? \"\").join(\"\");\n const fence = codeFence(text);\n return `${fence}${language}\\n${text}\\n${fence}`;\n }\n});\n\n/** Bảng — bỏ dòng trống bản gốc kẹp hai đầu (xem `trimMarkdown`). */\nconst ComitorTable = Table.extend({\n renderMarkdown(node, helpers) {\n return renderTableToMarkdown(node, helpers).replace(/^\\n+|\\n+$/g, \"\");\n }\n});\n\n/*\n * Ô bảng chứa ĐÚNG MỘT đoạn văn — bản gốc cho `block+` (danh sách, khối mã, nhiều đoạn trong một ô).\n *\n * Ô bảng GFM là một dòng chữ, không chứa khối được. Đã đo trên 3.31.4: ô có hai đoạn ghi thành\n * `dòng 1<br>dòng 2`, ô có danh sách thành `- a<br>- b`, khối mã thành ```` ```<br>code<br>``` ````;\n * mở lại thì `<br>` là CHỮ THƯỜNG, và lần lưu kế tiếp nó thành `&lt;br&gt;` — mỗi lần lưu hỏng thêm\n * một lớp. Thu hẹp lược đồ là cách duy nhất chặn từ gốc: luật nhập (`- `, `# `, ```` ``` ````) và\n * lệnh khối tự thất bại trong ô vì lược đồ không cho, không cần chặn từng chỗ. Xuống dòng cứng trong ô\n * thì `createEditorKeymap` nuốt — `hardBreak` là node NỘI DÒNG nên lược đồ không chặn được nó.\n */\nconst ComitorTableCell = TableCell.extend({ content: \"paragraph\" });\nconst ComitorTableHeader = TableHeader.extend({ content: \"paragraph\" });\n\n/**\n * Bảng chữ markdown RIÊNG cho từng trình soạn.\n *\n * ⚠ Không truyền thì `@tiptap/markdown` dùng `marked` TOÀN CỤC, và mỗi lần trình soạn mount nó gọi\n * `marked.use()` đăng ký lại bộ tách từ (bảng, nhắc tên…) — không bao giờ gỡ. Đã đo trên 3.31.4:\n * mỗi `MarkdownManager` thêm 4 bộ tách; sau 200 lần dựng, parse cùng một tài liệu 200 đoạn chậm từ\n * 18,6ms lên 282ms, và còn tăng tiếp. Trong một SPA, 200 lần là một buổi làm việc mở việc này sang\n * việc khác. Một bản `Marked` mỗi trình soạn: 4 bộ tách, 5,7ms, mất cùng trình soạn.\n *\n * Ép kiểu vì option khai `typeof marked` (hàm `marked` toàn cục, gọi được) mà thứ đúng là một bản\n * `Marked`: `MarkdownManager` chỉ dùng `.use`, `.Lexer`, `.defaults`, `.lexer`, `.setOptions` — bản\n * `Marked` có đủ cả năm, chỉ thiếu chữ ký gọi-như-hàm mà không chỗ nào gọi tới.\n */\nfunction createMarkedInstance(): typeof marked {\n return new Marked() as unknown as typeof marked;\n}\n\n/**\n * Danh sách extension tạo nên lược đồ + định dạng markdown của trình soạn.\n *\n * ⚠ `underline` CỐ Ý tắt: markdown không có cú pháp gạch chân. `@tiptap/markdown` ghi nó thành\n * `++chữ++` — cú pháp riêng, nên email thông báo, tác tử đọc qua MCP và mọi trình xem markdown khác\n * in nguyên hai dấu cộng. Một định dạng chỉ sống trong trình soạn này thì không nên tồn tại trong\n * định dạng LƯU.\n */\nexport function createMarkdownSchemaExtensions(options: MarkdownSchemaOptions = {}): AnyExtension[] {\n const runtime = options.runtime ?? { current: DEFAULT_RUNTIME };\n\n return [\n StarterKit.configure({\n paragraph: false,\n codeBlock: false,\n underline: false,\n link: {\n // Đang sửa thì bấm vào liên kết là đặt con trỏ, không phải mở tab mới (thanh định dạng có nút\n // Mở). Chỉ đọc thì Tiptap không chặn cú bấm — thẻ `<a target=\"_blank\">` tự mở như thường.\n openOnClick: false,\n autolink: true,\n linkOnPaste: true,\n // Gõ/dán `[chữ](https://…)` là thành liên kết — người quen markdown gõ vậy theo phản xạ.\n markdownLinks: true,\n defaultProtocol: \"https\",\n HTMLAttributes: { target: \"_blank\", rel: \"noopener noreferrer nofollow\" }\n }\n }),\n ComitorParagraph,\n ComitorCodeBlock.configure({\n lowlight,\n defaultLanguage: null,\n // Tab trong khối mã là thụt lề — đúng điều người viết mã chờ đợi. Thoát khối: ↓ ở dòng cuối,\n // hoặc Enter ba lần (mặc định của Tiptap).\n enableTabIndentation: true,\n tabSize: 2\n }),\n TaskList,\n TaskItem.configure({\n nested: true,\n // Chữ của ĐOẠN ĐẦU, không phải `node.textContent`: mục cha có danh sách con thì `textContent` dính\n // liền chữ của mọi mục con (\"Gửi mã qua emailGiới hạn 5 lần/giờ\") — đã gặp.\n a11y: { checkboxLabel: (node) => runtime.current.taskCheckbox(node.firstChild?.textContent ?? \"\") }\n }),\n TableKit.configure({ table: false, tableCell: false, tableHeader: false }),\n ComitorTable.configure({ resizable: false }),\n ComitorTableCell,\n ComitorTableHeader,\n createImageExtension(runtime),\n createMentionExtension(runtime, options.mentionSuggestion),\n Placeholder.configure({ placeholder: () => runtime.current.placeholder }),\n Markdown.configure({ marked: createMarkedInstance(), indentation: { style: \"space\", size: 2 } })\n ];\n}\n\n/**\n * Ảnh — tách địa chỉ LƯU khỏi địa chỉ HIỂN THỊ, và thoát cú pháp khi ghi markdown.\n *\n * Kho ảnh của module Comitor là bucket RIÊNG TƯ: thứ hiện được là URL đã ký, có hạn. Lưu URL đó\n * vào markdown là lưu một giá trị sẽ chết ngay trong database — nên markdown giữ `src` app trả về\n * (khoá, hoặc một route ổn định của app), còn `resolveImageSrc` đổi nó thành địa chỉ vẽ được lúc vẽ.\n *\n * Ảnh ĐANG tải lên không phải node — xem đầu `image-upload.ts`.\n */\nfunction createImageExtension(runtime: { readonly current: MarkdownEditorRuntime }) {\n return Image.extend({\n renderHTML({ HTMLAttributes }) {\n const src = typeof HTMLAttributes.src === \"string\" ? HTMLAttributes.src : \"\";\n return [\n \"img\",\n { ...this.options.HTMLAttributes, ...HTMLAttributes, src: runtime.current.resolveImageSrc?.(src) ?? src }\n ];\n },\n renderMarkdown(node: JSONContent) {\n return imageMarkdown(String(node.attrs?.src ?? \"\"), node.attrs?.alt, node.attrs?.title);\n }\n }).configure({ allowBase64: false });\n}\n\n/**\n * Nhắc tên — định dạng lưu `[@Tên](mention:<id>)` (lý do ở `markdown-syntax.ts`) và tên SỐNG lúc vẽ.\n */\nfunction createMentionExtension(\n runtime: { readonly current: MarkdownEditorRuntime },\n suggestion: MarkdownSchemaOptions[\"mentionSuggestion\"]\n) {\n const labelOf = (attrs: Record<string, unknown>) => {\n const id = typeof attrs.id === \"string\" ? attrs.id : \"\";\n const stored = typeof attrs.label === \"string\" ? attrs.label : \"\";\n return runtime.current.resolveMentionLabel?.(id) ?? (stored || id);\n };\n\n return Mention.extend({\n markdownTokenName: \"mention\",\n markdownTokenizer: {\n name: \"mention\",\n level: \"inline\",\n start: (source: string) => source.indexOf(\"[@\"),\n tokenize: (source: string) => {\n const parsed = parseMentionMarkdown(source);\n return parsed ? { type: \"mention\", raw: parsed.raw, id: parsed.id, label: parsed.label } : undefined;\n }\n },\n parseMarkdown: (token, helpers) => helpers.createNode(\"mention\", { id: token.id, label: token.label }),\n renderMarkdown: (node: JSONContent) =>\n renderMentionMarkdown(String(node.attrs?.id ?? \"\"), node.attrs?.label as string | null | undefined)\n }).configure({\n renderText: ({ node }) => `@${labelOf(node.attrs)}`,\n // `options.HTMLAttributes` ở đây là bản Tiptap đã trộn sẵn (`data-type`, `data-id`, `data-label`).\n renderHTML: ({ options, node }) => [\"span\", options.HTMLAttributes, `@${labelOf(node.attrs)}`],\n suggestion: { char: \"@\", allow: () => false, ...suggestion }\n });\n}\n\n/** Phím tắt riêng của trình soạn — xem `createEditorKeymap`. */\nexport interface EditorKeymapHandlers {\n /** ⌘/Ctrl+K — mở ô liên kết. Trả `true` khi đã xử lý. */\n onLinkShortcut: () => boolean;\n /** ⌘/Ctrl+Enter ngoài danh sách việc. Trả `false` để phím đi tiếp (xuống dòng cứng của Tiptap). */\n onSubmitShortcut: () => boolean;\n}\n\n/**\n * Phím tắt của RIÊNG gói, ưu tiên cao hơn mặc định của Tiptap.\n *\n * ⌘/Ctrl+Enter: trong danh sách việc thì tick/bỏ tick mục đang đứng (như Linear); ngoài đó thì gửi\n * (`onSubmit`) — đúng phím mà ô bình luận nào cũng dùng. Không có `onSubmit` thì trả phím về cho\n * Tiptap (bản gốc gán nó cho xuống dòng cứng).\n *\n * ⌘/Ctrl+K: ô liên kết. Không đụng bảng lệnh của shell — listener của shell tự bỏ qua phím khi\n * đích là vùng `contenteditable` (`shell-context.tsx`, nhánh `isTyping`).\n */\nexport function createEditorKeymap(handlers: { readonly current: EditorKeymapHandlers }): Extension {\n return Extension.create({\n name: \"comitorEditorKeymap\",\n priority: 1000,\n addKeyboardShortcuts() {\n return {\n \"Mod-k\": () => handlers.current.onLinkShortcut(),\n \"Mod-Enter\": ({ editor }) => {\n const toggled = editor.commands.command(({ tr, state }) => {\n const { $from } = state.selection;\n for (let depth = $from.depth; depth > 0; depth--) {\n const node = $from.node(depth);\n if (node.type.name !== \"taskItem\") continue;\n tr.setNodeMarkup($from.before(depth), undefined, { ...node.attrs, checked: !node.attrs.checked });\n return true;\n }\n return false;\n });\n if (toggled || handlers.current.onSubmitShortcut()) return true;\n // Phím đi tiếp tới Tiptap là xuống dòng cứng — trong ô bảng thì nuốt (xem `ComitorTableCell`).\n return isInTableCell(editor.state.selection.$from);\n },\n \"Shift-Enter\": ({ editor }) => isInTableCell(editor.state.selection.$from)\n };\n }\n });\n}\n\n/** Con trỏ có đang đứng trong một ô bảng (thường hay tiêu đề) không. */\nfunction isInTableCell($pos: ResolvedPos): boolean {\n for (let depth = $pos.depth; depth > 0; depth--) {\n const name = $pos.node(depth).type.name;\n if (name === \"tableCell\" || name === \"tableHeader\") return true;\n }\n return false;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/editor/markdown-extensions.ts"],"sourcesContent":["/**\n * LƯỢC ĐỒ của hai trình soạn — danh sách extension Tiptap, không React, không DOM.\n *\n * Mọi thứ quyết định \"tài liệu này lưu xuống thành chuỗi nào, và đọc lên lại thành tài liệu nào\"\n * nằm ở đây, và CHỈ ở đây: trình soạn dùng nó, cổng `test:editor-markdown` dựng `MarkdownManager`\n * từ chính hàm này trên `dist/` rồi đo vòng tài liệu → markdown → tài liệu. Hai nơi mà dùng hai danh\n * sách là hai lược đồ, và cổng xanh trên một lược đồ không ai dùng.\n *\n * Hai định dạng LƯU, hai lược đồ khác nhau ở đúng những chỗ định dạng ấy biểu diễn được hay không\n * (`createSchemaExtensions`): markdown (`MarkdownEditor`) hẹp hơn — không gạch chân, ô bảng một đoạn —\n * vì thứ gì không ghi được thành markdown thì sẽ mất ở lần lưu; HTML (`RichTextEditor`) giữ được cả hai.\n *\n * Phần GIAO DIỆN (menu `/`, menu `@`, tải ảnh, phím tắt) gắn vào qua tham số — xem\n * `MarkdownSchemaOptions`. Gọi không tham số là ra đúng lược đồ, đủ để đọc/ghi.\n */\n\nimport { type AnyExtension, Extension, InputRule, type JSONContent, mergeAttributes } from \"@tiptap/core\";\nimport { CodeBlockLowlight } from \"@tiptap/extension-code-block-lowlight\";\nimport { Image } from \"@tiptap/extension-image\";\nimport { TaskItem, TaskList } from \"@tiptap/extension-list\";\nimport { Mention, type MentionOptions } from \"@tiptap/extension-mention\";\nimport { Paragraph } from \"@tiptap/extension-paragraph\";\nimport { renderTableToMarkdown, Table, TableCell, TableHeader, TableKit } from \"@tiptap/extension-table\";\nimport { Placeholder } from \"@tiptap/extensions\";\nimport { Markdown } from \"@tiptap/markdown\";\nimport { Fragment, type Node as ProseMirrorNode, type ResolvedPos, Slice } from \"@tiptap/pm/model\";\nimport { Plugin } from \"@tiptap/pm/state\";\nimport { StarterKit } from \"@tiptap/starter-kit\";\nimport { common, createLowlight } from \"lowlight\";\nimport { Marked, type marked } from \"marked\";\nimport type { EditorFormat } from \"./editor-types.js\";\nimport {\n codeFence,\n escapeParagraphMarkdown,\n imageMarkdown,\n parseMentionMarkdown,\n renderMentionMarkdown\n} from \"./markdown-syntax.js\";\n\n/**\n * Giá trị đổi được SAU KHI trình soạn đã dựng. Extension đọc chúng lúc CẦN (lúc vẽ chữ mờ, lúc vẽ một\n * lần nhắc tên…), không chép lúc dựng — vì đổi danh sách extension là dựng lại cả trình soạn: mất\n * lịch sử hoàn tác, mất con trỏ. Component giữ object này trong một ref và ghi đè từng lượt render.\n */\nexport interface MarkdownEditorRuntime {\n placeholder: string;\n taskCheckbox: (text: string) => string;\n /** Đổi `src` ĐÃ LƯU thành địa chỉ HIỂN THỊ được (vd URL đã ký). Markdown vẫn giữ `src` gốc. */\n resolveImageSrc?: (src: string) => string;\n /** Tên SỐNG của một người theo id. `undefined` thì rơi về tên chụp lúc nhắc. */\n resolveMentionLabel?: (id: string) => string | undefined;\n}\n\nconst DEFAULT_RUNTIME: MarkdownEditorRuntime = {\n placeholder: \"\",\n taskCheckbox: (text) => text\n};\n\nexport interface MarkdownSchemaOptions {\n /** Định dạng LƯU — quyết định lược đồ. Mặc định `markdown`. */\n format?: EditorFormat;\n /** Đọc lười — xem `MarkdownEditorRuntime`. Bỏ trống ở nơi chỉ cần đọc/ghi (cổng, máy chủ). */\n runtime?: { readonly current: MarkdownEditorRuntime };\n /**\n * Cấu hình gợi ý `@` của `Mention` (danh sách, cách vẽ, `allow`). Bỏ trống = node `mention` vẫn có\n * trong lược đồ — để tài liệu CÓ nhắc tên vẫn đọc ra đúng ở chế độ chỉ đọc — nhưng gõ `@` không\n * mở gì.\n */\n mentionSuggestion?: Partial<MentionOptions[\"suggestion\"]>;\n}\n\n/*\n * Ngôn ngữ tô màu: bộ `common` của highlight.js (37 ngôn ngữ, ~53KB gzip — đo bằng esbuild trên\n * lowlight 3.3.0). Dựng MỘT lần cho cả module: bảng ngôn ngữ chỉ đọc, chia được giữa các trình soạn.\n */\nconst lowlight = createLowlight(common);\n\n/**\n * Đoạn văn — thêm phép thoát cú pháp khối (ca 1 ở đầu `markdown-syntax.ts`). Mọi hành vi khác giữ\n * nguyên bản gốc, kể cả `&nbsp;` cho đoạn rỗng: đó là cách CỐ Ý của Tiptap để giữ dòng trống người\n * dùng gõ — bỏ nó đi là nội dung \"co\" lại sau mỗi lần lưu.\n */\nconst ComitorParagraph = Paragraph.extend({\n renderMarkdown(node, helpers, context) {\n return escapeParagraphMarkdown(this.parent?.(node, helpers, context) ?? \"\");\n }\n});\n\n/** Khối mã — hàng rào dài hơn mọi dãy backtick bên trong (ca 2 ở đầu `markdown-syntax.ts`). */\nconst ComitorCodeBlock = CodeBlockLowlight.extend({\n renderMarkdown(node: JSONContent) {\n const language = typeof node.attrs?.language === \"string\" ? node.attrs.language : \"\";\n const text = (node.content ?? []).map((child) => child.text ?? \"\").join(\"\");\n const fence = codeFence(text);\n return `${fence}${language}\\n${text}\\n${fence}`;\n }\n});\n\n/** Bảng — bỏ dòng trống bản gốc kẹp hai đầu (xem `trimMarkdown`). */\nconst ComitorTable = Table.extend({\n renderMarkdown(node, helpers) {\n return renderTableToMarkdown(node, helpers).replace(/^\\n+|\\n+$/g, \"\");\n }\n});\n\n/*\n * Ô bảng chứa ĐÚNG MỘT đoạn văn — bản gốc cho `block+` (danh sách, khối mã, nhiều đoạn trong một ô).\n *\n * Ô bảng GFM là một dòng chữ, không chứa khối được. Đã đo trên 3.31.4: ô có hai đoạn ghi thành\n * `dòng 1<br>dòng 2`, ô có danh sách thành `- a<br>- b`, khối mã thành ```` ```<br>code<br>``` ````;\n * mở lại thì `<br>` là CHỮ THƯỜNG, và lần lưu kế tiếp nó thành `&lt;br&gt;` — mỗi lần lưu hỏng thêm\n * một lớp. Thu hẹp lược đồ là cách duy nhất chặn từ gốc: luật nhập (`- `, `# `, ```` ``` ````) và\n * lệnh khối tự thất bại trong ô vì lược đồ không cho, không cần chặn từng chỗ. Xuống dòng cứng trong ô\n * thì `createEditorKeymap` nuốt — `hardBreak` là node NỘI DÒNG nên lược đồ không chặn được nó.\n */\nconst ComitorTableCell = TableCell.extend({ content: \"paragraph\" });\nconst ComitorTableHeader = TableHeader.extend({ content: \"paragraph\" });\n\n/** Mẫu luật nhập của `HorizontalRule` trong StarterKit (3.31.4) — xem `MarkdownTableCellGuard`. */\nconst HORIZONTAL_RULE_INPUT = /^(?:---|—-|___\\s|\\*\\*\\*\\s)$/;\n\n/**\n * Ô bảng MARKDOWN — chặn nốt những đường CHÈN KHỐI mà lược đồ không chặn được.\n *\n * `ComitorTableCell` làm luật nhập BỌC (`- `, `# `, `> `) và lệnh đổi khối tự thất bại trong ô. Nhưng\n * phép THAY của ProseMirror không thất bại: khối không vừa chỗ thì nó TÁCH các node cha cho tới khi\n * vừa — tức tách CẢ BẢNG. Đã đo trên 3.31.4 (harness của `3.6.0`; cả ba có từ `3.5.0`):\n * · gõ `---` trong ô → bảng gãy làm hai, một đường kẻ ở giữa (luật nhập của `HorizontalRule`);\n * · dán một danh sách vào ô → bảng gãy làm hai, danh sách ở giữa;\n * · dán hai dòng chữ vào ô → hai dòng GHI ĐÈ lên ô đang đứng và ô kế bên — chữ cũ của cả hai mất.\n * Ba lớp, từ cụ thể tới chung:\n * 1. dán/thả từ ngoài: mọi khối của đoạn dán gộp thành MỘT dòng (`flattenSliceToLine`) — ô GFM là\n * một dòng chữ;\n * 2. `---` gõ trong ô ở lại là CHỮ — \"---\" là cách người ta hay ghi \"không có\" trong một ô bảng;\n * 3. lưới cuối: giao dịch nào làm SỐ BẢNG tăng khi con trỏ đang ở trong ô thì bị bỏ. Nó bắt mọi\n * đường còn lại (luật nhập ảnh `![a](b)`, kéo thả một khối trong tài liệu, `insertContent` của app),\n * với cái giá là phím hay cú thả vừa rồi không có tác dụng — mất một phím còn hơn gãy một bảng.\n * Hoàn tác/làm lại và thay CẢ tài liệu (`setContent`, `value` đổi từ ngoài) đi thẳng.\n *\n * Ô bảng của `RichTextEditor` chứa được khối nên không cần lớp nào trong ba.\n */\nconst MarkdownTableCellGuard = Extension.create({\n name: \"comitorMarkdownTableCellGuard\",\n // Cao hơn StarterKit (100): mỗi extension một plugin luật nhập, xếp theo `priority` — luật dưới đây\n // phải được hỏi TRƯỚC luật đường kẻ, không thì đường kẻ đã chèn xong.\n priority: 1000,\n addInputRules() {\n return [\n new InputRule({\n find: HORIZONTAL_RULE_INPUT,\n undoable: false,\n handler: ({ state, range, match }) => {\n if (!isInTableCell(state.doc.resolve(range.from))) return null;\n state.tr.insertText(match[0], range.from, range.to);\n }\n })\n ];\n },\n addProseMirrorPlugins() {\n /*\n * Đoạn dán có thật là BẢNG không — phải hỏi ở chuỗi GỐC. `normalizeSiblings` của ProseMirror bọc\n * sẵn các đoạn văn dán vào ô thành Ô BẢNG (để chúng \"vừa\" chỗ) trước khi `transformPasted` thấy\n * chúng, nên nhìn slice thì \"dán hai dòng chữ\" và \"dán hai ô\" là một — và `prosemirror-tables` dán\n * cả hai theo kiểu ô, đè lên ô kế bên. Đã đo: đó chính là ca \"hai dòng ghi đè hai ô\" ở trên.\n */\n let pastedTable = false;\n return [\n new Plugin({\n props: {\n transformPastedHTML: (html) => {\n pastedTable = /<(?:table|tr|td|th)[\\s>/]/i.test(html);\n return html;\n },\n transformPastedText: (text) => {\n pastedTable = false;\n return text;\n },\n transformPasted: (slice, view) => {\n const keepTables = pastedTable;\n pastedTable = false;\n return isInTableCell(view.state.selection.$from) ? flattenSliceToLine(slice, keepTables) : slice;\n }\n },\n filterTransaction: (tr, state) => {\n if (!tr.docChanged || tr.getMeta(\"history$\") || !isInTableCell(state.selection.$from)) return true;\n if (replacesWholeDocument(tr.steps, state.doc.content.size)) return true;\n return countTables(tr.doc) <= countTables(state.doc);\n }\n })\n ];\n }\n});\n\n/**\n * Gộp một đoạn dán thành MỘT dòng nội dòng: các khối chữ nối nhau bằng một dấu cách, xuống dòng (cả\n * `hardBreak` lẫn `\\n` trong khối mã) thành dấu cách, khối không phải chữ (ảnh, đường kẻ) bỏ đi.\n * Nội dung nội dòng sẵn thì để nguyên; `keepTables` (đoạn dán GỐC có bảng) thì đoạn dán mang ô bảng\n * cũng để nguyên — dán theo ô là việc của `prosemirror-tables`.\n * Xuất ra cho cổng `test:editor-markdown`; không nằm trong entry.\n */\nexport function flattenSliceToLine(slice: Slice, keepTables = false): Slice {\n const first = slice.content.firstChild;\n if (!first || first.isInline) return slice;\n if (keepTables) {\n let tabular = false;\n slice.content.descendants((node) => {\n if (node.type.spec.tableRole) tabular = true;\n return !tabular;\n });\n if (tabular) return slice;\n }\n\n const schema = first.type.schema;\n const line: ProseMirrorNode[] = [];\n slice.content.descendants((node) => {\n if (!node.isTextblock) return true;\n if (node.content.size === 0) return false;\n if (line.length > 0) line.push(schema.text(\" \"));\n node.content.forEach((child) => {\n if (child.type.name === \"hardBreak\") line.push(schema.text(\" \"));\n else if (child.isText && child.text?.includes(\"\\n\"))\n line.push(schema.text(child.text.replace(/\\s*\\n\\s*/g, \" \"), child.marks));\n else line.push(child);\n });\n return false;\n });\n return new Slice(Fragment.from(line), 0, 0);\n}\n\nfunction countTables(doc: ProseMirrorNode): number {\n let count = 0;\n doc.descendants((node) => {\n if (node.type.name === \"table\") count++;\n return node.type.name !== \"table\";\n });\n return count;\n}\n\n/** Có bước nào thay TOÀN BỘ tài liệu không — `setContent` làm đúng một bước như vậy. */\nfunction replacesWholeDocument(steps: readonly unknown[], size: number): boolean {\n return steps.some((step) => {\n const { from, to } = step as { from?: unknown; to?: unknown };\n return from === 0 && to === size;\n });\n}\n\n/**\n * Bảng chữ markdown RIÊNG cho từng trình soạn.\n *\n * ⚠ Không truyền thì `@tiptap/markdown` dùng `marked` TOÀN CỤC, và mỗi lần trình soạn mount nó gọi\n * `marked.use()` đăng ký lại bộ tách từ (bảng, nhắc tên…) — không bao giờ gỡ. Đã đo trên 3.31.4:\n * mỗi `MarkdownManager` thêm 4 bộ tách; sau 200 lần dựng, parse cùng một tài liệu 200 đoạn chậm từ\n * 18,6ms lên 282ms, và còn tăng tiếp. Trong một SPA, 200 lần là một buổi làm việc mở việc này sang\n * việc khác. Một bản `Marked` mỗi trình soạn: 4 bộ tách, 5,7ms, mất cùng trình soạn.\n *\n * Ép kiểu vì option khai `typeof marked` (hàm `marked` toàn cục, gọi được) mà thứ đúng là một bản\n * `Marked`: `MarkdownManager` chỉ dùng `.use`, `.Lexer`, `.defaults`, `.lexer`, `.setOptions` — bản\n * `Marked` có đủ cả năm, chỉ thiếu chữ ký gọi-như-hàm mà không chỗ nào gọi tới.\n */\nfunction createMarkedInstance(): typeof marked {\n return new Marked() as unknown as typeof marked;\n}\n\n/**\n * Danh sách extension tạo nên lược đồ của trình soạn, theo định dạng LƯU.\n *\n * Markdown (`format: \"markdown\"`, mặc định) hẹp hơn HTML ở hai chỗ, cả hai vì cùng một lý do — thứ gì\n * không ghi được thành markdown thì MẤT ở lần lưu, mà người dùng không được báo:\n * · `underline` tắt: markdown không có cú pháp gạch chân. `@tiptap/markdown` ghi nó thành\n * `++chữ++` — cú pháp riêng, nên email thông báo, tác tử đọc qua MCP và mọi trình xem markdown\n * khác in nguyên hai dấu cộng;\n * · ô bảng chỉ chứa MỘT đoạn văn (`ComitorTableCell`).\n * Hai bản vá markdown (`ComitorParagraph`, `Markdown` với `Marked` riêng) cũng chỉ nạp ở định dạng đó.\n *\n * HTML (`format: \"html\"`) giữ cả gạch chân lẫn ô bảng nhiều khối — `<u>`, `<td><ul>…</ul></td>` là HTML\n * hợp lệ và đọc lên lại đúng.\n */\nexport function createSchemaExtensions(options: MarkdownSchemaOptions = {}): AnyExtension[] {\n const runtime = options.runtime ?? { current: DEFAULT_RUNTIME };\n const markdown = (options.format ?? \"markdown\") === \"markdown\";\n\n return [\n StarterKit.configure({\n paragraph: false,\n codeBlock: false,\n ...(markdown ? { underline: false as const } : {}),\n link: {\n // Đang sửa thì bấm vào liên kết là đặt con trỏ, không phải mở tab mới (thanh định dạng có nút\n // Mở). Chỉ đọc thì Tiptap không chặn cú bấm — thẻ `<a target=\"_blank\">` tự mở như thường.\n openOnClick: false,\n autolink: true,\n linkOnPaste: true,\n // Gõ/dán `[chữ](https://…)` là thành liên kết — người quen markdown gõ vậy theo phản xạ.\n markdownLinks: true,\n defaultProtocol: \"https\",\n HTMLAttributes: { target: \"_blank\", rel: \"noopener noreferrer nofollow\" }\n }\n }),\n markdown ? ComitorParagraph : Paragraph,\n ComitorCodeBlock.configure({\n lowlight,\n defaultLanguage: null,\n // Tab trong khối mã là thụt lề — đúng điều người viết mã chờ đợi. Thoát khối: ↓ ở dòng cuối,\n // hoặc Enter ba lần (mặc định của Tiptap).\n enableTabIndentation: true,\n tabSize: 2\n }),\n TaskList,\n TaskItem.configure({\n nested: true,\n // Chữ của ĐOẠN ĐẦU, không phải `node.textContent`: mục cha có danh sách con thì `textContent` dính\n // liền chữ của mọi mục con (\"Gửi mã qua emailGiới hạn 5 lần/giờ\") — đã gặp.\n a11y: { checkboxLabel: (node) => runtime.current.taskCheckbox(node.firstChild?.textContent ?? \"\") }\n }),\n ...(markdown\n ? [\n TableKit.configure({ table: false, tableCell: false, tableHeader: false }),\n ComitorTable.configure({ resizable: false }),\n ComitorTableCell,\n ComitorTableHeader,\n MarkdownTableCellGuard\n ]\n : [TableKit.configure({ table: { resizable: false } })]),\n createImageExtension(runtime),\n createMentionExtension(runtime, options.mentionSuggestion),\n Placeholder.configure({ placeholder: () => runtime.current.placeholder }),\n ...(markdown\n ? [Markdown.configure({ marked: createMarkedInstance(), indentation: { style: \"space\", size: 2 } })]\n : [])\n ];\n}\n\n/** Lược đồ của `MarkdownEditor` — tên có từ `3.5.0`, cổng `test:editor-markdown` gọi thẳng nó. */\nexport function createMarkdownSchemaExtensions(options: Omit<MarkdownSchemaOptions, \"format\"> = {}): AnyExtension[] {\n return createSchemaExtensions({ ...options, format: \"markdown\" });\n}\n\n/**\n * Ảnh — tách địa chỉ LƯU khỏi địa chỉ HIỂN THỊ, và thoát cú pháp khi ghi markdown.\n *\n * Kho ảnh của module Comitor là bucket RIÊNG TƯ: thứ hiện được là URL đã ký, có hạn. Lưu URL đó vào\n * nội dung là lưu một giá trị sẽ chết ngay trong database — nên nội dung giữ `src` app trả về (khoá,\n * hoặc một route ổn định của app), còn `resolveImageSrc` đổi nó thành địa chỉ vẽ được lúc vẽ.\n *\n * ⚠ Đổi địa chỉ ở NODE VIEW, không ở `renderHTML`. `renderHTML` vừa vẽ ảnh trong trình soạn vừa là\n * thứ `editor.getHTML()` dùng để GHI — đổi `src` ở đó (cách của `3.5.0`, khi giá trị còn là markdown)\n * là `RichTextEditor` ghi URL đã ký vào HTML lưu xuống. Node view chỉ là DOM của màn hình; HTML lưu\n * vẫn đi `renderHTML` gốc với `src` gốc.\n *\n * Ảnh ĐANG tải lên không phải node — xem đầu `image-upload.ts`.\n */\nfunction createImageExtension(runtime: { readonly current: MarkdownEditorRuntime }) {\n return Image.extend({\n addNodeView() {\n return ({ node, HTMLAttributes }) => {\n const image = document.createElement(\"img\");\n setAttributes(image, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), [\"src\"]);\n const src = typeof node.attrs.src === \"string\" ? node.attrs.src : \"\";\n // `src=\"\"` thì trình duyệt tải lại CHÍNH trang làm ảnh — thiếu địa chỉ thì bỏ hẳn thuộc tính.\n if (src) image.setAttribute(\"src\", runtime.current.resolveImageSrc?.(src) ?? src);\n return { dom: image, update: (updated) => sameNode(updated, node) };\n };\n },\n renderMarkdown(node: JSONContent) {\n return imageMarkdown(String(node.attrs?.src ?? \"\"), node.attrs?.alt, node.attrs?.title);\n }\n }).configure({ allowBase64: false });\n}\n\n/**\n * Nhắc tên — định dạng lưu `[@Tên](mention:<id>)` (lý do ở `markdown-syntax.ts`) và tên SỐNG lúc vẽ.\n *\n * Tên sống chỉ đi vào thứ NGƯỜI XEM thấy: node view và `renderText` (chữ khi chép ra ngoài).\n * `renderHTML` — thứ `getHTML()` ghi xuống — giữ TÊN CHỤP lúc nhắc, cùng luật với markdown: nội dung\n * lưu không tự đổi theo danh bạ chỉ vì có người mở và lưu lại.\n */\nfunction createMentionExtension(\n runtime: { readonly current: MarkdownEditorRuntime },\n suggestion: MarkdownSchemaOptions[\"mentionSuggestion\"]\n) {\n const storedLabelOf = (attrs: Record<string, unknown>) => {\n const stored = typeof attrs.label === \"string\" ? attrs.label : \"\";\n return stored || (typeof attrs.id === \"string\" ? attrs.id : \"\");\n };\n const labelOf = (attrs: Record<string, unknown>) =>\n runtime.current.resolveMentionLabel?.(typeof attrs.id === \"string\" ? attrs.id : \"\") ?? storedLabelOf(attrs);\n\n return Mention.extend({\n markdownTokenName: \"mention\",\n markdownTokenizer: {\n name: \"mention\",\n level: \"inline\",\n start: (source: string) => source.indexOf(\"[@\"),\n tokenize: (source: string) => {\n const parsed = parseMentionMarkdown(source);\n return parsed ? { type: \"mention\", raw: parsed.raw, id: parsed.id, label: parsed.label } : undefined;\n }\n },\n parseMarkdown: (token, helpers) => helpers.createNode(\"mention\", { id: token.id, label: token.label }),\n renderMarkdown: (node: JSONContent) =>\n renderMentionMarkdown(String(node.attrs?.id ?? \"\"), node.attrs?.label as string | null | undefined),\n addNodeView() {\n return ({ node, HTMLAttributes }) => {\n const dom = document.createElement(\"span\");\n setAttributes(dom, mergeAttributes({ \"data-type\": this.name }, this.options.HTMLAttributes, HTMLAttributes));\n dom.textContent = `@${labelOf(node.attrs)}`;\n return { dom, update: (updated) => sameNode(updated, node) };\n };\n }\n }).configure({\n renderText: ({ node }) => `@${labelOf(node.attrs)}`,\n // `options.HTMLAttributes` ở đây là bản Tiptap đã trộn sẵn (`data-type`, `data-id`, `data-label`).\n renderHTML: ({ options, node }) => [\"span\", options.HTMLAttributes, `@${storedLabelOf(node.attrs)}`],\n suggestion: { char: \"@\", allow: () => false, ...suggestion }\n });\n}\n\n/** Phím tắt riêng của trình soạn — xem `createEditorKeymap`. */\nexport interface EditorKeymapHandlers {\n /** ⌘/Ctrl+K — mở ô liên kết. Trả `true` khi đã xử lý. */\n onLinkShortcut: () => boolean;\n /** ⌘/Ctrl+Enter ngoài danh sách việc. Trả `false` để phím đi tiếp (xuống dòng cứng của Tiptap). */\n onSubmitShortcut: () => boolean;\n}\n\n/**\n * Phím tắt của RIÊNG gói, ưu tiên cao hơn mặc định của Tiptap.\n *\n * ⌘/Ctrl+Enter: trong danh sách việc thì tick/bỏ tick mục đang đứng (như Linear); ngoài đó thì gửi\n * (`onSubmit`) — đúng phím mà ô bình luận nào cũng dùng. Không có `onSubmit` thì trả phím về cho\n * Tiptap (bản gốc gán nó cho xuống dòng cứng).\n *\n * ⌘/Ctrl+K: ô liên kết. Không đụng bảng lệnh của shell — listener của shell tự bỏ qua phím khi\n * đích là vùng `contenteditable` (`shell-context.tsx`, nhánh `isTyping`).\n */\nexport function createEditorKeymap(\n handlers: { readonly current: EditorKeymapHandlers },\n format: EditorFormat = \"markdown\"\n): Extension {\n // Chỉ ô bảng MARKDOWN cấm xuống dòng cứng (`ComitorTableCell`); ô bảng HTML chứa được cả khối.\n const swallowInCell = (editor: { state: { selection: { $from: ResolvedPos } } }) =>\n format === \"markdown\" && isInTableCell(editor.state.selection.$from);\n return Extension.create({\n name: \"comitorEditorKeymap\",\n priority: 1000,\n addKeyboardShortcuts() {\n return {\n \"Mod-k\": () => handlers.current.onLinkShortcut(),\n \"Mod-Enter\": ({ editor }) => {\n const toggled = editor.commands.command(({ tr, state }) => {\n const { $from } = state.selection;\n for (let depth = $from.depth; depth > 0; depth--) {\n const node = $from.node(depth);\n if (node.type.name !== \"taskItem\") continue;\n tr.setNodeMarkup($from.before(depth), undefined, { ...node.attrs, checked: !node.attrs.checked });\n return true;\n }\n return false;\n });\n if (toggled || handlers.current.onSubmitShortcut()) return true;\n // Phím đi tiếp tới Tiptap là xuống dòng cứng — trong ô bảng markdown thì nuốt (xem `ComitorTableCell`).\n return swallowInCell(editor);\n },\n \"Shift-Enter\": ({ editor }) => swallowInCell(editor)\n };\n }\n });\n}\n\n/** Con trỏ có đang đứng trong một ô bảng (thường hay tiêu đề) không. */\nfunction isInTableCell($pos: ResolvedPos): boolean {\n for (let depth = $pos.depth; depth > 0; depth--) {\n const name = $pos.node(depth).type.name;\n if (name === \"tableCell\" || name === \"tableHeader\") return true;\n }\n return false;\n}\n\n/** Ghi thuộc tính lên phần tử của node view — bỏ giá trị rỗng, như `DOMSerializer` làm với `renderHTML`. */\nfunction setAttributes(element: HTMLElement, attributes: Record<string, unknown>, skip: readonly string[] = []): void {\n for (const [key, value] of Object.entries(attributes)) {\n if (skip.includes(key) || value === null || value === undefined || value === false) continue;\n element.setAttribute(key, String(value));\n }\n}\n\n/**\n * Node view của nút LÁ (ảnh, nhắc tên): thuộc tính không đổi thì giữ DOM; đổi thì trả `false` để\n * ProseMirror dựng lại — rẻ, và không có trạng thái nào trong DOM đáng giữ.\n */\nfunction sameNode(updated: ProseMirrorNode, current: ProseMirrorNode): boolean {\n if (updated.type !== current.type) return false;\n return Object.keys(current.attrs).every((key) => updated.attrs[key] === current.attrs[key]);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,48 @@
1
+ import * as React from 'react';
2
+ import { g as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BMST7-VR.js';
3
+ import '@tiptap/core';
4
+ import './markdown-editor-constants.js';
5
+
6
+ /**
7
+ * RichTextEditor — trình soạn văn bản giàu định dạng LƯU bằng HTML, cho nội dung xuất bản (CMS):
8
+ * bài viết, trang tin, mô tả sản phẩm, mẫu email — nơi nội dung được hiển thị lại ngoài trình soạn và
9
+ * người soạn chờ đợi một thanh công cụ.
10
+ *
11
+ * Cùng lõi với `MarkdownEditor` (`editor-core.tsx`): menu `/`, nhắc tên `@`, ảnh, liên kết, phím tắt và
12
+ * cú pháp gõ tắt (`## `, `- `…) y hệt. Khác ở ba chỗ:
13
+ * · giá trị là CHUỖI HTML (`editor.getHTML()`), tài liệu rỗng là `""`;
14
+ * · lược đồ RỘNG hơn — gạch chân, ô bảng chứa nhiều khối (danh sách, ảnh, nhiều đoạn): HTML giữ được
15
+ * cả hai, markdown thì không (lý do ở `createSchemaExtensions`);
16
+ * · thanh công cụ mặc định là `fixed` — thanh dính đầu khung, kiểu CMS.
17
+ *
18
+ * ⚠ HTML ĐI VÀO đi qua lược đồ, và lược đồ là một danh sách cho phép: thẻ và thuộc tính nó không biết
19
+ * (`<script>`, `style`, `onerror`, `<iframe>`) rơi mất khi nạp. Nhưng HTML ĐÃ LƯU là dữ liệu người dùng
20
+ * — ai gọi thẳng API của app thì ghi được bất cứ chuỗi nào. Hiển thị lại bằng
21
+ * `<RichTextEditor editable={false} value={html} />` (đi qua cùng lược đồ), đừng `dangerouslySetInnerHTML`
22
+ * khi chưa có bộ lọc phía máy chủ. Chi tiết ở docs/editor.md.
23
+ */
24
+
25
+ /** Điều khiển trình soạn từ ngoài — lấy qua prop `ref`. */
26
+ interface RichTextEditorHandle extends EditorHandleBase {
27
+ /** HTML hiện tại — cùng chuỗi mà `onChange` nhận (`""` khi rỗng). */
28
+ getHTML: () => string;
29
+ /** Thay toàn bộ nội dung bằng HTML (đi qua lược đồ — xem đầu file). Có gọi `onChange`. */
30
+ setHTML: (html: string) => void;
31
+ }
32
+ interface RichTextEditorProps extends EditorBaseProps {
33
+ /** HTML — điều khiển từ ngoài. Đổi giá trị KHÁC thứ trình soạn vừa phát là nạp lại nội dung. */
34
+ value?: string;
35
+ /** HTML ban đầu khi KHÔNG điều khiển. */
36
+ defaultValue?: string;
37
+ /** Mỗi lần nội dung đổi, nhận HTML của tài liệu — `""` khi tài liệu rỗng. */
38
+ onChange?: (html: string) => void;
39
+ /**
40
+ * `fixed` (mặc định) — thanh dính đầu khung, mọi thao tác luôn thấy. `bubble` — kiểu Linear, định
41
+ * dạng hiện ở chỗ bôi chọn. Xem `EditorToolbarMode`.
42
+ */
43
+ toolbar?: EditorToolbarMode;
44
+ ref?: React.Ref<RichTextEditorHandle>;
45
+ }
46
+ declare function RichTextEditor({ toolbar, ref, ...props }: RichTextEditorProps): React.JSX.Element;
47
+
48
+ export { RichTextEditor, type RichTextEditorHandle, type RichTextEditorProps };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { EditorCore } from "./editor-core.js";
4
+ function createRichTextHandle(core) {
5
+ return {
6
+ focus: core.focus,
7
+ getHTML: core.getValue,
8
+ setHTML: core.setValue,
9
+ clear: core.clear,
10
+ isEmpty: core.isEmpty,
11
+ isUploading: core.isUploading,
12
+ get editor() {
13
+ return core.editor;
14
+ }
15
+ };
16
+ }
17
+ function RichTextEditor({ toolbar = "fixed", ref, ...props }) {
18
+ return /* @__PURE__ */ jsx(
19
+ EditorCore,
20
+ {
21
+ ...props,
22
+ format: "html",
23
+ toolbar,
24
+ slot: "rich-text-editor",
25
+ handleRef: ref,
26
+ createHandle: createRichTextHandle
27
+ }
28
+ );
29
+ }
30
+ export {
31
+ RichTextEditor
32
+ };
33
+ //# sourceMappingURL=rich-text-editor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/rich-text-editor.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * RichTextEditor — trình soạn văn bản giàu định dạng LƯU bằng HTML, cho nội dung xuất bản (CMS):\n * bài viết, trang tin, mô tả sản phẩm, mẫu email — nơi nội dung được hiển thị lại ngoài trình soạn và\n * người soạn chờ đợi một thanh công cụ.\n *\n * Cùng lõi với `MarkdownEditor` (`editor-core.tsx`): menu `/`, nhắc tên `@`, ảnh, liên kết, phím tắt và\n * cú pháp gõ tắt (`## `, `- `…) y hệt. Khác ở ba chỗ:\n * · giá trị là CHUỖI HTML (`editor.getHTML()`), tài liệu rỗng là `\"\"`;\n * · lược đồ RỘNG hơn — gạch chân, ô bảng chứa nhiều khối (danh sách, ảnh, nhiều đoạn): HTML giữ được\n * cả hai, markdown thì không (lý do ở `createSchemaExtensions`);\n * · thanh công cụ mặc định là `fixed` — thanh dính đầu khung, kiểu CMS.\n *\n * ⚠ HTML ĐI VÀO đi qua lược đồ, và lược đồ là một danh sách cho phép: thẻ và thuộc tính nó không biết\n * (`<script>`, `style`, `onerror`, `<iframe>`) rơi mất khi nạp. Nhưng HTML ĐÃ LƯU là dữ liệu người dùng\n * — ai gọi thẳng API của app thì ghi được bất cứ chuỗi nào. Hiển thị lại bằng\n * `<RichTextEditor editable={false} value={html} />` (đi qua cùng lược đồ), đừng `dangerouslySetInnerHTML`\n * khi chưa có bộ lọc phía máy chủ. Chi tiết ở docs/editor.md.\n */\n\nimport type * as React from \"react\";\nimport { EditorCore, type EditorCoreHandle } from \"./editor-core.js\";\nimport type { EditorBaseProps, EditorHandleBase, EditorToolbarMode } from \"./editor-types.js\";\n\n/** Điều khiển trình soạn từ ngoài — lấy qua prop `ref`. */\nexport interface RichTextEditorHandle extends EditorHandleBase {\n /** HTML hiện tại — cùng chuỗi mà `onChange` nhận (`\"\"` khi rỗng). */\n getHTML: () => string;\n /** Thay toàn bộ nội dung bằng HTML (đi qua lược đồ — xem đầu file). Có gọi `onChange`. */\n setHTML: (html: string) => void;\n}\n\nexport interface RichTextEditorProps extends EditorBaseProps {\n /** HTML — điều khiển từ ngoài. Đổi giá trị KHÁC thứ trình soạn vừa phát là nạp lại nội dung. */\n value?: string;\n /** HTML ban đầu khi KHÔNG điều khiển. */\n defaultValue?: string;\n /** Mỗi lần nội dung đổi, nhận HTML của tài liệu — `\"\"` khi tài liệu rỗng. */\n onChange?: (html: string) => void;\n /**\n * `fixed` (mặc định) — thanh dính đầu khung, mọi thao tác luôn thấy. `bubble` — kiểu Linear, định\n * dạng hiện ở chỗ bôi chọn. Xem `EditorToolbarMode`.\n */\n toolbar?: EditorToolbarMode;\n ref?: React.Ref<RichTextEditorHandle>;\n}\n\nfunction createRichTextHandle(core: EditorCoreHandle): RichTextEditorHandle {\n return {\n focus: core.focus,\n getHTML: core.getValue,\n setHTML: core.setValue,\n clear: core.clear,\n isEmpty: core.isEmpty,\n isUploading: core.isUploading,\n get editor() {\n return core.editor;\n }\n };\n}\n\nexport function RichTextEditor({ toolbar = \"fixed\", ref, ...props }: RichTextEditorProps) {\n return (\n <EditorCore\n {...props}\n format=\"html\"\n toolbar={toolbar}\n slot=\"rich-text-editor\"\n handleRef={ref}\n createHandle={createRichTextHandle}\n />\n );\n}\n"],"mappings":";AAgEI;AA1CJ,SAAS,kBAAyC;AA0BlD,SAAS,qBAAqB,MAA8C;AAC1E,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,SAAS,KAAK;AAAA,IACd,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,aAAa,KAAK;AAAA,IAClB,IAAI,SAAS;AACX,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAEO,SAAS,eAAe,EAAE,UAAU,SAAS,KAAK,GAAG,MAAM,GAAwB;AACxF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,QAAO;AAAA,MACP;AAAA,MACA,MAAK;AAAA,MACL,WAAW;AAAA,MACX,cAAc;AAAA;AAAA,EAChB;AAEJ;","names":[]}
@@ -2,6 +2,7 @@ import { Editor, Range, Extension } from '@tiptap/core';
2
2
  import { PluginKey } from '@tiptap/pm/state';
3
3
  import { SuggestionOptions } from '@tiptap/suggestion';
4
4
  import { LucideIcon } from 'lucide-react';
5
+ import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
5
6
  import { MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
6
7
 
7
8
  /**
@@ -14,6 +15,8 @@ import { MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-c
14
15
 
15
16
  /** Thứ menu cần mà chỉ component biết — vd mở hộp chọn tệp. */
16
17
  interface SlashCommandContext {
18
+ /** Định dạng lưu — quyết định khối nào chèn được vào ô bảng. Mặc định `markdown`. */
19
+ format?: EditorFormat;
17
20
  /** Có = app đã truyền `onImageUpload`, khối "Ảnh" hiện ra. */
18
21
  pickImage?: () => void;
19
22
  }
@@ -27,7 +30,7 @@ interface SlashCommand {
27
30
  hint?: string;
28
31
  /** Bí danh máy, cách nhau bằng dấu cách — luôn tìm được, ở mọi ngôn ngữ. */
29
32
  aliases: string;
30
- /** Mục có hiện ở vị trí con trỏ này không. Bỏ trống = hiện khi KHÔNG ở trong bảng. */
33
+ /** Mục có hiện ở vị trí con trỏ này không. Bỏ trống = hiện ở mọi chỗ chèn được KHỐI (`blockAllowed`). */
31
34
  when?: (editor: Editor, context: SlashCommandContext) => boolean;
32
35
  run: (editor: Editor, range: Range, context: SlashCommandContext) => void;
33
36
  }
@@ -22,6 +22,7 @@ import {
22
22
  } from "lucide-react";
23
23
  import { matchesSearch } from "../lib/text-search.js";
24
24
  const inTable = (editor) => editor.isActive("table");
25
+ const blockAllowed = (editor, context) => context.format === "html" || !inTable(editor);
25
26
  const SLASH_COMMANDS = [
26
27
  {
27
28
  id: "text",
@@ -89,6 +90,8 @@ const SLASH_COMMANDS = [
89
90
  id: "table",
90
91
  icon: Table,
91
92
  aliases: "table spreadsheet",
93
+ // Không lồng bảng trong bảng — GFM không có, và hiếm khi là điều người soạn muốn.
94
+ when: (editor) => !inTable(editor),
92
95
  run: (editor, range) => editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
93
96
  },
94
97
  {
@@ -102,7 +105,7 @@ const SLASH_COMMANDS = [
102
105
  id: "image",
103
106
  icon: ImagePlus,
104
107
  aliases: "img image picture upload",
105
- when: (editor, context) => !inTable(editor) && Boolean(context.pickImage),
108
+ when: (editor, context) => blockAllowed(editor, context) && Boolean(context.pickImage),
106
109
  run: (editor, range, context) => {
107
110
  editor.chain().focus().deleteRange(range).run();
108
111
  context.pickImage?.();
@@ -146,7 +149,9 @@ const SLASH_COMMANDS = [
146
149
  ];
147
150
  function filterSlashCommands(editor, query, labels, context) {
148
151
  const words = query.trim().split(/\s+/).filter(Boolean);
149
- return SLASH_COMMANDS.filter((command) => command.when ? command.when(editor, context) : !inTable(editor)).filter((command) => {
152
+ return SLASH_COMMANDS.filter(
153
+ (command) => command.when ? command.when(editor, context) : blockAllowed(editor, context)
154
+ ).filter((command) => {
150
155
  const haystack = `${labels[command.id]} ${labels.slashKeywords[command.id]} ${command.aliases}`;
151
156
  return words.every((word) => matchesSearch(haystack, word));
152
157
  }).map((command) => ({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/slash-commands.ts"],"sourcesContent":["/**\n * Menu `/` — danh sách khối chèn được, và extension Tiptap mở nó.\n *\n * Không `\"use client\"`: bảng dưới đây là dữ liệu (icon là component, `run` là hàm) — file thuần.\n * Chữ hiển thị KHÔNG nằm ở đây: tên khối và từ đồng nghĩa đi qua namespace `markdownEditor`; ở đây\n * chỉ có bí danh MÁY tiếng Anh (`h1`, `ul`, `todo`…), thứ người dùng gõ như nhau ở mọi ngôn ngữ.\n */\n\nimport { type Editor, Extension, type Range } from \"@tiptap/core\";\nimport { PluginKey } from \"@tiptap/pm/state\";\nimport { Suggestion, type SuggestionOptions } from \"@tiptap/suggestion\";\nimport {\n BetweenHorizontalEnd,\n BetweenVerticalEnd,\n Columns3,\n Heading1,\n Heading2,\n Heading3,\n ImagePlus,\n List,\n ListChecks,\n ListOrdered,\n type LucideIcon,\n Minus,\n Pilcrow,\n Rows3,\n SquareCode,\n Table,\n TextQuote,\n Trash2\n} from \"lucide-react\";\nimport { matchesSearch } from \"../lib/text-search.js\";\nimport type { MarkdownEditorBlockId, MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\n\n/** Thứ menu cần mà chỉ component biết — vd mở hộp chọn tệp. */\nexport interface SlashCommandContext {\n /** Có = app đã truyền `onImageUpload`, khối \"Ảnh\" hiện ra. */\n pickImage?: () => void;\n}\n\nexport interface SlashCommand {\n id: MarkdownEditorBlockId;\n icon: LucideIcon;\n /**\n * Cú pháp gõ tắt, hiện mờ bên phải mục — kiểu Linear. Menu `/` vì vậy cũng là chỗ DẠY cú pháp:\n * lần sau người dùng gõ thẳng `## ` mà không cần mở menu.\n */\n hint?: string;\n /** Bí danh máy, cách nhau bằng dấu cách — luôn tìm được, ở mọi ngôn ngữ. */\n aliases: string;\n /** Mục có hiện ở vị trí con trỏ này không. Bỏ trống = hiện khi KHÔNG ở trong bảng. */\n when?: (editor: Editor, context: SlashCommandContext) => boolean;\n run: (editor: Editor, range: Range, context: SlashCommandContext) => void;\n}\n\n/** Mục hiện trong menu: lệnh + chữ đã dịch, ngữ cảnh đã buộc sẵn. */\nexport interface SlashMenuItem {\n id: MarkdownEditorBlockId;\n icon: LucideIcon;\n hint?: string;\n label: string;\n execute: (editor: Editor, range: Range) => void;\n}\n\nconst inTable = (editor: Editor) => editor.isActive(\"table\");\n\n/*\n * Thứ tự theo tần suất dùng của một mô tả công việc (văn bản → tiêu đề → danh sách → khối), không\n * theo bảng chữ cái: menu lọc theo chữ gõ, nên thứ tự chỉ có nghĩa ở lượt mở ĐẦU, khi chưa gõ gì.\n *\n * Trong bảng chỉ còn thao tác bảng: ô bảng chỉ chứa một đoạn văn (xem `ComitorTableCell`), nên mọi\n * khối khác có hiện cũng không chèn được.\n */\nexport const SLASH_COMMANDS: readonly SlashCommand[] = [\n {\n id: \"text\",\n icon: Pilcrow,\n aliases: \"p text paragraph\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setParagraph().run()\n },\n {\n id: \"heading1\",\n icon: Heading1,\n hint: \"#\",\n aliases: \"h1 heading\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 1 }).run()\n },\n {\n id: \"heading2\",\n icon: Heading2,\n hint: \"##\",\n aliases: \"h2 heading\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 2 }).run()\n },\n {\n id: \"heading3\",\n icon: Heading3,\n hint: \"###\",\n aliases: \"h3 heading\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 3 }).run()\n },\n {\n id: \"bulletList\",\n icon: List,\n hint: \"-\",\n aliases: \"ul bullet list\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleBulletList().run()\n },\n {\n id: \"orderedList\",\n icon: ListOrdered,\n hint: \"1.\",\n aliases: \"ol ordered number list\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleOrderedList().run()\n },\n {\n id: \"taskList\",\n icon: ListChecks,\n hint: \"[]\",\n aliases: \"todo task checklist checkbox\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleTaskList().run()\n },\n {\n id: \"blockquote\",\n icon: TextQuote,\n hint: \">\",\n aliases: \"quote blockquote\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleBlockquote().run()\n },\n {\n id: \"codeBlock\",\n icon: SquareCode,\n hint: \"```\",\n aliases: \"code pre codeblock\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setCodeBlock().run()\n },\n {\n id: \"table\",\n icon: Table,\n aliases: \"table spreadsheet\",\n run: (editor, range) =>\n editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()\n },\n {\n id: \"divider\",\n icon: Minus,\n hint: \"---\",\n aliases: \"hr divider rule separator\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHorizontalRule().run()\n },\n {\n id: \"image\",\n icon: ImagePlus,\n aliases: \"img image picture upload\",\n when: (editor, context) => !inTable(editor) && Boolean(context.pickImage),\n run: (editor, range, context) => {\n editor.chain().focus().deleteRange(range).run();\n context.pickImage?.();\n }\n },\n {\n id: \"tableAddRow\",\n icon: BetweenHorizontalEnd,\n aliases: \"row add insert\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).addRowAfter().run()\n },\n {\n id: \"tableAddColumn\",\n icon: BetweenVerticalEnd,\n aliases: \"column col add insert\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).addColumnAfter().run()\n },\n {\n id: \"tableDeleteRow\",\n icon: Rows3,\n aliases: \"row delete remove\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).deleteRow().run()\n },\n {\n id: \"tableDeleteColumn\",\n icon: Columns3,\n aliases: \"column col delete remove\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).deleteColumn().run()\n },\n {\n id: \"tableDelete\",\n icon: Trash2,\n aliases: \"table delete remove\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).deleteTable().run()\n }\n];\n\n/**\n * Các mục hiện được ở vị trí con trỏ, khớp với chữ đã gõ.\n *\n * Khớp = MỌI từ của chữ gõ nằm trong (tên đã dịch + từ đồng nghĩa đã dịch + bí danh máy) — không\n * dấu, không phân biệt hoa thường, đúng luật `matchesSearch` của `Combobox`: \"/tieu\" ra \"Tiêu đề\",\n * \"/h2\" ra \"Tiêu đề 2\" ở mọi ngôn ngữ.\n */\nexport function filterSlashCommands(\n editor: Editor,\n query: string,\n labels: MarkdownEditorLabels,\n context: SlashCommandContext\n): SlashMenuItem[] {\n const words = query.trim().split(/\\s+/).filter(Boolean);\n return SLASH_COMMANDS.filter((command) => (command.when ? command.when(editor, context) : !inTable(editor)))\n .filter((command) => {\n const haystack = `${labels[command.id]} ${labels.slashKeywords[command.id]} ${command.aliases}`;\n return words.every((word) => matchesSearch(haystack, word));\n })\n .map((command) => ({\n id: command.id,\n icon: command.icon,\n hint: command.hint,\n label: labels[command.id],\n execute: (target, range) => command.run(target, range, context)\n }));\n}\n\nexport const SLASH_PLUGIN_KEY = new PluginKey(\"comitorSlashMenu\");\n\n/**\n * Extension mở menu `/`. Phần GIAO DIỆN (danh sách, bàn phím) đến qua `render` — xem\n * `SuggestionStore`.\n *\n * `/` chỉ kích hoạt ở đầu dòng hoặc sau dấu cách (`allowedPrefixes` mặc định của Tiptap), nên gõ\n * đường dẫn `a/b` hay ngày `02/10` không bật menu. Không bật trong khối mã và mã nội dòng: ở đó `/`\n * là chữ của mã.\n */\nexport function createSlashCommandExtension(\n options: Pick<SuggestionOptions<SlashMenuItem, SlashMenuItem>, \"items\" | \"render\">\n): Extension {\n return Extension.create({\n name: \"comitorSlashMenu\",\n addProseMirrorPlugins() {\n return [\n Suggestion<SlashMenuItem, SlashMenuItem>({\n editor: this.editor,\n pluginKey: SLASH_PLUGIN_KEY,\n char: \"/\",\n allowSpaces: false,\n items: options.items,\n render: options.render,\n command: ({ editor, range, props }) => props.execute(editor, range),\n allow: ({ editor }) => editor.isEditable && !editor.isActive(\"codeBlock\") && !editor.isActive(\"code\")\n })\n ];\n }\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/editor/slash-commands.ts"],"sourcesContent":["/**\n * Menu `/` — danh sách khối chèn được, và extension Tiptap mở nó.\n *\n * Không `\"use client\"`: bảng dưới đây là dữ liệu (icon là component, `run` là hàm) — file thuần.\n * Chữ hiển thị KHÔNG nằm ở đây: tên khối và từ đồng nghĩa đi qua namespace `markdownEditor`; ở đây\n * chỉ có bí danh MÁY tiếng Anh (`h1`, `ul`, `todo`…), thứ người dùng gõ như nhau ở mọi ngôn ngữ.\n */\n\nimport { type Editor, Extension, type Range } from \"@tiptap/core\";\nimport { PluginKey } from \"@tiptap/pm/state\";\nimport { Suggestion, type SuggestionOptions } from \"@tiptap/suggestion\";\nimport {\n BetweenHorizontalEnd,\n BetweenVerticalEnd,\n Columns3,\n Heading1,\n Heading2,\n Heading3,\n ImagePlus,\n List,\n ListChecks,\n ListOrdered,\n type LucideIcon,\n Minus,\n Pilcrow,\n Rows3,\n SquareCode,\n Table,\n TextQuote,\n Trash2\n} from \"lucide-react\";\nimport { matchesSearch } from \"../lib/text-search.js\";\nimport type { EditorFormat } from \"./editor-types.js\";\nimport type { MarkdownEditorBlockId, MarkdownEditorLabels } from \"./markdown-editor-constants.js\";\n\n/** Thứ menu cần mà chỉ component biết — vd mở hộp chọn tệp. */\nexport interface SlashCommandContext {\n /** Định dạng lưu — quyết định khối nào chèn được vào ô bảng. Mặc định `markdown`. */\n format?: EditorFormat;\n /** Có = app đã truyền `onImageUpload`, khối \"Ảnh\" hiện ra. */\n pickImage?: () => void;\n}\n\nexport interface SlashCommand {\n id: MarkdownEditorBlockId;\n icon: LucideIcon;\n /**\n * Cú pháp gõ tắt, hiện mờ bên phải mục — kiểu Linear. Menu `/` vì vậy cũng là chỗ DẠY cú pháp:\n * lần sau người dùng gõ thẳng `## ` mà không cần mở menu.\n */\n hint?: string;\n /** Bí danh máy, cách nhau bằng dấu cách — luôn tìm được, ở mọi ngôn ngữ. */\n aliases: string;\n /** Mục có hiện ở vị trí con trỏ này không. Bỏ trống = hiện ở mọi chỗ chèn được KHỐI (`blockAllowed`). */\n when?: (editor: Editor, context: SlashCommandContext) => boolean;\n run: (editor: Editor, range: Range, context: SlashCommandContext) => void;\n}\n\n/** Mục hiện trong menu: lệnh + chữ đã dịch, ngữ cảnh đã buộc sẵn. */\nexport interface SlashMenuItem {\n id: MarkdownEditorBlockId;\n icon: LucideIcon;\n hint?: string;\n label: string;\n execute: (editor: Editor, range: Range) => void;\n}\n\nconst inTable = (editor: Editor) => editor.isActive(\"table\");\n\n/**\n * Chỗ con trỏ đứng có nhận một KHỐI không. Ô bảng của trình soạn MARKDOWN chỉ chứa một đoạn văn (xem\n * `ComitorTableCell`), nên ở đó mọi khối có hiện cũng không chèn được; ô bảng HTML thì nhận.\n */\nconst blockAllowed = (editor: Editor, context: SlashCommandContext) => context.format === \"html\" || !inTable(editor);\n\n/*\n * Thứ tự theo tần suất dùng của một mô tả công việc (văn bản → tiêu đề → danh sách → khối), không\n * theo bảng chữ cái: menu lọc theo chữ gõ, nên thứ tự chỉ có nghĩa ở lượt mở ĐẦU, khi chưa gõ gì.\n *\n * Trong bảng có thêm thao tác bảng (thêm/xoá hàng, cột); ở trình soạn markdown thì CHỈ còn chúng.\n */\nexport const SLASH_COMMANDS: readonly SlashCommand[] = [\n {\n id: \"text\",\n icon: Pilcrow,\n aliases: \"p text paragraph\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setParagraph().run()\n },\n {\n id: \"heading1\",\n icon: Heading1,\n hint: \"#\",\n aliases: \"h1 heading\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 1 }).run()\n },\n {\n id: \"heading2\",\n icon: Heading2,\n hint: \"##\",\n aliases: \"h2 heading\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 2 }).run()\n },\n {\n id: \"heading3\",\n icon: Heading3,\n hint: \"###\",\n aliases: \"h3 heading\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 3 }).run()\n },\n {\n id: \"bulletList\",\n icon: List,\n hint: \"-\",\n aliases: \"ul bullet list\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleBulletList().run()\n },\n {\n id: \"orderedList\",\n icon: ListOrdered,\n hint: \"1.\",\n aliases: \"ol ordered number list\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleOrderedList().run()\n },\n {\n id: \"taskList\",\n icon: ListChecks,\n hint: \"[]\",\n aliases: \"todo task checklist checkbox\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleTaskList().run()\n },\n {\n id: \"blockquote\",\n icon: TextQuote,\n hint: \">\",\n aliases: \"quote blockquote\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).toggleBlockquote().run()\n },\n {\n id: \"codeBlock\",\n icon: SquareCode,\n hint: \"```\",\n aliases: \"code pre codeblock\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setCodeBlock().run()\n },\n {\n id: \"table\",\n icon: Table,\n aliases: \"table spreadsheet\",\n // Không lồng bảng trong bảng — GFM không có, và hiếm khi là điều người soạn muốn.\n when: (editor) => !inTable(editor),\n run: (editor, range) =>\n editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()\n },\n {\n id: \"divider\",\n icon: Minus,\n hint: \"---\",\n aliases: \"hr divider rule separator\",\n run: (editor, range) => editor.chain().focus().deleteRange(range).setHorizontalRule().run()\n },\n {\n id: \"image\",\n icon: ImagePlus,\n aliases: \"img image picture upload\",\n when: (editor, context) => blockAllowed(editor, context) && Boolean(context.pickImage),\n run: (editor, range, context) => {\n editor.chain().focus().deleteRange(range).run();\n context.pickImage?.();\n }\n },\n {\n id: \"tableAddRow\",\n icon: BetweenHorizontalEnd,\n aliases: \"row add insert\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).addRowAfter().run()\n },\n {\n id: \"tableAddColumn\",\n icon: BetweenVerticalEnd,\n aliases: \"column col add insert\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).addColumnAfter().run()\n },\n {\n id: \"tableDeleteRow\",\n icon: Rows3,\n aliases: \"row delete remove\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).deleteRow().run()\n },\n {\n id: \"tableDeleteColumn\",\n icon: Columns3,\n aliases: \"column col delete remove\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).deleteColumn().run()\n },\n {\n id: \"tableDelete\",\n icon: Trash2,\n aliases: \"table delete remove\",\n when: inTable,\n run: (editor, range) => editor.chain().focus().deleteRange(range).deleteTable().run()\n }\n];\n\n/**\n * Các mục hiện được ở vị trí con trỏ, khớp với chữ đã gõ.\n *\n * Khớp = MỌI từ của chữ gõ nằm trong (tên đã dịch + từ đồng nghĩa đã dịch + bí danh máy) — không\n * dấu, không phân biệt hoa thường, đúng luật `matchesSearch` của `Combobox`: \"/tieu\" ra \"Tiêu đề\",\n * \"/h2\" ra \"Tiêu đề 2\" ở mọi ngôn ngữ.\n */\nexport function filterSlashCommands(\n editor: Editor,\n query: string,\n labels: MarkdownEditorLabels,\n context: SlashCommandContext\n): SlashMenuItem[] {\n const words = query.trim().split(/\\s+/).filter(Boolean);\n return SLASH_COMMANDS.filter((command) =>\n command.when ? command.when(editor, context) : blockAllowed(editor, context)\n )\n .filter((command) => {\n const haystack = `${labels[command.id]} ${labels.slashKeywords[command.id]} ${command.aliases}`;\n return words.every((word) => matchesSearch(haystack, word));\n })\n .map((command) => ({\n id: command.id,\n icon: command.icon,\n hint: command.hint,\n label: labels[command.id],\n execute: (target, range) => command.run(target, range, context)\n }));\n}\n\nexport const SLASH_PLUGIN_KEY = new PluginKey(\"comitorSlashMenu\");\n\n/**\n * Extension mở menu `/`. Phần GIAO DIỆN (danh sách, bàn phím) đến qua `render` — xem\n * `SuggestionStore`.\n *\n * `/` chỉ kích hoạt ở đầu dòng hoặc sau dấu cách (`allowedPrefixes` mặc định của Tiptap), nên gõ\n * đường dẫn `a/b` hay ngày `02/10` không bật menu. Không bật trong khối mã và mã nội dòng: ở đó `/`\n * là chữ của mã.\n */\nexport function createSlashCommandExtension(\n options: Pick<SuggestionOptions<SlashMenuItem, SlashMenuItem>, \"items\" | \"render\">\n): Extension {\n return Extension.create({\n name: \"comitorSlashMenu\",\n addProseMirrorPlugins() {\n return [\n Suggestion<SlashMenuItem, SlashMenuItem>({\n editor: this.editor,\n pluginKey: SLASH_PLUGIN_KEY,\n char: \"/\",\n allowSpaces: false,\n items: options.items,\n render: options.render,\n command: ({ editor, range, props }) => props.execute(editor, range),\n allow: ({ editor }) => editor.isEditable && !editor.isActive(\"codeBlock\") && !editor.isActive(\"code\")\n })\n ];\n }\n });\n}\n"],"mappings":"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","names":[]}