Files
Momento/memento-note/components/tiptap-chart-extension.tsx
Antigravity 835e1872bb fix(chart): convert markdown to HTML for TipTap insertion
- Convert chart markdown to <pre><code class="language-chart"> HTML format
- Fix parseHTML to store code content in node attrs
- Fix renderHTML to output actual code content instead of placeholder

This fixes charts rendering as raw markdown text instead of visual charts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:51:30 +00:00

179 lines
5.5 KiB
TypeScript

'use client'
import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react'
import { NoteChartFromCode } from './note-chart'
import { useState } from 'react'
import { Code, BarChart3, AlertCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
/**
* ChartExtension - TipTap Node extension for rendering chart blocks
*
* Detects <pre><code class="language-chart"> blocks and renders them
* as visual charts using the NoteChartFromCode component.
*/
export const ChartExtension = Node.create({
name: 'chartBlock',
group: 'block',
code: true,
defining: true,
addOptions() {
return {
HTMLAttributes: {},
}
},
addAttributes() {
return {
code: {
default: '',
parseHTML: element => {
if (element instanceof HTMLElement) {
const codeEl = element.querySelector('code')
return codeEl?.textContent || ''
}
return ''
},
renderHTML: () => ({})
},
language: {
default: 'chart',
parseHTML: element => {
if (element instanceof HTMLElement) {
const codeEl = element.querySelector('code')
// Check for class="language-chart" or data-language="chart"
if (codeEl?.classList.contains('language-chart')) return 'chart'
return element.getAttribute('data-language') || 'chart'
}
return 'chart'
},
renderHTML: attributes => ({
'data-language': attributes.language,
})
}
}
},
parseHTML() {
return [
{
tag: 'pre',
getAttrs: node => {
if (typeof node === 'string') return false
const element = node as HTMLElement
const codeEl = element.querySelector('code')
// Detect chart blocks by class="language-chart"
if (codeEl && codeEl.classList.contains('language-chart')) {
// Store the code content as an attribute
const codeContent = codeEl.textContent || ''
return {
code: codeContent,
language: 'chart'
}
}
return false
}
}
]
},
renderHTML({ HTMLAttributes, node }) {
// Get the code content from node attrs
const code = node.attrs.code || ''
return ['pre', { ...HTMLAttributes, class: 'language-chart' }, ['code', { class: 'language-chart' }, code]]
},
addNodeView() {
return ReactNodeViewRenderer(ChartBlockView, {
contentEditable: false,
})
}
})
/**
* ChartBlockView - React component for rendering chart blocks in TipTap
*
* Features:
* - Visual chart rendering with NoteChartFromCode
* - Toggle between visual and code view
* - Edit mode for modifying chart data
* - Error handling for invalid chart data
*/
function ChartBlockView(props: any) {
const [isEditing, setIsEditing] = useState(false)
const [parseError, setParseError] = useState(false)
const code = props.node?.attrs?.code || ''
// Check if chart code is valid when not editing
const isValidChart = !isEditing && code.trim().length > 0
if (isEditing) {
return (
<NodeViewWrapper className="notion-chart-code-block my-4">
<div className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-t-lg border-b border-border">
<Code className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Chart Code</span>
<button
onClick={() => setIsEditing(false)}
className="ml-auto text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
>
Done
</button>
</div>
<NodeViewContent as="pre" className="p-4 bg-muted/30 rounded-b-lg overflow-x-auto" />
</NodeViewWrapper>
)
}
return (
<NodeViewWrapper className="notion-chart-block my-4 group relative">
<div className="relative">
{/* Chart visual rendering */}
{isValidChart ? (
<NoteChartFromCode code={code} />
) : (
<div className="my-6 rounded-xl border border-dashed border-border bg-muted/30 p-6">
<div className="flex items-center gap-3 text-muted-foreground">
<AlertCircle className="w-5 h-5" />
<div>
<p className="font-medium">Invalid Chart</p>
<p className="text-sm">This chart block contains invalid or empty data.</p>
</div>
</div>
</div>
)}
{/* Edit button - visible on hover */}
<button
onClick={() => setIsEditing(true)}
className={cn(
'absolute top-2 right-2 p-2 rounded-lg bg-background/80 backdrop-blur border border-border shadow-sm opacity-0 group-hover:opacity-100 transition-opacity',
'hover:bg-background hover:shadow-md'
)}
title="Edit chart source"
>
<Code className="w-4 h-4 text-muted-foreground" />
</button>
{/* Chart type indicator */}
{isValidChart && (
<div className="absolute top-2 left-2 px-2 py-1 rounded bg-background/80 backdrop-blur border border-border shadow-sm opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<BarChart3 className="w-3 h-3" />
<span>Chart</span>
</div>
</div>
)}
</div>
</NodeViewWrapper>
)
}