From 34a32a7e29ec07fc694e701da0bb55db63ffc3fd Mon Sep 17 00:00:00 2001 From: VexCode24 Date: Mon, 25 May 2026 16:40:57 -0400 Subject: [PATCH] Add markdown task descriptions --- index.html | 6 ++++ src/app.js | 18 +++++++++-- src/markdown.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ src/tasks.js | 3 +- styles.css | 54 +++++++++++++++++++++++++++---- 5 files changed, 156 insertions(+), 9 deletions(-) create mode 100644 src/markdown.js diff --git a/index.html b/index.html index 7309515..759ad7f 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,12 @@

TaskForge

autocomplete="off" required /> + diff --git a/src/app.js b/src/app.js index fe0e9c1..77c29cc 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,10 @@ import { load, save } from "./storage.js"; +import { renderMarkdown } from "./markdown.js"; import { createTask, toggleTask, removeTask } from "./tasks.js"; const form = document.getElementById("task-form"); const input = document.getElementById("task-input"); +const description = document.getElementById("task-description"); const list = document.getElementById("task-list"); const emptyState = document.getElementById("empty-state"); @@ -29,9 +31,20 @@ function render() { render(); }); + const content = document.createElement("div"); + content.className = "task-content"; + const label = document.createElement("span"); label.className = "task-title"; label.textContent = task.title; + content.appendChild(label); + + if (task.description) { + const taskDescription = document.createElement("div"); + taskDescription.className = "task-description"; + taskDescription.appendChild(renderMarkdown(task.description)); + content.appendChild(taskDescription); + } const del = document.createElement("button"); del.type = "button"; @@ -44,7 +57,7 @@ function render() { render(); }); - li.append(checkbox, label, del); + li.append(checkbox, content, del); list.appendChild(li); } } @@ -53,9 +66,10 @@ form.addEventListener("submit", (e) => { e.preventDefault(); const title = input.value.trim(); if (!title) return; - tasks = [createTask(title), ...tasks]; + tasks = [createTask(title, description.value), ...tasks]; save(tasks); input.value = ""; + description.value = ""; input.focus(); render(); }); diff --git a/src/markdown.js b/src/markdown.js new file mode 100644 index 0000000..8f955a9 --- /dev/null +++ b/src/markdown.js @@ -0,0 +1,84 @@ +const TOKEN_PATTERN = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\))/g; +const SAFE_LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]); + +export function renderMarkdown(markdown) { + const fragment = document.createDocumentFragment(); + const blocks = String(markdown || "") + .replace(/\r\n?/g, "\n") + .split(/\n{2,}/) + .map((block) => block.trim()) + .filter(Boolean); + + for (const block of blocks) { + const paragraph = document.createElement("p"); + appendInlineMarkdown(paragraph, block.replace(/\n/g, " ")); + fragment.appendChild(paragraph); + } + + return fragment; +} + +function appendInlineMarkdown(parent, text) { + let index = 0; + for (const match of text.matchAll(TOKEN_PATTERN)) { + if (match.index > index) { + parent.append(document.createTextNode(text.slice(index, match.index))); + } + parent.append(createInlineNode(match[0])); + index = match.index + match[0].length; + } + + if (index < text.length) { + parent.append(document.createTextNode(text.slice(index))); + } +} + +function createInlineNode(token) { + if (token.startsWith("`")) { + const code = document.createElement("code"); + code.textContent = token.slice(1, -1); + return code; + } + + if (token.startsWith("**")) { + const strong = document.createElement("strong"); + strong.textContent = token.slice(2, -2); + return strong; + } + + if (token.startsWith("*")) { + const emphasis = document.createElement("em"); + emphasis.textContent = token.slice(1, -1); + return emphasis; + } + + const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (linkMatch) { + return createSafeLink(linkMatch[1], linkMatch[2]); + } + + return document.createTextNode(token); +} + +function createSafeLink(label, rawUrl) { + const anchor = document.createElement("a"); + anchor.textContent = label; + anchor.target = "_blank"; + anchor.rel = "noopener noreferrer"; + + const url = safeUrl(rawUrl); + if (url) { + anchor.href = url; + } + + return anchor; +} + +function safeUrl(rawUrl) { + try { + const url = new URL(rawUrl, window.location.href); + return SAFE_LINK_PROTOCOLS.has(url.protocol) ? url.href : ""; + } catch { + return ""; + } +} diff --git a/src/tasks.js b/src/tasks.js index a54af4a..83a521d 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -1,7 +1,8 @@ -export function createTask(title) { +export function createTask(title, description = "") { return { id: cryptoRandomId(), title: title.trim(), + description: description.trim(), done: false, createdAt: Date.now(), }; diff --git a/styles.css b/styles.css index 2fa1e18..adb665e 100644 --- a/styles.css +++ b/styles.css @@ -47,13 +47,15 @@ body { } .task-form { - display: flex; + display: grid; + grid-template-columns: 1fr auto; gap: 0.5rem; margin-bottom: 1.25rem; } -.task-form input { - flex: 1; +.task-form input, +.task-form textarea { + width: 100%; padding: 0.5rem 0.75rem; font: inherit; color: inherit; @@ -62,13 +64,21 @@ body { border-radius: var(--radius); } -.task-form input:focus { +.task-form textarea { + grid-column: 1 / -1; + min-height: 5rem; + resize: vertical; +} + +.task-form input:focus, +.task-form textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); } .task-form button { + align-self: start; padding: 0.5rem 1rem; font: inherit; color: var(--accent-fg); @@ -100,16 +110,48 @@ body { border-radius: var(--radius); } -.task-item.done .task-title { +.task-item.done .task-content { text-decoration: line-through; color: var(--done); } -.task-title { +.task-content { flex: 1; + min-width: 0; +} + +.task-title { + display: block; + font-weight: 600; + word-break: break-word; +} + +.task-description { + margin-top: 0.35rem; + color: var(--muted); + font-size: 0.95rem; word-break: break-word; } +.task-description p { + margin: 0.25rem 0 0; +} + +.task-description code { + padding: 0.1rem 0.25rem; + color: var(--fg); + background: #f6f8fa; + border: 1px solid var(--border); + border-radius: 4px; + font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", + monospace; + font-size: 0.9em; +} + +.task-description a { + color: var(--accent); +} + .task-delete { appearance: none; background: transparent;