File size: 9,815 Bytes
d15d7f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32a4aca
 
d15d7f7
 
 
 
32a4aca
 
 
d15d7f7
 
 
 
 
 
 
 
32a4aca
d15d7f7
32a4aca
 
d15d7f7
32a4aca
d15d7f7
 
32a4aca
 
d15d7f7
 
 
32a4aca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d15d7f7
32a4aca
 
d15d7f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32a4aca
 
 
 
 
 
d15d7f7
 
32a4aca
d15d7f7
 
 
 
 
 
 
 
 
 
32a4aca
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# 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) |