| # Architecture |
|
|
| ## Overview |
|
|
| The collab-editor is a collaborative article editor built for Hugging Face Spaces. It runs as a single Docker container serving both the backend (Express + Hocuspocus) and the frontend (React + TipTap). |
|
|
| ``` |
| ┌─────────────────────────────────────────────────────────┐ |
| │ Docker Container (port 8080) │ |
| │ │ |
| │ ┌──────────────────┐ ┌────────────────────────────┐ │ |
| │ │ Express Server │ │ Hocuspocus (Y.js collab) │ │ |
| │ │ │ │ │ │ |
| │ │ /api/* │ │ /collab (WebSocket) │ │ |
| │ │ /published/* │ │ │ │ |
| │ │ /editor │ │ │ │ |
| │ │ / (published) │ │ │ │ |
| │ └──────────────────┘ └────────────────────────────┘ │ |
| │ │ |
| │ ┌──────────────────┐ ┌────────────────────────────┐ │ |
| │ │ Publisher │ │ Frontend (static) │ │ |
| │ │ (HTML renderer) │ │ /editor -> SPA │ │ |
| │ │ (PDF generator) │ │ React + TipTap │ │ |
| │ └──────────────────┘ └────────────────────────────┘ │ |
| └─────────────────────────────────────────────────────────┘ |
| ``` |
|
|
| ## Key directories |
|
|
| | Path | Description | |
| |------|-------------| |
| | `backend/src/server.ts` | Entry point: imports `createApp()`, starts listener, signal handlers | |
| | `backend/src/create-app.ts` | Express app factory: routes, Hocuspocus, WebSocket, middleware | |
| | `backend/src/publisher/` | HTML rendering, PDF generation, bibliography formatting | |
| | `backend/src/publisher/html-renderer.ts` | Converts TipTap JSON to static HTML page | |
| | `backend/src/publisher/extensions.ts` | Server-side TipTap extensions (mirrors frontend) | |
| | `backend/src/shared/component-defs.ts` | Shared component definitions (single source of truth) | |
| | `backend/src/utils.ts` | Shared utilities: `docPath()`, `sanitizeName()`, injectable `DATA_DIR` | |
| | `backend/src/auth.ts` | OAuth flow, token extraction, user resolution | |
| | `backend/src/hf-storage.ts` | HF dataset sync (push/pull documents and assets) | |
| | `frontend/src/editor/` | TipTap editor, toolbars, components | |
| | `frontend/src/editor/components/registry.ts` | Component registry (imports from shared defs) | |
| | `frontend/src/styles/` | All CSS files | |
|
|
| ## Styling architecture |
|
|
| ### CSS layers |
|
|
| The project uses five CSS layers, loaded in this order by `main.tsx`: |
|
|
| 1. **Template foundation** (`_variables.css`, `_reset.css`, `_base.css`, `_layout.css`, `_print.css`, `components/*`) |
| - Defines the article's visual identity (typography, grid, components) |
| - Uses CSS custom properties (`--text-color`, `--surface-bg`, `--primary-color`, etc.) |
| - Layout tokens centralize grid math (`--layout-toc-width`, `--layout-content-width`, `--layout-gap`, breakpoints) |
| - Shared between the editor preview and the published output |
|
|
| 2. **Editor chrome** (`_ui.css`) |
| - Styles the editor UI: top-bar, sidebars, dialogs, chat panel, embed studio |
| - Uses `--ed-*` custom properties for the dark editor theme |
| - Only loaded in the editor, never in published output |
|
|
| 3. **Design tokens** (`tokens.css`) |
| - Light/dark theming tokens for both editor and article |
| - Text, background, border, accent, code highlighting, danger, shadows |
| - Supports `data-theme` attribute and `prefers-color-scheme` media query |
| - Shared between editor and published output (injected by publisher) |
|
|
| 4. **Shared article styles** (`article.css`, `toc.css`) |
| - Article content styles shared between editor preview and published output |
| - `article.css` includes wrapper components (Note, Stack, Quote, Sidenote...), with editor-specific variants scoped by `.editor-app` |
|
|
| 5. **Editor-only styles** (`styles/editor/*.css`, 5 files) |
| - `_layout.css`: grid overrides (3-col symmetric, aligned with template), TOC drawer, responsive breakpoints (1100px collapse, 768px mobile) |
| - `_chrome.css`: ProseMirror editing visuals (placeholder, selection, cursors, comment marks, math editing) |
| - `_block-tools.css`: block handles (drag + add) and slash menu |
| - `_panels.css`: image upload card, footnote tooltip, citation panel |
| - `_hero-editable.css`: transparent click-to-edit inputs for FrontmatterHero |
|
|
| 6. **Publisher CSS** (`_publisher.css`) |
| - Styles specific to the published static HTML page |
| - Theme toggle animations, wide/fullWidth breakout, sidenote float, lightbox, PDF link |
| - Only injected by the HTML renderer (backend), never loaded in the editor |
|
|
| ### No CSS-in-JS |
|
|
| The project does not use MUI, Emotion, or any CSS-in-JS library. All styling is done via: |
| - CSS custom properties for theming |
| - Vanilla CSS files with BEM-like class naming |
| - `Floating UI` for tooltip positioning (lightweight, no CSS-in-JS) |
|
|
| ### Color theming |
|
|
| - The article area uses `data-theme="light"` / `data-theme="dark"` with CSS variable overrides |
| - The editor chrome is always dark, using `--ed-*` tokens |
| - The primary accent color is controlled via `--primary-color` (synced via Yjs settings) |
|
|
| ## HF Spaces constraints |
|
|
| ### Iframe embedding |
|
|
| When deployed as a HF Space, the app runs inside an iframe. This affects: |
|
|
| - **Viewport width**: the iframe is ~968px wide, not the full browser width |
| - **No `target="_top"` navigation**: links open within the iframe unless using `target="_blank"` |
| - **OAuth flow**: the OAuth callback URL must match the Space URL |
| - **CSP restrictions**: sandboxed iframes may restrict certain APIs |
| |
| ### Two-page architecture |
| |
| | URL | What it serves | |
| |-----|----------------| |
| | `/` | Published article (static HTML) or login prompt | |
| | `/editor` | The SPA editor (requires authentication) | |
| |
| This means: |
| - The published article is a completely standalone HTML file |
| - It does NOT load React or any JS framework |
| - The editor is a separate React SPA at `/editor` |
| |
| ### CSS cascade |
| |
| Because the published HTML is self-contained (all CSS inlined in `<style>`), there are no CSS conflicts with the HF Spaces iframe CSS. The editor uses Vite's CSS pipeline. |
| |
| ## Shared component registry |
| |
| Component definitions (name, kind, fields, defaults) are defined once in `backend/src/shared/component-defs.ts`. This file is the single source of truth. |
| |
| - **Backend** (`extensions.ts`): imports `SHARED_COMPONENT_DEFS` to generate TipTap server extensions for `generateHTML()` |
| - **Frontend** (`registry.ts`): imports `SHARED_COMPONENT_DEFS` via Vite alias `#shared` and decorates each entry with UI metadata (icon, label, description, placeholders) |
|
|
| Adding a new component: |
| 1. Add the entry to `shared/component-defs.ts` |
| 2. Add UI metadata to `frontend/src/editor/components/registry.ts` in `UI_META` |
| 3. Add CSS for the published view to `frontend/src/styles/_publisher.css` |
|
|
| ## Publisher pipeline |
|
|
| ``` |
| Y.Doc -> TiptapTransformer -> JSON -> generateHTML() -> postProcess() -> full HTML page |
| | |
| v |
| PDF (Playwright) |
| | |
| v |
| Upload to HF dataset |
| ``` |
|
|
| ### Post-processing (linkedom) |
|
|
| The `postProcess()` function uses `linkedom` for DOM manipulation instead of regex: |
| - Accordion `<div>` -> `<details>/<summary>` |
| - Citation `<span>` -> `<a>` links with bibliography anchors |
| - Bibliography placeholder -> formatted HTML with entry IDs |
| - Mermaid `<div>` -> `<pre class="mermaid">` |
| - HtmlEmbed `<div>` -> `<iframe>` |
| - Footnotes -> superscript links + appended section |
|
|
| ### Preview endpoint |
|
|
| `GET /api/preview/:docName` renders the HTML without saving or uploading. Useful for testing the publisher pipeline. |
|
|
| ## Auth and security |
|
|
| - AI chat routes (`/api/chat`, `/api/embed-chat`) are auth-guarded when OAuth is enabled |
| - The `requireEditor` middleware checks cookie token and verifies write access |
| - Published articles are served without auth (public) |
|
|
| ## Testing |
|
|
| Tests use Vitest + Supertest. Run with `npm test` from `backend/`. |
|
|
| | Test file | What it covers | |
| |-----------|----------------| |
| | `tests/publisher.test.ts` | Y.Doc extraction, HTML generation, post-processing, idempotency | |
| | `tests/html-renderer-snapshot.test.ts` | Snapshot tests for each postProcess transformation | |
| | `tests/security.test.ts` | XSS prevention in published HTML | |
| | `tests/css-resolution.test.ts` | @custom-media resolution | |
| | `tests/utils.test.ts` | Path sanitization utilities | |
| | `tests/auth.test.ts` | Token extraction, OAuth configuration | |
| | `tests/hf-storage.test.ts` | HF dataset storage configuration | |
| | `tests/persistence.test.ts` | Local file persistence, debounced save | |
| | `tests/api-routes.test.ts` | API route integration tests (publish, auth status) | |
|
|