chore(backend): Aanpassingen Dwengo Learning-Object-Repository
Processing uit Dwengo Learning-Object-Repository geconverteerd naar TypeScript en aangepast aan onze app. Functionaliteit van Dwengo Learning-Object-Repository in ons project gekopiëerd en deels aanBestanden die enkel types of interfaces exporteren hernoemd naar *.d.tsgepast aan TypeScript en ons project.
This commit is contained in:
parent
ba3da01d2d
commit
463c8c9fc0
45 changed files with 1258 additions and 3747 deletions
|
@ -17,13 +17,16 @@
|
||||||
"@mikro-orm/core": "^6.4.6",
|
"@mikro-orm/core": "^6.4.6",
|
||||||
"@mikro-orm/postgresql": "^6.4.6",
|
"@mikro-orm/postgresql": "^6.4.6",
|
||||||
"@mikro-orm/reflection": "^6.4.6",
|
"@mikro-orm/reflection": "^6.4.6",
|
||||||
|
"@mikro-orm/sqlite": "6.4.6",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.1",
|
||||||
"@mikro-orm/sqlite": "6.4.6",
|
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"uuid": "^11.1.0",
|
"gift-pegjs": "^1.0.2",
|
||||||
|
"isomorphic-dompurify": "^2.22.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"marked": "^15.0.7",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mikro-orm/cli": "^6.4.6",
|
"@mikro-orm/cli": "^6.4.6",
|
||||||
|
|
|
@ -5,6 +5,8 @@ import learningObjectService from "../services/learning-objects/learning-object-
|
||||||
import {EnvVars, getEnvVar} from "../util/envvars";
|
import {EnvVars, getEnvVar} from "../util/envvars";
|
||||||
import {Language} from "../entities/content/language";
|
import {Language} from "../entities/content/language";
|
||||||
import {BadRequestException} from "../exceptions";
|
import {BadRequestException} from "../exceptions";
|
||||||
|
import attachmentService from "../services/learning-objects/attachment-service";
|
||||||
|
import {NotFoundError} from "@mikro-orm/core";
|
||||||
|
|
||||||
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
|
function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier {
|
||||||
if (!req.params.hruid) {
|
if (!req.params.hruid) {
|
||||||
|
@ -60,3 +62,14 @@ export async function getLearningObjectHTML(req: Request, res: Response): Promis
|
||||||
const learningObject = await learningObjectService.getLearningObjectHTML(learningObjectId);
|
const learningObject = await learningObjectService.getLearningObjectHTML(learningObjectId);
|
||||||
res.send(learningObject);
|
res.send(learningObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAttachment(req: Request, res: Response): Promise<void> {
|
||||||
|
const learningObjectId = getLearningObjectIdentifierFromRequest(req);
|
||||||
|
const name = req.params.attachmentName;
|
||||||
|
const attachment = await attachmentService.getAttachment(learningObjectId, name);
|
||||||
|
|
||||||
|
if (!attachment) {
|
||||||
|
throw new NotFoundError(`Attachment ${name} not found`);
|
||||||
|
}
|
||||||
|
res.setHeader("Content-Type", attachment.mimeType).send(attachment.content)
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,40 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { Attachment } from '../../entities/content/attachment.entity.js';
|
import { Attachment } from '../../entities/content/attachment.entity.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import {Language} from "../../entities/content/language";
|
||||||
|
import {LearningObjectIdentifier} from "../../entities/content/learning-object-identifier";
|
||||||
|
|
||||||
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
export class AttachmentRepository extends DwengoEntityRepository<Attachment> {
|
||||||
public findByLearningObjectAndNumber(
|
public findByLearningObjectIdAndName(
|
||||||
learningObject: LearningObject,
|
learningObjectId: LearningObjectIdentifier,
|
||||||
sequenceNumber: number
|
name: string
|
||||||
) {
|
): Promise<Attachment | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
learningObject: learningObject,
|
learningObject: {
|
||||||
sequenceNumber: sequenceNumber,
|
hruid: learningObjectId.hruid,
|
||||||
|
language: learningObjectId.language,
|
||||||
|
version: learningObjectId.version,
|
||||||
|
},
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public findByMostRecentVersionOfLearningObjectAndName(
|
||||||
|
hruid: string,
|
||||||
|
language: Language,
|
||||||
|
attachmentName: string
|
||||||
|
): Promise<Attachment | null> {
|
||||||
|
return this.findOne({
|
||||||
|
learningObject: {
|
||||||
|
hruid: hruid,
|
||||||
|
language: language
|
||||||
|
},
|
||||||
|
name: attachmentName
|
||||||
|
}, {
|
||||||
|
orderBy: {
|
||||||
|
learningObject: {
|
||||||
|
version: 'DESC'
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// This repository is read-only for now since creating own learning object is an extension feature.
|
// This repository is read-only for now since creating own learning object is an extension feature.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
|
import {Language} from "../../entities/content/language";
|
||||||
|
|
||||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||||
public findByIdentifier(
|
public findByIdentifier(
|
||||||
|
@ -12,5 +13,16 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
version: identifier.version,
|
version: identifier.version,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findLatestByHruidAndLanguage(hruid: string, language: Language) {
|
||||||
|
return this.findOne({
|
||||||
|
hruid: hruid,
|
||||||
|
language: language
|
||||||
|
}, {
|
||||||
|
orderBy: {
|
||||||
|
version: "DESC"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
// This repository is read-only for now since creating own learning object is an extension feature.
|
// This repository is read-only for now since creating own learning object is an extension feature.
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
|
||||||
import { LearningObject } from './learning-object.entity.js';
|
import { LearningObject } from './learning-object.entity.js';
|
||||||
|
import {AttachmentRepository} from "../../data/content/attachment-repository";
|
||||||
|
|
||||||
@Entity()
|
@Entity({repository: () => AttachmentRepository})
|
||||||
export class Attachment {
|
export class Attachment {
|
||||||
@ManyToOne({ entity: () => LearningObject, primary: true })
|
@ManyToOne({ entity: () => LearningObject, primary: true })
|
||||||
learningObject!: LearningObject;
|
learningObject!: LearningObject;
|
||||||
|
|
||||||
@PrimaryKey({ type: 'integer' })
|
@PrimaryKey({ type: 'string' })
|
||||||
sequenceNumber!: number;
|
name!: string;
|
||||||
|
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
mimeType!: string;
|
mimeType!: string;
|
||||||
|
|
|
@ -1,6 +1,186 @@
|
||||||
export enum Language {
|
export enum Language {
|
||||||
|
Afar = 'aa',
|
||||||
|
Abkhazian = 'ab',
|
||||||
|
Afrikaans = 'af',
|
||||||
|
Akan = 'ak',
|
||||||
|
Albanian = 'sq',
|
||||||
|
Amharic = 'am',
|
||||||
|
Arabic = 'ar',
|
||||||
|
Aragonese = 'an',
|
||||||
|
Armenian = 'hy',
|
||||||
|
Assamese = 'as',
|
||||||
|
Avaric = 'av',
|
||||||
|
Avestan = 'ae',
|
||||||
|
Aymara = 'ay',
|
||||||
|
Azerbaijani = 'az',
|
||||||
|
Bashkir = 'ba',
|
||||||
|
Bambara = 'bm',
|
||||||
|
Basque = 'eu',
|
||||||
|
Belarusian = 'be',
|
||||||
|
Bengali = 'bn',
|
||||||
|
Bihari = 'bh',
|
||||||
|
Bislama = 'bi',
|
||||||
|
Bosnian = 'bs',
|
||||||
|
Breton = 'br',
|
||||||
|
Bulgarian = 'bg',
|
||||||
|
Burmese = 'my',
|
||||||
|
Catalan = 'ca',
|
||||||
|
Chamorro = 'ch',
|
||||||
|
Chechen = 'ce',
|
||||||
|
Chinese = 'zh',
|
||||||
|
ChurchSlavic = 'cu',
|
||||||
|
Chuvash = 'cv',
|
||||||
|
Cornish = 'kw',
|
||||||
|
Corsican = 'co',
|
||||||
|
Cree = 'cr',
|
||||||
|
Czech = 'cs',
|
||||||
|
Danish = 'da',
|
||||||
|
Divehi = 'dv',
|
||||||
Dutch = 'nl',
|
Dutch = 'nl',
|
||||||
French = 'fr',
|
Dzongkha = 'dz',
|
||||||
English = 'en',
|
English = 'en',
|
||||||
Germany = 'de',
|
Esperanto = 'eo',
|
||||||
|
Estonian = 'et',
|
||||||
|
Ewe = 'ee',
|
||||||
|
Faroese = 'fo',
|
||||||
|
Fijian = 'fj',
|
||||||
|
Finnish = 'fi',
|
||||||
|
French = 'fr',
|
||||||
|
Frisian = 'fy',
|
||||||
|
Fulah = 'ff',
|
||||||
|
Georgian = 'ka',
|
||||||
|
German = 'de',
|
||||||
|
Gaelic = 'gd',
|
||||||
|
Irish = 'ga',
|
||||||
|
Galician = 'gl',
|
||||||
|
Manx = 'gv',
|
||||||
|
Greek = 'el',
|
||||||
|
Guarani = 'gn',
|
||||||
|
Gujarati = 'gu',
|
||||||
|
Haitian = 'ht',
|
||||||
|
Hausa = 'ha',
|
||||||
|
Hebrew = 'he',
|
||||||
|
Herero = 'hz',
|
||||||
|
Hindi = 'hi',
|
||||||
|
HiriMotu = 'ho',
|
||||||
|
Croatian = 'hr',
|
||||||
|
Hungarian = 'hu',
|
||||||
|
Igbo = 'ig',
|
||||||
|
Icelandic = 'is',
|
||||||
|
Ido = 'io',
|
||||||
|
SichuanYi = 'ii',
|
||||||
|
Inuktitut = 'iu',
|
||||||
|
Interlingue = 'ie',
|
||||||
|
Interlingua = 'ia',
|
||||||
|
Indonesian = 'id',
|
||||||
|
Inupiaq = 'ik',
|
||||||
|
Italian = 'it',
|
||||||
|
Javanese = 'jv',
|
||||||
|
Japanese = 'ja',
|
||||||
|
Kalaallisut = 'kl',
|
||||||
|
Kannada = 'kn',
|
||||||
|
Kashmiri = 'ks',
|
||||||
|
Kanuri = 'kr',
|
||||||
|
Kazakh = 'kk',
|
||||||
|
Khmer = 'km',
|
||||||
|
Kikuyu = 'ki',
|
||||||
|
Kinyarwanda = 'rw',
|
||||||
|
Kirghiz = 'ky',
|
||||||
|
Komi = 'kv',
|
||||||
|
Kongo = 'kg',
|
||||||
|
Korean = 'ko',
|
||||||
|
Kuanyama = 'kj',
|
||||||
|
Kurdish = 'ku',
|
||||||
|
Lao = 'lo',
|
||||||
|
Latin = 'la',
|
||||||
|
Latvian = 'lv',
|
||||||
|
Limburgan = 'li',
|
||||||
|
Lingala = 'ln',
|
||||||
|
Lithuanian = 'lt',
|
||||||
|
Luxembourgish = 'lb',
|
||||||
|
LubaKatanga = 'lu',
|
||||||
|
Ganda = 'lg',
|
||||||
|
Macedonian = 'mk',
|
||||||
|
Marshallese = 'mh',
|
||||||
|
Malayalam = 'ml',
|
||||||
|
Maori = 'mi',
|
||||||
|
Marathi = 'mr',
|
||||||
|
Malay = 'ms',
|
||||||
|
Malagasy = 'mg',
|
||||||
|
Maltese = 'mt',
|
||||||
|
Mongolian = 'mn',
|
||||||
|
Nauru = 'na',
|
||||||
|
Navajo = 'nv',
|
||||||
|
SouthNdebele = 'nr',
|
||||||
|
NorthNdebele = 'nd',
|
||||||
|
Ndonga = 'ng',
|
||||||
|
Nepali = 'ne',
|
||||||
|
NorwegianNynorsk = 'nn',
|
||||||
|
NorwegianBokmal = 'nb',
|
||||||
|
Norwegian = 'no',
|
||||||
|
Chichewa = 'ny',
|
||||||
|
Occitan = 'oc',
|
||||||
|
Ojibwa = 'oj',
|
||||||
|
Oriya = 'or',
|
||||||
|
Oromo = 'om',
|
||||||
|
Ossetian = 'os',
|
||||||
|
Punjabi = 'pa',
|
||||||
|
Persian = 'fa',
|
||||||
|
Pali = 'pi',
|
||||||
|
Polish = 'pl',
|
||||||
|
Portuguese = 'pt',
|
||||||
|
Pashto = 'ps',
|
||||||
|
Quechua = 'qu',
|
||||||
|
Romansh = 'rm',
|
||||||
|
Romanian = 'ro',
|
||||||
|
Rundi = 'rn',
|
||||||
|
Russian = 'ru',
|
||||||
|
Sango = 'sg',
|
||||||
|
Sanskrit = 'sa',
|
||||||
|
Sinhala = 'si',
|
||||||
|
Slovak = 'sk',
|
||||||
|
Slovenian = 'sl',
|
||||||
|
NorthernSami = 'se',
|
||||||
|
Samoan = 'sm',
|
||||||
|
Shona = 'sn',
|
||||||
|
Sindhi = 'sd',
|
||||||
|
Somali = 'so',
|
||||||
|
Sotho = 'st',
|
||||||
|
Spanish = 'es',
|
||||||
|
Sardinian = 'sc',
|
||||||
|
Serbian = 'sr',
|
||||||
|
Swati = 'ss',
|
||||||
|
Sundanese = 'su',
|
||||||
|
Swahili = 'sw',
|
||||||
|
Swedish = 'sv',
|
||||||
|
Tahitian = 'ty',
|
||||||
|
Tamil = 'ta',
|
||||||
|
Tatar = 'tt',
|
||||||
|
Telugu = 'te',
|
||||||
|
Tajik = 'tg',
|
||||||
|
Tagalog = 'tl',
|
||||||
|
Thai = 'th',
|
||||||
|
Tibetan = 'bo',
|
||||||
|
Tigrinya = 'ti',
|
||||||
|
Tonga = 'to',
|
||||||
|
Tswana = 'tn',
|
||||||
|
Tsonga = 'ts',
|
||||||
|
Turkmen = 'tk',
|
||||||
|
Turkish = 'tr',
|
||||||
|
Twi = 'tw',
|
||||||
|
Uighur = 'ug',
|
||||||
|
Ukrainian = 'uk',
|
||||||
|
Urdu = 'ur',
|
||||||
|
Uzbek = 'uz',
|
||||||
|
Venda = 've',
|
||||||
|
Vietnamese = 'vi',
|
||||||
|
Volapuk = 'vo',
|
||||||
|
Welsh = 'cy',
|
||||||
|
Walloon = 'wa',
|
||||||
|
Wolof = 'wo',
|
||||||
|
Xhosa = 'xh',
|
||||||
|
Yiddish = 'yi',
|
||||||
|
Yoruba = 'yo',
|
||||||
|
Zhuang = 'za',
|
||||||
|
Zulu = 'zu'
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
import { Language } from './language.js';
|
import { Language } from './language.js';
|
||||||
import { Attachment } from './attachment.entity.js';
|
import { Attachment } from './attachment.entity.js';
|
||||||
import { Teacher } from '../users/teacher.entity.js';
|
import { Teacher } from '../users/teacher.entity.js';
|
||||||
import {DwengoContentType} from "../../services/learning-objects/processing/content_type";
|
import {DwengoContentType} from "../../services/learning-objects/processing/content-type";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class LearningObject {
|
export class LearningObject {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import {
|
import {
|
||||||
getAllLearningObjects,
|
getAllLearningObjects, getAttachment,
|
||||||
getLearningObject, getLearningObjectHTML,
|
getLearningObject, getLearningObjectHTML,
|
||||||
} from '../controllers/learning-objects.js';
|
} from '../controllers/learning-objects.js';
|
||||||
|
|
||||||
|
@ -30,4 +30,10 @@ router.get('/:hruid', getLearningObject);
|
||||||
// Example: http://localhost:3000/learningObject/un_ai7/html
|
// Example: http://localhost:3000/learningObject/un_ai7/html
|
||||||
router.get('/:hruid/html', getLearningObjectHTML);
|
router.get('/:hruid/html', getLearningObjectHTML);
|
||||||
|
|
||||||
|
// Parameter: hruid of learning object, name of attachment.
|
||||||
|
// Query: language, version (optional).
|
||||||
|
// Route to get the raw data of the attachment for one learning object based on its hruid.
|
||||||
|
// Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png
|
||||||
|
router.get('/:hruid/html/:attachmentName', getAttachment);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
21
backend/src/services/learning-objects/attachment-service.ts
Normal file
21
backend/src/services/learning-objects/attachment-service.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import {getAttachmentRepository} from "../../data/repositories";
|
||||||
|
import {Attachment} from "../../entities/content/attachment.entity";
|
||||||
|
import {LearningObjectIdentifier} from "../../interfaces/learning-content";
|
||||||
|
|
||||||
|
const attachmentRepo = getAttachmentRepository();
|
||||||
|
|
||||||
|
const attachmentService = {
|
||||||
|
getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise<Attachment | null> {
|
||||||
|
if (learningObjectId.version) {
|
||||||
|
return attachmentRepo.findByLearningObjectIdAndName({
|
||||||
|
hruid: learningObjectId.hruid,
|
||||||
|
language: learningObjectId.language,
|
||||||
|
version: learningObjectId.version,
|
||||||
|
}, attachmentName);
|
||||||
|
} else {
|
||||||
|
return attachmentRepo.findByMostRecentVersionOfLearningObjectAndName(learningObjectId.hruid, learningObjectId.language, attachmentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default attachmentService;
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/audio/audio_processor.js
|
||||||
|
*/
|
||||||
|
import Processor from "../processor.js";
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import {type} from "node:os";
|
||||||
|
import {DwengoContentType} from "../content-type";
|
||||||
|
|
||||||
|
class AudioProcessor extends Processor<string> {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(DwengoContentType.AUDIO_MPEG);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderFn(audioUrl: string): string {
|
||||||
|
return DOMPurify.sanitize(`<audio controls>
|
||||||
|
<source src="${audioUrl}" type=${type}>
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioProcessor;
|
|
@ -1,82 +0,0 @@
|
||||||
/**
|
|
||||||
* Based on
|
|
||||||
*/
|
|
||||||
import Processor from "../processor.ts";
|
|
||||||
import { isValidHttpUrl } from '../../utils/utils.js'
|
|
||||||
import { findFile } from '../../utils/file_io.js'
|
|
||||||
import InvalidArgumentError from '../../utils/invalid_argument_error.js'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import ProcessingHistory from "../../models/processing_history.js";
|
|
||||||
import path from "path"
|
|
||||||
import fs from "fs"
|
|
||||||
|
|
||||||
class AudioProcessor extends Processor {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.types = ["audio/mpeg"] // TODO add functionality to accept other audio types (ogg, wav)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} audioUrl
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the AudioProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(audioUrl, args = { files: [], metadata: {} }) {
|
|
||||||
|
|
||||||
if ((!args.files || args.files.length <= 0 || !findFile(audioUrl, args.files)) && !isValidHttpUrl(audioUrl)) {
|
|
||||||
if (args.metadata && args.metadata.hruid && args.metadata.version && args.metadata.language){
|
|
||||||
ProcessingHistory.error(args.metadata.hruid, args.metadata.version, args.metadata.language, "The audio file cannot be found. Please check if the url is spelled correctly.")
|
|
||||||
}else{
|
|
||||||
ProcessingHistory.error("generalError", "99999999", "en", "The audio file cannot be found. Please check if the url is spelled correctly.")
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidArgumentError("The audio file cannot be found. Please check if the url is spelled correctly.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let type;
|
|
||||||
if (!args.metadata || !args.metadata.content_type || !this.types.includes(args.metadata.content_type)) {
|
|
||||||
type = this.types[0];
|
|
||||||
} else {
|
|
||||||
type = args.metadata.content_type;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidHttpUrl(audioUrl)) {
|
|
||||||
return DOMPurify.sanitize(`<audio controls>
|
|
||||||
<source src="${audioUrl}" type=${type}>
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.metadata._id) {
|
|
||||||
throw new InvalidArgumentError("The metadata for for the object which uses the file '" + audioUrl + "' is not loaded in the processor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return DOMPurify.sanitize(`<audio controls>
|
|
||||||
<source src="@@URL_REPLACE@@/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${args.metadata._id}/${audioUrl}" type=${type}>
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let args = {}
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext == ".mp3") {
|
|
||||||
inputString = f["originalname"]
|
|
||||||
// add files to args to check if file exists
|
|
||||||
args.files = files;
|
|
||||||
args.metadata = metadata
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [this.render(inputString, args), files]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AudioProcessor;
|
|
|
@ -1,83 +0,0 @@
|
||||||
import Processor from "../processor.ts";
|
|
||||||
import { isValidHttpUrl } from '../../utils/utils.js'
|
|
||||||
import InvalidArgumentError from '../../utils/invalid_argument_error.js'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import Logger from "../../logger.js";
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
let logger = Logger.getLogger()
|
|
||||||
class BlocklyProcessor extends Processor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.blockly_base_url = process.env.SIMULATOR_READONLY_BASE_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} blocklyXml
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the BlocklyProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(blocklyXml, args = { language: "nl", id: "" }, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) {
|
|
||||||
if (!args.language || args.language.trim() == "") {
|
|
||||||
args.language = "nl";
|
|
||||||
}
|
|
||||||
if (!args.id || args.id.trim() == "") {
|
|
||||||
throw new InvalidArgumentError("The unique object id must be passed to the blockly processor.");
|
|
||||||
}
|
|
||||||
let languages = ["aa", "ab", "af", "ak", "sq", "am", "ar", "an", "hy", "as", "av", "ae", "ay", "az", "ba", "bm", "eu", "be", "bn", "bh", "bi", "bs", "br", "bg", "my", "ca", "ch", "ce", "zh", "cu", "cv", "kw", "co", "cr", "cs", "da", "dv", "nl", "dz", "en", "eo", "et", "ee", "fo", "fj", "fi", "fr", "fy", "ff", "ka", "de", "gd", "ga", "gl", "gv", "el", "gn", "gu", "ht", "ha", "he", "hz", "hi", "ho", "hr", "hu", "ig", "is", "io", "ii", "iu", "ie", "ia", "id", "ik", "it", "jv", "ja", "kl", "kn", "ks", "kr", "kk", "km", "ki", "rw", "ky", "kv", "kg", "ko", "kj", "ku", "lo", "la", "lv", "li", "ln", "lt", "lb", "lu", "lg", "mk", "mh", "ml", "mi", "mr", "ms", "mg", "mt", "mn", "na", "nv", "nr", "nd", "ng", "ne", "nn", "nb", "no", "ny", "oc", "oj", "or", "om", "os", "pa", "fa", "pi", "pl", "pt", "ps", "qu", "rm", "ro", "rn", "ru", "sg", "sa", "si", "sk", "sl", "se", "sm", "sn", "sd", "so", "st", "es", "sc", "sr", "ss", "su", "sw", "sv", "ty", "ta", "tt", "te", "tg", "tl", "th", "bo", "ti", "to", "tn", "ts", "tk", "tr", "tw", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "cy", "wa", "wo", "xh", "yi", "yo", "za", "zu"];
|
|
||||||
if (!languages.includes(args.language)) {
|
|
||||||
throw new InvalidArgumentError("The language must be valid. " + args.language + " is not a supported language.")
|
|
||||||
}
|
|
||||||
if (typeof blocklyXml == 'undefined') {
|
|
||||||
throw new InvalidArgumentError("The blockly XML is undefined. Please provide correct XML code.")
|
|
||||||
}
|
|
||||||
|
|
||||||
let simulatorUrl = `${this.blockly_base_url}`
|
|
||||||
|
|
||||||
let form = `
|
|
||||||
<form action="${simulatorUrl}" method="post" id="blockly_form_${args.id}" target="blockly_iframe_${args.id}">
|
|
||||||
<input name="xml" type="hidden" value='${blocklyXml}'>
|
|
||||||
</form>
|
|
||||||
`
|
|
||||||
|
|
||||||
let iframe = `
|
|
||||||
<div class="iframe-container ${aspect_ratio}"><iframe name="blockly_iframe_${args.id}" height="530px" width="420px" allowfullscreen></iframe></div>
|
|
||||||
`
|
|
||||||
|
|
||||||
let code = `(function(){
|
|
||||||
var auto = setTimeout(function(){ submitform(); }, 50);
|
|
||||||
|
|
||||||
function submitform(){
|
|
||||||
document.forms["blockly_form_${args.id}"].submit();
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
`
|
|
||||||
|
|
||||||
let script = `<script>${code}</script>`
|
|
||||||
|
|
||||||
let html = form + iframe // DOMPurify.sanitize(form + iframe, {ALLOW_UNKNOWN_PROTOCOLS: true, ADD_TAGS: ["iframe", "xml"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'target']});
|
|
||||||
html = html + script;
|
|
||||||
|
|
||||||
return html; //TODO is not sanitized using DOMPurify.sanitize (problems with script tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let args = {}
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext == ".xml") {
|
|
||||||
inputString = f.buffer.toString('utf8');
|
|
||||||
args.language = metadata.language;
|
|
||||||
args.id = metadata._id;
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [this.render(inputString, args), files]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlocklyProcessor;
|
|
|
@ -1,67 +0,0 @@
|
||||||
import Logger from '../../logger.js';
|
|
||||||
import InvalidArgumentError from "../../utils/invalid_argument_error.js";
|
|
||||||
import { MarkdownProcessor } from '../markdown/markdown_processor.js';
|
|
||||||
|
|
||||||
class CTSchemaProcessor extends MarkdownProcessor{
|
|
||||||
logger = Logger.getLogger();
|
|
||||||
constructor(args = { files: [], metadata: {} }) {
|
|
||||||
super();
|
|
||||||
this.staticPath = `${process.env.DOMAIN_URL}${process.env.STATIC_BASE_PATH}/img/ct_schema/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} mdText a string containing the content for the four ct schema in markdown
|
|
||||||
* @returns The sanitized version of the generated html.
|
|
||||||
*/
|
|
||||||
render(text, args = {}) {
|
|
||||||
let html = "";
|
|
||||||
// 1. Split text into markdown parts for each CT aspect
|
|
||||||
// 2. Call super.render on the individual parts
|
|
||||||
// 3. Group the parts together with specific html structure
|
|
||||||
|
|
||||||
const regexObject = {
|
|
||||||
context: /<context>([\s\S]*?)<\/context>/,
|
|
||||||
decomp: /<decomposition>([\s\S]*?)<\/decomposition>/,
|
|
||||||
abstr: /<abstraction>([\s\S]*?)<\/abstraction>/,
|
|
||||||
pattern: /<patternRecognition>([\s\S]*?)<\/patternRecognition>/,
|
|
||||||
algo: /<algorithms>([\s\S]*?)<\/algorithms>/,
|
|
||||||
impl: /<implementation>([\s\S]*?)<\/implementation>/,
|
|
||||||
}
|
|
||||||
|
|
||||||
let htmlObject = {}
|
|
||||||
|
|
||||||
let htmlStructure = (valueObject) => `
|
|
||||||
<div class="ct_schema_container">
|
|
||||||
<div class="ct_context_container">${valueObject.context}</div>
|
|
||||||
<div class="ct_row1_container">
|
|
||||||
<div class="ct_decomposition_container">${valueObject.decomp}<div class="ct_logo"><img src="${this.staticPath + "decompositie.png"}"/></div></div>
|
|
||||||
<div class="ct_pattern_recognition_container">${valueObject.pattern}<div class="ct_logo"><img src="${this.staticPath + "patroonherkenning.png"}"/></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="ct_row2_container">
|
|
||||||
<div class="ct_abstraction_container">${valueObject.abstr}<div class="ct_logo"><img src="${this.staticPath + "abstractie.png"}"/></div></div>
|
|
||||||
<div class="ct_algorithm_container">${valueObject.algo}<div class="ct_logo"><img src="${this.staticPath + "algoritme.png"}"/></div></div>
|
|
||||||
</div>
|
|
||||||
<div class="ct_implementation_container">${valueObject.impl}<div class="ct_logo"><img src="${this.staticPath + "decompositie.png"}"/></div></div>
|
|
||||||
</div>`
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (let key in regexObject) {
|
|
||||||
let match = text.match(regexObject[key]);
|
|
||||||
if (match && match.length >= 1){
|
|
||||||
htmlObject[key] = super.render(match[1]);
|
|
||||||
}else{
|
|
||||||
htmlObject[key] = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new InvalidArgumentError(e.message);
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return htmlStructure(htmlObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export { CTSchemaProcessor };
|
|
38
backend/src/services/learning-objects/processing/extern/extern-processor.ts
vendored
Normal file
38
backend/src/services/learning-objects/processing/extern/extern-processor.ts
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/extern/extern_processor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Processor from "../processor.js";
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import {ProcessingError} from "../processing-error";
|
||||||
|
import {isValidHttpUrl} from "../../../../util/links";
|
||||||
|
import {DwengoContentType} from "../content-type";
|
||||||
|
|
||||||
|
class ExternProcessor extends Processor<string> {
|
||||||
|
constructor() {
|
||||||
|
super(DwengoContentType.EXTERN);
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(externURL: string) {
|
||||||
|
if (!isValidHttpUrl(externURL)) {
|
||||||
|
throw new ProcessingError("The url is not valid: " + externURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a seperate youtube-processor would be added, this code would need to move to that processor
|
||||||
|
// Converts youtube urls to youtube-embed urls
|
||||||
|
let match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL)
|
||||||
|
if (match) {
|
||||||
|
externURL = match[1] + "embed/" + match[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return DOMPurify.sanitize(`
|
||||||
|
<div class="iframe-container">
|
||||||
|
<iframe src="${externURL}" allowfullscreen></iframe>
|
||||||
|
</div>`,
|
||||||
|
{ ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExternProcessor;
|
|
@ -1,37 +0,0 @@
|
||||||
import Processor from "../processor.ts";
|
|
||||||
import { isValidHttpUrl } from '../../utils/utils.js'
|
|
||||||
import InvalidArgumentError from '../../utils/invalid_argument_error.js'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import Logger from "../../logger.js";
|
|
||||||
|
|
||||||
let logger = Logger.getLogger()
|
|
||||||
class ExternProcessor extends Processor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} externURL
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the ExternProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(externURL, {height = 315, aspect_ratio = 'iframe-16-9'} = {}) {
|
|
||||||
if (!isValidHttpUrl(externURL)) {
|
|
||||||
throw new InvalidArgumentError("The url is not valid: " + externURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a seperate youtube-processor would be added, this code would need to move to that processor
|
|
||||||
// Converts youtube urls to youtube-embed urls
|
|
||||||
let match = /(.*youtube.com\/)watch\?v=(.*)/.exec(externURL)
|
|
||||||
if (match) {
|
|
||||||
externURL = match[1] + "embed/" + match[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return DOMPurify.sanitize(`<div class="iframe-container ${aspect_ratio}"><iframe width="420px" height="${height}px" src="${externURL}" allowfullscreen></iframe></div>`, { ADD_TAGS: ["iframe"], ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling']});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExternProcessor;
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/gift/gift_processor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Processor from "../processor.js";
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import {GIFTQuestion, parse} from "gift-pegjs"
|
||||||
|
import {DwengoContentType} from "../content-type";
|
||||||
|
import {GIFTQuestionRenderer} from "./question-renderers/gift-question-renderer";
|
||||||
|
import {MultipleChoiceQuestionRenderer} from "./question-renderers/multiple-choice-question-renderer";
|
||||||
|
import {CategoryQuestionRenderer} from "./question-renderers/category-question-renderer";
|
||||||
|
import {DescriptionQuestionRenderer} from "./question-renderers/description-question-renderer";
|
||||||
|
import {EssayQuestionRenderer} from "./question-renderers/essay-question-renderer";
|
||||||
|
import {MatchingQuestionRenderer} from "./question-renderers/matching-question-renderer";
|
||||||
|
import {NumericalQuestionRenderer} from "./question-renderers/numerical-question-renderer";
|
||||||
|
import {ShortQuestionRenderer} from "./question-renderers/short-question-renderer";
|
||||||
|
import {TrueFalseQuestionRenderer} from "./question-renderers/true-false-question-renderer";
|
||||||
|
|
||||||
|
class GiftProcessor extends Processor<string> {
|
||||||
|
|
||||||
|
private renderers: RendererMap = {
|
||||||
|
Category: new CategoryQuestionRenderer(),
|
||||||
|
Description: new DescriptionQuestionRenderer(),
|
||||||
|
Essay: new EssayQuestionRenderer(),
|
||||||
|
Matching: new MatchingQuestionRenderer(),
|
||||||
|
Numerical: new NumericalQuestionRenderer(),
|
||||||
|
Short: new ShortQuestionRenderer(),
|
||||||
|
TF: new TrueFalseQuestionRenderer(),
|
||||||
|
MC: new MultipleChoiceQuestionRenderer()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(DwengoContentType.GIFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(giftString: string) {
|
||||||
|
const quizQuestions: GIFTQuestion[] = parse(giftString);
|
||||||
|
|
||||||
|
let html = "<div class='gift'>";
|
||||||
|
for (let question of quizQuestions) {
|
||||||
|
html += this.renderQuestion(question);
|
||||||
|
}
|
||||||
|
html += "</div>"
|
||||||
|
|
||||||
|
return DOMPurify.sanitize(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderQuestion<T extends GIFTQuestion>(question: T): string {
|
||||||
|
const renderer = this.renderers[question.type] as GIFTQuestionRenderer<T>;
|
||||||
|
return renderer.render(question);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RendererMap = {
|
||||||
|
[K in GIFTQuestion["type"]]: GIFTQuestionRenderer<Extract<GIFTQuestion, { type: K }>>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GiftProcessor;
|
|
@ -1,52 +0,0 @@
|
||||||
import Processor from "../processor.ts";
|
|
||||||
import { isValidHttpUrl } from '../../utils/utils.js'
|
|
||||||
import { findFile } from '../../utils/file_io.js'
|
|
||||||
import InvalidArgumentError from '../../utils/invalid_argument_error.js'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import ProcessingHistory from "../../models/processing_history.js";
|
|
||||||
import path from "path"
|
|
||||||
import fs from "fs"
|
|
||||||
import { parse } from "gift-pegjs"
|
|
||||||
|
|
||||||
class GiftProcessor extends Processor {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.types = ["text/gift"]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} audioUrl
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the GiftProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(giftString, args = { }) {
|
|
||||||
|
|
||||||
const quizQuestions = parse(giftString);
|
|
||||||
console.log(quizQuestions);
|
|
||||||
|
|
||||||
|
|
||||||
return DOMPurify.sanitize(`<audio controls>
|
|
||||||
<source src="@@URL_REPLACE@@/>
|
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext == ".txt") {
|
|
||||||
inputString = f.buffer.toString('utf8');
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [this.render(inputString), files]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GiftProcessor;
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {Category} from "gift-pegjs";
|
||||||
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
|
export class CategoryQuestionRenderer extends GIFTQuestionRenderer<Category> {
|
||||||
|
render(question: Category): string {
|
||||||
|
throw new ProcessingError("The question type 'Category' is not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {Description} from "gift-pegjs";
|
||||||
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
|
export class DescriptionQuestionRenderer extends GIFTQuestionRenderer<Description> {
|
||||||
|
render(question: Description): string {
|
||||||
|
throw new ProcessingError("The question type 'Description' is not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {Essay} from "gift-pegjs";
|
||||||
|
|
||||||
|
export class EssayQuestionRenderer extends GIFTQuestionRenderer<Essay> {
|
||||||
|
render(question: Essay): string {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import {GIFTQuestion} from "gift-pegjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses of this class are renderers which can render a specific type of GIFT questions to HTML.
|
||||||
|
*/
|
||||||
|
export abstract class GIFTQuestionRenderer<T extends GIFTQuestion> {
|
||||||
|
/**
|
||||||
|
* Render the given question to HTML.
|
||||||
|
* @param question The question.
|
||||||
|
* @returns The question rendered as HTML.
|
||||||
|
*/
|
||||||
|
abstract render(question: T): string;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {Matching} from "gift-pegjs";
|
||||||
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
|
export class MatchingQuestionRenderer extends GIFTQuestionRenderer<Matching> {
|
||||||
|
render(question: Matching): string {
|
||||||
|
throw new ProcessingError("The question type 'Matching' is not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {MultipleChoice} from "gift-pegjs";
|
||||||
|
|
||||||
|
export class MultipleChoiceQuestionRenderer extends GIFTQuestionRenderer<MultipleChoice> {
|
||||||
|
render(question: MultipleChoice): string {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {Numerical} from "gift-pegjs";
|
||||||
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
|
export class NumericalQuestionRenderer extends GIFTQuestionRenderer<Numerical> {
|
||||||
|
render(question: Numerical): string {
|
||||||
|
throw new ProcessingError("The question type 'Numerical' is not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {ShortAnswer} from "gift-pegjs";
|
||||||
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
|
export class ShortQuestionRenderer extends GIFTQuestionRenderer<ShortAnswer> {
|
||||||
|
render(question: ShortAnswer): string {
|
||||||
|
throw new ProcessingError("The question type 'ShortAnswer' is not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {GIFTQuestionRenderer} from "./gift-question-renderer";
|
||||||
|
import {TrueFalse} from "gift-pegjs";
|
||||||
|
import {ProcessingError} from "../../processing-error";
|
||||||
|
|
||||||
|
export class TrueFalseQuestionRenderer extends GIFTQuestionRenderer<TrueFalse> {
|
||||||
|
render(question: TrueFalse): string {
|
||||||
|
throw new ProcessingError("The question type 'TrueFalse' is not supported yet!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/image/block_image_processor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import InlineImageProcessor from "./inline-image-processor.js"
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
||||||
|
class BlockImageProcessor extends InlineImageProcessor {
|
||||||
|
constructor(){
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(imageUrl: string){
|
||||||
|
let inlineHtml = super.render(imageUrl);
|
||||||
|
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockImageProcessor;
|
|
@ -1,23 +0,0 @@
|
||||||
import InlineImageProcessor from "./inline_image_processor.js"
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
class BlockImageProcessor extends InlineImageProcessor{
|
|
||||||
constructor(){
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} imageUrl
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the BlockImageProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(imageUrl, args = { altText: ""}){
|
|
||||||
let inlineHtml = super.render(imageUrl, args);
|
|
||||||
return DOMPurify.sanitize(`<div>${inlineHtml}</div>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BlockImageProcessor;
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/image/inline_image_processor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Processor from "../processor.js";
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import {DwengoContentType} from "../content-type.js";
|
||||||
|
import {ProcessingError} from "../processing-error.js";
|
||||||
|
import {isValidHttpUrl} from "../../../../util/links";
|
||||||
|
|
||||||
|
class InlineImageProcessor extends Processor<string> {
|
||||||
|
constructor(contentType: DwengoContentType = DwengoContentType.IMAGE_INLINE) {
|
||||||
|
super(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(imageUrl: string) {
|
||||||
|
if (!isValidHttpUrl(imageUrl)) {
|
||||||
|
throw new ProcessingError(`Image URL is invalid: ${imageUrl}`);
|
||||||
|
}
|
||||||
|
return DOMPurify.sanitize(`<img src="${imageUrl}" alt="">`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InlineImageProcessor;
|
|
@ -1,62 +0,0 @@
|
||||||
import Processor from "../processor.ts";
|
|
||||||
import { isValidHttpUrl } from '../../utils/utils.js'
|
|
||||||
import InvalidArgumentError from '../../utils/invalid_argument_error.js'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import ProcessingHistory from "../../models/processing_history.js";
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
class InlineImageProcessor extends Processor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} imageUrl
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the InlineImageProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(imageUrl, args = { altText: "", metadata: {} }) {
|
|
||||||
|
|
||||||
if (!isValidHttpUrl(imageUrl) && (!imageUrl || !imageUrl.toLowerCase().match(/^(?!http.*$)[^.].*\.(jpe?g|png|svg|gif)/))) {
|
|
||||||
if (args.metadata && args.metadata.hruid && args.metadata.version && args.metadata.language){
|
|
||||||
ProcessingHistory.error(args.metadata.hruid, args.metadata.version, args.metadata.language, "The image cannot be found. Please check if the url is spelled correctly.")
|
|
||||||
}else{
|
|
||||||
ProcessingHistory.error("generalError", "99999999", "en", "The image cannot be found. Please check if the url is spelled correctly.")
|
|
||||||
}
|
|
||||||
throw new InvalidArgumentError("The image cannot be found. Please check if the url is spelled correctly.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof args.altText == 'undefined') {
|
|
||||||
args.altText = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidHttpUrl(imageUrl)) {
|
|
||||||
return DOMPurify.sanitize(`<img src="${imageUrl}" alt="${args.altText}">`);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.metadata._id) {
|
|
||||||
throw new InvalidArgumentError("The metadata for for the object which uses the file '" + imageUrl + "' is not loaded in the processor.");
|
|
||||||
}
|
|
||||||
return DOMPurify.sanitize(`<img src="@@URL_REPLACE@@/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${args.metadata._id}/${imageUrl}" alt="${args.altText}">`);
|
|
||||||
}
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let args = {};
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext.match(/\.(jpe?g)|(png)|(svg)$/)){
|
|
||||||
inputString = f["originalname"];
|
|
||||||
args.metadata = metadata
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [this.render(inputString, args), files]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InlineImageProcessor;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import Processor from "../processor.ts";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LearningObjectProcessor extends Processor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} learningObjectId
|
|
||||||
* @param {object} args Optional arguments
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(objectString, args = {}) {
|
|
||||||
return `@@OBJECT_REPLACE/${objectString}@@`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LearningObjectProcessor;
|
|
|
@ -1,109 +0,0 @@
|
||||||
import LearningObjectProcessor from "../learning_object/learing_object_processor.js";
|
|
||||||
import PdfProcessor from "../pdf/pdf_processor.js";
|
|
||||||
import AudioProcessor from "../audio/audio_processor.js";
|
|
||||||
import ExternProcessor from "../extern/extern_processor.js";
|
|
||||||
import BlocklyProcessor from "../blockly/blockly_processor.js";
|
|
||||||
import { findFile } from "../../utils/file_io.js";
|
|
||||||
import InlineImageProcessor from "../image/inline_image_processor.js";
|
|
||||||
import { isValidHttpUrl } from "../../utils/utils.js";
|
|
||||||
import ProcessingHistory from "../../models/processing_history.js";
|
|
||||||
|
|
||||||
class LearningObjectMarkdownRenderer {
|
|
||||||
learingObjectPrefix = '@learning-object';
|
|
||||||
pdfPrefix = '@pdf';
|
|
||||||
audioPrefix = '@audio';
|
|
||||||
externPrefix = '@extern';
|
|
||||||
videoPrefix = '@youtube';
|
|
||||||
notebookPrefix = '@notebook';
|
|
||||||
blocklyPrefix = '@blockly';
|
|
||||||
|
|
||||||
constructor(args = { files: [], metadata: {} }) {
|
|
||||||
this.args = args;
|
|
||||||
}
|
|
||||||
|
|
||||||
heading(text, level) {
|
|
||||||
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<h${level}>
|
|
||||||
<a name="${escapedText}" class="anchor" href="#${escapedText}">
|
|
||||||
<span class="header-link"></span>
|
|
||||||
</a>
|
|
||||||
${text}
|
|
||||||
</h${level}>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the syntax for a link is used => [text](href "title")
|
|
||||||
// render a custom link when the prefix for a learning object is used.
|
|
||||||
link(href, title, text) {
|
|
||||||
if (href.startsWith(this.learingObjectPrefix)) {
|
|
||||||
// link to learning-object
|
|
||||||
let query = href.split(/\/(.+)/, 2)[1].split("/")
|
|
||||||
return `<a href="@@URL_REPLACE@@/api/learningObject/getRaw?hruid=${query[0]}&language=${query[1]}&version=${query[2]}&redirect=true" target="_blank" title="${title}">${text}</a>`
|
|
||||||
} else if (href.startsWith(this.blocklyPrefix)) {
|
|
||||||
// link to blockly (downloads)
|
|
||||||
if (title) {
|
|
||||||
return `<a href="@@URL_REPLACE@@/api/learningObject/downloadFile/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${this.args.metadata._id}/${href.split(/\/(.+)/, 2)[1]}" target="_blank" title="${title}">${text}</a>`
|
|
||||||
}
|
|
||||||
return `<a href="@@URL_REPLACE@@/api/learningObject/downloadFile/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${this.args.metadata._id}/${href.split(/\/(.+)/, 2)[1]}" target="_blank">${text}</a>`
|
|
||||||
} else {
|
|
||||||
// any other link
|
|
||||||
if (isValidHttpUrl(href)) {
|
|
||||||
return `<a href="${href}" target="_blank" title="${title}">${text}</a>`;
|
|
||||||
} else {
|
|
||||||
if (title) {
|
|
||||||
return `<a href="@@URL_REPLACE@@/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${this.args.metadata._id}/${href}" target="_blank" title="${title}">${text}</a>`
|
|
||||||
}
|
|
||||||
return `<a href="@@URL_REPLACE@@/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${this.args.metadata._id}/${href}" target="_blank" >${text}</a>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// When the syntax for an image is used => 
|
|
||||||
// render a learning object, pdf, audio or video if a prefix is used.
|
|
||||||
image(href, title, text) {
|
|
||||||
if (href.startsWith(this.learingObjectPrefix)) {
|
|
||||||
// embedded learning-object
|
|
||||||
let proc = new LearningObjectProcessor();
|
|
||||||
return proc.render(href.split(/\/(.+)/, 2)[1]);
|
|
||||||
|
|
||||||
} else if (href.startsWith(this.pdfPrefix)) {
|
|
||||||
// embedded pdf
|
|
||||||
let proc = new PdfProcessor();
|
|
||||||
return proc.render(href.split(/\/(.+)/, 2)[1], { files: this.args.files, metadata: this.args.metadata });
|
|
||||||
|
|
||||||
} else if (href.startsWith(this.audioPrefix)) {
|
|
||||||
// embedded audio
|
|
||||||
let proc = new AudioProcessor();
|
|
||||||
return proc.render(href.split(/\/(.+)/, 2)[1], { files: this.args.files, metadata: this.args.metadata });
|
|
||||||
|
|
||||||
} else if (href.startsWith(this.externPrefix) || href.startsWith(this.videoPrefix) || href.startsWith(this.notebookPrefix)) {
|
|
||||||
// embedded youtube video or notebook (or other extern content)
|
|
||||||
let proc = new ExternProcessor();
|
|
||||||
return proc.render(href.split(/\/(.+)/, 2)[1]);
|
|
||||||
|
|
||||||
} else if (href.startsWith(this.blocklyPrefix)) {
|
|
||||||
// embedded blockly program
|
|
||||||
let proc = new BlocklyProcessor();
|
|
||||||
if (this.args.files) {
|
|
||||||
let file = findFile(href.split(/\/(.+)/, 2)[1], this.args.files)
|
|
||||||
if (file) {
|
|
||||||
return proc.render(file.buffer, { language: this.args.metadata.language ? this.args.metadata.language : "nl", id: this.args.metadata._id });
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
ProcessingHistory.error("generalError", "99999999", "en", `"The blockly preview with reference ${href} could not be loaded. Are you sure the correct xml file was passed?`)
|
|
||||||
return "";
|
|
||||||
}/*else if (href.startsWith(this.learingObjectPrefix)){
|
|
||||||
let [hruid, version, language] = href.split(/\/(.+)/, 2)[1].split("/")
|
|
||||||
return learningObjectApiController.getHtmlObject({hruid: hruid, version: version, language: language})
|
|
||||||
}*/ else {
|
|
||||||
// embedded image
|
|
||||||
let proc = new InlineImageProcessor();
|
|
||||||
return proc.render(href, { metadata: this.args.metadata })
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LearningObjectMarkdownRenderer
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import PdfProcessor from "../pdf/pdf-processor.js";
|
||||||
|
import AudioProcessor from "../audio/audio-processor.js";
|
||||||
|
import ExternProcessor from "../extern/extern-processor.js";
|
||||||
|
import InlineImageProcessor from "../image/inline-image-processor.js";
|
||||||
|
import {RendererObject, Tokens} from "marked";
|
||||||
|
import {getUrlStringForLearningObject, isValidHttpUrl} from "../../../../util/links";
|
||||||
|
import {ProcessingError} from "../processing-error";
|
||||||
|
import {LearningObjectIdentifier} from "../../../../interfaces/learning-content";
|
||||||
|
import {Language} from "../../../../entities/content/language";
|
||||||
|
import Image = Tokens.Image;
|
||||||
|
import Heading = Tokens.Heading;
|
||||||
|
import Link = Tokens.Link;
|
||||||
|
|
||||||
|
const prefixes = {
|
||||||
|
learningObject: '@learning-object',
|
||||||
|
pdf: '@pdf',
|
||||||
|
audio: '@audio',
|
||||||
|
extern: '@extern',
|
||||||
|
video: '@youtube',
|
||||||
|
notebook: '@notebook',
|
||||||
|
blockly: '@blockly',
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractLearningObjectIdFromHref(href: string): LearningObjectIdentifier {
|
||||||
|
const [hruid, language, version] = href.split(/\/(.+)/, 2)[1].split("/");
|
||||||
|
return {hruid, language: language as Language, version};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension for the renderer of the Marked Markdown renderer which adds support for
|
||||||
|
* - a custom heading,
|
||||||
|
* - links to other learning objects,
|
||||||
|
* - embeddings of other learning objects.
|
||||||
|
*/
|
||||||
|
const dwengoMarkedRenderer: RendererObject = {
|
||||||
|
heading(heading: Heading) {
|
||||||
|
const text = heading.text;
|
||||||
|
const level = heading.depth;
|
||||||
|
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<h${level}>
|
||||||
|
<a name="${escapedText}" class="anchor" href="#${escapedText}">
|
||||||
|
<span class="header-link"></span>
|
||||||
|
</a>
|
||||||
|
${text}
|
||||||
|
</h${level}>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// When the syntax for a link is used => [text](href "title")
|
||||||
|
// render a custom link when the prefix for a learning object is used.
|
||||||
|
link(link: Link) {
|
||||||
|
const href = link.href;
|
||||||
|
const title = link.title || "";
|
||||||
|
const text = link.text;
|
||||||
|
|
||||||
|
if (href.startsWith(prefixes.learningObject)) {
|
||||||
|
// link to learning-object
|
||||||
|
const learningObjectId = extractLearningObjectIdFromHref(href);
|
||||||
|
return `
|
||||||
|
<a href="${getUrlStringForLearningObject(learningObjectId)}]" target="_blank" title="${title}">${text}</a>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// any other link
|
||||||
|
if (!isValidHttpUrl(href)) {
|
||||||
|
throw new ProcessingError("Link is not a valid HTTP URL!");
|
||||||
|
}
|
||||||
|
return `<a href="${href}" target="_blank" title="${title}">${text}</a>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// When the syntax for an image is used => 
|
||||||
|
// render a learning object, pdf, audio or video if a prefix is used.
|
||||||
|
image(img: Image) {
|
||||||
|
const href = img.href;
|
||||||
|
if (href.startsWith(prefixes.learningObject)) {
|
||||||
|
// embedded learning-object
|
||||||
|
const learningObjectId = extractLearningObjectIdFromHref(href);
|
||||||
|
return `
|
||||||
|
<learning-object hruid="${learningObjectId.hruid}" language="${learningObjectId.language}" version="${learningObjectId.version}"/>
|
||||||
|
`; // Placeholder for the learning object since we cannot fetch its HTML here (this has to be a sync function!)
|
||||||
|
} else if (href.startsWith(prefixes.pdf)) {
|
||||||
|
// embedded pdf
|
||||||
|
let proc = new PdfProcessor();
|
||||||
|
return proc.render(href.split(/\/(.+)/, 2)[1]);
|
||||||
|
} else if (href.startsWith(prefixes.audio)) {
|
||||||
|
// embedded audio
|
||||||
|
let proc = new AudioProcessor();
|
||||||
|
return proc.render(href.split(/\/(.+)/, 2)[1]);
|
||||||
|
} else if (href.startsWith(prefixes.extern) || href.startsWith(prefixes.video) || href.startsWith(prefixes.notebook)) {
|
||||||
|
// embedded youtube video or notebook (or other extern content)
|
||||||
|
let proc = new ExternProcessor();
|
||||||
|
return proc.render(href.split(/\/(.+)/, 2)[1]);
|
||||||
|
} else {
|
||||||
|
// embedded image
|
||||||
|
let proc = new InlineImageProcessor();
|
||||||
|
return proc.render(href)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dwengoMarkedRenderer;
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/markdown_processor.js
|
||||||
|
* and https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/markdown/learing_object_markdown_renderer.js [sic!]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {marked} from 'marked'
|
||||||
|
import Processor from '../processor.js';
|
||||||
|
import InlineImageProcessor from '../image/inline-image-processor.js';
|
||||||
|
import {DwengoContentType} from "../content-type";
|
||||||
|
import {ProcessingError} from "../processing-error";
|
||||||
|
import dwengoMarkedRenderer from "./learning-object-markdown-renderer";
|
||||||
|
|
||||||
|
class MarkdownProcessor extends Processor<string> {
|
||||||
|
constructor() {
|
||||||
|
super(DwengoContentType.TEXT_MARKDOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(mdText: string) {
|
||||||
|
let html = "";
|
||||||
|
try {
|
||||||
|
mdText = this.replaceLinks(mdText); // Replace html image links with path based on metadata
|
||||||
|
marked.use({renderer: dwengoMarkedRenderer});
|
||||||
|
html = marked(mdText, {async: false});
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new ProcessingError(e.message);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceLinks(html: string) {
|
||||||
|
let proc = new InlineImageProcessor();
|
||||||
|
html = html.replace(/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, (
|
||||||
|
match: string,
|
||||||
|
src: string,
|
||||||
|
alt: string,
|
||||||
|
altText: string,
|
||||||
|
title: string,
|
||||||
|
titleText: string
|
||||||
|
) => {
|
||||||
|
return proc.render(src);
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MarkdownProcessor };
|
|
@ -1,112 +0,0 @@
|
||||||
import { marked } from 'marked'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import LearningObjectMarkdownRenderer from './learing_object_markdown_renderer.js';
|
|
||||||
import ObjectConverter from '../../utils/object_converter.js';
|
|
||||||
import yaml from "js-yaml"
|
|
||||||
import Logger from '../../logger.js';
|
|
||||||
import Processor from '../processor.ts';
|
|
||||||
import InvalidArgumentError from "../../utils/invalid_argument_error.js";
|
|
||||||
import ProcessingHistory from '../../models/processing_history.js';
|
|
||||||
import path from "path";
|
|
||||||
import InlineImageProcessor from '../image/inline_image_processor.js';
|
|
||||||
|
|
||||||
class MarkdownProcessor extends Processor {
|
|
||||||
logger = Logger.getLogger();
|
|
||||||
constructor(args = { files: [], metadata: {} }) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} mdText Plain markdown string to be converted to html. May contain links to learning objects which results in recursive processing.
|
|
||||||
* @returns The sanitized version of the generated html.
|
|
||||||
*/
|
|
||||||
render(mdText, args = {metadata: {}}) {
|
|
||||||
let html = "";
|
|
||||||
try {
|
|
||||||
mdText = this.replaceLinks(mdText, args.metadata); // Replace html image links with path based on metadata
|
|
||||||
html = marked(mdText); //DOMPurify.sanitize(marked(mdText), { ADD_TAGS: ["embed", "iframe", "script"] });
|
|
||||||
} catch (e) {
|
|
||||||
throw new InvalidArgumentError(e.message);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceLinks(html, metadata) {
|
|
||||||
let proc = new InlineImageProcessor();
|
|
||||||
html = html.replace(/<img.*?src="(.*?)".*?(alt="(.*?)")?.*?(title="(.*?)")?.*?>/g, (match, src, alt, altText, title, titleText) => {
|
|
||||||
return proc.render(src, { metadata: metadata, altText: altText || "No alt text", title: titleText || "No title" });
|
|
||||||
});
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} mdTextWithYAMLMeta Markdown string with metadata. Compatible with jekyll (https://jekyllrb.com/docs/front-matter/)
|
|
||||||
* @returns {object} {original input, metadata string, }
|
|
||||||
*/
|
|
||||||
static stripYAMLMetaData(mdTextWithYAMLMeta) {
|
|
||||||
let trimmedInput = mdTextWithYAMLMeta.trim();
|
|
||||||
const metadataregex = /(?<=^---).+?(?=---)/s
|
|
||||||
const mdregex = /(?<=---.*---).+?$/s
|
|
||||||
let metadataText = trimmedInput.match(metadataregex);
|
|
||||||
let mdText = "";
|
|
||||||
|
|
||||||
if (metadataText) {
|
|
||||||
// Yes, metadata
|
|
||||||
metadataText = metadataText[0].trim();
|
|
||||||
mdText = trimmedInput.match(mdregex);
|
|
||||||
mdText = mdText ? mdText[0].trim() : "";
|
|
||||||
} else {
|
|
||||||
// No metadata
|
|
||||||
metadataText = "";
|
|
||||||
mdText = trimmedInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = {};
|
|
||||||
try {
|
|
||||||
metadata = yaml.load(metadataText);
|
|
||||||
} catch (e) {
|
|
||||||
ProcessingHistory.error("generalError", "99999999", "en", `Unable to convert metadata to YAML: ${e}`)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
original: mdTextWithYAMLMeta,
|
|
||||||
metadata: metadata,
|
|
||||||
markdown: mdText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let resfiles = [];
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext == ".md"){
|
|
||||||
inputString = f.buffer.toString('utf8');
|
|
||||||
resfiles = files;
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove index.md file to create a list of files for checking during rendering process
|
|
||||||
let filtered = files.filter((f) => {
|
|
||||||
let ignoreregex = /(.*index\.md)|(^\..*)$/;
|
|
||||||
return !f["originalname"].match(ignoreregex);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Strip metadata from content
|
|
||||||
let splitdata = MarkdownProcessor.stripYAMLMetaData(inputString)
|
|
||||||
|
|
||||||
// A bit stupid but marked does not work with an instance of a class only with plain object
|
|
||||||
const renderer = new ObjectConverter().toJSON(new LearningObjectMarkdownRenderer({ files: filtered, metadata: metadata }));
|
|
||||||
marked.use({ renderer });
|
|
||||||
|
|
||||||
return [this.render(splitdata.markdown, {metadata: metadata}), resfiles]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { MarkdownProcessor };
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/pdf/pdf_processor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Processor from "../processor.js";
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import {DwengoContentType} from "../content-type.js";
|
||||||
|
import {isValidHttpUrl} from "../../../../util/links.js";
|
||||||
|
import {ProcessingError} from "../processing-error.js";
|
||||||
|
|
||||||
|
class PdfProcessor extends Processor<string> {
|
||||||
|
constructor() {
|
||||||
|
super(DwengoContentType.APPLICATION_PDF);
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(pdfUrl: string) {
|
||||||
|
if (!isValidHttpUrl(pdfUrl)) {
|
||||||
|
throw new ProcessingError(`PDF URL is invalid: ${pdfUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DOMPurify.sanitize(`
|
||||||
|
<embed src="${pdfUrl}" type="application/pdf" width="100%" height="800px"/>
|
||||||
|
`, { ADD_TAGS: ["embed"] }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PdfProcessor;
|
|
@ -1,63 +0,0 @@
|
||||||
import Processor from "../processor.ts";
|
|
||||||
import { isValidHttpUrl } from '../../utils/utils.js'
|
|
||||||
import { findFile } from '../../utils/file_io.js'
|
|
||||||
import InvalidArgumentError from '../../utils/invalid_argument_error.js'
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
|
||||||
import ProcessingHistory from "../../models/processing_history.js";
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
class PdfProcessor extends Processor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} pdfUrl
|
|
||||||
* @param {object} args Optional arguments specific to the render function of the PdfProcessor
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(pdfUrl, args = { files: [], metadata: {} }) {
|
|
||||||
if ((!args.files || args.files.length <= 0 || !findFile(pdfUrl, args.files)) && !isValidHttpUrl(pdfUrl)) {
|
|
||||||
let errormessage = `The pdf file ${pdfUrl} cannot be found. Please check if the url is spelled correctly.`
|
|
||||||
if (args.metadata && args.metadata.hruid && args.metadata.version && args.metadata.language){
|
|
||||||
ProcessingHistory.error(args.metadata.hruid, args.metadata.version, args.metadata.language, errormessage)
|
|
||||||
}else{
|
|
||||||
ProcessingHistory.error("generalError", "99999999", "en", errormessage)
|
|
||||||
}
|
|
||||||
throw new InvalidArgumentError("The pdf file cannot be found. Please check if the url is spelled correctly.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isValidHttpUrl(pdfUrl)) {
|
|
||||||
return DOMPurify.sanitize(`<embed src="${pdfUrl}" type="application/pdf" width="100%" height="800px"/>`, { ADD_TAGS: ["embed"] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!args.metadata || !args.metadata._id) {
|
|
||||||
throw new InvalidArgumentError("The metadata for for the object which uses the file '" + pdfUrl + "' is not loaded in the processor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return DOMPurify.sanitize(`<embed src="@@URL_REPLACE@@/${process.env.LEARNING_OBJECT_STORAGE_NAME}/${args.metadata._id}/${pdfUrl}" type="application/pdf" width="100%" height="800px"/>`, { ADD_TAGS: ["embed"] })
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let args = {}
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext == ".pdf") {
|
|
||||||
inputString = f["originalname"]
|
|
||||||
// add files to args to check if file exists
|
|
||||||
args.files = files;
|
|
||||||
args.metadata = metadata
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [this.render(inputString, args), files]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PdfProcessor;
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processing_proxy.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import BlockImageProcessor from "./image/block-image-processor.js";
|
||||||
|
import InlineImageProcessor from "./image/inline-image-processor.js";
|
||||||
|
import { MarkdownProcessor } from "./markdown/markdown-processor.js";
|
||||||
|
import TextProcessor from "./text/text-processor.js";
|
||||||
|
import AudioProcessor from "./audio/audio-processor.js";
|
||||||
|
import PdfProcessor from "./pdf/pdf-processor.js";
|
||||||
|
import ExternProcessor from "./extern/extern-processor.js";
|
||||||
|
import GiftProcessor from "./gift/gift-processor.js";
|
||||||
|
import {LearningObject} from "../../../entities/content/learning-object.entity";
|
||||||
|
import Processor from "./processor";
|
||||||
|
import {DwengoContentType} from "./content-type";
|
||||||
|
import {LearningObjectIdentifier} from "../../../interfaces/learning-content";
|
||||||
|
import {Language} from "../../../entities/content/language";
|
||||||
|
|
||||||
|
const EMBEDDED_LEARNING_OBJECT_PLACEHOLDER = /<learning-object hruid="([^"]+)" language="([^"]+)" version="([^"]+)"\/>/g;
|
||||||
|
|
||||||
|
class ProcessingService {
|
||||||
|
private processors!: Map<DwengoContentType, Processor<any>>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const processors = [
|
||||||
|
new InlineImageProcessor(),
|
||||||
|
new BlockImageProcessor(),
|
||||||
|
new MarkdownProcessor(),
|
||||||
|
new TextProcessor(),
|
||||||
|
new AudioProcessor(),
|
||||||
|
new PdfProcessor(),
|
||||||
|
new ExternProcessor(),
|
||||||
|
new GiftProcessor()
|
||||||
|
];
|
||||||
|
|
||||||
|
processors.forEach(processor => {
|
||||||
|
this.processors.set(processor.contentType, processor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the given learning object.
|
||||||
|
* @param learningObject The learning object to render
|
||||||
|
* @param fetchEmbeddedLearningObjects A function which takes a learning object identifier as an argument and
|
||||||
|
* returns the corresponding learning object. This is used to fetch learning
|
||||||
|
* objects embedded into this one.
|
||||||
|
* If this argument is omitted, embedded learning objects will be represented
|
||||||
|
* by placeholders.
|
||||||
|
* @returns Rendered HTML for this LearningObject as a string.
|
||||||
|
*/
|
||||||
|
render(learningObject: LearningObject, fetchEmbeddedLearningObjects?: (loId: LearningObjectIdentifier) => LearningObject): string {
|
||||||
|
let html = this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject);
|
||||||
|
if (fetchEmbeddedLearningObjects) {
|
||||||
|
// Replace all embedded learning objects.
|
||||||
|
return html.replace(
|
||||||
|
EMBEDDED_LEARNING_OBJECT_PLACEHOLDER,
|
||||||
|
(_, hruid: string, language: string, version: string): string => {
|
||||||
|
// Fetch the embedded learning object...
|
||||||
|
const learningObject = fetchEmbeddedLearningObjects({hruid, language: language as Language, version})
|
||||||
|
|
||||||
|
// ... and render it.
|
||||||
|
return this.render(learningObject);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessingService;
|
|
@ -1,50 +0,0 @@
|
||||||
/**
|
|
||||||
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processing_proxy.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
import BlockImageProcessor from "./image/block_image_processor.js";
|
|
||||||
import InlineImageProcessor from "./image/inline_image_processor.js";
|
|
||||||
import { CTSchemaProcessor } from "./ct_schema/ct_schema_processor.js";
|
|
||||||
import { MarkdownProcessor } from "./markdown/markdown_processor.js";
|
|
||||||
import TextProcessor from "./text/text_processor.js";
|
|
||||||
import AudioProcessor from "./audio/audio_processor.js";
|
|
||||||
import PdfProcessor from "./pdf/pdf_processor.js";
|
|
||||||
import ExternProcessor from "./extern/extern_processor.js";
|
|
||||||
import BlocklyProcessor from "./blockly/blockly_processor.js";
|
|
||||||
import GiftProcessor from "./gift/gift_processor.js";
|
|
||||||
import {LearningObject} from "../../../entities/content/learning-object.entity";
|
|
||||||
import Processor from "./processor";
|
|
||||||
import {DwengoContentType} from "./content_type";
|
|
||||||
|
|
||||||
class ProcessingService {
|
|
||||||
private processors!: Map<DwengoContentType, Processor<any>>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const processors = [
|
|
||||||
new InlineImageProcessor(),
|
|
||||||
new BlockImageProcessor(),
|
|
||||||
new MarkdownProcessor(),
|
|
||||||
new TextProcessor(),
|
|
||||||
new AudioProcessor(),
|
|
||||||
new PdfProcessor(),
|
|
||||||
new ExternProcessor(),
|
|
||||||
new BlocklyProcessor(),
|
|
||||||
new GiftProcessor(),
|
|
||||||
new CTSchemaProcessor()
|
|
||||||
];
|
|
||||||
|
|
||||||
processors.forEach(processor => {
|
|
||||||
this.processors.set(processor.contentType, processor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the given learning object.
|
|
||||||
* @param learningObject
|
|
||||||
*/
|
|
||||||
render(learningObject: LearningObject): string {
|
|
||||||
return this.processors.get(learningObject.contentType)!.renderLearningObject(learningObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProcessingService;
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {LearningObject} from "../../../entities/content/learning-object.entity";
|
import {LearningObject} from "../../../entities/content/learning-object.entity";
|
||||||
import {ProcessingError} from "./processing-error";
|
import {ProcessingError} from "./processing-error";
|
||||||
import {DwengoContentType} from "./content_type";
|
import {DwengoContentType} from "./content-type";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base class for all processors.
|
* Abstract base class for all processors.
|
||||||
|
@ -19,7 +19,9 @@ abstract class Processor<T> {
|
||||||
* @return Rendered HTML-string
|
* @return Rendered HTML-string
|
||||||
* @throws ProcessingError if the rendering fails.
|
* @throws ProcessingError if the rendering fails.
|
||||||
*/
|
*/
|
||||||
abstract render(toRender: T): string;
|
render(toRender: T): string {
|
||||||
|
return this.renderFn(toRender);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a learning object with the content type for which this processor is responsible.
|
* Render a learning object with the content type for which this processor is responsible.
|
||||||
|
@ -35,6 +37,15 @@ abstract class Processor<T> {
|
||||||
return this.renderLearningObjectFn(toRender);
|
return this.renderLearningObjectFn(toRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function which actually renders the content.
|
||||||
|
*
|
||||||
|
* @param toRender Content to be rendered
|
||||||
|
* @return Rendered HTML as a string
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected abstract renderFn(toRender: T): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function which actually executes the rendering of a learning object.
|
* Function which actually executes the rendering of a learning object.
|
||||||
* By default, this just means rendering the content in the content property of the learning object.
|
* By default, this just means rendering the content in the content property of the learning object.
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/text/text_processor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DOMPurify from 'isomorphic-dompurify'
|
||||||
|
import Processor from "../processor.js"
|
||||||
|
import {DwengoContentType} from "../content-type.js";
|
||||||
|
|
||||||
|
class TextProcessor extends Processor<string> {
|
||||||
|
constructor() {
|
||||||
|
super(DwengoContentType.TEXT_PLAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
override renderFn(text: string) {
|
||||||
|
// Sanitize plain text to prevent xss.
|
||||||
|
return DOMPurify.sanitize(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextProcessor;
|
|
@ -1,36 +0,0 @@
|
||||||
import DOMPurify from 'isomorphic-dompurify'
|
|
||||||
import Processor from "../processor.ts"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
class TextProcessor extends Processor {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} plain text
|
|
||||||
* @param {object} args Optional arguments
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
render(text, args = {}) {
|
|
||||||
// Sanitize plain text to prevent xss.
|
|
||||||
return DOMPurify.sanitize(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
processFiles(files, metadata){
|
|
||||||
let inputString = "";
|
|
||||||
let file = files.find((f) => {
|
|
||||||
let ext = path.extname(f.originalname);
|
|
||||||
if (ext == ".txt") {
|
|
||||||
inputString = f.buffer.toString('utf8');
|
|
||||||
return true;
|
|
||||||
}else{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return [this.render(inputString), files]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TextProcessor;
|
|
18
backend/src/util/links.ts
Normal file
18
backend/src/util/links.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {LearningObjectIdentifier} from "../interfaces/learning-content";
|
||||||
|
|
||||||
|
export function isValidHttpUrl(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUrlStringForLearningObject(learningObjectIdentifier: LearningObjectIdentifier): string {
|
||||||
|
let url = `/learningObject/${learningObjectIdentifier.hruid}/html?language=${learningObjectIdentifier.language}`;
|
||||||
|
if (learningObjectIdentifier.version) {
|
||||||
|
url += `&version=${learningObjectIdentifier.version}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
3371
package-lock.json
generated
3371
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue