Neues bare-Attr am transclusion-Node; $-Präfix in Markdown-Regel, Serializer und Autocomplete; HTML-Placeholder trägt data-transclusion-bare, Server-Expansion und NodeView lassen bei bare Rahmen und Titel weg. Gleiche Tiefen-/Zyklen-/Permission-Regeln, zählt weiter als Link. Unit- und DB-Tests ergänzt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155v2aT8AG1kZDQEZiCLBWC
104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
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<RenderedContent>(`/read/${pondSlug}/${slug}`),
|
|
enabled: !editable && exists,
|
|
retry: false,
|
|
});
|
|
|
|
if (editable || !exists || content.isError) {
|
|
return (
|
|
<NodeViewWrapper className="dt-transclusion-card" contentEditable={false}>
|
|
<span className="dt-transclusion-card__icon" aria-hidden>
|
|
⧉
|
|
</span>
|
|
<span className="dt-transclusion-card__label">
|
|
{t(bare ? 'transclusion.embeddedBare' : 'transclusion.embedded', { title: label })}
|
|
</span>
|
|
<Link className="dt-transclusion-card__open" to={`/p/${pondSlug}/${slug}`}>
|
|
{t('transclusion.open')}
|
|
</Link>
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
if (bare) {
|
|
// `$[[…]]` (#146): the embed reads as part of the host page — no frame,
|
|
// no title, just the target's rendered content.
|
|
return (
|
|
<NodeViewWrapper className="dt-embed-bare" contentEditable={false}>
|
|
{content.data ? (
|
|
<div dangerouslySetInnerHTML={{ __html: content.data.html }} />
|
|
) : (
|
|
<div aria-busy="true" />
|
|
)}
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<NodeViewWrapper className="dt-embed" contentEditable={false}>
|
|
<div className="dt-embed__title">
|
|
<Link className="wikilink" to={`/p/${pondSlug}/${slug}`}>
|
|
{content.data?.title ?? label}
|
|
</Link>
|
|
</div>
|
|
{content.data ? (
|
|
// Server-sanitized read HTML (shared docToHtml pipeline) — safe by
|
|
// contract, same as the public view.
|
|
<div className="dt-embed__body" dangerouslySetInnerHTML={{ __html: content.data.html }} />
|
|
) : (
|
|
<div className="dt-embed__body" aria-busy="true" />
|
|
)}
|
|
</NodeViewWrapper>
|
|
);
|
|
}
|
|
|
|
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);
|
|
},
|
|
});
|