Add optional rich text body field to items

- Add body field to data model (stored as markdown in file)
- Parser recognizes `> ` prefixed lines as body content
- Serializer outputs body as indented blockquote lines
- Add lightweight WYSIWYG editor (contenteditable, no external deps)
- Toolbar: bold, italic, link, H1/H2/H3, lists, code block, table, image
- Clipboard export: HTML (for Docs/Word), Markdown, plain text
- Editor shows collapsed when empty, expands on click
- Include body in text search filter
- Dark mode support for editor styles
- Update README with feature and file format example
This commit is contained in:
Ilia Sharin
2026-08-20 10:10:11 -04:00
parent 062d9ca736
commit 290905e3d9
6 changed files with 1402 additions and 14 deletions
+31 -3
View File
@@ -70,9 +70,30 @@ const Parser = {
const uid = () => 'i-' + (++idSeq).toString(36);
const entries = [];
const stack = [{ children: entries, depth: -1, level: 0 }];
const stack = [{ children: entries, depth: -1, level: 0, item: null }];
const lines = entriesText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for body line (starts with whitespace + "> ")
const bodyMatch = line.match(/^(\s*)>\s?(.*)$/);
if (bodyMatch) {
// Body line - attach to current item at matching depth
const bodyDepth = bodyMatch[1].length / 2;
const bodyLine = bodyMatch[2];
// Find the item at this depth
for (let j = stack.length - 1; j >= 0; j--) {
if (stack[j].depth === bodyDepth && stack[j].item) {
if (!stack[j].item.body) stack[j].item.body = '';
else stack[j].item.body += '\n';
stack[j].item.body += bodyLine;
break;
}
}
continue;
}
for (const line of entriesText.split('\n')) {
const m = line.match(/^(\s*)[-*]\s*\[([ x!>/\-])\]\s*(.*)$/);
if (!m) continue;
@@ -108,9 +129,10 @@ const Parser = {
progress: status === 'done' ? 100 : progress,
collapsed: false,
children: [],
body: null,
};
stack[stack.length - 1].children.push(item);
stack.push({ children: item.children, depth, level: item.level });
stack.push({ children: item.children, depth, level: item.level, item });
}
return { entries, history, meta, checksumOk };
@@ -143,6 +165,12 @@ const Parser = {
const metaStr = meta.length ? ` *(${meta.join(', ')})*` : '';
const prefix = it.priority ? `[${it.priority}] ` : '';
lines.push(`${indent}- [${GLYPH[it.status] || ' '}] ${prefix}${it.title}${metaStr}`);
// Serialize body as indented blockquote lines
if (it.body) {
for (const bodyLine of it.body.split('\n')) {
lines.push(`${indent}> ${bodyLine}`);
}
}
if (it.children?.length) {
lines.push(...this._serializeEntries(it.children, depth + 1).split('\n'));
}