diff --git a/flake.nix b/flake.nix
index d80c69f..e12cdc8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -25,7 +25,7 @@
api = pkgs.mkShell {
packages = with pkgs; [ postgresql_16 ];
shellHook = ''
- alias dbmate="${pkgs.dbmate}/bin/dbmate --url postgres://postgres@localhost:15432/archtika?sslmode=disable"
+ alias dbmate="${pkgs.dbmate}/bin/dbmate --no-dump-schema --url postgres://postgres@localhost:15432/archtika?sslmode=disable"
alias formatsql="${pkgs.pgformatter}/bin/pg_format -s 2 -f 2 -U 2 -i db/migrations/*.sql"
'';
};
diff --git a/web-app/package-lock.json b/web-app/package-lock.json
index 1927efe..33a77dc 100644
--- a/web-app/package-lock.json
+++ b/web-app/package-lock.json
@@ -8,6 +8,7 @@
"name": "web-app",
"version": "0.0.1",
"dependencies": {
+ "github-slugger": "2.0.0",
"highlight.js": "11.10.0",
"marked": "14.0.0",
"marked-highlight": "2.1.4"
@@ -1429,6 +1430,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/github-slugger": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
+ "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
+ "license": "ISC"
+ },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
diff --git a/web-app/package.json b/web-app/package.json
index 19335d7..6965d7b 100644
--- a/web-app/package.json
+++ b/web-app/package.json
@@ -26,6 +26,7 @@
},
"type": "module",
"dependencies": {
+ "github-slugger": "2.0.0",
"highlight.js": "11.10.0",
"marked": "14.0.0",
"marked-highlight": "2.1.4"
diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts
index 507a067..3a297d2 100644
--- a/web-app/src/lib/utils.ts
+++ b/web-app/src/lib/utils.ts
@@ -1,6 +1,8 @@
import { Marked } from "marked";
+import type { Renderer, Token } from "marked";
import { markedHighlight } from "marked-highlight";
import hljs from "highlight.js";
+import GithubSlugger from "github-slugger";
export const sortOptions = [
{ value: "creation-time", text: "Creation time" },
@@ -31,6 +33,98 @@ const createMarkdownParser = () => {
})
);
+ const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;
+
+ function unescape(html: string) {
+ return html.replace(unescapeTest, (_, n) => {
+ n = n.toLowerCase();
+ if (n === "colon") return ":";
+ if (n.charAt(0) === "#") {
+ return n.charAt(1) === "x"
+ ? String.fromCharCode(parseInt(n.substring(2), 16))
+ : String.fromCharCode(+n.substring(1));
+ }
+ return "";
+ });
+ }
+
+ let slugger = new GithubSlugger();
+ let headings: { text: string; raw: string; level: number; id: string }[] = [];
+ let sectionStack: { level: number; id: string }[] = [];
+
+ function gfmHeadingId({ prefix = "" } = {}) {
+ return {
+ renderer: {
+ heading(this: Renderer, { tokens, depth }: { tokens: Token[]; depth: number }) {
+ const text = this.parser.parseInline(tokens);
+ const raw = unescape(this.parser.parseInline(tokens, this.parser.textRenderer))
+ .trim()
+ .replace(/<[!\/a-z].*?>/gi, "");
+ const level = depth;
+ const id = `${prefix}${slugger.slug(raw.toLowerCase())}`;
+ const heading = { level, text, id, raw };
+ headings.push(heading);
+
+ // Close any sections that are at a higher level than the current heading
+ let closingSections = "";
+ while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
+ sectionStack.pop();
+ closingSections += "";
+ }
+
+ // Open a new section for this heading
+ sectionStack.push({ level, id });
+ const openingSection = ``;
+
+ return `
+ ${closingSections}
+ ${openingSection}
+
+ ${text}
+
+ `;
+ }
+ },
+ hooks: {
+ preprocess(src: string) {
+ headings = [];
+ sectionStack = [];
+ slugger = new GithubSlugger();
+
+ return src;
+ },
+ postprocess(html: string) {
+ // Close any remaining open sections
+ const closingRemainingSection = "".repeat(sectionStack.length);
+
+ // Generate table of contents
+ const tableOfContents =
+ headings.length > 0
+ ? `
+ Table of contents
+
+ ${headings
+ .map(
+ ({ id, text, level }) => `
+ - ${text}
`
+ )
+ .join("")}
+
+ `
+ : "";
+
+ return `
+ ${tableOfContents}
+ ${html}
+ ${closingRemainingSection}
+ `;
+ }
+ }
+ };
+ }
+
+ marked.use(gfmHeadingId());
+
return marked;
};
@@ -42,8 +136,6 @@ export const md = async (markdownContent: string) => {
return html;
};
-// test
-
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {
const clipboardItems = Array.from(event.clipboardData?.items || []);
const file = clipboardItems.find((item) => item.type.startsWith("image/"));