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
											
										
									
								
							
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger