Skip to content

feat: Complete export pipeline — PDF via @react-pdf/renderer, canonical JSON schema, and Markdown round-trip import #289

@d-oit

Description

@d-oit

Problem

src/features/export/ and src/lib/export-core.ts exist, and docx is a dependency, but:

  1. There is no PDF export — a critical feature for a knowledge studio
  2. There is no canonical JSON schema for the full knowledge studio state (notes + graph + mind map)
  3. There is no Markdown import — only export exists, making the format a dead-end
  4. The CLI (cli/index.ts) lacks headless export commands

Current Export State

src/features/export/      # exists but incomplete
src/lib/export-core.ts    # basic export, no PDF
dependencies:
  docx: installed          # Word export
  # @react-pdf/renderer: NOT installed
  # No PDF support

Proposed Implementation

1. PDF Export via @react-pdf/renderer

npm install @react-pdf/renderer
// src/features/export/pdf-exporter.tsx
import { Document, Page, Text, View, StyleSheet, pdf } from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: { padding: 40, fontFamily: 'Helvetica' },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
  body: { fontSize: 11, lineHeight: 1.6 },
  tag: { fontSize: 9, color: '#666', marginTop: 8 },
});

function NotePDF({ note }: { note: Note }) {
  return (
    <Document>
      <Page size="A4" style={styles.page}>
        <Text style={styles.title}>{note.title}</Text>
        <Text style={styles.body}>{note.content}</Text>
        {note.tags?.length && (
          <Text style={styles.tag}>Tags: {note.tags.join(', ')}</Text>
        )}
      </Page>
    </Document>
  );
}

export async function exportNoteToPDF(note: Note): Promise<Blob> {
  const blob = await pdf(<NotePDF note={note} />).toBlob();
  return blob;
}

export async function exportAllNotesToPDF(notes: Note[]): Promise<Blob> {
  // Multi-page PDF with table of contents
  const blob = await pdf(
    <Document>
      {notes.map(note => (
        <Page key={note.id} size="A4" style={styles.page}>
          <Text style={styles.title}>{note.title}</Text>
          <Text style={styles.body}>{note.content}</Text>
        </Page>
      ))}
    </Document>
  ).toBlob();
  return blob;
}

2. Canonical JSON schema

// src/lib/export-core.ts
export interface KnowledgeStudioExport {
  version: '1.0';
  exportedAt: string;  // ISO 8601
  metadata: {
    title: string;
    description?: string;
  };
  notes: Note[];
  graph: {
    nodes: GraphNode[];
    edges: GraphEdge[];
  };
  mindMap: MindMapData | null;
  tags: string[];
}

export function exportToJson(state: KnowledgeStudioExport): string {
  return JSON.stringify(state, null, 2);
}

export function importFromJson(json: string): KnowledgeStudioExport {
  const data = JSON.parse(json);
  // Validate schema version
  if (!data.version || data.version !== '1.0') {
    throw new Error(`Unsupported export version: ${data.version}`);
  }
  return data as KnowledgeStudioExport;
}

3. Markdown import with frontmatter

// src/lib/markdown-importer.ts
import matter from 'gray-matter';

export interface MarkdownImportResult {
  notes: Note[];
  errors: Array<{ file: string; error: string }>;
}

export function importFromMarkdown(markdownFiles: { name: string; content: string }[]): MarkdownImportResult {
  const notes: Note[] = [];
  const errors: MarkdownImportResult['errors'] = [];
  
  for (const file of markdownFiles) {
    try {
      const { data: frontmatter, content } = matter(file.content);
      notes.push({
        id: frontmatter.id ?? crypto.randomUUID(),
        title: frontmatter.title ?? file.name.replace('.md', ''),
        content,
        tags: frontmatter.tags ?? [],
        createdAt: frontmatter.createdAt ? new Date(frontmatter.createdAt).getTime() : Date.now(),
        updatedAt: Date.now(),
      });
    } catch (err) {
      errors.push({ file: file.name, error: String(err) });
    }
  }
  
  return { notes, errors };
}

4. Markdown export with frontmatter

// Ensure exported Markdown is re-importable (round-trip safe)
export function exportNoteToMarkdown(note: Note): string {
  const frontmatter = [
    '---',
    `id: ${note.id}`,
    `title: ${note.title}`,
    `tags: [${note.tags?.join(', ') ?? ''}]`,
    `createdAt: ${new Date(note.createdAt).toISOString()}`,
    `updatedAt: ${new Date(note.updatedAt).toISOString()}`,
    '---',
    '',
  ].join('\n');
  return frontmatter + note.content;
}

5. CLI headless export commands

// cli/index.ts
program
  .command('export <format>')
  .description('Export all notes (formats: json, markdown, pdf, docx)')
  .option('-o, --output <dir>', 'Output directory', './export')
  .action(async (format, opts) => {
    const notes = await loadNotesFromDb();
    switch (format) {
      case 'json': /* ... */ break;
      case 'markdown': /* ... */ break;
      case 'pdf': /* ... */ break;
      case 'docx': /* ... */ break;
    }
  });

program
  .command('import <file>')
  .description('Import notes from JSON or Markdown')
  .action(async (file) => { /* ... */ });

Acceptance Criteria

  • @react-pdf/renderer installed and single-note PDF export working
  • Multi-note PDF export with table of contents
  • KnowledgeStudioExport JSON schema v1.0 defined
  • exportToJson() and importFromJson() with schema validation
  • exportNoteToMarkdown() with frontmatter (round-trip safe)
  • importFromMarkdown() accepting file array, respecting frontmatter
  • Export UI in app: PDF, JSON, Markdown, DOCX format options
  • Import UI: drag-and-drop JSON or zip of Markdown files
  • CLI export and import commands
  • Unit tests for all export/import functions with sample fixtures

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions