# Phase 4 — Document Management
**Status**: Architecture Plan (Pre-Implementation)
**Date**: June 15, 2026
**Goal**: Transform Bayan from a text editor into a document editor with import/export
**Prerequisites**: Phase 1 editor engine complete; Phase 2 theme system in place
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Architecture Plan](#2-architecture-plan)
3. [Required Libraries](#3-required-libraries)
4. [File Structure Changes](#4-file-structure-changes)
5. [Feature Specifications](#5-feature-specifications)
6. [UI Design](#6-ui-design)
7. [Implementation Roadmap](#7-implementation-roadmap)
8. [Risk Analysis](#8-risk-analysis)
9. [Testing Checklist](#9-testing-checklist)
10. [Approval Gate](#10-approval-gate)
---
## 1. Executive Summary
Phase 4 adds **client-side document I/O** without changing the Phase 1 highlight pipeline. All import paths produce **plain text** loaded through existing `selection.js` APIs. All export paths read **plain text** via `getEditorText()`. The offset-based renderer, cursor preservation, and analyze flow remain untouched.
| Format | Direction | Library | Complexity |
|--------|-----------|---------|------------|
| `.txt` | Import + Export | Native `FileReader` + Blob | Low |
| `.docx` | Import | Mammoth.js | Medium |
| `.docx` | Export | docx.js | Medium–High (RTL) |
| `.pdf` | Export | html2pdf.js (recommended) | Medium–High (Arabic) |
**Out of scope for Phase 4**: Server-side conversion, cloud storage, auth, database persistence, `.doc` (legacy binary), rich formatting preservation, images/tables in DOCX.
---
## 2. Architecture Plan
### 2.1 Current Editor Data Flow (unchanged)
```
User types → contenteditable #editor-container
→ getEditorText() [selection.js]
→ /api/analyze
→ render({ text, suggestions }) [renderer.js]
→ setEditorHTML(html) [selection.js]
→ restoreSelection() [selection.js]
```
Phase 4 inserts a **parallel I/O layer** that only talks to the editor through two public seams:
| Seam | Function | Owner |
|------|----------|-------|
| Read | `getEditorText()` | `selection.js` — **do not modify** |
| Write | `setEditorHTML(escapeHtml(text))` + trigger analysis | via new `loadDocumentText()` in `editor.js` |
### 2.2 Proposed Document Layer
```
┌─────────────────────────────────────────────────────────────┐
│ UI: Import button · Export dropdown · hidden │
└──────────────────────────┬──────────────────────────────────┘
│
┌────────────▼────────────┐
│ documents.js (facade) │
│ initDocuments() │
└────────────┬────────────┘
┌───────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ import.js │ │ export.js │ │ utils.js │
│ · importTxt│ │ · exportTxt│ │ · download│
│ · importDocx│ │ · exportDocx│ │ · filename│
└──────┬──────┘ │ · exportPdf│ └───────────┘
│ └──────┬──────┘
│ │
└───────┬───────┘
▼
loadDocumentText(plainText) getEditorText()
│ ▲
▼ │
editor.js ─────────────────────┘
(new wrapper only — no renderer changes)
│
▼
selection.js · renderer.js (UNTOUCHED)
```
### 2.3 `loadDocumentText()` Contract (new in `editor.js`)
Single entry point for all imports. Keeps renderer/selection logic isolated.
```javascript
/**
* Load plain text into editor — used by document import only.
* @param {string} text - UTF-8 plain text
* @param {object} options - { analyze: true, filename: 'doc.txt' }
*/
function loadDocumentText(text, options = {}) {
const normalized = normalizeImportedText(text); // CRLF → LF, trim BOM
setEditorHTML(escapeHtml(normalized));
window.currentSuggestions = [];
updatePlaceholder();
updateEditorStats();
updateSuggestionCounts(0, 0, 0);
updateWritingScore(0, 0, 0);
updateSuggestionsList([]);
hideTooltip();
if (options.analyze !== false) {
analyzeTextDelayed();
}
}
```
**Rules:**
- Always pass through `escapeHtml()` before `setEditorHTML()` (XSS safety)
- Never call `render()` directly from import layer
- After load, debounced `analyzeText()` runs as if user typed
### 2.4 Paragraph Model
Internal representation remains **plain text with `\n` paragraph breaks**:
| Source | Paragraph handling |
|--------|-------------------|
| TXT | Preserve `\n` as-is |
| DOCX import (Mammoth) | `mammoth.extractRawText()` — paragraphs separated by `\n\n` |
| DOCX export (docx.js) | Split on `\n\n` or `\n` → one `Paragraph` per block |
| PDF export | Render editor DOM or text block with `dir="rtl"` |
### 2.5 Backend Constraint
`/api/analyze` enforces `MAX_TEXT_LENGTH = 5000` characters (`app.py`). Imports may load longer documents for **editing/export**, but analysis will only process what the API accepts. Phase 4 should:
1. Show a non-blocking warning if imported text exceeds 5000 chars
2. Still load full text into editor (export works on full content)
3. Optionally analyze first 5000 chars only — **defer to Phase 4.1** if needed
---
## 3. Required Libraries
### 3.1 Recommended Versions (CDN for Vanilla JS)
| Library | Version | Purpose | CDN |
|---------|---------|---------|-----|
| **Mammoth.js** | 1.8.x | DOCX → text | `https://cdn.jsdelivr.net/npm/mammoth@1.8.0/mammoth.browser.min.js` |
| **docx** | 8.5.x | Build DOCX | `https://unpkg.com/docx@8.5.0/build/index.umd.js` |
| **file-saver** | 2.0.5 | Trigger download | `https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js` |
| **html2pdf.js** | 0.10.2 | PDF via HTML render | `https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.2/html2pdf.bundle.min.js` |
### 3.2 Library Evaluation
#### Mammoth.js (DOCX Import) — **Selected**
```javascript
mammoth.extractRawText({ arrayBuffer })
.then(result => result.value); // plain text, paragraphs as \n\n
```
- Ignores styling, tables, images by design (`extractRawText`)
- Works in browser with `FileReader.readAsArrayBuffer`
- No server required
#### docx.js (DOCX Export) — **Selected**
```javascript
const doc = new docx.Document({
sections: [{
properties: { rightToLeft: true },
children: paragraphs
}]
});
docx.Packer.toBlob(doc).then(blob => saveAs(blob, 'document.docx'));
```
- Native RTL section support via `rightToLeft: true`
- Paragraph-level Arabic text
- Requires mapping `\n` splits to `docx.Paragraph` + `docx.TextRun`
#### PDF Export — **html2pdf.js (recommended over raw jsPDF)**
| Approach | Arabic support | Searchable text | Effort |
|----------|---------------|-----------------|--------|
| jsPDF alone | Poor (no Arabic glyphs) | Yes | High (embed Amiri font) |
| jsPDF + custom Arabic font | Good | Yes | High (~500KB font) |
| **html2pdf.js** (html2canvas + jsPDF) | **Good** (browser renders Cairo) | No (image-based) | Medium |
| Browser print dialog | Good | Yes | Low UX |
**Recommendation**: Start with **html2pdf.js** rendering a cloned off-screen RTL div with editor plain text + Cairo font. Upgrade to jsPDF + Amiri font in Phase 4.1 if searchable PDF is required.
### 3.3 No New Python Dependencies
All conversion is client-side. Flask continues to serve static files only.
---
## 4. File Structure Changes
```
src/
├── js/
│ ├── renderer.js (unchanged)
│ ├── selection.js (unchanged)
│ ├── editor.js (+ loadDocumentText, export hook)
│ ├── documents/
│ │ ├── documents.js # init, UI event wiring
│ │ ├── import.js # importTxt, importDocx
│ │ ├── export.js # exportTxt, exportDocx, exportPdf
│ │ └── doc-utils.js # normalizeText, downloadBlob, defaultFilename
│ ├── theme.js
│ └── ui.js
├── css/
│ └── components.css (+ .doc-dropdown, .doc-menu-item)
└── index.html (+ script tags, toolbar UI, hidden file input)
```
### 4.1 New Files
| File | ~Lines | Responsibility |
|------|--------|----------------|
| `documents/documents.js` | 80 | `initDocuments()`, wire buttons |
| `documents/import.js` | 120 | TXT + DOCX import |
| `documents/export.js` | 180 | TXT + DOCX + PDF export |
| `documents/doc-utils.js` | 60 | Shared helpers |
### 4.2 Modified Files
| File | Change |
|------|--------|
| `editor.js` | Add `loadDocumentText()` only (~25 lines) |
| `index.html` | Toolbar UI, CDN scripts, `initDocuments()` call |
| `components.css` | Dropdown + import button styles (~80 lines) |
### 4.3 Untouched Files
- `renderer.js` — no changes
- `selection.js` — no changes
- `app.py` — no changes (optional future: raise `MAX_TEXT_LENGTH`)
---
## 5. Feature Specifications
### 5.1 TXT Import
```javascript
function importTxt(file) {
const reader = new FileReader();
reader.onload = (e) => loadDocumentText(e.target.result, { filename: file.name });
reader.readAsText(file, 'UTF-8');
}
```
| Requirement | Implementation |
|-------------|----------------|
| Upload `.txt` | Hidden `` |
| FileReader | `readAsText(file, 'UTF-8')` |
| Load into editor | `loadDocumentText()` |
| Preserve architecture | No renderer/selection changes |
| BOM handling | Strip `\uFEFF` prefix in `normalizeImportedText()` |
### 5.2 TXT Export
```javascript
function exportTxt() {
const text = getEditorText();
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
downloadBlob(blob, defaultFilename('txt'));
}
```
| Requirement | Implementation |
|-------------|----------------|
| UTF-8 | `Blob` with `charset=utf-8` |
| Arabic preserved | UTF-8 encoding (no `escape` on export) |
| Download | `file-saver` `saveAs()` or `` + `URL.createObjectURL` |
### 5.3 DOCX Import
```javascript
async function importDocx(file) {
const arrayBuffer = await file.arrayBuffer();
const result = await mammoth.extractRawText({ arrayBuffer });
if (result.messages.length) console.warn('Mammoth warnings:', result.messages);
loadDocumentText(result.value, { filename: file.name });
}
```
| Requirement | Implementation |
|-------------|----------------|
| Mammoth.js | `extractRawText` only |
| Text only | No `convertToHtml` |
| Ignore tables/images | Default Mammoth raw text behavior |
| Paragraphs | Normalize `\n\n` → preserve |
**Accept attribute**: `.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document`
### 5.4 DOCX Export
```javascript
async function exportDocx() {
const text = getEditorText();
const paragraphs = text.split(/\n\s*\n/).filter(Boolean).map(block =>
new docx.Paragraph({
bidirectional: true,
children: [new docx.TextRun({ text: block, rightToLeft: true, font: 'Arial' })],
alignment: docx.AlignmentType.RIGHT
})
);
const doc = new docx.Document({
sections: [{ properties: { rightToLeft: true }, children: paragraphs }]
});
const blob = await docx.Packer.toBlob(doc);
saveAs(blob, defaultFilename('docx'));
}
```
| Requirement | Implementation |
|-------------|----------------|
| docx.js | `Document`, `Paragraph`, `TextRun`, `Packer` |
| Paragraphs | Split on double newline (fallback: single `\n`) |
| RTL | `rightToLeft: true` on section + `bidirectional: true` on paragraphs |
| Font | Arial or Traditional Arabic (Word-safe) |
### 5.5 PDF Export
```javascript
async function exportPdf() {
const text = getEditorText();
const el = buildPdfExportNode(text); // off-screen div, dir=rtl, Cairo font
await html2pdf().set({
margin: 15,
filename: defaultFilename('pdf'),
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(el).save();
}
```
| Requirement | Implementation |
|-------------|----------------|
| Arabic rendering | Clone styled RTL node with Cairo font |
| Content source | `getEditorText()` (plain text, not highlighted HTML) |
| Theme | Export node uses light theme tokens for readability |
---
## 6. UI Design
### 6.1 Toolbar Layout (proposed)
```
┌──────────────────────────────────────────────────────────────────┐
│ [كتابة] [تلخيص] │ [↑ استيراد] [↓ تصدير ▾] │ جاري... │ ١٢٣ كلمة │
└──────────────────────────────────────────────────────────────────┘
```
**Import button** (`btn-ghost`):
- Label: `استيراد`
- Icon: upload arrow
- Opens file picker (`.txt,.docx`)
- `aria-label="استيراد ملف نصي أو Word"`
**Export dropdown** (`btn-ghost` + menu):
```
تصدير ▾
├── ملف نصي (.txt)
├── Word (.docx)
└── PDF (.pdf)
```
### 6.2 Design System Compliance
| Element | Classes |
|---------|---------|
| Import button | `.btn-ghost`, `.doc-btn` |
| Export trigger | `.btn-ghost`, `.doc-dropdown__trigger` |
| Menu panel | `.surface-card`, `.doc-dropdown__menu` |
| Menu items | `.doc-dropdown__item` |
| Focus | `:focus-visible` via existing tokens |
| Theme | `var(--color-surface)`, `var(--color-border)`, `var(--color-text-primary)` |
### 6.3 Mobile
- Import + Export move to editor footer alongside `مسح الكل` / `نسخ النص`
- Export dropdown opens as bottom sheet on `<640px` (reuse Phase 2 pattern)
### 6.4 User Feedback
| Event | Feedback |
|-------|----------|
| Import success | Toast: `تم تحميل الملف` |
| Import error | Toast: `تعذر قراءة الملف` |
| Export success | Browser download (implicit) |
| File too large for analysis | Banner: `النص أطول من ٥٠٠٠ حرف — التحليل على الجزء الأول فقط` |
| Empty export | Disable export items when editor empty |
---
## 7. Implementation Roadmap
### Phase 4.1 — Foundation (1 day)
| Task | Verify |
|------|--------|
| Create `documents/doc-utils.js` | Unit: `normalizeImportedText`, `downloadBlob` |
| Add `loadDocumentText()` to `editor.js` | Import "مرحبا" → editor shows text, analyze runs |
| Add CDN scripts to `index.html` | Libraries load without console errors |
### Phase 4.2 — TXT I/O (0.5 day)
| Task | Verify |
|------|--------|
| `importTxt()` + file input | Arabic .txt round-trips correctly |
| `exportTxt()` | Downloaded file opens in Notepad with correct Arabic |
### Phase 4.3 — DOCX Import (1 day)
| Task | Verify |
|------|--------|
| Integrate Mammoth.js | Sample .docx loads as plain text |
| Error handling | Corrupt file shows user message |
| Paragraph breaks | Multi-paragraph doc preserves structure |
### Phase 4.4 — DOCX Export (1 day)
| Task | Verify |
|------|--------|
| Integrate docx.js | File opens in Word |
| RTL verification | Arabic aligns right in Word |
| Paragraph mapping | Blank lines → new paragraphs |
### Phase 4.5 — PDF Export (1–2 days)
| Task | Verify |
|------|--------|
| html2pdf.js integration | PDF downloads |
| Arabic visual check |-glyphs render correctly |
| Multi-page | Long text paginates |
### Phase 4.6 — UI + Polish (1 day)
| Task | Verify |
|------|--------|
| Toolbar import/export buttons | Matches design system both themes |
| Mobile layout | Footer placement works |
| Empty state / loading states | Disabled when appropriate |
| Toast notifications | Success/error feedback |
### Phase 4.7 — QA (0.5 day)
| Task | Verify |
|------|--------|
| Full testing checklist | All pass |
| `node test_renderer.js` | Still passes (no regression) |
| Manual Phase 1 tests | Duplicate highlights, cursor, XSS |
**Total estimate**: 6–7 working days
---
## 8. Risk Analysis
| # | Risk | Severity | Likelihood | Mitigation |
|---|------|----------|------------|------------|
| R1 | **Arabic PDF garbled** | High | Medium | Use html2pdf with Cairo font on RTL clone; fallback Phase 4.1: jsPDF + Amiri |
| R2 | **DOCX export RTL broken in Word** | Medium | Medium | Test `rightToLeft` section + `bidirectional` paragraphs; use Word-safe font |
| R3 | **Mammoth loses complex DOCX structure** | Low | High | Expected — document "text only" scope; show warning if messages |
| R4 | **Import XSS** | High | Low | Always `escapeHtml()` before `setEditorHTML()` |
| R5 | **Import breaks cursor/offsets** | Medium | Low | Use `loadDocumentText()` → `setEditorHTML` + fresh analyze (same as clear+type) |
| R6 | **File > 5000 chars breaks analyze** | Medium | High | Warn user; full text still editable/exportable |
| R7 | **CDN library unavailable offline** | Medium | Medium | Vendor copies in `src/js/vendor/` for graduation demo |
| R8 | **docx.js UMD global name mismatch** | Low | Medium | Verify `window.docx` after script load in dev |
| R9 | **Large file memory** | Low | Low | Client-side limit ~2MB; reject with message |
| R10 | **Phase 1 regression** | High | Low | Do not touch `renderer.js` / `selection.js`; run existing tests |
### 8.1 Critical Path
```
TXT import/export (low risk, validates architecture)
→ DOCX import (Mammoth)
→ DOCX export (RTL)
→ PDF export (Arabic rendering) ← highest risk, schedule last
```
---
## 9. Testing Checklist
### 9.1 TXT
- [ ] Import UTF-8 Arabic `.txt` — text appears correctly in editor
- [ ] Import UTF-8 with BOM — BOM stripped, text correct
- [ ] Import Windows-1256 file — detect mojibake or reject gracefully
- [ ] Export `.txt` — re-import round-trip preserves content
- [ ] Export empty editor — button disabled or shows error
- [ ] Import triggers `analyzeTextDelayed()` — highlights appear
### 9.2 DOCX Import
- [ ] Import simple Arabic `.docx` — text loads
- [ ] Multi-paragraph document — paragraph breaks preserved
- [ ] DOCX with images/tables — text extracted, no crash
- [ ] Invalid/corrupt file — error toast, editor unchanged
- [ ] `.doc` (legacy) — rejected by accept filter
### 9.3 DOCX Export
- [ ] Export opens in Microsoft Word without error
- [ ] Export opens in LibreOffice Writer
- [ ] Arabic text is right-aligned
- [ ] Paragraph breaks match editor
- [ ] Empty lines preserved as empty paragraphs
- [ ] Re-import exported DOCX — content matches (round-trip via Mammoth)
### 9.4 PDF Export
- [ ] PDF downloads with correct filename
- [ ] Arabic glyphs render correctly (visual inspection)
- [ ] Multi-page document paginates
- [ ] Long Arabic text (2000+ chars) exports without truncation
- [ ] Empty editor — export disabled
### 9.5 UI / Theme
- [ ] Import button visible in dark theme
- [ ] Import button visible in light theme
- [ ] Export dropdown readable in both themes
- [ ] Keyboard: Tab reaches import/export controls
- [ ] Mobile: buttons accessible in footer
- [ ] `aria-label` on import and export controls
### 9.6 Phase 1 Regression (must pass)
- [ ] `node test_renderer.js` — all tests pass
- [ ] Type in editor after import — cursor preserved on analyze
- [ ] Duplicate word highlights (`ذهبو` ×3) — independent spans
- [ ] Click highlight → popover → apply correction
- [ ] `