/// import { action, computed, handler, NAME, navigateTo, pattern, type Stream, UI, type VNode, wish, Writable, } from "commontools"; import { type MentionablePiece, type NoteMdInput, type NotePiece, } from "./schemas.tsx"; // Handler for clicking a backlink chip - module scope required for .map() binding const handleBacklinkClick = handler< void, { piece: Writable } >((_, { piece }) => navigateTo(piece)); // ===== Output Type ===== interface NoteMdOutput { [NAME]: string; [UI]: VNode; /** Passthrough note reference */ note: NotePiece; /** Hidden from default-app piece list */ isHidden: true; /** Excluded from mentions autocomplete (notes in notebooks may be hidden but still mentionable) */ isMentionable: false; /** Minimal UI for embedding in other patterns */ embeddedUI: VNode; /** Processed content with wiki-links converted to markdown links */ processedContent: string; /** Stream to toggle checkboxes in content */ checkboxToggle: Stream<{ detail: { index: number; checked: boolean } }>; /** Whether backlinks section should be visible */ hasBacklinks: boolean; /** Stream to navigate back to source note for editing */ goToEdit: Stream; } export default pattern( ({ note, sourceNoteRef, content }) => { const displayName = computed(() => { const title = note?.title || "Untitled"; return `📖 ${title}`; }); const hasBacklinks = computed(() => (note?.backlinks?.length ?? 0) > 0); // Convert [[Name (id)]] wiki-links to markdown links [Name](/of:id) // ct-markdown will then convert these to clickable ct-cell-link components // Use content prop if provided, otherwise fall back to note.content const processedContent = computed(() => { const raw = content?.get?.() ?? note?.content ?? ""; // Match [[Name (id)]] pattern and convert to [Name](/of:id) return raw.replace( /\[\[([^\]]*?)\s*\(([^)]+)\)\]\]/g, (_match, name, id) => `[${name.trim()}](/of:${id})`, ); }); // Type-based discovery for notes via mentionable list const noteWish = wish({ query: "#note", scope: ["."] }); // Use sourceNoteRef directly if provided, otherwise fall back to noteId lookup const sourceNote = computed(() => { if (sourceNoteRef) { return sourceNoteRef; } const myNoteId = note?.noteId; if (!myNoteId) return null; return noteWish.candidates.find((piece) => piece?.noteId === myNoteId); }); // Action: navigate back to source note for editing const goToEdit = action(() => { if (sourceNote) { return navigateTo(sourceNote); } }); // Action: handle checkbox toggle in markdown content const handleCheckboxToggle = action( (event: { detail: { index: number; checked: boolean } }) => { if (!content) return; const currentContent = content.get(); const { index, checked } = event.detail; // Find all checkbox patterns in the content const checkboxPattern = /- \[([ xX])\]/g; let match; let currentIndex = 0; let result = currentContent; checkboxPattern.lastIndex = 0; while ((match = checkboxPattern.exec(currentContent)) !== null) { if (currentIndex === index) { const newCheckbox = checked ? "- [x]" : "- [ ]"; result = currentContent.slice(0, match.index) + newCheckbox + currentContent.slice(match.index + match[0].length); break; } currentIndex++; } if (result !== currentContent) { content.set(result); } }, ); // Scrollable content with markdown + backlinks (for print support) const markdownViewer = (
{/* Markdown content with wiki-links converted to clickable links */} {/* Backlinks section - ct-chips at bottom */}
(hasBacklinks ? "block" : "none")), marginTop: "2rem", paddingTop: "1rem", borderTop: "1px solid var(--ct-color-border, #e5e5e7)", }} > Linked from: {note?.backlinks?.map((piece) => ( ))}
); return { [NAME]: displayName, [UI]: ( {computed(() => note?.title || "Untitled Note")} {/* Edit button - navigates back to source note for editing */} Edit {markdownViewer} ), note, isHidden: true, isMentionable: false, embeddedUI: markdownViewer, processedContent, checkboxToggle: handleCheckboxToggle, hasBacklinks, goToEdit, }; }, );