diff --git a/README.md b/README.md
index 8c33fae..b3a3e0e 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
> Your Self-Hosted Knowledge Base
+๐ **[Visit the official website](https://www.notediscovery.com)**
+
## What is NoteDiscovery?
NoteDiscovery is a **lightweight, self-hosted note-taking application** that puts you in complete control of your knowledge base. Write, organize, and discover your notes with a beautiful, modern interfaceโall running on your own server.
diff --git a/backend/main.py b/backend/main.py
index 4b0e2c1..35a99e4 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -23,7 +23,6 @@ from .utils import (
save_note,
delete_note,
search_notes,
- parse_wiki_links,
create_note_metadata,
ensure_directories,
create_folder,
@@ -336,8 +335,8 @@ async def api_documentation():
{
"method": "GET",
"path": "/api/graph",
- "description": "Get graph data for note visualization (wiki links)",
- "response": "{ nodes: [{ id, label }], edges: [{ from, to }] }"
+ "description": "Get graph data for note visualization",
+ "response": "{ nodes: [{ id, label }], edges: [] }"
},
{
"method": "GET",
@@ -651,13 +650,9 @@ async def get_note(note_path: str):
if transformed_content is not None:
content = transformed_content
- # Parse wiki links
- links = parse_wiki_links(content)
-
return {
"path": note_path,
"content": content,
- "links": links,
"metadata": create_note_metadata(config['storage']['notes_dir'], note_path)
}
except HTTPException:
@@ -745,28 +740,22 @@ async def search(q: str):
@api_router.get("/graph")
async def get_graph():
- """Get graph data for visualization"""
+ """Get graph data for note visualization (currently only returns nodes, link detection not implemented)"""
try:
notes = get_all_notes(config['storage']['notes_dir'])
nodes = []
edges = []
- # Build graph structure
+ # Build graph structure - only nodes for now
for note in notes:
- nodes.append({
- "id": note['path'],
- "label": note['name']
- })
-
- # Get links from this note
- content = get_note_content(config['storage']['notes_dir'], note['path'])
- if content:
- links = parse_wiki_links(content)
- for link in links:
- edges.append({
- "from": note['path'],
- "to": link
- })
+ if note.get('type') == 'note': # Only include actual notes
+ nodes.append({
+ "id": note['path'],
+ "label": note['name']
+ })
+
+ # Note: Link detection between notes could be implemented here using markdown link parsing
+ # For now, returning empty edges
return {"nodes": nodes, "edges": edges}
except Exception as e:
diff --git a/backend/utils.py b/backend/utils.py
index e953e93..365cf53 100644
--- a/backend/utils.py
+++ b/backend/utils.py
@@ -236,13 +236,6 @@ def delete_note(notes_dir: str, note_path: str) -> bool:
return True
-def parse_wiki_links(content: str) -> List[str]:
- """Extract wiki-style links [[link]] from markdown content"""
- pattern = r'\[\[([^\]]+)\]\]'
- matches = re.findall(pattern, content)
- return matches
-
-
def search_notes(notes_dir: str, query: str) -> List[Dict]:
"""Simple full-text search through all notes"""
results = []
diff --git a/docs/index.html b/docs/index.html
index 2527a08..e9f3244 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -641,8 +641,8 @@
๐
- Wiki Links
- Connect notes with internal links
+ Internal Links
+ Connect notes with markdown links
@@ -725,7 +725,7 @@
"url": "https://www.notediscovery.com",
"applicationCategory": "ProductivityApplication",
"operatingSystem": "Linux, Windows, macOS",
- "featureList": "Markdown editor, LaTeX math equations, Mermaid diagrams, Code syntax highlighting, Dark mode, Plugin system, Internal wiki links, HTML export, Full-text search, Self-hosted, Obsidian, Evernote, Notion, Onenote, Second brain",
+ "featureList": "Markdown editor, LaTeX math equations, Mermaid diagrams, Code syntax highlighting, Dark mode, Plugin system, Internal links, HTML export, Full-text search, Self-hosted, Obsidian, Evernote, Notion, Onenote, Second brain",
"offers": {
"@type": "Offer",
"price": "0",
diff --git a/documentation/FEATURES.md b/documentation/FEATURES.md
index 994fe06..be629fa 100644
--- a/documentation/FEATURES.md
+++ b/documentation/FEATURES.md
@@ -28,7 +28,7 @@
## ๐ Linking & Discovery
### Internal Links
-- **Wiki-style links** - `[[Note Name]]` syntax
+- **Markdown links** - `[Note Name](note.md)` syntax
- **Drag to link** - Drag notes or images into the editor to insert links
- **Click to navigate** - Jump between notes seamlessly
- **External links** - Open in new tabs automatically
@@ -139,6 +139,8 @@ graph TD
## โก Keyboard Shortcuts
+### General
+
| Windows/Linux | Mac | Action |
|---------------|-----|--------|
| `Ctrl+S` | `Cmd+S` | Save note |
@@ -149,6 +151,14 @@ graph TD
| `F3` | `F3` | Next search match |
| `Shift+F3` | `Shift+F3` | Previous search match |
+### Markdown Formatting
+
+| Windows/Linux | Mac | Action | Result |
+|---------------|-----|--------|--------|
+| `Ctrl+B` | `Cmd+B` | Bold | `**text**` |
+| `Ctrl+I` | `Cmd+I` | Italic | `*text*` |
+| `Ctrl+K` | `Cmd+K` | Insert link | `[text](url)` |
+
## ๐ Performance
- **Instant loading** - No lag, no loading spinners
diff --git a/documentation/PLUGIN_NOTE_STATISTICS.md b/documentation/PLUGIN_NOTE_STATISTICS.md
index 1633012..2494dc5 100644
--- a/documentation/PLUGIN_NOTE_STATISTICS.md
+++ b/documentation/PLUGIN_NOTE_STATISTICS.md
@@ -55,7 +55,7 @@ curl -X POST http://localhost:8000/api/plugins/note_stats/toggle \
### ๐ Links & References
- **Total Links** - All `[text](url)` links
-- **Internal Links** - `[[WikiLinks]]` to other notes
+- **Internal Links** - Links to other notes (`.md` files)
- **External Links** - HTTP/HTTPS URLs
- **Images** - `` count
@@ -191,7 +191,7 @@ Find orphaned notes or track reference density.
**Words:** Split on whitespace, excluding code blocks
**Reading Time:** `words / 200 minutes`
-**Links:** Regex match `[text](url)` and `[[WikiLink]]`
+**Links:** Regex match `[text](url)` markdown links
**Tasks:** Match `- [ ]` and `- [x]`
**Code Blocks:** Match ` ```language ` ` fences
**Headings:** Match `#`, `##`, `###` at line start
diff --git a/frontend/app.js b/frontend/app.js
index 7041c09..56d0a22 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -94,89 +94,112 @@ function noteApp() {
// Dropdown state
showNewDropdown: false,
- // Homepage folder navigation
+ // Homepage state
selectedHomepageFolder: '',
+ _homepageCache: {
+ folderPath: null,
+ notes: null,
+ folders: null,
+ breadcrumb: null
+ },
- // Computed-like helpers for homepage (use methods for safer evaluation)
+ // Homepage constants
+ HOMEPAGE_MAX_NOTES: 50,
+
+ // Computed-like helpers for homepage (cached for performance)
homepageNotes() {
+ // Return cached result if folder hasn't changed
+ if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.notes) {
+ return this._homepageCache.notes;
+ }
+
if (!this.folderTree || typeof this.folderTree !== 'object') {
return [];
}
const folderNode = this.getFolderNode(this.selectedHomepageFolder || '');
- if (!folderNode || !Array.isArray(folderNode.notes)) {
- return [];
- }
+ const result = (folderNode && Array.isArray(folderNode.notes)) ? folderNode.notes : [];
- return folderNode.notes;
- },
-
- // Helper: Format file size nicely
- formatSize(bytes) {
- if (bytes === 0 || !bytes) return '0 B';
-
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
-
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+ // Cache the result
+ this._homepageCache.notes = result;
+ this._homepageCache.folderPath = this.selectedHomepageFolder;
+
+ return result;
},
homepageFolders() {
+ // Return cached result if folder hasn't changed
+ if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.folders) {
+ return this._homepageCache.folders;
+ }
+
if (!this.folderTree || typeof this.folderTree !== 'object') {
return [];
}
+ // Get child folders
let childFolders = [];
if (!this.selectedHomepageFolder) {
+ // Root level: all top-level folders
childFolders = Object.entries(this.folderTree)
.filter(([key]) => key !== '__root__')
.map(([, folder]) => folder);
} else {
+ // Inside a folder: get its children
const parentFolder = this.getFolderNode(this.selectedHomepageFolder);
- if (!parentFolder || !parentFolder.children) {
- return [];
+ if (parentFolder && parentFolder.children) {
+ childFolders = Object.values(parentFolder.children);
}
- childFolders = Object.values(parentFolder.children);
}
- if (!Array.isArray(childFolders) || childFolders.length === 0) {
- return [];
- }
-
- return childFolders
+ // Map to simplified structure (note count already cached in folder node)
+ const result = childFolders
.map(folder => ({
name: folder.name,
path: folder.path,
- noteCount: this.calculateFolderNoteCount(folder)
+ noteCount: folder.noteCount || 0 // Use pre-calculated count
}))
.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+
+ // Cache the result
+ this._homepageCache.folders = result;
+ this._homepageCache.folderPath = this.selectedHomepageFolder;
+
+ return result;
},
homepageBreadcrumb() {
- // Always return at least Home breadcrumb
- // Safety check - ensure we always return an array
- try {
- const breadcrumb = [{ name: 'Home', path: '' }];
-
- if (!this.selectedHomepageFolder) {
- return breadcrumb;
- }
-
- const parts = (this.selectedHomepageFolder || '').split('/').filter(part => part.trim() !== '');
-
+ // Return cached result if folder hasn't changed
+ if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.breadcrumb) {
+ return this._homepageCache.breadcrumb;
+ }
+
+ const breadcrumb = [{ name: 'Home', path: '' }];
+
+ if (this.selectedHomepageFolder) {
+ const parts = this.selectedHomepageFolder.split('/').filter(Boolean);
let currentPath = '';
+
parts.forEach(part => {
- currentPath = currentPath ? currentPath + '/' + part : part;
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
breadcrumb.push({ name: part, path: currentPath });
});
-
- return breadcrumb;
- } catch (error) {
- // Fallback to just Home if anything goes wrong
- return [{ name: 'Home', path: '' }];
}
+
+ // Cache the result
+ this._homepageCache.breadcrumb = breadcrumb;
+ this._homepageCache.folderPath = this.selectedHomepageFolder;
+
+ return breadcrumb;
+ },
+
+ // Helper: Format file size nicely
+ formatSize(bytes) {
+ if (!bytes) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
},
getFolderNode(folderPath = '') {
@@ -185,10 +208,7 @@ function noteApp() {
}
if (!folderPath) {
- if (this.folderTree['__root__']) {
- return this.folderTree['__root__'];
- }
- return { name: '', path: '', children: {}, notes: [] };
+ return this.folderTree['__root__'] || { name: '', path: '', children: {}, notes: [], noteCount: 0 };
}
const parts = folderPath.split('/').filter(Boolean);
@@ -206,22 +226,6 @@ function noteApp() {
return node;
},
- calculateFolderNoteCount(folderNode) {
- if (!folderNode || typeof folderNode !== 'object') {
- return 0;
- }
-
- const directNotes = Array.isArray(folderNode.notes) ? folderNode.notes.length : 0;
- if (!folderNode.children || Object.keys(folderNode.children).length === 0) {
- return directNotes;
- }
-
- return Object.values(folderNode.children).reduce(
- (total, child) => total + this.calculateFolderNoteCount(child),
- directNotes
- );
- },
-
// Check if app is empty (no notes and no folders)
get isAppEmpty() {
const notesArray = Array.isArray(this.notes) ? this.notes : [];
@@ -291,6 +295,14 @@ function noteApp() {
this.selectedHomepageFolder = '';
}
+ // Invalidate cache to force recalculation
+ this._homepageCache = {
+ folderPath: null,
+ notes: null,
+ folders: null,
+ breadcrumb: null
+ };
+
// Clear search
this.searchQuery = '';
this.searchResults = [];
@@ -373,6 +385,28 @@ function noteApp() {
e.preventDefault();
this.previousMatch();
}
+
+ // Only apply markdown shortcuts when editor is focused and a note is open
+ const isEditorFocused = document.activeElement?.id === 'note-editor';
+ if (isEditorFocused && this.currentNote) {
+ // Ctrl/Cmd + B for bold
+ if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
+ e.preventDefault();
+ this.wrapSelection('**', '**', 'bold text');
+ }
+
+ // Ctrl/Cmd + I for italic
+ if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
+ e.preventDefault();
+ this.wrapSelection('*', '*', 'italic text');
+ }
+
+ // Ctrl/Cmd + K for link
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ this.insertLink();
+ }
+ }
});
}
@@ -590,8 +624,41 @@ function noteApp() {
}
});
- // Force Alpine reactivity by creating a new object reference
- this.folderTree = { ...tree };
+ // Calculate and cache note counts recursively (for performance)
+ const calculateNoteCounts = (folderNode) => {
+ const directNotes = folderNode.notes ? folderNode.notes.length : 0;
+
+ if (!folderNode.children || Object.keys(folderNode.children).length === 0) {
+ folderNode.noteCount = directNotes;
+ return directNotes;
+ }
+
+ const childNotesCount = Object.values(folderNode.children).reduce(
+ (total, child) => total + calculateNoteCounts(child),
+ 0
+ );
+
+ folderNode.noteCount = directNotes + childNotesCount;
+ return folderNode.noteCount;
+ };
+
+ // Calculate note counts for all folders
+ Object.values(tree).forEach(folder => {
+ if (folder.path !== undefined || folder === tree['__root__']) {
+ calculateNoteCounts(folder);
+ }
+ });
+
+ // Invalidate homepage cache when tree is rebuilt
+ this._homepageCache = {
+ folderPath: null,
+ notes: null,
+ folders: null,
+ breadcrumb: null
+ };
+
+ // Assign new tree (Alpine will detect the change)
+ this.folderTree = tree;
},
// Render folder recursively (helper for deep nesting)
@@ -756,8 +823,6 @@ function noteApp() {
}
// Force Alpine reactivity by creating new Set reference
this.expandedFolders = new Set(this.expandedFolders);
- // Also trigger folderTree reactivity to re-render x-html
- this.folderTree = { ...this.folderTree };
},
// Check if folder is expanded
@@ -767,25 +832,22 @@ function noteApp() {
// Expand all folders
expandAllFolders() {
- // Add all folder paths to expandedFolders
this.allFolders.forEach(folder => {
this.expandedFolders.add(folder);
});
- // Force Alpine reactivity by creating new object reference (no rebuild needed)
- this.folderTree = { ...this.folderTree };
+ // Force Alpine reactivity
+ this.expandedFolders = new Set(this.expandedFolders);
},
// Collapse all folders
collapseAllFolders() {
this.expandedFolders.clear();
- // Force Alpine reactivity by creating new object reference (no rebuild needed)
- this.folderTree = { ...this.folderTree };
+ // Force Alpine reactivity
+ this.expandedFolders = new Set(this.expandedFolders);
},
// Expand folder tree to show a specific note
expandFolderForNote(notePath) {
- // Extract folder path from note path
- // e.g., "folder1/folder2/note.md" -> ["folder1", "folder1/folder2"]
const parts = notePath.split('/');
// If note is in root, no folders to expand
@@ -801,13 +863,8 @@ function noteApp() {
this.expandedFolders.add(currentPath);
});
- // Force Alpine reactivity by creating new object reference
- // This ensures the UI updates to show the expanded folders
- this.folderTree = { ...this.folderTree };
-
- // Also force a re-evaluation by modifying the Set (create new Set)
- const oldFolders = this.expandedFolders;
- this.expandedFolders = new Set(oldFolders);
+ // Force Alpine reactivity
+ this.expandedFolders = new Set(this.expandedFolders);
},
// Scroll note into view in the sidebar navigation
@@ -1932,6 +1989,68 @@ function noteApp() {
});
},
+ // Markdown formatting helpers
+ wrapSelection(before, after, placeholder) {
+ const editor = document.getElementById('note-editor');
+ if (!editor) return;
+
+ const start = editor.selectionStart;
+ const end = editor.selectionEnd;
+ const selectedText = this.noteContent.substring(start, end);
+ const textToWrap = selectedText || placeholder;
+
+ // Build the new text
+ const newText = before + textToWrap + after;
+
+ // Update content
+ this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end);
+
+ // Set cursor position (select the wrapped text or placeholder)
+ this.$nextTick(() => {
+ if (selectedText) {
+ // If text was selected, keep it selected (inside the wrapper)
+ editor.setSelectionRange(start + before.length, start + before.length + selectedText.length);
+ } else {
+ // If no text selected, select the placeholder
+ editor.setSelectionRange(start + before.length, start + before.length + placeholder.length);
+ }
+ editor.focus();
+ });
+
+ // Trigger autosave
+ this.autoSave();
+ },
+
+ insertLink() {
+ const editor = document.getElementById('note-editor');
+ if (!editor) return;
+
+ const start = editor.selectionStart;
+ const end = editor.selectionEnd;
+ const selectedText = this.noteContent.substring(start, end);
+
+ // If text is selected, use it as link text; otherwise use placeholder
+ const linkText = selectedText || 'link text';
+ const linkUrl = 'url';
+
+ // Build the markdown link
+ const newText = `[${linkText}](${linkUrl})`;
+
+ // Update content
+ this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end);
+
+ // Set cursor position to select the URL part for easy editing
+ this.$nextTick(() => {
+ const urlStart = start + linkText.length + 3; // After "[linkText]("
+ const urlEnd = urlStart + linkUrl.length;
+ editor.setSelectionRange(urlStart, urlEnd);
+ editor.focus();
+ });
+
+ // Trigger autosave
+ this.autoSave();
+ },
+
// Save current note
async saveNote() {
if (!this.currentNote) return;
@@ -2202,13 +2321,8 @@ function noteApp() {
}
});
- // Convert wiki-style links [[link]] to HTML links
- let content = this.noteContent.replace(/\[\[([^\]]+)\]\]/g, (match, linkText) => {
- return `[[${linkText}]]`;
- });
-
// Parse markdown
- let html = marked.parse(content);
+ let html = marked.parse(this.noteContent);
// Post-process: Add target="_blank" to external links and title attributes to images
// Parse as DOM to safely manipulate
@@ -2955,12 +3069,37 @@ function noteApp() {
// Homepage folder navigation methods
goToHomepageFolder(folderPath) {
- // Navigate to folder (empty string = root)
this.selectedHomepageFolder = folderPath || '';
- // Update browser history
- const state = { homepageFolder: folderPath || '' };
- window.history.pushState(state, '', '/');
+ // Invalidate cache to force recalculation
+ this._homepageCache = {
+ folderPath: null,
+ notes: null,
+ folders: null,
+ breadcrumb: null
+ };
+
+ window.history.pushState({ homepageFolder: folderPath || '' }, '', '/');
+ },
+
+ // Navigate to homepage root and clear all editor state
+ goHome() {
+ this.selectedHomepageFolder = '';
+ this.currentNote = '';
+ this.currentNoteName = '';
+ this.noteContent = '';
+ this.currentImage = '';
+ this.mobileSidebarOpen = false;
+
+ // Invalidate cache to force recalculation
+ this._homepageCache = {
+ folderPath: null,
+ notes: null,
+ folders: null,
+ breadcrumb: null
+ };
+
+ window.history.pushState({ homepageFolder: '' }, '', '/');
}
}
}
diff --git a/frontend/index.html b/frontend/index.html
index 224c607..0db64b9 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -730,7 +730,7 @@
@@ -997,6 +997,17 @@
notes
ยท
+ ยท
+
+ Website
+
@@ -1019,21 +1030,21 @@
-
+
-
- Create Your First Note
-
-
+ style="background-color: var(--accent-primary);"
+ onmouseover="this.style.backgroundColor='var(--accent-hover)'"
+ onmouseout="this.style.backgroundColor='var(--accent-primary)'"
+ >
+ Create Your First Note
+
+
@@ -1061,8 +1072,8 @@
:style="crumb.path === selectedHomepageFolder ? 'color: var(--accent-primary); font-weight: 600;' : 'color: var(--text-secondary);'"
x-text="crumb.name"
>
-
-
+
+
@@ -1142,7 +1153,7 @@
-
+
-
+
- Showing 50 of notes.
-
- View all in sidebar
-
+ Showing of notes in this folder.
@@ -1301,6 +1309,7 @@
:style="viewMode === 'split' ? `width: ${editorWidth}%;` : 'width: 100%;'"
>