Spaces:
Sleeping
Sleeping
Upload 36 files
Browse filesacabei de adicionar os arquivos para paginação do editor
- Document-Editor--master/.gitignore +2 -0
- Document-Editor--master/AI_USAGE.md +86 -0
- Document-Editor--master/ARCHITECTURE.md +272 -0
- Document-Editor--master/README.md +246 -0
- Document-Editor--master/client/.gitignore +23 -0
- Document-Editor--master/client/README.md +46 -0
- Document-Editor--master/client/package-lock.json +0 -0
- Document-Editor--master/client/package.json +44 -0
- Document-Editor--master/client/public/favicon.ico +0 -0
- Document-Editor--master/client/public/index.html +43 -0
- Document-Editor--master/client/public/logo192.png +0 -0
- Document-Editor--master/client/public/logo512.png +0 -0
- Document-Editor--master/client/public/manifest.json +25 -0
- Document-Editor--master/client/public/robots.txt +3 -0
- Document-Editor--master/client/src/App.css +163 -0
- Document-Editor--master/client/src/App.test.tsx +9 -0
- Document-Editor--master/client/src/App.tsx +611 -0
- Document-Editor--master/client/src/index.css +13 -0
- Document-Editor--master/client/src/index.tsx +19 -0
- Document-Editor--master/client/src/logo.svg +1 -0
- Document-Editor--master/client/src/paginationEngine.ts +77 -0
- Document-Editor--master/client/src/react-app-env.d.ts +1 -0
- Document-Editor--master/client/src/reportWebVitals.ts +15 -0
- Document-Editor--master/client/src/setupTests.ts +5 -0
- Document-Editor--master/client/src/textMeasurer.ts +68 -0
- Document-Editor--master/client/src/types.ts +35 -0
- Document-Editor--master/client/tsconfig.json +26 -0
- Document-Editor--master/package.json +10 -0
- Document-Editor--master/server/data/040e38fc-f5ff-448b-a38e-528b77d62c7c.json +192 -0
- Document-Editor--master/server/data/71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de.json +72 -0
- Document-Editor--master/server/data/92a85bb2-938a-41b0-9c69-1c36919da23c.json +32 -0
- Document-Editor--master/server/data/d8b5e4ae-e7c1-4d7b-a70d-45f7e9d5f2d8.json +20 -0
- Document-Editor--master/server/index.js +61 -0
- Document-Editor--master/server/package-lock.json +851 -0
- Document-Editor--master/server/package.json +18 -0
- Document-Editor--master/server/tempCodeRunnerFile.js +3 -0
Document-Editor--master/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/node_modules
|
| 2 |
+
/server/node_modules
|
Document-Editor--master/AI_USAGE.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Usage Documentation
|
| 2 |
+
|
| 3 |
+
This document details how AI tools (specifically Claude by Anthropic) were used throughout the development of this document editor, the AI tools were relied on for codegen and documentation.
|
| 4 |
+
|
| 5 |
+
## AI Tool Used
|
| 6 |
+
|
| 7 |
+
**Primary Tool**: Claude (Anthropic AI Assistant)
|
| 8 |
+
|
| 9 |
+
## Development Phases
|
| 10 |
+
|
| 11 |
+
NOTE: AI was used to generate but much of the refactoring, debugging and testing was mannual and has not been mentioned in this document. Many features and improvements were attempted and discarded due to the time constraint and those have not been mentioned in this file as well. Only the bits used are written here.
|
| 12 |
+
|
| 13 |
+
### Phase 1: Initial Architecture & Pagination
|
| 14 |
+
|
| 15 |
+
**Task**: Design and implement basic pagination system
|
| 16 |
+
|
| 17 |
+
**AI Contribution**:
|
| 18 |
+
Gave a useable skeleton schema for types.ts adn textMeasurer.ts which was reviewed and updated along the project
|
| 19 |
+
|
| 20 |
+
**Key Prompts**:
|
| 21 |
+
```
|
| 22 |
+
I am building a document editor with pagination, give me the initial static requirement files which will be used as a base throughout the project and will hold the truth about the content that the user inputs for consistency. This is for a standard A4 size document assume any general case for any missing information that you may need.
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
**Code Generated**:
|
| 26 |
+
- `paginationEngine.ts` (initial version)
|
| 27 |
+
- `textMeasurer.ts` (initial version)
|
| 28 |
+
- `types.ts` (ContentBlock, Page, Line interfaces)
|
| 29 |
+
|
| 30 |
+
### Phase 2: Bug Fixes - Pagination Overlapping Issues
|
| 31 |
+
(These errors were faced after the data structure archtecture was changed to achieve a better approach which is also closer to industry standard)
|
| 32 |
+
|
| 33 |
+
**Task**: Fix content overlapping across pages and improve React key handling as there are contenboxes with similar key and they are overlapping when the content over them reflows instead of rearranging themselves.
|
| 34 |
+
|
| 35 |
+
**AI Contribution**:
|
| 36 |
+
- Implemented unique key generation per segment
|
| 37 |
+
- Added block height calculation based on actual line count
|
| 38 |
+
- Improved auto-split logic for blocks spanning pages
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
**Code Generated**:
|
| 42 |
+
- Unique key generation: `page-${pageNumber}-block-${blockId}-instance-${count}`
|
| 43 |
+
- Auto-split useEffect (lines 25-76 in initial version)
|
| 44 |
+
- Block height calculation logic
|
| 45 |
+
- willBlockOverflowPage() and splitOverflowingBlock() functions
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
### Phase 3: Major Refactor - Data Model Separation
|
| 49 |
+
|
| 50 |
+
**Task**: Separate data model from page rendering layer to fix selection issues
|
| 51 |
+
|
| 52 |
+
**AI Contribution**:
|
| 53 |
+
- Designed new segment-based architecture
|
| 54 |
+
- Removed all block-splitting logic
|
| 55 |
+
- Implemented BlockSegment interface
|
| 56 |
+
- Updated PaginationEngine to generate layout metadata
|
| 57 |
+
- Rewrote rendering to use segments
|
| 58 |
+
|
| 59 |
+
**Key Prompts**:
|
| 60 |
+
```
|
| 61 |
+
"Refactor to separate data model from page rendering layer. Keep text blocks
|
| 62 |
+
intact - only break on user intent (Enter key). Create separate page layout
|
| 63 |
+
layer with metadata."
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
**Code Generated**:
|
| 67 |
+
- New `BlockSegment` interface with startOffset/endOffset
|
| 68 |
+
- Complete rewrite of PaginationEngine.paginate()
|
| 69 |
+
- Removed willBlockOverflowPage() and splitOverflowingBlock()
|
| 70 |
+
- Simplified handleInput() to not check for overflow
|
| 71 |
+
- New segment-based rendering logic
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
### Phase 4: Feature Additions
|
| 75 |
+
|
| 76 |
+
**Task**: Add clear document button and improve UX
|
| 77 |
+
|
| 78 |
+
**AI Contribution**:
|
| 79 |
+
- Implemented handleClear function with confirmation dialog
|
| 80 |
+
- Added red-styled clear button to UI
|
| 81 |
+
- Added user-select CSS for cross-block selection
|
| 82 |
+
|
| 83 |
+
**Code Generated**:
|
| 84 |
+
- handleClear() function with window.confirm
|
| 85 |
+
- Clear button in controls with custom styling
|
| 86 |
+
- userSelect CSS properties for all browsers
|
Document-Editor--master/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Architecture Documentation
|
| 2 |
+
|
| 3 |
+
## Editor State Model
|
| 4 |
+
|
| 5 |
+
### State Hierarchy
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
App Component (Root)
|
| 9 |
+
├─ blocks: ContentBlock[] // Source of truth for content
|
| 10 |
+
├─ pages: Page[] // Derived state from pagination
|
| 11 |
+
├─ docId: string // Current document ID
|
| 12 |
+
├─ cursorPositionRef: Map<blockId, absoluteOffset> // Cursor tracking
|
| 13 |
+
└─ isRestoringCursorRef: boolean // Race condition guard
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
### State Flow
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
User Input → handleInput() → Update blocks[] → Trigger Pagination
|
| 20 |
+
↓
|
| 21 |
+
pages[] calculated
|
| 22 |
+
↓
|
| 23 |
+
React re-renders
|
| 24 |
+
↓
|
| 25 |
+
Cursor restoration
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### ContentBlock (Data Model)
|
| 29 |
+
|
| 30 |
+
**Philosophy**: Blocks represent *semantic* content units (paragraphs), not visual layout.
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
interface ContentBlock {
|
| 34 |
+
id: string; // "block-1701234567890-0.123456"
|
| 35 |
+
text: string; // Full paragraph text (unbounded length)
|
| 36 |
+
}
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
**Lifecycle**:
|
| 40 |
+
- Created: User presses Enter (splits current block)
|
| 41 |
+
- Modified: User types (text updates)
|
| 42 |
+
- Deleted: User backspaces at block start (merges with previous)
|
| 43 |
+
- **Never split due to pagination**
|
| 44 |
+
|
| 45 |
+
### BlockSegment (Rendering Model)
|
| 46 |
+
|
| 47 |
+
**Philosophy**: Segments are *ephemeral* or complete views of block data for rendering.
|
| 48 |
+
|
| 49 |
+
```typescript
|
| 50 |
+
interface BlockSegment {
|
| 51 |
+
blockId: string; // References ContentBlock
|
| 52 |
+
startOffset: number; // Character position in block.text
|
| 53 |
+
endOffset: number; // Character position in block.text
|
| 54 |
+
startLine: number; // Line index within block
|
| 55 |
+
endLine: number; // Line index within block
|
| 56 |
+
y: number; // Absolute Y on page
|
| 57 |
+
height: number; // Pixel height
|
| 58 |
+
lines: string[]; // Actual visible text
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
**Properties**:
|
| 63 |
+
- Calculated during pagination (not stored)
|
| 64 |
+
- Multiple segments can reference same block
|
| 65 |
+
- Each segment renders only its portion of block text
|
| 66 |
+
- Segments on different pages have different offsets
|
| 67 |
+
|
| 68 |
+
## Pagination Algorithm
|
| 69 |
+
|
| 70 |
+
The `PaginationEngine` performs pagination by processing content blocks sequentially, breaking each block's text into lines that fit the page width using `TextMeasurer`. It tracks the current vertical position (`currentY`) and places lines on the active page until reaching the bottom margin. When a line won't fit, it finalizes the current page, creates a new one, and continues from the top margin. Each block is split into segments that record character offsets and line boundaries, allowing the engine to track exactly which portion of each block appears on each page.
|
| 71 |
+
|
| 72 |
+
### Input & Output
|
| 73 |
+
|
| 74 |
+
**Input**: `ContentBlock[]` (array of paragraphs)
|
| 75 |
+
**Output**: `Page[]` (array of pages with segments)
|
| 76 |
+
|
| 77 |
+
### Algorithm Steps
|
| 78 |
+
|
| 79 |
+
#### Phase 1: Text Measurement
|
| 80 |
+
|
| 81 |
+
1. Canvas is used for Measuring text with it's inbuilt function ctx.measureText(text),
|
| 82 |
+
2. This phase has:
|
| 83 |
+
Word-wrapping algorithm
|
| 84 |
+
Preserves whitespace and newlines
|
| 85 |
+
Returns array of line strings
|
| 86 |
+
|
| 87 |
+
#### Phase 2: Line Breaking
|
| 88 |
+
|
| 89 |
+
For each block:
|
| 90 |
+
1. Split text by explicit newlines (`\n`)
|
| 91 |
+
2. For each paragraph:
|
| 92 |
+
- Split into words (preserving spaces)
|
| 93 |
+
- Build lines until width exceeds `maxWidth`
|
| 94 |
+
- Track character offsets for each line
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
#### Phase 3: Page Layout
|
| 98 |
+
|
| 99 |
+
```javascript
|
| 100 |
+
let currentY = PAGE_CONFIG.marginTop;
|
| 101 |
+
let currentPage = { pageNumber: 1, segments: [] };
|
| 102 |
+
|
| 103 |
+
for (const block of blocks) {
|
| 104 |
+
const lines = measurer.breakIntoLines(block.text, contentWidth);
|
| 105 |
+
|
| 106 |
+
let segmentLines = [];
|
| 107 |
+
let segmentStartLine = 0;
|
| 108 |
+
|
| 109 |
+
for (let i = 0; i < lines.length; i++) {
|
| 110 |
+
if (currentY + lineHeight > pageBottom) {
|
| 111 |
+
// Finalize segment
|
| 112 |
+
currentPage.segments.push({
|
| 113 |
+
blockId: block.id,
|
| 114 |
+
startOffset: lineOffsets[segmentStartLine].start,
|
| 115 |
+
endOffset: lineOffsets[i-1].end,
|
| 116 |
+
lines: segmentLines,
|
| 117 |
+
y: segmentStartY,
|
| 118 |
+
height: (i - segmentStartLine) * lineHeight
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
// New page
|
| 122 |
+
pages.push(currentPage);
|
| 123 |
+
currentPage = { pageNumber: pages.length + 1, segments: [] };
|
| 124 |
+
currentY = PAGE_CONFIG.marginTop;
|
| 125 |
+
|
| 126 |
+
// Reset segment tracking
|
| 127 |
+
segmentLines = [];
|
| 128 |
+
segmentStartLine = i;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
segmentLines.push(lines[i]);
|
| 132 |
+
currentY += lineHeight;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Finalize last segment of block
|
| 136 |
+
currentPage.segments.push({ /* ... */ });
|
| 137 |
+
}
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
### Edge Cases Handled
|
| 141 |
+
|
| 142 |
+
1. **Empty blocks**: Rendered as single blank line
|
| 143 |
+
2. **Very long words**: Break at character boundary if no spaces
|
| 144 |
+
3. **Multi-page blocks**: Single block can span 10+ pages
|
| 145 |
+
|
| 146 |
+
## Rendering Strategy
|
| 147 |
+
|
| 148 |
+
### Segment Rendering
|
| 149 |
+
|
| 150 |
+
Each segment is an independent `contentEditable` div:
|
| 151 |
+
|
| 152 |
+
**Critical**: Renders `segment.lines`, NOT full `block.text`
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
### Cursor Persistence
|
| 156 |
+
|
| 157 |
+
**Challenge**: React re-renders destroy DOM elements, losing cursor position.
|
| 158 |
+
|
| 159 |
+
**Solution**: Three-phase restoration
|
| 160 |
+
|
| 161 |
+
```javascript
|
| 162 |
+
// Phase 1: Save (during input)
|
| 163 |
+
const cursorOffset = getCursorOffset(element);
|
| 164 |
+
const absoluteOffset = segmentStart + cursorOffset;
|
| 165 |
+
cursorPositionRef.current.set(blockId, absoluteOffset);
|
| 166 |
+
|
| 167 |
+
// Phase 2: Re-render
|
| 168 |
+
// React updates DOM, cursor position lost
|
| 169 |
+
|
| 170 |
+
// Phase 3: Restore (after render)
|
| 171 |
+
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
**Race Condition Prevention**:
|
| 175 |
+
- `isRestoringCursorRef`: Guards against concurrent restorations
|
| 176 |
+
- Input blocking during restoration
|
| 177 |
+
- Cleared cursor map after each restoration
|
| 178 |
+
|
| 179 |
+
## Persistence Flow
|
| 180 |
+
|
| 181 |
+
### Save Flow
|
| 182 |
+
|
| 183 |
+
```
|
| 184 |
+
User clicks "Save"
|
| 185 |
+
↓
|
| 186 |
+
POST /documents { title, content: blocks[] }
|
| 187 |
+
↓
|
| 188 |
+
Server generates UUID
|
| 189 |
+
↓
|
| 190 |
+
Server writes JSON file: data/{uuid}.json
|
| 191 |
+
↓
|
| 192 |
+
Server returns { id: uuid }
|
| 193 |
+
↓
|
| 194 |
+
Client displays document ID
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
### Load Flow
|
| 198 |
+
|
| 199 |
+
```
|
| 200 |
+
User enters document ID
|
| 201 |
+
↓
|
| 202 |
+
GET /documents/{id}
|
| 203 |
+
↓
|
| 204 |
+
Server reads data/{id}.json
|
| 205 |
+
↓
|
| 206 |
+
Server returns { id, title, content, created_at, updated_at }
|
| 207 |
+
↓
|
| 208 |
+
Client: setBlocks(response.content)
|
| 209 |
+
↓
|
| 210 |
+
Pagination recalculates
|
| 211 |
+
↓
|
| 212 |
+
Editor displays loaded content
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
### Storage Format
|
| 216 |
+
|
| 217 |
+
File: `server/data/71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de.json`
|
| 218 |
+
|
| 219 |
+
```json
|
| 220 |
+
{
|
| 221 |
+
"id": "71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de",
|
| 222 |
+
"title": "My Document",
|
| 223 |
+
"content": [
|
| 224 |
+
{
|
| 225 |
+
"id": "block-1701234567890-0.123",
|
| 226 |
+
"text": "First paragraph with potentially very long text..."
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"id": "block-1701234567891-0.456",
|
| 230 |
+
"text": "Second paragraph..."
|
| 231 |
+
}
|
| 232 |
+
],
|
| 233 |
+
"created_at": "2024-11-26T10:30:00.000Z",
|
| 234 |
+
"updated_at": "2024-11-26T10:30:00.000Z"
|
| 235 |
+
}
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
**Benefits**:
|
| 239 |
+
- Human-readable
|
| 240 |
+
- Version control friendly
|
| 241 |
+
- Easy debugging
|
| 242 |
+
- Simple backup/restore
|
| 243 |
+
|
| 244 |
+
**Trade-offs**:
|
| 245 |
+
- Not suitable for large-scale production
|
| 246 |
+
- No concurrent write protection
|
| 247 |
+
- File I/O overhead
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
**Current Status**: Optimized for documents up to 50-100 pages. Beyond that, consider implementing above optimizations.
|
| 251 |
+
|
| 252 |
+
## Design Decisions
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
### Why Canvas for Text Measurement?
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
**Canvas API Benefits**:
|
| 259 |
+
- Pixel-perfect accuracy
|
| 260 |
+
- No DOM manipulation
|
| 261 |
+
- Fast (native code)
|
| 262 |
+
- Matches actual rendering
|
| 263 |
+
|
| 264 |
+
### Why File-Based Storage?
|
| 265 |
+
|
| 266 |
+
**Decision**: Simplicity for prototype/demo
|
| 267 |
+
- Fast to implement
|
| 268 |
+
- Zero configuration
|
| 269 |
+
- Easy to inspect
|
| 270 |
+
- Human-readable
|
| 271 |
+
|
| 272 |
+
**Production Alternative**: PostgreSQL for block content
|
Document-Editor--master/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Document Editor - Multi-Page Text Editor with Real-Time Pagination
|
| 2 |
+
|
| 3 |
+
Try It out Here: https://document-editor-qqg3-r8zfcn2ag.vercel.app/
|
| 4 |
+
|
| 5 |
+
A sophisticated document editor with automatic pagination, multi-block editing, and persistent storage. Text flows naturally across pages with real-time layout calculation.
|
| 6 |
+
|
| 7 |
+
## Prerequisites
|
| 8 |
+
|
| 9 |
+
- Node.js 16+ and npm
|
| 10 |
+
- Modern browser (Chrome, Firefox, Safari, Edge)
|
| 11 |
+
|
| 12 |
+
## Quick Start
|
| 13 |
+
|
| 14 |
+
```bash
|
| 15 |
+
# Install dependencies
|
| 16 |
+
cd server && npm install express
|
| 17 |
+
cd ../client && npm install
|
| 18 |
+
|
| 19 |
+
# Start backend (Terminal 1)
|
| 20 |
+
cd server && node index.js
|
| 21 |
+
|
| 22 |
+
# Start frontend (Terminal 2)
|
| 23 |
+
cd client && npm start
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
The editor will open at `http://localhost:3000` with the backend API at `http://localhost:3001`.
|
| 27 |
+
|
| 28 |
+
## Project Structure
|
| 29 |
+
|
| 30 |
+
```
|
| 31 |
+
Document-Editor-/
|
| 32 |
+
├── client/ # React TypeScript frontend
|
| 33 |
+
│ ├── src/
|
| 34 |
+
│ │ ├── App.tsx # Main editor component
|
| 35 |
+
│ │ ├── paginationEngine.ts # Pagination algorithm
|
| 36 |
+
│ │ ├── textMeasurer.ts # Text measurement utilities
|
| 37 |
+
│ │ ├── types.ts # TypeScript interfaces
|
| 38 |
+
│ │ └── App.css # Styling
|
| 39 |
+
│ └── package.json
|
| 40 |
+
├── server/ # Express.js backend
|
| 41 |
+
│ ├── index.js # API server
|
| 42 |
+
│ ├── data/ # Document storage (JSON files)
|
| 43 |
+
│ └── package.json
|
| 44 |
+
├── README.md
|
| 45 |
+
├── ARCHITECTURE.md
|
| 46 |
+
└── AI_USAGE.md
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Features
|
| 50 |
+
|
| 51 |
+
### Core Functionality
|
| 52 |
+
- ✅ Multi-page document editing with automatic pagination
|
| 53 |
+
- ✅ Real-time text reflow across pages
|
| 54 |
+
- ✅ Cursor persistence across edits and page boundaries
|
| 55 |
+
- ✅ Multi-block selection and deletion
|
| 56 |
+
- ✅ Block-based content model (paragraphs remain intact)
|
| 57 |
+
- ✅ Save/Load documents with unique IDs
|
| 58 |
+
- ✅ Clear document functionality
|
| 59 |
+
|
| 60 |
+
### Technical Highlights
|
| 61 |
+
- **Segment-Based Rendering**: Blocks can span pages without splitting in data model
|
| 62 |
+
- **Accurate Text Measurement**: Canvas-based character width calculation
|
| 63 |
+
- **Cursor Restoration**: Complex logic to maintain cursor position across re-renders
|
| 64 |
+
- **Race Condition Prevention**: Guards against concurrent cursor restoration
|
| 65 |
+
- **Keyboard Navigation**: Arrow keys, Enter, Backspace work across pages
|
| 66 |
+
|
| 67 |
+
## API Endpoints
|
| 68 |
+
|
| 69 |
+
### POST /documents
|
| 70 |
+
Create and save a new document.
|
| 71 |
+
|
| 72 |
+
**Request:**
|
| 73 |
+
```json
|
| 74 |
+
{
|
| 75 |
+
"title": "My Document",
|
| 76 |
+
"content": [
|
| 77 |
+
{ "id": "block-1", "text": "First paragraph..." },
|
| 78 |
+
{ "id": "block-2", "text": "Second paragraph..." }
|
| 79 |
+
]
|
| 80 |
+
}
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
**Response:**
|
| 84 |
+
```json
|
| 85 |
+
{
|
| 86 |
+
"id": "71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de"
|
| 87 |
+
}
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### GET /documents/:id
|
| 91 |
+
Retrieve a saved document by ID.
|
| 92 |
+
|
| 93 |
+
**Response:**
|
| 94 |
+
```json
|
| 95 |
+
{
|
| 96 |
+
"id": "71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de",
|
| 97 |
+
"title": "My Document",
|
| 98 |
+
"content": [
|
| 99 |
+
{ "id": "block-1", "text": "First paragraph..." },
|
| 100 |
+
{ "id": "block-2", "text": "Second paragraph..." }
|
| 101 |
+
],
|
| 102 |
+
"created_at": "2024-11-26T10:30:00.000Z",
|
| 103 |
+
"updated_at": "2024-11-26T10:30:00.000Z"
|
| 104 |
+
}
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
## Data Model
|
| 108 |
+
|
| 109 |
+
### ContentBlock
|
| 110 |
+
```typescript
|
| 111 |
+
interface ContentBlock {
|
| 112 |
+
id: string; // Unique identifier (e.g., "block-1234567890")
|
| 113 |
+
text: string; // Complete paragraph text (can span multiple pages)
|
| 114 |
+
}
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
**Key Principle**: Blocks represent logical content units (paragraphs), NOT page fragments. A block is only split when the user presses Enter, never due to pagination.
|
| 118 |
+
|
| 119 |
+
### BlockSegment (Rendering Layer)
|
| 120 |
+
```typescript
|
| 121 |
+
interface BlockSegment {
|
| 122 |
+
blockId: string; // Reference to ContentBlock
|
| 123 |
+
startOffset: number; // Character offset where segment starts
|
| 124 |
+
endOffset: number; // Character offset where segment ends
|
| 125 |
+
startLine: number; // First line index in this segment
|
| 126 |
+
endLine: number; // Last line index in this segment
|
| 127 |
+
y: number; // Y position on page
|
| 128 |
+
height: number; // Height of segment
|
| 129 |
+
lines: string[]; // Visible text lines for this segment
|
| 130 |
+
}
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Page
|
| 134 |
+
```typescript
|
| 135 |
+
interface Page {
|
| 136 |
+
pageNumber: number;
|
| 137 |
+
lines: Line[]; // Legacy compatibility
|
| 138 |
+
segments: BlockSegment[]; // Primary rendering data
|
| 139 |
+
}
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
### Document (Persistence)
|
| 143 |
+
```typescript
|
| 144 |
+
interface Document {
|
| 145 |
+
id: string;
|
| 146 |
+
title: string;
|
| 147 |
+
content: ContentBlock[];
|
| 148 |
+
created_at: string;
|
| 149 |
+
updated_at: string;
|
| 150 |
+
}
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
## Pagination Strategy
|
| 154 |
+
|
| 155 |
+
### Overview
|
| 156 |
+
The editor uses a **two-layer architecture**:
|
| 157 |
+
1. **Data Layer**: Immutable blocks (paragraphs)
|
| 158 |
+
2. **Rendering Layer**: Dynamic segments (visual layout)
|
| 159 |
+
|
| 160 |
+
### How It Works
|
| 161 |
+
|
| 162 |
+
#### 1. Text Measurement
|
| 163 |
+
- Uses HTML5 Canvas API for pixel-perfect text width calculation
|
| 164 |
+
- Accounts for font family, size, and browser rendering
|
| 165 |
+
- Breaks text into lines at word boundaries
|
| 166 |
+
|
| 167 |
+
#### 2. Line Breaking Algorithm
|
| 168 |
+
```
|
| 169 |
+
For each block:
|
| 170 |
+
1. Measure text with current font
|
| 171 |
+
2. Break into lines that fit page width
|
| 172 |
+
3. Track character offsets for each line
|
| 173 |
+
4. Preserve whitespace and newlines
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
#### 3. Page Assignment
|
| 177 |
+
```
|
| 178 |
+
currentY = marginTop
|
| 179 |
+
For each line:
|
| 180 |
+
If (currentY + lineHeight > pageBottom):
|
| 181 |
+
- Finalize current segment
|
| 182 |
+
- Start new page
|
| 183 |
+
- Reset Y position
|
| 184 |
+
Add line to current segment
|
| 185 |
+
currentY += lineHeight
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
#### 4. Segment Creation
|
| 189 |
+
When a block spans multiple pages, create separate segments:
|
| 190 |
+
```javascript
|
| 191 |
+
// Block spans pages 1 and 2
|
| 192 |
+
Segment 1: { blockId: "block-1", startOffset: 0, endOffset: 150, lines: [...] }
|
| 193 |
+
Segment 2: { blockId: "block-1", startOffset: 151, endOffset: 300, lines: [...] }
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
### Key Benefits
|
| 197 |
+
- ✅ **Clean Data Model**: Content structure independent of layout
|
| 198 |
+
- ✅ **Seamless Editing**: Text selection works across page boundaries
|
| 199 |
+
- ✅ **Version Control Friendly**: Changes tracked at paragraph level
|
| 200 |
+
- ✅ **Efficient Reflow**: Only pagination recalculates, data unchanged
|
| 201 |
+
|
| 202 |
+
## Cursor Persistence
|
| 203 |
+
|
| 204 |
+
Critical for user experience. Implementation uses:
|
| 205 |
+
1. **Absolute Offset Tracking**: Store cursor position relative to full block
|
| 206 |
+
2. **Segment Mapping**: Find which segment contains cursor position
|
| 207 |
+
3. **Relative Calculation**: Convert absolute offset to segment-relative position
|
| 208 |
+
4. **Race Condition Guards**: Prevent overlapping restoration attempts
|
| 209 |
+
5. **Async Restoration**: Use `requestAnimationFrame` + `setTimeout` for DOM readiness
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
### Environment Variables
|
| 213 |
+
No environment variables required. Default configuration:
|
| 214 |
+
- Backend: `http://localhost:3001`
|
| 215 |
+
- Frontend: `http://localhost:3000`
|
| 216 |
+
- Data storage: `server/data/` directory
|
| 217 |
+
|
| 218 |
+
### Known Limitations
|
| 219 |
+
- No collaborative editing (single user)
|
| 220 |
+
- No rich text formatting (plain text only)
|
| 221 |
+
- File-based storage (no database)
|
| 222 |
+
- No authentication/authorization
|
| 223 |
+
|
| 224 |
+
## Troubleshooting
|
| 225 |
+
|
| 226 |
+
**Cursor jumps to beginning**
|
| 227 |
+
- Fixed in latest version with concurrent restoration guards
|
| 228 |
+
|
| 229 |
+
**Content overlaps pages**
|
| 230 |
+
- Ensure using segment-based rendering (post-refactor architecture)
|
| 231 |
+
|
| 232 |
+
**Backend not starting**
|
| 233 |
+
- Check port 3001 is available
|
| 234 |
+
- Verify Node.js 16+ installed
|
| 235 |
+
|
| 236 |
+
**Frontend build errors**
|
| 237 |
+
- Clear cache: `rm -rf node_modules && npm install`
|
| 238 |
+
- Check Node version matches prerequisites
|
| 239 |
+
|
| 240 |
+
## License
|
| 241 |
+
|
| 242 |
+
MIT
|
| 243 |
+
|
| 244 |
+
## Contributors
|
| 245 |
+
|
| 246 |
+
Built with assistance from Claude (Anthropic AI) for architecture design, pagination algorithm implementation, and bug fixes.
|
Document-Editor--master/client/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# production
|
| 12 |
+
/build
|
| 13 |
+
|
| 14 |
+
# misc
|
| 15 |
+
.DS_Store
|
| 16 |
+
.env.local
|
| 17 |
+
.env.development.local
|
| 18 |
+
.env.test.local
|
| 19 |
+
.env.production.local
|
| 20 |
+
|
| 21 |
+
npm-debug.log*
|
| 22 |
+
yarn-debug.log*
|
| 23 |
+
yarn-error.log*
|
Document-Editor--master/client/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Getting Started with Create React App
|
| 2 |
+
|
| 3 |
+
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 4 |
+
|
| 5 |
+
## Available Scripts
|
| 6 |
+
|
| 7 |
+
In the project directory, you can run:
|
| 8 |
+
|
| 9 |
+
### `npm start`
|
| 10 |
+
|
| 11 |
+
Runs the app in the development mode.\
|
| 12 |
+
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
| 13 |
+
|
| 14 |
+
The page will reload if you make edits.\
|
| 15 |
+
You will also see any lint errors in the console.
|
| 16 |
+
|
| 17 |
+
### `npm test`
|
| 18 |
+
|
| 19 |
+
Launches the test runner in the interactive watch mode.\
|
| 20 |
+
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 21 |
+
|
| 22 |
+
### `npm run build`
|
| 23 |
+
|
| 24 |
+
Builds the app for production to the `build` folder.\
|
| 25 |
+
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 26 |
+
|
| 27 |
+
The build is minified and the filenames include the hashes.\
|
| 28 |
+
Your app is ready to be deployed!
|
| 29 |
+
|
| 30 |
+
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 31 |
+
|
| 32 |
+
### `npm run eject`
|
| 33 |
+
|
| 34 |
+
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
| 35 |
+
|
| 36 |
+
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 37 |
+
|
| 38 |
+
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
| 39 |
+
|
| 40 |
+
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
| 41 |
+
|
| 42 |
+
## Learn More
|
| 43 |
+
|
| 44 |
+
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 45 |
+
|
| 46 |
+
To learn React, check out the [React documentation](https://reactjs.org/).
|
Document-Editor--master/client/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
Document-Editor--master/client/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "client",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"dependencies": {
|
| 6 |
+
"@testing-library/dom": "^10.4.1",
|
| 7 |
+
"@testing-library/jest-dom": "^6.9.1",
|
| 8 |
+
"@testing-library/react": "^16.3.0",
|
| 9 |
+
"@testing-library/user-event": "^13.5.0",
|
| 10 |
+
"@types/jest": "^27.5.2",
|
| 11 |
+
"@types/node": "^16.18.126",
|
| 12 |
+
"@types/react": "^19.2.7",
|
| 13 |
+
"@types/react-dom": "^19.2.3",
|
| 14 |
+
"react": "^19.2.0",
|
| 15 |
+
"react-dom": "^19.2.0",
|
| 16 |
+
"react-scripts": "5.0.1",
|
| 17 |
+
"typescript": "^4.9.5",
|
| 18 |
+
"web-vitals": "^2.1.4"
|
| 19 |
+
},
|
| 20 |
+
"scripts": {
|
| 21 |
+
"start": "react-scripts start",
|
| 22 |
+
"build": "react-scripts build",
|
| 23 |
+
"test": "react-scripts test",
|
| 24 |
+
"eject": "react-scripts eject"
|
| 25 |
+
},
|
| 26 |
+
"eslintConfig": {
|
| 27 |
+
"extends": [
|
| 28 |
+
"react-app",
|
| 29 |
+
"react-app/jest"
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
"browserslist": {
|
| 33 |
+
"production": [
|
| 34 |
+
">0.2%",
|
| 35 |
+
"not dead",
|
| 36 |
+
"not op_mini all"
|
| 37 |
+
],
|
| 38 |
+
"development": [
|
| 39 |
+
"last 1 chrome version",
|
| 40 |
+
"last 1 firefox version",
|
| 41 |
+
"last 1 safari version"
|
| 42 |
+
]
|
| 43 |
+
}
|
| 44 |
+
}
|
Document-Editor--master/client/public/favicon.ico
ADDED
|
|
Document-Editor--master/client/public/index.html
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
+
<meta name="theme-color" content="#000000" />
|
| 8 |
+
<meta
|
| 9 |
+
name="description"
|
| 10 |
+
content="Web site created using create-react-app"
|
| 11 |
+
/>
|
| 12 |
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
+
<!--
|
| 14 |
+
manifest.json provides metadata used when your web app is installed on a
|
| 15 |
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 16 |
+
-->
|
| 17 |
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
| 18 |
+
<!--
|
| 19 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
| 20 |
+
It will be replaced with the URL of the `public` folder during the build.
|
| 21 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
| 22 |
+
|
| 23 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
| 24 |
+
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
+
-->
|
| 27 |
+
<title>React App</title>
|
| 28 |
+
</head>
|
| 29 |
+
<body>
|
| 30 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 31 |
+
<div id="root"></div>
|
| 32 |
+
<!--
|
| 33 |
+
This HTML file is a template.
|
| 34 |
+
If you open it directly in the browser, you will see an empty page.
|
| 35 |
+
|
| 36 |
+
You can add webfonts, meta tags, or analytics to this file.
|
| 37 |
+
The build step will place the bundled scripts into the <body> tag.
|
| 38 |
+
|
| 39 |
+
To begin the development, run `npm start` or `yarn start`.
|
| 40 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
| 41 |
+
-->
|
| 42 |
+
</body>
|
| 43 |
+
</html>
|
Document-Editor--master/client/public/logo192.png
ADDED
|
Document-Editor--master/client/public/logo512.png
ADDED
|
Document-Editor--master/client/public/manifest.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"short_name": "React App",
|
| 3 |
+
"name": "Create React App Sample",
|
| 4 |
+
"icons": [
|
| 5 |
+
{
|
| 6 |
+
"src": "favicon.ico",
|
| 7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
| 8 |
+
"type": "image/x-icon"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"src": "logo192.png",
|
| 12 |
+
"type": "image/png",
|
| 13 |
+
"sizes": "192x192"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"src": "logo512.png",
|
| 17 |
+
"type": "image/png",
|
| 18 |
+
"sizes": "512x512"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"start_url": ".",
|
| 22 |
+
"display": "standalone",
|
| 23 |
+
"theme_color": "#000000",
|
| 24 |
+
"background_color": "#ffffff"
|
| 25 |
+
}
|
Document-Editor--master/client/public/robots.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://www.robotstxt.org/robotstxt.html
|
| 2 |
+
User-agent: *
|
| 3 |
+
Disallow:
|
Document-Editor--master/client/src/App.css
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
body {
|
| 6 |
+
margin: 0;
|
| 7 |
+
padding: 0;
|
| 8 |
+
background: #f0f0f0;
|
| 9 |
+
font-family: Arial, sans-serif;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.App {
|
| 13 |
+
min-height: 100vh;
|
| 14 |
+
padding-bottom: 100px;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.controls {
|
| 18 |
+
position: sticky;
|
| 19 |
+
top: 0;
|
| 20 |
+
background: white;
|
| 21 |
+
padding: 15px 20px;
|
| 22 |
+
border-bottom: 2px solid #333;
|
| 23 |
+
display: flex;
|
| 24 |
+
gap: 10px;
|
| 25 |
+
align-items: center;
|
| 26 |
+
z-index: 1000;
|
| 27 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.controls button {
|
| 31 |
+
padding: 10px 20px;
|
| 32 |
+
font-size: 14px;
|
| 33 |
+
cursor: pointer;
|
| 34 |
+
background: #007bff;
|
| 35 |
+
color: white;
|
| 36 |
+
border: none;
|
| 37 |
+
border-radius: 4px;
|
| 38 |
+
font-weight: 500;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.controls button:hover {
|
| 42 |
+
background: #0056b3;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.controls input {
|
| 46 |
+
padding: 10px;
|
| 47 |
+
font-size: 14px;
|
| 48 |
+
width: 300px;
|
| 49 |
+
border: 1px solid #ccc;
|
| 50 |
+
border-radius: 4px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.doc-id {
|
| 54 |
+
font-size: 12px;
|
| 55 |
+
color: #666;
|
| 56 |
+
padding: 8px 12px;
|
| 57 |
+
background: #f5f5f5;
|
| 58 |
+
border-radius: 4px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.page-count {
|
| 62 |
+
font-size: 12px;
|
| 63 |
+
color: #666;
|
| 64 |
+
margin-left: auto;
|
| 65 |
+
padding: 8px 12px;
|
| 66 |
+
background: #f5f5f5;
|
| 67 |
+
border-radius: 4px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.document-container {
|
| 71 |
+
max-width: 900px;
|
| 72 |
+
margin: 40px auto;
|
| 73 |
+
padding: 0 20px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.page {
|
| 77 |
+
background: white;
|
| 78 |
+
border: 1px solid #d0d0d0;
|
| 79 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 80 |
+
margin-bottom: 20px;
|
| 81 |
+
position: relative;
|
| 82 |
+
box-sizing: border-box;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.page-content {
|
| 86 |
+
position: relative;
|
| 87 |
+
width: 100%;
|
| 88 |
+
height: 100%;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.page-number {
|
| 92 |
+
position: absolute;
|
| 93 |
+
bottom: 20px;
|
| 94 |
+
left: 50%;
|
| 95 |
+
transform: translateX(-50%);
|
| 96 |
+
font-size: 11px;
|
| 97 |
+
color: #999;
|
| 98 |
+
pointer-events: none;
|
| 99 |
+
user-select: none;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* Ensure contenteditable elements look clean */
|
| 103 |
+
[contenteditable] {
|
| 104 |
+
cursor: text;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
[contenteditable]:focus {
|
| 108 |
+
outline: none;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* [contenteditable]:empty:before {
|
| 112 |
+
content: 'Start typing...';
|
| 113 |
+
color: #ccc;
|
| 114 |
+
} */
|
| 115 |
+
|
| 116 |
+
.page-content [contenteditable] {
|
| 117 |
+
white-space: pre-wrap;
|
| 118 |
+
word-wrap: break-word;
|
| 119 |
+
tab-size: 4;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.page {
|
| 123 |
+
background: white;
|
| 124 |
+
border: 1px solid #d0d0d0;
|
| 125 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 126 |
+
margin-bottom: 20px;
|
| 127 |
+
position: relative;
|
| 128 |
+
box-sizing: border-box;
|
| 129 |
+
overflow: hidden; /* CRITICAL: Clips content at page boundaries */
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.page-content {
|
| 133 |
+
position: relative;
|
| 134 |
+
width: 100%;
|
| 135 |
+
height: 100%;
|
| 136 |
+
direction: ltr;
|
| 137 |
+
text-align: left;
|
| 138 |
+
overflow: hidden; /* CRITICAL: Ensures no overflow */
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.hint {
|
| 142 |
+
font-size: 12px;
|
| 143 |
+
color: #999;
|
| 144 |
+
margin-top: 10px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
kbd {
|
| 148 |
+
background: #f0f0f0;
|
| 149 |
+
border: 1px solid #ccc;
|
| 150 |
+
border-radius: 3px;
|
| 151 |
+
padding: 2px 6px;
|
| 152 |
+
font-family: monospace;
|
| 153 |
+
font-size: 11px;
|
| 154 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.editable-block::selection {
|
| 158 |
+
background: #b3d7ff;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.editable-block::-moz-selection {
|
| 162 |
+
background: #b3d7ff;
|
| 163 |
+
}
|
Document-Editor--master/client/src/App.test.tsx
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen } from '@testing-library/react';
|
| 3 |
+
import App from './App';
|
| 4 |
+
|
| 5 |
+
test('renders learn react link', () => {
|
| 6 |
+
render(<App />);
|
| 7 |
+
const linkElement = screen.getByText(/learn react/i);
|
| 8 |
+
expect(linkElement).toBeInTheDocument();
|
| 9 |
+
});
|
Document-Editor--master/client/src/App.tsx
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
| 2 |
+
import { ContentBlock, Page, PAGE_CONFIG } from './types';
|
| 3 |
+
import { PaginationEngine } from './paginationEngine';
|
| 4 |
+
import './App.css';
|
| 5 |
+
|
| 6 |
+
const App: React.FC = () => {
|
| 7 |
+
const [blocks, setBlocks] = useState<ContentBlock[]>([
|
| 8 |
+
{ id: Date.now().toString(), text: '' }
|
| 9 |
+
]);
|
| 10 |
+
const [pages, setPages] = useState<Page[]>([]);
|
| 11 |
+
const [docId, setDocId] = useState<string>('');
|
| 12 |
+
const [loadId, setLoadId] = useState<string>('');
|
| 13 |
+
const [engine] = useState(() => new PaginationEngine());
|
| 14 |
+
const editorRef = useRef<HTMLDivElement>(null);
|
| 15 |
+
const cursorPositionRef = useRef<Map<string, number>>(new Map());
|
| 16 |
+
const isComposingRef = useRef(false);
|
| 17 |
+
const [selectAllActive, setSelectAllActive] = useState(false);
|
| 18 |
+
|
| 19 |
+
// Repaginate whenever blocks change
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
const newPages = engine.paginate(blocks);
|
| 22 |
+
setPages(newPages);
|
| 23 |
+
}, [blocks, engine]);
|
| 24 |
+
|
| 25 |
+
// Auto-split blocks that span multiple pages
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
if (pages.length === 0) return;
|
| 28 |
+
|
| 29 |
+
// Find blocks that appear on multiple pages
|
| 30 |
+
const blockPageMap = new Map<string, Set<number>>();
|
| 31 |
+
pages.forEach(page => {
|
| 32 |
+
page.lines.forEach(line => {
|
| 33 |
+
if (!blockPageMap.has(line.blockId)) {
|
| 34 |
+
blockPageMap.set(line.blockId, new Set());
|
| 35 |
+
}
|
| 36 |
+
blockPageMap.get(line.blockId)!.add(page.pageNumber);
|
| 37 |
+
});
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// Find the first block that spans pages
|
| 41 |
+
const spanningBlockId = Array.from(blockPageMap.entries())
|
| 42 |
+
.find(([_, pageSet]) => pageSet.size > 1)?.[0];
|
| 43 |
+
|
| 44 |
+
if (spanningBlockId) {
|
| 45 |
+
const block = blocks.find(b => b.id === spanningBlockId);
|
| 46 |
+
if (!block) return;
|
| 47 |
+
|
| 48 |
+
// Find where to split - at the page boundary
|
| 49 |
+
const firstPage = Math.min(...Array.from(blockPageMap.get(spanningBlockId)!));
|
| 50 |
+
const firstPageLines = pages[firstPage - 1].lines.filter(l => l.blockId === spanningBlockId);
|
| 51 |
+
|
| 52 |
+
// Calculate how much text fits on the first page
|
| 53 |
+
const textMeasurer = new (require('./textMeasurer').TextMeasurer)();
|
| 54 |
+
const contentWidth = PAGE_CONFIG.width - PAGE_CONFIG.marginLeft - PAGE_CONFIG.marginRight;
|
| 55 |
+
const allLines = textMeasurer.breakIntoLines(block.text, contentWidth);
|
| 56 |
+
|
| 57 |
+
// Find the split point (end of lines on first page)
|
| 58 |
+
const linesOnFirstPage = firstPageLines.length;
|
| 59 |
+
const textForFirstPage = allLines.slice(0, linesOnFirstPage).join('\n');
|
| 60 |
+
const textForNextPage = allLines.slice(linesOnFirstPage).join('\n');
|
| 61 |
+
|
| 62 |
+
if (textForFirstPage && textForNextPage) {
|
| 63 |
+
const blockIndex = blocks.findIndex(b => b.id === spanningBlockId);
|
| 64 |
+
|
| 65 |
+
setBlocks(prev => {
|
| 66 |
+
const newBlocks = [...prev];
|
| 67 |
+
newBlocks[blockIndex] = { ...newBlocks[blockIndex], text: textForFirstPage };
|
| 68 |
+
newBlocks.splice(blockIndex + 1, 0, {
|
| 69 |
+
id: `block-${Date.now()}-${Math.random()}`,
|
| 70 |
+
text: textForNextPage
|
| 71 |
+
});
|
| 72 |
+
return newBlocks;
|
| 73 |
+
});
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}, [pages, blocks]);
|
| 77 |
+
|
| 78 |
+
// Restore cursor positions after render
|
| 79 |
+
useEffect(() => {
|
| 80 |
+
if (selectAllActive) return; // Don't restore cursor during select all
|
| 81 |
+
|
| 82 |
+
cursorPositionRef.current.forEach((offset, blockId) => {
|
| 83 |
+
const element = document.querySelector(`[data-block-id="${blockId}"]`) as HTMLElement;
|
| 84 |
+
if (element && document.activeElement === element) {
|
| 85 |
+
restoreCursorPosition(element, offset);
|
| 86 |
+
}
|
| 87 |
+
});
|
| 88 |
+
cursorPositionRef.current.clear();
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
const handleSave = async () => {
|
| 92 |
+
try {
|
| 93 |
+
const response = await fetch('http://localhost:3001/documents', {
|
| 94 |
+
method: 'POST',
|
| 95 |
+
headers: { 'Content-Type': 'application/json' },
|
| 96 |
+
body: JSON.stringify({
|
| 97 |
+
title: 'My Document',
|
| 98 |
+
content: blocks
|
| 99 |
+
})
|
| 100 |
+
});
|
| 101 |
+
const data = await response.json();
|
| 102 |
+
setDocId(data.id);
|
| 103 |
+
alert(`Document saved! ID: ${data.id}`);
|
| 104 |
+
} catch (err) {
|
| 105 |
+
alert('Save failed: ' + err);
|
| 106 |
+
}
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
const handleLoad = async () => {
|
| 110 |
+
if (!loadId.trim()) {
|
| 111 |
+
alert('Enter a document ID');
|
| 112 |
+
return;
|
| 113 |
+
}
|
| 114 |
+
try {
|
| 115 |
+
const response = await fetch(`http://localhost:3001/documents/${loadId}`);
|
| 116 |
+
const data = await response.json();
|
| 117 |
+
setBlocks(data.content);
|
| 118 |
+
setDocId(data.id);
|
| 119 |
+
alert('Document loaded!');
|
| 120 |
+
} catch (err) {
|
| 121 |
+
alert('Load failed: ' + err);
|
| 122 |
+
}
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
const saveCursorPosition = (element: HTMLElement): number | null => {
|
| 126 |
+
const selection = window.getSelection();
|
| 127 |
+
if (!selection || selection.rangeCount === 0) return null;
|
| 128 |
+
|
| 129 |
+
const range = selection.getRangeAt(0);
|
| 130 |
+
const preCaretRange = range.cloneRange();
|
| 131 |
+
preCaretRange.selectNodeContents(element);
|
| 132 |
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
| 133 |
+
const caretOffset = preCaretRange.toString().length;
|
| 134 |
+
|
| 135 |
+
return caretOffset;
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
const restoreCursorPosition = (element: HTMLElement, offset: number) => {
|
| 139 |
+
const selection = window.getSelection();
|
| 140 |
+
if (!selection) return;
|
| 141 |
+
|
| 142 |
+
if (!element.textContent) {
|
| 143 |
+
const range = document.createRange();
|
| 144 |
+
range.setStart(element, 0);
|
| 145 |
+
range.collapse(true);
|
| 146 |
+
selection.removeAllRanges();
|
| 147 |
+
selection.addRange(range);
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
let charCount = 0;
|
| 152 |
+
|
| 153 |
+
const traverseNodes = (node: Node): boolean => {
|
| 154 |
+
if (node.nodeType === Node.TEXT_NODE) {
|
| 155 |
+
const textLength = node.textContent?.length || 0;
|
| 156 |
+
if (charCount + textLength >= offset) {
|
| 157 |
+
const range = document.createRange();
|
| 158 |
+
const nodeOffset = Math.min(offset - charCount, textLength);
|
| 159 |
+
range.setStart(node, nodeOffset);
|
| 160 |
+
range.collapse(true);
|
| 161 |
+
selection.removeAllRanges();
|
| 162 |
+
selection.addRange(range);
|
| 163 |
+
return true;
|
| 164 |
+
}
|
| 165 |
+
charCount += textLength;
|
| 166 |
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
| 167 |
+
for (let i = 0; i < node.childNodes.length; i++) {
|
| 168 |
+
if (traverseNodes(node.childNodes[i])) {
|
| 169 |
+
return true;
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
return false;
|
| 174 |
+
};
|
| 175 |
+
|
| 176 |
+
traverseNodes(element);
|
| 177 |
+
};
|
| 178 |
+
|
| 179 |
+
// Check if block will overflow page
|
| 180 |
+
const willBlockOverflowPage = (blockId: string, newText: string): boolean => {
|
| 181 |
+
// Create temporary blocks array with the new text
|
| 182 |
+
const tempBlocks = blocks.map(b =>
|
| 183 |
+
b.id === blockId ? { ...b, text: newText } : b
|
| 184 |
+
);
|
| 185 |
+
|
| 186 |
+
// Paginate with new content
|
| 187 |
+
const tempPages = engine.paginate(tempBlocks);
|
| 188 |
+
|
| 189 |
+
// Find which pages this block appears on
|
| 190 |
+
const blockPages = new Set<number>();
|
| 191 |
+
tempPages.forEach(page => {
|
| 192 |
+
page.lines.forEach(line => {
|
| 193 |
+
if (line.blockId === blockId) {
|
| 194 |
+
blockPages.add(page.pageNumber);
|
| 195 |
+
}
|
| 196 |
+
});
|
| 197 |
+
});
|
| 198 |
+
|
| 199 |
+
// If block spans more than one page, it overflows
|
| 200 |
+
return blockPages.size > 1;
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
// Split block that overflows into current and new block
|
| 204 |
+
const splitOverflowingBlock = (blockId: string, newText: string) => {
|
| 205 |
+
const blockIndex = blocks.findIndex(b => b.id === blockId);
|
| 206 |
+
if (blockIndex === -1) return;
|
| 207 |
+
|
| 208 |
+
// Binary search to find the maximum text that fits on one page
|
| 209 |
+
let left = 0;
|
| 210 |
+
let right = newText.length;
|
| 211 |
+
let maxFitLength = 0;
|
| 212 |
+
|
| 213 |
+
while (left <= right) {
|
| 214 |
+
const mid = Math.floor((left + right) / 2);
|
| 215 |
+
const testText = newText.substring(0, mid);
|
| 216 |
+
|
| 217 |
+
const tempBlocks = blocks.map(b =>
|
| 218 |
+
b.id === blockId ? { ...b, text: testText } : b
|
| 219 |
+
);
|
| 220 |
+
const tempPages = engine.paginate(tempBlocks);
|
| 221 |
+
|
| 222 |
+
const blockPages = new Set<number>();
|
| 223 |
+
tempPages.forEach(page => {
|
| 224 |
+
page.lines.forEach(line => {
|
| 225 |
+
if (line.blockId === blockId) {
|
| 226 |
+
blockPages.add(page.pageNumber);
|
| 227 |
+
}
|
| 228 |
+
});
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
if (blockPages.size <= 1) {
|
| 232 |
+
maxFitLength = mid;
|
| 233 |
+
left = mid + 1;
|
| 234 |
+
} else {
|
| 235 |
+
right = mid - 1;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
// Find last word boundary before maxFitLength
|
| 240 |
+
let splitPoint = maxFitLength;
|
| 241 |
+
while (splitPoint > 0 && newText[splitPoint] !== ' ' && newText[splitPoint] !== '\n') {
|
| 242 |
+
splitPoint--;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// If no word boundary found, split at character
|
| 246 |
+
if (splitPoint === 0) {
|
| 247 |
+
splitPoint = maxFitLength;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
const textForCurrentBlock = newText.substring(0, splitPoint).trimEnd();
|
| 251 |
+
const textForNewBlock = newText.substring(splitPoint).trimStart();
|
| 252 |
+
|
| 253 |
+
// Create new block with overflow text
|
| 254 |
+
const newBlock: ContentBlock = {
|
| 255 |
+
id: `block-${Date.now()}-${Math.random()}`,
|
| 256 |
+
text: textForNewBlock
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
+
// Update blocks
|
| 260 |
+
setBlocks(prev => {
|
| 261 |
+
const newBlocks = [...prev];
|
| 262 |
+
newBlocks[blockIndex] = { ...newBlocks[blockIndex], text: textForCurrentBlock };
|
| 263 |
+
newBlocks.splice(blockIndex + 1, 0, newBlock);
|
| 264 |
+
return newBlocks;
|
| 265 |
+
});
|
| 266 |
+
|
| 267 |
+
// Focus new block
|
| 268 |
+
setTimeout(() => {
|
| 269 |
+
const newElement = document.querySelector(`[data-block-id="${newBlock.id}"]`) as HTMLDivElement;
|
| 270 |
+
if (newElement) {
|
| 271 |
+
newElement.focus();
|
| 272 |
+
restoreCursorPosition(newElement, 0);
|
| 273 |
+
}
|
| 274 |
+
}, 10);
|
| 275 |
+
};
|
| 276 |
+
|
| 277 |
+
const handleCompositionStart = useCallback(() => {
|
| 278 |
+
isComposingRef.current = true;
|
| 279 |
+
}, []);
|
| 280 |
+
|
| 281 |
+
const handleCompositionEnd = useCallback(() => {
|
| 282 |
+
isComposingRef.current = false;
|
| 283 |
+
}, []);
|
| 284 |
+
|
| 285 |
+
const handleInput = useCallback((e: React.FormEvent<HTMLDivElement>) => {
|
| 286 |
+
if (isComposingRef.current) return;
|
| 287 |
+
|
| 288 |
+
const target = e.currentTarget;
|
| 289 |
+
const blockId = target.getAttribute('data-block-id');
|
| 290 |
+
if (!blockId) return;
|
| 291 |
+
|
| 292 |
+
const cursorPosition = saveCursorPosition(target);
|
| 293 |
+
const newText = target.textContent || '';
|
| 294 |
+
|
| 295 |
+
// Check if this will cause overflow
|
| 296 |
+
if (willBlockOverflowPage(blockId, newText)) {
|
| 297 |
+
// Split the block
|
| 298 |
+
splitOverflowingBlock(blockId, newText);
|
| 299 |
+
return;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
if (cursorPosition !== null) {
|
| 303 |
+
cursorPositionRef.current.set(blockId, cursorPosition);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
setBlocks(prev => prev.map(block =>
|
| 307 |
+
block.id === blockId ? { ...block, text: newText } : block
|
| 308 |
+
));
|
| 309 |
+
}, [blocks, engine]);
|
| 310 |
+
|
| 311 |
+
// Handle Ctrl+A to select all content
|
| 312 |
+
const handleSelectAll = useCallback((e: React.KeyboardEvent) => {
|
| 313 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
| 314 |
+
e.preventDefault();
|
| 315 |
+
setSelectAllActive(true);
|
| 316 |
+
|
| 317 |
+
// Select all contentEditable elements
|
| 318 |
+
const allEditables = document.querySelectorAll('[contenteditable="true"]');
|
| 319 |
+
const selection = window.getSelection();
|
| 320 |
+
|
| 321 |
+
if (selection && allEditables.length > 0) {
|
| 322 |
+
const range = document.createRange();
|
| 323 |
+
const firstElement = allEditables[0];
|
| 324 |
+
const lastElement = allEditables[allEditables.length - 1];
|
| 325 |
+
|
| 326 |
+
range.setStartBefore(firstElement);
|
| 327 |
+
range.setEndAfter(lastElement);
|
| 328 |
+
|
| 329 |
+
selection.removeAllRanges();
|
| 330 |
+
selection.addRange(range);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
// Reset select all state after a moment
|
| 334 |
+
setTimeout(() => {
|
| 335 |
+
setSelectAllActive(false);
|
| 336 |
+
}, 100);
|
| 337 |
+
}
|
| 338 |
+
}, []);
|
| 339 |
+
|
| 340 |
+
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
| 341 |
+
// Handle Ctrl+A first
|
| 342 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
| 343 |
+
handleSelectAll(e);
|
| 344 |
+
return;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
const target = e.currentTarget;
|
| 348 |
+
const blockId = target.getAttribute('data-block-id');
|
| 349 |
+
if (!blockId) return;
|
| 350 |
+
|
| 351 |
+
if (e.key === 'Enter') {
|
| 352 |
+
e.preventDefault();
|
| 353 |
+
|
| 354 |
+
const currentIndex = blocks.findIndex(b => b.id === blockId);
|
| 355 |
+
if (currentIndex === -1) return;
|
| 356 |
+
|
| 357 |
+
const selection = window.getSelection();
|
| 358 |
+
if (!selection || selection.rangeCount === 0) return;
|
| 359 |
+
|
| 360 |
+
const range = selection.getRangeAt(0);
|
| 361 |
+
const preCaretRange = range.cloneRange();
|
| 362 |
+
preCaretRange.selectNodeContents(target);
|
| 363 |
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
| 364 |
+
const caretOffset = preCaretRange.toString().length;
|
| 365 |
+
|
| 366 |
+
const currentText = target.textContent || '';
|
| 367 |
+
const beforeCursor = currentText.substring(0, caretOffset);
|
| 368 |
+
const afterCursor = currentText.substring(caretOffset);
|
| 369 |
+
|
| 370 |
+
const newBlock: ContentBlock = {
|
| 371 |
+
id: `block-${Date.now()}-${Math.random()}`,
|
| 372 |
+
text: afterCursor
|
| 373 |
+
};
|
| 374 |
+
|
| 375 |
+
setBlocks(prev => {
|
| 376 |
+
const newBlocks = [...prev];
|
| 377 |
+
newBlocks[currentIndex] = { ...newBlocks[currentIndex], text: beforeCursor };
|
| 378 |
+
newBlocks.splice(currentIndex + 1, 0, newBlock);
|
| 379 |
+
return newBlocks;
|
| 380 |
+
});
|
| 381 |
+
|
| 382 |
+
setTimeout(() => {
|
| 383 |
+
const newElement = document.querySelector(`[data-block-id="${newBlock.id}"]`) as HTMLDivElement;
|
| 384 |
+
if (newElement) {
|
| 385 |
+
newElement.focus();
|
| 386 |
+
const range = document.createRange();
|
| 387 |
+
const sel = window.getSelection();
|
| 388 |
+
if (newElement.childNodes.length > 0) {
|
| 389 |
+
range.setStart(newElement.childNodes[0], 0);
|
| 390 |
+
} else {
|
| 391 |
+
range.setStart(newElement, 0);
|
| 392 |
+
}
|
| 393 |
+
range.collapse(true);
|
| 394 |
+
sel?.removeAllRanges();
|
| 395 |
+
sel?.addRange(range);
|
| 396 |
+
}
|
| 397 |
+
}, 10);
|
| 398 |
+
} else if (e.key === 'Backspace') {
|
| 399 |
+
const selection = window.getSelection();
|
| 400 |
+
if (!selection || selection.rangeCount === 0) return;
|
| 401 |
+
|
| 402 |
+
const range = selection.getRangeAt(0);
|
| 403 |
+
const preCaretRange = range.cloneRange();
|
| 404 |
+
preCaretRange.selectNodeContents(target);
|
| 405 |
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
| 406 |
+
const caretOffset = preCaretRange.toString().length;
|
| 407 |
+
|
| 408 |
+
if (caretOffset === 0 && blocks.length > 1) {
|
| 409 |
+
e.preventDefault();
|
| 410 |
+
const currentIndex = blocks.findIndex(b => b.id === blockId);
|
| 411 |
+
if (currentIndex > 0) {
|
| 412 |
+
const prevBlock = blocks[currentIndex - 1];
|
| 413 |
+
const currentBlock = blocks[currentIndex];
|
| 414 |
+
const prevTextLength = prevBlock.text.length;
|
| 415 |
+
|
| 416 |
+
setBlocks(prev => {
|
| 417 |
+
const newBlocks = [...prev];
|
| 418 |
+
newBlocks[currentIndex - 1] = {
|
| 419 |
+
...prevBlock,
|
| 420 |
+
text: prevBlock.text + currentBlock.text
|
| 421 |
+
};
|
| 422 |
+
newBlocks.splice(currentIndex, 1);
|
| 423 |
+
return newBlocks;
|
| 424 |
+
});
|
| 425 |
+
|
| 426 |
+
setTimeout(() => {
|
| 427 |
+
const prevElement = document.querySelector(`[data-block-id="${prevBlock.id}"]`) as HTMLDivElement;
|
| 428 |
+
if (prevElement) {
|
| 429 |
+
prevElement.focus();
|
| 430 |
+
restoreCursorPosition(prevElement, prevTextLength);
|
| 431 |
+
}
|
| 432 |
+
}, 10);
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
| 436 |
+
const currentIndex = blocks.findIndex(b => b.id === blockId);
|
| 437 |
+
const selection = window.getSelection();
|
| 438 |
+
if (!selection || selection.rangeCount === 0) return;
|
| 439 |
+
|
| 440 |
+
const range = selection.getRangeAt(0);
|
| 441 |
+
const preCaretRange = range.cloneRange();
|
| 442 |
+
preCaretRange.selectNodeContents(target);
|
| 443 |
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
| 444 |
+
const caretOffset = preCaretRange.toString().length;
|
| 445 |
+
|
| 446 |
+
if (e.key === 'ArrowUp' && caretOffset === 0 && currentIndex > 0) {
|
| 447 |
+
e.preventDefault();
|
| 448 |
+
const prevBlock = blocks[currentIndex - 1];
|
| 449 |
+
const prevElement = document.querySelector(`[data-block-id="${prevBlock.id}"]`) as HTMLDivElement;
|
| 450 |
+
if (prevElement) {
|
| 451 |
+
prevElement.focus();
|
| 452 |
+
restoreCursorPosition(prevElement, prevBlock.text.length);
|
| 453 |
+
}
|
| 454 |
+
} else if (e.key === 'ArrowDown' && caretOffset === target.textContent?.length && currentIndex < blocks.length - 1) {
|
| 455 |
+
e.preventDefault();
|
| 456 |
+
const nextBlock = blocks[currentIndex + 1];
|
| 457 |
+
const nextElement = document.querySelector(`[data-block-id="${nextBlock.id}"]`) as HTMLDivElement;
|
| 458 |
+
if (nextElement) {
|
| 459 |
+
nextElement.focus();
|
| 460 |
+
restoreCursorPosition(nextElement, 0);
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
}, [blocks, handleSelectAll]);
|
| 465 |
+
|
| 466 |
+
// Global keyboard event listener for Ctrl+A
|
| 467 |
+
useEffect(() => {
|
| 468 |
+
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
| 469 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
| 470 |
+
// Check if focus is inside document container
|
| 471 |
+
const activeElement = document.activeElement;
|
| 472 |
+
const isInEditor = activeElement?.closest('.document-container');
|
| 473 |
+
|
| 474 |
+
if (isInEditor) {
|
| 475 |
+
e.preventDefault();
|
| 476 |
+
setSelectAllActive(true);
|
| 477 |
+
|
| 478 |
+
const allEditables = document.querySelectorAll('[contenteditable="true"]');
|
| 479 |
+
const selection = window.getSelection();
|
| 480 |
+
|
| 481 |
+
if (selection && allEditables.length > 0) {
|
| 482 |
+
const range = document.createRange();
|
| 483 |
+
const firstElement = allEditables[0];
|
| 484 |
+
const lastElement = allEditables[allEditables.length - 1];
|
| 485 |
+
|
| 486 |
+
range.setStartBefore(firstElement);
|
| 487 |
+
range.setEndAfter(lastElement);
|
| 488 |
+
|
| 489 |
+
selection.removeAllRanges();
|
| 490 |
+
selection.addRange(range);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
setTimeout(() => {
|
| 494 |
+
setSelectAllActive(false);
|
| 495 |
+
}, 100);
|
| 496 |
+
}
|
| 497 |
+
}
|
| 498 |
+
};
|
| 499 |
+
|
| 500 |
+
document.addEventListener('keydown', handleGlobalKeyDown);
|
| 501 |
+
return () => document.removeEventListener('keydown', handleGlobalKeyDown);
|
| 502 |
+
}, []);
|
| 503 |
+
|
| 504 |
+
return (
|
| 505 |
+
<div className="App">
|
| 506 |
+
<div className="controls">
|
| 507 |
+
<button onClick={handleSave}>💾 Save</button>
|
| 508 |
+
<input
|
| 509 |
+
type="text"
|
| 510 |
+
placeholder="Enter Document ID to load"
|
| 511 |
+
value={loadId}
|
| 512 |
+
onChange={e => setLoadId(e.target.value)}
|
| 513 |
+
/>
|
| 514 |
+
<button onClick={handleLoad}>📂 Load</button>
|
| 515 |
+
{docId && <span className="doc-id">ID: {docId}</span>}
|
| 516 |
+
<span className="page-count">📄 {pages.length} {pages.length === 1 ? 'Page' : 'Pages'}</span>
|
| 517 |
+
</div>
|
| 518 |
+
|
| 519 |
+
<div className="document-container" ref={editorRef}>
|
| 520 |
+
{pages.length === 0 ? (
|
| 521 |
+
<div className="empty-state">
|
| 522 |
+
<p>Click on the page below to start typing...</p>
|
| 523 |
+
<p className="hint">Press <kbd>Ctrl+A</kbd> (or <kbd>Cmd+A</kbd>) to select all content</p>
|
| 524 |
+
</div>
|
| 525 |
+
) : null}
|
| 526 |
+
|
| 527 |
+
{pages.map(page => (
|
| 528 |
+
<div
|
| 529 |
+
key={page.pageNumber}
|
| 530 |
+
className="page"
|
| 531 |
+
style={{
|
| 532 |
+
width: PAGE_CONFIG.width,
|
| 533 |
+
height: PAGE_CONFIG.height,
|
| 534 |
+
padding: `${PAGE_CONFIG.marginTop}px ${PAGE_CONFIG.marginRight}px ${PAGE_CONFIG.marginBottom}px ${PAGE_CONFIG.marginLeft}px`,
|
| 535 |
+
}}
|
| 536 |
+
>
|
| 537 |
+
<div className="page-content">
|
| 538 |
+
{(() => {
|
| 539 |
+
// Group lines by blockId to handle blocks that span across pages
|
| 540 |
+
const blockGroups = new Map<string, typeof page.lines>();
|
| 541 |
+
page.lines.forEach(line => {
|
| 542 |
+
if (!blockGroups.has(line.blockId)) {
|
| 543 |
+
blockGroups.set(line.blockId, []);
|
| 544 |
+
}
|
| 545 |
+
blockGroups.get(line.blockId)!.push(line);
|
| 546 |
+
});
|
| 547 |
+
|
| 548 |
+
// Track which block instances we've seen on this page for unique keys
|
| 549 |
+
const blockInstanceCounts = new Map<string, number>();
|
| 550 |
+
|
| 551 |
+
return Array.from(blockGroups.entries()).map(([blockId, lines], groupIndex) => {
|
| 552 |
+
const block = blocks.find(b => b.id === blockId);
|
| 553 |
+
if (!block) return null;
|
| 554 |
+
|
| 555 |
+
// Generate unique key combining page number, block ID, and instance index
|
| 556 |
+
const instanceCount = blockInstanceCounts.get(blockId) || 0;
|
| 557 |
+
blockInstanceCounts.set(blockId, instanceCount + 1);
|
| 558 |
+
const uniqueKey = `page-${page.pageNumber}-block-${blockId}-instance-${instanceCount}`;
|
| 559 |
+
|
| 560 |
+
const firstLine = lines[0];
|
| 561 |
+
const lastLine = lines[lines.length - 1];
|
| 562 |
+
|
| 563 |
+
// Calculate the height needed for this block segment on this page
|
| 564 |
+
const blockHeight = (lastLine.y - firstLine.y) + PAGE_CONFIG.lineHeight;
|
| 565 |
+
|
| 566 |
+
return (
|
| 567 |
+
<div
|
| 568 |
+
key={uniqueKey}
|
| 569 |
+
data-block-id={blockId}
|
| 570 |
+
contentEditable
|
| 571 |
+
suppressContentEditableWarning
|
| 572 |
+
onInput={handleInput}
|
| 573 |
+
onKeyDown={handleKeyDown}
|
| 574 |
+
onCompositionStart={handleCompositionStart}
|
| 575 |
+
onCompositionEnd={handleCompositionEnd}
|
| 576 |
+
className="editable-block"
|
| 577 |
+
style={{
|
| 578 |
+
position: 'absolute',
|
| 579 |
+
left: 0,
|
| 580 |
+
top: firstLine.y - PAGE_CONFIG.marginTop,
|
| 581 |
+
width: '100%',
|
| 582 |
+
height: blockHeight,
|
| 583 |
+
lineHeight: `${PAGE_CONFIG.lineHeight}px`,
|
| 584 |
+
fontSize: PAGE_CONFIG.fontSize,
|
| 585 |
+
fontFamily: PAGE_CONFIG.fontFamily,
|
| 586 |
+
outline: 'none',
|
| 587 |
+
whiteSpace: 'pre-wrap',
|
| 588 |
+
wordWrap: 'break-word',
|
| 589 |
+
direction: 'ltr',
|
| 590 |
+
textAlign: 'left',
|
| 591 |
+
unicodeBidi: 'embed',
|
| 592 |
+
}}
|
| 593 |
+
>
|
| 594 |
+
{block.text}
|
| 595 |
+
</div>
|
| 596 |
+
);
|
| 597 |
+
});
|
| 598 |
+
})()}
|
| 599 |
+
</div>
|
| 600 |
+
|
| 601 |
+
<div className="page-number">
|
| 602 |
+
Page {page.pageNumber} of {pages.length}
|
| 603 |
+
</div>
|
| 604 |
+
</div>
|
| 605 |
+
))}
|
| 606 |
+
</div>
|
| 607 |
+
</div>
|
| 608 |
+
);
|
| 609 |
+
};
|
| 610 |
+
|
| 611 |
+
export default App;
|
Document-Editor--master/client/src/index.css
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
margin: 0;
|
| 3 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
| 4 |
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
| 5 |
+
sans-serif;
|
| 6 |
+
-webkit-font-smoothing: antialiased;
|
| 7 |
+
-moz-osx-font-smoothing: grayscale;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
code {
|
| 11 |
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
| 12 |
+
monospace;
|
| 13 |
+
}
|
Document-Editor--master/client/src/index.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import './index.css';
|
| 4 |
+
import App from './App';
|
| 5 |
+
import reportWebVitals from './reportWebVitals';
|
| 6 |
+
|
| 7 |
+
const root = ReactDOM.createRoot(
|
| 8 |
+
document.getElementById('root') as HTMLElement
|
| 9 |
+
);
|
| 10 |
+
root.render(
|
| 11 |
+
<React.StrictMode>
|
| 12 |
+
<App />
|
| 13 |
+
</React.StrictMode>
|
| 14 |
+
);
|
| 15 |
+
|
| 16 |
+
// If you want to start measuring performance in your app, pass a function
|
| 17 |
+
// to log results (for example: reportWebVitals(console.log))
|
| 18 |
+
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
| 19 |
+
reportWebVitals();
|
Document-Editor--master/client/src/logo.svg
ADDED
|
|
Document-Editor--master/client/src/paginationEngine.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PAGE_CONFIG, ContentBlock, Page, Line } from './types';
|
| 2 |
+
import { TextMeasurer } from './textMeasurer';
|
| 3 |
+
|
| 4 |
+
export class PaginationEngine {
|
| 5 |
+
private measurer: TextMeasurer;
|
| 6 |
+
|
| 7 |
+
constructor() {
|
| 8 |
+
this.measurer = new TextMeasurer();
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
private getContentWidth(): number {
|
| 12 |
+
return PAGE_CONFIG.width - PAGE_CONFIG.marginLeft - PAGE_CONFIG.marginRight;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
private getContentHeight(): number {
|
| 16 |
+
return PAGE_CONFIG.height - PAGE_CONFIG.marginTop - PAGE_CONFIG.marginBottom;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
paginate(blocks: ContentBlock[]): Page[] {
|
| 20 |
+
const pages: Page[] = [];
|
| 21 |
+
let currentPage: Page = { pageNumber: 1, lines: [] };
|
| 22 |
+
let currentY = PAGE_CONFIG.marginTop;
|
| 23 |
+
|
| 24 |
+
for (const block of blocks) {
|
| 25 |
+
// Handle completely empty blocks
|
| 26 |
+
if (block.text.length === 0) {
|
| 27 |
+
const lineHeight = PAGE_CONFIG.lineHeight;
|
| 28 |
+
|
| 29 |
+
if (currentY + lineHeight > PAGE_CONFIG.height - PAGE_CONFIG.marginBottom) {
|
| 30 |
+
pages.push(currentPage);
|
| 31 |
+
currentPage = { pageNumber: pages.length + 1, lines: [] };
|
| 32 |
+
currentY = PAGE_CONFIG.marginTop;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
currentPage.lines.push({
|
| 36 |
+
text: '',
|
| 37 |
+
y: currentY,
|
| 38 |
+
blockId: block.id
|
| 39 |
+
});
|
| 40 |
+
currentY += lineHeight;
|
| 41 |
+
continue;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Break text into lines (preserving whitespace)
|
| 45 |
+
const lines = this.measurer.breakIntoLines(
|
| 46 |
+
block.text,
|
| 47 |
+
this.getContentWidth()
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
for (const lineText of lines) {
|
| 51 |
+
const lineHeight = PAGE_CONFIG.lineHeight;
|
| 52 |
+
|
| 53 |
+
// Check if line fits on current page
|
| 54 |
+
if (currentY + lineHeight > PAGE_CONFIG.height - PAGE_CONFIG.marginBottom) {
|
| 55 |
+
pages.push(currentPage);
|
| 56 |
+
currentPage = { pageNumber: pages.length + 1, lines: [] };
|
| 57 |
+
currentY = PAGE_CONFIG.marginTop;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
currentPage.lines.push({
|
| 61 |
+
text: lineText,
|
| 62 |
+
y: currentY,
|
| 63 |
+
blockId: block.id
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
currentY += lineHeight;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Push last page
|
| 71 |
+
if (currentPage.lines.length > 0) {
|
| 72 |
+
pages.push(currentPage);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return pages;
|
| 76 |
+
}
|
| 77 |
+
}
|
Document-Editor--master/client/src/react-app-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="react-scripts" />
|
Document-Editor--master/client/src/reportWebVitals.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ReportHandler } from 'web-vitals';
|
| 2 |
+
|
| 3 |
+
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
| 4 |
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
| 5 |
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
| 6 |
+
getCLS(onPerfEntry);
|
| 7 |
+
getFID(onPerfEntry);
|
| 8 |
+
getFCP(onPerfEntry);
|
| 9 |
+
getLCP(onPerfEntry);
|
| 10 |
+
getTTFB(onPerfEntry);
|
| 11 |
+
});
|
| 12 |
+
}
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export default reportWebVitals;
|
Document-Editor--master/client/src/setupTests.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
| 2 |
+
// allows you to do things like:
|
| 3 |
+
// expect(element).toHaveTextContent(/react/i)
|
| 4 |
+
// learn more: https://github.com/testing-library/jest-dom
|
| 5 |
+
import '@testing-library/jest-dom';
|
Document-Editor--master/client/src/textMeasurer.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PAGE_CONFIG } from './types';
|
| 2 |
+
|
| 3 |
+
export class TextMeasurer {
|
| 4 |
+
private canvas: HTMLCanvasElement;
|
| 5 |
+
private ctx: CanvasRenderingContext2D;
|
| 6 |
+
|
| 7 |
+
constructor() {
|
| 8 |
+
this.canvas = document.createElement('canvas');
|
| 9 |
+
this.ctx = this.canvas.getContext('2d')!;
|
| 10 |
+
this.ctx.font = `${PAGE_CONFIG.fontSize}px ${PAGE_CONFIG.fontFamily}`;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
measureText(text: string): number {
|
| 14 |
+
return this.ctx.measureText(text).width;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
breakIntoLines(text: string, maxWidth: number): string[] {
|
| 18 |
+
if (!text) return [''];
|
| 19 |
+
|
| 20 |
+
// Replace tabs with 4 spaces (standard tab width)
|
| 21 |
+
const processedText = text.replace(/\t/g, ' ');
|
| 22 |
+
|
| 23 |
+
// Handle explicit line breaks
|
| 24 |
+
const paragraphs = processedText.split('\n');
|
| 25 |
+
const allLines: string[] = [];
|
| 26 |
+
|
| 27 |
+
for (const paragraph of paragraphs) {
|
| 28 |
+
if (!paragraph.trim()) {
|
| 29 |
+
// Preserve empty lines
|
| 30 |
+
allLines.push('');
|
| 31 |
+
continue;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Word wrap algorithm that preserves spaces
|
| 35 |
+
const words = paragraph.split(/(\s+)/); // Split but keep spaces
|
| 36 |
+
const lines: string[] = [];
|
| 37 |
+
let currentLine = '';
|
| 38 |
+
|
| 39 |
+
for (let i = 0; i < words.length; i++) {
|
| 40 |
+
const word = words[i];
|
| 41 |
+
const testLine = currentLine + word;
|
| 42 |
+
const width = this.measureText(testLine);
|
| 43 |
+
|
| 44 |
+
if (width > maxWidth && currentLine.trim()) {
|
| 45 |
+
// Line would overflow, push current line and start new
|
| 46 |
+
lines.push(currentLine);
|
| 47 |
+
|
| 48 |
+
// Skip leading space on new line if current word is whitespace
|
| 49 |
+
if (word.trim() === '') {
|
| 50 |
+
currentLine = '';
|
| 51 |
+
} else {
|
| 52 |
+
currentLine = word;
|
| 53 |
+
}
|
| 54 |
+
} else {
|
| 55 |
+
currentLine = testLine;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
if (currentLine) {
|
| 60 |
+
lines.push(currentLine);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
allLines.push(...(lines.length > 0 ? lines : ['']));
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return allLines;
|
| 67 |
+
}
|
| 68 |
+
}
|
Document-Editor--master/client/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const PAGE_CONFIG = {
|
| 2 |
+
width: 794,
|
| 3 |
+
height: 1123,
|
| 4 |
+
marginTop: 72,
|
| 5 |
+
marginBottom: 72,
|
| 6 |
+
marginLeft: 72,
|
| 7 |
+
marginRight: 72,
|
| 8 |
+
lineHeight: 24,
|
| 9 |
+
fontSize: 16,
|
| 10 |
+
fontFamily: 'Arial, sans-serif'
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
export interface ContentBlock {
|
| 14 |
+
id: string;
|
| 15 |
+
text: string;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export interface Document {
|
| 19 |
+
id?: string;
|
| 20 |
+
title: string;
|
| 21 |
+
content: ContentBlock[];
|
| 22 |
+
created_at?: string;
|
| 23 |
+
updated_at?: string;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export interface Line {
|
| 27 |
+
text: string;
|
| 28 |
+
y: number;
|
| 29 |
+
blockId: string;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export interface Page {
|
| 33 |
+
pageNumber: number;
|
| 34 |
+
lines: Line[];
|
| 35 |
+
}
|
Document-Editor--master/client/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "es5",
|
| 4 |
+
"lib": [
|
| 5 |
+
"dom",
|
| 6 |
+
"dom.iterable",
|
| 7 |
+
"esnext"
|
| 8 |
+
],
|
| 9 |
+
"allowJs": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"esModuleInterop": true,
|
| 12 |
+
"allowSyntheticDefaultImports": true,
|
| 13 |
+
"strict": true,
|
| 14 |
+
"forceConsistentCasingInFileNames": true,
|
| 15 |
+
"noFallthroughCasesInSwitch": true,
|
| 16 |
+
"module": "esnext",
|
| 17 |
+
"moduleResolution": "node",
|
| 18 |
+
"resolveJsonModule": true,
|
| 19 |
+
"isolatedModules": true,
|
| 20 |
+
"noEmit": true,
|
| 21 |
+
"jsx": "react-jsx"
|
| 22 |
+
},
|
| 23 |
+
"include": [
|
| 24 |
+
"src"
|
| 25 |
+
]
|
| 26 |
+
}
|
Document-Editor--master/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "fullstack-app",
|
| 3 |
+
"type": "module",
|
| 4 |
+
"scripts": {
|
| 5 |
+
"start": "node server/index.js",
|
| 6 |
+
"server": "nodemon server/index.js",
|
| 7 |
+
"client": "npm start --prefix client",
|
| 8 |
+
"build": "npm install --prefix client && npm run build --prefix client"
|
| 9 |
+
}
|
| 10 |
+
}
|
Document-Editor--master/server/data/040e38fc-f5ff-448b-a38e-528b77d62c7c.json
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "040e38fc-f5ff-448b-a38e-528b77d62c7c",
|
| 3 |
+
"title": "My Document",
|
| 4 |
+
"content": [
|
| 5 |
+
{
|
| 6 |
+
"id": "1764185322326",
|
| 7 |
+
"text": "Hello world"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "block-1764185326179-0.745027487574992",
|
| 11 |
+
"text": ""
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"id": "block-1764185326459-0.8631461999435833",
|
| 15 |
+
"text": "Hello "
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": "block-1764185332099-0.4020291953985998",
|
| 19 |
+
"text": ""
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "block-1764185333507-0.4573760865672791",
|
| 23 |
+
"text": ""
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "block-1764185333723-0.2570954943893221",
|
| 27 |
+
"text": "Hello "
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "block-1764185341291-0.460156802426543",
|
| 31 |
+
"text": ""
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "block-1764185342946-0.3570515473860154",
|
| 35 |
+
"text": ""
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "block-1764185345731-0.0795202500495431",
|
| 39 |
+
"text": ""
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"id": "block-1764185345867-0.8343009476817695",
|
| 43 |
+
"text": ""
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "block-1764185346058-0.10172043543927756",
|
| 47 |
+
"text": ""
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"id": "block-1764185346250-0.6051099698361062",
|
| 51 |
+
"text": "\n\n\nBackend: Express (Node.js)\n\n\nGoal: Host for free\n\n\nThe best free choices right now are:\n🥇 Railway (free tier) or 🥈 Render (free tier)\nBoth can host your full-stack Node + static React build. I’ll show the easiest full-stack method (one deployment).\n\n🎉 Best Option: Host Both Together on Railway/Render\n📌 Step 1 — Build your React Client\nOpen terminal:\ncd client\nnpm install\nnpm run build\n\nThis creates a build/ folder.\n\n📌 Step 2 — Make Express Serve the React Build\nIn your server folder, modify your Express app:\nimport express from \"express\";"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"id": "block-1764185358773-0.36930011138627317",
|
| 55 |
+
"text": "import path from \"path\";\nimport { fileURLToPath } from \"url\";\n"
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "block-1764185387263-0.5847362075190897",
|
| 59 |
+
"text": "const __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst app = express();\n\n// 👉 Serve React build\napp.use(express.static(path.join(__dirname, \"../client/build\")));\n\napp.get(\"*\", (_, res) => {\n res.sendFile(path.join(__dirname, \"../client/build/index.html\"));\n});\n\napp.listen(process.env.PORT || 5000, () => {\n console.log(\"Server running...\");\n});\n\n\n📌 Step 3 — Add a Root package.json Script\nIn the root folder (not client or server), create a package.json if it doesn’t exist:\n{\n \"name\": \"fullstack-app\",\n \"scripts\": {\n \"start\": \"node server/index.js\",\n \"server\": \"nodemon server/index.js\",\n \"client\": \"npm start --prefix client\",\n \"build\": \"npm install --prefix client && npm run build --prefix client\"\n }\n}\n\n(Make sure your entry file is server/index.js or adjust path.)\n\n📌 Step 4 — Push to GitHub\nMake sure your repo has this structure:\n/client\n/server\npackage.json (root)\n"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"id": "block-1764185358797-0.7712865336178318",
|
| 63 |
+
"text": "Commit and push to GitHub.\n\n🚀 Step 5 — Deploy on Railway (FREE)"
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"id": "block-1764185387271-0.6136933963371233",
|
| 67 |
+
"text": "\n\nGo to https://railway.app/\n\n\nLogin with GitHub\n\n\nClick New Project → Deploy from GitHub Repo\n\n\nSelect your repo\n\n\nPORT=8080\nFeature\nRailway\nRender"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"id": "block-1764185358813-0.12024335256399588",
|
| 71 |
+
"text": "\n\n\n\nFree limit\n~5$/month usage\nFree 750 hours\n"
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"id": "block-1764185372204-0.7695423897884532",
|
| 75 |
+
"text": "\nSleeps on inactivity\nYes\nYes\n\n\nBuild both together easily\n✔️ Best\n✔️ Good\n\n"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"id": "block-1764185379334-0.4269752564809537",
|
| 79 |
+
"text": "\nBoth work, but Railway is simpler.\n"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"id": "block-1764185387283-0.18512521330701626",
|
| 83 |
+
"text": "🧐 Want Separate Hosting Instead?\nIf you want:\n\n\nBackend on Railway\n\n\nFrontend on Vercel\n\n\nI can show that too — just say I want them separate.\n\n📌 Need Help with Environment Variables or Build Errors?\nSend:\n\n\nyour folder structure\n"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"id": "block-1764185358824-0.6519678463146473",
|
| 87 |
+
"text": "\npackage.json files\n\n\nI’ll fix them for you 🚀🛠️"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"id": "block-1764185341458-0.3415649538707576",
|
| 91 |
+
"text": ""
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"id": "block-1764185341692-0.4827620206238017",
|
| 95 |
+
"text": "world"
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"id": "block-1764185336714-0.6645613696878259",
|
| 99 |
+
"text": ""
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"id": "block-1764185336883-0.6190069978347903",
|
| 103 |
+
"text": "Great! You have:\n\n\nFrontend: TypeScript React\n\n\nBackend: Express (Node.js)\n\n\nGoal: Host for free\n"
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"id": "block-1764185379346-0.9188524656035867",
|
| 107 |
+
"text": "\nThe best free choices right now are:\n🥇 Railway (free tier) or 🥈 Render (free tier)"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"id": "block-1764185387293-0.39363787926915195",
|
| 111 |
+
"text": "Both can host your full-stack Node + static React build. I’ll show the easiest full-stack \nmethod (one deployment).\n\n🎉 Best Option: Host Both Together on Railway/Render\n📌 Step 1 — Build your React Client\nOpen terminal:\ncd client\nnpm install\nnpm run build\n\nThis creates a build/ folder.\n\n📌 Step 2 — Make Express Serve the React Build\nIn your server folder, modify your Express app:\nimport express from \"express\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";"
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"id": "block-1764185366760-0.2924867741893613",
|
| 115 |
+
"text": "const __filename = fileURLToPath(import.meta.url);"
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"id": "block-1764185366772-0.647275959118245",
|
| 119 |
+
"text": "const __dirname = path.dirname(__filename);\n\nconst app = express();\n\n// 👉 Serve React build\napp.use(express.static(path.join(__dirname, \"../client/build\")));\n\napp.get(\"*\", (_, res) => {"
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"id": "block-1764185372217-0.3848900377626562",
|
| 123 |
+
"text": " res.sendFile(path.join(__dirname, \"../client/build/index.html\"));\n});\n\napp.listen(process.env.PORT || 5000, () => {\n console.log(\"Server running...\");\n});\n\n\n📌 Step 3 — Add a Root package.json Script\nIn the root folder (not client or server), create a package.json if it doesn’t exist:\n{"
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"id": "block-1764185379354-0.21129516703548934",
|
| 127 |
+
"text": " \"name\": \"fullstack-app\",\n \"scripts\": {\n \"start\": \"node server/index.js\","
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"id": "block-1764185387300-0.37138435896043853",
|
| 131 |
+
"text": " \"server\": \"nodemon server/index.js\",\n \"client\": \"npm start --prefix client\",\n \"build\": \"npm install --prefix client && npm run build --prefix client\"\n }\n}\n\n(Make sure your entry file is server/index.js or adjust path.)\n\n📌 Step 4 — Push to GitHub\nMake sure your repo has this structure:\n/client\n/server\npackage.json (root)\n\nCommit and push to GitHub.\n\n🚀 Step 5 — Deploy on Railway (FREE)\n"
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"id": "block-1764185366791-0.7988936374851168",
|
| 135 |
+
"text": "\nGo to https://railway.app/\n\n\nLogin with GitHub\n\n\nClick New Project → Deploy from GitHub Repo"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"id": "block-1764185372227-0.20480405535862745",
|
| 139 |
+
"text": "\n\nSelect your repo\n\n\nImportant:\n\n\nSet Root project path to your root, not /client or /server.\n\n"
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": "block-1764185379364-0.2759495671627247",
|
| 143 |
+
"text": "Add Environment Variables\n→ Railway → Variables → Add:\nPORT=8080"
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"id": "block-1764185387310-0.8035233227403694",
|
| 147 |
+
"text": "(any other secrets like Mongo URL)\n\n\n🎊 Done! Your app URL will look like:\nhttps://yourapp.up.railway.app/\n\n\n⚠️ Important Notes for Free Hosting\n\n\n\nFeature\nRailway\nRender\n\n\n\n"
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"id": "block-1764185366808-0.7356968921107767",
|
| 151 |
+
"text": "Free limit\n~5$/month usage\nFree 750 hours\n\n\nSleeps on inactivity\nYes\nYes"
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "block-1764185372236-0.9535100220474891",
|
| 155 |
+
"text": "\n\nBuild both together easily\n✔️ Best\n✔️ Good\n\n\n\nBoth work, but Railway is simpler.\n\n🧐 Want Separate Hosting Instead?"
|
| 156 |
+
},
|
| 157 |
+
{
|
| 158 |
+
"id": "block-1764185379371-0.9362557720680834",
|
| 159 |
+
"text": "If you want:\n\n"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"id": "block-1764185387319-0.4459054843320799",
|
| 163 |
+
"text": "Backend on Railway\n\n\nFrontend on Vercel\n\n\nI can show that too — just say I want them separate.\n\n📌 Need Help with Environment Variables or Build Errors?\nSend:\n\n\nyour folder structure\n\n\npackage.json files\n\n"
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"id": "block-1764185366823-0.34698356755452187",
|
| 167 |
+
"text": "I’ll fix them for you 🚀🛠️"
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"id": "block-1764185337403-0.8706107040484482",
|
| 171 |
+
"text": ""
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"id": "block-1764185337490-0.6390174492297295",
|
| 175 |
+
"text": ""
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"id": "block-1764185333882-0.22483528279064768",
|
| 179 |
+
"text": "world"
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"id": "block-1764185330842-0.6293666256813296",
|
| 183 |
+
"text": ""
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"id": "block-1764185329171-0.5813949064381904",
|
| 187 |
+
"text": ""
|
| 188 |
+
}
|
| 189 |
+
],
|
| 190 |
+
"created_at": "2025-11-26T19:29:49.804Z",
|
| 191 |
+
"updated_at": "2025-11-26T19:29:49.804Z"
|
| 192 |
+
}
|
Document-Editor--master/server/data/71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "71b8d791-d7b2-44e3-bf5b-bfa4ca3a34de",
|
| 3 |
+
"title": "My Document",
|
| 4 |
+
"content": [
|
| 5 |
+
{
|
| 6 |
+
"id": "1764119897129",
|
| 7 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds."
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "block-1764119915257-0.4292094413105557",
|
| 11 |
+
"text": ""
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"id": "block-1764119915769-0.9249138680032881",
|
| 15 |
+
"text": ""
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": "block-1764119915799-0.7964691271821067",
|
| 19 |
+
"text": ""
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "block-1764119915827-0.018955485880886136",
|
| 23 |
+
"text": ""
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "block-1764119915862-0.8424512262384346",
|
| 27 |
+
"text": ""
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "block-1764119915895-0.7436883957463118",
|
| 31 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds."
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "block-1764119917418-0.5005262987305623",
|
| 35 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds."
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "block-1764119919503-0.6967482027215017",
|
| 39 |
+
"text": ""
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"id": "block-1764119924536-0.9639945946185713",
|
| 43 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds."
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"id": "block-1764119926560-0.9575022093644173",
|
| 47 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"id": "block-1764119928702-0.18037476296660337",
|
| 51 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.HelloHello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Worlds.HeHello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"id": "block-1764120044271-0.04877565396456174",
|
| 55 |
+
"text": "Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.HelloHello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Worlds.HeWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello"
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"id": "block-1764120045607-0.3191495481339375",
|
| 59 |
+
"text": "Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worlds Worlds.Hello Worlds.Hello Worlds.HellossssssssHello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"id": "block-1764120096263-0.7501005669073386",
|
| 63 |
+
"text": "Worlds.Hello Worlds.Worlds.He.He"
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"id": "block-1764120098441-0.995989493698729",
|
| 67 |
+
"text": "Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worlds Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worldsWorlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Worlds.Hello Hello worlds"
|
| 68 |
+
}
|
| 69 |
+
],
|
| 70 |
+
"created_at": "2025-11-26T01:24:16.750Z",
|
| 71 |
+
"updated_at": "2025-11-26T01:24:16.750Z"
|
| 72 |
+
}
|
Document-Editor--master/server/data/92a85bb2-938a-41b0-9c69-1c36919da23c.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "92a85bb2-938a-41b0-9c69-1c36919da23c",
|
| 3 |
+
"title": "My Document",
|
| 4 |
+
"content": [
|
| 5 |
+
{
|
| 6 |
+
"id": "1764117603114",
|
| 7 |
+
"text": "Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world."
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "block-1764117826759-0.5817665547194508",
|
| 11 |
+
"text": "Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world."
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"id": "block-1764117828658-0.037742925663947324",
|
| 15 |
+
"text": "Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world."
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"id": "block-1764117833450-0.4200369076586793",
|
| 19 |
+
"text": "Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world."
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "block-1764117844918-0.9777791714244016",
|
| 23 |
+
"text": "Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world."
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "block-1764117837034-0.7351909718040901",
|
| 27 |
+
"text": "Hello world.Hello world.Hello world.Hello world.Hello world.Hello world.Hello world."
|
| 28 |
+
}
|
| 29 |
+
],
|
| 30 |
+
"created_at": "2025-11-26T00:44:55.001Z",
|
| 31 |
+
"updated_at": "2025-11-26T00:44:55.001Z"
|
| 32 |
+
}
|
Document-Editor--master/server/data/d8b5e4ae-e7c1-4d7b-a70d-45f7e9d5f2d8.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "d8b5e4ae-e7c1-4d7b-a70d-45f7e9d5f2d8",
|
| 3 |
+
"title": "My Document",
|
| 4 |
+
"content": [
|
| 5 |
+
{
|
| 6 |
+
"id": "1764106395642",
|
| 7 |
+
"text": "hello sdfsfsdgaefawrw sejrqworj;wqroej"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "1764106670364",
|
| 11 |
+
"text": "import express from 'express';\nimport cors from 'cors';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { v4 as uuidv4 } from 'uuid';\n\n// Get __dirname in ES modules\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\nconst DATA_DIR = path.join(__dirname, 'data');\n\n// Ensure data directory exists\nfs.mkdir(DATA_DIR, { recursive: true }).catch(() => {});\n\n// POST /documents\napp.post('/documents', async (req, res) => {\n try {\n const doc = {\n id: uuidv4(),\n title: req.body.title || 'Untitled',\n content: req.body.content,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString()\n };\n \n await fs.writeFile(\n path.join(DATA_DIR, `${doc.id}.json`),\n JSON.stringify(doc, null, 2)\n );\n \n res.json({ id: doc.id });\n } catch (err) {\n console.error('Save error:', err);\n res.status(500).json({ error: 'Save failed', message: err.message });\n }\n});\n\n// GET /documents/:id\napp.get('/documents/:id', async (req, res) => {\n try {\n const data = await fs.readFile(\n path.join(DATA_DIR, `${req.params.id}.json`),\n 'utf-8'\n );\n res.json(JSON.parse(data));\n } catch (err) {\n console.error('Load error:', err);\n res.status(404).json({ error: 'Document not found' });\n }\n});\n\nconst PORT = 3001;\napp.listen(PORT, () => {\n console.log(`✓ Server running on http://localhost:${PORT}`);\n});import express from 'express';\nimport cors from 'cors';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { v4 as uuidv4 } from 'uuid';\n\n// Get __dirname in ES modules\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\nconst DATA_DIR = path.join(__dirname, 'data');\n\n// Ensure data directory exists\nfs.mkdir(DATA_DIR, { recursive: true }).catch(() => {});\n\n// POST /documents\napp.post('/documents', async (req, res) => {\n try {\n const doc = {\n id: uuidv4(),\n title: req.body.title || 'Untitled',\n content: req.body.content,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString()\n };\n \n await fs.writeFile(\n path.join(DATA_DIR, `${doc.id}.json`),\n JSON.stringify(doc, null, 2)\n );\n \n res.json({ id: doc.id });\n } catch (err) {\n console.error('Save error:', err);\n res.status(500).json({ error: 'Save failed', message: err.message });\n }\n});\n\n// GET /documents/:id\napp.get('/documents/:id', async (req, res) => {\n try {\n const data = await fs.readFile(\n path.join(DATA_DIR, `${req.params.id}.json`),\n 'utf-8'\n );\n res.json(JSON.parse(data));\n } catch (err) {\n console.error('Load error:', err);\n res.status(404).json({ error: 'Document not found' });\n }\n});\n\nconst PORT = 3001;\napp.listen(PORT, () => {\n console.log(`✓ Server running on http://localhost:${PORT}`);\n});import express from 'express';\nimport cors from 'cors';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { v4 as uuidv4 } from 'uuid';\n\n// Get __dirname in ES modules\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nconst app = express();\napp.use(cors());\napp.use(express.json());\n\nconst DATA_DIR = path.join(__dirname, 'data');\n\n// Ensure data directory exists\nfs.mkdir(DATA_DIR, { recursive: true }).catch(() => {});\n\n// POST /documents\napp.post('/documents', async (req, res) => {\n try {\n const doc = {\n id: uuidv4(),\n title: req.body.title || 'Untitled',\n content: req.body.content,\n created_at: new Date().toISOString(),\n updated_at: new Date().toISOString()\n };\n \n await fs.writeFile(\n path.join(DATA_DIR, `${doc.id}.json`),\n JSON.stringify(doc, null, 2)\n );\n \n res.json({ id: doc.id });\n } catch (err) {\n console.error('Save error:', err);\n res.status(500).json({ error: 'Save failed', message: err.message });\n }\n});\n\n// GET /documents/:id\napp.get('/documents/:id', async (req, res) => {\n try {\n const data = await fs.readFile(\n path.join(DATA_DIR, `${req.params.id}.json`),\n 'utf-8'\n );\n res.json(JSON.parse(data));\n } catch (err) {\n console.error('Load error:', err);\n res.status(404).json({ error: 'Document not found' });\n }\n});\n\nconst PORT = 3001;\napp.listen(PORT, () => {\n console.log(`✓ Server running on http://localhost:${PORT}`);\n});"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"id": "1764106670567",
|
| 15 |
+
"text": "ewjklnrjwernflewnfwlrhnweornhi3hni32r"
|
| 16 |
+
}
|
| 17 |
+
],
|
| 18 |
+
"created_at": "2025-11-25T21:38:43.439Z",
|
| 19 |
+
"updated_at": "2025-11-25T21:38:43.439Z"
|
| 20 |
+
}
|
Document-Editor--master/server/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import express from 'express';
|
| 2 |
+
import cors from 'cors';
|
| 3 |
+
import { promises as fs } from 'fs';
|
| 4 |
+
import path from 'path';
|
| 5 |
+
import { fileURLToPath } from 'url';
|
| 6 |
+
import { v4 as uuidv4 } from 'uuid';
|
| 7 |
+
|
| 8 |
+
// Get __dirname in ES modules
|
| 9 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 10 |
+
const __dirname = path.dirname(__filename);
|
| 11 |
+
|
| 12 |
+
const app = express();
|
| 13 |
+
app.use(cors());
|
| 14 |
+
app.use(express.json());
|
| 15 |
+
|
| 16 |
+
const DATA_DIR = path.join(__dirname, 'data');
|
| 17 |
+
|
| 18 |
+
// Ensure data directory exists
|
| 19 |
+
fs.mkdir(DATA_DIR, { recursive: true }).catch(() => {});
|
| 20 |
+
|
| 21 |
+
// POST /documents
|
| 22 |
+
app.post('/documents', async (req, res) => {
|
| 23 |
+
try {
|
| 24 |
+
const doc = {
|
| 25 |
+
id: uuidv4(),
|
| 26 |
+
title: req.body.title || 'Untitled',
|
| 27 |
+
content: req.body.content,
|
| 28 |
+
created_at: new Date().toISOString(),
|
| 29 |
+
updated_at: new Date().toISOString()
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
await fs.writeFile(
|
| 33 |
+
path.join(DATA_DIR, `${doc.id}.json`),
|
| 34 |
+
JSON.stringify(doc, null, 2)
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
res.json({ id: doc.id });
|
| 38 |
+
} catch (err) {
|
| 39 |
+
console.error('Save error:', err);
|
| 40 |
+
res.status(500).json({ error: 'Save failed', message: err.message });
|
| 41 |
+
}
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// GET /documents/:id
|
| 45 |
+
app.get('/documents/:id', async (req, res) => {
|
| 46 |
+
try {
|
| 47 |
+
const data = await fs.readFile(
|
| 48 |
+
path.join(DATA_DIR, `${req.params.id}.json`),
|
| 49 |
+
'utf-8'
|
| 50 |
+
);
|
| 51 |
+
res.json(JSON.parse(data));
|
| 52 |
+
} catch (err) {
|
| 53 |
+
console.error('Load error:', err);
|
| 54 |
+
res.status(404).json({ error: 'Document not found' });
|
| 55 |
+
}
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
const PORT = 3001;
|
| 59 |
+
app.listen(PORT, () => {
|
| 60 |
+
console.log(`✓ Server running on http://localhost:${PORT}`);
|
| 61 |
+
});
|
Document-Editor--master/server/package-lock.json
ADDED
|
@@ -0,0 +1,851 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "server",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "server",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"license": "ISC",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"cors": "^2.8.5",
|
| 13 |
+
"express": "^5.1.0",
|
| 14 |
+
"uuid": "^13.0.0"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"node_modules/accepts": {
|
| 18 |
+
"version": "2.0.0",
|
| 19 |
+
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
| 20 |
+
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
| 21 |
+
"license": "MIT",
|
| 22 |
+
"dependencies": {
|
| 23 |
+
"mime-types": "^3.0.0",
|
| 24 |
+
"negotiator": "^1.0.0"
|
| 25 |
+
},
|
| 26 |
+
"engines": {
|
| 27 |
+
"node": ">= 0.6"
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"node_modules/body-parser": {
|
| 31 |
+
"version": "2.2.1",
|
| 32 |
+
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
| 33 |
+
"integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
|
| 34 |
+
"license": "MIT",
|
| 35 |
+
"dependencies": {
|
| 36 |
+
"bytes": "^3.1.2",
|
| 37 |
+
"content-type": "^1.0.5",
|
| 38 |
+
"debug": "^4.4.3",
|
| 39 |
+
"http-errors": "^2.0.0",
|
| 40 |
+
"iconv-lite": "^0.7.0",
|
| 41 |
+
"on-finished": "^2.4.1",
|
| 42 |
+
"qs": "^6.14.0",
|
| 43 |
+
"raw-body": "^3.0.1",
|
| 44 |
+
"type-is": "^2.0.1"
|
| 45 |
+
},
|
| 46 |
+
"engines": {
|
| 47 |
+
"node": ">=18"
|
| 48 |
+
},
|
| 49 |
+
"funding": {
|
| 50 |
+
"type": "opencollective",
|
| 51 |
+
"url": "https://opencollective.com/express"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"node_modules/bytes": {
|
| 55 |
+
"version": "3.1.2",
|
| 56 |
+
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
| 57 |
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
| 58 |
+
"license": "MIT",
|
| 59 |
+
"engines": {
|
| 60 |
+
"node": ">= 0.8"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
"node_modules/call-bind-apply-helpers": {
|
| 64 |
+
"version": "1.0.2",
|
| 65 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 66 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 67 |
+
"license": "MIT",
|
| 68 |
+
"dependencies": {
|
| 69 |
+
"es-errors": "^1.3.0",
|
| 70 |
+
"function-bind": "^1.1.2"
|
| 71 |
+
},
|
| 72 |
+
"engines": {
|
| 73 |
+
"node": ">= 0.4"
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"node_modules/call-bound": {
|
| 77 |
+
"version": "1.0.4",
|
| 78 |
+
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
| 79 |
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
| 80 |
+
"license": "MIT",
|
| 81 |
+
"dependencies": {
|
| 82 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 83 |
+
"get-intrinsic": "^1.3.0"
|
| 84 |
+
},
|
| 85 |
+
"engines": {
|
| 86 |
+
"node": ">= 0.4"
|
| 87 |
+
},
|
| 88 |
+
"funding": {
|
| 89 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"node_modules/content-disposition": {
|
| 93 |
+
"version": "1.0.1",
|
| 94 |
+
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
| 95 |
+
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
| 96 |
+
"license": "MIT",
|
| 97 |
+
"engines": {
|
| 98 |
+
"node": ">=18"
|
| 99 |
+
},
|
| 100 |
+
"funding": {
|
| 101 |
+
"type": "opencollective",
|
| 102 |
+
"url": "https://opencollective.com/express"
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
"node_modules/content-type": {
|
| 106 |
+
"version": "1.0.5",
|
| 107 |
+
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
| 108 |
+
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
| 109 |
+
"license": "MIT",
|
| 110 |
+
"engines": {
|
| 111 |
+
"node": ">= 0.6"
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"node_modules/cookie": {
|
| 115 |
+
"version": "0.7.2",
|
| 116 |
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
| 117 |
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
| 118 |
+
"license": "MIT",
|
| 119 |
+
"engines": {
|
| 120 |
+
"node": ">= 0.6"
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"node_modules/cookie-signature": {
|
| 124 |
+
"version": "1.2.2",
|
| 125 |
+
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
| 126 |
+
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
| 127 |
+
"license": "MIT",
|
| 128 |
+
"engines": {
|
| 129 |
+
"node": ">=6.6.0"
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"node_modules/cors": {
|
| 133 |
+
"version": "2.8.5",
|
| 134 |
+
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
| 135 |
+
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
| 136 |
+
"license": "MIT",
|
| 137 |
+
"dependencies": {
|
| 138 |
+
"object-assign": "^4",
|
| 139 |
+
"vary": "^1"
|
| 140 |
+
},
|
| 141 |
+
"engines": {
|
| 142 |
+
"node": ">= 0.10"
|
| 143 |
+
}
|
| 144 |
+
},
|
| 145 |
+
"node_modules/debug": {
|
| 146 |
+
"version": "4.4.3",
|
| 147 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 148 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 149 |
+
"license": "MIT",
|
| 150 |
+
"dependencies": {
|
| 151 |
+
"ms": "^2.1.3"
|
| 152 |
+
},
|
| 153 |
+
"engines": {
|
| 154 |
+
"node": ">=6.0"
|
| 155 |
+
},
|
| 156 |
+
"peerDependenciesMeta": {
|
| 157 |
+
"supports-color": {
|
| 158 |
+
"optional": true
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
"node_modules/depd": {
|
| 163 |
+
"version": "2.0.0",
|
| 164 |
+
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
| 165 |
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
| 166 |
+
"license": "MIT",
|
| 167 |
+
"engines": {
|
| 168 |
+
"node": ">= 0.8"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/dunder-proto": {
|
| 172 |
+
"version": "1.0.1",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 174 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 175 |
+
"license": "MIT",
|
| 176 |
+
"dependencies": {
|
| 177 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 178 |
+
"es-errors": "^1.3.0",
|
| 179 |
+
"gopd": "^1.2.0"
|
| 180 |
+
},
|
| 181 |
+
"engines": {
|
| 182 |
+
"node": ">= 0.4"
|
| 183 |
+
}
|
| 184 |
+
},
|
| 185 |
+
"node_modules/ee-first": {
|
| 186 |
+
"version": "1.1.1",
|
| 187 |
+
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
| 188 |
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
| 189 |
+
"license": "MIT"
|
| 190 |
+
},
|
| 191 |
+
"node_modules/encodeurl": {
|
| 192 |
+
"version": "2.0.0",
|
| 193 |
+
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
| 194 |
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
| 195 |
+
"license": "MIT",
|
| 196 |
+
"engines": {
|
| 197 |
+
"node": ">= 0.8"
|
| 198 |
+
}
|
| 199 |
+
},
|
| 200 |
+
"node_modules/es-define-property": {
|
| 201 |
+
"version": "1.0.1",
|
| 202 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 203 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 204 |
+
"license": "MIT",
|
| 205 |
+
"engines": {
|
| 206 |
+
"node": ">= 0.4"
|
| 207 |
+
}
|
| 208 |
+
},
|
| 209 |
+
"node_modules/es-errors": {
|
| 210 |
+
"version": "1.3.0",
|
| 211 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 212 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 213 |
+
"license": "MIT",
|
| 214 |
+
"engines": {
|
| 215 |
+
"node": ">= 0.4"
|
| 216 |
+
}
|
| 217 |
+
},
|
| 218 |
+
"node_modules/es-object-atoms": {
|
| 219 |
+
"version": "1.1.1",
|
| 220 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 221 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 222 |
+
"license": "MIT",
|
| 223 |
+
"dependencies": {
|
| 224 |
+
"es-errors": "^1.3.0"
|
| 225 |
+
},
|
| 226 |
+
"engines": {
|
| 227 |
+
"node": ">= 0.4"
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"node_modules/escape-html": {
|
| 231 |
+
"version": "1.0.3",
|
| 232 |
+
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
| 233 |
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
| 234 |
+
"license": "MIT"
|
| 235 |
+
},
|
| 236 |
+
"node_modules/etag": {
|
| 237 |
+
"version": "1.8.1",
|
| 238 |
+
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
| 239 |
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
| 240 |
+
"license": "MIT",
|
| 241 |
+
"engines": {
|
| 242 |
+
"node": ">= 0.6"
|
| 243 |
+
}
|
| 244 |
+
},
|
| 245 |
+
"node_modules/express": {
|
| 246 |
+
"version": "5.1.0",
|
| 247 |
+
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
| 248 |
+
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
| 249 |
+
"license": "MIT",
|
| 250 |
+
"dependencies": {
|
| 251 |
+
"accepts": "^2.0.0",
|
| 252 |
+
"body-parser": "^2.2.0",
|
| 253 |
+
"content-disposition": "^1.0.0",
|
| 254 |
+
"content-type": "^1.0.5",
|
| 255 |
+
"cookie": "^0.7.1",
|
| 256 |
+
"cookie-signature": "^1.2.1",
|
| 257 |
+
"debug": "^4.4.0",
|
| 258 |
+
"encodeurl": "^2.0.0",
|
| 259 |
+
"escape-html": "^1.0.3",
|
| 260 |
+
"etag": "^1.8.1",
|
| 261 |
+
"finalhandler": "^2.1.0",
|
| 262 |
+
"fresh": "^2.0.0",
|
| 263 |
+
"http-errors": "^2.0.0",
|
| 264 |
+
"merge-descriptors": "^2.0.0",
|
| 265 |
+
"mime-types": "^3.0.0",
|
| 266 |
+
"on-finished": "^2.4.1",
|
| 267 |
+
"once": "^1.4.0",
|
| 268 |
+
"parseurl": "^1.3.3",
|
| 269 |
+
"proxy-addr": "^2.0.7",
|
| 270 |
+
"qs": "^6.14.0",
|
| 271 |
+
"range-parser": "^1.2.1",
|
| 272 |
+
"router": "^2.2.0",
|
| 273 |
+
"send": "^1.1.0",
|
| 274 |
+
"serve-static": "^2.2.0",
|
| 275 |
+
"statuses": "^2.0.1",
|
| 276 |
+
"type-is": "^2.0.1",
|
| 277 |
+
"vary": "^1.1.2"
|
| 278 |
+
},
|
| 279 |
+
"engines": {
|
| 280 |
+
"node": ">= 18"
|
| 281 |
+
},
|
| 282 |
+
"funding": {
|
| 283 |
+
"type": "opencollective",
|
| 284 |
+
"url": "https://opencollective.com/express"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
"node_modules/finalhandler": {
|
| 288 |
+
"version": "2.1.0",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
| 290 |
+
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
|
| 291 |
+
"license": "MIT",
|
| 292 |
+
"dependencies": {
|
| 293 |
+
"debug": "^4.4.0",
|
| 294 |
+
"encodeurl": "^2.0.0",
|
| 295 |
+
"escape-html": "^1.0.3",
|
| 296 |
+
"on-finished": "^2.4.1",
|
| 297 |
+
"parseurl": "^1.3.3",
|
| 298 |
+
"statuses": "^2.0.1"
|
| 299 |
+
},
|
| 300 |
+
"engines": {
|
| 301 |
+
"node": ">= 0.8"
|
| 302 |
+
}
|
| 303 |
+
},
|
| 304 |
+
"node_modules/forwarded": {
|
| 305 |
+
"version": "0.2.0",
|
| 306 |
+
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
| 307 |
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
| 308 |
+
"license": "MIT",
|
| 309 |
+
"engines": {
|
| 310 |
+
"node": ">= 0.6"
|
| 311 |
+
}
|
| 312 |
+
},
|
| 313 |
+
"node_modules/fresh": {
|
| 314 |
+
"version": "2.0.0",
|
| 315 |
+
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
| 316 |
+
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
| 317 |
+
"license": "MIT",
|
| 318 |
+
"engines": {
|
| 319 |
+
"node": ">= 0.8"
|
| 320 |
+
}
|
| 321 |
+
},
|
| 322 |
+
"node_modules/function-bind": {
|
| 323 |
+
"version": "1.1.2",
|
| 324 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 325 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 326 |
+
"license": "MIT",
|
| 327 |
+
"funding": {
|
| 328 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 329 |
+
}
|
| 330 |
+
},
|
| 331 |
+
"node_modules/get-intrinsic": {
|
| 332 |
+
"version": "1.3.0",
|
| 333 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 334 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 335 |
+
"license": "MIT",
|
| 336 |
+
"dependencies": {
|
| 337 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 338 |
+
"es-define-property": "^1.0.1",
|
| 339 |
+
"es-errors": "^1.3.0",
|
| 340 |
+
"es-object-atoms": "^1.1.1",
|
| 341 |
+
"function-bind": "^1.1.2",
|
| 342 |
+
"get-proto": "^1.0.1",
|
| 343 |
+
"gopd": "^1.2.0",
|
| 344 |
+
"has-symbols": "^1.1.0",
|
| 345 |
+
"hasown": "^2.0.2",
|
| 346 |
+
"math-intrinsics": "^1.1.0"
|
| 347 |
+
},
|
| 348 |
+
"engines": {
|
| 349 |
+
"node": ">= 0.4"
|
| 350 |
+
},
|
| 351 |
+
"funding": {
|
| 352 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 353 |
+
}
|
| 354 |
+
},
|
| 355 |
+
"node_modules/get-proto": {
|
| 356 |
+
"version": "1.0.1",
|
| 357 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 358 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 359 |
+
"license": "MIT",
|
| 360 |
+
"dependencies": {
|
| 361 |
+
"dunder-proto": "^1.0.1",
|
| 362 |
+
"es-object-atoms": "^1.0.0"
|
| 363 |
+
},
|
| 364 |
+
"engines": {
|
| 365 |
+
"node": ">= 0.4"
|
| 366 |
+
}
|
| 367 |
+
},
|
| 368 |
+
"node_modules/gopd": {
|
| 369 |
+
"version": "1.2.0",
|
| 370 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 371 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 372 |
+
"license": "MIT",
|
| 373 |
+
"engines": {
|
| 374 |
+
"node": ">= 0.4"
|
| 375 |
+
},
|
| 376 |
+
"funding": {
|
| 377 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 378 |
+
}
|
| 379 |
+
},
|
| 380 |
+
"node_modules/has-symbols": {
|
| 381 |
+
"version": "1.1.0",
|
| 382 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 383 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 384 |
+
"license": "MIT",
|
| 385 |
+
"engines": {
|
| 386 |
+
"node": ">= 0.4"
|
| 387 |
+
},
|
| 388 |
+
"funding": {
|
| 389 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 390 |
+
}
|
| 391 |
+
},
|
| 392 |
+
"node_modules/hasown": {
|
| 393 |
+
"version": "2.0.2",
|
| 394 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 395 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 396 |
+
"license": "MIT",
|
| 397 |
+
"dependencies": {
|
| 398 |
+
"function-bind": "^1.1.2"
|
| 399 |
+
},
|
| 400 |
+
"engines": {
|
| 401 |
+
"node": ">= 0.4"
|
| 402 |
+
}
|
| 403 |
+
},
|
| 404 |
+
"node_modules/http-errors": {
|
| 405 |
+
"version": "2.0.1",
|
| 406 |
+
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
| 407 |
+
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
| 408 |
+
"license": "MIT",
|
| 409 |
+
"dependencies": {
|
| 410 |
+
"depd": "~2.0.0",
|
| 411 |
+
"inherits": "~2.0.4",
|
| 412 |
+
"setprototypeof": "~1.2.0",
|
| 413 |
+
"statuses": "~2.0.2",
|
| 414 |
+
"toidentifier": "~1.0.1"
|
| 415 |
+
},
|
| 416 |
+
"engines": {
|
| 417 |
+
"node": ">= 0.8"
|
| 418 |
+
},
|
| 419 |
+
"funding": {
|
| 420 |
+
"type": "opencollective",
|
| 421 |
+
"url": "https://opencollective.com/express"
|
| 422 |
+
}
|
| 423 |
+
},
|
| 424 |
+
"node_modules/iconv-lite": {
|
| 425 |
+
"version": "0.7.0",
|
| 426 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
| 427 |
+
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
| 428 |
+
"license": "MIT",
|
| 429 |
+
"dependencies": {
|
| 430 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 431 |
+
},
|
| 432 |
+
"engines": {
|
| 433 |
+
"node": ">=0.10.0"
|
| 434 |
+
},
|
| 435 |
+
"funding": {
|
| 436 |
+
"type": "opencollective",
|
| 437 |
+
"url": "https://opencollective.com/express"
|
| 438 |
+
}
|
| 439 |
+
},
|
| 440 |
+
"node_modules/inherits": {
|
| 441 |
+
"version": "2.0.4",
|
| 442 |
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
| 443 |
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
| 444 |
+
"license": "ISC"
|
| 445 |
+
},
|
| 446 |
+
"node_modules/ipaddr.js": {
|
| 447 |
+
"version": "1.9.1",
|
| 448 |
+
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
| 449 |
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
| 450 |
+
"license": "MIT",
|
| 451 |
+
"engines": {
|
| 452 |
+
"node": ">= 0.10"
|
| 453 |
+
}
|
| 454 |
+
},
|
| 455 |
+
"node_modules/is-promise": {
|
| 456 |
+
"version": "4.0.0",
|
| 457 |
+
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
| 458 |
+
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
| 459 |
+
"license": "MIT"
|
| 460 |
+
},
|
| 461 |
+
"node_modules/math-intrinsics": {
|
| 462 |
+
"version": "1.1.0",
|
| 463 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 464 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 465 |
+
"license": "MIT",
|
| 466 |
+
"engines": {
|
| 467 |
+
"node": ">= 0.4"
|
| 468 |
+
}
|
| 469 |
+
},
|
| 470 |
+
"node_modules/media-typer": {
|
| 471 |
+
"version": "1.1.0",
|
| 472 |
+
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
| 473 |
+
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
| 474 |
+
"license": "MIT",
|
| 475 |
+
"engines": {
|
| 476 |
+
"node": ">= 0.8"
|
| 477 |
+
}
|
| 478 |
+
},
|
| 479 |
+
"node_modules/merge-descriptors": {
|
| 480 |
+
"version": "2.0.0",
|
| 481 |
+
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
| 482 |
+
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
| 483 |
+
"license": "MIT",
|
| 484 |
+
"engines": {
|
| 485 |
+
"node": ">=18"
|
| 486 |
+
},
|
| 487 |
+
"funding": {
|
| 488 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 489 |
+
}
|
| 490 |
+
},
|
| 491 |
+
"node_modules/mime-db": {
|
| 492 |
+
"version": "1.54.0",
|
| 493 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
| 494 |
+
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
| 495 |
+
"license": "MIT",
|
| 496 |
+
"engines": {
|
| 497 |
+
"node": ">= 0.6"
|
| 498 |
+
}
|
| 499 |
+
},
|
| 500 |
+
"node_modules/mime-types": {
|
| 501 |
+
"version": "3.0.2",
|
| 502 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
| 503 |
+
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
| 504 |
+
"license": "MIT",
|
| 505 |
+
"dependencies": {
|
| 506 |
+
"mime-db": "^1.54.0"
|
| 507 |
+
},
|
| 508 |
+
"engines": {
|
| 509 |
+
"node": ">=18"
|
| 510 |
+
},
|
| 511 |
+
"funding": {
|
| 512 |
+
"type": "opencollective",
|
| 513 |
+
"url": "https://opencollective.com/express"
|
| 514 |
+
}
|
| 515 |
+
},
|
| 516 |
+
"node_modules/ms": {
|
| 517 |
+
"version": "2.1.3",
|
| 518 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 519 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 520 |
+
"license": "MIT"
|
| 521 |
+
},
|
| 522 |
+
"node_modules/negotiator": {
|
| 523 |
+
"version": "1.0.0",
|
| 524 |
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
| 525 |
+
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
| 526 |
+
"license": "MIT",
|
| 527 |
+
"engines": {
|
| 528 |
+
"node": ">= 0.6"
|
| 529 |
+
}
|
| 530 |
+
},
|
| 531 |
+
"node_modules/object-assign": {
|
| 532 |
+
"version": "4.1.1",
|
| 533 |
+
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
| 534 |
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 535 |
+
"license": "MIT",
|
| 536 |
+
"engines": {
|
| 537 |
+
"node": ">=0.10.0"
|
| 538 |
+
}
|
| 539 |
+
},
|
| 540 |
+
"node_modules/object-inspect": {
|
| 541 |
+
"version": "1.13.4",
|
| 542 |
+
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
| 543 |
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
| 544 |
+
"license": "MIT",
|
| 545 |
+
"engines": {
|
| 546 |
+
"node": ">= 0.4"
|
| 547 |
+
},
|
| 548 |
+
"funding": {
|
| 549 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 550 |
+
}
|
| 551 |
+
},
|
| 552 |
+
"node_modules/on-finished": {
|
| 553 |
+
"version": "2.4.1",
|
| 554 |
+
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
| 555 |
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
| 556 |
+
"license": "MIT",
|
| 557 |
+
"dependencies": {
|
| 558 |
+
"ee-first": "1.1.1"
|
| 559 |
+
},
|
| 560 |
+
"engines": {
|
| 561 |
+
"node": ">= 0.8"
|
| 562 |
+
}
|
| 563 |
+
},
|
| 564 |
+
"node_modules/once": {
|
| 565 |
+
"version": "1.4.0",
|
| 566 |
+
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
| 567 |
+
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
| 568 |
+
"license": "ISC",
|
| 569 |
+
"dependencies": {
|
| 570 |
+
"wrappy": "1"
|
| 571 |
+
}
|
| 572 |
+
},
|
| 573 |
+
"node_modules/parseurl": {
|
| 574 |
+
"version": "1.3.3",
|
| 575 |
+
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
| 576 |
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
| 577 |
+
"license": "MIT",
|
| 578 |
+
"engines": {
|
| 579 |
+
"node": ">= 0.8"
|
| 580 |
+
}
|
| 581 |
+
},
|
| 582 |
+
"node_modules/path-to-regexp": {
|
| 583 |
+
"version": "8.3.0",
|
| 584 |
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
| 585 |
+
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
| 586 |
+
"license": "MIT",
|
| 587 |
+
"funding": {
|
| 588 |
+
"type": "opencollective",
|
| 589 |
+
"url": "https://opencollective.com/express"
|
| 590 |
+
}
|
| 591 |
+
},
|
| 592 |
+
"node_modules/proxy-addr": {
|
| 593 |
+
"version": "2.0.7",
|
| 594 |
+
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
| 595 |
+
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
| 596 |
+
"license": "MIT",
|
| 597 |
+
"dependencies": {
|
| 598 |
+
"forwarded": "0.2.0",
|
| 599 |
+
"ipaddr.js": "1.9.1"
|
| 600 |
+
},
|
| 601 |
+
"engines": {
|
| 602 |
+
"node": ">= 0.10"
|
| 603 |
+
}
|
| 604 |
+
},
|
| 605 |
+
"node_modules/qs": {
|
| 606 |
+
"version": "6.14.0",
|
| 607 |
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
| 608 |
+
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
| 609 |
+
"license": "BSD-3-Clause",
|
| 610 |
+
"dependencies": {
|
| 611 |
+
"side-channel": "^1.1.0"
|
| 612 |
+
},
|
| 613 |
+
"engines": {
|
| 614 |
+
"node": ">=0.6"
|
| 615 |
+
},
|
| 616 |
+
"funding": {
|
| 617 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 618 |
+
}
|
| 619 |
+
},
|
| 620 |
+
"node_modules/range-parser": {
|
| 621 |
+
"version": "1.2.1",
|
| 622 |
+
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
| 623 |
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
| 624 |
+
"license": "MIT",
|
| 625 |
+
"engines": {
|
| 626 |
+
"node": ">= 0.6"
|
| 627 |
+
}
|
| 628 |
+
},
|
| 629 |
+
"node_modules/raw-body": {
|
| 630 |
+
"version": "3.0.2",
|
| 631 |
+
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
| 632 |
+
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
| 633 |
+
"license": "MIT",
|
| 634 |
+
"dependencies": {
|
| 635 |
+
"bytes": "~3.1.2",
|
| 636 |
+
"http-errors": "~2.0.1",
|
| 637 |
+
"iconv-lite": "~0.7.0",
|
| 638 |
+
"unpipe": "~1.0.0"
|
| 639 |
+
},
|
| 640 |
+
"engines": {
|
| 641 |
+
"node": ">= 0.10"
|
| 642 |
+
}
|
| 643 |
+
},
|
| 644 |
+
"node_modules/router": {
|
| 645 |
+
"version": "2.2.0",
|
| 646 |
+
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
| 647 |
+
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
| 648 |
+
"license": "MIT",
|
| 649 |
+
"dependencies": {
|
| 650 |
+
"debug": "^4.4.0",
|
| 651 |
+
"depd": "^2.0.0",
|
| 652 |
+
"is-promise": "^4.0.0",
|
| 653 |
+
"parseurl": "^1.3.3",
|
| 654 |
+
"path-to-regexp": "^8.0.0"
|
| 655 |
+
},
|
| 656 |
+
"engines": {
|
| 657 |
+
"node": ">= 18"
|
| 658 |
+
}
|
| 659 |
+
},
|
| 660 |
+
"node_modules/safer-buffer": {
|
| 661 |
+
"version": "2.1.2",
|
| 662 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 663 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 664 |
+
"license": "MIT"
|
| 665 |
+
},
|
| 666 |
+
"node_modules/send": {
|
| 667 |
+
"version": "1.2.0",
|
| 668 |
+
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
| 669 |
+
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
|
| 670 |
+
"license": "MIT",
|
| 671 |
+
"dependencies": {
|
| 672 |
+
"debug": "^4.3.5",
|
| 673 |
+
"encodeurl": "^2.0.0",
|
| 674 |
+
"escape-html": "^1.0.3",
|
| 675 |
+
"etag": "^1.8.1",
|
| 676 |
+
"fresh": "^2.0.0",
|
| 677 |
+
"http-errors": "^2.0.0",
|
| 678 |
+
"mime-types": "^3.0.1",
|
| 679 |
+
"ms": "^2.1.3",
|
| 680 |
+
"on-finished": "^2.4.1",
|
| 681 |
+
"range-parser": "^1.2.1",
|
| 682 |
+
"statuses": "^2.0.1"
|
| 683 |
+
},
|
| 684 |
+
"engines": {
|
| 685 |
+
"node": ">= 18"
|
| 686 |
+
}
|
| 687 |
+
},
|
| 688 |
+
"node_modules/serve-static": {
|
| 689 |
+
"version": "2.2.0",
|
| 690 |
+
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
| 691 |
+
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
|
| 692 |
+
"license": "MIT",
|
| 693 |
+
"dependencies": {
|
| 694 |
+
"encodeurl": "^2.0.0",
|
| 695 |
+
"escape-html": "^1.0.3",
|
| 696 |
+
"parseurl": "^1.3.3",
|
| 697 |
+
"send": "^1.2.0"
|
| 698 |
+
},
|
| 699 |
+
"engines": {
|
| 700 |
+
"node": ">= 18"
|
| 701 |
+
}
|
| 702 |
+
},
|
| 703 |
+
"node_modules/setprototypeof": {
|
| 704 |
+
"version": "1.2.0",
|
| 705 |
+
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
| 706 |
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
| 707 |
+
"license": "ISC"
|
| 708 |
+
},
|
| 709 |
+
"node_modules/side-channel": {
|
| 710 |
+
"version": "1.1.0",
|
| 711 |
+
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
| 712 |
+
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
| 713 |
+
"license": "MIT",
|
| 714 |
+
"dependencies": {
|
| 715 |
+
"es-errors": "^1.3.0",
|
| 716 |
+
"object-inspect": "^1.13.3",
|
| 717 |
+
"side-channel-list": "^1.0.0",
|
| 718 |
+
"side-channel-map": "^1.0.1",
|
| 719 |
+
"side-channel-weakmap": "^1.0.2"
|
| 720 |
+
},
|
| 721 |
+
"engines": {
|
| 722 |
+
"node": ">= 0.4"
|
| 723 |
+
},
|
| 724 |
+
"funding": {
|
| 725 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 726 |
+
}
|
| 727 |
+
},
|
| 728 |
+
"node_modules/side-channel-list": {
|
| 729 |
+
"version": "1.0.0",
|
| 730 |
+
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
| 731 |
+
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
| 732 |
+
"license": "MIT",
|
| 733 |
+
"dependencies": {
|
| 734 |
+
"es-errors": "^1.3.0",
|
| 735 |
+
"object-inspect": "^1.13.3"
|
| 736 |
+
},
|
| 737 |
+
"engines": {
|
| 738 |
+
"node": ">= 0.4"
|
| 739 |
+
},
|
| 740 |
+
"funding": {
|
| 741 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 742 |
+
}
|
| 743 |
+
},
|
| 744 |
+
"node_modules/side-channel-map": {
|
| 745 |
+
"version": "1.0.1",
|
| 746 |
+
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
| 747 |
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
| 748 |
+
"license": "MIT",
|
| 749 |
+
"dependencies": {
|
| 750 |
+
"call-bound": "^1.0.2",
|
| 751 |
+
"es-errors": "^1.3.0",
|
| 752 |
+
"get-intrinsic": "^1.2.5",
|
| 753 |
+
"object-inspect": "^1.13.3"
|
| 754 |
+
},
|
| 755 |
+
"engines": {
|
| 756 |
+
"node": ">= 0.4"
|
| 757 |
+
},
|
| 758 |
+
"funding": {
|
| 759 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 760 |
+
}
|
| 761 |
+
},
|
| 762 |
+
"node_modules/side-channel-weakmap": {
|
| 763 |
+
"version": "1.0.2",
|
| 764 |
+
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
| 765 |
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
| 766 |
+
"license": "MIT",
|
| 767 |
+
"dependencies": {
|
| 768 |
+
"call-bound": "^1.0.2",
|
| 769 |
+
"es-errors": "^1.3.0",
|
| 770 |
+
"get-intrinsic": "^1.2.5",
|
| 771 |
+
"object-inspect": "^1.13.3",
|
| 772 |
+
"side-channel-map": "^1.0.1"
|
| 773 |
+
},
|
| 774 |
+
"engines": {
|
| 775 |
+
"node": ">= 0.4"
|
| 776 |
+
},
|
| 777 |
+
"funding": {
|
| 778 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 779 |
+
}
|
| 780 |
+
},
|
| 781 |
+
"node_modules/statuses": {
|
| 782 |
+
"version": "2.0.2",
|
| 783 |
+
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
| 784 |
+
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
| 785 |
+
"license": "MIT",
|
| 786 |
+
"engines": {
|
| 787 |
+
"node": ">= 0.8"
|
| 788 |
+
}
|
| 789 |
+
},
|
| 790 |
+
"node_modules/toidentifier": {
|
| 791 |
+
"version": "1.0.1",
|
| 792 |
+
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
| 793 |
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
| 794 |
+
"license": "MIT",
|
| 795 |
+
"engines": {
|
| 796 |
+
"node": ">=0.6"
|
| 797 |
+
}
|
| 798 |
+
},
|
| 799 |
+
"node_modules/type-is": {
|
| 800 |
+
"version": "2.0.1",
|
| 801 |
+
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
| 802 |
+
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
| 803 |
+
"license": "MIT",
|
| 804 |
+
"dependencies": {
|
| 805 |
+
"content-type": "^1.0.5",
|
| 806 |
+
"media-typer": "^1.1.0",
|
| 807 |
+
"mime-types": "^3.0.0"
|
| 808 |
+
},
|
| 809 |
+
"engines": {
|
| 810 |
+
"node": ">= 0.6"
|
| 811 |
+
}
|
| 812 |
+
},
|
| 813 |
+
"node_modules/unpipe": {
|
| 814 |
+
"version": "1.0.0",
|
| 815 |
+
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
| 816 |
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
| 817 |
+
"license": "MIT",
|
| 818 |
+
"engines": {
|
| 819 |
+
"node": ">= 0.8"
|
| 820 |
+
}
|
| 821 |
+
},
|
| 822 |
+
"node_modules/uuid": {
|
| 823 |
+
"version": "13.0.0",
|
| 824 |
+
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
| 825 |
+
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
| 826 |
+
"funding": [
|
| 827 |
+
"https://github.com/sponsors/broofa",
|
| 828 |
+
"https://github.com/sponsors/ctavan"
|
| 829 |
+
],
|
| 830 |
+
"license": "MIT",
|
| 831 |
+
"bin": {
|
| 832 |
+
"uuid": "dist-node/bin/uuid"
|
| 833 |
+
}
|
| 834 |
+
},
|
| 835 |
+
"node_modules/vary": {
|
| 836 |
+
"version": "1.1.2",
|
| 837 |
+
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
| 838 |
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
| 839 |
+
"license": "MIT",
|
| 840 |
+
"engines": {
|
| 841 |
+
"node": ">= 0.8"
|
| 842 |
+
}
|
| 843 |
+
},
|
| 844 |
+
"node_modules/wrappy": {
|
| 845 |
+
"version": "1.0.2",
|
| 846 |
+
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
| 847 |
+
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
| 848 |
+
"license": "ISC"
|
| 849 |
+
}
|
| 850 |
+
}
|
| 851 |
+
}
|
Document-Editor--master/server/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "server",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"main": "index.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 8 |
+
},
|
| 9 |
+
"keywords": [],
|
| 10 |
+
"author": "",
|
| 11 |
+
"license": "ISC",
|
| 12 |
+
"description": "",
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"cors": "^2.8.5",
|
| 15 |
+
"express": "^5.1.0",
|
| 16 |
+
"uuid": "^13.0.0"
|
| 17 |
+
}
|
| 18 |
+
}
|
Document-Editor--master/server/tempCodeRunnerFile.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const cors = require('cors');
|
| 3 |
+
const fs = require('fs').promises;
|