From abc8f1ae16d26871062ce4060e70a438fc9662d9 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 22 Apr 2024 11:02:09 -1000 Subject: [PATCH] refactor(api): :art: Improve Markdown parsing with markdown-it instead of marked --- bun.lockb | Bin 151996 -> 158484 bytes database/entities/Status.ts | 43 ++++++++++++++++-- package.json | 8 +++- server/api/api/auth/mastodon-login/index.ts | 11 +++-- .../v1/accounts/update_credentials/index.ts | 17 ++++--- .../api/v1/instance/extended_description.ts | 8 ++-- server/api/api/v1/statuses/index.ts | 19 +------- types.d.ts | 1 + utils/formatting.ts | 29 ------------ 9 files changed, 71 insertions(+), 65 deletions(-) delete mode 100644 utils/formatting.ts diff --git a/bun.lockb b/bun.lockb index 7cc9b0dd28d86508bb782f0e2989d2f39620ce2b..061bd1cda657456dfad3f19dc78b1a12eead091e 100755 GIT binary patch delta 28065 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 KQRtf{@Aw}R!?&pb diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 59243cad..178c85f1 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -24,7 +24,6 @@ import { maybe, oneOrMore, } from "magic-regexp"; -import { parse } from "marked"; import { db } from "~drizzle/db"; import { Attachments, @@ -60,6 +59,11 @@ import { userExtrasTemplate, userRelations, } from "./User"; +import MarkdownIt from "markdown-it"; +import markdownItTocDoneRight from "markdown-it-toc-done-right"; +import markdownItContainer from "markdown-it-container"; +import markdownItAnchor from "markdown-it-anchor"; +import markdownItTaskLists from "@hackmd/markdown-it-task-lists"; export type Status = InferSelectModel; @@ -577,9 +581,9 @@ export const contentToHtml = async ( htmlContent = content["text/html"].content; } else if (content["text/markdown"]) { htmlContent = await sanitizeHtml( - await parse(content["text/markdown"].content), + await markdownParse(content["text/markdown"].content), ); - } else if (content["text/plain"]) { + } else if (content["text/plain"]?.content) { // Split by newline and add

tags htmlContent = content["text/plain"].content .split("\n") @@ -605,6 +609,39 @@ export const contentToHtml = async ( return htmlContent; }; +export const markdownParse = async (content: string) => { + return (await getMarkdownRenderer()).render(content); +}; + +export const getMarkdownRenderer = async () => { + const renderer = MarkdownIt({ + html: true, + linkify: true, + }); + + renderer.use(markdownItAnchor, { + permalink: markdownItAnchor.permalink.ariaHidden({ + symbol: "", + placement: "before", + }), + }); + + renderer.use(markdownItTocDoneRight, { + containerClass: "toc", + level: [1, 2, 3, 4], + listType: "ul", + listClass: "toc-list", + itemClass: "toc-item", + linkClass: "toc-link", + }); + + renderer.use(markdownItTaskLists); + + renderer.use(markdownItContainer); + + return renderer; +}; + export const federateNote = async (note: Note) => { for (const user of await note.getUsersToFederateTo()) { // TODO: Add queue system diff --git a/package.json b/package.json index fcbd17a9..90d673aa 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/html-to-text": "^9.0.4", "@types/ioredis": "^5.0.0", "@types/jsonld": "^1.5.13", + "@types/markdown-it-container": "^2.0.10", "@types/mime-types": "^2.1.4", "@types/pg": "^8.11.5", "bun-types": "latest", @@ -66,7 +67,9 @@ "typescript": "^5.3.2" }, "dependencies": { + "@hackmd/markdown-it-task-lists": "^2.1.4", "@json2csv/plainjs": "^7.0.6", + "@shikijs/markdown-it": "^1.3.0", "blurhash": "^2.0.5", "bullmq": "^5.7.1", "chalk": "^5.3.0", @@ -86,7 +89,10 @@ "linkifyjs": "^4.1.3", "log-manager": "workspace:*", "magic-regexp": "^0.8.0", - "marked": "^12.0.1", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "markdown-it-container": "^4.0.0", + "markdown-it-toc-done-right": "^4.2.0", "media-manager": "workspace:*", "megalodon": "^10.0.0", "meilisearch": "^0.38.0", diff --git a/server/api/api/auth/mastodon-login/index.ts b/server/api/api/auth/mastodon-login/index.ts index 9cd0f342..0d19261b 100644 --- a/server/api/api/auth/mastodon-login/index.ts +++ b/server/api/api/auth/mastodon-login/index.ts @@ -20,8 +20,10 @@ export const meta = applyConfig({ }); export const schema = z.object({ - "user[email]": z.string().email(), - "user[password]": z.string().max(100).min(3), + user: z.object({ + email: z.string().email(), + password: z.string().max(100).min(3), + }), }); /** @@ -29,8 +31,9 @@ export const schema = z.object({ */ export default apiRoute( async (req, matchedRoute, extraData) => { - const { "user[email]": email, "user[password]": password } = - extraData.parsedRequest; + const { + user: { email, password }, + } = extraData.parsedRequest; const redirectToLogin = (error: string) => Response.redirect( diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index c56768da..e184f890 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -1,5 +1,4 @@ import { apiRoute, applyConfig } from "@api"; -import { convertTextToHtml } from "@formatting"; import { errorResponse, jsonResponse } from "@response"; import { sanitizeHtml } from "@sanitization"; import { config } from "config-manager"; @@ -11,10 +10,10 @@ import { LocalMediaBackend, S3MediaBackend } from "media-manager"; import { z } from "zod"; import { getUrl } from "~database/entities/Attachment"; import { parseEmojis } from "~database/entities/Emoji"; +import { contentToHtml } from "~database/entities/Status"; import { findFirstUser, userToAPI } from "~database/entities/User"; import { db } from "~drizzle/db"; import { EmojiToUser, Users } from "~drizzle/schema"; -import type { Source as APISource } from "~types/mastodon/source"; export const meta = applyConfig({ allowedMethods: ["PATCH"], @@ -125,20 +124,24 @@ export default apiRoute( return errorResponse("Bio contains blocked words", 422); } - (self.source as APISource).note = sanitizedNote; - self.note = await convertTextToHtml(sanitizedNote); + self.source.note = sanitizedNote; + self.note = await contentToHtml({ + "text/markdown": { + content: sanitizedNote, + }, + }); } if (source_privacy && self.source) { - (self.source as APISource).privacy = source_privacy; + self.source.privacy = source_privacy; } if (source_sensitive && self.source) { - (self.source as APISource).sensitive = source_sensitive; + self.source.sensitive = source_sensitive; } if (source_language && self.source) { - (self.source as APISource).language = source_language; + self.source.language = source_language; } if (avatar) { diff --git a/server/api/api/v1/instance/extended_description.ts b/server/api/api/v1/instance/extended_description.ts index 01c063ad..e0563929 100644 --- a/server/api/api/v1/instance/extended_description.ts +++ b/server/api/api/v1/instance/extended_description.ts @@ -1,7 +1,7 @@ import { apiRoute, applyConfig } from "@api"; import { dualLogger } from "@loggers"; import { jsonResponse } from "@response"; -import { parse } from "marked"; +import { getMarkdownRenderer } from "~database/entities/Status"; import { LogLevel } from "~packages/log-manager"; export const meta = applyConfig({ @@ -19,7 +19,7 @@ export const meta = applyConfig({ export default apiRoute(async (req, matchedRoute, extraData) => { const config = await extraData.configManager.getConfig(); - let extended_description = parse( + let extended_description = (await getMarkdownRenderer()).render( "This is a [Lysand](https://lysand.org) server with the default extended description.", ); let lastModified = new Date(2024, 0, 0); @@ -30,13 +30,13 @@ export default apiRoute(async (req, matchedRoute, extraData) => { if (await extended_description_file.exists()) { extended_description = - (await parse( + (await getMarkdownRenderer()).render( (await extended_description_file.text().catch(async (e) => { await dualLogger.logError(LogLevel.ERROR, "Routes", e); return ""; })) || "This is a [Lysand](https://lysand.org) server with the default extended description.", - )) || ""; + ) || ""; lastModified = new Date(extended_description_file.lastModified); } diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 09ce510e..5043f45b 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -1,11 +1,8 @@ import { apiRoute, applyConfig, idValidator } from "@api"; import { errorResponse, jsonResponse } from "@response"; -import { sanitizeHtml } from "@sanitization"; import { config } from "config-manager"; import ISO6391 from "iso-639-1"; -import { parse } from "marked"; import { z } from "zod"; -import type { StatusWithRelations } from "~database/entities/Status"; import { federateNote, parseTextMentions } from "~database/entities/Status"; import { db } from "~drizzle/db"; import { Note } from "~packages/database-interface/note"; @@ -106,18 +103,6 @@ export default apiRoute( } } - let sanitizedStatus: string; - - if (content_type === "text/markdown") { - sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string); - } else if (content_type === "text/x.misskeymarkdown") { - // Parse as MFM - // TODO: Parse as MFM - sanitizedStatus = await sanitizeHtml(parse(status ?? "") as string); - } else { - sanitizedStatus = await sanitizeHtml(status ?? ""); - } - // Check if status body doesnt match filters if ( config.filters.note_content.some((filter) => status?.match(filter)) @@ -152,13 +137,13 @@ export default apiRoute( } } - const mentions = await parseTextMentions(sanitizedStatus); + const mentions = await parseTextMentions(status ?? ""); const newNote = await Note.fromData( user, { [content_type]: { - content: sanitizedStatus ?? "", + content: status ?? "", }, }, visibility, diff --git a/types.d.ts b/types.d.ts index e69de29b..2ab38b10 100644 --- a/types.d.ts +++ b/types.d.ts @@ -0,0 +1 @@ +declare module "@hackmd/markdown-it-task-lists"; diff --git a/utils/formatting.ts b/utils/formatting.ts deleted file mode 100644 index 2d802dfe..00000000 --- a/utils/formatting.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { sanitizeHtml } from "@sanitization"; -import linkifyHtml from "linkify-html"; -import linkifyStr from "linkify-string"; -import { parse } from "marked"; - -/** - * Converts plaintext, MFM or Markdown to HTML - * @param text Text to convert - * @param content_type Content type of the text (optional, defaults to plaintext) - * @returns HTML - */ -export const convertTextToHtml = async ( - text: string, - content_type?: string, -) => { - if (content_type === "text/markdown") { - return linkifyHtml(await sanitizeHtml(await parse(text))); - } - if (content_type === "text/x.misskeymarkdown") { - // Parse as MFM - // TODO: Implement MFM - return text; - } - // Parse as plaintext - return linkifyStr(text) - .split("\n") - .map((line) => `

${line}

`) - .join("\n"); -};