summaryrefslogtreecommitdiff
path: root/app/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/db.server.ts50
-rw-r--r--app/lib/md.server.ts33
2 files changed, 83 insertions, 0 deletions
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts
new file mode 100644
index 0000000..b076ec7
--- /dev/null
+++ b/app/lib/db.server.ts
@@ -0,0 +1,50 @@
+import Database from "better-sqlite3";
+import path from "path";
+
+let db: Database.Database | null = null;
+
+export function getDb(): Database.Database {
+ if (!db) {
+ const dbPath = process.env.DB_PATH ?? path.resolve("microblog.db");
+ db = new Database(dbPath);
+ db.pragma("journal_mode = WAL");
+ db.pragma("foreign_keys = ON");
+ initSchema(db);
+ }
+ return db;
+}
+
+function initSchema(db: Database.Database) {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS posts (
+ id TEXT PRIMARY KEY,
+ content TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+ );
+ CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC);
+ `);
+}
+
+export interface Post {
+ id: string;
+ content: string;
+ created_at: string;
+}
+
+export interface CreatePostInput {
+ id: string;
+ content: string;
+}
+
+export function listPosts(): Post[] {
+ return getDb().prepare("SELECT * FROM posts ORDER BY created_at DESC").all() as Post[];
+}
+
+export function getPostById(id: string): Post | null {
+ return getDb().prepare("SELECT * FROM posts WHERE id = ?").get(id) as Post | null;
+}
+
+export function createPost(input: CreatePostInput): Post {
+ getDb().prepare("INSERT INTO posts (id, content) VALUES (?, ?)").run(input.id, input.content);
+ return getPostById(input.id)!;
+}
diff --git a/app/lib/md.server.ts b/app/lib/md.server.ts
new file mode 100644
index 0000000..8f0d6d6
--- /dev/null
+++ b/app/lib/md.server.ts
@@ -0,0 +1,33 @@
+import { marked } from "marked";
+import sanitizeHtml from "sanitize-html";
+
+const ALLOWED_TAGS = [
+ "p", "br",
+ "h1", "h2", "h3", "h4", "h5", "h6",
+ "ul", "ol", "li",
+ "blockquote",
+ "pre", "code",
+ "strong", "em", "del", "s",
+ "a",
+ "hr",
+ "table", "thead", "tbody", "tr", "th", "td",
+ "img",
+];
+
+const ALLOWED_ATTRIBUTES: sanitizeHtml.IOptions["allowedAttributes"] = {
+ a: ["href", "title"],
+ img: ["src", "alt", "title"],
+ code: ["class"],
+ td: ["align"],
+ th: ["align"],
+};
+
+export function renderMarkdown(content: string): string {
+ const raw = marked.parse(content, { async: false }) as string;
+ return sanitizeHtml(raw, {
+ allowedTags: ALLOWED_TAGS,
+ allowedAttributes: ALLOWED_ATTRIBUTES,
+ allowedSchemes: ["http", "https", "mailto"],
+ allowedSchemesByTag: { img: ["http", "https"] },
+ });
+}