Spaces:
Running
Running
Download lib/export.ts from Leon4gr45/nodepad-hypotheses: direct link, hf CLI and curl.
- Browser
- Download file 11.2 kB
-
https://huggingface.co/spaces/Leon4gr45/nodepad-hypotheses/resolve/main/lib/export.ts
- Command line
-
hf download hf://spaces/Leon4gr45/nodepad-hypotheses/lib/export.ts
-
curl -L -o export.ts https://huggingface.co/spaces/Leon4gr45/nodepad-hypotheses/resolve/main/lib/export.ts
11.2 kB
| import type { ContentType } from "./content-types" | |
| // Escape characters that would break markdown table cells | |
| const mdCell = (s: string) => s.replace(/\|/g, "\\|").replace(/\n/g, " ") | |
| export interface ExportBlock { | |
| id: string | |
| text: string | |
| contentType: ContentType | |
| category?: string | |
| annotation?: string | |
| confidence?: number | null | |
| sources?: { url: string; title: string; siteName: string }[] | |
| isPinned?: boolean | |
| timestamp?: number | |
| } | |
| // ββ Type ordering β research-logical flow βββββββββββββββββββββββββββββββββββββ | |
| const TYPE_ORDER: ContentType[] = [ | |
| "thesis", | |
| "claim", | |
| "question", | |
| "idea", | |
| "task", | |
| "entity", | |
| "definition", | |
| "reference", | |
| "quote", | |
| "opinion", | |
| "reflection", | |
| "narrative", | |
| "comparison", | |
| "general", | |
| ] | |
| const TYPE_META: Record<ContentType, { heading: string; emoji: string; description: string }> = { | |
| thesis: { heading: "Thesis", emoji: "π¬", description: "Synthesised conclusions" }, | |
| claim: { heading: "Claims", emoji: "β‘", description: "Assertions with confidence scores" }, | |
| question: { heading: "Questions", emoji: "β", description: "Open threads and unknowns" }, | |
| idea: { heading: "Ideas", emoji: "π‘", description: "Raw concepts and hunches" }, | |
| task: { heading: "Tasks", emoji: "β ", description: "Action items" }, | |
| entity: { heading: "Entities", emoji: "π·οΈ", description: "People, places, and named concepts" }, | |
| definition: { heading: "Definitions", emoji: "π", description: "Terminology and explanations" }, | |
| reference: { heading: "References", emoji: "π", description: "Sources and links" }, | |
| quote: { heading: "Quotes", emoji: "π¬", description: "Direct quotations" }, | |
| opinion: { heading: "Opinions", emoji: "π£οΈ", description: "Subjective takes" }, | |
| reflection: { heading: "Reflections", emoji: "πͺ", description: "Personal observations" }, | |
| narrative: { heading: "Narratives", emoji: "π", description: "Extended accounts" }, | |
| comparison: { heading: "Comparisons", emoji: "βοΈ", description: "Contrasts and parallels" }, | |
| general: { heading: "Notes", emoji: "π", description: "Miscellaneous notes" }, | |
| } | |
| // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function formatDate(ts?: number): string { | |
| const d = ts ? new Date(ts) : new Date() | |
| return d.toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" }) | |
| } | |
| function formatTime(ts?: number): string { | |
| if (!ts) return "" | |
| return new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) | |
| } | |
| /** Five-block filled/empty bar + percentage */ | |
| function confidenceBar(c: number): string { | |
| const clamped = Math.min(100, Math.max(0, c)) | |
| const pct = Math.round(clamped) | |
| const filled = Math.round((clamped / 100) * 5) | |
| const bar = "β".repeat(filled) + "β".repeat(5 - filled) | |
| return `${bar} ${pct}%` | |
| } | |
| /** Anchor-safe slug for TOC links */ | |
| function slug(text: string): string { | |
| return text | |
| .toLowerCase() | |
| .replace(/[^a-z0-9\s-]/g, "") | |
| .trim() | |
| .replace(/\s+/g, "-") | |
| .slice(0, 50) | |
| } | |
| // ββ Main export βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Export a project as a richly-formatted Markdown document. | |
| * Uses YAML front matter, a table of contents, grouped sections, | |
| * tables for claims, task lists, blockquotes, inline code, and source links | |
| * so the output renders well in GitHub, Obsidian, Notion, and any LLM context. | |
| */ | |
| export function exportToMarkdown(projectName: string, blocks: ExportBlock[]): string { | |
| if (blocks.length === 0) { | |
| return [ | |
| `# ${projectName}`, | |
| ``, | |
| `> _No nodes yet._`, | |
| ``, | |
| `---`, | |
| `*Generated by [nodepad](https://nodepad.space)*`, | |
| ].join("\n") | |
| } | |
| // Group by type | |
| const byType = new Map<ContentType, ExportBlock[]>() | |
| for (const block of blocks) { | |
| const t = block.contentType || "general" | |
| if (!byType.has(t)) byType.set(t, []) | |
| byType.get(t)!.push(block) | |
| } | |
| const presentTypes = TYPE_ORDER.filter(t => (byType.get(t)?.length ?? 0) > 0) | |
| const date = formatDate(blocks[0]?.timestamp) | |
| const pinned = blocks.filter(b => b.isPinned) | |
| const lines: string[] = [] | |
| // ββ YAML front matter βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| lines.push(`---`) | |
| lines.push(`title: "${projectName}"`) | |
| lines.push(`date: "${date}"`) | |
| lines.push(`nodes: ${blocks.length}`) | |
| lines.push(`types: [${presentTypes.map(t => `"${t}"`).join(", ")}]`) | |
| lines.push(`source: nodepad β https://nodepad.space`) | |
| lines.push(`---`) | |
| lines.push(``) | |
| // ββ Title block βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| lines.push(`# ${projectName}`) | |
| lines.push(``) | |
| lines.push(`> **Generated by [nodepad](https://nodepad.space)** Β· ${date} Β· ${blocks.length} node${blocks.length !== 1 ? "s" : ""}`) | |
| lines.push(``) | |
| // ββ Stats table βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| lines.push(`## Overview`) | |
| lines.push(``) | |
| lines.push(`| Type | Count |`) | |
| lines.push(`|------|-------|`) | |
| for (const type of presentTypes) { | |
| const { emoji, heading } = TYPE_META[type] | |
| lines.push(`| ${emoji} ${heading} | ${byType.get(type)!.length} |`) | |
| } | |
| lines.push(``) | |
| // ββ Pinned notes callout (if any) βββββββββββββββββββββββββββββββββββββββββ | |
| if (pinned.length > 0) { | |
| lines.push(`> **π Pinned nodes**`) | |
| for (const b of pinned) { | |
| lines.push(`> - ${b.text}`) | |
| } | |
| lines.push(``) | |
| } | |
| // ββ Table of contents βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| lines.push(`## Contents`) | |
| lines.push(``) | |
| for (const type of presentTypes) { | |
| const { emoji, heading } = TYPE_META[type] | |
| const count = byType.get(type)!.length | |
| lines.push(`- [${emoji} ${heading}](#${slug(heading)}) *(${count})*`) | |
| } | |
| lines.push(``) | |
| // ββ Sections ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| for (const type of presentTypes) { | |
| const group = byType.get(type)! | |
| const { emoji, heading, description } = TYPE_META[type] | |
| lines.push(`---`) | |
| lines.push(``) | |
| lines.push(`## ${emoji} ${heading}`) | |
| lines.push(``) | |
| lines.push(`*${description}*`) | |
| lines.push(``) | |
| // Claims β table for scannability | |
| if (type === "claim") { | |
| lines.push(`| Claim | Category | Confidence | AI Annotation |`) | |
| lines.push(`|-------|----------|------------|---------------|`) | |
| for (const block of group) { | |
| const cat = block.category ? `\`${mdCell(block.category)}\`` : "β" | |
| const conf = block.confidence != null | |
| ? `\`${confidenceBar(block.confidence)}\`` | |
| : "β" | |
| const annot = block.annotation | |
| ? block.annotation.replace(/\n/g, " ").replace(/\|/g, "\\|").slice(0, 120) + (block.annotation.length > 120 ? "β¦" : "") | |
| : "β" | |
| lines.push(`| **${block.text.replace(/\|/g, "\\|")}** | ${cat} | ${conf} | ${annot} |`) | |
| } | |
| lines.push(``) | |
| continue | |
| } | |
| // Tasks β GFM task list | |
| if (type === "task") { | |
| for (const block of group) { | |
| lines.push(`- [ ] **${block.text}**`) | |
| if (block.category) lines.push(` - Category: \`${block.category}\``) | |
| if (block.annotation) { | |
| lines.push(` - > ${block.annotation.replace(/\n/g, " ")}`) | |
| } | |
| } | |
| lines.push(``) | |
| continue | |
| } | |
| // Quotes β blockquote with attribution styling | |
| if (type === "quote") { | |
| for (const block of group) { | |
| lines.push(`> ${block.text}`) | |
| if (block.category) lines.push(`>`) | |
| if (block.category) lines.push(`> β \`${block.category}\``) | |
| if (block.annotation) { | |
| lines.push(`>`) | |
| lines.push(`> *${block.annotation.replace(/\n/g, " ")}*`) | |
| } | |
| lines.push(``) | |
| } | |
| continue | |
| } | |
| // All other types β heading + metadata + annotation | |
| for (const block of group) { | |
| lines.push(`### ${block.text}`) | |
| lines.push(``) | |
| // Metadata row | |
| const meta: string[] = [] | |
| if (block.category) meta.push(`**Category:** \`${block.category}\``) | |
| if (block.timestamp) meta.push(`**Added:** ${formatDate(block.timestamp)} ${formatTime(block.timestamp)}`) | |
| if (block.isPinned) meta.push(`π *Pinned*`) | |
| if (meta.length > 0) { | |
| lines.push(meta.join(" Β· ")) | |
| lines.push(``) | |
| } | |
| // AI annotation as blockquote | |
| if (block.annotation) { | |
| lines.push(`> **AI Annotation**`) | |
| for (const l of block.annotation.split("\n")) { | |
| lines.push(`> ${l}`) | |
| } | |
| lines.push(``) | |
| } | |
| // Sources as a nested list | |
| if (block.sources && block.sources.length > 0) { | |
| lines.push(`**Sources**`) | |
| lines.push(``) | |
| for (const s of block.sources) { | |
| const label = s.title || s.siteName || s.url | |
| lines.push(`- [${label}](${s.url})${s.siteName ? ` *(${s.siteName})*` : ""}`) | |
| } | |
| lines.push(``) | |
| } | |
| } | |
| } | |
| // ββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| lines.push(`---`) | |
| lines.push(``) | |
| lines.push(`*Generated by [nodepad](https://nodepad.space) Β· ${date}*`) | |
| lines.push(``) | |
| return lines.join("\n") | |
| } | |
| // ββ Download / clipboard helpers βββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Trigger a browser download of a .md file. */ | |
| export function downloadMarkdown(filename: string, content: string): void { | |
| const blob = new Blob([content], { type: "text/markdown;charset=utf-8" }) | |
| const url = URL.createObjectURL(blob) | |
| const a = document.createElement("a") | |
| a.href = url | |
| a.download = filename | |
| a.click() | |
| URL.revokeObjectURL(url) | |
| } | |
| /** Copy content to clipboard. Returns true on success. */ | |
| export async function copyToClipboard(content: string): Promise<boolean> { | |
| try { | |
| await navigator.clipboard.writeText(content) | |
| return true | |
| } catch { | |
| return false | |
| } | |
| } | |