Store YAML frontmatter in notes.

This commit is contained in:
2025-07-06 22:29:13 +02:00
parent d28c7bc83e
commit 9113dc1a69
11 changed files with 946 additions and 23 deletions

View File

@@ -89,6 +89,10 @@
<div class="form-group editor-group">
<div class="editor-toolbar">
<button type="button" class="toolbar-btn" onclick="toggleFrontmatter()" title="Toggle Frontmatter" style="background: #e3f2fd;">
<span style="font-family: monospace;">---</span>
</button>
<span class="toolbar-separator"></span>
<button type="button" class="toolbar-btn toolbar-bold" onclick="insertMarkdown('**', '**')" title="Bold">
<b>B</b>
</button>
@@ -635,6 +639,25 @@
// Global Ace Editor instance
let aceEditor;
// Toggle frontmatter visibility
function toggleFrontmatter() {
if (!aceEditor) return;
const content = aceEditor.getValue();
if (content.trim().startsWith('---')) {
// Frontmatter exists, just move cursor to it
aceEditor.moveCursorTo(0, 0);
aceEditor.focus();
} else {
// Add frontmatter
const newContent = updateContentFrontmatter(content);
aceEditor.setValue(newContent, -1);
aceEditor.moveCursorTo(0, 0);
aceEditor.focus();
}
}
// Markdown toolbar functions for Ace Editor
function insertMarkdown(before, after) {
if (!aceEditor) return;
@@ -666,14 +689,39 @@ function insertMarkdown(before, after) {
syncContentAndUpdatePreview();
}
// Extract title from first line of content
// Extract title from content (frontmatter or first line)
function extractTitleFromContent(content) {
// Check for frontmatter first
if (content.trim().startsWith('---')) {
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (frontmatterMatch) {
const frontmatterContent = frontmatterMatch[1];
const titleMatch = frontmatterContent.match(/title:\s*(.+)/);
if (titleMatch) {
// Remove quotes if present
return titleMatch[1].replace(/^["']|["']$/g, '').trim();
}
}
}
// Otherwise extract from first line
const lines = content.split('\n');
let firstLine = '';
// Find the first non-empty line
for (let line of lines) {
const trimmed = line.trim();
// Skip frontmatter if present
let skipUntil = 0;
if (lines[0].trim() === '---') {
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '---') {
skipUntil = i + 1;
break;
}
}
}
// Find the first non-empty line after frontmatter
for (let i = skipUntil; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed) {
// Remove markdown headers if present
firstLine = trimmed.replace(/^#+\s*/, '');
@@ -685,14 +733,101 @@ function extractTitleFromContent(content) {
return firstLine || 'Untitled Note';
}
// Update or create frontmatter in content
function updateContentFrontmatter(content) {
const settings = {
visibility: document.getElementById('visibility').value.toLowerCase(),
folder: document.getElementById('folder').value || undefined,
tags: document.getElementById('tags').value ?
document.getElementById('tags').value.split(',').map(t => t.trim()).filter(t => t) : undefined,
project: document.getElementById('project_id').selectedOptions[0]?.text.split(' - ')[0] || undefined,
task_id: parseInt(document.getElementById('task_id').value) || undefined,
pinned: false
};
// Remove undefined values
Object.keys(settings).forEach(key => settings[key] === undefined && delete settings[key]);
// Parse existing frontmatter
let body = content;
let existingFrontmatter = {};
if (content.trim().startsWith('---')) {
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
if (match) {
try {
// Simple YAML parsing for our use case
const yamlContent = match[1];
yamlContent.split('\n').forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
existingFrontmatter[key] = value.replace(/^["']|["']$/g, '');
}
});
body = match[2];
} catch (e) {
console.error('Error parsing frontmatter:', e);
}
}
}
// Merge settings with existing frontmatter
const frontmatter = { ...existingFrontmatter, ...settings };
// Add title from content or form field
const titleField = document.getElementById('title').value;
if (titleField && titleField !== 'Untitled Note') {
frontmatter.title = titleField;
} else {
frontmatter.title = extractTitleFromContent(body);
}
// Build new frontmatter
let yamlContent = '---\n';
Object.entries(frontmatter).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
if (Array.isArray(value)) {
yamlContent += `${key}:\n`;
value.forEach(item => {
yamlContent += ` - ${item}\n`;
});
} else if (typeof value === 'string' && (value.includes(':') || value.includes('"'))) {
yamlContent += `${key}: "${value}"\n`;
} else {
yamlContent += `${key}: ${value}\n`;
}
}
});
yamlContent += '---\n\n';
return yamlContent + body;
}
// Sync Ace Editor content with hidden textarea and update preview
function syncContentAndUpdatePreview() {
let frontmatterUpdateTimer;
function syncContentAndUpdatePreview(updateFrontmatter = true) {
if (!aceEditor) return;
const content = aceEditor.getValue();
let content = aceEditor.getValue();
// Only update frontmatter when settings change, not on every keystroke
if (updateFrontmatter) {
clearTimeout(frontmatterUpdateTimer);
frontmatterUpdateTimer = setTimeout(() => {
const newContent = updateContentFrontmatter(aceEditor.getValue());
if (aceEditor.getValue() !== newContent) {
const currentPosition = aceEditor.getCursorPosition();
aceEditor.setValue(newContent, -1);
aceEditor.moveCursorToPosition(currentPosition);
}
}, 2000); // Wait 2 seconds after typing stops
}
document.getElementById('content').value = content;
// Update title from first line
// Update title from content
const title = extractTitleFromContent(content);
document.getElementById('title').value = title;
@@ -703,9 +838,61 @@ function syncContentAndUpdatePreview() {
headerTitle.textContent = title ? (isEdit ? `Edit: ${title}` : title) : (isEdit ? 'Edit Note' : 'Create Note');
}
// Sync settings from frontmatter
syncSettingsFromFrontmatter(content);
updatePreview();
}
// Sync settings UI from frontmatter
function syncSettingsFromFrontmatter(content) {
if (!content.trim().startsWith('---')) return;
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!match) return;
const yamlContent = match[1];
const frontmatter = {};
// Simple YAML parsing
yamlContent.split('\n').forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
let value = line.substring(colonIndex + 1).trim();
// Handle arrays (tags)
if (key === 'tags' && !value) {
// Multi-line array, skip for now
return;
}
value = value.replace(/^["']|["']$/g, '');
frontmatter[key] = value;
}
});
// Update UI elements
if (frontmatter.visibility) {
const visibilitySelect = document.getElementById('visibility');
const capitalizedVisibility = frontmatter.visibility.charAt(0).toUpperCase() + frontmatter.visibility.slice(1);
for (let option of visibilitySelect.options) {
if (option.value === capitalizedVisibility) {
visibilitySelect.value = capitalizedVisibility;
break;
}
}
}
if (frontmatter.folder !== undefined) {
document.getElementById('folder').value = frontmatter.folder;
}
if (frontmatter.tags !== undefined) {
document.getElementById('tags').value = frontmatter.tags;
}
}
// Live preview update
let previewTimer;
function updatePreview() {
@@ -747,7 +934,7 @@ function initializeAceEditor() {
// Set theme (use github theme for light mode)
aceEditor.setTheme("ace/theme/github");
// Set markdown mode
// Set markdown mode (which includes YAML frontmatter highlighting)
aceEditor.session.setMode("ace/mode/markdown");
// Configure editor options
@@ -770,15 +957,43 @@ function initializeAceEditor() {
const initialContent = document.getElementById('content').value;
aceEditor.setValue(initialContent, -1); // -1 moves cursor to start
// If editing and has content, extract title
// If editing and has content, sync from frontmatter
if (initialContent) {
const title = extractTitleFromContent(initialContent);
// If editing existing note without frontmatter, add it
if (!initialContent.trim().startsWith('---')) {
const newContent = updateContentFrontmatter(initialContent);
aceEditor.setValue(newContent, -1);
}
syncSettingsFromFrontmatter(aceEditor.getValue());
const title = extractTitleFromContent(aceEditor.getValue());
document.getElementById('title').value = title;
}
// Listen for changes in Ace Editor
aceEditor.on('change', function() {
syncContentAndUpdatePreview();
syncContentAndUpdatePreview(false); // Don't update frontmatter on every keystroke
});
// If this is a new note, add initial frontmatter
if (!initialContent || initialContent.trim() === '') {
const newContent = updateContentFrontmatter('# New Note\n\nStart writing here...');
aceEditor.setValue(newContent, -1);
syncContentAndUpdatePreview(false);
}
// Listen for changes in settings to update frontmatter
['visibility', 'folder', 'tags', 'project_id', 'task_id'].forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('change', function() {
// Force immediate frontmatter update when settings change
const content = updateContentFrontmatter(aceEditor.getValue());
const currentPosition = aceEditor.getCursorPosition();
aceEditor.setValue(content, -1);
aceEditor.moveCursorToPosition(currentPosition);
syncContentAndUpdatePreview(false);
});
}
});
// Handle form submission - ensure content is synced