// To Do page — renders the bulleted list in webpage/data/todo.md.
//
// The whole file is wrapped in an IIFE so its helpers stay out of the shared
// global scope that every text/babel script compiles into (see the "no
// top-level const" gotcha in docs/design_style_guide.md).
(function () {
'use strict';
const { Box } = MaterialUI;
// IIFE scope, so the module-level helpers below can reach the design system
// too (the component re-reads it for readability at its own top).
const DS = window.DesignSystem;
const TODO_URL = '/data/todo.md';
// `- item`, `* item`, `+ item`, `1. item`, `1) item`
const LIST_RE = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/;
// Optional task-list checkbox at the start of an item: `- [ ] item`
const CHECKBOX_RE = /^\[([ xX])\]\s+(.*)$/;
const HEADING_RE = /^(#{1,6})\s+(.*)$/;
// --- markdown -> section/item tree ---------------------------------------
// Only what a to-do list needs: headings become sections, list items nest by
// indentation, and anything else becomes a note line under the section.
const parseTodoMarkdown = (text) => {
const lines = text.replace(/\r\n?/g, '\n').split('\n');
let docTitle = null;
const sections = [];
let section = null;
// Stack of { indent, items } — `items` is the list the next item at that
// depth gets pushed onto.
let stack = [];
const ensureSection = () => {
if (!section) {
section = { title: null, items: [], notes: [] };
sections.push(section);
}
return section;
};
lines.forEach((rawLine) => {
const line = rawLine.replace(/\s+$/, '');
if (line.trim() === '') {
return;
}
const heading = line.match(HEADING_RE);
if (heading) {
const level = heading[1].length;
const title = heading[2].trim();
if (level === 1 && docTitle === null && sections.length === 0) {
docTitle = title;
} else {
section = { title: title, items: [], notes: [] };
sections.push(section);
}
stack = [];
return;
}
const listItem = line.match(LIST_RE);
if (listItem) {
const indent = listItem[1].replace(/\t/g, ' ').length;
const marker = listItem[2];
const ordered = /\d/.test(marker);
let content = listItem[3].trim();
let done = null;
const checkbox = content.match(CHECKBOX_RE);
if (checkbox) {
done = checkbox[1].toLowerCase() === 'x';
content = checkbox[2].trim();
}
const item = {
text: content,
ordered: ordered,
number: ordered ? parseInt(marker, 10) : null,
done: done,
children: []
};
const target = ensureSection();
// Pop back to the level this item belongs at, then either nest under
// the last item (deeper indent) or sit alongside it.
while (stack.length > 0 && indent < stack[stack.length - 1].indent) {
stack.pop();
}
if (stack.length === 0) {
stack = [{ indent: indent, items: target.items }];
} else if (indent > stack[stack.length - 1].indent) {
const parentItems = stack[stack.length - 1].items;
const parent = parentItems[parentItems.length - 1];
if (parent) {
stack.push({ indent: indent, items: parent.children });
}
}
stack[stack.length - 1].items.push(item);
return;
}
// A non-list, indented line continues the previous item's text;
// otherwise it's a standalone note under the current section.
const target = ensureSection();
const currentItems = stack.length > 0 ? stack[stack.length - 1].items : null;
const last = currentItems && currentItems[currentItems.length - 1];
if (last && /^\s/.test(rawLine)) {
last.text += ' ' + line.trim();
} else {
target.notes.push(line.trim());
stack = [];
}
});
return { title: docTitle, sections: sections };
};
// --- inline formatting ----------------------------------------------------
// `code`, **bold**, *italic*, [text](href) — rendered as React nodes so no
// markdown source leaks into the page and no HTML is injected.
const renderInline = (text, T, keyPrefix) => {
const pattern = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*]+\*)|(\[[^\]]+\]\([^)\s]+\))/g;
const nodes = [];
let lastIndex = 0;
let match;
let i = 0;
while ((match = pattern.exec(text)) !== null) {
if (match.index > lastIndex) {
nodes.push(text.slice(lastIndex, match.index));
}
const token = match[0];
const key = keyPrefix + '-' + i++;
if (token.startsWith('`')) {
nodes.push(
{token.slice(1, -1)}
);
} else if (token.startsWith('**')) {
nodes.push({token.slice(2, -2)});
} else if (token.startsWith('*')) {
nodes.push({token.slice(1, -1)});
} else {
const split = token.indexOf('](');
nodes.push(
{token.slice(1, split)}
);
}
lastIndex = match.index + token.length;
}
if (lastIndex < text.length) {
nodes.push(text.slice(lastIndex));
}
return nodes;
};
// --- list rendering -------------------------------------------------------
// Depth 0 markers are accent squares (or the item's number); deeper tiers
// step down to a hairline outline square, mirroring the design-system rules.
const Marker = ({ T, item, depth }) => {
if (item.done !== null) {
return (
{item.done ? '✓' : ''}
);
}
if (item.ordered) {
return (
{item.number}.
);
}
return (
);
};
const TodoList = ({ T, items, depth, keyPrefix }) => (
{items.map((item, index) => {
const key = keyPrefix + '-' + index;
return (
{renderInline(item.text, T, key)}
{item.children.length > 0 && (
)}
);
})}
);
const TodoPage = () => {
const {
CircularProgress, Button, Dialog, DialogTitle, DialogContent, DialogActions,
TextField, Snackbar, Alert
} = MaterialUI;
const theme = MaterialUI.useTheme();
const darkMode = theme.palette.mode === 'dark';
const DS = window.DesignSystem;
const T = DS.tokens(darkMode);
const [state, setState] = React.useState({
status: 'loading', doc: null, source: '', error: null
});
// Unlock state mirrors the Plants page: the password itself doubles as the
// "unlocked" flag, lives only in memory, and is re-sent with every write.
const [unlockPassword, setUnlockPassword] = React.useState('');
const [unlockDialogOpen, setUnlockDialogOpen] = React.useState(false);
const [unlockInput, setUnlockInput] = React.useState('');
const [unlockError, setUnlockError] = React.useState(false);
const [snackbar, setSnackbar] = React.useState({ open: false, message: '', severity: 'success' });
const [editing, setEditing] = React.useState(false);
const [draft, setDraft] = React.useState('');
const [saving, setSaving] = React.useState(false);
React.useEffect(() => {
let cancelled = false;
fetch(TODO_URL, { cache: 'no-store' })
.then((response) => {
if (!response.ok) {
throw new Error(`Could not load ${TODO_URL} (HTTP ${response.status})`);
}
return response.text();
})
.then((text) => {
if (!cancelled) {
setState({ status: 'ready', doc: parseTodoMarkdown(text), source: text, error: null });
}
})
.catch((error) => {
if (!cancelled) {
setState({ status: 'error', doc: null, source: '', error: error.message });
}
});
return () => {
cancelled = true;
};
}, []);
const handleUnlock = async () => {
try {
const response = await fetch('/data/todo/unlock', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: unlockInput })
});
const data = await response.json();
if (data.unlocked) {
setUnlockPassword(unlockInput);
setUnlockDialogOpen(false);
setUnlockInput('');
setUnlockError(false);
setSnackbar({ open: true, message: 'To Do editing unlocked', severity: 'success' });
} else {
setUnlockError(true);
}
} catch (error) {
setSnackbar({ open: true, message: `Unlock failed: ${error.message}`, severity: 'error' });
}
};
const handleSave = async () => {
setSaving(true);
try {
const response = await fetch('/data/todo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: draft, password: unlockPassword })
});
if (!response.ok) {
const message = await response.text();
if (response.status === 403) {
setUnlockPassword('');
setEditing(false);
}
setSnackbar({ open: true, message: message || `Save failed (HTTP ${response.status})`, severity: 'error' });
return;
}
setState({ status: 'ready', doc: parseTodoMarkdown(draft), source: draft, error: null });
setEditing(false);
setSnackbar({ open: true, message: 'To Do saved', severity: 'success' });
} catch (error) {
setSnackbar({ open: true, message: `Save failed: ${error.message}`, severity: 'error' });
} finally {
setSaving(false);
}
};
const doc = state.doc;
const modBtnSx = DS.secondaryBtnSx(T);
return (
{state.status === 'loading' && (
)}
{!editing && state.status === 'error' && (
Error
{state.error}
Add a bulleted markdown list at webpage/data/todo.md to populate this page.
)}
{editing && (
Editing todo.md
setDraft(event.target.value)}
sx={{
display: 'block',
width: '100%',
marginTop: '10px',
minHeight: '480px',
padding: '12px',
resize: 'vertical',
boxSizing: 'border-box',
fontFamily: DS.MONO,
fontSize: '13px',
lineHeight: 1.6,
color: T.text,
backgroundColor: T.surface,
border: DS.hairline(T),
borderRadius: 0,
outline: 'none',
'&:focus': { borderColor: T.accent }
}}
/>
)}
{!editing && state.status === 'ready' && doc.sections.length === 0 && (
todo.md is empty — nothing to do.
)}
{!editing && state.status === 'ready' && doc.sections.length > 0 && (
{doc.sections.map((section, index) => (
{section.title && (
{section.title}
)}
{section.notes.map((note, noteIndex) => (
{renderInline(note, T, 'note-' + index + '-' + noteIndex)}
))}
{section.items.length > 0 && (
)}
))}
)}
{/* Footer: editing controls — sits on the ground with a 2px top rule */}
{state.status !== 'loading' && (
{unlockPassword ? 'List editing · unlocked' : 'List editing · locked'}
{!unlockPassword && (
)}
{unlockPassword && !editing && (
)}
{unlockPassword && editing && (
)}
)}
{/* Unlock dialog */}
{/* Snackbar for success/error feedback */}
setSnackbar({ ...snackbar, open: false })}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
sx={{ width: '100%' }}
>
{snackbar.message}
);
};
window.TodoPage = TodoPage;
})();