chore(backend): Opzetten processing begonnen.
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
							
								
									2d9f17484c
								
							
						
					
					
						commit
						ba3da01d2d
					
				
					 18 changed files with 875 additions and 11 deletions
				
			
		|  | @ -24,7 +24,6 @@ | ||||||
|         "express": "^5.0.1", |         "express": "^5.0.1", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|         "js-yaml": "^4.1.0", |         "js-yaml": "^4.1.0", | ||||||
|         "@types/js-yaml": "^4.0.9" |  | ||||||
|     }, |     }, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@mikro-orm/cli": "^6.4.6", |         "@mikro-orm/cli": "^6.4.6", | ||||||
|  |  | ||||||
|  | @ -11,6 +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"; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class LearningObject { | export class LearningObject { | ||||||
|  | @ -33,7 +34,7 @@ export class LearningObject { | ||||||
|     description!: string; |     description!: string; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'string' }) |     @Property({ type: 'string' }) | ||||||
|     contentType!: string; |     contentType!: DwengoContentType; | ||||||
| 
 | 
 | ||||||
|     @Property({ type: 'array' }) |     @Property({ type: 'array' }) | ||||||
|     keywords: string[] = []; |     keywords: string[] = []; | ||||||
|  | @ -95,12 +96,3 @@ export class ReturnValue { | ||||||
|     @Property({ type: 'json' }) |     @Property({ type: 'json' }) | ||||||
|     callbackSchema!: string; |     callbackSchema!: string; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export enum ContentType { |  | ||||||
|     Markdown = 'text/markdown', |  | ||||||
|     Image = 'image/image', |  | ||||||
|     Mpeg = 'audio/mpeg', |  | ||||||
|     Pdf = 'application/pdf', |  | ||||||
|     Extern = 'extern', |  | ||||||
|     Blockly = 'Blockly', |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | /** | ||||||
|  |  * 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; | ||||||
|  | @ -0,0 +1,83 @@ | ||||||
|  | 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; | ||||||
|  | @ -0,0 +1,18 @@ | ||||||
|  | /** | ||||||
|  |  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/content_type.js
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | enum DwengoContentType { | ||||||
|  |     TEXT_PLAIN = "text/plain", | ||||||
|  |     TEXT_MARKDOWN = "text/markdown", | ||||||
|  |     IMAGE_BLOCK = "image/image-block", | ||||||
|  |     IMAGE_INLINE = "image/image", | ||||||
|  |     AUDIO_MPEG = "audio/mpeg", | ||||||
|  |     APPLICATION_PDF = "application/pdf", | ||||||
|  |     EXTERN = "extern", | ||||||
|  |     BLOCKLY = "blockly", | ||||||
|  |     GIFT = "text/gift", | ||||||
|  |     CT_SCHEMA = "text/ct-schema" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { DwengoContentType } | ||||||
|  | @ -0,0 +1,67 @@ | ||||||
|  | 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 }; | ||||||
							
								
								
									
										37
									
								
								backend/src/services/learning-objects/processing/extern/extern_processor.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								backend/src/services/learning-objects/processing/extern/extern_processor.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | 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,52 @@ | ||||||
|  | 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,23 @@ | ||||||
|  | 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,62 @@ | ||||||
|  | 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; | ||||||
|  | @ -0,0 +1,21 @@ | ||||||
|  | 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; | ||||||
|  | @ -0,0 +1,109 @@ | ||||||
|  | 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,112 @@ | ||||||
|  | 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,63 @@ | ||||||
|  | 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,5 @@ | ||||||
|  | export class ProcessingError extends Error { | ||||||
|  |     constructor(error: string) { | ||||||
|  |         super(error); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,50 @@ | ||||||
|  | /** | ||||||
|  |  * 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; | ||||||
|  | @ -0,0 +1,53 @@ | ||||||
|  | import {LearningObject} from "../../../entities/content/learning-object.entity"; | ||||||
|  | import {ProcessingError} from "./processing-error"; | ||||||
|  | import {DwengoContentType} from "./content_type"; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstract base class for all processors. | ||||||
|  |  * Each processor is responsible for a specific format a learning object can be in, which i tcan render to HTML. | ||||||
|  |  * | ||||||
|  |  * Based on https://github.com/dwengovzw/Learning-Object-Repository/blob/main/app/processors/processor.js
 | ||||||
|  |  */ | ||||||
|  | abstract class Processor<T> { | ||||||
|  |     protected constructor(public contentType: DwengoContentType) {} | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Render the given object. | ||||||
|  |      * | ||||||
|  |      * @param toRender Object which has to be rendered to HTML. This object has to be in the format for which this | ||||||
|  |      *                 Processor is responsible. | ||||||
|  |      * @return Rendered HTML-string | ||||||
|  |      * @throws ProcessingError if the rendering fails. | ||||||
|  |      */ | ||||||
|  |     abstract render(toRender: T): string; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Render a learning object with the content type for which this processor is responsible. | ||||||
|  |      * @param toRender | ||||||
|  |      */ | ||||||
|  |     renderLearningObject(toRender: LearningObject): string { | ||||||
|  |         if (toRender.contentType !== this.contentType) { | ||||||
|  |             throw new ProcessingError( | ||||||
|  |                 `Unsupported content type: ${toRender.contentType}.
 | ||||||
|  |                 This processor is only responsible for content of type ${this.contentType}.` | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         return this.renderLearningObjectFn(toRender); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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. | ||||||
|  |      * | ||||||
|  |      * When implementing this function, we may assume that we are responsible for the content type of the learning | ||||||
|  |      * object. | ||||||
|  |      * | ||||||
|  |      * @param toRender Learning object to render | ||||||
|  |      * @protected | ||||||
|  |      */ | ||||||
|  |     protected renderLearningObjectFn(toRender: LearningObject): string { | ||||||
|  |         return this.render(toRender.content as T); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default Processor; | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | 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; | ||||||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger