@comitor/ui 3.3.4 → 3.5.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.
Files changed (55) hide show
  1. package/README.md +19 -5
  2. package/dist/components/composite/form-section.js.map +1 -1
  3. package/dist/components/composite/relative-time.d.ts +13 -2
  4. package/dist/components/composite/relative-time.js +12 -1
  5. package/dist/components/composite/relative-time.js.map +1 -1
  6. package/dist/components/composite/stat-card.d.ts +3 -0
  7. package/dist/components/composite/stat-card.js +2 -1
  8. package/dist/components/composite/stat-card.js.map +1 -1
  9. package/dist/components/ui/badge.js.map +1 -1
  10. package/dist/components/ui/field.js.map +1 -1
  11. package/dist/components/ui/item.d.ts +1 -1
  12. package/dist/data-grid/use-data-grid.js +24 -10
  13. package/dist/data-grid/use-data-grid.js.map +1 -1
  14. package/dist/editor/bubble-toolbar.d.ts +34 -0
  15. package/dist/editor/bubble-toolbar.js +391 -0
  16. package/dist/editor/bubble-toolbar.js.map +1 -0
  17. package/dist/editor/image-upload.d.ts +57 -0
  18. package/dist/editor/image-upload.js +136 -0
  19. package/dist/editor/image-upload.js.map +1 -0
  20. package/dist/editor/index.d.ts +5 -0
  21. package/dist/editor/index.js +11 -0
  22. package/dist/editor/index.js.map +1 -0
  23. package/dist/editor/markdown-editor-classes.d.ts +16 -0
  24. package/dist/editor/markdown-editor-classes.js +117 -0
  25. package/dist/editor/markdown-editor-classes.js.map +1 -0
  26. package/dist/editor/markdown-editor-constants.d.ts +92 -0
  27. package/dist/editor/markdown-editor-constants.js +6 -0
  28. package/dist/editor/markdown-editor-constants.js.map +1 -0
  29. package/dist/editor/markdown-editor.d.ts +125 -0
  30. package/dist/editor/markdown-editor.js +325 -0
  31. package/dist/editor/markdown-editor.js.map +1 -0
  32. package/dist/editor/markdown-extensions.d.ts +71 -0
  33. package/dist/editor/markdown-extensions.js +172 -0
  34. package/dist/editor/markdown-extensions.js.map +1 -0
  35. package/dist/editor/markdown-syntax.d.ts +87 -0
  36. package/dist/editor/markdown-syntax.js +58 -0
  37. package/dist/editor/markdown-syntax.js.map +1 -0
  38. package/dist/editor/slash-commands.d.ts +62 -0
  39. package/dist/editor/slash-commands.js +186 -0
  40. package/dist/editor/slash-commands.js.map +1 -0
  41. package/dist/editor/suggestion-menu.d.ts +38 -0
  42. package/dist/editor/suggestion-menu.js +144 -0
  43. package/dist/editor/suggestion-menu.js.map +1 -0
  44. package/dist/editor/suggestion-store.d.ts +47 -0
  45. package/dist/editor/suggestion-store.js +49 -0
  46. package/dist/editor/suggestion-store.js.map +1 -0
  47. package/dist/i18n/locale-provider.d.ts +1 -0
  48. package/dist/i18n/messages.d.ts +2 -0
  49. package/dist/i18n/messages.js +3 -0
  50. package/dist/i18n/messages.js.map +1 -1
  51. package/dist/i18n/namespaces/markdown-editor.d.ts +6 -0
  52. package/dist/i18n/namespaces/markdown-editor.js +127 -0
  53. package/dist/i18n/namespaces/markdown-editor.js.map +1 -0
  54. package/dist/index.d.ts +1 -0
  55. package/package.json +80 -2
@@ -0,0 +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":[]}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Phép biến đổi CHUỖI markdown của `MarkdownEditor` — hàm thuần, không nhập Tiptap, không DOM.
3
+ *
4
+ * Tách khỏi `markdown-extensions.ts` để cổng `test:editor-markdown` gọi thẳng được từng hàm trên
5
+ * `dist/`, và để mỗi chỗ vá `@tiptap/markdown` nằm cạnh lý do của nó.
6
+ *
7
+ * ── VÌ SAO PHẢI VÁ ───────────────────────────────────────────────────────────────────────────
8
+ * Markdown là định dạng LƯU của trình soạn (cột mô tả, bình luận, tài liệu; công cụ MCP đọc/ghi
9
+ * thẳng nó). Nên phép đo đúng không phải "hiện ra có đẹp không" mà là: tài liệu → markdown → tài
10
+ * liệu có ra ĐÚNG tài liệu ban đầu không. `@tiptap/markdown` 3.31.4 trượt hai ca, cả hai IM LẶNG —
11
+ * không lỗi, chỉ là lần mở sau nội dung đã khác (đo bằng `MarkdownManager` trên chính bản đó):
12
+ *
13
+ * 1. Đoạn văn mở đầu bằng cú pháp KHỐI không được thoát. Đoạn "# không phải tiêu đề" ra
14
+ * `# không phải tiêu đề` → mở lại thành TIÊU ĐỀ. Cùng họ: "- …" thành danh sách, "2024. …"
15
+ * thành danh sách số bắt đầu từ 2024, dòng toàn `===` biến dòng trên thành tiêu đề, và 4 dấu
16
+ * cách đầu đoạn thành khối mã. Bộ thoát nội dòng của nó chỉ lo `\ ` * _ [ ] ~` và thực thể
17
+ * HTML. Ca thật dễ gặp nhất: gõ "1. " (luật nhập đổi thành danh sách) rồi ⌘Z để giữ chữ.
18
+ * 2. Khối mã CHỨA ``` vẫn được bọc bằng ``` — hàng rào bên trong đóng khối sớm, phần còn lại
19
+ * thành văn bản thường. Ca thật: tài liệu hướng dẫn viết markdown, đúng thứ đội phần mềm viết.
20
+ */
21
+ /**
22
+ * Thoát cú pháp KHỐI ở đầu MỖI dòng của một đoạn văn đã dựng — vá ca 1.
23
+ *
24
+ * Mỗi dòng chứ không chỉ dòng đầu: xuống dòng cứng (Shift+Enter) ra `" \n"`, và dòng sau đó vẫn
25
+ * mở được một danh sách (danh sách được phép ngắt đoạn văn trong CommonMark).
26
+ *
27
+ * Khoảng trắng đầu dòng bị BỎ: markdown vốn bỏ tới 3 dấu cách lúc đọc, còn 4 thì đổi đoạn thành
28
+ * khối mã — giữ chúng không mua được gì ngoài ca hỏng đó.
29
+ *
30
+ * `*` không cần ở đây: bộ thoát nội dòng của `@tiptap/markdown` đã đổi nó thành `\*`. `>` cũng
31
+ * không: nó ra `&gt;`. Ô bảng đi qua hàm này cũng không sao — `\-` vẫn hiện là `-`.
32
+ *
33
+ * @example escapeParagraphMarkdown("# C#") // "\\# C#"
34
+ * @example escapeParagraphMarkdown("2024. Mục") // "2024\\. Mục"
35
+ */
36
+ declare function escapeParagraphMarkdown(markdown: string): string;
37
+ /**
38
+ * Hàng rào cho một khối mã — vá ca 2. Luật CommonMark: hàng rào đóng phải dài ít nhất bằng hàng
39
+ * rào mở, và một dãy backtick NGẮN hơn bên trong không đóng được khối. Nên chọn dài hơn dãy dài
40
+ * nhất có trong nội dung, tối thiểu 3.
41
+ *
42
+ * @example codeFence("a ``` b") // "````"
43
+ */
44
+ declare function codeFence(content: string): string;
45
+ /**
46
+ * Cắt xuống dòng THỪA ở hai đầu chuỗi đã dựng. Bảng ra `"\n| … |\n"` (đã đo), nên một tài liệu mở
47
+ * đầu bằng bảng có dòng trống ở đầu; còn `TrailingNode` luôn giữ một đoạn rỗng sau khối mã/bảng
48
+ * cuối, thành `"\n\n"` ở đuôi. Cả hai không đổi nghĩa — chỉ làm hai bản lưu của CÙNG một nội dung
49
+ * khác nhau, tức một phép so sánh phiên bản báo "có sửa" cho một lần không ai sửa gì.
50
+ *
51
+ * Đầu chuỗi chỉ cắt `\n` (dấu cách đầu đoạn đã bị `escapeParagraphMarkdown` bỏ); đuôi cắt mọi
52
+ * khoảng trắng — kể cả `" "` của một xuống dòng cứng đứng cuối, thứ không còn nghĩa gì ở đó.
53
+ */
54
+ declare function trimMarkdown(markdown: string): string;
55
+ /**
56
+ * Một ảnh markdown — thay bản gốc của `@tiptap/extension-image`, bản ghép thẳng `![${alt}](${src})`
57
+ * không thoát gì. Hai chỗ hỏng thật với ảnh người dùng tải lên:
58
+ * · `alt` mặc định là TÊN TỆP, và tên tệp có `]` lẻ ("bao-gia]v2.png") đóng phần chữ sớm;
59
+ * · `src` có dấu cách hay ngoặc tròn (một route app tự đặt) cắt địa chỉ làm đôi.
60
+ * CommonMark cho bọc địa chỉ trong `<…>` để chứa được cả hai; `<`/`>` bên trong thì mã hoá phần trăm.
61
+ *
62
+ * @example imageMarkdown("/files/a b.png", "ảnh]1") // "![ảnh\\]1](</files/a b.png>)"
63
+ */
64
+ declare function imageMarkdown(src: string, alt?: unknown, title?: unknown): string;
65
+ /** Lược đồ URL của liên kết nhắc tên. Đổi nó là đổi định dạng LƯU — dữ liệu cũ thôi đọc được. */
66
+ declare const MENTION_URL_SCHEME = "mention:";
67
+ /**
68
+ * Dạng lưu của một lần nhắc tên. Tên rỗng thì dùng id — liên kết không được có chữ rỗng.
69
+ *
70
+ * ⚠ `encodeURIComponent` CHỪA `(` và `)` — mà ngoặc tròn chính là thứ đóng phần địa chỉ của liên
71
+ * kết markdown. Id `user (1)` thì ra `mention:user%20(1)` và cổng `test:editor-markdown` đọc lại thành
72
+ * một liên kết thường. Mã hoá nốt hai ký tự đó.
73
+ */
74
+ declare function renderMentionMarkdown(id: string, label?: string | null): string;
75
+ interface ParsedMention {
76
+ /** Toàn bộ đoạn đã khớp — để bộ tách từ (tokenizer) biết đã ăn bao nhiêu ký tự */
77
+ raw: string;
78
+ id: string;
79
+ label: string;
80
+ }
81
+ /**
82
+ * Đọc MỘT lần nhắc tên ở ĐẦU chuỗi. Trả `undefined` khi không khớp — kể cả khi phần trăm mã hoá
83
+ * hỏng (`%E0%A4%A`): khi đó chuỗi được giữ nguyên là chữ thường, thay vì ném lỗi giữa lúc mở tài liệu.
84
+ */
85
+ declare function parseMentionMarkdown(source: string): ParsedMention | undefined;
86
+
87
+ export { MENTION_URL_SCHEME, type ParsedMention, codeFence, escapeParagraphMarkdown, imageMarkdown, parseMentionMarkdown, renderMentionMarkdown, trimMarkdown };
@@ -0,0 +1,58 @@
1
+ const ATX_HEADING = /^#{1,6}(?:[ \t]|$)/;
2
+ const BULLET_MARKER = /^[-+](?:[ \t]|$)/;
3
+ const ORDERED_MARKER = /^(\d{1,9})([.)])(?=[ \t]|$)/;
4
+ const SETEXT_UNDERLINE = /^(?:=+|-+)[ \t]*$/;
5
+ function escapeParagraphMarkdown(markdown) {
6
+ return markdown.split("\n").map((line) => {
7
+ const text = line.replace(/^[ \t]+/, "");
8
+ if (ATX_HEADING.test(text) || BULLET_MARKER.test(text) || SETEXT_UNDERLINE.test(text)) return `\\${text}`;
9
+ const ordered = ORDERED_MARKER.exec(text);
10
+ if (ordered?.[1]) return `${ordered[1]}\\${text.slice(ordered[1].length)}`;
11
+ return text;
12
+ }).join("\n");
13
+ }
14
+ function codeFence(content) {
15
+ let longest = 0;
16
+ for (const run of content.match(/`+/g) ?? []) longest = Math.max(longest, run.length);
17
+ return "`".repeat(Math.max(3, longest + 1));
18
+ }
19
+ function trimMarkdown(markdown) {
20
+ return markdown.replace(/^\n+/, "").replace(/\s+$/, "");
21
+ }
22
+ function imageMarkdown(src, alt, title) {
23
+ const text = typeof alt === "string" ? alt.replace(/[\\[\]]/g, "\\$&") : "";
24
+ const destination = /[\s()<>]/.test(src) ? `<${src.replace(/</g, "%3C").replace(/>/g, "%3E")}>` : src;
25
+ const escapedTitle = typeof title === "string" ? title.replace(/[\x22\\]/g, "\\$&") : "";
26
+ const caption = escapedTitle ? ` "${escapedTitle}"` : "";
27
+ return `![${text}](${destination}${caption})`;
28
+ }
29
+ const MENTION_URL_SCHEME = "mention:";
30
+ const MENTION_LINK = /^\[@((?:\\.|[^\\\]\n])*)\]\(mention:([^()\s]+)\)/;
31
+ function escapeMentionLabel(label) {
32
+ return label.replace(/[\\[\]]/g, "\\$&");
33
+ }
34
+ function renderMentionMarkdown(id, label) {
35
+ const encodedId = encodeURIComponent(id).replace(/\(/g, "%28").replace(/\)/g, "%29");
36
+ return `[@${escapeMentionLabel(label || id)}](${MENTION_URL_SCHEME}${encodedId})`;
37
+ }
38
+ function parseMentionMarkdown(source) {
39
+ const match = MENTION_LINK.exec(source);
40
+ if (!match?.[2]) return void 0;
41
+ try {
42
+ const id = decodeURIComponent(match[2]);
43
+ const label = (match[1] ?? "").replace(/\\(.)/g, "$1");
44
+ return { raw: match[0], id, label: label || id };
45
+ } catch {
46
+ return void 0;
47
+ }
48
+ }
49
+ export {
50
+ MENTION_URL_SCHEME,
51
+ codeFence,
52
+ escapeParagraphMarkdown,
53
+ imageMarkdown,
54
+ parseMentionMarkdown,
55
+ renderMentionMarkdown,
56
+ trimMarkdown
57
+ };
58
+ //# sourceMappingURL=markdown-syntax.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/editor/markdown-syntax.ts"],"sourcesContent":["/**\n * Phép biến đổi CHUỖI markdown của `MarkdownEditor` — hàm thuần, không nhập Tiptap, không DOM.\n *\n * Tách khỏi `markdown-extensions.ts` để cổng `test:editor-markdown` gọi thẳng được từng hàm trên\n * `dist/`, và để mỗi chỗ vá `@tiptap/markdown` nằm cạnh lý do của nó.\n *\n * ── VÌ SAO PHẢI VÁ ───────────────────────────────────────────────────────────────────────────\n * Markdown là định dạng LƯU của trình soạn (cột mô tả, bình luận, tài liệu; công cụ MCP đọc/ghi\n * thẳng nó). Nên phép đo đúng không phải \"hiện ra có đẹp không\" mà là: tài liệu → markdown → tài\n * liệu có ra ĐÚNG tài liệu ban đầu không. `@tiptap/markdown` 3.31.4 trượt hai ca, cả hai IM LẶNG —\n * không lỗi, chỉ là lần mở sau nội dung đã khác (đo bằng `MarkdownManager` trên chính bản đó):\n *\n * 1. Đoạn văn mở đầu bằng cú pháp KHỐI không được thoát. Đoạn \"# không phải tiêu đề\" ra\n * `# không phải tiêu đề` → mở lại thành TIÊU ĐỀ. Cùng họ: \"- …\" thành danh sách, \"2024. …\"\n * thành danh sách số bắt đầu từ 2024, dòng toàn `===` biến dòng trên thành tiêu đề, và 4 dấu\n * cách đầu đoạn thành khối mã. Bộ thoát nội dòng của nó chỉ lo `\\ ` * _ [ ] ~` và thực thể\n * HTML. Ca thật dễ gặp nhất: gõ \"1. \" (luật nhập đổi thành danh sách) rồi ⌘Z để giữ chữ.\n * 2. Khối mã CHỨA ``` vẫn được bọc bằng ``` — hàng rào bên trong đóng khối sớm, phần còn lại\n * thành văn bản thường. Ca thật: tài liệu hướng dẫn viết markdown, đúng thứ đội phần mềm viết.\n */\n\n/** Cú pháp khối mở đầu một dòng — xem ca 1 ở đầu file. */\nconst ATX_HEADING = /^#{1,6}(?:[ \\t]|$)/;\nconst BULLET_MARKER = /^[-+](?:[ \\t]|$)/;\nconst ORDERED_MARKER = /^(\\d{1,9})([.)])(?=[ \\t]|$)/;\nconst SETEXT_UNDERLINE = /^(?:=+|-+)[ \\t]*$/;\n\n/**\n * Thoát cú pháp KHỐI ở đầu MỖI dòng của một đoạn văn đã dựng — vá ca 1.\n *\n * Mỗi dòng chứ không chỉ dòng đầu: xuống dòng cứng (Shift+Enter) ra `\" \\n\"`, và dòng sau đó vẫn\n * mở được một danh sách (danh sách được phép ngắt đoạn văn trong CommonMark).\n *\n * Khoảng trắng đầu dòng bị BỎ: markdown vốn bỏ tới 3 dấu cách lúc đọc, còn 4 thì đổi đoạn thành\n * khối mã — giữ chúng không mua được gì ngoài ca hỏng đó.\n *\n * `*` không cần ở đây: bộ thoát nội dòng của `@tiptap/markdown` đã đổi nó thành `\\*`. `>` cũng\n * không: nó ra `&gt;`. Ô bảng đi qua hàm này cũng không sao — `\\-` vẫn hiện là `-`.\n *\n * @example escapeParagraphMarkdown(\"# C#\") // \"\\\\# C#\"\n * @example escapeParagraphMarkdown(\"2024. Mục\") // \"2024\\\\. Mục\"\n */\nexport function escapeParagraphMarkdown(markdown: string): string {\n return markdown\n .split(\"\\n\")\n .map((line) => {\n const text = line.replace(/^[ \\t]+/, \"\");\n if (ATX_HEADING.test(text) || BULLET_MARKER.test(text) || SETEXT_UNDERLINE.test(text)) return `\\\\${text}`;\n const ordered = ORDERED_MARKER.exec(text);\n if (ordered?.[1]) return `${ordered[1]}\\\\${text.slice(ordered[1].length)}`;\n return text;\n })\n .join(\"\\n\");\n}\n\n/**\n * Hàng rào cho một khối mã — vá ca 2. Luật CommonMark: hàng rào đóng phải dài ít nhất bằng hàng\n * rào mở, và một dãy backtick NGẮN hơn bên trong không đóng được khối. Nên chọn dài hơn dãy dài\n * nhất có trong nội dung, tối thiểu 3.\n *\n * @example codeFence(\"a ``` b\") // \"````\"\n */\nexport function codeFence(content: string): string {\n let longest = 0;\n for (const run of content.match(/`+/g) ?? []) longest = Math.max(longest, run.length);\n return \"`\".repeat(Math.max(3, longest + 1));\n}\n\n/**\n * Cắt xuống dòng THỪA ở hai đầu chuỗi đã dựng. Bảng ra `\"\\n| … |\\n\"` (đã đo), nên một tài liệu mở\n * đầu bằng bảng có dòng trống ở đầu; còn `TrailingNode` luôn giữ một đoạn rỗng sau khối mã/bảng\n * cuối, thành `\"\\n\\n\"` ở đuôi. Cả hai không đổi nghĩa — chỉ làm hai bản lưu của CÙNG một nội dung\n * khác nhau, tức một phép so sánh phiên bản báo \"có sửa\" cho một lần không ai sửa gì.\n *\n * Đầu chuỗi chỉ cắt `\\n` (dấu cách đầu đoạn đã bị `escapeParagraphMarkdown` bỏ); đuôi cắt mọi\n * khoảng trắng — kể cả `\" \"` của một xuống dòng cứng đứng cuối, thứ không còn nghĩa gì ở đó.\n */\nexport function trimMarkdown(markdown: string): string {\n return markdown.replace(/^\\n+/, \"\").replace(/\\s+$/, \"\");\n}\n\n/**\n * Một ảnh markdown — thay bản gốc của `@tiptap/extension-image`, bản ghép thẳng `![${alt}](${src})`\n * không thoát gì. Hai chỗ hỏng thật với ảnh người dùng tải lên:\n * · `alt` mặc định là TÊN TỆP, và tên tệp có `]` lẻ (\"bao-gia]v2.png\") đóng phần chữ sớm;\n * · `src` có dấu cách hay ngoặc tròn (một route app tự đặt) cắt địa chỉ làm đôi.\n * CommonMark cho bọc địa chỉ trong `<…>` để chứa được cả hai; `<`/`>` bên trong thì mã hoá phần trăm.\n *\n * @example imageMarkdown(\"/files/a b.png\", \"ảnh]1\") // \"![ảnh\\\\]1](</files/a b.png>)\"\n */\nexport function imageMarkdown(src: string, alt?: unknown, title?: unknown): string {\n const text = typeof alt === \"string\" ? alt.replace(/[\\\\[\\]]/g, \"\\\\$&\") : \"\";\n const destination = /[\\s()<>]/.test(src) ? `<${src.replace(/</g, \"%3C\").replace(/>/g, \"%3E\")}>` : src;\n // `\\x22` là dấu nháy kép. Viết thẳng ký tự đó trong regex literal thì bộ quét của `test:i18n` (không\n // hiểu regex) tưởng là mở một chuỗi, và báo nửa sau của file là chữ hiển thị tiếng Việt — đã gặp.\n const escapedTitle = typeof title === \"string\" ? title.replace(/[\\x22\\\\]/g, \"\\\\$&\") : \"\";\n const caption = escapedTitle ? ` \"${escapedTitle}\"` : \"\";\n return `![${text}](${destination}${caption})`;\n}\n\n/*\n * ── NHẮC TÊN (`@`) ───────────────────────────────────────────────────────────────────────────\n * Dạng lưu là một LIÊN KẾT markdown thường: `[@Tên hiển thị](mention:<id>)`.\n *\n * Không dùng dạng mặc định của Tiptap (`[@ id=\"…\" label=\"…\"]`): đó là cú pháp riêng, nên mọi chỗ\n * đọc markdown mà không có Tiptap — email thông báo, tác tử đọc qua MCP, trình xem markdown bất kỳ —\n * in nguyên chuỗi ngoặc vuông ra cho người đọc. Dạng liên kết thì rơi về đúng chữ \"@Tên\": trình xem\n * nào bỏ lược đồ lạ thì còn chữ, trình nào giữ thì còn cả liên kết.\n *\n * Tên trong đó chỉ là ẢNH CHỤP lúc nhắc. Thứ có thẩm quyền là `id` — tên sống thì app tra lại khi\n * hiển thị (`mentions.resolveLabel`), cùng lý do một module Comitor không giữ cột tên người.\n */\n\n/** Lược đồ URL của liên kết nhắc tên. Đổi nó là đổi định dạng LƯU — dữ liệu cũ thôi đọc được. */\nexport const MENTION_URL_SCHEME = \"mention:\";\n\n/*\n * Tên: mọi ký tự trừ `]` và `\\` trần (chúng đi kèm dấu thoát), không xuống dòng. Id: không dấu\n * cách, không ngoặc tròn — `renderMentionMarkdown` đã mã hoá phần trăm mọi ký tự đặc biệt.\n */\nconst MENTION_LINK = /^\\[@((?:\\\\.|[^\\\\\\]\\n])*)\\]\\(mention:([^()\\s]+)\\)/;\n\n/** Thoát `\\`, `[`, `]` trong tên — ba ký tự duy nhất phá được cấu trúc `[@…](…)`. */\nfunction escapeMentionLabel(label: string): string {\n return label.replace(/[\\\\[\\]]/g, \"\\\\$&\");\n}\n\n/**\n * Dạng lưu của một lần nhắc tên. Tên rỗng thì dùng id — liên kết không được có chữ rỗng.\n *\n * ⚠ `encodeURIComponent` CHỪA `(` và `)` — mà ngoặc tròn chính là thứ đóng phần địa chỉ của liên\n * kết markdown. Id `user (1)` thì ra `mention:user%20(1)` và cổng `test:editor-markdown` đọc lại thành\n * một liên kết thường. Mã hoá nốt hai ký tự đó.\n */\nexport function renderMentionMarkdown(id: string, label?: string | null): string {\n const encodedId = encodeURIComponent(id).replace(/\\(/g, \"%28\").replace(/\\)/g, \"%29\");\n return `[@${escapeMentionLabel(label || id)}](${MENTION_URL_SCHEME}${encodedId})`;\n}\n\nexport interface ParsedMention {\n /** Toàn bộ đoạn đã khớp — để bộ tách từ (tokenizer) biết đã ăn bao nhiêu ký tự */\n raw: string;\n id: string;\n label: string;\n}\n\n/**\n * Đọc MỘT lần nhắc tên ở ĐẦU chuỗi. Trả `undefined` khi không khớp — kể cả khi phần trăm mã hoá\n * hỏng (`%E0%A4%A`): khi đó chuỗi được giữ nguyên là chữ thường, thay vì ném lỗi giữa lúc mở tài liệu.\n */\nexport function parseMentionMarkdown(source: string): ParsedMention | undefined {\n const match = MENTION_LINK.exec(source);\n if (!match?.[2]) return undefined;\n try {\n const id = decodeURIComponent(match[2]);\n const label = (match[1] ?? \"\").replace(/\\\\(.)/g, \"$1\");\n return { raw: match[0], id, label: label || id };\n } catch {\n return undefined;\n }\n}\n"],"mappings":"AAsBA,MAAM,cAAc;AACpB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AAiBlB,SAAS,wBAAwB,UAA0B;AAChE,SAAO,SACJ,MAAM,IAAI,EACV,IAAI,CAAC,SAAS;AACb,UAAM,OAAO,KAAK,QAAQ,WAAW,EAAE;AACvC,QAAI,YAAY,KAAK,IAAI,KAAK,cAAc,KAAK,IAAI,KAAK,iBAAiB,KAAK,IAAI,EAAG,QAAO,KAAK,IAAI;AACvG,UAAM,UAAU,eAAe,KAAK,IAAI;AACxC,QAAI,UAAU,CAAC,EAAG,QAAO,GAAG,QAAQ,CAAC,CAAC,KAAK,KAAK,MAAM,QAAQ,CAAC,EAAE,MAAM,CAAC;AACxE,WAAO;AAAA,EACT,CAAC,EACA,KAAK,IAAI;AACd;AASO,SAAS,UAAU,SAAyB;AACjD,MAAI,UAAU;AACd,aAAW,OAAO,QAAQ,MAAM,KAAK,KAAK,CAAC,EAAG,WAAU,KAAK,IAAI,SAAS,IAAI,MAAM;AACpF,SAAO,IAAI,OAAO,KAAK,IAAI,GAAG,UAAU,CAAC,CAAC;AAC5C;AAWO,SAAS,aAAa,UAA0B;AACrD,SAAO,SAAS,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE;AACxD;AAWO,SAAS,cAAc,KAAa,KAAe,OAAyB;AACjF,QAAM,OAAO,OAAO,QAAQ,WAAW,IAAI,QAAQ,YAAY,MAAM,IAAI;AACzE,QAAM,cAAc,WAAW,KAAK,GAAG,IAAI,IAAI,IAAI,QAAQ,MAAM,KAAK,EAAE,QAAQ,MAAM,KAAK,CAAC,MAAM;AAGlG,QAAM,eAAe,OAAO,UAAU,WAAW,MAAM,QAAQ,aAAa,MAAM,IAAI;AACtF,QAAM,UAAU,eAAe,KAAK,YAAY,MAAM;AACtD,SAAO,KAAK,IAAI,KAAK,WAAW,GAAG,OAAO;AAC5C;AAgBO,MAAM,qBAAqB;AAMlC,MAAM,eAAe;AAGrB,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MAAM,QAAQ,YAAY,MAAM;AACzC;AASO,SAAS,sBAAsB,IAAY,OAA+B;AAC/E,QAAM,YAAY,mBAAmB,EAAE,EAAE,QAAQ,OAAO,KAAK,EAAE,QAAQ,OAAO,KAAK;AACnF,SAAO,KAAK,mBAAmB,SAAS,EAAE,CAAC,KAAK,kBAAkB,GAAG,SAAS;AAChF;AAaO,SAAS,qBAAqB,QAA2C;AAC9E,QAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,MAAI,CAAC,QAAQ,CAAC,EAAG,QAAO;AACxB,MAAI;AACF,UAAM,KAAK,mBAAmB,MAAM,CAAC,CAAC;AACtC,UAAM,SAAS,MAAM,CAAC,KAAK,IAAI,QAAQ,UAAU,IAAI;AACrD,WAAO,EAAE,KAAK,MAAM,CAAC,GAAG,IAAI,OAAO,SAAS,GAAG;AAAA,EACjD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,62 @@
1
+ import { Editor, Range, Extension } from '@tiptap/core';
2
+ import { PluginKey } from '@tiptap/pm/state';
3
+ import { SuggestionOptions } from '@tiptap/suggestion';
4
+ import { LucideIcon } from 'lucide-react';
5
+ import { MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
6
+
7
+ /**
8
+ * Menu `/` — danh sách khối chèn được, và extension Tiptap mở nó.
9
+ *
10
+ * Không `"use client"`: bảng dưới đây là dữ liệu (icon là component, `run` là hàm) — file thuần.
11
+ * Chữ hiển thị KHÔNG nằm ở đây: tên khối và từ đồng nghĩa đi qua namespace `markdownEditor`; ở đây
12
+ * 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ữ.
13
+ */
14
+
15
+ /** Thứ menu cần mà chỉ component biết — vd mở hộp chọn tệp. */
16
+ interface SlashCommandContext {
17
+ /** Có = app đã truyền `onImageUpload`, khối "Ảnh" hiện ra. */
18
+ pickImage?: () => void;
19
+ }
20
+ interface SlashCommand {
21
+ id: MarkdownEditorBlockId;
22
+ icon: LucideIcon;
23
+ /**
24
+ * 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:
25
+ * lần sau người dùng gõ thẳng `## ` mà không cần mở menu.
26
+ */
27
+ hint?: string;
28
+ /** Bí danh máy, cách nhau bằng dấu cách — luôn tìm được, ở mọi ngôn ngữ. */
29
+ 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. */
31
+ when?: (editor: Editor, context: SlashCommandContext) => boolean;
32
+ run: (editor: Editor, range: Range, context: SlashCommandContext) => void;
33
+ }
34
+ /** Mục hiện trong menu: lệnh + chữ đã dịch, ngữ cảnh đã buộc sẵn. */
35
+ interface SlashMenuItem {
36
+ id: MarkdownEditorBlockId;
37
+ icon: LucideIcon;
38
+ hint?: string;
39
+ label: string;
40
+ execute: (editor: Editor, range: Range) => void;
41
+ }
42
+ declare const SLASH_COMMANDS: readonly SlashCommand[];
43
+ /**
44
+ * Các mục hiện được ở vị trí con trỏ, khớp với chữ đã gõ.
45
+ *
46
+ * 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
47
+ * dấu, không phân biệt hoa thường, đúng luật `matchesSearch` của `Combobox`: "/tieu" ra "Tiêu đề",
48
+ * "/h2" ra "Tiêu đề 2" ở mọi ngôn ngữ.
49
+ */
50
+ declare function filterSlashCommands(editor: Editor, query: string, labels: MarkdownEditorLabels, context: SlashCommandContext): SlashMenuItem[];
51
+ declare const SLASH_PLUGIN_KEY: PluginKey<any>;
52
+ /**
53
+ * Extension mở menu `/`. Phần GIAO DIỆN (danh sách, bàn phím) đến qua `render` — xem
54
+ * `SuggestionStore`.
55
+ *
56
+ * `/` 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õ
57
+ * đườ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: ở đó `/`
58
+ * là chữ của mã.
59
+ */
60
+ declare function createSlashCommandExtension(options: Pick<SuggestionOptions<SlashMenuItem, SlashMenuItem>, "items" | "render">): Extension;
61
+
62
+ export { SLASH_COMMANDS, SLASH_PLUGIN_KEY, type SlashCommand, type SlashCommandContext, type SlashMenuItem, createSlashCommandExtension, filterSlashCommands };
@@ -0,0 +1,186 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { PluginKey } from "@tiptap/pm/state";
3
+ import { Suggestion } from "@tiptap/suggestion";
4
+ import {
5
+ BetweenHorizontalEnd,
6
+ BetweenVerticalEnd,
7
+ Columns3,
8
+ Heading1,
9
+ Heading2,
10
+ Heading3,
11
+ ImagePlus,
12
+ List,
13
+ ListChecks,
14
+ ListOrdered,
15
+ Minus,
16
+ Pilcrow,
17
+ Rows3,
18
+ SquareCode,
19
+ Table,
20
+ TextQuote,
21
+ Trash2
22
+ } from "lucide-react";
23
+ import { matchesSearch } from "../lib/text-search.js";
24
+ const inTable = (editor) => editor.isActive("table");
25
+ const SLASH_COMMANDS = [
26
+ {
27
+ id: "text",
28
+ icon: Pilcrow,
29
+ aliases: "p text paragraph",
30
+ run: (editor, range) => editor.chain().focus().deleteRange(range).setParagraph().run()
31
+ },
32
+ {
33
+ id: "heading1",
34
+ icon: Heading1,
35
+ hint: "#",
36
+ aliases: "h1 heading",
37
+ run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 1 }).run()
38
+ },
39
+ {
40
+ id: "heading2",
41
+ icon: Heading2,
42
+ hint: "##",
43
+ aliases: "h2 heading",
44
+ run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 2 }).run()
45
+ },
46
+ {
47
+ id: "heading3",
48
+ icon: Heading3,
49
+ hint: "###",
50
+ aliases: "h3 heading",
51
+ run: (editor, range) => editor.chain().focus().deleteRange(range).setHeading({ level: 3 }).run()
52
+ },
53
+ {
54
+ id: "bulletList",
55
+ icon: List,
56
+ hint: "-",
57
+ aliases: "ul bullet list",
58
+ run: (editor, range) => editor.chain().focus().deleteRange(range).toggleBulletList().run()
59
+ },
60
+ {
61
+ id: "orderedList",
62
+ icon: ListOrdered,
63
+ hint: "1.",
64
+ aliases: "ol ordered number list",
65
+ run: (editor, range) => editor.chain().focus().deleteRange(range).toggleOrderedList().run()
66
+ },
67
+ {
68
+ id: "taskList",
69
+ icon: ListChecks,
70
+ hint: "[]",
71
+ aliases: "todo task checklist checkbox",
72
+ run: (editor, range) => editor.chain().focus().deleteRange(range).toggleTaskList().run()
73
+ },
74
+ {
75
+ id: "blockquote",
76
+ icon: TextQuote,
77
+ hint: ">",
78
+ aliases: "quote blockquote",
79
+ run: (editor, range) => editor.chain().focus().deleteRange(range).toggleBlockquote().run()
80
+ },
81
+ {
82
+ id: "codeBlock",
83
+ icon: SquareCode,
84
+ hint: "```",
85
+ aliases: "code pre codeblock",
86
+ run: (editor, range) => editor.chain().focus().deleteRange(range).setCodeBlock().run()
87
+ },
88
+ {
89
+ id: "table",
90
+ icon: Table,
91
+ aliases: "table spreadsheet",
92
+ run: (editor, range) => editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
93
+ },
94
+ {
95
+ id: "divider",
96
+ icon: Minus,
97
+ hint: "---",
98
+ aliases: "hr divider rule separator",
99
+ run: (editor, range) => editor.chain().focus().deleteRange(range).setHorizontalRule().run()
100
+ },
101
+ {
102
+ id: "image",
103
+ icon: ImagePlus,
104
+ aliases: "img image picture upload",
105
+ when: (editor, context) => !inTable(editor) && Boolean(context.pickImage),
106
+ run: (editor, range, context) => {
107
+ editor.chain().focus().deleteRange(range).run();
108
+ context.pickImage?.();
109
+ }
110
+ },
111
+ {
112
+ id: "tableAddRow",
113
+ icon: BetweenHorizontalEnd,
114
+ aliases: "row add insert",
115
+ when: inTable,
116
+ run: (editor, range) => editor.chain().focus().deleteRange(range).addRowAfter().run()
117
+ },
118
+ {
119
+ id: "tableAddColumn",
120
+ icon: BetweenVerticalEnd,
121
+ aliases: "column col add insert",
122
+ when: inTable,
123
+ run: (editor, range) => editor.chain().focus().deleteRange(range).addColumnAfter().run()
124
+ },
125
+ {
126
+ id: "tableDeleteRow",
127
+ icon: Rows3,
128
+ aliases: "row delete remove",
129
+ when: inTable,
130
+ run: (editor, range) => editor.chain().focus().deleteRange(range).deleteRow().run()
131
+ },
132
+ {
133
+ id: "tableDeleteColumn",
134
+ icon: Columns3,
135
+ aliases: "column col delete remove",
136
+ when: inTable,
137
+ run: (editor, range) => editor.chain().focus().deleteRange(range).deleteColumn().run()
138
+ },
139
+ {
140
+ id: "tableDelete",
141
+ icon: Trash2,
142
+ aliases: "table delete remove",
143
+ when: inTable,
144
+ run: (editor, range) => editor.chain().focus().deleteRange(range).deleteTable().run()
145
+ }
146
+ ];
147
+ function filterSlashCommands(editor, query, labels, context) {
148
+ const words = query.trim().split(/\s+/).filter(Boolean);
149
+ return SLASH_COMMANDS.filter((command) => command.when ? command.when(editor, context) : !inTable(editor)).filter((command) => {
150
+ const haystack = `${labels[command.id]} ${labels.slashKeywords[command.id]} ${command.aliases}`;
151
+ return words.every((word) => matchesSearch(haystack, word));
152
+ }).map((command) => ({
153
+ id: command.id,
154
+ icon: command.icon,
155
+ hint: command.hint,
156
+ label: labels[command.id],
157
+ execute: (target, range) => command.run(target, range, context)
158
+ }));
159
+ }
160
+ const SLASH_PLUGIN_KEY = new PluginKey("comitorSlashMenu");
161
+ function createSlashCommandExtension(options) {
162
+ return Extension.create({
163
+ name: "comitorSlashMenu",
164
+ addProseMirrorPlugins() {
165
+ return [
166
+ Suggestion({
167
+ editor: this.editor,
168
+ pluginKey: SLASH_PLUGIN_KEY,
169
+ char: "/",
170
+ allowSpaces: false,
171
+ items: options.items,
172
+ render: options.render,
173
+ command: ({ editor, range, props }) => props.execute(editor, range),
174
+ allow: ({ editor }) => editor.isEditable && !editor.isActive("codeBlock") && !editor.isActive("code")
175
+ })
176
+ ];
177
+ }
178
+ });
179
+ }
180
+ export {
181
+ SLASH_COMMANDS,
182
+ SLASH_PLUGIN_KEY,
183
+ createSlashCommandExtension,
184
+ filterSlashCommands
185
+ };
186
+ //# sourceMappingURL=slash-commands.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,38 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import * as React from 'react';
3
+ import { SuggestionStore } from './suggestion-store.js';
4
+ import '@tiptap/pm/state';
5
+ import '@tiptap/suggestion';
6
+
7
+ /**
8
+ * Danh sách gợi ý nổi của trình soạn — DÙNG CHUNG cho menu `/` (khối) và `@` (người).
9
+ *
10
+ * Con trỏ KHÔNG rời vùng soạn: người dùng vẫn đang gõ chữ lọc, nên focus ở lại ProseMirror và bàn
11
+ * phím đi đường plugin → `SuggestionStore.keyHandler` → đây. Vì vậy danh sách là `role="listbox"`
12
+ * điều khiển bằng `aria-activedescendant` đặt lên CHÍNH vùng soạn (khuôn combobox của ARIA), không
13
+ * phải một danh sách nhận focus.
14
+ *
15
+ * Mở bằng `Popover` của gói neo vào một phần tử ẢO (khung của đoạn `/que…` đang gõ) — lý do không
16
+ * gắn thẳng vào `document.body` ở đầu `suggestion-store.ts`.
17
+ */
18
+
19
+ interface SuggestionMenuProps<Item, Selected> {
20
+ editor: Editor;
21
+ store: SuggestionStore<Item, Selected>;
22
+ /** Tên của listbox cho trình đọc màn hình */
23
+ label: string;
24
+ emptyLabel: string;
25
+ loadingLabel?: string;
26
+ getKey: (item: Item) => string;
27
+ renderItem: (item: Item) => React.ReactNode;
28
+ /**
29
+ * Ẩn hẳn menu (và thoát chế độ gợi ý) khi KHÔNG có kết quả. Dùng cho `@`: tên người có dấu cách
30
+ * nên chữ lọc được phép chứa dấu cách — khi không còn ai khớp, người dùng đang viết câu thường
31
+ * chứ không còn tìm người, và một ô "Không tìm thấy" bám theo từng chữ là phiền.
32
+ */
33
+ exitWhenEmpty?: (query: string) => boolean;
34
+ className?: string;
35
+ }
36
+ declare function SuggestionMenu<Item, Selected>({ editor, store, label, emptyLabel, loadingLabel, getKey, renderItem, exitWhenEmpty, className }: SuggestionMenuProps<Item, Selected>): React.JSX.Element;
37
+
38
+ export { SuggestionMenu, type SuggestionMenuProps };