M6: lightweight markdown rendering on note cards
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 30s

Render a small Markdown subset in the read view — headings (#..###), bullet/ordered lists, blockquote, fenced code, and inline **bold** / *italic* / `code` — alongside the existing [[wiki-links]] (task 1905). Capture stays plain text; only the card render is formatted.

Hand-rolled dependency-free parser (notes/markdown.ts) rendered as Vue vnodes (MarkdownText/MarkdownInline), never v-html, so there is no HTML-injection surface — matches the no-heavy-dep ethos. Headings require '# ' (space), so a #tag (no space) stays plain text and is never mistaken for a heading; the two coexist. Replaces LinkedText (links-only) on the card; LinkedText deleted.

Pure frontend — no backend/DB/migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-22 14:44:03 -04:00
co-authored by Claude Opus 4.8
parent bb33d5c495
commit 261d8fcac5
5 changed files with 217 additions and 65 deletions
@@ -0,0 +1,47 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useNotesStore } from "../stores/notes";
import { useTitlesStore } from "../stores/titles";
import type { InlineToken } from "../notes/markdown";
defineProps<{ tokens: InlineToken[] }>();
const router = useRouter();
const notes = useNotesStore();
const titles = useTitlesStore();
// A [[wiki-link]] on the card: resolve the title and open the target note (creating
// it first if it doesn't exist), via the board's ?open=<id> mechanism.
async function follow(title: string) {
await titles.load();
let hit = titles.resolve(title);
if (!hit) {
const created = await notes.createTitled(title);
await titles.reload();
hit = { id: created.id, title: created.title ?? title };
}
void router.push({ path: "/", query: { open: hit.id } });
}
</script>
<!-- Rendered tightly (no whitespace between tokens) so a token's own leading/trailing
spaces are preserved and no extra spaces are introduced. -->
<template
><template v-for="(t, i) in tokens" :key="i"
><span
v-if="t.type === 'link'"
role="link"
tabindex="0"
class="cursor-pointer font-medium text-brand-700 underline-offset-2 hover:underline dark:text-brand"
@click.stop="follow(t.value)"
@keydown.enter.stop.prevent="follow(t.value)"
>{{ t.value }}</span
><strong v-else-if="t.type === 'bold'" class="font-semibold">{{ t.value }}</strong
><em v-else-if="t.type === 'italic'">{{ t.value }}</em
><code
v-else-if="t.type === 'code'"
class="rounded bg-black/5 px-1 py-0.5 font-mono text-[0.85em] dark:bg-white/10"
>{{ t.value }}</code
><template v-else>{{ t.value }}</template></template
></template
>