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.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Notes Routes
3
+ * Handles note CRUD operations
4
+ */
5
+
6
+ import express from '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
+ export default router;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Search Routes
3
+ * Handles note searching
4
+ */
5
+
6
+ import express from '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
+ export default router;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Upload Routes
3
+ * Handles image and video uploads
4
+ */
5
+
6
+ import express from 'express';
7
+ import fileUpload from 'express-fileupload';
8
+ const router = express.Router();
9
+
10
+ // File upload middleware
11
+ router.use(fileUpload());
12
+
13
+ // Upload image (multipart form)
14
+ router.post('/upload-image', async (req, res) => {
15
+ try {
16
+ if (!req.files || !req.files.image) {
17
+ return res.status(400).json({ error: 'No image file provided' });
18
+ }
19
+
20
+ const imageFile = req.files.image;
21
+ const folder = req.body.folder || '';
22
+
23
+ const { storageClient } = req.notesEngine;
24
+ const imageUrl = await storageClient.uploadImage(
25
+ imageFile.data,
26
+ imageFile.name,
27
+ folder
28
+ );
29
+
30
+ res.json({ imageUrl });
31
+ } catch (error) {
32
+ console.error('Error uploading image:', error);
33
+ res.status(500).json({ error: 'Failed to upload image' });
34
+ }
35
+ });
36
+
37
+ // Upload image (base64)
38
+ router.post('/upload-image-base64', async (req, res) => {
39
+ try {
40
+ const { image, filename, folder = '' } = req.body;
41
+
42
+ if (!image || !filename) {
43
+ return res.status(400).json({ error: 'Image data and filename are required' });
44
+ }
45
+
46
+ // Remove data URL prefix if present
47
+ const base64Data = image.replace(/^data:image\/\w+;base64,/, '');
48
+ const buffer = Buffer.from(base64Data, 'base64');
49
+
50
+ const { storageClient } = req.notesEngine;
51
+ const imageUrl = await storageClient.uploadImage(buffer, filename, folder);
52
+
53
+ res.json({ imageUrl });
54
+ } catch (error) {
55
+ console.error('Error uploading image:', error);
56
+ res.status(500).json({ error: 'Failed to upload image' });
57
+ }
58
+ });
59
+
60
+ // Upload video
61
+ router.post('/upload-video', async (req, res) => {
62
+ try {
63
+ if (!req.files || !req.files.video) {
64
+ return res.status(400).json({ error: 'No video file provided' });
65
+ }
66
+
67
+ const videoFile = req.files.video;
68
+ const folder = req.body.folder || '';
69
+
70
+ const { storageClient } = req.notesEngine;
71
+ const videoUrl = await storageClient.uploadVideo(
72
+ videoFile.data,
73
+ videoFile.name,
74
+ folder
75
+ );
76
+
77
+ res.json({ videoUrl });
78
+ } catch (error) {
79
+ console.error('Error uploading video:', error);
80
+ res.status(500).json({ error: 'Failed to upload video' });
81
+ }
82
+ });
83
+
84
+ // Delete image
85
+ router.delete('/image', async (req, res) => {
86
+ try {
87
+ const { imageUrl } = req.body;
88
+
89
+ if (!imageUrl) {
90
+ return res.status(400).json({ error: 'Image URL is required' });
91
+ }
92
+
93
+ const { storageClient } = req.notesEngine;
94
+ await storageClient.deleteFile(imageUrl);
95
+
96
+ res.json({ success: true });
97
+ } catch (error) {
98
+ console.error('Error deleting image:', error);
99
+ res.status(500).json({ error: 'Failed to delete image' });
100
+ }
101
+ });
102
+
103
+ // Delete video
104
+ router.delete('/video', async (req, res) => {
105
+ try {
106
+ const { videoUrl } = req.body;
107
+
108
+ if (!videoUrl) {
109
+ return res.status(400).json({ error: 'Video URL is required' });
110
+ }
111
+
112
+ const { storageClient } = req.notesEngine;
113
+ await storageClient.deleteFile(videoUrl);
114
+
115
+ res.json({ success: true });
116
+ } catch (error) {
117
+ console.error('Error deleting video:', error);
118
+ res.status(500).json({ error: 'Failed to delete video' });
119
+ }
120
+ });
121
+
122
+ export default router;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Storage Client
3
+ * Handles file uploads to R2 or S3
4
+ */
5
+
6
+ import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
7
+
8
+ export class StorageClient {
9
+ constructor(config) {
10
+ this.config = config;
11
+ this.publicUrl = config.publicUrl;
12
+
13
+ const s3Config = {
14
+ region: config.type === 'r2' ? 'auto' : (config.region || 'us-east-1'),
15
+ credentials: {
16
+ accessKeyId: config.accessKeyId,
17
+ secretAccessKey: config.secretAccessKey
18
+ }
19
+ };
20
+
21
+ // R2-specific endpoint
22
+ if (config.type === 'r2') {
23
+ s3Config.endpoint = `https://${config.accountId}.r2.cloudflarestorage.com`;
24
+ }
25
+
26
+ this.s3Client = new S3Client(s3Config);
27
+ this.bucketName = config.bucketName;
28
+ }
29
+
30
+ /**
31
+ * Upload an image file
32
+ * @param {Buffer} fileBuffer - File buffer
33
+ * @param {string} filename - Original filename
34
+ * @param {string} [folder=''] - Optional folder path
35
+ * @returns {Promise<string>} Public URL of uploaded file
36
+ */
37
+ async uploadImage(fileBuffer, filename, folder = '') {
38
+ const timestamp = Date.now();
39
+ const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
40
+ const key = folder
41
+ ? `images/${folder}/${timestamp}-${sanitizedFilename}`
42
+ : `images/${timestamp}-${sanitizedFilename}`;
43
+
44
+ await this.s3Client.send(new PutObjectCommand({
45
+ Bucket: this.bucketName,
46
+ Key: key,
47
+ Body: fileBuffer,
48
+ ContentType: this._getContentType(filename)
49
+ }));
50
+
51
+ return `${this.publicUrl}/${key}`;
52
+ }
53
+
54
+ /**
55
+ * Upload a video file
56
+ * @param {Buffer} fileBuffer - File buffer
57
+ * @param {string} filename - Original filename
58
+ * @param {string} [folder=''] - Optional folder path
59
+ * @returns {Promise<string>} Public URL of uploaded file
60
+ */
61
+ async uploadVideo(fileBuffer, filename, folder = '') {
62
+ const timestamp = Date.now();
63
+ const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
64
+ const key = folder
65
+ ? `videos/${folder}/${timestamp}-${sanitizedFilename}`
66
+ : `videos/${timestamp}-${sanitizedFilename}`;
67
+
68
+ await this.s3Client.send(new PutObjectCommand({
69
+ Bucket: this.bucketName,
70
+ Key: key,
71
+ Body: fileBuffer,
72
+ ContentType: this._getContentType(filename)
73
+ }));
74
+
75
+ return `${this.publicUrl}/${key}`;
76
+ }
77
+
78
+ /**
79
+ * Delete a file
80
+ * @param {string} fileUrl - Public URL of the file
81
+ * @returns {Promise<void>}
82
+ */
83
+ async deleteFile(fileUrl) {
84
+ // Extract key from URL
85
+ const key = fileUrl.replace(this.publicUrl + '/', '');
86
+
87
+ await this.s3Client.send(new DeleteObjectCommand({
88
+ Bucket: this.bucketName,
89
+ Key: key
90
+ }));
91
+ }
92
+
93
+ /**
94
+ * Get content type from filename
95
+ * @private
96
+ */
97
+ _getContentType(filename) {
98
+ const ext = filename.split('.').pop().toLowerCase();
99
+
100
+ const contentTypes = {
101
+ // Images
102
+ jpg: 'image/jpeg',
103
+ jpeg: 'image/jpeg',
104
+ png: 'image/png',
105
+ gif: 'image/gif',
106
+ webp: 'image/webp',
107
+ svg: 'image/svg+xml',
108
+
109
+ // Videos
110
+ mp4: 'video/mp4',
111
+ webm: 'video/webm',
112
+ mov: 'video/quicktime',
113
+ avi: 'video/x-msvideo',
114
+ mkv: 'video/x-matroska'
115
+ };
116
+
117
+ return contentTypes[ext] || 'application/octet-stream';
118
+ }
119
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Markdown Notes Engine - Frontend Module
3
+ *
4
+ * A markdown note-taking editor with GitHub integration
5
+ */
6
+
7
+ // Note: This frontend module is browser-based and already supports both module systems.
8
+ // For ES modules in browser, you would import this, but the class itself works the same way.
9
+ // The actual implementation is in index.js and works in both environments.
10
+
11
+ // Re-export the NotesEditor class for ES module users
12
+ import { NotesEditor } from './index.js';
13
+
14
+ export { NotesEditor };
15
+ export default NotesEditor;
package/lib/index.mjs ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Markdown Notes Engine
3
+ *
4
+ * A complete markdown note-taking solution with GitHub integration
5
+ * and media hosting (R2/S3).
6
+ *
7
+ * @module markdown-notes-engine
8
+ */
9
+
10
+ // Backend exports
11
+ export { createNotesRouter } from './backend/index.mjs';
12
+ export { GitHubClient } from './backend/github.mjs';
13
+ export { StorageClient } from './backend/storage.mjs';
14
+ export { MarkdownRenderer } from './backend/markdown.mjs';
15
+
16
+ // Frontend exports
17
+ export { NotesEditor } from './frontend/index.mjs';
package/package.json CHANGED
@@ -1,8 +1,35 @@
1
1
  {
2
2
  "name": "markdown-notes-engine",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A complete markdown note-taking engine with GitHub integration and media hosting (R2/S3)",
5
5
  "main": "lib/index.js",
6
+ "module": "lib/index.mjs",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./lib/index.mjs",
10
+ "require": "./lib/index.js"
11
+ },
12
+ "./backend": {
13
+ "import": "./lib/backend/index.mjs",
14
+ "require": "./lib/backend/index.js"
15
+ },
16
+ "./backend/github": {
17
+ "import": "./lib/backend/github.mjs",
18
+ "require": "./lib/backend/github.js"
19
+ },
20
+ "./backend/storage": {
21
+ "import": "./lib/backend/storage.mjs",
22
+ "require": "./lib/backend/storage.js"
23
+ },
24
+ "./backend/markdown": {
25
+ "import": "./lib/backend/markdown.mjs",
26
+ "require": "./lib/backend/markdown.js"
27
+ },
28
+ "./frontend": {
29
+ "import": "./lib/frontend/index.mjs",
30
+ "require": "./lib/frontend/index.js"
31
+ }
32
+ },
6
33
  "scripts": {
7
34
  "start": "node server.js",
8
35
  "dev": "nodemon server.js",
@@ -25,7 +52,8 @@
25
52
  "url": "https://github.com/cdthomp1/markdown-notes-engine"
26
53
  },
27
54
  "files": [
28
- "lib/**/*",
55
+ "lib/**/*.js",
56
+ "lib/**/*.mjs",
29
57
  "README.md",
30
58
  "LICENSE"
31
59
  ],