mermaid ink image support

This commit is contained in:
2025-07-15 12:47:53 -06:00
parent 57b7d8ac1e
commit 43ed57e558
8 changed files with 68 additions and 9 deletions

View File

@@ -45,6 +45,7 @@
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@types/node": "^24.0.10", "@types/node": "^24.0.10",
"@types/pako": "^2.0.3",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",

8
pnpm-lock.yaml generated
View File

@@ -99,6 +99,9 @@ importers:
'@types/node': '@types/node':
specifier: ^24.0.10 specifier: ^24.0.10
version: 24.0.13 version: 24.0.13
'@types/pako':
specifier: ^2.0.3
version: 2.0.3
'@types/react': '@types/react':
specifier: ^19.1.8 specifier: ^19.1.8
version: 19.1.8 version: 19.1.8
@@ -1059,6 +1062,9 @@ packages:
'@types/node@24.0.13': '@types/node@24.0.13':
resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==} resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==}
'@types/pako@2.0.3':
resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
'@types/react-dom@19.1.6': '@types/react-dom@19.1.6':
resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==} resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==}
peerDependencies: peerDependencies:
@@ -3791,6 +3797,8 @@ snapshots:
dependencies: dependencies:
undici-types: 7.8.0 undici-types: 7.8.0
'@types/pako@2.0.3': {}
'@types/react-dom@19.1.6(@types/react@19.1.8)': '@types/react-dom@19.1.6(@types/react@19.1.8)':
dependencies: dependencies:
'@types/react': 19.1.8 '@types/react': 19.1.8

View File

@@ -45,10 +45,10 @@ export default function CourseCalendar() {
className=" className="
min-h-0 min-h-0
flex-grow flex-grow
border-4 border-2
border-gray-900 border-gray-900
rounded-lg rounded-lg
bg-slate-950 bg-slate-950/70
sm:p-1 sm:p-1
" "
> >

View File

@@ -2,6 +2,7 @@ import MarkdownDisplay from "@/components/MarkdownDisplay";
import { LocalAssignment } from "@/models/local/assignment/localAssignment"; import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { rubricItemIsExtraCredit } from "@/models/local/assignment/rubricItem"; import { rubricItemIsExtraCredit } from "@/models/local/assignment/rubricItem";
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils"; import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
import { formatHumanReadableDate } from "@/services/utils/dateFormat";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
export default function AssignmentPreview({ export default function AssignmentPreview({
@@ -19,11 +20,15 @@ export default function AssignmentPreview({
<section> <section>
<div className="flex"> <div className="flex">
<div className="flex-1 text-end pe-3">Due Date</div> <div className="flex-1 text-end pe-3">Due Date</div>
<div className="flex-1">{assignment.dueAt}</div> <div className="flex-1">
{formatHumanReadableDate(assignment.dueAt)}
</div>
</div> </div>
<div className="flex"> <div className="flex">
<div className="flex-1 text-end pe-3">Lock Date</div> <div className="flex-1 text-end pe-3">Lock Date</div>
<div className="flex-1">{assignment.lockAt}</div> <div className="flex-1">
{assignment.lockAt && formatHumanReadableDate(assignment.lockAt)}
</div>
</div> </div>
<div className="flex"> <div className="flex">
<div className="flex-1 text-end pe-3">Assignment Group Name</div> <div className="flex-1 text-end pe-3">Assignment Group Name</div>

View File

@@ -14,14 +14,14 @@ export default function EditPageHeader({
return ( return (
<div className="py-1 flex flex-row justify-start gap-3"> <div className="py-1 flex flex-row justify-start gap-3">
<Link <Link
className="btn btn-thin" className="btn"
href={getCourseUrl(courseName)} href={getCourseUrl(courseName)}
shallow={true} shallow={true}
> >
{courseName} {courseName}
</Link> </Link>
<UpdatePageName pageName={pageName} moduleName={moduleName} /> <UpdatePageName pageName={pageName} moduleName={moduleName} />
<div>{pageName}</div> <div className="my-auto">{pageName}</div>
</div> </div>
); );
} }

View File

@@ -14,7 +14,7 @@ export default function EditQuizHeader({
return ( return (
<div className="py-1 flex flex-row justify-start gap-3"> <div className="py-1 flex flex-row justify-start gap-3">
<Link <Link
className="btn btn-thin" className="btn"
href={getCourseUrl(courseName)} href={getCourseUrl(courseName)}
shallow={true} shallow={true}
> >

View File

@@ -1,9 +1,34 @@
"use client"; "use client";
import { marked } from "marked"; import { marked, MarkedExtension } from "marked";
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
import { LocalCourseSettings } from "@/models/local/localCourseSettings"; import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import markedKatex from "marked-katex-extension"; import markedKatex from "marked-katex-extension";
const mermaidExtension = {
name: "mermaid",
level: "block" as const,
start(src: string) {
return src.indexOf("```mermaid");
},
tokenizer(src: string) {
const rule = /^```mermaid\n([\s\S]+?)```(?:\n|$)/;
const match = rule.exec(src);
if (match) {
return {
type: "mermaid",
raw: match[0],
text: match[1].trim(),
};
}
},
renderer(token: { text: string }) {
const base64 = btoa(token.text);
const url = `https://mermaid.ink/img/${base64}?type=png`
console.log(token.text, url);
return `<img src="${url}" alt="Mermaid diagram" />`;
},
};
marked.use( marked.use(
markedKatex({ markedKatex({
throwOnError: false, throwOnError: false,
@@ -11,6 +36,8 @@ marked.use(
}) })
); );
marked.use({ extensions: [mermaidExtension] });
export function extractImageSources(htmlString: string) { export function extractImageSources(htmlString: string) {
const srcUrls = []; const srcUrls = [];
const regex = /<img[^>]+src=["']?([^"'>]+)["']?/g; const regex = /<img[^>]+src=["']?([^"'>]+)["']?/g;
@@ -22,6 +49,7 @@ export function extractImageSources(htmlString: string) {
return srcUrls; return srcUrls;
} }
export function convertImagesToCanvasImages( export function convertImagesToCanvasImages(
html: string, html: string,
settings: LocalCourseSettings settings: LocalCourseSettings
@@ -37,7 +65,9 @@ export function convertImagesToCanvasImages(
for (const imageSrc of imageSources) { for (const imageSrc of imageSources) {
const destinationUrl = imageLookup[imageSrc]; const destinationUrl = imageLookup[imageSrc];
if (typeof destinationUrl === "undefined") { if (typeof destinationUrl === "undefined") {
console.log(`No image in settings for ${imageSrc}, do you have NEXT_PUBLIC_ENABLE_FILE_SYNC=true in your settings?`) console.log(
`No image in settings for ${imageSrc}, do you have NEXT_PUBLIC_ENABLE_FILE_SYNC=true in your settings?`
);
} }
// could error check here, but better to just not display an image... // could error check here, but better to just not display an image...
// if (typeof destinationUrl === "undefined") { // if (typeof destinationUrl === "undefined") {

View File

@@ -0,0 +1,15 @@
export function formatHumanReadableDate(date: Date | string): string {
const d = typeof date === "string" ? new Date(date) : date;
if (isNaN(d.getTime())) return "Invalid date";
const options: Intl.DateTimeFormatOptions = {
weekday: "short",
// year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
};
return d.toLocaleString(undefined, options);
}