markdown-notes-engine 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -2
- package/lib/README.md +123 -2
- package/lib/backend/github.mjs +316 -0
- package/lib/backend/index.mjs +74 -0
- package/lib/backend/markdown.mjs +60 -0
- package/lib/backend/routes/notes.mjs +197 -0
- package/lib/backend/routes/search.mjs +28 -0
- package/lib/backend/routes/upload.mjs +122 -0
- package/lib/backend/storage.mjs +119 -0
- package/lib/frontend/index.mjs +15 -0
- package/lib/index.mjs +17 -0
- package/package.json +30 -2
- package/lib/frontend/styles.css +0 -431
package/README.md
CHANGED
|
@@ -26,11 +26,60 @@ A complete, production-ready markdown note-taking engine with GitHub integration
|
|
|
26
26
|
npm install markdown-notes-engine
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### Module Support
|
|
30
|
+
|
|
31
|
+
This package supports both **ES Modules (ESM)** and **CommonJS (CJS)**:
|
|
32
|
+
|
|
33
|
+
**ES Modules (import)**
|
|
34
|
+
```javascript
|
|
35
|
+
import { createNotesRouter, GitHubClient, StorageClient } from 'markdown-notes-engine';
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**CommonJS (require)**
|
|
39
|
+
```javascript
|
|
40
|
+
const { createNotesRouter, GitHubClient, StorageClient } = require('markdown-notes-engine');
|
|
41
|
+
```
|
|
42
|
+
|
|
29
43
|
### Backend (Express)
|
|
30
44
|
|
|
45
|
+
<details>
|
|
46
|
+
<summary><b>ES Modules (import)</b></summary>
|
|
47
|
+
|
|
31
48
|
```javascript
|
|
49
|
+
import express from 'express';
|
|
50
|
+
import { createNotesRouter } from 'markdown-notes-engine';
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
|
|
54
|
+
const notesRouter = createNotesRouter({
|
|
55
|
+
github: {
|
|
56
|
+
token: process.env.GITHUB_TOKEN,
|
|
57
|
+
owner: process.env.GITHUB_OWNER,
|
|
58
|
+
repo: process.env.GITHUB_REPO
|
|
59
|
+
},
|
|
60
|
+
storage: {
|
|
61
|
+
type: 'r2',
|
|
62
|
+
accountId: process.env.R2_ACCOUNT_ID,
|
|
63
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
64
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
65
|
+
bucketName: process.env.R2_BUCKET_NAME,
|
|
66
|
+
publicUrl: process.env.R2_PUBLIC_URL
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
app.use('/api', notesRouter);
|
|
71
|
+
```
|
|
72
|
+
</details>
|
|
73
|
+
|
|
74
|
+
<details>
|
|
75
|
+
<summary><b>CommonJS (require)</b></summary>
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
const express = require('express');
|
|
32
79
|
const { createNotesRouter } = require('markdown-notes-engine');
|
|
33
80
|
|
|
81
|
+
const app = express();
|
|
82
|
+
|
|
34
83
|
const notesRouter = createNotesRouter({
|
|
35
84
|
github: {
|
|
36
85
|
token: process.env.GITHUB_TOKEN,
|
|
@@ -49,6 +98,7 @@ const notesRouter = createNotesRouter({
|
|
|
49
98
|
|
|
50
99
|
app.use('/api', notesRouter);
|
|
51
100
|
```
|
|
101
|
+
</details>
|
|
52
102
|
|
|
53
103
|
### Frontend (JavaScript)
|
|
54
104
|
|
|
@@ -121,13 +171,16 @@ This repository contains both the package source and a working notes application
|
|
|
121
171
|
markdown-notes-engine/
|
|
122
172
|
├── lib/ # NPM package source
|
|
123
173
|
│ ├── backend/ # Express router and API
|
|
174
|
+
│ │ ├── *.js # CommonJS modules
|
|
175
|
+
│ │ └── *.mjs # ES modules
|
|
124
176
|
│ ├── frontend/ # Editor UI component
|
|
125
|
-
│
|
|
177
|
+
│ ├── index.js # Main entry (CommonJS)
|
|
178
|
+
│ └── index.mjs # Main entry (ES modules)
|
|
126
179
|
├── examples/ # Usage examples
|
|
127
180
|
│ └── express-app/ # Express integration example
|
|
128
181
|
├── server.js # Demo server
|
|
129
182
|
├── public/ # Demo frontend
|
|
130
|
-
└── package.json # Package configuration
|
|
183
|
+
└── package.json # Package configuration (dual module support)
|
|
131
184
|
```
|
|
132
185
|
|
|
133
186
|
### Running the Demo
|
package/lib/README.md
CHANGED
|
@@ -23,8 +23,63 @@ npm install markdown-notes-engine
|
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
26
|
+
### Module Support
|
|
27
|
+
|
|
28
|
+
This package supports both **ES Modules (ESM)** and **CommonJS (CJS)**. Choose the syntax that works for your project:
|
|
29
|
+
|
|
30
|
+
**ES Modules (import)**
|
|
31
|
+
```javascript
|
|
32
|
+
import { createNotesRouter, GitHubClient, StorageClient, MarkdownRenderer } from 'markdown-notes-engine';
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**CommonJS (require)**
|
|
36
|
+
```javascript
|
|
37
|
+
const { createNotesRouter, GitHubClient, StorageClient, MarkdownRenderer } = require('markdown-notes-engine');
|
|
38
|
+
```
|
|
39
|
+
|
|
26
40
|
### Backend Setup (Express)
|
|
27
41
|
|
|
42
|
+
<details open>
|
|
43
|
+
<summary><b>ES Modules (import)</b></summary>
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
import express from 'express';
|
|
47
|
+
import { createNotesRouter } from 'markdown-notes-engine';
|
|
48
|
+
|
|
49
|
+
const app = express();
|
|
50
|
+
|
|
51
|
+
// Create the notes router with your configuration
|
|
52
|
+
const notesRouter = createNotesRouter({
|
|
53
|
+
github: {
|
|
54
|
+
token: process.env.GITHUB_TOKEN,
|
|
55
|
+
owner: process.env.GITHUB_OWNER,
|
|
56
|
+
repo: process.env.GITHUB_REPO
|
|
57
|
+
},
|
|
58
|
+
storage: {
|
|
59
|
+
type: 'r2', // or 's3'
|
|
60
|
+
accountId: process.env.R2_ACCOUNT_ID,
|
|
61
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
62
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
63
|
+
bucketName: process.env.R2_BUCKET_NAME,
|
|
64
|
+
publicUrl: process.env.R2_PUBLIC_URL
|
|
65
|
+
},
|
|
66
|
+
options: {
|
|
67
|
+
autoUpdateReadme: true // Auto-update README with note index
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Mount the router
|
|
72
|
+
app.use('/api', notesRouter);
|
|
73
|
+
|
|
74
|
+
app.listen(3000, () => {
|
|
75
|
+
console.log('Notes app running on http://localhost:3000');
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
</details>
|
|
79
|
+
|
|
80
|
+
<details>
|
|
81
|
+
<summary><b>CommonJS (require)</b></summary>
|
|
82
|
+
|
|
28
83
|
```javascript
|
|
29
84
|
const express = require('express');
|
|
30
85
|
const { createNotesRouter } = require('markdown-notes-engine');
|
|
@@ -58,6 +113,7 @@ app.listen(3000, () => {
|
|
|
58
113
|
console.log('Notes app running on http://localhost:3000');
|
|
59
114
|
});
|
|
60
115
|
```
|
|
116
|
+
</details>
|
|
61
117
|
|
|
62
118
|
### Frontend Setup (HTML + JavaScript)
|
|
63
119
|
|
|
@@ -191,7 +247,52 @@ When you mount the notes router, it provides these endpoints:
|
|
|
191
247
|
|
|
192
248
|
### Using Individual Components
|
|
193
249
|
|
|
194
|
-
You can use individual backend components if you need more control
|
|
250
|
+
You can use individual backend components if you need more control.
|
|
251
|
+
|
|
252
|
+
<details open>
|
|
253
|
+
<summary><b>ES Modules (import)</b></summary>
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
import { GitHubClient, StorageClient, MarkdownRenderer } from 'markdown-notes-engine';
|
|
257
|
+
|
|
258
|
+
// GitHub client
|
|
259
|
+
const github = new GitHubClient({
|
|
260
|
+
token: 'xxx',
|
|
261
|
+
owner: 'user',
|
|
262
|
+
repo: 'repo',
|
|
263
|
+
branch: 'main'
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Get file structure
|
|
267
|
+
const structure = await github.getFileStructure();
|
|
268
|
+
|
|
269
|
+
// Get file content
|
|
270
|
+
const file = await github.getFile('path/to/note.md');
|
|
271
|
+
|
|
272
|
+
// Save file
|
|
273
|
+
await github.saveFile('path/to/note.md', 'content', file.sha);
|
|
274
|
+
|
|
275
|
+
// Storage client
|
|
276
|
+
const storage = new StorageClient({
|
|
277
|
+
type: 'r2',
|
|
278
|
+
accountId: 'xxx',
|
|
279
|
+
accessKeyId: 'xxx',
|
|
280
|
+
secretAccessKey: 'xxx',
|
|
281
|
+
bucketName: 'bucket',
|
|
282
|
+
publicUrl: 'https://bucket.r2.dev'
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Upload image
|
|
286
|
+
const imageUrl = await storage.uploadImage(buffer, 'image.png', 'folder');
|
|
287
|
+
|
|
288
|
+
// Markdown renderer
|
|
289
|
+
const renderer = new MarkdownRenderer();
|
|
290
|
+
const html = renderer.render('# Hello World');
|
|
291
|
+
```
|
|
292
|
+
</details>
|
|
293
|
+
|
|
294
|
+
<details>
|
|
295
|
+
<summary><b>CommonJS (require)</b></summary>
|
|
195
296
|
|
|
196
297
|
```javascript
|
|
197
298
|
const { GitHubClient, StorageClient, MarkdownRenderer } = require('markdown-notes-engine');
|
|
@@ -200,7 +301,8 @@ const { GitHubClient, StorageClient, MarkdownRenderer } = require('markdown-note
|
|
|
200
301
|
const github = new GitHubClient({
|
|
201
302
|
token: 'xxx',
|
|
202
303
|
owner: 'user',
|
|
203
|
-
repo: 'repo'
|
|
304
|
+
repo: 'repo',
|
|
305
|
+
branch: 'main'
|
|
204
306
|
});
|
|
205
307
|
|
|
206
308
|
// Get file structure
|
|
@@ -229,6 +331,25 @@ const imageUrl = await storage.uploadImage(buffer, 'image.png', 'folder');
|
|
|
229
331
|
const renderer = new MarkdownRenderer();
|
|
230
332
|
const html = renderer.render('# Hello World');
|
|
231
333
|
```
|
|
334
|
+
</details>
|
|
335
|
+
|
|
336
|
+
### Subpath Imports
|
|
337
|
+
|
|
338
|
+
You can also import components directly from their subpaths:
|
|
339
|
+
|
|
340
|
+
**ES Modules**
|
|
341
|
+
```javascript
|
|
342
|
+
import { GitHubClient } from 'markdown-notes-engine/backend/github';
|
|
343
|
+
import { StorageClient } from 'markdown-notes-engine/backend/storage';
|
|
344
|
+
import { MarkdownRenderer } from 'markdown-notes-engine/backend/markdown';
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**CommonJS**
|
|
348
|
+
```javascript
|
|
349
|
+
const { GitHubClient } = require('markdown-notes-engine/backend/github');
|
|
350
|
+
const { StorageClient } = require('markdown-notes-engine/backend/storage');
|
|
351
|
+
const { MarkdownRenderer } = require('markdown-notes-engine/backend/markdown');
|
|
352
|
+
```
|
|
232
353
|
|
|
233
354
|
### Frontend Callbacks
|
|
234
355
|
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Client Wrapper
|
|
3
|
+
* Handles all GitHub API operations for note management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Octokit } from '@octokit/rest';
|
|
7
|
+
|
|
8
|
+
export class GitHubClient {
|
|
9
|
+
constructor({ token, owner, repo, branch = 'main' }) {
|
|
10
|
+
this.octokit = new Octokit({ auth: token });
|
|
11
|
+
this.owner = owner;
|
|
12
|
+
this.repo = repo;
|
|
13
|
+
this.branch = branch;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the default branch for the repository
|
|
18
|
+
* @returns {Promise<string>} Default branch name
|
|
19
|
+
*/
|
|
20
|
+
async getDefaultBranch() {
|
|
21
|
+
try {
|
|
22
|
+
const response = await this.octokit.repos.get({
|
|
23
|
+
owner: this.owner,
|
|
24
|
+
repo: this.repo
|
|
25
|
+
});
|
|
26
|
+
return response.data.default_branch;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Failed to get default branch:', error.message);
|
|
29
|
+
return 'main'; // fallback
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get repository file tree structure
|
|
35
|
+
* @returns {Promise<Array>} Array of files and folders
|
|
36
|
+
*/
|
|
37
|
+
async getFileStructure() {
|
|
38
|
+
try {
|
|
39
|
+
// Get the entire tree recursively using git API
|
|
40
|
+
const { data } = await this.octokit.git.getTree({
|
|
41
|
+
owner: this.owner,
|
|
42
|
+
repo: this.repo,
|
|
43
|
+
tree_sha: this.branch,
|
|
44
|
+
recursive: '1'
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Filter to only blob files (not trees)
|
|
48
|
+
const files = data.tree.filter(item => item.type === 'blob');
|
|
49
|
+
|
|
50
|
+
return this._buildStructureFromFiles(files);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.status === 404) {
|
|
53
|
+
// Branch doesn't exist, try to get default branch
|
|
54
|
+
try {
|
|
55
|
+
const defaultBranch = await this.getDefaultBranch();
|
|
56
|
+
this.branch = defaultBranch; // Update for future calls
|
|
57
|
+
|
|
58
|
+
const { data } = await this.octokit.git.getTree({
|
|
59
|
+
owner: this.owner,
|
|
60
|
+
repo: this.repo,
|
|
61
|
+
tree_sha: defaultBranch,
|
|
62
|
+
recursive: '1'
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const files = data.tree.filter(item => item.type === 'blob');
|
|
66
|
+
return this._buildStructureFromFiles(files);
|
|
67
|
+
} catch (retryError) {
|
|
68
|
+
// Repository might be empty
|
|
69
|
+
console.error('Repository appears to be empty or inaccessible:', retryError.message);
|
|
70
|
+
return []; // Return empty array for empty repos
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get file content
|
|
79
|
+
* @param {string} path - File path
|
|
80
|
+
* @returns {Promise<Object>} File content and metadata
|
|
81
|
+
*/
|
|
82
|
+
async getFile(path) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await this.octokit.repos.getContent({
|
|
85
|
+
owner: this.owner,
|
|
86
|
+
repo: this.repo,
|
|
87
|
+
path: path,
|
|
88
|
+
ref: this.branch
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Check if this is a file (not a directory)
|
|
92
|
+
if (!response.data.content) {
|
|
93
|
+
console.error('No content in response - might be a directory');
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: Buffer.from(response.data.content, 'base64').toString('utf-8'),
|
|
99
|
+
sha: response.data.sha,
|
|
100
|
+
path: response.data.path
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error.status === 404) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create or update a file
|
|
112
|
+
* @param {string} path - File path
|
|
113
|
+
* @param {string} content - File content
|
|
114
|
+
* @param {string} [sha] - File SHA (required for updates)
|
|
115
|
+
* @returns {Promise<Object>} Response from GitHub
|
|
116
|
+
*/
|
|
117
|
+
async saveFile(path, content, sha = null) {
|
|
118
|
+
const params = {
|
|
119
|
+
owner: this.owner,
|
|
120
|
+
repo: this.repo,
|
|
121
|
+
path: path,
|
|
122
|
+
message: sha ? `Update ${path}` : `Create ${path}`,
|
|
123
|
+
content: Buffer.from(content).toString('base64'),
|
|
124
|
+
branch: this.branch
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (sha) {
|
|
128
|
+
params.sha = sha;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const response = await this.octokit.repos.createOrUpdateFileContents(params);
|
|
132
|
+
return response.data;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete a file
|
|
137
|
+
* @param {string} path - File path
|
|
138
|
+
* @param {string} sha - File SHA
|
|
139
|
+
* @returns {Promise<Object>} Response from GitHub
|
|
140
|
+
*/
|
|
141
|
+
async deleteFile(path, sha) {
|
|
142
|
+
const response = await this.octokit.repos.deleteFile({
|
|
143
|
+
owner: this.owner,
|
|
144
|
+
repo: this.repo,
|
|
145
|
+
path: path,
|
|
146
|
+
message: `Delete ${path}`,
|
|
147
|
+
sha: sha,
|
|
148
|
+
branch: this.branch
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return response.data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a folder (by creating a .gitkeep file)
|
|
156
|
+
* @param {string} path - Folder path
|
|
157
|
+
* @returns {Promise<Object>} Response from GitHub
|
|
158
|
+
*/
|
|
159
|
+
async createFolder(path) {
|
|
160
|
+
const gitkeepPath = `${path}/.gitkeep`;
|
|
161
|
+
return this.saveFile(gitkeepPath, '', null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Delete a folder (recursively delete all files)
|
|
166
|
+
* @param {string} path - Folder path
|
|
167
|
+
* @returns {Promise<Array>} Array of delete responses
|
|
168
|
+
*/
|
|
169
|
+
async deleteFolder(path) {
|
|
170
|
+
const files = await this._getFilesInFolder(path);
|
|
171
|
+
const deletePromises = files.map(file =>
|
|
172
|
+
this.deleteFile(file.path, file.sha)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return Promise.all(deletePromises);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Search notes for a query
|
|
180
|
+
* @param {string} query - Search query
|
|
181
|
+
* @returns {Promise<Array>} Search results
|
|
182
|
+
*/
|
|
183
|
+
async searchNotes(query) {
|
|
184
|
+
const searchQuery = `${query} repo:${this.owner}/${this.repo}`;
|
|
185
|
+
const response = await this.octokit.search.code({
|
|
186
|
+
q: searchQuery
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const results = [];
|
|
190
|
+
|
|
191
|
+
for (const item of response.data.items) {
|
|
192
|
+
if (!item.path.endsWith('.md')) continue;
|
|
193
|
+
|
|
194
|
+
const file = await this.getFile(item.path);
|
|
195
|
+
if (!file) continue;
|
|
196
|
+
|
|
197
|
+
const lines = file.content.split('\n');
|
|
198
|
+
const matches = [];
|
|
199
|
+
|
|
200
|
+
lines.forEach((line, index) => {
|
|
201
|
+
if (line.toLowerCase().includes(query.toLowerCase())) {
|
|
202
|
+
matches.push({
|
|
203
|
+
line: index + 1,
|
|
204
|
+
content: line
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (matches.length > 0) {
|
|
210
|
+
results.push({
|
|
211
|
+
path: item.path,
|
|
212
|
+
matches: matches
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return results;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get all files in a folder
|
|
222
|
+
* @private
|
|
223
|
+
*/
|
|
224
|
+
async _getFilesInFolder(path) {
|
|
225
|
+
const response = await this.octokit.repos.getContent({
|
|
226
|
+
owner: this.owner,
|
|
227
|
+
repo: this.repo,
|
|
228
|
+
path: path,
|
|
229
|
+
ref: this.branch
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let files = [];
|
|
233
|
+
|
|
234
|
+
for (const item of response.data) {
|
|
235
|
+
if (item.type === 'file') {
|
|
236
|
+
files.push(item);
|
|
237
|
+
} else if (item.type === 'dir') {
|
|
238
|
+
const subFiles = await this._getFilesInFolder(item.path);
|
|
239
|
+
files = files.concat(subFiles);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return files;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Build hierarchical structure from flat file list
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
_buildStructureFromFiles(files) {
|
|
251
|
+
const root = [];
|
|
252
|
+
const folderMap = new Map();
|
|
253
|
+
|
|
254
|
+
// Sort files to ensure consistent ordering
|
|
255
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
256
|
+
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
const parts = file.path.split('/');
|
|
259
|
+
let currentLevel = root;
|
|
260
|
+
let currentPath = '';
|
|
261
|
+
|
|
262
|
+
// Process each part of the path
|
|
263
|
+
for (let i = 0; i < parts.length; i++) {
|
|
264
|
+
const part = parts[i];
|
|
265
|
+
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
|
266
|
+
|
|
267
|
+
if (i === parts.length - 1) {
|
|
268
|
+
// It's a file - skip .gitkeep files
|
|
269
|
+
if (part === '.gitkeep') {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
currentLevel.push({
|
|
274
|
+
name: part,
|
|
275
|
+
type: 'file',
|
|
276
|
+
path: file.path,
|
|
277
|
+
sha: file.sha
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
// It's a folder
|
|
281
|
+
let folder = folderMap.get(currentPath);
|
|
282
|
+
|
|
283
|
+
if (!folder) {
|
|
284
|
+
folder = {
|
|
285
|
+
name: part,
|
|
286
|
+
type: 'folder',
|
|
287
|
+
path: currentPath,
|
|
288
|
+
children: []
|
|
289
|
+
};
|
|
290
|
+
folderMap.set(currentPath, folder);
|
|
291
|
+
currentLevel.push(folder);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
currentLevel = folder.children;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Sort function: folders first, then alphabetically
|
|
300
|
+
const sortStructure = (items) => {
|
|
301
|
+
items.sort((a, b) => {
|
|
302
|
+
if (a.type === b.type) return a.name.localeCompare(b.name);
|
|
303
|
+
return a.type === 'folder' ? -1 : 1;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
items.forEach((item) => {
|
|
307
|
+
if (item.type === 'folder' && item.children) {
|
|
308
|
+
sortStructure(item.children);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
sortStructure(root);
|
|
314
|
+
return root;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Notes Engine - Backend Router
|
|
3
|
+
*
|
|
4
|
+
* Creates an Express router with all note-taking API endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { GitHubClient } from './github.mjs';
|
|
9
|
+
import { StorageClient } from './storage.mjs';
|
|
10
|
+
import { MarkdownRenderer } from './markdown.mjs';
|
|
11
|
+
import notesRoutes from './routes/notes.mjs';
|
|
12
|
+
import uploadRoutes from './routes/upload.mjs';
|
|
13
|
+
import searchRoutes from './routes/search.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a configured notes router
|
|
17
|
+
* @param {Object} config - Configuration object
|
|
18
|
+
* @param {Object} config.github - GitHub configuration
|
|
19
|
+
* @param {string} config.github.token - GitHub personal access token
|
|
20
|
+
* @param {string} config.github.owner - Repository owner
|
|
21
|
+
* @param {string} config.github.repo - Repository name
|
|
22
|
+
* @param {string} [config.github.branch='main'] - Repository branch (defaults to 'main')
|
|
23
|
+
* @param {Object} config.storage - Storage configuration (R2 or S3)
|
|
24
|
+
* @param {string} config.storage.type - 'r2' or 's3'
|
|
25
|
+
* @param {string} config.storage.accountId - Account ID (R2) or region (S3)
|
|
26
|
+
* @param {string} config.storage.accessKeyId - Access key ID
|
|
27
|
+
* @param {string} config.storage.secretAccessKey - Secret access key
|
|
28
|
+
* @param {string} config.storage.bucketName - Bucket name
|
|
29
|
+
* @param {string} config.storage.publicUrl - Public URL for accessing files
|
|
30
|
+
* @param {Object} [config.options] - Optional configuration
|
|
31
|
+
* @param {boolean} [config.options.autoUpdateReadme=true] - Auto-update README on note save
|
|
32
|
+
* @returns {express.Router} Configured Express router
|
|
33
|
+
*/
|
|
34
|
+
export function createNotesRouter(config) {
|
|
35
|
+
if (!config.github || !config.github.token || !config.github.owner || !config.github.repo) {
|
|
36
|
+
throw new Error('GitHub configuration is required: token, owner, and repo');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!config.storage) {
|
|
40
|
+
throw new Error('Storage configuration is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const router = express.Router();
|
|
44
|
+
|
|
45
|
+
// Initialize clients
|
|
46
|
+
const githubClient = new GitHubClient({
|
|
47
|
+
token: config.github.token,
|
|
48
|
+
owner: config.github.owner,
|
|
49
|
+
repo: config.github.repo,
|
|
50
|
+
branch: config.github.branch || 'main'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const storageClient = new StorageClient(config.storage);
|
|
54
|
+
const markdownRenderer = new MarkdownRenderer();
|
|
55
|
+
const options = config.options || {};
|
|
56
|
+
|
|
57
|
+
// Middleware to attach clients to request
|
|
58
|
+
router.use((req, res, next) => {
|
|
59
|
+
req.notesEngine = {
|
|
60
|
+
githubClient,
|
|
61
|
+
storageClient,
|
|
62
|
+
markdownRenderer,
|
|
63
|
+
options
|
|
64
|
+
};
|
|
65
|
+
next();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Mount route handlers
|
|
69
|
+
router.use(notesRoutes);
|
|
70
|
+
router.use(uploadRoutes);
|
|
71
|
+
router.use(searchRoutes);
|
|
72
|
+
|
|
73
|
+
return router;
|
|
74
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Renderer
|
|
3
|
+
* Renders markdown to HTML with syntax highlighting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { marked } from 'marked';
|
|
7
|
+
import { markedHighlight } from 'marked-highlight';
|
|
8
|
+
import hljs from 'highlight.js';
|
|
9
|
+
|
|
10
|
+
export class MarkdownRenderer {
|
|
11
|
+
constructor() {
|
|
12
|
+
// Configure marked with syntax highlighting
|
|
13
|
+
marked.use(markedHighlight({
|
|
14
|
+
langPrefix: 'hljs language-',
|
|
15
|
+
highlight(code, lang) {
|
|
16
|
+
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
17
|
+
return hljs.highlight(code, { language }).value;
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Custom renderer for links and videos
|
|
22
|
+
const renderer = new marked.Renderer();
|
|
23
|
+
|
|
24
|
+
// Custom link renderer - handles internal markdown links
|
|
25
|
+
renderer.link = (href, title, text) => {
|
|
26
|
+
if (href.endsWith('.md')) {
|
|
27
|
+
return `<a href="#" class="internal-link" data-path="${href}">${text}</a>`;
|
|
28
|
+
}
|
|
29
|
+
return `<a href="${href}" ${title ? `title="${title}"` : ''}>${text}</a>`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Custom image renderer - detects videos and renders video tag
|
|
33
|
+
renderer.image = (href, title, text) => {
|
|
34
|
+
// Check if it's a video
|
|
35
|
+
const videoExtensions = ['.mp4', '.webm', '.mov', '.avi', '.mkv'];
|
|
36
|
+
const isVideo = videoExtensions.some(ext => href.toLowerCase().endsWith(ext)) ||
|
|
37
|
+
href.includes('/videos/');
|
|
38
|
+
|
|
39
|
+
if (isVideo) {
|
|
40
|
+
return `<video controls style="max-width: 100%;" ${title ? `title="${title}"` : ''}>
|
|
41
|
+
<source src="${href}" type="video/mp4">
|
|
42
|
+
Your browser does not support the video tag.
|
|
43
|
+
</video>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `<img src="${href}" alt="${text}" ${title ? `title="${title}"` : ''} style="max-width: 100%;">`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
marked.use({ renderer });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render markdown to HTML
|
|
54
|
+
* @param {string} markdown - Markdown content
|
|
55
|
+
* @returns {string} HTML content
|
|
56
|
+
*/
|
|
57
|
+
render(markdown) {
|
|
58
|
+
return marked(markdown);
|
|
59
|
+
}
|
|
60
|
+
}
|