- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/ - Move person relationship components to lib/person/relationship/ - Move Stammbaum components to lib/person/genealogy/ - Move HelpPopover to lib/shared/primitives/ - Update all import paths across routes, specs, and lib files - Update vi.mock() paths in server-project test files - Remove now-empty legacy directories (components/, hooks/, server/, etc.) - Update vite.config.ts coverage include paths for new structure - Update frontend/CLAUDE.md to reflect domain-based lib/ layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import type { JSONContent } from '@tiptap/core';
|
|
import type { PersonMention } from '$lib/shared/types';
|
|
|
|
/**
|
|
* Converts stored block text + sidecar into a Tiptap ProseMirror document.
|
|
*
|
|
* The text is split by "\n" into paragraphs. Within each paragraph, sidecar
|
|
* entries are matched against "@displayName" tokens (longest first) and
|
|
* converted to mention nodes. Unmatched text becomes plain text nodes.
|
|
*
|
|
* Round-trip invariant: serialize(deserialize(text, sidecar)).text === text
|
|
*/
|
|
export function deserialize(text: string, sidecar: PersonMention[]): JSONContent {
|
|
const lines = text === '' ? [''] : text.split('\n');
|
|
|
|
// Sort sidecar by displayName length descending so longer names shadow
|
|
// shorter prefix matches (same heuristic as renderTranscriptionBody).
|
|
const sorted = [...sidecar].sort((a, b) => b.displayName.length - a.displayName.length);
|
|
|
|
return {
|
|
type: 'doc',
|
|
content: lines.map((line) => ({
|
|
type: 'paragraph',
|
|
content: parseLine(line, sorted)
|
|
}))
|
|
};
|
|
}
|
|
|
|
function parseLine(text: string, sidecar: PersonMention[]): JSONContent[] {
|
|
if (text === '') return [];
|
|
|
|
if (sidecar.length === 0) {
|
|
return [{ type: 'text', text }];
|
|
}
|
|
|
|
// Build a list of mention ranges: { start, end, mention }
|
|
const ranges: Array<{ start: number; end: number; mention: PersonMention }> = [];
|
|
|
|
for (const mention of sidecar) {
|
|
const needle = `@${mention.displayName}`;
|
|
let idx = 0;
|
|
while (idx < text.length) {
|
|
const pos = text.indexOf(needle, idx);
|
|
if (pos === -1) break;
|
|
// Check that the range doesn't overlap an already-found range
|
|
const end = pos + needle.length;
|
|
const overlaps = ranges.some((r) => r.start < end && r.end > pos);
|
|
if (!overlaps) {
|
|
ranges.push({ start: pos, end, mention });
|
|
}
|
|
idx = pos + 1;
|
|
}
|
|
}
|
|
|
|
if (ranges.length === 0) {
|
|
return [{ type: 'text', text }];
|
|
}
|
|
|
|
// Sort by position
|
|
ranges.sort((a, b) => a.start - b.start);
|
|
|
|
const nodes: JSONContent[] = [];
|
|
let cursor = 0;
|
|
|
|
for (const { start, end, mention } of ranges) {
|
|
if (start > cursor) {
|
|
nodes.push({ type: 'text', text: text.slice(cursor, start) });
|
|
}
|
|
nodes.push({
|
|
type: 'mention',
|
|
attrs: { displayName: mention.displayName, personId: mention.personId }
|
|
});
|
|
cursor = end;
|
|
}
|
|
|
|
if (cursor < text.length) {
|
|
nodes.push({ type: 'text', text: text.slice(cursor) });
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
|
|
/**
|
|
* Converts a Tiptap ProseMirror document back to stored block text + sidecar.
|
|
*
|
|
* Paragraphs are joined with "\n". Mention nodes are emitted as "@displayName"
|
|
* and collected into mentionedPersons (de-duplicated by personId).
|
|
*/
|
|
export function serialize(doc: JSONContent): { text: string; mentionedPersons: PersonMention[] } {
|
|
const paragraphs = doc.content ?? [];
|
|
const mentionedPersons: PersonMention[] = [];
|
|
const seenIds = new Set<string>();
|
|
const lines: string[] = [];
|
|
|
|
for (const para of paragraphs) {
|
|
let line = '';
|
|
for (const node of para.content ?? []) {
|
|
if (node.type === 'text') {
|
|
line += node.text ?? '';
|
|
} else if (node.type === 'mention') {
|
|
const { displayName, personId } = node.attrs ?? {};
|
|
line += `@${displayName}`;
|
|
if (!seenIds.has(personId)) {
|
|
seenIds.add(personId);
|
|
mentionedPersons.push({ personId, displayName });
|
|
}
|
|
}
|
|
}
|
|
lines.push(line);
|
|
}
|
|
|
|
return { text: lines.join('\n'), mentionedPersons };
|
|
}
|