Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development: Upgrade markdown library to markdown-it #9354

Merged
merged 45 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
11cdb16
`Development`: Upgrade markdown library to markdown-it
Strohgelaender Sep 22, 2024
fbe63f7
fix plantUML rendering
Strohgelaender Sep 22, 2024
4be890e
fix tables
Strohgelaender Sep 22, 2024
f791e11
remove showdown
Strohgelaender Sep 22, 2024
70c6e01
Improve comments
Strohgelaender Sep 22, 2024
279b1e6
update typing
Strohgelaender Sep 22, 2024
185cbb4
fix client test
Strohgelaender Sep 22, 2024
5527c89
remove newline
Strohgelaender Sep 22, 2024
6c2457e
fix problem statement test
Strohgelaender Sep 22, 2024
288ba01
fix text unit test
Strohgelaender Sep 22, 2024
aa74d48
fix task linebreak
Strohgelaender Sep 22, 2024
c6e771c
Merge branch 'develop' of https://github.com/ls1intum/Artemis into ch…
Strohgelaender Sep 22, 2024
1bb82dc
use different markdown library
Strohgelaender Sep 22, 2024
9e6bdf0
Add inline migrator plugin
Strohgelaender Sep 22, 2024
6d7c856
remove console log
Strohgelaender Sep 22, 2024
762bf2e
fix matrix representation
Strohgelaender Sep 22, 2024
e924b21
AI review
Strohgelaender Sep 22, 2024
8ff9acb
reuse turndown service object
Strohgelaender Sep 23, 2024
cf16fb8
refactor plugin, add client test
Strohgelaender Sep 23, 2024
faa3137
Update src/main/webapp/app/shared/markdown-editor/extensions/ArtemisT…
Strohgelaender Sep 23, 2024
c423142
AI review
Strohgelaender Sep 23, 2024
578b49e
Merge remote-tracking branch 'origin/chore/markdown-it-replacement' i…
Strohgelaender Sep 23, 2024
2449de5
Add comment
Strohgelaender Sep 23, 2024
32c841d
Update markdown playground link
Strohgelaender Sep 25, 2024
8aeb9ec
upgrade documentation
Strohgelaender Sep 25, 2024
213d581
Update docs/user/markdown-support.rst
Strohgelaender Sep 25, 2024
7e9f3d8
typo
Strohgelaender Sep 26, 2024
8d938b6
Merge remote-tracking branch 'origin/chore/markdown-it-replacement' i…
Strohgelaender Sep 26, 2024
fbc0040
merge
Strohgelaender Sep 30, 2024
eeee062
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 2, 2024
94a12c1
merge
Strohgelaender Oct 4, 2024
9c4da0c
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 4, 2024
d4fc56f
merge
Strohgelaender Oct 6, 2024
29c233b
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 9, 2024
c990c4d
Merge branch 'develop' of https://github.com/ls1intum/Artemis into ch…
Strohgelaender Oct 11, 2024
6cd7753
merge
Strohgelaender Oct 13, 2024
9debc24
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 13, 2024
2a0c146
Merge branch 'develop' into chore/markdown-it-replacement
krusche Oct 13, 2024
401abab
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 15, 2024
891293c
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 16, 2024
1353039
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 18, 2024
1393b0d
merge
Strohgelaender Oct 21, 2024
0e62c1e
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 23, 2024
72f95c7
Merge branch 'develop' into chore/markdown-it-replacement
Strohgelaender Oct 24, 2024
2a5acb6
merge
Strohgelaender Oct 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@
"react-is",
"rfdc",
"shallowequal",
"showdown-highlight",
"showdown-katex",
"showdown",
"markdown-it-class",
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
"smoothscroll-polyfill",
"sockjs-client",
"use-sync-external-store/shim",
Expand Down
221 changes: 125 additions & 96 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@swimlane/ngx-charts": "20.5.0",
"@swimlane/ngx-graph": "8.4.0",
"@vscode/codicons": "0.0.36",
"@vscode/markdown-it-katex": "1.1.0",
"bootstrap": "5.3.3",
"compare-versions": "6.1.1",
"core-js": "3.38.1",
Expand All @@ -57,6 +58,9 @@
"js-video-url-parser": "0.5.1",
"jszip": "3.10.1",
"lodash-es": "4.17.21",
"markdown-it": "14.1.0",
"markdown-it-class": "1.0.0",
"markdown-it-highlightjs": "4.2.0",
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
"mobile-drag-drop": "3.0.0-rc.0",
"monaco-editor": "0.52.0",
"ngx-infinite-scroll": "18.0.0",
Expand All @@ -65,15 +69,13 @@
"pdfjs-dist": "4.6.82",
"posthog-js": "1.163.0",
"rxjs": "7.8.1",
"showdown": "2.1.0",
"showdown-highlight": "3.1.0",
"showdown-katex": "0.6.0",
"simple-statistics": "7.8.5",
"smoothscroll-polyfill": "0.4.4",
"sockjs-client": "1.6.1",
"split.js": "1.6.5",
"ts-cacheable": "1.0.10",
"tslib": "2.7.0",
"turndown": "7.2.0",
"uuid": "10.0.0",
"webstomp-client": "1.2.6",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
Expand All @@ -100,13 +102,9 @@
"@typescript-eslint/eslint-plugin": "^8.6.0"
},
"jsdom": "25.0.0",
"katex": "0.16.11",
"postcss": "8.4.47",
"rimraf": "6.0.1",
"semver": "7.6.3",
"showdown-katex": {
"showdown": "2.1.0"
},
"tough-cookie": "5.0.0",
"vite": "5.4.6",
"webpack-dev-middleware": "7.4.2",
Expand All @@ -131,11 +129,12 @@
"@types/dompurify": "3.0.5",
"@types/jest": "29.5.13",
"@types/lodash-es": "4.17.12",
"@types/markdown-it": "14.1.2",
"@types/node": "22.5.5",
"@types/papaparse": "5.3.14",
"@types/showdown": "2.0.6",
"@types/smoothscroll-polyfill": "0.3.4",
"@types/sockjs-client": "1.5.4",
"@types/turndown": "5.0.5",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.6.0",
"@typescript-eslint/parser": "8.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Injectable } from '@angular/core';
import { ProgrammingExerciseTestCase } from 'app/entities/programming/programming-exercise-test-case.model';
import { ArtemisTextReplacementExtension } from 'app/shared/markdown-editor/extensions/ArtemisTextReplacementExtension';
import { escapeStringForUseInRegex } from 'app/shared/util/global.utils';
import { Subject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { escapeStringForUseInRegex } from 'app/shared/util/global.utils';
import { ProgrammingExerciseInstructionService, TestCaseState } from 'app/exercises/programming/shared/instructions-render/service/programming-exercise-instruction.service';
import { ProgrammingExercisePlantUmlService } from 'app/exercises/programming/shared/instructions-render/service/programming-exercise-plant-uml.service';
import { ArtemisShowdownExtensionWrapper } from 'app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper';
import { Result } from 'app/entities/result.model';
import { ShowdownExtension } from 'showdown';
import DOMPurify from 'dompurify';

// This regex is the same as in the server: ProgrammingExerciseTaskService.java
const testsColorRegex = /testsColor\((\s*[^()\s]+(\([^()]*\))?)\)/g;

@Injectable({ providedIn: 'root' })
export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowdownExtensionWrapper {
export class ProgrammingExercisePlantUmlExtensionWrapper extends ArtemisTextReplacementExtension {
private latestResult?: Result;
private testCases?: ProgrammingExerciseTestCase[];
private injectableElementsFoundSubject = new Subject<() => void>();
Expand All @@ -25,7 +24,9 @@ export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowd
constructor(
private programmingExerciseInstructionService: ProgrammingExerciseInstructionService,
private plantUmlService: ProgrammingExercisePlantUmlService,
) {}
) {
super();
}

/**
* Sets latest result according to parameter.
Expand Down Expand Up @@ -67,7 +68,6 @@ export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowd
}

/**
* Creates and returns an extension to current exercise.
* The extension provides a custom rendering mechanism for embedded plantUml diagrams.
* The mechanism works as follows:
* 1) Find (multiple) embedded plantUml diagrams based on a regex (startuml, enduml).
Expand All @@ -76,55 +76,49 @@ export class ProgrammingExercisePlantUmlExtensionWrapper implements ArtemisShowd
* 4) Send the plantUml content to the server for rendering a svg (the result will be cached for performance reasons)
* 5) Inject the computed svg for the plantUml (from the server) into the plantUml div container based on the unique placeholder id (see step 2)
*/
getExtension() {
const extension: ShowdownExtension = {
type: 'lang',
filter: (text: string) => {
const idPlaceholder = '%idPlaceholder%';
// E.g. [task][Implement BubbleSort](testBubbleSort)
const plantUmlRegex = /@startuml([^@]*)@enduml/g;
// E.g. Implement BubbleSort, testBubbleSort
const plantUmlContainer = `<div class="mb-4" id="plantUml-${idPlaceholder}"></div>`;
// Replace test status markers.
const plantUmls = text.match(plantUmlRegex) ?? [];
// Assign unique ids to uml data structure at the beginning.
const plantUmlsIndexed = plantUmls.map((plantUml) => {
const nextIndex = this.plantUmlIndex;
// increase the global unique index so that the next plantUml gets a unique global id
this.plantUmlIndex++;
return { plantUmlId: nextIndex, plantUml };
});
// custom markdown to html rendering: replace the plantUml in the markdown with a simple <div></div> container with a unique id placeholder
// with the global unique id so that we can find the plantUml later on, when it was rendered, and then inject the 'actual' inner html (actually a svg image)
const replacedText = plantUmlsIndexed.reduce((acc: string, umlIndexed: { plantUmlId: number; plantUml: string }): string => {
return acc.replace(new RegExp(escapeStringForUseInRegex(umlIndexed.plantUml), 'g'), plantUmlContainer.replace(idPlaceholder, umlIndexed.plantUmlId.toString()));
}, text);
// before we send the plantUml to the server for rendering, we need to inject the current test status so that the colors can be adapted
// (green == implemented, red == not yet implemented, grey == unknown)
const plantUmlsValidated = plantUmlsIndexed.map((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => {
plantUmlIndexed.plantUml = plantUmlIndexed.plantUml.replace(testsColorRegex, (match: any, capture: string) => {
const tests = this.programmingExerciseInstructionService.convertTestListToIds(capture, this.testCases);
const { testCaseState } = this.programmingExerciseInstructionService.testStatusForTask(tests, this.latestResult);
switch (testCaseState) {
case TestCaseState.SUCCESS:
return 'green';
case TestCaseState.FAIL:
return 'red';
default:
return 'grey';
}
});
return plantUmlIndexed;
});
// send the adapted plantUml to the server for rendering and inject the result into the html DOM based on the unique plantUml id
this.injectableElementsFoundSubject.next(() => {
plantUmlsValidated.forEach((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => {
this.loadAndInjectPlantUml(plantUmlIndexed.plantUml, plantUmlIndexed.plantUmlId);
});
});
return replacedText;
},
};
return extension;
replaceText(text: string): string {
const idPlaceholder = '%idPlaceholder%';
// E.g. [task][Implement BubbleSort](testBubbleSort)
const plantUmlRegex = /@startuml([^@]*)@enduml/g;
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
// E.g. Implement BubbleSort, testBubbleSort
const plantUmlContainer = `<div class="mb-4" id="plantUml-${idPlaceholder}"></div>`;
// Replace test status markers.
const plantUmls = text.match(plantUmlRegex) ?? [];
// Assign unique ids to uml data structure at the beginning.
const plantUmlsIndexed = plantUmls.map((plantUml) => {
const nextIndex = this.plantUmlIndex;
// increase the global unique index so that the next plantUml gets a unique global id
this.plantUmlIndex++;
return { plantUmlId: nextIndex, plantUml };
});
// custom markdown to html rendering: replace the plantUml in the markdown with a simple <div></div> container with a unique id placeholder
// with the global unique id so that we can find the plantUml later on, when it was rendered, and then inject the 'actual' inner html (actually a svg image)
const replacedText = plantUmlsIndexed.reduce((acc: string, umlIndexed: { plantUmlId: number; plantUml: string }): string => {
return acc.replace(new RegExp(escapeStringForUseInRegex(umlIndexed.plantUml), 'g'), plantUmlContainer.replace(idPlaceholder, umlIndexed.plantUmlId.toString()));
}, text);
// before we send the plantUml to the server for rendering, we need to inject the current test status so that the colors can be adapted
// (green == implemented, red == not yet implemented, grey == unknown)
const plantUmlsValidated = plantUmlsIndexed.map((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => {
plantUmlIndexed.plantUml = plantUmlIndexed.plantUml.replace(testsColorRegex, (match: any, capture: string) => {
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
const tests = this.programmingExerciseInstructionService.convertTestListToIds(capture, this.testCases);
const { testCaseState } = this.programmingExerciseInstructionService.testStatusForTask(tests, this.latestResult);
switch (testCaseState) {
case TestCaseState.SUCCESS:
return 'green';
case TestCaseState.FAIL:
return 'red';
default:
return 'grey';
}
});
return plantUmlIndexed;
});
// send the adapted plantUml to the server for rendering and inject the result into the html DOM based on the unique plantUml id
this.injectableElementsFoundSubject.next(() => {
plantUmlsValidated.forEach((plantUmlIndexed: { plantUmlId: number; plantUml: string }) => {
this.loadAndInjectPlantUml(plantUmlIndexed.plantUml, plantUmlIndexed.plantUmlId);
});
});
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
return replacedText;
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Injectable, ViewContainerRef } from '@angular/core';
import { TaskArrayWithExercise } from 'app/exercises/programming/shared/instructions-render/task/programming-exercise-task.model';
import { ArtemisShowdownExtensionWrapper } from 'app/shared/markdown-editor/extensions/artemis-showdown-extension-wrapper';
import { ArtemisTextReplacementExtension } from 'app/shared/markdown-editor/extensions/ArtemisTextReplacementExtension';
import { Observable, Subject } from 'rxjs';
import { ShowdownExtension } from 'showdown';

/**
* Regular expression for finding tasks.
Expand All @@ -18,15 +17,12 @@ import { ShowdownExtension } from 'showdown';
export const taskRegex = /\[task]\[([^[\]]+)]\(((?:[^(),]+(?:\([^()]*\)[^(),]*)?(?:,[^(),]+(?:\([^()]*\)[^(),]*)?)*)?)\)/g;

@Injectable({ providedIn: 'root' })
export class ProgrammingExerciseTaskExtensionWrapper implements ArtemisShowdownExtensionWrapper {
export class ProgrammingExerciseTaskExtensionWrapper extends ArtemisTextReplacementExtension {
// We don't have a provider for ViewContainerRef, so we pass it from ProgrammingExerciseInstructionComponent
viewContainerRef: ViewContainerRef;

private testsForTaskSubject = new Subject<TaskArrayWithExercise>();
private injectableElementsFoundSubject = new Subject<() => void>();

constructor() {}

/**
* Subscribes to injectableElementsFoundSubject.
*/
Expand All @@ -35,23 +31,12 @@ export class ProgrammingExerciseTaskExtensionWrapper implements ArtemisShowdownE
}

/**
* Creates and returns an extension to current exercise.
* The task regex is coupled to the value used in ProgrammingExerciseTaskService in the server and
* `TaskCommand` in the client
* The task regex is coupled to the value used in ProgrammingExerciseTaskService in the server
* and `TaskCommand` in the client
* If you change the regex, make sure to change it in all places!
*/
getExtension() {
const extension: ShowdownExtension = {
type: 'lang',
filter: (problemStatement: string) => {
return this.createTasks(problemStatement);
},
};
return extension;
}

public createTasks(problemStatement: string): string {
return problemStatement.replace(taskRegex, (match) => {
replaceText(text: string): string {
return text.replace(taskRegex, (match) => {
return this.escapeTaskSpecialCharactersForMarkdown(match);
});
}
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ThemeService } from 'app/core/theme/theme.service';
import { ProgrammingExerciseTestCase } from 'app/entities/programming/programming-exercise-test-case.model';
import { ProgrammingExerciseGradingService } from 'app/exercises/programming/manage/services/programming-exercise-grading.service';
import { ShowdownExtension } from 'showdown';
import type { PluginSimple } from 'markdown-it';
import { catchError, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { Observable, Subscription, merge, of } from 'rxjs';
import { ProgrammingExercise } from 'app/entities/programming/programming-exercise.model';
Expand Down Expand Up @@ -80,7 +80,7 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes
public renderedMarkdown: SafeHtml;
private injectableContentForMarkdownCallbacks: Array<() => void> = [];

markdownExtensions: ShowdownExtension[];
markdownExtensions: PluginSimple[];
private injectableContentFoundSubscription: Subscription;
private tasksSubscription: Subscription;
private generateHtmlSubscription: Subscription;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export class ShortAnswerQuestionUtil {
if (firstWord === '') {
continue;
}

const firstWordIndex = element.indexOf(firstWord);
const whitespace = '&nbsp;'.repeat(this.getIndentation(originalTextParts[i][0]).length);
formattedTextParts[i][0] = [element.substring(0, firstWordIndex), whitespace, element.substring(firstWordIndex).trim()].join('');
Expand Down
9 changes: 2 additions & 7 deletions src/main/webapp/app/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
declare module 'showdown-katex' {
const main: () => ShowDownExtension;
export = main;
}

declare module 'showdown-highlight' {
const main: ({ pre: boolean }) => ShowDownExtension;
declare module 'markdown-it-class' {
const main: (md: MarkdownIt) => void;
export = main;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type MarkdownIt from 'markdown-it';
import type { PluginSimple } from 'markdown-it';

/**
* Markdown-It extension that allows replacing text in the raw markdown before tokenizing.
*/
export abstract class ArtemisTextReplacementExtension {
getExtension(): PluginSimple {
return (md: MarkdownIt): void => {
// Override the `render` method to process the raw Markdown text before tokenizing
const originalRender = md.render.bind(md);
md.render = (markdownText: string, ...args) => {
// Perform the replacement on the raw markdown text
const modifiedText = this.replaceText(markdownText);
// Call the original render method with the modified text
return originalRender(modifiedText, ...args);
};
};
}

abstract replaceText(text: string): string;
}

This file was deleted.

8 changes: 4 additions & 4 deletions src/main/webapp/app/shared/markdown.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { addCSSClass, htmlForMarkdown } from 'app/shared/util/markdown.conversion.util';
import showdown from 'showdown';
import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util';
import type { PluginSimple } from 'markdown-it';

@Injectable({ providedIn: 'root' })
export class ArtemisMarkdownService {
Expand All @@ -18,14 +18,14 @@ export class ArtemisMarkdownService {
*/
safeHtmlForMarkdown(
markdownText?: string,
extensions: showdown.ShowdownExtension[] = [],
extensions: PluginSimple[] = [],
Strohgelaender marked this conversation as resolved.
Show resolved Hide resolved
allowedHtmlTags: string[] | undefined = undefined,
allowedHtmlAttributes: string[] | undefined = undefined,
): SafeHtml {
if (!markdownText || markdownText === '') {
return '';
}
const convertedString = htmlForMarkdown(markdownText, [...extensions, ...addCSSClass], allowedHtmlTags, allowedHtmlAttributes);
const convertedString = htmlForMarkdown(markdownText, extensions, allowedHtmlTags, allowedHtmlAttributes);
return this.sanitizer.bypassSecurityTrustHtml(convertedString);
}

Expand Down
Loading
Loading