import { useQuery } from '@tanstack/react-query'; import { Node } from '@tiptap/core'; import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'; import type { NodeViewProps } from '@tiptap/react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { apiGet } from '../../lib/api'; import { attributesFromSpec, nodeSpec } from '../spec-utils'; import { useWikilinks } from '../wikilink-context'; interface RenderedContent { title: string; html: string; } /** * Renders a `![[page embed]]` (issue #135). In edit mode — and while loading or * when the target is missing/unreadable — it shows a compact placeholder card * (title + open link) so writing stays fast. In read mode it fetches the * target's server-rendered HTML (`/read/:pond/:slug`, permission-checked, with * nested embeds already expanded) and shows it inline. */ function TransclusionView({ node, editor }: NodeViewProps): React.JSX.Element { const { t } = useTranslation('editor'); const { resolve, pondSlug } = useWikilinks(); const slug = node.attrs.targetSlug as string; const display = node.attrs.displayText as string | null; const bare = node.attrs.bare as boolean; const { title, exists } = resolve(slug); const label = display ?? title ?? slug; const editable = editor.isEditable; const content = useQuery({ queryKey: ['rendered', pondSlug, slug], queryFn: () => apiGet(`/read/${pondSlug}/${slug}`), enabled: !editable && exists, retry: false, }); if (editable || !exists || content.isError) { return ( {t(bare ? 'transclusion.embeddedBare' : 'transclusion.embedded', { title: label })} {t('transclusion.open')} ); } if (bare) { // `$[[…]]` (#146): the embed reads as part of the host page — no frame, // no title, just the target's rendered content. return ( {content.data ? (
) : (
)} ); } return (
{content.data?.title ?? label}
{content.data ? ( // Server-sanitized read HTML (shared docToHtml pipeline) — safe by // contract, same as the public view.
) : (
)} ); } const transclusionSpec = nodeSpec('transclusion'); export const Transclusion = Node.create({ name: 'transclusion', group: transclusionSpec.group, atom: transclusionSpec.atom, addAttributes() { return attributesFromSpec(transclusionSpec); }, parseHTML: () => transclusionSpec.parseDOM, renderHTML: ({ node }) => transclusionSpec.toDOM!(node), addNodeView() { return ReactNodeViewRenderer(TransclusionView); }, });