@comitor/ui 3.6.2 → 3.7.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 (67) hide show
  1. package/README.md +5 -5
  2. package/dist/components/composite/combobox.d.ts +9 -0
  3. package/dist/components/composite/combobox.js +16 -3
  4. package/dist/components/composite/combobox.js.map +1 -1
  5. package/dist/components/composite/inline-edit.d.ts +7 -1
  6. package/dist/components/composite/inline-edit.js +10 -1
  7. package/dist/components/composite/inline-edit.js.map +1 -1
  8. package/dist/components/composite/key-sequence.d.ts +44 -0
  9. package/dist/components/composite/key-sequence.js +32 -0
  10. package/dist/components/composite/key-sequence.js.map +1 -0
  11. package/dist/components/composite/keyboard-shortcuts-dialog.d.ts +39 -0
  12. package/dist/components/composite/keyboard-shortcuts-dialog.js +78 -0
  13. package/dist/components/composite/keyboard-shortcuts-dialog.js.map +1 -0
  14. package/dist/components/composite/use-key-sequences.d.ts +28 -0
  15. package/dist/components/composite/use-key-sequences.js +44 -0
  16. package/dist/components/composite/use-key-sequences.js.map +1 -0
  17. package/dist/components/ui/badge.d.ts +19 -2
  18. package/dist/components/ui/badge.js +44 -4
  19. package/dist/components/ui/badge.js.map +1 -1
  20. package/dist/components/ui/item.d.ts +1 -1
  21. package/dist/components/ui/item.js +13 -2
  22. package/dist/components/ui/item.js.map +1 -1
  23. package/dist/editor/bubble-toolbar.d.ts +2 -1
  24. package/dist/editor/editor-core.d.ts +2 -1
  25. package/dist/editor/editor-core.js +6 -4
  26. package/dist/editor/editor-core.js.map +1 -1
  27. package/dist/editor/editor-types.d.ts +3 -1
  28. package/dist/editor/fixed-toolbar.d.ts +2 -1
  29. package/dist/editor/image-upload.d.ts +3 -1
  30. package/dist/editor/index.d.ts +2 -1
  31. package/dist/editor/markdown-editor-classes.js +3 -1
  32. package/dist/editor/markdown-editor-classes.js.map +1 -1
  33. package/dist/editor/markdown-editor.d.ts +2 -1
  34. package/dist/editor/markdown-extensions.d.ts +9 -3
  35. package/dist/editor/markdown-extensions.js +28 -5
  36. package/dist/editor/markdown-extensions.js.map +1 -1
  37. package/dist/editor/markdown-syntax.d.ts +3 -1
  38. package/dist/editor/markdown-syntax.js +10 -4
  39. package/dist/editor/markdown-syntax.js.map +1 -1
  40. package/dist/editor/rich-text-editor.d.ts +2 -1
  41. package/dist/editor/slash-commands.d.ts +3 -1
  42. package/dist/editor/toolbar-actions.d.ts +3 -1
  43. package/dist/{editor-types-BMST7-VR.d.ts → editor-types-CSrMeuSw.d.ts} +20 -3
  44. package/dist/i18n/locale-provider.d.ts +1 -0
  45. package/dist/i18n/messages.d.ts +2 -0
  46. package/dist/i18n/messages.js +3 -0
  47. package/dist/i18n/messages.js.map +1 -1
  48. package/dist/i18n/namespaces/keyboard-shortcuts.d.ts +13 -0
  49. package/dist/i18n/namespaces/keyboard-shortcuts.js +15 -0
  50. package/dist/i18n/namespaces/keyboard-shortcuts.js.map +1 -0
  51. package/dist/i18n/namespaces/shell.js +4 -2
  52. package/dist/i18n/namespaces/shell.js.map +1 -1
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.js +3 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/shell/app-shell.d.ts +14 -1
  57. package/dist/shell/app-shell.js +24 -2
  58. package/dist/shell/app-shell.js.map +1 -1
  59. package/dist/shell/display-prefs-seed.d.ts +64 -0
  60. package/dist/shell/display-prefs-seed.js +58 -0
  61. package/dist/shell/display-prefs-seed.js.map +1 -0
  62. package/dist/shell/index.d.ts +1 -0
  63. package/dist/shell/index.js +1 -0
  64. package/dist/shell/index.js.map +1 -1
  65. package/dist/shell/types.d.ts +2 -0
  66. package/dist/shell/types.js.map +1 -1
  67. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/editor/markdown-editor-classes.ts"],"sourcesContent":["/**\n * Kiểu chữ của vùng soạn — file thuần, không `\"use client\"`.\n *\n * DOM bên trong do ProseMirror dựng (không phải JSX của gói), nên không có `className` nào gắn được\n * lên từng `<h2>`, `<li>`, `<pre>`: mọi kiểu đi qua biến thể con cháu `[&_h2]:…` đặt trên PHẦN TỬ\n * GỐC của ProseMirror. Tailwind của app vẫn sinh được chúng vì chuỗi nằm nguyên văn trong `dist/`\n * (`@source \"./dist\"` của `styles.css`) — nên KHÔNG dựng chuỗi lớp động ở đây.\n *\n * Màu chỉ đi qua token VAI TRÒ, đúng hợp đồng của gói: chữ thường kế thừa `--foreground`, liên kết\n * `--link-ink`, mã `--muted`, nhắc tên `--info/15` (cùng khối của `Mention`), tô màu cú pháp các bậc\n * `-ink` — chúng là bậc dành cho CHỮ trên nền trang/muted ở cả hai bảng màu.\n */\n\n/*\n * Bố cục khối: khoảng cách giữa các khối CẤP ĐẦU bằng `> * + *` (không margin hai đầu tài liệu),\n * tiêu đề cách xa hơn phía trên để nhóm với phần nó mở. Cỡ chữ theo `Textarea`: 16px ở màn hẹp —\n * dưới 16px là Safari iOS tự phóng to trang khi chạm vào vùng soạn — 14px từ `md`.\n */\nconst LAYOUT = [\n \"min-w-0 text-base leading-relaxed text-foreground outline-none md:text-sm\",\n \"[&>*+*]:mt-3 [&>*:first-child]:mt-0\",\n \"[&>h1]:mt-6 [&>h2]:mt-5 [&>h3]:mt-4\"\n];\n\nconst HEADINGS = [\n \"[&_h1]:text-xl [&_h1]:leading-snug [&_h1]:font-semibold [&_h1]:tracking-tight\",\n \"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold\",\n \"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold\",\n \"[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold\"\n];\n\nconst INLINE = [\n \"[&_strong]:font-semibold\",\n // Liên kết: khuôn của `Button variant=\"link\"` / `FieldDescription` — bậc `--link-ink`, gạch dưới.\n \"[&_a]:text-link-ink [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-ink-hover\",\n \"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5\",\n \"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]\",\n // Nhắc tên: cùng cặp với khối `@Tên` của `Mention` (nền `info/15`, chữ thường) — đã đo ở mention.tsx.\n \"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5\",\n \"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground\"\n];\n\n/*\n * Danh sách thường. Danh sách việc (`ul[data-type=taskList]`) bỏ chấm đầu dòng và tự vẽ ô tick: DOM\n * do `TaskItem` dựng là `li[data-checked] > label > input + span(sr-only)` cạnh `div` nội dung.\n * ⚠ `<li>` KHÔNG mang `data-type=\"taskItem\"`: thuộc tính đó chỉ có trong `renderHTML`, còn node view\n * (thứ thật sự vẽ trong trình soạn) không gắn nó — đã đo trên 3.31.4, selector `li[data-type=…]`\n * trượt hết và ô tick nằm một dòng, chữ rơi xuống dòng dưới. Nhắm qua `ul` cha.\n */\nconst LISTS = [\n \"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal\",\n \"[&_ul:not([data-type=taskList])]:pl-6 [&_ol]:pl-6 [&_li]:marker:text-muted-foreground\",\n \"[&_li+li]:mt-1 [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_li>p]:my-0\",\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0.5\",\n \"[&_[data-type=taskList]>li]:flex [&_[data-type=taskList]>li]:items-start [&_[data-type=taskList]>li]:gap-2\",\n \"[&_[data-type=taskList]>li>label]:flex [&_[data-type=taskList]>li>label]:h-[1.625em] [&_[data-type=taskList]>li>label]:items-center\",\n \"[&_[data-type=taskList]>li>label]:select-none [&_[data-type=taskList]>li>div]:min-w-0 [&_[data-type=taskList]>li>div]:flex-1\",\n // Soạn được: con trỏ tay trên ô tick. Chỉ đọc: ô không nhận chuột — Tiptap để nó bấm được rồi tự lật\n // lại (không có `onReadOnlyChecked`), nên một cú bấm \"không ăn\" là thứ duy nhất người dùng thấy.\n \"[&[contenteditable=true]_[data-type=taskList]>li>label>input]:cursor-pointer\",\n \"[&[contenteditable=false]_[data-type=taskList]>li>label]:pointer-events-none\",\n // Mục đã xong: chữ nhạt + gạch — CHỈ đoạn của chính mục, không lan xuống danh sách con chưa xong.\n \"[&_li[data-checked=true]>div>p]:text-muted-foreground [&_li[data-checked=true]>div>p]:line-through\"\n];\n\n/*\n * Ô tick vẽ bằng CSS trên chính `<input>` (`appearance-none`), dấu tick là một chữ L xoay 45° vẽ\n * bằng viền của `::before` — không ảnh, không SVG, không mã màu: mọi màu là token của `Checkbox`\n * (`--choice-edge` viền nghỉ, `--control-on` nền khi chọn, `--control-check` dấu tick).\n */\nconst TASK_CHECKBOX = [\n \"[&_[data-type=taskList]>li>label>input]:relative [&_[data-type=taskList]>li>label>input]:m-0 [&_[data-type=taskList]>li>label>input]:grid\",\n \"[&_[data-type=taskList]>li>label>input]:size-4 [&_[data-type=taskList]>li>label>input]:shrink-0 [&_[data-type=taskList]>li>label>input]:appearance-none\",\n \"[&_[data-type=taskList]>li>label>input]:place-content-center [&_[data-type=taskList]>li>label>input]:rounded-[4px]\",\n \"[&_[data-type=taskList]>li>label>input]:border [&_[data-type=taskList]>li>label>input]:border-choice-edge\",\n \"[&_[data-type=taskList]>li>label>input]:bg-transparent [&_[data-type=taskList]>li>label>input]:transition-colors\",\n \"[&_[data-type=taskList]>li>label>input:checked]:border-control-on [&_[data-type=taskList]>li>label>input:checked]:bg-control-on\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-2 [&_[data-type=taskList]>li>label>input:focus-visible]:outline-offset-2\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-ring\",\n // Dấu tick CHỈ tồn tại khi đã tick (`content` chỉ có ở `:checked`) — không ẩn bằng `opacity-0`: một\n // dấu vô hình vẫn là một nét mà cổng màu phải đo, và đo ra 1,00:1.\n \"[&_[data-type=taskList]>li>label>input:checked]:before:h-2 [&_[data-type=taskList]>li>label>input:checked]:before:w-1\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:-translate-y-px [&_[data-type=taskList]>li>label>input:checked]:before:rotate-45\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-r-2 [&_[data-type=taskList]>li>label>input:checked]:before:border-b-2\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-control-check\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:content-['']\"\n];\n\n/*\n * Trích dẫn: vạch trái bằng `--muted-foreground` ĐẶC. `--border` đúng vai trò trang trí nhưng 1,16:1\n * ở light — gần như vô hình, trích dẫn chỉ còn là một đoạn chữ xám thụt vào. Bản `/40` dễ nhìn hơn\n * nhưng 1,71:1 trên `--muted` (light) / 1,87:1 (dark): cổng chặn ở bảng tương phản cao. Bản đặc qua\n * 3:1 ở cả bốn tổ hợp bảng màu × theme (đo bằng `pnpm lint:a11y`).\n */\nconst BLOCKS = [\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground\",\n \"[&_blockquote>*+*]:mt-2\",\n // Đường kẻ và lưới bảng (dưới) dùng `--input`, không `--border` — lý do ở chỗ khai `TABLES`.\n \"[&_hr]:my-1 [&_hr]:border-input\",\n // Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng\n // trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn\n // (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.\n \"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-4 [&_pre]:py-3\",\n \"[&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-relaxed [&_pre]:whitespace-pre-wrap\",\n \"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit\"\n];\n\n/*\n * Tô màu cú pháp (lớp `hljs-*` do lowlight sinh). Phân vai kiểu bảng màu sáng quen thuộc của các\n * trình xem mã: từ khoá đỏ, chuỗi xanh đậm, hằng/số xanh ngọc, tên hàm cam, chú thích nhạt + nghiêng.\n * Toàn bậc `-ink`, nên đổi sang dark hay bảng tương phản cao là token tự đổi theo.\n */\nconst SYNTAX = [\n \"[&_.hljs-comment]:text-muted-foreground [&_.hljs-comment]:italic [&_.hljs-quote]:text-muted-foreground\",\n \"[&_.hljs-keyword]:text-red-ink [&_.hljs-selector-tag]:text-red-ink [&_.hljs-doctag]:text-red-ink\",\n \"[&_.hljs-meta_.hljs-keyword]:text-red-ink [&_.hljs-template-tag]:text-red-ink\",\n \"[&_.hljs-string]:text-navy-ink [&_.hljs-regexp]:text-navy-ink [&_.hljs-meta_.hljs-string]:text-navy-ink\",\n \"[&_.hljs-number]:text-teal-ink [&_.hljs-literal]:text-teal-ink [&_.hljs-attr]:text-teal-ink\",\n \"[&_.hljs-attribute]:text-teal-ink [&_.hljs-variable]:text-teal-ink [&_.hljs-template-variable]:text-teal-ink\",\n \"[&_.hljs-selector-attr]:text-teal-ink [&_.hljs-selector-class]:text-teal-ink [&_.hljs-selector-id]:text-teal-ink\",\n \"[&_.hljs-title]:text-orange-ink [&_.hljs-section]:text-orange-ink [&_.hljs-name]:text-orange-ink\",\n \"[&_.hljs-type]:text-orange-ink [&_.hljs-built_in]:text-orange-ink [&_.hljs-symbol]:text-orange-ink\",\n \"[&_.hljs-meta]:text-muted-foreground [&_.hljs-bullet]:text-orange-ink [&_.hljs-link]:underline\",\n \"[&_.hljs-addition]:text-success-ink [&_.hljs-deletion]:text-destructive-ink\",\n \"[&_.hljs-emphasis]:italic [&_.hljs-strong]:font-semibold\"\n];\n\n/*\n * Bảng. Ô bảng của `MarkdownEditor` chỉ chứa MỘT đoạn văn (xem `ComitorTableCell`); ô của\n * `RichTextEditor` chứa được nhiều khối (đoạn, danh sách, ảnh) — `> * + *` cho chúng một nhịp ngắn\n * hơn nhịp của tài liệu, như khối trong trích dẫn.\n * Cuộn ngang ở `.tableWrapper` (Tiptap bọc sẵn) — bảng rộng không được đẩy cả trang tràn ngang ở 375px.\n *\n * Lưới là `border-input`, không `border-border`: khung `outline` ở dark nằm trên `--input-fill` (`--input`\n * 30%), mà `--border` của dark (#1e2128) gần như TRÙNG màu nền đó — đo ở harness `3.6.0`: viền ô\n * rgb(30,33,40) trên nền ≈ rgb(30,34,42), cả lưới biến mất, bảng chỉ còn là chữ xếp cột. `--input` là bậc\n * viền của chính bề mặt nhập ấy; ở bảng màu sáng hai token cùng giá trị, nên light không đổi gì.\n */\nconst TABLES = [\n \"[&_.tableWrapper]:overflow-x-auto\",\n \"[&_table]:w-full [&_table]:border-collapse [&_table]:text-left\",\n \"[&_td]:border [&_td]:border-input [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top\",\n \"[&_th]:border [&_th]:border-input [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium\",\n \"[&_td>p]:my-0 [&_th>p]:my-0 [&_td>*+*]:mt-2 [&_th>*+*]:mt-2\",\n \"[&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground\"\n];\n\nconst MEDIA = [\n \"[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border\",\n // Ảnh đang tải lên — một widget decoration, không phải node (xem `image-upload.ts`): đứng thành\n // một khối riêng, mờ và nhấp nháy tới khi ảnh thật được chèn vào chỗ nó.\n \"[&_[data-upload-preview]]:my-2 [&_[data-upload-preview]]:block [&_[data-upload-preview]]:w-fit\",\n \"[&_[data-upload-preview]>img]:animate-pulse [&_[data-upload-preview]>img]:opacity-60\",\n // Node được chọn bằng bấm (ảnh, đường kẻ): viền focus của gói, không phải nền.\n \"[&_.ProseMirror-selectednode]:outline-2 [&_.ProseMirror-selectednode]:outline-offset-2 [&_.ProseMirror-selectednode]:outline-ring\"\n];\n\n/*\n * Chữ mờ khi tài liệu rỗng — `Placeholder` gắn `.is-editor-empty` + `data-placeholder` lên đoạn\n * đầu. Đoạn đang gõ dở sau `/` hoặc `@` mang `.suggestion`: tô nền nhẹ để thấy chữ nào đang là lệnh.\n * Con trỏ khe (`gapcursor`, giữa hai khối không gõ được như bảng/ảnh): CSS mặc định Tiptap chèn\n * là viền `black` — vô hình ở dark, nên đổi sang `--foreground`.\n */\nconst AFFORDANCES = [\n \"[&_.is-editor-empty:first-child]:before:pointer-events-none [&_.is-editor-empty:first-child]:before:float-left\",\n \"[&_.is-editor-empty:first-child]:before:h-0 [&_.is-editor-empty:first-child]:before:text-muted-foreground\",\n \"[&_.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]\",\n \"[&_.suggestion]:rounded-sm [&_.suggestion]:bg-muted\",\n \"[&_.ProseMirror-gapcursor]:after:border-foreground\"\n];\n\n/** Toàn bộ lớp của phần tử gốc ProseMirror (`.tiptap`). */\nexport const MARKDOWN_EDITOR_CONTENT_CLASS = [\n ...LAYOUT,\n ...HEADINGS,\n ...INLINE,\n ...LISTS,\n ...TASK_CHECKBOX,\n ...BLOCKS,\n ...SYNTAX,\n ...TABLES,\n ...MEDIA,\n ...AFFORDANCES\n].join(\" \");\n"],"mappings":"AAkBA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAS;AAAA,EACb;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AACF;AASA,MAAM,QAAQ;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAOA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AACF;AAOA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAaA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,QAAQ;AAAA,EACZ;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQA,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,gCAAgC;AAAA,EAC3C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG;","names":[]}
1
+ {"version":3,"sources":["../../src/editor/markdown-editor-classes.ts"],"sourcesContent":["/**\n * Kiểu chữ của vùng soạn — file thuần, không `\"use client\"`.\n *\n * DOM bên trong do ProseMirror dựng (không phải JSX của gói), nên không có `className` nào gắn được\n * lên từng `<h2>`, `<li>`, `<pre>`: mọi kiểu đi qua biến thể con cháu `[&_h2]:…` đặt trên PHẦN TỬ\n * GỐC của ProseMirror. Tailwind của app vẫn sinh được chúng vì chuỗi nằm nguyên văn trong `dist/`\n * (`@source \"./dist\"` của `styles.css`) — nên KHÔNG dựng chuỗi lớp động ở đây.\n *\n * Màu chỉ đi qua token VAI TRÒ, đúng hợp đồng của gói: chữ thường kế thừa `--foreground`, liên kết\n * `--link-ink`, mã `--muted`, nhắc tên `--info/15` (cùng khối của `Mention`), tô màu cú pháp các bậc\n * `-ink` — chúng là bậc dành cho CHỮ trên nền trang/muted ở cả hai bảng màu.\n */\n\n/*\n * Bố cục khối: khoảng cách giữa các khối CẤP ĐẦU bằng `> * + *` (không margin hai đầu tài liệu),\n * tiêu đề cách xa hơn phía trên để nhóm với phần nó mở. Cỡ chữ theo `Textarea`: 16px ở màn hẹp —\n * dưới 16px là Safari iOS tự phóng to trang khi chạm vào vùng soạn — 14px từ `md`.\n */\nconst LAYOUT = [\n \"min-w-0 text-base leading-relaxed text-foreground outline-none md:text-sm\",\n \"[&>*+*]:mt-3 [&>*:first-child]:mt-0\",\n \"[&>h1]:mt-6 [&>h2]:mt-5 [&>h3]:mt-4\"\n];\n\nconst HEADINGS = [\n \"[&_h1]:text-xl [&_h1]:leading-snug [&_h1]:font-semibold [&_h1]:tracking-tight\",\n \"[&_h2]:text-lg [&_h2]:leading-snug [&_h2]:font-semibold\",\n \"[&_h3]:text-base [&_h3]:leading-snug [&_h3]:font-semibold\",\n \"[&_h4]:font-semibold [&_h5]:font-semibold [&_h6]:font-semibold\"\n];\n\nconst INLINE = [\n \"[&_strong]:font-semibold\",\n // Liên kết: khuôn của `Button variant=\"link\"` / `FieldDescription` — bậc `--link-ink`, gạch dưới.\n \"[&_a]:text-link-ink [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-ink-hover\",\n \"[&_:not(pre)>code]:rounded-sm [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:py-0.5\",\n \"[&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.875em]\",\n // Nhắc tên: cùng cặp với khối `@Tên` của `Mention` (nền `info/15`, chữ thường) — đã đo ở mention.tsx.\n \"[&_[data-type=mention]]:rounded-sm [&_[data-type=mention]]:bg-info/15 [&_[data-type=mention]]:px-0.5\",\n \"[&_[data-type=mention]]:py-px [&_[data-type=mention]]:font-medium [&_[data-type=mention]]:text-foreground\",\n // Nhắc có địa chỉ (`mentions.getHref`) là `<a>`: gạch chân khi trỏ vào để biết bấm được — màu giữ nguyên.\n \"[&_a[data-type=mention]]:underline-offset-4 [&_a[data-type=mention]:hover]:underline\"\n];\n\n/*\n * Danh sách thường. Danh sách việc (`ul[data-type=taskList]`) bỏ chấm đầu dòng và tự vẽ ô tick: DOM\n * do `TaskItem` dựng là `li[data-checked] > label > input + span(sr-only)` cạnh `div` nội dung.\n * ⚠ `<li>` KHÔNG mang `data-type=\"taskItem\"`: thuộc tính đó chỉ có trong `renderHTML`, còn node view\n * (thứ thật sự vẽ trong trình soạn) không gắn nó — đã đo trên 3.31.4, selector `li[data-type=…]`\n * trượt hết và ô tick nằm một dòng, chữ rơi xuống dòng dưới. Nhắm qua `ul` cha.\n */\nconst LISTS = [\n \"[&_ul:not([data-type=taskList])]:list-disc [&_ol]:list-decimal\",\n \"[&_ul:not([data-type=taskList])]:pl-6 [&_ol]:pl-6 [&_li]:marker:text-muted-foreground\",\n \"[&_li+li]:mt-1 [&_li>ul]:mt-1 [&_li>ol]:mt-1 [&_li>p]:my-0\",\n \"[&_ul[data-type=taskList]]:list-none [&_ul[data-type=taskList]]:pl-0.5\",\n \"[&_[data-type=taskList]>li]:flex [&_[data-type=taskList]>li]:items-start [&_[data-type=taskList]>li]:gap-2\",\n \"[&_[data-type=taskList]>li>label]:flex [&_[data-type=taskList]>li>label]:h-[1.625em] [&_[data-type=taskList]>li>label]:items-center\",\n \"[&_[data-type=taskList]>li>label]:select-none [&_[data-type=taskList]>li>div]:min-w-0 [&_[data-type=taskList]>li>div]:flex-1\",\n // Soạn được: con trỏ tay trên ô tick. Chỉ đọc: ô không nhận chuột — Tiptap để nó bấm được rồi tự lật\n // lại (không có `onReadOnlyChecked`), nên một cú bấm \"không ăn\" là thứ duy nhất người dùng thấy.\n \"[&[contenteditable=true]_[data-type=taskList]>li>label>input]:cursor-pointer\",\n \"[&[contenteditable=false]_[data-type=taskList]>li>label]:pointer-events-none\",\n // Mục đã xong: chữ nhạt + gạch — CHỈ đoạn của chính mục, không lan xuống danh sách con chưa xong.\n \"[&_li[data-checked=true]>div>p]:text-muted-foreground [&_li[data-checked=true]>div>p]:line-through\"\n];\n\n/*\n * Ô tick vẽ bằng CSS trên chính `<input>` (`appearance-none`), dấu tick là một chữ L xoay 45° vẽ\n * bằng viền của `::before` — không ảnh, không SVG, không mã màu: mọi màu là token của `Checkbox`\n * (`--choice-edge` viền nghỉ, `--control-on` nền khi chọn, `--control-check` dấu tick).\n */\nconst TASK_CHECKBOX = [\n \"[&_[data-type=taskList]>li>label>input]:relative [&_[data-type=taskList]>li>label>input]:m-0 [&_[data-type=taskList]>li>label>input]:grid\",\n \"[&_[data-type=taskList]>li>label>input]:size-4 [&_[data-type=taskList]>li>label>input]:shrink-0 [&_[data-type=taskList]>li>label>input]:appearance-none\",\n \"[&_[data-type=taskList]>li>label>input]:place-content-center [&_[data-type=taskList]>li>label>input]:rounded-[4px]\",\n \"[&_[data-type=taskList]>li>label>input]:border [&_[data-type=taskList]>li>label>input]:border-choice-edge\",\n \"[&_[data-type=taskList]>li>label>input]:bg-transparent [&_[data-type=taskList]>li>label>input]:transition-colors\",\n \"[&_[data-type=taskList]>li>label>input:checked]:border-control-on [&_[data-type=taskList]>li>label>input:checked]:bg-control-on\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-2 [&_[data-type=taskList]>li>label>input:focus-visible]:outline-offset-2\",\n \"[&_[data-type=taskList]>li>label>input:focus-visible]:outline-ring\",\n // Dấu tick CHỈ tồn tại khi đã tick (`content` chỉ có ở `:checked`) — không ẩn bằng `opacity-0`: một\n // dấu vô hình vẫn là một nét mà cổng màu phải đo, và đo ra 1,00:1.\n \"[&_[data-type=taskList]>li>label>input:checked]:before:h-2 [&_[data-type=taskList]>li>label>input:checked]:before:w-1\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:-translate-y-px [&_[data-type=taskList]>li>label>input:checked]:before:rotate-45\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-r-2 [&_[data-type=taskList]>li>label>input:checked]:before:border-b-2\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:border-control-check\",\n \"[&_[data-type=taskList]>li>label>input:checked]:before:content-['']\"\n];\n\n/*\n * Trích dẫn: vạch trái bằng `--muted-foreground` ĐẶC. `--border` đúng vai trò trang trí nhưng 1,16:1\n * ở light — gần như vô hình, trích dẫn chỉ còn là một đoạn chữ xám thụt vào. Bản `/40` dễ nhìn hơn\n * nhưng 1,71:1 trên `--muted` (light) / 1,87:1 (dark): cổng chặn ở bảng tương phản cao. Bản đặc qua\n * 3:1 ở cả bốn tổ hợp bảng màu × theme (đo bằng `pnpm lint:a11y`).\n */\nconst BLOCKS = [\n \"[&_blockquote]:border-l-2 [&_blockquote]:border-muted-foreground [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground\",\n \"[&_blockquote>*+*]:mt-2\",\n // Đường kẻ và lưới bảng (dưới) dùng `--input`, không `--border` — lý do ở chỗ khai `TABLES`.\n \"[&_hr]:my-1 [&_hr]:border-input\",\n // Khối mã XUỐNG DÒNG (`pre-wrap`) thay vì cuộn ngang: trong trình soạn, một thanh cuộn ngang lồng\n // trong khung cuộn dọc là chỗ con trỏ biến mất khỏi tầm nhìn khi gõ. Đó cũng là CSS Tiptap chèn sẵn\n // (`.ProseMirror pre`) — bản trước ghi `whitespace-pre` ở đây, và nó thua luật đó mà không ai thấy.\n \"[&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-4 [&_pre]:py-3\",\n \"[&_pre]:font-mono [&_pre]:text-[0.8125rem] [&_pre]:leading-relaxed [&_pre]:whitespace-pre-wrap\",\n \"[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit\"\n];\n\n/*\n * Tô màu cú pháp (lớp `hljs-*` do lowlight sinh). Phân vai kiểu bảng màu sáng quen thuộc của các\n * trình xem mã: từ khoá đỏ, chuỗi xanh đậm, hằng/số xanh ngọc, tên hàm cam, chú thích nhạt + nghiêng.\n * Toàn bậc `-ink`, nên đổi sang dark hay bảng tương phản cao là token tự đổi theo.\n */\nconst SYNTAX = [\n \"[&_.hljs-comment]:text-muted-foreground [&_.hljs-comment]:italic [&_.hljs-quote]:text-muted-foreground\",\n \"[&_.hljs-keyword]:text-red-ink [&_.hljs-selector-tag]:text-red-ink [&_.hljs-doctag]:text-red-ink\",\n \"[&_.hljs-meta_.hljs-keyword]:text-red-ink [&_.hljs-template-tag]:text-red-ink\",\n \"[&_.hljs-string]:text-navy-ink [&_.hljs-regexp]:text-navy-ink [&_.hljs-meta_.hljs-string]:text-navy-ink\",\n \"[&_.hljs-number]:text-teal-ink [&_.hljs-literal]:text-teal-ink [&_.hljs-attr]:text-teal-ink\",\n \"[&_.hljs-attribute]:text-teal-ink [&_.hljs-variable]:text-teal-ink [&_.hljs-template-variable]:text-teal-ink\",\n \"[&_.hljs-selector-attr]:text-teal-ink [&_.hljs-selector-class]:text-teal-ink [&_.hljs-selector-id]:text-teal-ink\",\n \"[&_.hljs-title]:text-orange-ink [&_.hljs-section]:text-orange-ink [&_.hljs-name]:text-orange-ink\",\n \"[&_.hljs-type]:text-orange-ink [&_.hljs-built_in]:text-orange-ink [&_.hljs-symbol]:text-orange-ink\",\n \"[&_.hljs-meta]:text-muted-foreground [&_.hljs-bullet]:text-orange-ink [&_.hljs-link]:underline\",\n \"[&_.hljs-addition]:text-success-ink [&_.hljs-deletion]:text-destructive-ink\",\n \"[&_.hljs-emphasis]:italic [&_.hljs-strong]:font-semibold\"\n];\n\n/*\n * Bảng. Ô bảng của `MarkdownEditor` chỉ chứa MỘT đoạn văn (xem `ComitorTableCell`); ô của\n * `RichTextEditor` chứa được nhiều khối (đoạn, danh sách, ảnh) — `> * + *` cho chúng một nhịp ngắn\n * hơn nhịp của tài liệu, như khối trong trích dẫn.\n * Cuộn ngang ở `.tableWrapper` (Tiptap bọc sẵn) — bảng rộng không được đẩy cả trang tràn ngang ở 375px.\n *\n * Lưới là `border-input`, không `border-border`: khung `outline` ở dark nằm trên `--input-fill` (`--input`\n * 30%), mà `--border` của dark (#1e2128) gần như TRÙNG màu nền đó — đo ở harness `3.6.0`: viền ô\n * rgb(30,33,40) trên nền ≈ rgb(30,34,42), cả lưới biến mất, bảng chỉ còn là chữ xếp cột. `--input` là bậc\n * viền của chính bề mặt nhập ấy; ở bảng màu sáng hai token cùng giá trị, nên light không đổi gì.\n */\nconst TABLES = [\n \"[&_.tableWrapper]:overflow-x-auto\",\n \"[&_table]:w-full [&_table]:border-collapse [&_table]:text-left\",\n \"[&_td]:border [&_td]:border-input [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top\",\n \"[&_th]:border [&_th]:border-input [&_th]:bg-muted [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:align-top [&_th]:font-medium\",\n \"[&_td>p]:my-0 [&_th>p]:my-0 [&_td>*+*]:mt-2 [&_th>*+*]:mt-2\",\n \"[&_.selectedCell]:bg-accent [&_.selectedCell]:text-accent-foreground\"\n];\n\nconst MEDIA = [\n \"[&_img]:h-auto [&_img]:max-w-full [&_img]:rounded-md [&_img]:border [&_img]:border-border\",\n // Ảnh đang tải lên — một widget decoration, không phải node (xem `image-upload.ts`): đứng thành\n // một khối riêng, mờ và nhấp nháy tới khi ảnh thật được chèn vào chỗ nó.\n \"[&_[data-upload-preview]]:my-2 [&_[data-upload-preview]]:block [&_[data-upload-preview]]:w-fit\",\n \"[&_[data-upload-preview]>img]:animate-pulse [&_[data-upload-preview]>img]:opacity-60\",\n // Node được chọn bằng bấm (ảnh, đường kẻ): viền focus của gói, không phải nền.\n \"[&_.ProseMirror-selectednode]:outline-2 [&_.ProseMirror-selectednode]:outline-offset-2 [&_.ProseMirror-selectednode]:outline-ring\"\n];\n\n/*\n * Chữ mờ khi tài liệu rỗng — `Placeholder` gắn `.is-editor-empty` + `data-placeholder` lên đoạn\n * đầu. Đoạn đang gõ dở sau `/` hoặc `@` mang `.suggestion`: tô nền nhẹ để thấy chữ nào đang là lệnh.\n * Con trỏ khe (`gapcursor`, giữa hai khối không gõ được như bảng/ảnh): CSS mặc định Tiptap chèn\n * là viền `black` — vô hình ở dark, nên đổi sang `--foreground`.\n */\nconst AFFORDANCES = [\n \"[&_.is-editor-empty:first-child]:before:pointer-events-none [&_.is-editor-empty:first-child]:before:float-left\",\n \"[&_.is-editor-empty:first-child]:before:h-0 [&_.is-editor-empty:first-child]:before:text-muted-foreground\",\n \"[&_.is-editor-empty:first-child]:before:content-[attr(data-placeholder)]\",\n \"[&_.suggestion]:rounded-sm [&_.suggestion]:bg-muted\",\n \"[&_.ProseMirror-gapcursor]:after:border-foreground\"\n];\n\n/** Toàn bộ lớp của phần tử gốc ProseMirror (`.tiptap`). */\nexport const MARKDOWN_EDITOR_CONTENT_CLASS = [\n ...LAYOUT,\n ...HEADINGS,\n ...INLINE,\n ...LISTS,\n ...TASK_CHECKBOX,\n ...BLOCKS,\n ...SYNTAX,\n ...TABLES,\n ...MEDIA,\n ...AFFORDANCES\n].join(\" \");\n"],"mappings":"AAkBA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,WAAW;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,SAAS;AAAA,EACb;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AASA,MAAM,QAAQ;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAOA,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AACF;AAOA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAaA,MAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,QAAQ;AAAA,EACZ;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA;AAAA,EAEA;AACF;AAQA,MAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,MAAM,gCAAgC;AAAA,EAC3C,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AACL,EAAE,KAAK,GAAG;","names":[]}
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
- import { g as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BMST7-VR.js';
2
+ import { g as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-CSrMeuSw.js';
3
3
  import '@tiptap/core';
4
+ import '../components/composite/empty-state.js';
4
5
  import './markdown-editor-constants.js';
5
6
 
6
7
  /**
@@ -1,7 +1,9 @@
1
1
  import { Extension, AnyExtension } from '@tiptap/core';
2
2
  import { MentionOptions } from '@tiptap/extension-mention';
3
3
  import { Slice } from '@tiptap/pm/model';
4
- import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
4
+ import { f as EditorFormat } from '../editor-types-CSrMeuSw.js';
5
+ import '../components/composite/empty-state.js';
6
+ import 'react';
5
7
  import './markdown-editor-constants.js';
6
8
 
7
9
  /**
@@ -30,8 +32,12 @@ interface MarkdownEditorRuntime {
30
32
  taskCheckbox: (text: string) => string;
31
33
  /** Đổi `src` ĐÃ LƯU thành địa chỉ HIỂN THỊ được (vd URL đã ký). Markdown vẫn giữ `src` gốc. */
32
34
  resolveImageSrc?: (src: string) => string;
33
- /** 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. */
34
- resolveMentionLabel?: (id: string) => string | undefined;
35
+ /**
36
+ * Tên SỐNG theo id (và loại — `undefined` là người). `undefined` thì rơi về tên chụp lúc nhắc.
37
+ */
38
+ resolveMentionLabel?: (id: string, kind?: string) => string | undefined;
39
+ /** Địa chỉ mở khi bấm một lần nhắc (vd `/tasks/<id>`). `undefined` = không phải liên kết. */
40
+ resolveMentionHref?: (id: string, kind?: string) => string | undefined;
35
41
  }
36
42
  interface MarkdownSchemaOptions {
37
43
  /** Định dạng LƯU — quyết định lược đồ. Mặc định `markdown`. */
@@ -209,8 +209,23 @@ function createMentionExtension(runtime, suggestion) {
209
209
  const stored = typeof attrs.label === "string" ? attrs.label : "";
210
210
  return stored || (typeof attrs.id === "string" ? attrs.id : "");
211
211
  };
212
- const labelOf = (attrs) => runtime.current.resolveMentionLabel?.(typeof attrs.id === "string" ? attrs.id : "") ?? storedLabelOf(attrs);
212
+ const kindOf = (attrs) => typeof attrs.kind === "string" && attrs.kind ? attrs.kind : void 0;
213
+ const labelOf = (attrs) => runtime.current.resolveMentionLabel?.(typeof attrs.id === "string" ? attrs.id : "", kindOf(attrs)) ?? storedLabelOf(attrs);
213
214
  return Mention.extend({
215
+ /*
216
+ * `kind` (từ 3.7.0): loại thực thể — `null` là người. Ra HTML thành `data-kind` (lưu được ở định dạng
217
+ * HTML và để app tô khác theo loại), ra markdown thành tiền tố `kind/` của địa chỉ.
218
+ */
219
+ addAttributes() {
220
+ return {
221
+ ...this.parent?.(),
222
+ kind: {
223
+ default: null,
224
+ parseHTML: (element) => element.getAttribute("data-kind") || null,
225
+ renderHTML: (attributes) => typeof attributes.kind === "string" && attributes.kind ? { "data-kind": attributes.kind } : {}
226
+ }
227
+ };
228
+ },
214
229
  markdownTokenName: "mention",
215
230
  markdownTokenizer: {
216
231
  name: "mention",
@@ -218,15 +233,23 @@ function createMentionExtension(runtime, suggestion) {
218
233
  start: (source) => source.indexOf("[@"),
219
234
  tokenize: (source) => {
220
235
  const parsed = parseMentionMarkdown(source);
221
- return parsed ? { type: "mention", raw: parsed.raw, id: parsed.id, label: parsed.label } : void 0;
236
+ return parsed ? { type: "mention", raw: parsed.raw, id: parsed.id, label: parsed.label, kind: parsed.kind ?? null } : void 0;
222
237
  }
223
238
  },
224
- parseMarkdown: (token, helpers) => helpers.createNode("mention", { id: token.id, label: token.label }),
225
- renderMarkdown: (node) => renderMentionMarkdown(String(node.attrs?.id ?? ""), node.attrs?.label),
239
+ parseMarkdown: (token, helpers) => helpers.createNode("mention", { id: token.id, label: token.label, kind: token.kind ?? null }),
240
+ renderMarkdown: (node) => renderMentionMarkdown(
241
+ String(node.attrs?.id ?? ""),
242
+ node.attrs?.label,
243
+ node.attrs?.kind
244
+ ),
226
245
  addNodeView() {
227
246
  return ({ node, HTMLAttributes }) => {
228
- const dom = document.createElement("span");
247
+ const id = typeof node.attrs.id === "string" ? node.attrs.id : "";
248
+ const href = runtime.current.resolveMentionHref?.(id, kindOf(node.attrs));
249
+ const safeHref = href && /^(?:\/(?![/\\])|https?:\/\/)/i.test(href) ? href : void 0;
250
+ const dom = document.createElement(safeHref ? "a" : "span");
229
251
  setAttributes(dom, mergeAttributes({ "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes));
252
+ if (safeHref) dom.setAttribute("href", safeHref);
230
253
  dom.textContent = `@${labelOf(node.attrs)}`;
231
254
  return { dom, update: (updated) => sameNode(updated, node) };
232
255
  };
@@ -1 +1 @@
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":[]}
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 /**\n * Tên SỐNG theo id (và loại — `undefined` là người). `undefined` thì rơi về tên chụp lúc nhắc.\n */\n resolveMentionLabel?: (id: string, kind?: string) => string | undefined;\n /** Địa chỉ mở khi bấm một lần nhắc (vd `/tasks/<id>`). `undefined` = không phải liên kết. */\n resolveMentionHref?: (id: string, kind?: 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 kindOf = (attrs: Record<string, unknown>) =>\n typeof attrs.kind === \"string\" && attrs.kind ? attrs.kind : undefined;\n const labelOf = (attrs: Record<string, unknown>) =>\n runtime.current.resolveMentionLabel?.(typeof attrs.id === \"string\" ? attrs.id : \"\", kindOf(attrs)) ??\n storedLabelOf(attrs);\n\n return Mention.extend({\n /*\n * `kind` (từ 3.7.0): loại thực thể — `null` là người. Ra HTML thành `data-kind` (lưu được ở định dạng\n * HTML và để app tô khác theo loại), ra markdown thành tiền tố `kind/` của địa chỉ.\n */\n addAttributes() {\n return {\n ...this.parent?.(),\n kind: {\n default: null,\n parseHTML: (element: HTMLElement) => element.getAttribute(\"data-kind\") || null,\n renderHTML: (attributes: Record<string, unknown>) =>\n typeof attributes.kind === \"string\" && attributes.kind ? { \"data-kind\": attributes.kind } : {}\n }\n };\n },\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\n ? { type: \"mention\", raw: parsed.raw, id: parsed.id, label: parsed.label, kind: parsed.kind ?? null }\n : undefined;\n }\n },\n parseMarkdown: (token, helpers) =>\n helpers.createNode(\"mention\", { id: token.id, label: token.label, kind: token.kind ?? null }),\n renderMarkdown: (node: JSONContent) =>\n renderMentionMarkdown(\n String(node.attrs?.id ?? \"\"),\n node.attrs?.label as string | null | undefined,\n node.attrs?.kind as string | null | undefined\n ),\n addNodeView() {\n return ({ node, HTMLAttributes }) => {\n /*\n * Có địa chỉ (`mentions.getHref`) thì vẽ `<a>`: nhắc VIỆC mở được việc đó. Chỉ nhận đường dẫn tương đối\n * cùng gốc hoặc `http(s):` — địa chỉ do app dựng, nhưng một `javascript:` lọt vào đây là chạy được.\n * Chỉ ở node VIEW: thứ GHI xuống (`renderHTML`, markdown) không mang địa chỉ nào.\n */\n const id = typeof node.attrs.id === \"string\" ? node.attrs.id : \"\";\n const href = runtime.current.resolveMentionHref?.(id, kindOf(node.attrs));\n const safeHref = href && /^(?:\\/(?![/\\\\])|https?:\\/\\/)/i.test(href) ? href : undefined;\n const dom = document.createElement(safeHref ? \"a\" : \"span\");\n setAttributes(dom, mergeAttributes({ \"data-type\": this.name }, this.options.HTMLAttributes, HTMLAttributes));\n if (safeHref) dom.setAttribute(\"href\", safeHref);\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":[]}
@@ -71,12 +71,14 @@ declare const MENTION_URL_SCHEME = "mention:";
71
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
72
  * một liên kết thường. Mã hoá nốt hai ký tự đó.
73
73
  */
74
- declare function renderMentionMarkdown(id: string, label?: string | null): string;
74
+ declare function renderMentionMarkdown(id: string, label?: string | null, kind?: string | null): string;
75
75
  interface ParsedMention {
76
76
  /** Toàn bộ đoạn đã khớp — để bộ tách từ (tokenizer) biết đã ăn bao nhiêu ký tự */
77
77
  raw: string;
78
78
  id: string;
79
79
  label: string;
80
+ /** Loại thực thể; `undefined` = người (dạng của 3.5.0) */
81
+ kind?: string;
80
82
  }
81
83
  /**
82
84
  * Đọ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á
@@ -27,21 +27,27 @@ function imageMarkdown(src, alt, title) {
27
27
  return `![${text}](${destination}${caption})`;
28
28
  }
29
29
  const MENTION_URL_SCHEME = "mention:";
30
+ const MENTION_KIND = /^[a-z][a-z0-9-]{0,31}$/;
30
31
  const MENTION_LINK = /^\[@((?:\\.|[^\\\]\n])*)\]\(mention:([^()\s]+)\)/;
31
32
  function escapeMentionLabel(label) {
32
33
  return label.replace(/[\\[\]]/g, "\\$&");
33
34
  }
34
- function renderMentionMarkdown(id, label) {
35
+ function renderMentionMarkdown(id, label, kind) {
35
36
  const encodedId = encodeURIComponent(id).replace(/\(/g, "%28").replace(/\)/g, "%29");
36
- return `[@${escapeMentionLabel(label || id)}](${MENTION_URL_SCHEME}${encodedId})`;
37
+ const prefix = kind && MENTION_KIND.test(kind) ? `${kind}/` : "";
38
+ return `[@${escapeMentionLabel(label || id)}](${MENTION_URL_SCHEME}${prefix}${encodedId})`;
37
39
  }
38
40
  function parseMentionMarkdown(source) {
39
41
  const match = MENTION_LINK.exec(source);
40
42
  if (!match?.[2]) return void 0;
43
+ const slash = match[2].indexOf("/");
44
+ const kind = slash === -1 ? void 0 : match[2].slice(0, slash);
45
+ if (kind !== void 0 && !MENTION_KIND.test(kind)) return void 0;
41
46
  try {
42
- const id = decodeURIComponent(match[2]);
47
+ const id = decodeURIComponent(slash === -1 ? match[2] : match[2].slice(slash + 1));
48
+ if (!id) return void 0;
43
49
  const label = (match[1] ?? "").replace(/\\(.)/g, "$1");
44
- return { raw: match[0], id, label: label || id };
50
+ return kind ? { raw: match[0], id, label: label || id, kind } : { raw: match[0], id, label: label || id };
45
51
  } catch {
46
52
  return void 0;
47
53
  }
@@ -1 +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":[]}
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 * ── LOẠI THỰC THỂ (`kind`, từ 3.7.0) ──────────────────────────────────────────────────────────\n * Nhắc NGƯỜI giữ nguyên dạng của 3.5.0: `[@Tên](mention:<id>)`. Nhắc thứ khác (việc, dự án…) mang\n * loại đứng trước id: `[@KEY-12](mention:task/<id>)`. Không mơ hồ, và tương thích ngược: id luôn đi qua\n * `encodeURIComponent` nên một `/` TRẦN trong địa chỉ không bao giờ đến từ id — có `/` là có loại.\n * Loại là chuỗi máy đọc (`MENTION_KIND`): chữ thường, số, gạch ngang — đọc gặp loại không hợp lệ thì\n * giữ nguyên chuỗi là chữ thường, như mọi ca hỏng khác.\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. Địa chỉ: 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_KIND = /^[a-z][a-z0-9-]{0,31}$/;\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, kind?: string | null): string {\n const encodedId = encodeURIComponent(id).replace(/\\(/g, \"%28\").replace(/\\)/g, \"%29\");\n // Loại không hợp lệ thì bỏ (rơi về nhắc NGƯỜI) chứ không ghi ra một chuỗi mà chính gói đọc không lại.\n const prefix = kind && MENTION_KIND.test(kind) ? `${kind}/` : \"\";\n return `[@${escapeMentionLabel(label || id)}](${MENTION_URL_SCHEME}${prefix}${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 /** Loại thực thể; `undefined` = người (dạng của 3.5.0) */\n kind?: 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 const slash = match[2].indexOf(\"/\");\n const kind = slash === -1 ? undefined : match[2].slice(0, slash);\n if (kind !== undefined && !MENTION_KIND.test(kind)) return undefined;\n try {\n const id = decodeURIComponent(slash === -1 ? match[2] : match[2].slice(slash + 1));\n if (!id) return undefined;\n const label = (match[1] ?? \"\").replace(/\\\\(.)/g, \"$1\");\n return kind ? { raw: match[0], id, label: label || id, kind } : { 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;AAalC,MAAM,eAAe;AACrB,MAAM,eAAe;AAGrB,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MAAM,QAAQ,YAAY,MAAM;AACzC;AASO,SAAS,sBAAsB,IAAY,OAAuB,MAA8B;AACrG,QAAM,YAAY,mBAAmB,EAAE,EAAE,QAAQ,OAAO,KAAK,EAAE,QAAQ,OAAO,KAAK;AAEnF,QAAM,SAAS,QAAQ,aAAa,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM;AAC9D,SAAO,KAAK,mBAAmB,SAAS,EAAE,CAAC,KAAK,kBAAkB,GAAG,MAAM,GAAG,SAAS;AACzF;AAeO,SAAS,qBAAqB,QAA2C;AAC9E,QAAM,QAAQ,aAAa,KAAK,MAAM;AACtC,MAAI,CAAC,QAAQ,CAAC,EAAG,QAAO;AACxB,QAAM,QAAQ,MAAM,CAAC,EAAE,QAAQ,GAAG;AAClC,QAAM,OAAO,UAAU,KAAK,SAAY,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK;AAC/D,MAAI,SAAS,UAAa,CAAC,aAAa,KAAK,IAAI,EAAG,QAAO;AAC3D,MAAI;AACF,UAAM,KAAK,mBAAmB,UAAU,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,EAAE,MAAM,QAAQ,CAAC,CAAC;AACjF,QAAI,CAAC,GAAI,QAAO;AAChB,UAAM,SAAS,MAAM,CAAC,KAAK,IAAI,QAAQ,UAAU,IAAI;AACrD,WAAO,OAAO,EAAE,KAAK,MAAM,CAAC,GAAG,IAAI,OAAO,SAAS,IAAI,KAAK,IAAI,EAAE,KAAK,MAAM,CAAC,GAAG,IAAI,OAAO,SAAS,GAAG;AAAA,EAC1G,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
- import { g as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-BMST7-VR.js';
2
+ import { g as EditorBaseProps, a as EditorToolbarMode, E as EditorHandleBase } from '../editor-types-CSrMeuSw.js';
3
3
  import '@tiptap/core';
4
+ import '../components/composite/empty-state.js';
4
5
  import './markdown-editor-constants.js';
5
6
 
6
7
  /**
@@ -2,8 +2,10 @@ 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
+ import { f as EditorFormat } from '../editor-types-CSrMeuSw.js';
6
6
  import { MarkdownEditorBlockId, MarkdownEditorLabels } from './markdown-editor-constants.js';
7
+ import '../components/composite/empty-state.js';
8
+ import 'react';
7
9
 
8
10
  /**
9
11
  * Menu `/` — danh sách khối chèn được, và extension Tiptap mở nó.
@@ -1,7 +1,9 @@
1
1
  import { Editor } from '@tiptap/core';
2
2
  import { LucideIcon } from 'lucide-react';
3
- import { f as EditorFormat } from '../editor-types-BMST7-VR.js';
3
+ import { f as EditorFormat } from '../editor-types-CSrMeuSw.js';
4
4
  import { MarkdownEditorLabels } from './markdown-editor-constants.js';
5
+ import '../components/composite/empty-state.js';
6
+ import 'react';
5
7
 
6
8
  /**
7
9
  * Hành động của thanh công cụ — MỘT bảng cho cả thanh nổi (`bubble-toolbar.tsx`) lẫn thanh cố định
@@ -1,4 +1,5 @@
1
1
  import { Extension, Editor, AnyExtension } from '@tiptap/core';
2
+ import { IconComponent } from './components/composite/empty-state.js';
2
3
  import { MarkdownEditorLabels } from './editor/markdown-editor-constants.js';
3
4
 
4
5
  /**
@@ -74,15 +75,23 @@ type EditorFormat = "markdown" | "html";
74
75
  * không thuộc phím tắt hay cú pháp gõ tắt. Ở chế độ này bong bóng chỉ còn phần LIÊN KẾT.
75
76
  */
76
77
  type EditorToolbarMode = "bubble" | "fixed";
77
- /** Một người trong danh sách `@`. */
78
+ /** Một mục trong danh sách `@` — một người, hoặc (có `kind`) một thực thể khác. */
78
79
  interface MarkdownEditorMentionItem {
79
- /** Định danh LƯU vào nội dung — với module Comitor là id người dùng của Account */
80
+ /** Định danh LƯU vào nội dung — với người của module Comitor là id người dùng của Account */
80
81
  id: string;
81
82
  /** Tên hiển thị lúc chọn — cũng là ảnh chụp tên ghi kèm trong nội dung */
82
83
  label: string;
83
84
  /** Dòng phụ mờ bên phải (email, chức danh) — cũng được dùng để lọc */
84
85
  description?: string;
85
86
  avatarUrl?: string | null;
87
+ /**
88
+ * Loại thực thể (từ 3.7.0) — `"task"`, `"project"`… Bỏ trống = NGƯỜI. Lưu thành tiền tố của địa chỉ:
89
+ * `[@KEY-12](mention:task/<id>)`; nhắc người giữ dạng cũ `[@Tên](mention:<id>)`. Chuỗi máy đọc: chữ
90
+ * thường, số, gạch ngang, tối đa 32 ký tự — giá trị khác bị bỏ (lưu như nhắc người).
91
+ */
92
+ kind?: string;
93
+ /** Icon vẽ thay avatar trong menu `@` — cho mục không phải người (việc, dự án). */
94
+ icon?: IconComponent;
86
95
  }
87
96
  /**
88
97
  * Nguồn danh sách `@`:
@@ -98,8 +107,16 @@ interface MarkdownEditorMentions {
98
107
  /**
99
108
  * Tên SỐNG theo id, dùng lúc VẼ. Nội dung giữ tên chụp lúc nhắc; người đó đổi tên ở Account thì
100
109
  * hàm này cho tên mới mà không phải sửa nội dung. `undefined` → rơi về tên chụp.
110
+ * `kind` là loại của lần nhắc (`undefined` = người) — id của hai loại khác nhau có thể trùng nhau.
111
+ */
112
+ resolveLabel?: (id: string, kind?: string) => string | undefined;
113
+ /**
114
+ * Địa chỉ của một lần nhắc (từ 3.7.0) — có thì nhắc tên vẽ thành LIÊN KẾT (vd nhắc việc → `/tasks/<id>`).
115
+ * Chỉ nhận đường dẫn tương đối cùng gốc (`/…`) hoặc `http(s)://`; giá trị khác bị bỏ. Không ghi vào nội dung.
116
+ * ⚠ Là `<a href>` thường: ở Next, bấm vào là tải lại trang (không qua router) — đổi lấy việc không phải gắn
117
+ * listener vào trình soạn.
101
118
  */
102
- resolveLabel?: (id: string) => string | undefined;
119
+ getHref?: (id: string, kind?: string) => string | undefined;
103
120
  }
104
121
  type MarkdownEditorVariant = "outline" | "ghost";
105
122
  /** Phần điều khiển chung của hai handle — mỗi trình soạn thêm cặp đọc/ghi theo định dạng của nó. */
@@ -14,6 +14,7 @@ import './namespaces/rating.js';
14
14
  import './namespaces/pagination.js';
15
15
  import './namespaces/mention.js';
16
16
  import '../editor/markdown-editor-constants.js';
17
+ import './namespaces/keyboard-shortcuts.js';
17
18
  import './namespaces/inline-edit.js';
18
19
  import './namespaces/filter-chips.js';
19
20
  import '../uploader/file-upload-constants.js';
@@ -9,6 +9,7 @@ import { RatingMessages } from './namespaces/rating.js';
9
9
  import { PaginationMessages } from './namespaces/pagination.js';
10
10
  import { MentionMessages } from './namespaces/mention.js';
11
11
  import { MarkdownEditorLabels } from '../editor/markdown-editor-constants.js';
12
+ import { KeyboardShortcutsMessages } from './namespaces/keyboard-shortcuts.js';
12
13
  import { InlineEditMessages } from './namespaces/inline-edit.js';
13
14
  import { FilterChipsMessages } from './namespaces/filter-chips.js';
14
15
  import { FileUploadLabels, ImageUploadFieldLabels } from '../uploader/file-upload-constants.js';
@@ -71,6 +72,7 @@ declare const comitorMessagesEn: {
71
72
  filterChips: FilterChipsMessages;
72
73
  imageUploadField: ImageUploadFieldLabels;
73
74
  inlineEdit: InlineEditMessages;
75
+ keyboardShortcuts: KeyboardShortcutsMessages;
74
76
  kanban: KanbanLabels;
75
77
  keyValue: KeyValueLabels;
76
78
  markdownEditor: MarkdownEditorLabels;
@@ -23,6 +23,7 @@ import { imageUploadFieldEn, imageUploadFieldVi } from "./namespaces/image-uploa
23
23
  import { inlineEditEn, inlineEditVi } from "./namespaces/inline-edit.js";
24
24
  import { kanbanEn, kanbanVi } from "./namespaces/kanban.js";
25
25
  import { keyValueEn, keyValueVi } from "./namespaces/key-value.js";
26
+ import { keyboardShortcutsEn, keyboardShortcutsVi } from "./namespaces/keyboard-shortcuts.js";
26
27
  import { markdownEditorEn, markdownEditorVi } from "./namespaces/markdown-editor.js";
27
28
  import { mentionEn, mentionVi } from "./namespaces/mention.js";
28
29
  import { paginationEn, paginationVi } from "./namespaces/pagination.js";
@@ -63,6 +64,7 @@ const comitorMessagesEn = {
63
64
  filterChips: filterChipsEn,
64
65
  imageUploadField: imageUploadFieldEn,
65
66
  inlineEdit: inlineEditEn,
67
+ keyboardShortcuts: keyboardShortcutsEn,
66
68
  kanban: kanbanEn,
67
69
  keyValue: keyValueEn,
68
70
  markdownEditor: markdownEditorEn,
@@ -106,6 +108,7 @@ const comitorMessagesVi = {
106
108
  filterChips: filterChipsVi,
107
109
  imageUploadField: imageUploadFieldVi,
108
110
  inlineEdit: inlineEditVi,
111
+ keyboardShortcuts: keyboardShortcutsVi,
109
112
  kanban: kanbanVi,
110
113
  keyValue: keyValueVi,
111
114
  markdownEditor: markdownEditorVi,