Initial commit: note-taking/task-tracking app with LLM integration scaffold
Vue 3 + TypeScript frontend with Pinia stores, markdown rendering (marked + DOMPurify), wikilink/tag linkification, and autocomplete. Quart async backend with SQLAlchemy 2.0, PostgreSQL ARRAY columns, task-note companion linking, backlinks, and note-to-task conversion. Docker Compose setup with PostgreSQL 16 and Ollama. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { marked } from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
import { linkifyTags, linkifyWikilinks } from "@/utils/tags";
|
||||
|
||||
export function renderMarkdown(text: string): string {
|
||||
const html = marked(text) as string;
|
||||
const withTags = linkifyTags(html);
|
||||
const withLinks = linkifyWikilinks(withTags);
|
||||
return DOMPurify.sanitize(withLinks, {
|
||||
ADD_ATTR: ["data-tag", "data-title"],
|
||||
});
|
||||
}
|
||||
|
||||
export function renderPreview(text: string): string {
|
||||
const html = marked(text) as string;
|
||||
const withTags = linkifyTags(html);
|
||||
const withLinks = linkifyWikilinks(withTags);
|
||||
return DOMPurify.sanitize(withLinks, {
|
||||
FORBID_TAGS: ["a", "img"],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
const CODE_FENCE_RE = /```[\s\S]*?```|`[^`\n]+`/g;
|
||||
const TAG_RE = /(?<!\w)#([\w]+(?:\/[\w]+)*)/g;
|
||||
|
||||
function escapeHtmlAttr(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
export function extractTags(body: string): string[] {
|
||||
const cleaned = body.replace(CODE_FENCE_RE, "");
|
||||
const tags = new Set<string>();
|
||||
let match;
|
||||
while ((match = TAG_RE.exec(cleaned)) !== null) {
|
||||
tags.add(match[1]);
|
||||
}
|
||||
TAG_RE.lastIndex = 0;
|
||||
return [...tags].sort();
|
||||
}
|
||||
|
||||
export function linkifyTags(html: string): string {
|
||||
// Split on code/pre blocks to avoid linkifying inside them
|
||||
const parts = html.split(/(<code[\s\S]*?<\/code>|<pre[\s\S]*?<\/pre>)/gi);
|
||||
return parts
|
||||
.map((part, i) => {
|
||||
// Odd indices are code/pre blocks, skip them
|
||||
if (i % 2 === 1) return part;
|
||||
return part.replace(TAG_RE, (full, tag) => {
|
||||
const encoded = encodeURIComponent(tag);
|
||||
return `<a class="inline-tag" data-tag="${escapeHtmlAttr(tag)}" href="/notes?tag=${encoded}">${full}</a>`;
|
||||
});
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
const WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
||||
|
||||
export function linkifyWikilinks(html: string): string {
|
||||
const parts = html.split(/(<code[\s\S]*?<\/code>|<pre[\s\S]*?<\/pre>)/gi);
|
||||
return parts
|
||||
.map((part, i) => {
|
||||
if (i % 2 === 1) return part;
|
||||
return part.replace(WIKILINK_RE, (_full, title: string, display?: string) => {
|
||||
const trimmed = title.trim();
|
||||
const label = display || trimmed;
|
||||
const encoded = encodeURIComponent(trimmed);
|
||||
return `<a class="wikilink" data-title="${escapeHtmlAttr(trimmed)}" href="/notes/by-title?title=${encoded}">${escapeHtmlAttr(label)}</a>`;
|
||||
});
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
Reference in New Issue
Block a user