dorfteich/packages/shared/src/editor-schema/transclusion.test.ts
Claude Fable 5 9bd25f6ce3 #146: Transparente Einbettung $[[Seite]] ohne Rahmen und Titel
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
2026-07-20 00:34:15 +02:00

92 lines
3.5 KiB
TypeScript

import { Node } from 'prosemirror-model';
import { describe, expect, it } from 'vitest';
import { docToHtml } from './html';
import { docToMarkdown, markdownToDoc } from './markdown';
import { extractWikilinkSlugs } from './wikilinks';
function firstTransclusion(doc: Node): Node | null {
let found: Node | null = null;
doc.descendants((node) => {
if (!found && node.type.name === 'transclusion') found = node;
});
return found;
}
describe('page embed / transclusion (issue #135)', () => {
it('parses a lone ![[slug]] line to a transclusion block and round-trips', () => {
const doc = markdownToDoc('Intro\n\n![[rennrad]]\n\nOutro');
const node = firstTransclusion(doc);
expect(node).not.toBeNull();
expect(node!.attrs.targetSlug).toBe('rennrad');
expect(node!.attrs.displayText).toBeNull();
expect(docToMarkdown(doc)).toContain('![[rennrad]]');
});
it('supports optional display text and round-trips it', () => {
const doc = markdownToDoc('![[rennrad|Mein Rad]]');
const node = firstTransclusion(doc);
expect(node!.attrs.targetSlug).toBe('rennrad');
expect(node!.attrs.displayText).toBe('Mein Rad');
expect(docToMarkdown(doc).trim()).toBe('![[rennrad|Mein Rad]]');
});
it('renders a placeholder div carrying the slug', () => {
const doc = markdownToDoc('![[rennrad]]');
const html = docToHtml(doc);
expect(html).toContain('class="dt-transclusion"');
expect(html).toContain('data-transclusion="rennrad"');
});
it('registers the embed target as an outgoing link (backlinks/graph)', () => {
const doc = markdownToDoc('![[rennrad]]\n\n[[dota]]');
expect(extractWikilinkSlugs(doc)).toEqual(['rennrad', 'dota']);
});
it('only embeds a lone line — mid-paragraph ![[x]] is not a transclusion', () => {
const doc = markdownToDoc('see ![[rennrad]] inline');
expect(firstTransclusion(doc)).toBeNull();
});
});
describe('bare page embed `$[[…]]` (issue #146)', () => {
it('parses a lone $[[slug]] line to a bare transclusion and round-trips', () => {
const doc = markdownToDoc('Intro\n\n$[[rennrad]]\n\nOutro');
const node = firstTransclusion(doc);
expect(node).not.toBeNull();
expect(node!.attrs.targetSlug).toBe('rennrad');
expect(node!.attrs.bare).toBe(true);
expect(docToMarkdown(doc)).toContain('$[[rennrad]]');
});
it('keeps `![[…]]` non-bare and round-trips both prefixes side by side', () => {
const doc = markdownToDoc('![[dota]]\n\n$[[rennrad]]');
const markdown = docToMarkdown(doc);
expect(markdown).toContain('![[dota]]');
expect(markdown).toContain('$[[rennrad]]');
let bares = 0;
doc.descendants((node) => {
if (node.type.name === 'transclusion' && node.attrs.bare) bares += 1;
});
expect(bares).toBe(1);
});
it('marks the placeholder div so the server expansion can drop the frame', () => {
const html = docToHtml(markdownToDoc('$[[rennrad]]'));
expect(html).toContain('data-transclusion="rennrad"');
expect(html).toContain('data-transclusion-bare="1"');
// The framed variant must NOT carry the marker.
expect(docToHtml(markdownToDoc('![[rennrad]]'))).not.toContain('data-transclusion-bare');
});
it('registers the bare embed target as an outgoing link too', () => {
const doc = markdownToDoc('$[[rennrad]]');
expect(extractWikilinkSlugs(doc)).toEqual(['rennrad']);
});
it('does not treat mid-paragraph $[[x]] as a transclusion', () => {
const doc = markdownToDoc('costs $[[rennrad]] inline');
expect(firstTransclusion(doc)).toBeNull();
});
});