- 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>
179 lines
5.5 KiB
TypeScript
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>
|
|
)
|
|
}
|