passing settings into markdown rendering

This commit is contained in:
2024-11-19 14:14:08 -07:00
parent e2eb63660d
commit cf200dab7f
19 changed files with 104 additions and 35 deletions

View File

@@ -2,4 +2,8 @@
- [ ] check out trpc
- <https://brockherion.dev/blog/posts/how-to-use-trpc-with-nextjs-app-router/>
- <https://trpc.io/docs/client/react/server-components>
- <https://trpc.io/docs/client/react/server-components>
lecture link prefectch false

View File

@@ -3,6 +3,7 @@ import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment";
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { CanvasQuiz } from "@/models/canvas/quizzes/canvasQuizModel";
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { LocalQuiz } from "@/models/local/quiz/localQuiz";
import {
@@ -17,10 +18,12 @@ export const getStatus = ({
item,
canvasItem,
type,
settings,
}: {
item: LocalQuiz | LocalAssignment | LocalCoursePage;
canvasItem?: CanvasQuiz | CanvasAssignment | CanvasPage;
type: "assignment" | "page" | "quiz";
settings: LocalCourseSettings;
}): {
status: "localOnly" | "incomplete" | "published";
message: ReactNode;
@@ -102,7 +105,7 @@ export const getStatus = ({
};
const htmlIsSame = htmlIsCloseEnough(
markdownToHTMLSafe(assignment.description),
markdownToHTMLSafe(assignment.description, settings),
canvasAssignment.description
);
if (!htmlIsSame)

View File

@@ -12,8 +12,10 @@ import {
import { ReactNode } from "react";
import { useCalendarItemsContext } from "../../context/calendarItemsContext";
import { getStatus } from "./getStatus";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
export function useTodaysItems(day: string) {
const [settings] = useLocalCourseSettingsQuery();
const dayAsDate = getDateFromStringOrThrow(
day,
"calculating same month in day items"
@@ -43,6 +45,7 @@ export function useTodaysItems(day: string) {
item: assignment,
canvasItem: canvasAssignment,
type: "assignment",
settings,
}),
};
})
@@ -65,6 +68,7 @@ export function useTodaysItems(day: string) {
item: quiz,
canvasItem: canvasQuiz,
type: "quiz",
settings,
}),
};
})
@@ -87,6 +91,7 @@ export function useTodaysItems(day: string) {
item: page,
canvasItem: canvasPage,
type: "page",
settings,
}),
};
})

View File

@@ -1,7 +1,9 @@
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { Lecture } from "@/models/local/lecture";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
export default function LecturePreview({ lecture }: { lecture: Lecture }) {
const [settings] = useLocalCourseSettingsQuery();
return (
<>
<section className="border-b-slate-700 border-b-4">
@@ -12,7 +14,7 @@ export default function LecturePreview({ lecture }: { lecture: Lecture }) {
<div
className="markdownPreview text-xl"
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(lecture.content),
__html: markdownToHTMLSafe(lecture.content, settings),
}}
></div>
</section>

View File

@@ -16,7 +16,6 @@ export default function LecturePreviewPage({
const lecture = weeks
.flatMap(({ lectures }) => lectures.map((lecture) => lecture))
.find((l) => l.date === lectureDay);
console.log(lecture);
if (!lecture) {
return <div>lecture not found for day</div>;

View File

@@ -1,3 +1,4 @@
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { LocalAssignment } from "@/models/local/assignment/localAssignment";
import { rubricItemIsExtraCredit } from "@/models/local/assignment/rubricItem";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
@@ -8,6 +9,7 @@ export default function AssignmentPreview({
}: {
assignment: LocalAssignment;
}) {
const [settings] = useLocalCourseSettingsQuery();
const totalPoints = assignment.rubric.reduce(
(sum, cur) => (rubricItemIsExtraCredit(cur) ? sum : sum + cur.points),
0
@@ -59,7 +61,7 @@ export default function AssignmentPreview({
<div
className="markdownPreview"
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(assignment.description),
__html: markdownToHTMLSafe(assignment.description, settings),
}}
></div>
</section>

View File

@@ -1,13 +1,15 @@
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
import React from "react";
export default function PagePreview({ page }: { page: LocalCoursePage }) {
const [settings] = useLocalCourseSettingsQuery();
return (
<div
className="markdownPreview"
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(page.text),
__html: markdownToHTMLSafe(page.text, settings),
}}
></div>
);

View File

@@ -1,4 +1,5 @@
import CheckIcon from "@/components/icons/CheckIcon";
import { useLocalCourseSettingsQuery } from "@/hooks/localCourse/localCoursesHooks";
import { useQuizQuery } from "@/hooks/localCourse/quizHooks";
import {
LocalQuizQuestion,
@@ -14,6 +15,7 @@ export default function QuizPreview({
moduleName: string;
}) {
const [quiz] = useQuizQuery(moduleName, quizName);
const [settings] = useLocalCourseSettingsQuery();
return (
<div style={{ overflow: "scroll", height: "100%" }}>
<div className="columns-2">
@@ -53,7 +55,7 @@ export default function QuizPreview({
<div
className="p-3 markdownPreview"
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(quiz.description),
__html: markdownToHTMLSafe(quiz.description, settings),
}}
></div>
<div className="p-3 rounded-md bg-slate-950 m-5 flex flex-col gap-3">
@@ -75,6 +77,7 @@ export default function QuizPreview({
}
function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
const [settings] = useLocalCourseSettingsQuery();
return (
<div className="rounded bg-slate-900 px-2">
<div className="flex flex-row justify-between text-slate-400">
@@ -86,7 +89,7 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
<div
className="ms-4 mb-2 markdownPreview"
dangerouslySetInnerHTML={{ __html: markdownToHTMLSafe(question.text) }}
dangerouslySetInnerHTML={{ __html: markdownToHTMLSafe(question.text, settings) }}
></div>
{question.questionType === QuestionType.MATCHING && (
<div>
@@ -128,7 +131,7 @@ function QuizQuestionPreview({ question }: { question: LocalQuizQuestion }) {
<div
className="markdownQuizAnswerPreview"
dangerouslySetInnerHTML={{
__html: markdownToHTMLSafe(answer.text),
__html: markdownToHTMLSafe(answer.text, settings),
}}
/>
</div>

View File

@@ -104,6 +104,7 @@ export default function NewCourseForm() {
return { ...groupWithoutCanvas, canvasId: undefined };
}
),
assets: [],
}
: {
name: selectedDirectory,
@@ -120,6 +121,7 @@ export default function NewCourseForm() {
defaultFileUploadTypes: ["pdf", "png", "jpg", "jpeg"],
defaultLockHoursOffset: 0,
holidays: [],
assets: [],
};
await createCourse.mutateAsync({
settings: newSettings,

View File

@@ -22,6 +22,7 @@ export const useCanvasAssignmentsQuery = () => {
});
};
export const useAddAssignmentToCanvasMutation = () => {
const [settings] = useLocalCourseSettingsQuery();
const { data: canvasModules } = useCanvasModulesQuery();
@@ -44,9 +45,11 @@ export const useAddAssignmentToCanvasMutation = () => {
const assignmentGroup = settings.assignmentGroups.find(
(g) => g.name === assignment.localAssignmentGroupName
);
const canvasAssignmentId = await canvasAssignmentService.create(
settings.canvasId,
assignment,
settings,
assignmentGroup?.canvasId
);
const canvasModule = canvasModules.find((c) => c.name === moduleName);
@@ -89,6 +92,7 @@ export const useUpdateAssignmentInCanvasMutation = () => {
settings.canvasId,
canvasAssignmentId,
assignment,
settings,
assignmentGroup?.canvasId
);
},

View File

@@ -44,7 +44,7 @@ export const useCreateCanvasPageMutation = () => {
}
const canvasPage = await canvasPageService.create(
settings.canvasId,
page
page,settings
);
const canvasModule = canvasModules.find((c) => c.name === moduleName);
@@ -78,7 +78,7 @@ export const useUpdateCanvasPageMutation = () => {
}: {
page: LocalCoursePage;
canvasPageId: number;
}) => canvasPageService.update(settings.canvasId, canvasPageId, page),
}) => canvasPageService.update(settings.canvasId, canvasPageId, page, settings),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: canvasPageKeys.pagesInCourse(settings.canvasId),

View File

@@ -50,6 +50,7 @@ export const useAddQuizToCanvasMutation = () => {
const canvasQuizId = await canvasQuizService.create(
settings.canvasId,
quiz,
settings,
assignmentGroup?.canvasId
);

View File

@@ -7,7 +7,6 @@ import {
LocalAssignmentGroup,
zodLocalAssignmentGroup,
} from "./assignment/localAssignmentGroup";
import { LocalModule } from "./localModules";
import { parse, stringify } from "yaml";
// export interface LocalCourse {
@@ -59,6 +58,10 @@ export interface LocalCourseSettings {
name: string;
days: string[];
}[];
assets: {
sourceUrl: string;
canvasUrl: string;
}[];
}
export const zodLocalCourseSettings = z.object({
@@ -78,6 +81,12 @@ export const zodLocalCourseSettings = z.object({
days: z.string().array(),
})
.array(),
assets: z
.object({
sourceUrl: z.string(),
canvasUrl: z.string(),
})
.array(),
});
export function getDayOfWeek(date: Date): DayOfWeek {

View File

@@ -7,6 +7,7 @@ import { CanvasRubricCreationResponse } from "@/models/canvas/assignments/canvas
import { assignmentPoints } from "@/models/local/assignment/utils/assignmentPointsUtils";
import { getDateFromString } from "@/models/local/utils/timeUtils";
import { getRubricCriterion } from "./canvasRubricUtils";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export const canvasAssignmentService = {
async getAll(courseId: number): Promise<CanvasAssignment[]> {
@@ -23,6 +24,7 @@ export const canvasAssignmentService = {
async create(
canvasCourseId: number,
localAssignment: LocalAssignment,
settings: LocalCourseSettings,
canvasAssignmentGroupId?: number
) {
console.log(`Creating assignment: ${localAssignment.name}`);
@@ -36,7 +38,7 @@ export const canvasAssignmentService = {
allowed_extensions: localAssignment.allowedFileUploadExtensions.map(
(e) => e.toString()
),
description: markdownToHTMLSafe(localAssignment.description),
description: markdownToHTMLSafe(localAssignment.description, settings),
due_at: getDateFromString(localAssignment.dueAt)?.toISOString(),
lock_at:
localAssignment.lockAt &&
@@ -58,6 +60,7 @@ export const canvasAssignmentService = {
courseId: number,
canvasAssignmentId: number,
localAssignment: LocalAssignment,
settings: LocalCourseSettings,
canvasAssignmentGroupId?: number
) {
console.log(`Updating assignment: ${localAssignment.name}`);
@@ -71,7 +74,7 @@ export const canvasAssignmentService = {
allowed_extensions: localAssignment.allowedFileUploadExtensions.map(
(e) => e.toString()
),
description: markdownToHTMLSafe(localAssignment.description),
description: markdownToHTMLSafe(localAssignment.description, settings),
due_at: getDateFromString(localAssignment.dueAt)?.toISOString(),
lock_at:
localAssignment.lockAt &&

View File

@@ -4,6 +4,7 @@ import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
import { markdownToHTMLSafe } from "../htmlMarkdownUtils";
import { axiosClient } from "../axiosUtils";
import { rateLimitAwareDelete } from "./canvasWebRequestor";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export const canvasPageService = {
async getAll(courseId: number): Promise<CanvasPage[]> {
@@ -17,14 +18,15 @@ export const canvasPageService = {
async create(
canvasCourseId: number,
page: LocalCoursePage
page: LocalCoursePage,
settings: LocalCourseSettings
): Promise<CanvasPage> {
console.log(`Creating course page: ${page.name}`);
const url = `${canvasApi}/courses/${canvasCourseId}/pages`;
const body = {
wiki_page: {
title: page.name,
body: markdownToHTMLSafe(page.text),
body: markdownToHTMLSafe(page.text, settings),
},
};
@@ -38,14 +40,15 @@ export const canvasPageService = {
async update(
courseId: number,
canvasPageId: number,
page: LocalCoursePage
page: LocalCoursePage,
settings: LocalCourseSettings
): Promise<void> {
console.log(`Updating course page: ${page.name}`);
const url = `${canvasApi}/courses/${courseId}/pages/${canvasPageId}`;
const body = {
wiki_page: {
title: page.name,
body: markdownToHTMLSafe(page.text),
body: markdownToHTMLSafe(page.text, settings),
},
};
await axiosClient.put(url, body);

View File

@@ -10,8 +10,12 @@ import {
QuestionType,
} from "@/models/local/quiz/localQuizQuestion";
import { CanvasQuizQuestion } from "@/models/canvas/quizzes/canvasQuizQuestionModel";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
const getAnswers = (question: LocalQuizQuestion) => {
const getAnswers = (
question: LocalQuizQuestion,
settings: LocalCourseSettings
) => {
if (question.questionType === QuestionType.MATCHING)
return question.answers.map((a) => ({
answer_match_left: a.text,
@@ -19,7 +23,7 @@ const getAnswers = (question: LocalQuizQuestion) => {
}));
return question.answers.map((answer) => ({
answer_html: markdownToHTMLSafe(answer.text),
answer_html: markdownToHTMLSafe(answer.text, settings),
answer_weight: answer.correct ? 100 : 0,
}));
};
@@ -28,18 +32,19 @@ const createQuestionOnly = async (
canvasCourseId: number,
canvasQuizId: number,
question: LocalQuizQuestion,
position: number
position: number,
settings: LocalCourseSettings
) => {
console.log("Creating individual question"); //, question);
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/questions`;
const body = {
question: {
question_text: markdownToHTMLSafe(question.text),
question_text: markdownToHTMLSafe(question.text, settings),
question_type: `${question.questionType}_question`,
points_possible: question.points,
position,
answers: getAnswers(question),
answers: getAnswers(question, settings),
},
};
@@ -93,13 +98,20 @@ const hackFixRedundantAssignments = async (canvasCourseId: number) => {
const createQuizQuestions = async (
canvasCourseId: number,
canvasQuizId: number,
localQuiz: LocalQuiz
localQuiz: LocalQuiz,
settings: LocalCourseSettings
) => {
console.log("Creating quiz questions"); //, localQuiz);
const tasks = localQuiz.questions.map(
async (question, index) =>
await createQuestionOnly(canvasCourseId, canvasQuizId, question, index)
await createQuestionOnly(
canvasCourseId,
canvasQuizId,
question,
index,
settings
)
);
const questionAndPositions = await Promise.all(tasks);
await hackFixQuestionOrdering(
@@ -126,15 +138,17 @@ export const canvasQuizService = {
async create(
canvasCourseId: number,
localQuiz: LocalQuiz,
settings: LocalCourseSettings,
canvasAssignmentGroupId?: number
) {
console.log("Creating quiz", localQuiz);
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes`;
const body = {
quiz: {
title: localQuiz.name,
description: markdownToHTMLSafe(localQuiz.description),
description: markdownToHTMLSafe(localQuiz.description, settings),
shuffle_answers: localQuiz.shuffleAnswers,
access_code: localQuiz.password,
show_correct_answers: localQuiz.showCorrectAnswers,
@@ -158,7 +172,7 @@ export const canvasQuizService = {
};
const { data: canvasQuiz } = await axiosClient.post<CanvasQuiz>(url, body);
await createQuizQuestions(canvasCourseId, canvasQuiz.id, localQuiz);
await createQuizQuestions(canvasCourseId, canvasQuiz.id, localQuiz, settings);
return canvasQuiz.id;
},
async delete(canvasCourseId: number, canvasQuizId: number) {

View File

@@ -50,6 +50,9 @@ const populateDefaultValues = (settingsFromFile: LocalCourseSettings) => {
holidays: Array.isArray(settingsFromFile.holidays)
? settingsFromFile.holidays
: [],
assets: Array.isArray(settingsFromFile.assets)
? settingsFromFile.assets
: [],
};
return settings;
};

View File

@@ -1,18 +1,27 @@
"use client";
import { marked } from "marked";
// import markedKatex from "marked-katex-extension";
import * as DOMPurify from "isomorphic-dompurify";
import { LocalCourseSettings } from "@/models/local/localCourseSettings";
export function markdownToHTMLSafe(markdownString: string) {
// const options = {
// throwOnError: false,
// nonStandard: true
// };
// marked.use(markedKatex(options));
function extractImageSources(html: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const imgElements = doc.querySelectorAll("img");
const srcUrls = Array.from(imgElements).map((img) => img.src);
return srcUrls;
}
function handleImages(html: string, settings: LocalCourseSettings) {
const imageSources = extractImageSources(html);
console.log(imageSources);
}
export function markdownToHTMLSafe(
markdownString: string,
settings: LocalCourseSettings
) {
const clean = DOMPurify.sanitize(
marked.parse(markdownString, { async: false, pedantic: false, gfm: true })
);
handleImages(clean, settings);
return clean;
}

View File

@@ -31,6 +31,7 @@ describe("FileStorageTests", () => {
defaultAssignmentSubmissionTypes: [],
defaultFileUploadTypes: [],
holidays: [],
assets: []
};
await fileStorageService.settings.updateCourseSettings(name, settings);