mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
mermaid ink image support
This commit is contained in:
@@ -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
8
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
15
src/services/utils/dateFormat.ts
Normal file
15
src/services/utils/dateFormat.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user