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 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
- └── index.js # Main package entry
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
+ }