All files / src/utils/helpers tinyMCEUtility.js

94.87% Statements 37/39
90% Branches 18/20
90% Functions 9/10
97.14% Lines 34/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 11137x                 7331x 2535x 2078x 2078x     7331x   6x 6x   6x                     7331x 724x 724x 724x 724x 1x   724x     724x 14x   724x 724x       7331x 7331x   7331x 7331x 7331x                 7331x                           7331x 47x     7331x 28x 17x 17x 22x                 7331x                                
const tinyMCEUtility = (htmlString) => {
    /**
     * Does this editor HTML hold anything a reader would actually see?
     *
     * Modified by Allan Ninal — lifted out of TinyMCETextEditor's change handler so
     * the editors can ask the same question before saving. Text-less content is still
     * real content: an image, a table, a rule or a formula is a perfectly good answer,
     * and a check that only counted characters would throw those away.
     */
    const hasEditorContent = (content) => {
        if (!content) return false;
        const plainText = content.replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ");
        return Boolean(plainText.trim()) || /<(img|table|hr|math-field)[\s/>]/i.test(content);
    };
 
    const tinyMCEtoString = (htmlString) => {
        // Create a temporary DOM element to parse the HTML
        const tempElement = document.createElement("div");
        tempElement.innerHTML = htmlString;
        // Extract and return the plain text content
        return tempElement.textContent || tempElement.innerText || "";
    };
 
    // Plain textContent collapses rendered math (e.g. MathLive's superscript markup) down to its
    // visible characters, so two formulas that differ only in structure (an exponent vs. an inline
    // multiplier) can end up as the same string. Swap each math formula's rendered DOM for its
    // literal LaTeX source before extracting text so structurally different formulas compare distinct.
    //
    // Image-only choices have no textContent at all — every `<p><img src="…"></p>` would otherwise
    // collapse to "" and look identical for duplicate detection and correct-answer matching. Swap
    // each image for a stable [[img:src]] token (same idea as the formula substitution above).
    const getChoiceComparisonKey = (htmlString) => {
        Iif (htmlString == null) return "";
        const tempElement = document.createElement("div");
        tempElement.innerHTML = typeof htmlString === "string" ? htmlString : String(htmlString);
        tempElement.querySelectorAll(".mfe-formula[data-latex]").forEach((formulaNode) => {
            formulaNode.textContent = `[[formula:${formulaNode.getAttribute("data-latex")}]]`;
        });
        tempElement.querySelectorAll("math-field[data-latex]").forEach((mathNode) => {
            mathNode.textContent = `[[formula:${mathNode.getAttribute("data-latex")}]]`;
        });
        tempElement.querySelectorAll("img[src]").forEach((imgNode) => {
            imgNode.textContent = `[[img:${imgNode.getAttribute("src")}]]`;
        });
        const text = tempElement.textContent || tempElement.innerText || "";
        return text.trim().toLowerCase();
    };
 
    const dragDropHTML =
        '<span style="width: 100px; border: 1px dashed #000; margin: 0px 5px; border-radius: 5px; background-color: #e5e5e5; padding: 5px 15px;" contenteditable="false"> Drop here </span>';
    const dropDownMenuHTML = `<span style="width: 100px; border: 1px dashed #000; margin: 0px 5px; border-radius: 5px; background-color: #e5e5e5; padding: 5px 15px;" contenteditable="false"> Menu here </span>`;
 
    const regexBlank = /__blank__/g;
    const regexDropHere = /<span[^>]*>\s*Drop here\s*<\/span>/gi;
    const regexMenuHere = /<span[^>]*>\s*Menu here\s*<\/span>/gi;
 
    /**
     * Embedded Multiple-Choice: unlike the drop-down/drag-drop markers above, the
     * choice text IS the paragraph text — the span wraps the words the teacher
     * highlighted rather than a placeholder — so no string<->HTML round trip is
     * needed for storage. `id` is what correctness is graded against (not the
     * text), so two identical-looking phrases marked separately stay unambiguous.
     */
    const EMBEDDED_CHOICE_ATTR = "data-embedded-choice-id";
 
    // A full outline border, not text-decoration/border-bottom: a math formula
    // renders as a `.mfe-formula { display: inline-block; }` chip (see
    // math-formula/styles.css), and text-decoration on an ancestor never paints
    // through an atomic inline-level descendant like an inline-block. A border
    // on the wrapper's own box always renders regardless of what's inside it.
    //
    // display:inline-block on the wrapper itself, not plain inline: a plain
    // inline element's border is sized to the LINE BOX (roughly font
    // ascent/descent), not to a taller inline-block descendant — a fraction or
    // square root inside a formula choice is taller than a text line, so the
    // border cut straight through the middle of it instead of enclosing it.
    // inline-block sizes the box to its actual (taller) content instead.
    const buildEmbeddedChoiceHTML = (id, text) =>
        `<span ${EMBEDDED_CHOICE_ATTR}="${id}" class="embedded-choice-mark" contenteditable="false" ` +
        `style="display:inline-block;vertical-align:middle;border:2px dashed;padding:1px 3px;border-radius:3px;">${text}</span>`;
 
    const extractEmbeddedChoices = (html) => {
        if (!html || typeof document === "undefined") return [];
        const container = document.createElement("div");
        container.innerHTML = html;
        return Array.from(container.querySelectorAll(`[${EMBEDDED_CHOICE_ATTR}]`)).map((node) => ({
            id: node.getAttribute(EMBEDDED_CHOICE_ATTR),
            // innerHTML, not textContent — a marked choice can be a math formula
            // (<span class="mfe-formula" data-latex="...">rendered markup</span>),
            // whose visible glyphs are meaningless as flattened plain text.
            text: node.innerHTML || "",
        }));
    };
 
    return {
        hasEditorContent,
        tinyMCEtoString,
        getChoiceComparisonKey,
        dragDropHTML,
        dropDownMenuHTML,
        regexBlank,
        regexDropHere,
        regexMenuHere,
        EMBEDDED_CHOICE_ATTR,
        buildEmbeddedChoiceHTML,
        extractEmbeddedChoices,
    };
};
 
export default tinyMCEUtility;