diff --git a/index.html b/index.html index b6b00fb..b57e5bb 100644 --- a/index.html +++ b/index.html @@ -35,6 +35,12 @@
$1")
+ .replace(BOLD_PATTERN, "$1")
+ .replace(ITALIC_PATTERN, "$1$2")
+ .replace(LINK_PATTERN, renderLink);
+}
+
+function renderLink(match, label, href) {
+ const safeHref = safeUrl(href);
+ if (!safeHref) return label;
+
+ return `${label}`;
+}
+
+function safeUrl(href) {
+ try {
+ const url = new URL(href, window.location.href);
+ if (!["http:", "https:", "mailto:"].includes(url.protocol)) {
+ return "";
+ }
+ return escapeAttribute(url.href);
+ } catch {
+ return "";
+ }
+}
+
+function escapeHtml(value) {
+ return value
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function escapeAttribute(value) {
+ return value.replace(/"/g, """);
+}
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 a6c7afc..838cbb0 100644
--- a/styles.css
+++ b/styles.css
@@ -97,11 +97,13 @@ body {
.task-form {
display: flex;
+ flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.25rem;
}
-.task-form input {
+.task-form input,
+.task-form textarea {
flex: 1;
padding: 0.5rem 0.75rem;
font: inherit;
@@ -111,7 +113,13 @@ body {
border-radius: var(--radius);
}
-.task-form input:focus {
+.task-form textarea {
+ min-height: 5.5rem;
+ resize: vertical;
+}
+
+.task-form input:focus,
+.task-form textarea:focus {
outline: 2px solid var(--accent);
outline-offset: -1px;
border-color: var(--accent);
@@ -155,10 +163,37 @@ body {
}
.task-title {
+ display: block;
+ font-weight: 600;
+ word-break: break-word;
+}
+
+.task-content {
flex: 1;
+ min-width: 0;
+}
+
+.task-description {
+ margin-top: 0.25rem;
+ color: var(--muted);
+ font-size: 0.93rem;
word-break: break-word;
}
+.task-description p {
+ margin: 0;
+}
+
+.task-description a {
+ color: var(--accent);
+}
+
+.task-description code {
+ padding: 0.08rem 0.25rem;
+ background: rgba(175, 184, 193, 0.2);
+ border-radius: 4px;
+}
+
.task-delete {
appearance: none;
background: transparent;
diff --git a/test/server.test.js b/test/server.test.js
index c3b9f65..febb8d0 100644
--- a/test/server.test.js
+++ b/test/server.test.js
@@ -10,12 +10,26 @@ const { SESSION_COOKIE, createServer, sanitizeTasks } = require("../server.js");
test("sanitizeTasks drops malformed rows and trims persisted fields", () => {
assert.deepEqual(
sanitizeTasks([
- { id: " a ", title: " Write tests ", done: 1, createdAt: 10 },
+ {
+ id: " a ",
+ title: " Write tests ",
+ description: " **Add assertions** ",
+ done: 1,
+ createdAt: 10,
+ },
{ id: "", title: "missing id" },
{ id: "missing-title", title: "" },
null,
]),
- [{ id: "a", title: "Write tests", done: true, createdAt: 10 }],
+ [
+ {
+ id: "a",
+ title: "Write tests",
+ description: "**Add assertions**",
+ done: true,
+ createdAt: 10,
+ },
+ ],
);
});
@@ -114,6 +128,7 @@ test("authenticated task API persists tasks by GitHub user id", async () => {
{
id: "task-1",
title: "Ship OAuth",
+ description: "",
done: false,
createdAt: loaded.body.tasks[0].createdAt,
},