render tables in markdown

This commit is contained in:
Ramnique Singh 2026-04-20 10:43:27 +05:30
parent 0d71ad33f5
commit 8e0a3e2991
4 changed files with 75 additions and 0 deletions

View file

@ -28,6 +28,7 @@
"@tiptap/extension-image": "^3.16.0", "@tiptap/extension-image": "^3.16.0",
"@tiptap/extension-link": "^3.15.3", "@tiptap/extension-link": "^3.15.3",
"@tiptap/extension-placeholder": "^3.15.3", "@tiptap/extension-placeholder": "^3.15.3",
"@tiptap/extension-table": "^3.22.4",
"@tiptap/extension-task-item": "^3.15.3", "@tiptap/extension-task-item": "^3.15.3",
"@tiptap/extension-task-list": "^3.15.3", "@tiptap/extension-task-list": "^3.15.3",
"@tiptap/pm": "^3.15.3", "@tiptap/pm": "^3.15.3",

View file

@ -7,6 +7,8 @@ import Image from '@tiptap/extension-image'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import TaskList from '@tiptap/extension-task-list' import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item' import TaskItem from '@tiptap/extension-task-item'
import { TableKit, renderTableToMarkdown } from '@tiptap/extension-table'
import type { JSONContent, MarkdownRendererHelpers } from '@tiptap/react'
import { ImageUploadPlaceholderExtension, createImageUploadHandler } from '@/extensions/image-upload' import { ImageUploadPlaceholderExtension, createImageUploadHandler } from '@/extensions/image-upload'
import { TaskBlockExtension } from '@/extensions/task-block' import { TaskBlockExtension } from '@/extensions/task-block'
import { TrackBlockExtension } from '@/extensions/track-block' import { TrackBlockExtension } from '@/extensions/track-block'
@ -149,6 +151,17 @@ function serializeList(listNode: JsonNode, indent: number): string[] {
return lines return lines
} }
// Adapter for tiptap's first-party renderTableToMarkdown. Only renderChildren is
// actually invoked — the other helpers are stubs to satisfy the type.
const tableRenderHelpers: MarkdownRendererHelpers = {
renderChildren: (nodes) => {
const arr = Array.isArray(nodes) ? nodes : [nodes]
return arr.map(n => n.type === 'paragraph' ? nodeToText(n as JsonNode) : '').join('')
},
wrapInBlock: (prefix, content) => prefix + content,
indent: (content) => content,
}
// Serialize a single top-level block to its markdown string. Empty paragraphs (or blank-marker // Serialize a single top-level block to its markdown string. Empty paragraphs (or blank-marker
// paragraphs) return '' to signal "blank line slot" for the join logic in serializeBlocksToMarkdown. // paragraphs) return '' to signal "blank line slot" for the join logic in serializeBlocksToMarkdown.
function blockToMarkdown(node: JsonNode): string { function blockToMarkdown(node: JsonNode): string {
@ -192,6 +205,8 @@ function blockToMarkdown(node: JsonNode): string {
return '```transcript\n' + (node.attrs?.data as string || '{}') + '\n```' return '```transcript\n' + (node.attrs?.data as string || '{}') + '\n```'
case 'mermaidBlock': case 'mermaidBlock':
return '```mermaid\n' + (node.attrs?.data as string || '') + '\n```' return '```mermaid\n' + (node.attrs?.data as string || '') + '\n```'
case 'table':
return renderTableToMarkdown(node as JSONContent, tableRenderHelpers).trim()
case 'codeBlock': { case 'codeBlock': {
const lang = (node.attrs?.language as string) || '' const lang = (node.attrs?.language as string) || ''
return '```' + lang + '\n' + nodeToText(node) + '\n```' return '```' + lang + '\n' + nodeToText(node) + '\n```'
@ -697,6 +712,9 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
TaskItem.configure({ TaskItem.configure({
nested: true, nested: true,
}), }),
TableKit.configure({
table: { resizable: false },
}),
Placeholder.configure({ Placeholder.configure({
placeholder, placeholder,
}), }),

View file

@ -146,6 +146,48 @@
color: #eb5757; color: #eb5757;
} }
/* Native GFM tables (distinct from the custom tableBlock above) */
.tiptap-editor .ProseMirror .tableWrapper {
overflow-x: auto;
margin: 8px 0;
}
.tiptap-editor .ProseMirror table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 13px;
margin: 8px 0;
}
.tiptap-editor .ProseMirror table th,
.tiptap-editor .ProseMirror table td {
border: 1px solid var(--border);
padding: 6px 10px;
vertical-align: top;
box-sizing: border-box;
position: relative;
min-width: 60px;
}
.tiptap-editor .ProseMirror table th {
background: color-mix(in srgb, var(--foreground) 4%, transparent);
font-weight: 600;
text-align: left;
}
.tiptap-editor .ProseMirror table p {
margin: 0;
}
.tiptap-editor .ProseMirror table .selectedCell::after {
content: '';
position: absolute;
inset: 0;
background: color-mix(in srgb, var(--foreground) 8%, transparent);
pointer-events: none;
}
/* Divider */ /* Divider */
.tiptap-editor .ProseMirror hr { .tiptap-editor .ProseMirror hr {
border: none; border: none;

14
apps/x/pnpm-lock.yaml generated
View file

@ -184,6 +184,9 @@ importers:
'@tiptap/extension-placeholder': '@tiptap/extension-placeholder':
specifier: ^3.15.3 specifier: ^3.15.3
version: 3.15.3(@tiptap/extensions@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)) version: 3.15.3(@tiptap/extensions@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
'@tiptap/extension-table':
specifier: ^3.22.4
version: 3.22.4(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)
'@tiptap/extension-task-item': '@tiptap/extension-task-item':
specifier: ^3.15.3 specifier: ^3.15.3
version: 3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)) version: 3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))
@ -3166,6 +3169,12 @@ packages:
peerDependencies: peerDependencies:
'@tiptap/core': ^3.15.3 '@tiptap/core': ^3.15.3
'@tiptap/extension-table@3.22.4':
resolution: {integrity: sha512-kjvLv3Z4JI+1tLDqZKa+bKU8VcxY+ZOyMCKWQA7wYmy8nKWkLJ60W+xy8AcXXpHB2goCIgSFLhsTyswx0GXH4w==}
peerDependencies:
'@tiptap/core': 3.22.4
'@tiptap/pm': 3.22.4
'@tiptap/extension-task-item@3.15.3': '@tiptap/extension-task-item@3.15.3':
resolution: {integrity: sha512-bkrmouc1rE5n9ONw2G7+zCGfBRoF2HJWq8REThPMzg/6+L5GJJ5YTN4UmncaP48U9jHX8xeihjgg9Ypenjl4lw==} resolution: {integrity: sha512-bkrmouc1rE5n9ONw2G7+zCGfBRoF2HJWq8REThPMzg/6+L5GJJ5YTN4UmncaP48U9jHX8xeihjgg9Ypenjl4lw==}
peerDependencies: peerDependencies:
@ -11151,6 +11160,11 @@ snapshots:
dependencies: dependencies:
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3) '@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
'@tiptap/extension-table@3.22.4(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)':
dependencies:
'@tiptap/core': 3.15.3(@tiptap/pm@3.15.3)
'@tiptap/pm': 3.15.3
'@tiptap/extension-task-item@3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))': '@tiptap/extension-task-item@3.15.3(@tiptap/extension-list@3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3))':
dependencies: dependencies:
'@tiptap/extension-list': 3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3) '@tiptap/extension-list': 3.15.3(@tiptap/core@3.15.3(@tiptap/pm@3.15.3))(@tiptap/pm@3.15.3)