diff --git a/package-lock.json b/package-lock.json index 1e8c4b7..9d6e9fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "dsec-notebook", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dsec-notebook", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { + "katex": "^0.18.5", "markdown-it": "^15.0.1", + "markdown-it-texmath": "^1.0.0", "resend": "^6.25.0" }, "devDependencies": { @@ -6455,6 +6457,31 @@ "node": ">=0.10.0" } }, + "node_modules/katex": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.5.tgz", + "integrity": "sha512-1FU5H3RjGJVj6GT9fMjf2uMIXmPQp232aXS3UEeQzf0dxefHg/CKMvNB3DuOC46LFG/E5PNxrE8dOwB782FDGg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^15.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -6823,6 +6850,12 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it-texmath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", + "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index a397aad..add4e04 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "lint:fix": "oxlint --fix" }, "dependencies": { + "katex": "^0.18.5", "markdown-it": "^15.0.1", + "markdown-it-texmath": "^1.0.0", "resend": "^6.25.0" }, "devDependencies": { diff --git a/src/lib/components/MarkdownEditor.svelte b/src/lib/components/MarkdownEditor.svelte index 66564ad..43b2d33 100644 --- a/src/lib/components/MarkdownEditor.svelte +++ b/src/lib/components/MarkdownEditor.svelte @@ -108,6 +108,20 @@ }); } + function insertMathBlock() { + const { start, end, text } = selection(); + const formula = text || "x^2 + y^2 = z^2"; + applyEdit((value) => { + const insertion = `$$\n${formula}\n$$`; + const next = value.slice(0, start) + insertion + value.slice(end); + return { + value: next, + start: start + 3, + end: start + 3 + formula.length, + }; + }); + } + function insertText(text: string) { applyEdit((value, start, end) => { const next = value.slice(0, start) + text + value.slice(end); @@ -210,6 +224,22 @@ + +
Markdown supported: **bold**, _italic_, `code`, [links](url), lists, quotes and code - blocks. Paste or insert images to embed them. + blocks. LaTeX equations use $inline$ or $$display$$ notation. Paste or insert images to + embed them.
{#if uploading}Uploading image...
diff --git a/src/lib/markdown.spec.ts b/src/lib/markdown.spec.ts index 794a922..bea2258 100644 --- a/src/lib/markdown.spec.ts +++ b/src/lib/markdown.spec.ts @@ -1,5 +1,20 @@ import { describe, expect, it } from "vitest"; -import { previewMarkdown } from "./markdown"; +import { previewMarkdown, renderMarkdown } from "./markdown"; + +describe("renderMarkdown", () => { + it("renders inline and display LaTeX with KaTeX", () => { + const rendered = renderMarkdown("Euler: $e^{i\\pi} + 1 = 0$\n\n$$\n\\frac{a}{b}\n$$"); + + expect(rendered).toContain('class="katex"'); + expect(rendered).toContain('class="katex-display"'); + expect(rendered).toContain("e^{i\\pi} + 1 = 0"); + expect(rendered).toContain("\\frac{a}{b}"); + }); + + it("renders invalid LaTeX as an error instead of throwing", () => { + expect(() => renderMarkdown("$\\notacommand{$")).not.toThrow(); + }); +}); describe("previewMarkdown", () => { it("returns readable text without markdown formatting", () => { diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index d671d03..2c778b3 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -1,4 +1,6 @@ import MarkdownIt from "markdown-it"; +import texmath from "markdown-it-texmath"; +import katex from "katex"; const md = new MarkdownIt({ html: false, @@ -6,6 +8,15 @@ const md = new MarkdownIt({ breaks: true, }); +md.use(texmath, { + engine: katex, + delimiters: ["dollars", "brackets"], + katexOptions: { + throwOnError: false, + output: "htmlAndMathml", + }, +}); + export function renderMarkdown(source: string): string { return md.render(source ?? ""); } @@ -14,7 +25,6 @@ export function previewMarkdown(source: string, maxLength = 180): string { const text = md .parse(source ?? "", {}) .map((token) => { - console.log(token); if (token.type === "inline") { return (token.children ?? []) .filter((child) => child.type !== "image") diff --git a/src/lib/server/api.ts b/src/lib/server/api.ts index c7b6b26..45d6f94 100644 --- a/src/lib/server/api.ts +++ b/src/lib/server/api.ts @@ -469,9 +469,12 @@ const NOTE_COLUMNS = function notesCreate( db: Db, - args: { token: string; title: string; content: string; topicId: string; unitId: string }, + args: { token: string; title: string; content: string; topicId?: string; unitId?: string }, ) { const user = requireAuth(db, args.token); + const topicId = args.topicId?.trim() ?? ""; + const unitId = args.unitId?.trim() ?? ""; + if (!topicId && !unitId) throw new Error("Select a topic or a unit"); const id = newId(); const now = Date.now(); db.exec("BEGIN"); @@ -479,17 +482,7 @@ function notesCreate( db.prepare( `INSERT INTO notes (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, commentCount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0)`, - ).run( - id, - args.title, - args.content, - args.topicId, - args.unitId, - user._id, - user.name, - now, - now, - ); + ).run(id, args.title, args.content, topicId, unitId, user._id, user.name, now, now); db.prepare( "INSERT INTO votes (id, userId, targetType, targetId, value) VALUES (?, ?, 'note', ?, 1)", ).run(newId(), user._id, id); @@ -587,9 +580,12 @@ const QUESTION_COLUMNS = function questionsCreate( db: Db, - args: { token: string; title: string; content: string; topicId: string; unitId: string }, + args: { token: string; title: string; content: string; topicId?: string; unitId?: string }, ) { const user = requireAuth(db, args.token); + const topicId = args.topicId?.trim() ?? ""; + const unitId = args.unitId?.trim() ?? ""; + if (!topicId && !unitId) throw new Error("Select a topic or a unit"); const id = newId(); const now = Date.now(); db.exec("BEGIN"); @@ -597,17 +593,7 @@ function questionsCreate( db.prepare( `INSERT INTO questions (id, title, content, topicId, unitId, authorId, authorName, createdAt, updatedAt, voteCount, answerCount, solved) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 0)`, - ).run( - id, - args.title, - args.content, - args.topicId, - args.unitId, - user._id, - user.name, - now, - now, - ); + ).run(id, args.title, args.content, topicId, unitId, user._id, user.name, now, now); db.prepare( "INSERT INTO votes (id, userId, targetType, targetId, value) VALUES (?, ?, 'question', ?, 1)", ).run(newId(), user._id, id); diff --git a/src/markdown-it-texmath.d.ts b/src/markdown-it-texmath.d.ts new file mode 100644 index 0000000..9d9c1f5 --- /dev/null +++ b/src/markdown-it-texmath.d.ts @@ -0,0 +1,22 @@ +declare module "markdown-it-texmath" { + import type MarkdownIt from "markdown-it"; + import type katex from "katex"; + + type Delimiter = + | "dollars" + | "brackets" + | "doxygen" + | "gitlab" + | "julia" + | "kramdown" + | "beg_end"; + + interface TexmathOptions { + engine: typeof katex; + delimiters?: Delimiter | Delimiter[]; + katexOptions?: katex.KatexOptions; + } + + const texmath: MarkdownIt.PluginWithOptions- Share study notes with the Deakin community. Focus on a specific topic, not an entire unit. -
+Share study notes with the Deakin community.