refactor: move shared utilities to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
113
frontend/src/lib/shared/discussion/mentionSerializer.ts
Normal file
113
frontend/src/lib/shared/discussion/mentionSerializer.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { JSONContent } from '@tiptap/core';
|
||||
import type { PersonMention } from '$lib/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 };
|
||||
}
|
||||
Reference in New Issue
Block a user