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
				
			
		| 
						 | 
				
			
			@ -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