markdown-notes-engine 1.0.0
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 +196 -0
- package/lib/README.md +312 -0
- package/lib/backend/github.js +318 -0
- package/lib/backend/index.js +76 -0
- package/lib/backend/markdown.js +62 -0
- package/lib/backend/routes/notes.js +197 -0
- package/lib/backend/routes/search.js +28 -0
- package/lib/backend/routes/upload.js +122 -0
- package/lib/backend/storage.js +121 -0
- package/lib/frontend/index.js +665 -0
- package/lib/frontend/styles.css +431 -0
- package/lib/index.js +28 -0
- package/package.json +51 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Client Wrapper
|
|
3
|
+
* Handles all GitHub API operations for note management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Octokit } = require('@octokit/rest');
|
|
7
|
+
|
|
8
|
+
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
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = { GitHubClient };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Notes Engine - Backend Router
|
|
3
|
+
*
|
|
4
|
+
* Creates an Express router with all note-taking API endpoints
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const { GitHubClient } = require('./github');
|
|
9
|
+
const { StorageClient } = require('./storage');
|
|
10
|
+
const { MarkdownRenderer } = require('./markdown');
|
|
11
|
+
const notesRoutes = require('./routes/notes');
|
|
12
|
+
const uploadRoutes = require('./routes/upload');
|
|
13
|
+
const searchRoutes = require('./routes/search');
|
|
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
|
+
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
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { createNotesRouter };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Renderer
|
|
3
|
+
* Renders markdown to HTML with syntax highlighting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { marked } = require('marked');
|
|
7
|
+
const { markedHighlight } = require('marked-highlight');
|
|
8
|
+
const hljs = require('highlight.js');
|
|
9
|
+
|
|
10
|
+
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
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { MarkdownRenderer };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes Routes
|
|
3
|
+
* Handles note CRUD operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
// Get file structure
|
|
10
|
+
router.get('/structure', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const { githubClient } = req.notesEngine;
|
|
13
|
+
const structure = await githubClient.getFileStructure();
|
|
14
|
+
res.json({ structure });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Error fetching structure:', error);
|
|
17
|
+
res.status(500).json({ error: 'Failed to fetch file structure' });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Get note content
|
|
22
|
+
router.get('/note', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { path } = req.query;
|
|
25
|
+
if (!path) {
|
|
26
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { githubClient } = req.notesEngine;
|
|
30
|
+
const file = await githubClient.getFile(path);
|
|
31
|
+
|
|
32
|
+
if (!file) {
|
|
33
|
+
return res.status(404).json({ error: 'Note not found' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
res.json(file);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error fetching note:', error);
|
|
39
|
+
res.status(500).json({ error: 'Failed to fetch note' });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Save note
|
|
44
|
+
router.post('/note', async (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
let { path, content } = req.body;
|
|
47
|
+
|
|
48
|
+
if (!path || content === undefined) {
|
|
49
|
+
return res.status(400).json({ error: 'Path and content are required' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Ensure .md extension
|
|
53
|
+
if (!path.endsWith('.md')) {
|
|
54
|
+
path += '.md';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { githubClient, options } = req.notesEngine;
|
|
58
|
+
|
|
59
|
+
// Get existing file SHA if it exists
|
|
60
|
+
const existingFile = await githubClient.getFile(path);
|
|
61
|
+
const sha = existingFile ? existingFile.sha : null;
|
|
62
|
+
|
|
63
|
+
// Save the file
|
|
64
|
+
await githubClient.saveFile(path, content, sha);
|
|
65
|
+
|
|
66
|
+
// Auto-update README if enabled
|
|
67
|
+
if (options.autoUpdateReadme !== false && path !== 'README.md') {
|
|
68
|
+
try {
|
|
69
|
+
await updateReadme(githubClient);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Error updating README:', error);
|
|
72
|
+
// Don't fail the request if README update fails
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
res.json({ success: true, path });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error saving note:', error);
|
|
79
|
+
res.status(500).json({ error: 'Failed to save note' });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Delete note
|
|
84
|
+
router.delete('/note', async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const { path } = req.body;
|
|
87
|
+
|
|
88
|
+
if (!path) {
|
|
89
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { githubClient } = req.notesEngine;
|
|
93
|
+
|
|
94
|
+
// Get file SHA
|
|
95
|
+
const file = await githubClient.getFile(path);
|
|
96
|
+
if (!file) {
|
|
97
|
+
return res.status(404).json({ error: 'Note not found' });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await githubClient.deleteFile(path, file.sha);
|
|
101
|
+
|
|
102
|
+
res.json({ success: true });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error deleting note:', error);
|
|
105
|
+
res.status(500).json({ error: 'Failed to delete note' });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Create folder
|
|
110
|
+
router.post('/folder', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const { path } = req.body;
|
|
113
|
+
|
|
114
|
+
if (!path) {
|
|
115
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { githubClient } = req.notesEngine;
|
|
119
|
+
await githubClient.createFolder(path);
|
|
120
|
+
|
|
121
|
+
res.json({ success: true });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error creating folder:', error);
|
|
124
|
+
res.status(500).json({ error: 'Failed to create folder' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Delete folder
|
|
129
|
+
router.delete('/folder', async (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { path } = req.body;
|
|
132
|
+
|
|
133
|
+
if (!path) {
|
|
134
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { githubClient } = req.notesEngine;
|
|
138
|
+
await githubClient.deleteFolder(path);
|
|
139
|
+
|
|
140
|
+
res.json({ success: true });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error deleting folder:', error);
|
|
143
|
+
res.status(500).json({ error: 'Failed to delete folder' });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Render markdown to HTML
|
|
148
|
+
router.post('/render', async (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const { markdown } = req.body;
|
|
151
|
+
|
|
152
|
+
if (markdown === undefined) {
|
|
153
|
+
return res.status(400).json({ error: 'Markdown content is required' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { markdownRenderer } = req.notesEngine;
|
|
157
|
+
const html = markdownRenderer.render(markdown);
|
|
158
|
+
|
|
159
|
+
res.json({ html });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('Error rendering markdown:', error);
|
|
162
|
+
res.status(500).json({ error: 'Failed to render markdown' });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Auto-update README with list of notes
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
async function updateReadme(githubClient) {
|
|
171
|
+
const structure = await githubClient.getFileStructure();
|
|
172
|
+
|
|
173
|
+
let readmeContent = '# Notes\n\n';
|
|
174
|
+
readmeContent += 'This is an auto-generated index of all notes.\n\n';
|
|
175
|
+
|
|
176
|
+
const mdFiles = structure
|
|
177
|
+
.filter(item => item.type === 'file' && item.name.endsWith('.md') && item.name !== 'README.md')
|
|
178
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
179
|
+
|
|
180
|
+
if (mdFiles.length === 0) {
|
|
181
|
+
readmeContent += '*No notes yet.*\n';
|
|
182
|
+
} else {
|
|
183
|
+
mdFiles.forEach(file => {
|
|
184
|
+
const displayName = file.name.replace('.md', '');
|
|
185
|
+
readmeContent += `- [${displayName}](${file.path})\n`;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
readmeContent += `\n---\n*Last updated: ${new Date().toISOString()}*\n`;
|
|
190
|
+
|
|
191
|
+
const existingReadme = await githubClient.getFile('README.md');
|
|
192
|
+
const sha = existingReadme ? existingReadme.sha : null;
|
|
193
|
+
|
|
194
|
+
await githubClient.saveFile('README.md', readmeContent, sha);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = router;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Routes
|
|
3
|
+
* Handles note searching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
// Search notes
|
|
10
|
+
router.get('/search', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const { q: query } = req.query;
|
|
13
|
+
|
|
14
|
+
if (!query) {
|
|
15
|
+
return res.status(400).json({ error: 'Query parameter "q" is required' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { githubClient } = req.notesEngine;
|
|
19
|
+
const results = await githubClient.searchNotes(query);
|
|
20
|
+
|
|
21
|
+
res.json({ results });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error searching notes:', error);
|
|
24
|
+
res.status(500).json({ error: 'Failed to search notes' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
module.exports = router;
|