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
48 lines
1.7 KiB
Vue
48 lines
1.7 KiB
Vue
<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
|
|
>
|