/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
(function() {
/** @file Contains code to load in the other script files, and initialise the exam.
 * Creates the global {@link Numbas} object, inside which everything else is stored, so as not to conflict with anything else that might be running in the page.
 */
    const _globalThis = (typeof globalThis !== 'undefined') ? globalThis : (typeof global !== 'undefined') ? global : window;
    if(typeof window == 'undefined') {
        window = _globalThis.window = _globalThis;
        _globalThis.alert = function(m) {
            console.error(m);
        }
    }
    if(!_globalThis.Numbas) {
        _globalThis.Numbas = {}
    }

// Polyfill for Promise.withResolvers. Remove once it's reached 95% compatibility.
if(!Promise.withResolvers) {
    Promise.withResolvers = function() {
        const out = {};
        out.promise = new this((resolve, reject) => {
            out.resolve = resolve;
            out.reject = reject;
        });
        return out;
    }
}

/** @namespace Numbas */
/** Extensions should add objects to this so they can be accessed */
Numbas.extensions = {};
/** A function for displaying debug info in the console. It will try to give a reference back to the line that called it, if it can.
 *
 * @param {string} msg - Text to display.
 * @param {boolean} [noStack=false] - Don't show the stack trace.
 * @param {Error} error
 */
Numbas.debug = function(msg, noStack, error) {
    if(window.console) {
        var e = new Error(msg);
        if(e.stack && !noStack) {
            var words = e.stack.split('\n')[2];
            if(error) {
                console.error(msg, error);
            } else {
                console.error(msg, " " + words);
            }
        } else {
            console.log(msg);
        }
    }
};
/** Display an error in a nice alert box. Also sends the error to the console via {@link Numbas.debug}.
 *
 * @param {Error} e
 */
Numbas.showError = function(e) {
    var message = (e || e.message) + '';
    message += ' <br> ' + e.stack.replace(/\n/g, '<br>\n');
    Numbas.debug(message, false, e);
    Numbas.display?.showAlert && Numbas.display.showAlert(message);
    throw(e);
};
/** Generic error class. Extends JavaScript's `Error`.
 *
 * @class
 * @param {string} message - A description of the error. Localised by R.js.
 * @param {object} args - Arguments for the error message.
 * @param {Error} originalError - If this is a re-thrown error, the original error object.
 */
Numbas.Error = function(message, args, originalError) {
    var e = new Error();
    e.name = "Numbas Error";
    e.message = _globalThis.R && R.apply(e, [message, args]);
    e.originalMessage = message;
    e.originalMessages = [message];
    if(originalError !== undefined) {
        e.originalError = originalError;
        if(originalError.originalMessages) {
            e.originalMessages = e.originalMessages.concat(originalError.originalMessages.filter(function(m) {
                return m != message
            }));
        }
    }
    return e;
}

var scriptreqs = Numbas.scriptreqs = {};
/** Keep track of loading status of a script and its dependencies.
 *
 * @param {string} file - Name of the script.
 * @param {Array.<string>} fdeps - Scripts which this one depends on.
 * @param {Function} callback
 * @global
 * @class
 * @property {string} file - Name of the script.
 * @property {boolean} loaded - Has the script been loaded yet?
 * @property {boolean} executed - Has the script been run?
 * @property {Array.<string>} backdeps - Scripts which depend on this one (need this one to run first)
 * @property {Array.<string>} fdeps - Scripts which this one depends on (it must run after them)
 * @property {Function} callback - The function to run when all this script's dependencies have run (this is the script itself)
 */
var RequireScript = Numbas.RequireScript = function(file, fdeps, callback) {
    this.file = file;
    scriptreqs[file] = this;
    this.backdeps = [];
    this.fdeps = fdeps || [];
    this.callback = callback;

    const {promise, resolve} = Promise.withResolvers();
    this.promise = promise;
    this.resolve = resolve;

    this.promise.then(() => {
        this.callback;
    });
}
RequireScript.prototype = {
    loaded: false,
    executed: false,
    backdeps: [],
    fdeps: [],
    callback: null,


    /** Try to run this script. It will run if all of its dependencies have run.
     * Once it has run, every script which depends on it will try to run.
     */
    script_loaded: function() {
        Promise.all(this.fdeps.map((r) => scriptreqs[r].promise)).then(() => {
            this.executed = true;

            if(this.callback) {
                var module = { exports: {} };
                this.callback.apply(window, [module]);
                for(var x in module.exports) {
                    window[x] = module.exports[x];
                    if(typeof global !== 'undefined') {
                        global[x] = module.exports[x];
                    }
                }
            }

            this.resolve();
        });
    }
};
/** Ask to load a javascript file. Unless `noreq` is set, the file's code must be wrapped in a call to Numbas.queueScript with its filename as the first parameter.
 *
 * @memberof Numbas
 * @param {string} file
 * @param {boolean} noreq - Don't create a {@link Numbas.RequireScript} object.
 * @returns {Numbas.RequireScript}
 */
var loadScript = Numbas.loadScript = function(file, noreq) {
    if(!noreq) {
        if(scriptreqs[file] !== undefined) {
            return scriptreqs[file];
        }
        var req = new RequireScript(file);
        return req;
    }
    return scriptreqs[file];
}

/**
 * Queue up a file's code to be executed.
 * Each script should be wrapped in this function.
 *
 * @param {string} file - Name of the script.
 * @param {Array.<string>} deps - A list of other scripts which need to be run before this one can be run.
 * @param {Function} callback - A function wrapping up this file's code.
 *
 * @returns {Promise} - Resolves when the file has been executed.
 */
Numbas.queueScript = function(file, deps, callback) {
    if(typeof(deps) == 'string') {
        deps = [deps];
    }

    for(var i = 0;i < deps.length;i++) {
        var dep = deps[i];
        deps[i] = dep;
        loadScript(dep);
        scriptreqs[dep].backdeps.push(file);
    }

    var req = scriptreqs[file];
    if(req) {
        req.fdeps = deps;
        req.callback = callback;
    } else {
        req = new RequireScript(file, deps, callback);
    }
    req.script_loaded();

    return req.promise;
}

/** Empty; kept for backwards compatibility. */
Numbas.tryInit = function() {
}


Numbas.awaitScripts = function(deps) {
    return Promise.all(deps.map((file) => loadScript(file).promise));
}

var extension_callbacks = {};
/** A wrapper round {@link Numbas.queueScript} to register extensions easily.
 * The extension is not run immediately - call {@link Numbas.activateExtension} to run the extension.
 *
 * @param {string} name - Unique name of the extension.
 * @param {Array.<string>} deps - A list of other scripts which need to be run before this one can be run.
 * @param {Function} callback - Code to set up the extension. It's given the object `Numbas.extensions.<name>` as a parameter, which contains a {@link Numbas.jme.Scope} object.
 *
 * @returns {Promise} - Resolves when the extension has been activated.
 */
Numbas.addExtension = function(name, deps, callback) {
    deps.push('jme');
    return Numbas.queueScript('extensions/' + name + '/' + name + '.js', deps, function() {
        var extension = Numbas.extensions[name] = {
            scope: new Numbas.jme.Scope()
        };
        extension_callbacks[name] = {
            callback: callback,
            extension: extension,
            activated: false
        }
    });
}

Numbas.extension_url_root = {};

/**
 * Get the URL of a standalone file from an extension.
 *
 * @param {string} extension - The name of the extension.
 * @param {string} path - The path to the script, relative to the extension's `standalone_scripts` folder.
 * @returns {string}
 */
Numbas.getStandaloneFileURL = function(extension, path) {
    const root = Numbas.extension_url_root[extension] || `extensions/${extension}`;
    return root + '/standalone_scripts/' + path;
}

/**
 * Load a standalone script from an extension.
 * Inserts a <script> tag into the page's head.
 *
 * @param {string} extension - The name of the extension.
 * @param {string} path - The path to the script, relative to the extension's `standalone_scripts` folder.
 * @param {string} [type] - The type of the script, such as `"module"`.
 */
Numbas.loadStandaloneScript = function(extension, path, type) {
    var script = document.createElement('script');
    if(type) { 
        script.setAttribute('type',type);
    }
    script.setAttribute('src', Numbas.getStandaloneFileURL(extension, path));
    document.head.appendChild(script);
}

/** Run the extension with the given name. The extension must have already been registered with {@link Numbas.addExtension}.
 *
 * @param {string} name
 */
Numbas.activateExtension = function(name) {
    var cb = extension_callbacks[name];
    if(!cb) {
        throw(new Numbas.Error("extension.not found", {name: name}));
    }
    if(!cb.activated) {
        cb.callback(cb.extension);
        cb.activated = true;
    }
}

/** Check all required scripts have executed - the theme should call this once the document has loaded.
 *
 * @returns {Array.<object>} A list of files which have not loaded.
 */
Numbas.checkAllScriptsLoaded = function() {
    var fails = [];
    for(var file in scriptreqs) {
        var req = scriptreqs[file];
        if(req.executed) {
            continue;
        }
        if(req.fdeps.every(function(f) {
            return scriptreqs[f].executed
        })) {
            var err = new Numbas.Error('die.script not loaded', {file:req.file});
            Numbas.display && Numbas.display.die(err);
        }
        fails.push({file: req.file, req: req, fdeps: req.fdeps.filter(function(f) {
            return !scriptreqs[f].executed
        })});
    };
    return fails;
}
})();

/** Resources to do with localisation: `preferred_locale` is the code of the locale to use, and `resources` is a dictionary of localisations.
 *
 * @name locale
 * @memberof Numbas
 * @type {object}
 */

/** Definitions of marking scripts for the built-in part types.
 *
 * @name raw_marking_scripts
 * @memberof Numbas
 * @type {Object<string>}
 */

/** Marking scripts for the built-in part types.
 *
 * @name marking_scripts
 * @memberof Numbas
 * @type {Object<Numbas.marking.MarkingScript>}
 */
;
Numbas.queueScript('diagnostic_scripts', ['diagnostic', 'marking'], function() {
            Numbas.raw_diagnostic_scripts = {"mastery": "// Mastery diagnostic script\n// The student must answer every question correctly.\n// They start with a topic that has no dependencies.\n// After answering a question, if they get it correct, it's done forever.\n// If it's incorrect, the question is put on the end of that topic's \"queue\", \n// so they'll be asked it again later.\n// Once all the questions in the topic are answered correctly, the next topic\n// with no unmet dependencies is picked.\n\n//////////////\n// Functions\n//////////////\n\nupdate_where (Update items in a list which satisfy the given predicate, applying the given function to them):\n    ((predicate, action, list) -> (if(predicate(x), action(x), x) for: x of: list))\n\n\nquestion_queue_for_topic (When starting a topic, this function makes a queue of questions which must be answered):\n    (topic) -> (\n        [\"question\": q, \"status\": \"unknown\"]\n        for: q\n        of: topic[\"topic\"][\"questions\"]\n    )\n\n\nstart_topic (A function to update the state, setting the current topic and filling the question queue from that topic):\n    (state,topic) -> \n        merge(\n            state,\n            [\n                \"current_topic\": topic,\n                \"question_queue\": question_queue_for_topic(topic)\n            ]\n        )\n\n\nget_next_question (A function to get the next question from the queue):\n    (state) -> \n        let(\n            queue, state[\"question_queue\"],\n\n            if(len(queue)>0,\n                queue[0][\"question\"], \n                nothing\n            )\n        )\n\n\nnext_topic (The next topic to assess):\n    (state) ->\n        let(\n            topics, state[\"topics\"], // List of the state object for each topic\n\n            topicdict, dict([t[\"topic\"][\"name\"],t] for: t of: topics), // A mapping from topic names to topic state objects\n\n            available_topics,   // Topics that we can move to next: either no dependencies, or all their dependencies have been passed.\n                filter(\n                    t -> let(\n                        all_deps_passed, all(topicdict[topicname][\"status\"] <> \"unknown\" for: topicname of: t[\"topic\"][\"depends_on\"]),\n                        all_deps_passed and t[\"status\"]=\"unknown\"\n                    )\n                    , topics\n                ),\n\n           if(len(available_topics)>0,available_topics[0],nothing)\n        )\n\n\n/////////////////////\n// Initial variables\n/////////////////////\n\nfirst_topic (The first topic to assess):\n    // Picks the first topic which doesn't depend on anything.\n    let(\n        topics, pre_state[\"topics\"],\n        filter(t -> len(t[\"topic\"][\"depends_on\"])=0, topics)[0]\n    )\n\n\nfirst_question (The first question to show the student):\n    get_next_question(state)\n\n\npre_state (A template for the `state` variable, which will be filled in with the chosen start topic):\n    [\n        \"topics\":   // For each topic, both the given info about that topic and a status, either \"passed\" or \"unknown\".\n            [\n                \"topic\": topic,\n                \"status\": if(len(topic[\"questions\"])=0,\"passed\",\"unknown\")  // A topic is \"passed\" if there are no questions left unasked.\n            ]\n            for: topic\n            of: values(topics)\n        ,\n        \"finished\": false   // Is the exam over?\n    ]\n\n\nstate (The initial state variable):\n    start_topic(pre_state, first_topic)\n\n\n/////////////////////////////\n// Notes used when moving on\n/////////////////////////////\n\ncorrect (Did the student get the current question right?):\n    current_question[\"credit\"]=1\n\n\nafter_answering (The state after the student answers a question):\n    let(\n        queue, state[\"question_queue\"],\n        empty_queue, len(queue) = 0,\n\n        nquestion, \n            // Set the status of this question in the queue.\n            if(not empty_queue, \n              merge(\n                queue[0],\n                [\"status\": if(correct,\"passed\",\"failed\")]\n              ), \n              nothing\n            ),\n\n        nqueue, \n            // Change the queue: either remove the current question if correct, or add it to the end.\n            queue[1..len(queue)] + if(correct or empty_queue, [], [nquestion]), \n\n        ntopics,\n            // Update the list of topics, setting the current topic to \"passed\" if the queue is now empty.\n            if(len(nqueue)=0,\n                update_where(t -> t=state[\"current_topic\"], t -> t+[\"status\": \"passed\"], state[\"topics\"]),\n                state[\"topics\"]\n            ),\n\n        merge(\n            // Return a new state with the new list of topics and question queue\n            state,\n            [\"topics\": ntopics, \"question_queue\": nqueue]\n        )\n    )\n\n\n///////////\n// Actions\n///////////\n\naction_next_question_same_topic (Move to the next question in the queue):\n    [\n        \"label\": translate(\"diagnostic.move to next question in topic\"),\n        \"state\": after_answering,\n        \"next_question\": get_next_question(after_answering)\n    ]\n\naction_next_topic (Move to the next topic):\n    let(\n        state, after_answering, // Start with the state we get from answering the question.\n\n        topic, next_topic(state), // Pick a new topic.\n\n        nstate, \n            if(topic <> nothing, \n                start_topic(state, topic)   // Update the state with the new topic.\n            , \n                state   // Otherwise, there's no next topic, so this action won't be used.\n            ),\n\n        [\n            \"label\": translate(\"diagnostic.move to next topic\"),\n            \"state\": nstate,\n            \"next_question\": get_next_question(nstate)\n        ]\n    )\n\nnext_actions (The list of possible actions after answering a question):\n    let(\n        state, after_answering,\n        queue_empty, len(state[\"question_queue\"])=0,\n        actions, \n            switch(\n                not queue_empty,\n                    [action_next_question_same_topic]   // Move to the next question in the queue\n            ,   next_topic(state) <> nothing,\n                    [action_next_topic] // Move to the next topic\n            ,\n                []  // End the exam\n            ),\n\n        [\n            \"feedback\": \"\",\n            \"actions\": actions\n        ]\n    )\n\nafter_exam_ended (The state after the exam has finished):\n    merge(\n        after_answering,\n        [\"finished\": true]\n    )\n\n\n//////////////////\n// Feedback notes\n//////////////////\n\nprogress (Summarise the student's progress through the exam):\n    let(\n        passed_topics, filter(t -> t[\"status\"] = \"passed\", state[\"topics\"])\n    ,   num_passed_topics, len(passed_topics)\n    ,   num_topics, len(state[\"topics\"])\n    ,   exam_progress, num_passed_topics/num_topics\n    ,   topic_credit, 1-len(state[\"question_queue\"])/len(state[\"current_topic\"][\"topic\"][\"questions\"])\n    ,   current_topic, state[\"current_topic\"][\"topic\"][\"name\"]\n    ,   lo_progress,\n            let(\n                ltopics, filter(t -> lo[\"name\"] in t[\"topic\"][\"learning_objectives\"], state[\"topics\"]),\n                passed, filter(t -> t[\"status\"]=\"passed\", ltopics),\n                p, len(passed)/len(ltopics),\n                [\"name\": lo[\"name\"], \"progress\": p, \"credit\": p]\n            )\n            for: lo\n            of: learning_objectives\n    ,   topic_progress, [[\"name\": \"Current topic: {current_topic}\", \"progress\": topic_credit, \"credit\": topic_credit]]\n\n    ,   if(state[\"finished\"], [], topic_progress)\n          + lo_progress\n          + [\n                [\"name\": translate(\"control.total\"), \"progress\": exam_progress, \"credit\": exam_progress]\n            ]\n    )\n\nfeedback (A text description of the current state): \n    if(state[\"finished\"],\n        translate(\"diagnostic.complete\")\n    ,\n        translate(\"diagnostic.studying topic\", [\"topic\": state[\"current_topic\"][\"topic\"][\"name\"]])\n    )\n\n", "diagnosys": "state (Produces the initial value of the state object): // should be renamed \"initial_state\"\n    [\n        \"topics\": map(\n            [\n                \"topic\": topic,\n                \"status\": \"unknown\" // \"unknown\", \"passed\", or \"failed\"\n            ],\n            topic,\n            values(topics)\n        ),\n        \"retries\": 3,\n        \"finished\": false,\n    ]\n\ntopics_by_objective (A dictionary mapping a learning objective name to a list of indices of topics):\n    dict(map(\n        let(\n            ltopics, values(topics),\n            indices, filter(lo[\"name\"] in ltopics[j][\"learning_objectives\"], j, 0..len(ltopics)-1),\n            [lo[\"name\"],indices]\n        ),\n        lo,\n        learning_objectives\n    ))\n\nunknown_topics (Which topics are still unknown?): \n    map(x[\"topic\"],x,filter(x[\"status\"]=\"unknown\",x,state[\"topics\"]))\n\nfirst_topic (The first topic to pick a question on):\n    unknown_topics[floor(len(unknown_topics)/2)][\"name\"]\n\nfirst_question (The first question to show the student):\n    random(topics[first_topic][\"questions\"])\n\nget_dependents (An expression which gets the topics to update after answering a question):\n    expression(\"\"\"\n        [target] + flatten(map(eval(get_dependents,[\"target\":t,\"correct\":correct]),t,topics[target][if(correct,\"depends_on\",\"leads_to\")]))\n    \"\"\")\n\ncorrect (Did the student get the current question right?):\n    current_question[\"credit\"]=1\n\nafter_answering (Update the state after the student answers a question):\n    let(\n        ntopics, eval(get_dependents,[\"target\":current_topic,\"correct\":correct])\n    ,   nstate, state + ['topics': map(\n                    if(tstate[\"topic\"][\"name\"] in ntopics, tstate + [\"status\":if(correct,\"passed\",\"failed\")], tstate),\n                    tstate,\n                    state[\"topics\"]\n                )]\n    , nstate\n    )\n\naction_retry (Use up one retry and visit the same topic again):\n    [\n        \"label\": translate(\"diagnostic.use retry\"),\n        \"state\": state + [\"retries\": state[\"retries\"]-1],\n        \"next_question\": random(topics[current_topic][\"questions\"])\n    ]\n\naction_stop (Stop the exam):\n    [\n        \"label\": translate(\"diagnostic.end test\"),\n        \"state\": state,\n        \"next_question\": nothing\n    ]\n\naction_move_on (Move to the next topic, or end the exam if there are no more):\n    let(\n        state, after_answering,\n        immediate_next_topics, topics[current_topic][if(correct, \"leads_to\", \"depends_on\")],\n        unknown_topics, map(x[\"topic\"],x,filter(x[\"status\"]=\"unknown\",x,state[\"topics\"])),\n        unknown_immediate_topics, filter(x[\"name\"] in immediate_next_topics,x,unknown_topics),\n        next_topics, if(len(unknown_immediate_topics), unknown_immediate_topics, unknown_topics),\n        finished, len(next_topics)=0 or state[\"finished\"],\n        topic,\n            if(not finished,\n                next_topics[floor(len(next_topics)/2)][\"name\"]\n            ,\n                nothing\n            ),\n        [\n            \"label\": translate(\"diagnostic.move to next topic\"),\n            \"state\": after_answering,\n            \"next_question\": if(not finished, random(topics[topic][\"questions\"]), nothing)\n        ]\n    )\n\ncan_move_on:\n    action_move_on[\"next_question\"]<>nothing\n\nnext_actions (Actions to offer to the student when they ask to move on):\n    let(\n        feedback, retries_feedback+\"\\n\\n\"+translate(\"diagnostic.next step question\")\n    ,   [\n            \"feedback\": feedback,\n            \"actions\": if(not correct and state[\"retries\"]>0, [action_retry], []) + if(can_move_on,[action_move_on],[action_stop])\n        ]\n    )\n\nafter_exam_ended (Update the state after the exam ends):\n    let(\n        state, after_answering,\n        ntopics, map(t+[\"status\": if(t[\"status\"]=\"unknown\",\"failed\",t[\"status\"])],t,state[\"topics\"]),\n        state+[\"finished\": true]\n    )\n\nfinished (Is the test finished? True if there are no unknown topics):\n    len(unknown_topics)=0 or state[\"finished\"]\n\ntotal_progress:\n    let(\n        num_topics, len(state[\"topics\"]),\n        known, filter(tstate[\"status\"]<>\"unknown\",tstate,state[\"topics\"]),\n        passed, filter(tstate[\"status\"]=\"passed\",tstate,known),\n        num_known, len(known),\n        num_passed, len(passed),\n        [\n            \"name\": translate(\"control.total\"),\n            \"progress\": if(num_topics>0,num_known/num_topics,0), \n            \"credit\": if(num_known>0,num_passed/num_topics,0)\n        ]\n    )\n\nlearning_objective_progress:\n    map(\n        let(\n            tstates, map(state[\"topics\"][j],j,topics_by_objective[lo[\"name\"]]),\n            known, filter(tstate[\"status\"]<>\"unknown\",tstate,tstates),\n            passed, filter(tstate[\"status\"]=\"passed\",tstate,known),\n            num_topics, len(tstates),\n            num_known, len(known),\n            num_passed, len(passed),\n            [\"name\": lo[\"name\"], \"progress\": if(num_topics>0,num_known/num_topics,0), \"credit\": if(finished,num_passed/num_topics,if(num_known>0,num_passed/num_known,0))]\n        ),\n        lo,\n        learning_objectives\n    )\n\nprogress (Progress on each of the learning objectives, plus total progress):\n    learning_objective_progress+\n    total_progress\n\nretries_feedback:\n    translate(\"diagnostic.now assessing topic\", [\"current_topic\": current_topic]) + \" \" +\n    let(\n        retries, state[\"retries\"], \n        pluralise(retries, translate(\"diagnostic.one retry left\"), translate(\"diagnostic.retries left\", [\"retries\": retries ]))\n    )\n    + \" \" +\n    let(\n        p,total_progress[\"progress\"],\n        percentage, dpformat(100p, 0),\n        translate(\"diagnostic.percentage completed\", [\"percentage\": percentage])\n    )\n\nweak_objective_threshold (The amount of credit below which a learning objective is considered weak):\n    0.6\n\nfinished_feedback:\n    let(\n        weak_objectives, filter(p[\"credit\"]<weak_objective_threshold, p, learning_objective_progress),\n    //\n        translate(\"diagnostic.test is over\") + \" \"\n        +\n        if(len(weak_objectives)=0,\n            translate(\"diagnostic.passed all lo\")\n        ,\n            translate(\"diagnostic.more work on lo\", [\"los\": \n                join(map(lo[\"name\"], lo, weak_objectives),\", \")])\n        )\n    )\n\nfeedback:\n    if(finished,\n        finished_feedback,\n        retries_feedback\n    )\n\n"};
        });
        ;

        Numbas.queueScript('localisation-resources', ['i18next'], function() {
        Numbas.locale = {
            preferred_locale: "en-GB",
            resources: {"ja-jp": {"translation": {"page.loading": "\u8aad\u307f\u8fbc\u307f\u4e2d...", "page.saving": "\u4fdd\u5b58\u4e2d\u3002\u6570\u79d2\u9593\u304b\u304b\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002", "mathjax.math processing error": "\"{{-message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "Numbas has failed", "die.sorry": "\u7533\u3057\u8a33\u3042\u308a\u307e\u305b\u3093\u3002Numbas\u306f\u30a8\u30e9\u30fc\u3092\u767a\u751f\u3057\u3053\u308c\u4ee5\u4e0a\u7d9a\u884c\u3067\u304d\u307e\u305b\u3093\u3002\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306f\u4ee5\u4e0b\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "die.error": "\u30a8\u30e9\u30fc", "modal.ok": "OK", "modal.cancel": "\u30ad\u30e3\u30f3\u30bb\u30eb", "exam.exam name": "\u8a66\u9a13\u540d:", "exam.random seed": "\u30bb\u30c3\u30b7\u30e7\u30f3ID:", "exam.student name": "\u5b66\u751f\u6c0f\u540d:", "exam.number of questions": "\u554f\u984c\u6570:", "exam.marks available": "\u63a1\u70b9\u53ef\u80fd:", "exam.pass percentage": "\u5408\u683c\u7387:", "exam.time allowed": "\u5236\u9650\u6642\u9593:", "exam.passed": "\u5408\u683c", "exam.failed": "\u4e0d\u5408\u683c", "exam.review header": "\u8b1b\u8a55:\u2423", "frontpage.start": "\u958b\u59cb", "suspend.paused header": "\u4e00\u6642\u4f11\u6b62\u4e2d", "suspend.exam suspended": "\u8a66\u9a13\u306f\u4e2d\u65ad\u3055\u308c\u307e\u3057\u305f\u3002<em>\u518d\u958b</em>\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u7d9a\u3051\u3066\u304f\u3060\u3055\u3044\u3002", "suspend.you can resume": "\u6b21\u56de\u3053\u306e\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u3092\u59cb\u3081\u308b\u3068\u304d\uff0c\u30bb\u30c3\u30b7\u30e7\u30f3\u3092\u518d\u958b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", "suspend.resume": "\u518d\u958b", "result.exit": "\u8a66\u9a13\u7d42\u4e86", "result.print": "Print this results summary", "result.exam summary": "\u8a66\u9a13\u6982\u8981", "result.performance summary": "\u6210\u7e3e\u306e\u6982\u8981", "result.exam start": "\u8a66\u9a13\u958b\u59cb:", "result.exam stop": "\u8a66\u9a13\u7d42\u4e86:", "result.time spent": "\u7d4c\u904e\u6642\u9593:", "result.questions attempted": "Questions Attempted:", "result.score": "\u5f97\u70b9:", "result.result": "\u7d50\u679c:", "result.question number": "\u554f\u984c\u756a\u53f7", "result.question score": "\u70b9\u6570", "result.question review title": "Review this question", "result.click a question to review": "\u554f\u984c\u756a\u53f7\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\uff0c\u3042\u306a\u305f\u306e\u89e3\u7b54\u304c\u3069\u306e\u3088\u3046\u306b\u63a1\u70b9\u3055\u308c\u305f\u306e\u304b\uff0c\u305d\u3057\u3066\uff0c\u53ef\u80fd\u306a\u3089\u6a21\u7bc4\u89e3\u7b54\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002", "end.exam has finished": "\u8a66\u9a13\u306f\u7d42\u4e86\u3057\u307e\u3057\u305f\u3002\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u3092\u9589\u3058\u3066\u3082\u304b\u307e\u3044\u307e\u305b\u3093\u3002", "control.confirm leave": "\u307e\u3060\u8a66\u9a13\u306f\u7d42\u4e86\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "control.not all questions answered": "\u3053\u306e\u8a66\u9a13\u306e\u554f\u984c\u306f\u5168\u3066\u306f\u5b8c\u4e86\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "control.not all questions submitted": "\uff11\u3064\u4ee5\u4e0a\u306e\u89e3\u7b54\u304c\u4fee\u6b63\u3055\u308c\u307e\u3057\u305f\u304c\u3001\u307e\u3060\u4fee\u6b63\u3055\u308c\u305f\u89e3\u7b54\u306f\u63d0\u51fa\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u5404\u554f\u984c\u304c\u63d0\u51fa\u3055\u308c\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "control.confirm end": "\u672c\u5f53\u306b\u8a66\u9a13\u3092\u7d42\u4e86\u3057\u305f\u3044\u3067\u3059\u304b\uff1f\u8a66\u9a13\u3092\u7d42\u4e86\u3059\u308b\u3068\u3001\u89e3\u7b54\u3092\u5909\u66f4\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", "control.confirm regen": "\u3053\u306e\u554f\u984c\u3092\u518d\u30e9\u30f3\u30c0\u30e0\u5316\u3057\u307e\u3059\u304b\uff1fOK\u3092\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u3001\u73fe\u5728\u306e\u554f\u984c\u306b\u5bfe\u3059\u308b\u3042\u306a\u305f\u306e\u89e3\u7b54\u3068\u70b9\u6570\u304c\u3059\u3079\u3066\u5931\u308f\u308c\u307e\u3059\u3002", "control.confirm reveal": "\u3053\u306e\u554f\u984c\u306e\u7b54\u3048\u3092\u8868\u793a\u3057\u307e\u3059\u304b\uff1f\u73fe\u6642\u70b9\u3067\u306e\u70b9\u6570\u3067\u78ba\u5b9a\u3068\u306a\u308a\u3001\u5f8c\u3067\u3053\u306e\u554f\u984c\u306b\u7b54\u3048\u308b\u3053\u3068\u306f\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", "control.proceed anyway": "\u3068\u306b\u304b\u304f\u5148\u306b\u9032\u307f\u307e\u3059\u304b\uff1f", "control.regen": "\u540c\u985e\u306e\u4ed6\u306e\u554f\u984c\u3092\u3084\u308a\u307e\u3057\u3087\u3046", "control.submit answer": "\u89e3\u7b54\u306e\u63d0\u51fa", "control.submit all parts": "\u5168\u3066\u306e\u30d1\u30fc\u30c8\u3092\u63d0\u51fa", "control.submit again": "\u518d\u63d0\u51fa", "control.submit": "\u63d0\u51fa", "control.previous": "\u524d", "control.next": "\u6b21", "control.advice": "\u30a2\u30c9\u30d0\u30a4\u30b9", "control.reveal": "Reveal answers", "control.total": "\u5408\u8a08", "control.pause": "\u4e00\u6642\u4f11\u6b62\u3059\u308b", "control.end exam": "\u8a66\u9a13\u3092\u7d42\u4e86\u3059\u308b", "control.back to results": "\u7d50\u679c\u306b\u623b\u308b", "display.part.jme.error making maths": "\u6570\u5f0f\u8868\u793a\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "\u3053\u306e\u8a66\u9a13\u306b\u306f\u554f\u984c\u304c\u3042\u308a\u307e\u305b\u3093\uff01.exam\u30d5\u30a1\u30a4\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "feedback.you were awarded": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid": "\u7121\u52b9\u306a\u5f0f\u3067\u3059: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "\u6f14\u7b97 {{op}}\u306e\u305f\u3081\u306e\u5909\u6570\u304c\u8db3\u308a\u307e\u305b\u3093\u3002", "jme.shunt.no left bracket in function": "\u95a2\u6570\u3084\u7d44\u306b\u5bfe\u5fdc\u3059\u308b\u5de6\u62ec\u5f27\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.shunt.no left square bracket": "\u5bfe\u5fdc\u3059\u308b\u5de6\u62ec\u5f27\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.shunt.no left bracket": "\u5bfe\u5fdc\u3059\u308b\u5de6\u62ec\u5f27\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.shunt.no right bracket": "\u5bfe\u5fdc\u3059\u308b\u53f3\u62ec\u5f27\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.shunt.no right square bracket": "\u30ea\u30b9\u30c8\u306e\u7d42\u308f\u308a\u306b\u5bfe\u5fdc\u3059\u308b\u53f3\u306e\u89d2\u62ec\u5f27\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.shunt.missing operator": "\u5f0f\u304c\u8a55\u4fa1\u3067\u304d\u307e\u305b\u3093\u3002-- \u6f14\u7b97\u5b50\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.typecheck.function maybe implicit multiplication": "\u6f14\u7b97 {{name}} \u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u3042\u306a\u305f\u304c\u8868\u3057\u305f\u304b\u3063\u305f\u306e\u306f <br/><code>{{first}}*{{possibleOp}}(...)</code>\u3067\u3059\u304b\uff1f", "jme.typecheck.function not defined": "\u6f14\u7b97 <code>{{op}}</code> \u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002 <code>{{op}}</code> \u306f\u5909\u6570\u3067\u3059\u304b\u3001\u3042\u306a\u305f\u304c\u8868\u3057\u305f\u304b\u3063\u305f\u306e\u306f <code>{{suggestion}}*(...)</code> \u3067\u3059\u304b\uff1f", "jme.typecheck.op not defined": "\u6f14\u7b97 '{{op}}' \u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "jme.typecheck.no right type definition": "No definition of '{{op}}' of correct type found.", "jme.typecheck.no right type unbound name": "\u5909\u6570 <code>{{name}}</code> \u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "jme.typecheck.map not on enumerable": "\u6f14\u7b97<code>map</code>\u306f\u3001{{type}}\u3067\u306f\u306a\u304f\u3001\u30ea\u30b9\u30c8\u3084\u7bc4\u56f2\u306b\u5bfe\u3057\u3066\u52d5\u4f5c\u3057\u307e\u3059\u3002", "jme.evaluate.undefined variable": "\u5909\u6570 {{name}} \u306f\u672a\u5b9a\u7fa9\u3067\u3059\u3002", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.matrix.invalid row type": "Can't construct a matrix from rows of type {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Missing parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "{{op}}\u306b\u5bfe\u5fdc\u3059\u308b<code>}</code>\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "\u95a2\u6570\u5b9a\u7fa9\u306e\u6587\u6cd5\u30a8\u30e9\u30fc", "jme.variables.variable not defined": "\u5909\u6570 <code>{{name}}</code> \u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "jme.variables.empty definition": "\u5909\u6570 <code>{{name}}</code> \u306e\u5b9a\u7fa9\u304c\u7a7a\u3067\u3059\u3002", "jme.variables.circular reference": "<code>{{name}}</code> \u306e\u5b9a\u7fa9\u306e\u4e2d\u3067\u5909\u6570\u306e\u53c2\u7167\u304c\u5faa\u74b0\u3057\u3066\u3044\u307e\u3059\u3002", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "\u8907\u7d20\u6570\u306e\u7d44\u307f\u5408\u308f\u305b\u306e\u6570\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "math.permutations.complex": "\u8907\u7d20\u6570\u306e\u9806\u5217\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "math.gcf.complex": "\u8907\u7d20\u6570\u306e\u6700\u5927\u516c\u7d04\u6570(GCD)\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "math.lcm.complex": "\u8907\u7d20\u6570\u306e\u6700\u5c0f\u516c\u500d\u6570(LCM)\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "math.lt.order complex numbers": "\u8907\u7d20\u6570\u3092\u9806\u5e8f\u4ed8\u3051\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093\u3002", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "\u6b63\u65b9\u884c\u5217\u3067\u306a\u3044\u884c\u5217\u306e\u884c\u5217\u5f0f\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "matrixmath.abs.too big": "\u3059\u307f\u307e\u305b\u3093\u30013\u6b21\u4ee5\u4e0a\u306e\u6b63\u65b9\u884c\u5217\u306e\u884c\u5217\u5f0f\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "matrixmath.mul.different sizes": "\u30b5\u30a4\u30ba\u306e\u7570\u306a\u308b\u884c\u5217\u306e\u639b\u3051\u7b97\u306f\u3067\u304d\u307e\u305b\u3093\u3002", "vectormath.cross.not 3d": "3\u6b21\u5143\u30d9\u30af\u30c8\u30eb\u306e\u307f\u5916\u7a4d\u3092\u8a08\u7b97\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", "vectormath.dot.matrix too big": "$1 \\times N$ or $N \\times 1$ \u3067\u306f\u306a\u3044\u884c\u5217\u306e\u30c9\u30c3\u30c8\u7a4d\u306f\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093\u3002", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "\u89e3\u7b54: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "\u6700\u521d\u306b {{part}} \u3092\u89e3\u7b54\u3057\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002", "part.marking.resubmit because of variable replacement": "\u3053\u306e\u90e8\u5206\u306e\u63a1\u70b9\u306f\u3001\u3042\u306a\u305f\u304c\u5909\u66f4\u3057\u305f\u4ed6\u306e\u90e8\u5206\u306e\u89e3\u7b54\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002\u70b9\u6570\u3092\u66f4\u65b0\u3059\u308b\u305f\u3081\u306b\u3053\u306e\u90e8\u5206\u3092\u518d\u63d0\u51fa\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "part.marking.not submitted": "\u89e3\u7b54\u304c\u63d0\u51fa\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "part.marking.did not answer": "\u3053\u306e\u554f\u984c\u306b\u89e3\u7b54\u3057\u3066\u3044\u307e\u305b\u3093", "part.marking.nothing entered": "\u89e3\u7b54\u3092\u5165\u529b\u3057\u3066\u3044\u307e\u305b\u3093", "part.marking.incorrect": "\u9593\u9055\u3044\u3067\u3059", "part.marking.correct": "\u6b63\u89e3\u3067\u3059", "part.marking.uncaught error": "{{part}}: {{-message}} \u63a1\u70b9\u6642\u306e\u30a8\u30e9\u30fc", "part.marking.no result": "\u3053\u306e\u90e8\u5206\u306f\u63a1\u70b9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "part.correct answer": "\u671f\u5f85\u3055\u308c\u308b\u89e3\u7b54:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "\u6b63\u89e3\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "part.jme.answer too long": "\u89e3\u7b54\u304c\u9577\u3059\u304e\u307e\u3059\u3002", "part.jme.answer too short": "\u89e3\u7b54\u304c\u77ed\u3059\u304e\u307e\u3059\u3002", "part.jme.answer invalid": "\u89e3\u7b54\u304c\u6570\u5f0f\u3068\u3057\u3066\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", "part.jme.marking.correct": "\u3042\u306a\u305f\u306e\u89e3\u7b54\u306f\u6570\u5024\u7684\u306b\u306f\u6b63\u3057\u3044\u3067\u3059\u3002", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "\u89e3\u7b54\u306f {{strings}} \u3092\u542b\u307e\u306a\u304f\u3066\u306f\u3044\u3051\u307e\u305b\u3093\u3002", "part.jme.must-have several": "\u89e3\u7b54\u306f {{strings}} \u306e\u3059\u3079\u3066\u3092\u542b\u307e\u306a\u304f\u3066\u306f\u3044\u3051\u307e\u305b\u3093\u3002", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "\u89e3\u7b54\u306f {{strings}} \u3092\u542b\u3093\u3067\u306f\u3044\u3051\u307e\u305b\u3093\u3002", "part.jme.not-allowed several": "\u89e3\u7b54\u306f {{strings}} \u306e\u3046\u3061\u306e\u3069\u308c\u3082\u542b\u3093\u3067\u306f\u3044\u3051\u307e\u305b\u3093\u3002", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "\u3042\u306a\u305f\u306e\u89e3\u7b54\u306f\u6709\u52b9\u306a\u7bc4\u56f2\u5185\u306b\u3042\u308a\u307e\u3059\u304c\u3001\u5c0f\u6570\u3067\u7b54\u3048\u3066\u306f\u3044\u3051\u307e\u305b\u3093\u3002", "part.numberentry.correct except fraction": "\u3042\u306a\u305f\u306e\u89e3\u7b54\u306f\u6709\u52b9\u306a\u7bc4\u56f2\u5185\u306b\u3042\u308a\u307e\u3059\u304c\u3001\u5206\u6570\u3067\u7b54\u3048\u3066\u306f\u3044\u3051\u307e\u305b\u3093\u3002", "part.numberentry.answer invalid": "\u6709\u52b9\u306a\u6570\u304c\u5165\u529b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "part.numberentry.answer not integer": "\u7121\u52b9\u306a\u89e3\u7b54\u3067\u3059\u3002\u5c0f\u6570\u3067\u306f\u306a\u304f\u30010\u4ee5\u4e0a\u306e\u6574\u6570\u3092\u5165\u529b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "part.numberentry.answer not integer or decimal": "\u7121\u52b9\u306a\u89e3\u7b54\u3067\u3059\u3002\u6574\u6570\u3082\u3057\u304f\u306f\u5c0f\u6570\u3092\u5165\u529b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.mcq.options def not a list": "{{properties}} \u3067\u5b9a\u7fa9\u3055\u308c\u308b\u5f0f\u306f\u30ea\u30b9\u30c8\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "\u9078\u629e\u80a2\u306e\u5b9a\u7fa9\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "\u9078\u629e\u80a2\u306e\u9078\u629e\u6570\u304c\u9593\u9055\u3063\u3066\u3044\u307e\u3059", "part.mcq.no choices selected": "\u9078\u629e\u80a2\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "\u6b63\u89e3\u3092\u9078\u3073\u307e\u3057\u305f\u3002", "part.matrix.answer invalid": "\u6709\u52b9\u306a\u89e3\u7b54\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "part.matrix.invalid cell": "\u89e3\u7b54\u306e\u3046\u3061\u3001\u3044\u304f\u3064\u304b\u306e\u6b04\u304c\u7a7a\u3082\u3057\u304f\u306f\u7121\u52b9\u3067\u3059\u3002", "part.matrix.some incorrect": "\u89e3\u7b54\u306e\u3046\u3061\u3001\u3044\u304f\u3064\u304b\u306e\u6b04\u306f\u9593\u9055\u3063\u3066\u3044\u307e\u3059\u304c\u3001\u6b8b\u308a\u306e\u6b04\u306f\u63a1\u70b9\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "part.matrix.empty": "\u307e\u3060\u89e3\u7b54\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "part.matrix.empty cell": "\u89e3\u7b54\u306e\u3046\u3061\u3001\u3044\u304f\u3064\u304b\u306e\u6b04\u304c\u7a7a\u3067\u3059\u3002", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "\u554f {{number}}: {{-message}}", "question.preamble.error": "Error in preamble: {{-message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "\u554f {{number}}", "question.submit part": "Save answer", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.advice": "Advice", "question.no such part": "Can't find part {{path}}.", "question.can not submit": "\u89e3\u7b54\u3092\u63d0\u51fa\u3067\u304d\u307e\u305b\u3093\u3002\u30a8\u30e9\u30fc\u3092\u30c1\u30a7\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "question.answer submitted": "\u63d0\u51fa\u3055\u308c\u305f\u89e3\u7b54", "question.score feedback.show": "\u30d5\u30a3\u30fc\u30c9\u30d0\u30c3\u30af\u3092\u8868\u793a\u3059\u308b", "question.score feedback.hide": "\u30d5\u30a3\u30fc\u30c9\u30d0\u30c3\u30af\u3092\u96a0\u3059", "question.score feedback.answered total actual": "\u70b9\u6570: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Answered.", "question.score feedback.answered actual": "\u70b9\u6570: {{scoreString}}", "question.score feedback.answered": "\u89e3\u7b54\u6e08\u307f\u3002", "question.score feedback.unanswered": "\u672a\u89e3\u7b54\u3002", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "\u6b63\u89e3\u3067\u3059", "question.score feedback.partial": "\u90e8\u5206\u7684\u306b\u6b63\u89e3\u3067\u3059", "question.score feedback.wrong": "\u9593\u9055\u3044\u3067\u3059", "question.selector.unsubmitted changes": "Unsubmitted changes.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "\u6b8b\u308a\u6642\u9593:", "xml.could not load": "XML\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u304c\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002: {{-message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "xml.error in variable definition": "\u5909\u6570 <code>{{name}}</code> \u306e\u5b9a\u7fa9\u306b\u304a\u3051\u308b\u30a8\u30e9\u30fc", "scorm.error initialising": "Error initialising SCORM protocol: {{-message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{-message}}", "scorm.error loading question": "Error loading question {{number}}: {{-message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{-message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "\u63a1\u70b9", "was": "was", "part": "\u90e8\u5206", "gap": "gap", "step": "step", "jme.substituteTree.undefined variable": "\u672a\u5b9a\u7fa9\u306e\u5909\u6570: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "\u3053\u306e\u90e8\u5206\u306b\u5bfe\u3059\u308b\u3042\u306a\u305f\u306e\u5f97\u70b9\u306f\u3001<strong>{{count,niceNumber}}</strong> $t(mark) \u3067\u3057\u305f\u3002", "part.numberentry.precision type.dp": "\u5c11\u6570\u4f4d", "part.numberentry.precision type.dp_plural": "\u5c11\u6570\u4f4d", "part.numberentry.precision type.sigfig": "\u6709\u52b9\u6570\u5b57", "part.numberentry.precision type.sigfig_plural": "\u6709\u52b9\u6570\u5b57", "part.numberentry.give your answer to precision": "{{count,niceNumber}} {{precisionType}}\u306b\u4e38\u3081\u3066\u7b54\u3048\u3066\u304f\u3060\u3055\u3044\u3002", "question.unsubmitted changes": "\u89e3\u7b54\u304c\u4fee\u6b63\u3055\u308c\u307e\u3057\u305f\u304c\u3001\u63d0\u51fa\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u89e3\u7b54\u3092\u78ba\u8a8d\u3057\u3066<strong>\u89e3\u7b54\u3092\u63d0\u51fa\u3059\u308b</strong>\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "question.unsubmitted changes_plural": "\u89e3\u7b54\u304c\u4fee\u6b63\u3055\u308c\u307e\u3057\u305f\u304c\u3001\u63d0\u51fa\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u5404\u90e8\u5206\u306e\u89e3\u7b54\u3092\u78ba\u8a8d\u3057\u3066<strong>\u3059\u3079\u3066\u306e\u89e3\u7b54\u3092\u63d0\u51fa\u3059\u308b</strong>\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "\u63a1\u70b9", "was_plural": "were", "die.script not loaded": "\u30d5\u30a1\u30a4\u30eb<code>{{file}}</code>\u304c\u30ed\u30fc\u30c9\u3055\u308c\u306a\u304b\u3063\u305f\u305f\u3081\u3001Numbas\u306f\u30b9\u30bf\u30fc\u30c8\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30d5\u30a1\u30a4\u30eb\u304c<code>scripts.js</code>\u306b\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u78ba\u304b\u3081\u3066\u304f\u3060\u3055\u3044\u3002", "math.combinations.n less than zero": "\u7d44\u307f\u5408\u308f\u305b\u306e\u6570\u304c\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093: n\u304c0\u672a\u6e80\u3067\u3059\u3002", "math.combinations.k less than zero": "\u7d44\u307f\u5408\u308f\u305b\u306e\u6570\u304c\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093: k\u304c0\u672a\u6e80\u3067\u3059\u3002", "math.combinations.n less than k": "\u7d44\u307f\u5408\u308f\u305b\u306e\u6570\u304c\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093: n\u304ck\u672a\u6e80\u3067\u3059\u3002", "math.permutations.n less than zero": "\u9806\u5217\u304c\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093: n\u304c0\u672a\u6e80\u3067\u3059\u3002", "math.permutations.k less than zero": "\u9806\u5217\u304c\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093: k\u304c0\u672a\u6e80\u3067\u3059\u3002", "math.permutations.n less than k": "\u9806\u5217\u304c\u8a08\u7b97\u3067\u304d\u307e\u305b\u3093: n\u304ck\u672a\u6e80\u3067\u3059\u3002", "part.numberentry.give your answer to precision_0": "\u89e3\u7b54\u3092\u6700\u3082\u8fd1\u3044\u6574\u6570\u306b\u4e38\u3081\u3066\u304f\u3060\u3055\u3044\u3002", "mathjax.error": "MathJax\u30d7\u30ed\u30bb\u30b9\u30a8\u30e9\u30fc: {{-message}}", "mathjax.error with context": "{{context}}\u3067\u306eMathJax\u51e6\u7406\u306e\u30a8\u30e9\u30fc: {{-message}}", "exam.introduction": "\u8a66\u9a13\u306e\u8aac\u660e", "exam.feedback": "\u8a66\u9a13\u306e\u30d5\u30a3\u30fc\u30c9\u30d0\u30c3\u30af\u30e1\u30c3\u30bb\u30fc\u30b8", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "\u30d5\u30a3\u30fc\u30c9\u30d0\u30c3\u30af", "part.numberentry.answer not reduced": "\u65e2\u7d04\u5206\u6570\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002", "part.numberentry.give your answer as a reduced fraction": "\u65e2\u7d04\u5206\u6570\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "\u9078\u629e\u80a2", "part.mcq.answers": "\u89e3\u7b54", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Statement", "ruleset.circular reference": "\u30eb\u30fc\u30eb\u30bb\u30c3\u30c8<code>{{name}}</code>\u306e\u5b9a\u7fa9\u3067\u53c2\u7167\u304c\u5faa\u74b0\u3057\u3066\u3044\u307e\u3059\u3002", "ruleset.set not defined": "\u30eb\u30fc\u30eb\u30bb\u30c3\u30c8{{name}}\u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "answer.number.not a number": "Your answer is not a valid number.", "answer.number.fractions not allowed": "You may not enter a fraction.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "You may not enter fractions.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "exam.enter password": "Password:", "exam.password.correct": "This password is correct. You can start the exam.", "exam.password.incorrect": "This password is incorrect.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Review", "control.confirm regen no marks": "Would you like to re-randomise this question?", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Partially answered", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "zh-cn": {"translation": {"page.loading": "\u7f51\u9875\u8f7d\u5165\u4e2d...", "page.saving": "<p>\u4fdd\u5b58</p>\n<p>\u8fd9\u53ef\u80fd\u9700\u8981\u51e0\u79d2\u949f</p>", "mathjax.math processing error": "\n\"{{-message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "\u56e0\u67d0\u4e9b\u539f\u56e0\uff0cNumbas\u65e0\u6cd5\u542f\u52a8\u3002", "die.sorry": "\u5f88\u62b1\u6b49\uff0c\u7531\u4e8e\u67d0\u4e9b\u9519\u8bef\u5bfc\u81f4Numbas\u65e0\u6cd5\u7ee7\u7eed\u8fd0\u4f5c\u3002\u8bf7\u770b\u4e0b\u9762\u9519\u8bef\u8bf4\u660e\u3002", "die.error": "\u9519\u8bef", "modal.ok": "\u597d\u7684", "modal.cancel": "\u53d6\u6d88", "exam.exam name": "\u8003\u8bd5\u540d\u79f0:", "exam.random seed": "\u4f1a\u8bddID:", "exam.student name": "\u5b66\u751f\u540d\u5b57:", "exam.number of questions": "\u95ee\u9898\u6570\u91cf:", "exam.marks available": "\u53ef\u5f97\u5230\u5206\u6570:", "exam.pass percentage": "\u53ca\u683c\u7387:", "exam.time allowed": "\u5141\u8bb8\u65f6\u95f4:", "exam.passed": "\u53ca\u683c", "exam.failed": "\u4e0d\u53ca\u683c", "exam.review header": "\u56de\u987e:", "frontpage.start": "\u5f00\u59cb", "suspend.paused header": "\u6682\u505c", "suspend.exam suspended": "\u8003\u8bd5\u5df2\u88ab\u6682\u505c\u3002\u8bf7\u6309<em>\u6062\u590d<em>\u6062\u590d\u8003\u8bd5\u3002", "suspend.you can resume": "\u4e0b\u6b21\u5f00\u59cb\u6b64\u6d3b\u52a8\u65f6\uff0c\u60a8\u5c06\u53ef\u4ee5\u7ee7\u7eed\u6b64\u4f1a\u8bdd\u3002", "suspend.resume": "\u6062\u590d", "result.exit": "\u9000\u51fa\u8003\u8bd5", "result.print": "\u6253\u5370\u6b64\u7ed3\u679c\u603b\u7ed3", "result.exam summary": "\u8003\u8bd5\u603b\u7ed3", "result.performance summary": "\u8868\u73b0\u603b\u7ed3", "result.exam start": "\u8003\u8bd5\u5f00\u59cb:", "result.exam stop": "\u8003\u8bd5\u7ed3\u675f:", "result.time spent": "\u6240\u82b1\u8d39\u65f6\u95f4:", "result.questions attempted": "\u5df2\u5b8c\u6210\u7684\u9898\u76ee:", "result.score": "\u5f97\u5206:", "result.result": "\u6210\u7ee9:", "result.question number": "\u95ee\u9898", "result.question score": "\u5f97\u5206", "result.question review title": "\u56de\u987e\u8be5\u95ee\u9898", "result.click a question to review": "\u60f3\u67e5\u770b\u6539\u7b54\u6848\u7684\u65b9\u5f0f\u4ee5\u53ca\u5b8c\u6574\u7b54\u6848\uff0c\u8bf7\u6309\u95ee\u9898\u53f7\u7801\u3002", "end.exam has finished": "\u8003\u8bd5\u7ed3\u675f\u3002 \u60a8\u73b0\u5728\u53ef\u4ee5\u5173\u95ed\u6b64\u7a97\u53e3\u3002", "control.confirm leave": "\u60a8\u5c1a\u672a\u5b8c\u6210\u8003\u8bd5\u3002", "control.not all questions answered": "\u60a8\u5c1a\u672a\u5b8c\u6210\u8003\u8bd5\u4e2d\u7684\u6240\u6709\u95ee\u9898\u3002", "control.not all questions submitted": "\u60a8\u5df2\u5bf9\u4e00\u4e2a\u6216\u591a\u4e2a\u7b54\u6848\u8fdb\u884c\u4e86\u66f4\u6539\uff0c\u4f46\u672a\u63d0\u4ea4\u3002 \u8bf7\u68c0\u67e5\u6bcf\u4e2a\u95ee\u9898\u662f\u5426\u5df2\u63d0\u4ea4\u3002", "control.confirm end": "\u60a8\u786e\u5b9a\u8981\u7ed3\u675f\u8003\u8bd5\u5417\uff1f \u8003\u8bd5\u7ed3\u675f\u540e\uff0c\u60a8\u5c06\u65e0\u6cd5\u66f4\u6539\u4efb\u4f55\u7b54\u6848\u3002", "control.confirm regen": "\u60a8\u60f3\u91cd\u65b0\u968f\u673a\u5316\u8fd9\u4e2a\u95ee\u9898\u5417\uff1f \u5982\u679c\u6309\u201c\u786e\u5b9a\u201d\uff0c\u5219\u5f53\u524d\u95ee\u9898\u7684\u6240\u6709\u7b54\u6848\u548c\u5206\u6570\u90fd\u5c06\u4e22\u5931\u3002", "control.confirm reveal": "\u60a8\u60f3\u663e\u793a\u8fd9\u4e2a\u95ee\u9898\u7684\u7b54\u6848\u5417\uff1f \u5230\u76ee\u524d\u4e3a\u6b62\uff0c\u60a8\u6536\u5230\u7684\u6240\u6709\u5206\u6570\u90fd\u5c06\u88ab\u9501\u5b9a\uff0c\u4ee5\u540e\u60a8\u5c06\u65e0\u6cd5\u56de\u7b54\u8be5\u95ee\u9898\u3002", "control.proceed anyway": "\u4ecd\u8981\u7ee7\u7eed\uff1f", "control.regen": "\u5c1d\u8bd5\u7c7b\u4f3c\u95ee\u9898", "control.submit answer": "\u63d0\u4ea4\u7b54\u6848", "control.submit all parts": "\u63d0\u4ea4\u6240\u6709\u90e8\u5206", "control.submit again": "\u518d\u6b21\u63d0\u4ea4", "control.submit": "\u63d0\u4ea4", "control.previous": "\u4e0a\u4e00\u4e2a", "control.next": "\u4e0b\u4e00\u4e2a", "control.advice": "\u5fe0\u544a", "control.reveal": "\u663e\u793a\u7b54\u6848", "control.total": "\u603b\u5206", "control.pause": "\u6682\u505c", "control.end exam": "\u7ed3\u675f\u8003\u8bd5", "control.back to results": "\u56de\u5230\u6210\u7ee9", "display.part.jme.error making maths": "\u8fdb\u884c\u6570\u5b66\u663e\u793a\u65f6\u51fa\u9519", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "\u8fd9\u9879\u8003\u8bd5\u6ca1\u6709\u95ee\u9898\uff01\u68c0\u67e5.exam\u6587\u4ef6\u662f\u5426\u6709\u9519\u8bef\u3002", "feedback.you were awarded": "\u60a8\u88ab\u6388\u4e88<strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid": "\u65e0\u6548\u8868\u8fbe:  <code>{{expression}}</code>", "jme.shunt.not enough arguments": "\u64cd\u4f5c\u53c2\u6570\u4e0d\u8db3 <code>{{op}}</code>", "jme.shunt.no left bracket in function": "\u51fd\u6570\u5e94\u7528\u7a0b\u5e8f\u6216\u5143\u7ec4\u4e2d\u6ca1\u6709\u5339\u914d\u7684\u5de6\u62ec\u53f7", "jme.shunt.no left square bracket": "\u6ca1\u6709\u5339\u914d\u7684\u5de6\u65b9\u62ec\u53f7", "jme.shunt.no left bracket": "\u6ca1\u6709\u5339\u914d\u7684\u5de6\u62ec\u53f7", "jme.shunt.no right bracket": "\u6ca1\u6709\u5339\u914d\u7684\u53f3\u62ec\u53f7", "jme.shunt.no right square bracket": "\u6ca1\u6709\u5339\u914d\u7684\u53f3\u65b9\u62ec\u53f7\u5728\u7ed3\u675f\u5217\u8868", "jme.shunt.missing operator": "\u65e0\u6cd5\u8bc4\u4f30\u8868\u8fbe\u5f0f-\u7f3a\u5c11\u8fd0\u7b97\u7b26\u3002", "jme.typecheck.function maybe implicit multiplication": "\u672a\u5b9a\u4e49\u51fd\u6570 <code>{{name}}</code>\u3002 \u60a8\u662f\u8bf4<code> {{first}}*{{possibleOp}}(...)</code>\u5417\uff1f", "jme.typecheck.function not defined": "\u672a\u5b9a\u4e49\u51fd\u6570<code>{{op}}</code>\u3002<code>{{op}}</code> \u662f\u53d8\u91cf\u5417\uff1f\u60a8\u7684\u610f\u601d\u662f<code>{{suggestion}}*(...)</code>\u5417\uff1f", "jme.typecheck.op not defined": "\u672a\u5b9a\u4e49\u8fd0\u7b97 '{{op}}'\u3002", "jme.typecheck.no right type definition": "\u627e\u4e0d\u5230\u6b63\u786e\u7c7b\u578b\u7684'{{op}}'\u7684\u5b9a\u4e49\u3002", "jme.typecheck.no right type unbound name": "\u672a\u5b9a\u4e49\u53d8\u91cf<code>{{name}}</code>\u3002", "jme.typecheck.map not on enumerable": "<code>map</code>\u8fd0\u7b97\u5fc5\u987b\u5728\u5217\u8868\u6216\u8303\u56f4\u5185\u5de5\u4f5c\uff0c\u800c\u4e0d\u662f{{type}}", "jme.evaluate.undefined variable": "\u672a\u5b9a\u4e49\u53d8\u91cf{{name}}", "jme.thtml.not html": "\u5c06\u975eHTML\u503c\u4f20\u9012\u7ed9THTML\u6784\u9020\u51fd\u6570\u3002", "jme.func.switch.no default case": "Switch\u8bed\u53e5\u6ca1\u6709\u9ed8\u8ba4\u60c5\u51b5", "jme.func.listval.invalid index": "\u5927\u5c0f\u4e3a{{size}}\u7684\u5217\u8868\u4e0a\u7684\u5217\u8868\u7d22\u5f15{{index}}\u65e0\u6548", "jme.func.listval.not a list": "\u5bf9\u8c61\u4e0d\u53ef\u4e0b\u6807", "jme.func.matrix.invalid row type": "\u65e0\u6cd5\u4ece{{type}}\u7c7b\u578b\u7684\u884c\u6784\u9020\u77e9\u9635", "jme.func.except.continuous range": "\u4e0d\u80fd\u5728\u8fde\u7eed\u8303\u56f4\u5185\u4f7f\u7528'except'\u8fd0\u7b97\u7b26\u3002", "jme.matrix.reports bad size": "\u77e9\u9635\u9519\u8bef\u5730\u62a5\u544a\u5176\u5927\u5c0f - \u80af\u5b9a\u662f\u6784\u9020\u51fd\u6570\u4e2d\u51fa\u73b0\u9519\u8bef", "jme.texsubvars.no right bracket": "{{op}}\u53c2\u6570\u4e2d\u6ca1\u6709\u5339\u914d\u7684<code>]</code>\u3002", "jme.texsubvars.missing parameter": "{{op}}\u4e2d\u7f3a\u5c11\u53c2\u6570: {{parameter}}", "jme.texsubvars.no right brace": "{{op}}\u4e2d\u6ca1\u6709\u5339\u914d\u7684 <code>}</code>", "jme.user javascript.error": "\u7528\u6237\u5b9a\u4e49\u7684javascript\u51fd\u6570<code>{{name}}</code>\u4e2d\u51fa\u73b0\u9519\u8bef: {{-message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "\u51fd\u6570<code> {{name}} </ code>\u51fa\u9519: {{-message}}", "jme.variables.variable not defined": "\u672a\u5b9a\u4e49\u53d8\u91cf<code>{{name}}</code>\u3002", "jme.variables.empty definition": "\u53d8\u91cf<code>{{name}}</code>\u7684\u5b9a\u4e49\u4e3a\u7a7a\u3002", "jme.variables.circular reference": "<code>{{name}}</code>\u5b9a\u4e49\u4e2d\u7684\u5faa\u73af\u53d8\u91cf\u5f15\u7528", "jme.variables.error computing dependency": "\u8ba1\u7b97\u5f15\u7528\u7684\u53d8\u91cf<<code>{{name}}</code>\u65f6\u51fa\u9519", "jme.variables.error evaluating variable": "\u8bc4\u4f30\u53d8\u91cf{{name}}\u65f6\u51fa\u9519: {{-message}}", "jme.variables.question took too many runs to generate variables": "\u6ca1\u6709\u53ca\u65f6\u751f\u6210\u6709\u6548\u7684\u95ee\u9898\u53d8\u91cf\u96c6\u3002", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "\u6ca1\u6709\u8bbe\u7f6e\u7ed9collectRuleset\u7684\u96c6\u5408\uff01", "jme.display.collectRuleset.set not defined": "Ruleset {{name}}\u5c1a\u672a\u5b9a\u4e49", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "\u65e0\u6cd5\u820d\u5165\u5230\u590d\u6570\u7684\u5c0f\u6570\u4f4d\u6570", "math.siground.complex": "\u65e0\u6cd5\u820d\u5165\u5230\u590d\u6570\u7684\u6709\u6548\u6570\u5b57", "math.combinations.complex": "\u65e0\u6cd5\u8ba1\u7b97\u590d\u6570\u7684\u7ec4\u5408", "math.permutations.complex": "\u65e0\u6cd5\u8ba1\u7b97\u590d\u6570\u7684\u6392\u5217", "math.gcf.complex": "\u65e0\u6cd5\u8ba1\u7b97\u590d\u6570\u7684\u6700\u5927\u516c\u7ea6\u6570", "math.lcm.complex": "\u65e0\u6cd5\u8ba1\u7b97\u590d\u6570\u7684\u6700\u5c0f\u516c\u500d\u6570", "math.lt.order complex numbers": "\u65e0\u6cd5\u6392\u5e8f\u590d\u6570", "math.choose.empty selection": "\u7a7a\u9009\u62e9\u8d4b\u4e88\u968f\u673a\u51fd\u6570", "matrixmath.abs.non-square": "\u65e0\u6cd5\u8ba1\u7b97\u975e\u6b63\u65b9\u5f62\u77e9\u9635\u7684\u884c\u5217\u5f0f\u3002", "matrixmath.abs.too big": "\u62b1\u6b49\uff0c\u8fd8\u4e0d\u80fd\u8ba1\u7b97\u5927\u4e8e3x3\u7684\u77e9\u9635\u7684\u884c\u5217\u5f0f\u3002", "matrixmath.mul.different sizes": "\u4e0d\u80fd\u5c06\u4e0d\u540c\u5927\u5c0f\u7684\u77e9\u9635\u76f8\u4e58\u3002", "vectormath.cross.not 3d": "\u53ea\u80fd\u53d63\u7ef4\u5411\u91cf\u7684\u53c9\u79ef\u3002", "vectormath.dot.matrix too big": "\u65e0\u6cd5\u8ba1\u7b97\u4e0d\u662f$1 \\times N$\u6216$N \\times 1$\u7684\u77e9\u9635\u7684\u70b9\u79ef\u3002", "vectormath.cross.matrix too big": "\u65e0\u6cd5\u8ba1\u7b97\u4e0d\u662f$1 \\times N$\u6216$N \\times 1$\u7684\u77e9\u9635\u7684\u53c9\u79ef\u3002", "part.with steps answer prompt": "\u56de\u7b54:", "part.script.error": "\u90e8\u5206{{path}}\u81ea\u5b9a\u4e49\u811a\u672c{{script}}\u4e2d\u7684\u9519\u8bef: {{-message}}", "part.marking.steps no matter": "\u56e0\u4e3a\u60a8\u5728\u8fd9\u90e8\u5206\u95ee\u9898\u4e0a\u83b7\u5f97\u4e86\u6ee1\u5206\uff0c\u6240\u4ee5\u4e0d\u8ba1\u7b97\u6b65\u9aa4\u7684\u7b54\u6848\u3002", "part.marking.revealed steps no penalty": "\u60a8\u663e\u793a\u4e86\u6b65\u9aa4\u3002", "part.marking.used variable replacements": "\u8be5\u90e8\u5206\u5df2\u4f7f\u7528\u60a8\u5bf9\u5148\u524d\u90e8\u5206\u7684\u7b54\u6848\u8fdb\u884c\u4e86\u8bc4\u5206\u3002", "part.marking.variable replacement part not answered": "\u60a8\u5fc5\u987b\u5148\u56de\u7b54{{part}}\u3002", "part.marking.resubmit because of variable replacement": "\u8be5\u90e8\u5206\u7684\u8bc4\u5206\u53d6\u51b3\u4e8e\u60a8\u5bf9\u5176\u4ed6\u90e8\u5206\u6240\u505a\u7684\u7b54\u6848\u66f4\u6539\u3002 \u518d\u6b21\u63d0\u4ea4\u6b64\u90e8\u5206\u4ee5\u66f4\u65b0\u60a8\u7684\u5206\u6570\u3002", "part.marking.not submitted": "\u6ca1\u6709\u7b54\u6848\u88ab\u63d0\u4ea4\u3002", "part.marking.did not answer": "\u60a8\u6ca1\u6709\u56de\u7b54\u8fd9\u4e2a\u95ee\u9898\u3002", "part.marking.nothing entered": "\u60a8\u6ca1\u6709\u8f93\u5165\u7b54\u6848\u3002", "part.marking.incorrect": "\u60a8\u7684\u7b54\u6848\u4e0d\u6b63\u786e\u3002", "part.marking.correct": "\u60a8\u7684\u56de\u7b54\u662f\u6b63\u786e\u7684\u3002", "part.marking.uncaught error": "\u8bc4\u5206\u65f6\u51fa\u9519: {{-message}}", "part.marking.no result": "\u65e0\u6cd5\u5bf9\u6b64\u90e8\u5206\u8bc4\u5206\u3002", "part.correct answer": "\u9884\u671f\u7b54\u6848:", "part.missing type attribute": "{{part}}: \u7f3a\u5c11\u96f6\u4ef6\u7c7b\u578b\u5c5e\u6027", "part.unknown type": "{{part}}: \u65e0\u6cd5\u8bc6\u522b\u7684\u96f6\u4ef6\u7c7b\u578b {{type}}", "part.setting not present": "\u672a\u8bbe\u7f6e\u5c5e\u6027 '{{property}}'", "part.jme.answer missing": "\u7f3a\u5c11\u6b63\u786e\u7b54\u6848", "part.jme.answer too long": "\u60a8\u7684\u7b54\u6848\u592a\u957f\u3002", "part.jme.answer too short": "\u60a8\u7684\u7b54\u6848\u592a\u77ed\u4e86\u3002", "part.jme.answer invalid": "\u60a8\u7684\u7b54\u6848\u4e0d\u662f\u6709\u6548\u7684\u6570\u5b66\u8868\u8fbe\u5f0f\u3002<br/>{{-message}}.\u3002", "part.jme.marking.correct": "\u60a8\u7684\u7b54\u6848\u5728\u6570\u5b57\u4e0a\u662f\u6b63\u786e\u7684\u3002", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "\u60a8\u7684\u7b54\u6848\u5fc5\u987b\u5305\u542b: {{strings}}", "part.jme.must-have several": "\u60a8\u7684\u7b54\u6848\u5fc5\u987b\u5305\u542b\u4ee5\u4e0b\u6240\u6709\u5185\u5bb9: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "\u60a8\u7684\u7b54\u6848\u4e0d\u5f97\u5305\u542b: {{strings}}", "part.jme.not-allowed several": "\u60a8\u7684\u7b54\u6848\u4e0d\u5f97\u5305\u542b\u4ee5\u4e0b\u4efb\u4f55\u5185\u5bb9: {{strings}}", "part.jme.unexpected variable name": "\u60a8\u7684\u7b54\u6848\u88ab\u89e3\u91ca\u4e3a\u4f7f\u7528\u4e86\u610f\u5916\u7684\u53d8\u91cf\u540d\u79f0<code>{{name}}</code>\u3002", "part.jme.unexpected variable name suggestion": "\u60a8\u7684\u7b54\u6848\u88ab\u89e3\u91ca\u4e3a\u4f7f\u7528\u4e86\u610f\u5916\u7684\u53d8\u91cf\u540d\u79f0 <code>{{name}}</code>\u3002 \u60a8\u662f\u8bf4<code>{{suggestion}}</code>\u5417\uff1f", "part.patternmatch.display answer missing": "\u663e\u793a\u7b54\u6848\u4e22\u5931", "part.patternmatch.correct except case": "\u9664\u60c5\u51b5\u5916\uff0c\u60a8\u7684\u7b54\u6848\u662f\u6b63\u786e\u7684\u3002", "part.numberentry.correct except decimal": "\u60a8\u7684\u7b54\u6848\u5728\u5141\u8bb8\u7684\u8303\u56f4\u5185\uff0c\u4f46\u4e0d\u5141\u8bb8\u4f7f\u7528\u5341\u8fdb\u5236\u6570\u5b57\u3002", "part.numberentry.correct except fraction": "\u60a8\u7684\u7b54\u6848\u5728\u5141\u8bb8\u7684\u8303\u56f4\u5185\uff0c\u4f46\u5206\u6570\u662f\u4e0d\u88ab\u5141\u8bb8\u7684\u3002", "part.numberentry.answer invalid": "\u60a8\u8f93\u5165\u7684\u53f7\u7801\u65e0\u6548\u3002", "part.numberentry.answer not integer": "\u60a8\u7684\u7b54\u6848\u65e0\u6548\u3002 \u60a8\u5fc5\u987b\u8f93\u5165\u6574\u6570\uff0c\u800c\u4e0d\u662f\u5c0f\u6570", "part.numberentry.answer not integer or decimal": "\u60a8\u7684\u7b54\u6848\u65e0\u6548\u3002 \u60a8\u5fc5\u987b\u8f93\u5165\u6574\u6570\u6216\u5c0f\u6570\u3002", "part.numberentry.zero sig fig": "\u8bbe\u7f6e\u8be5\u90e8\u5206\u7684\u76ee\u7684\u662f\u5c06\u5b66\u751f\u7684\u7b54\u6848\u56db\u820d\u4e94\u5165\u5230\u96f6\u6709\u6548\u6570\u5b57\uff0c\u8fd9\u6ca1\u6709\u4efb\u4f55\u610f\u4e49\u3002", "part.mcq.options def not a list": "\u5b9a\u4e49{{properties}}\u7684\u8868\u8fbe\u5f0f\u4e0d\u662f\u5217\u8868\u3002", "part.mcq.marking matrix string empty": "\u81ea\u5b9a\u4e49\u6807\u8bb0\u77e9\u9635\u8868\u8fbe\u5f0f\u4e3a\u7a7a\u3002", "part.mcq.choices missing": "\u7f3a\u5931\u9009\u62e9\u7684\u5b9a\u4e49", "part.mcq.matrix not a number": "\u90e8\u5206{{part}}\u6807\u8bb0\u77e9\u9635\u5355\u5143\u683c({{row}},{{column}})\u7684\u8ba1\u7b97\u7ed3\u679c\u4e0d\u662f\u6570\u5b57", "part.mcq.wrong number of choices": "\u60a8\u6240\u9009\u62e9\u7684\u9009\u9879\u6570\u76ee\u662f\u9519\u8bef\u7684", "part.mcq.no choices selected": "\u672a\u9009\u62e9\u4efb\u4f55\u9009\u9879\u3002", "part.mcq.matrix not a list": "\u7531JME\u8868\u8fbe\u5f0f\u5b9a\u4e49\u7684\u6807\u8bb0\u77e9\u9635\u4e0d\u662f\u5217\u8868\uff0c\u4f46\u5e94\u8be5\u662f\u5217\u8868\u3002", "part.mcq.matrix wrong type": "\u6807\u8bb0\u77e9\u9635\u4e2d\u4f7f\u7528\u7684\u65e0\u6548\u7c7b\u578b'{{type}}'\u7684\u5143\u7d20\u3002", "part.mcq.matrix mix of numbers and lists": "\u77e9\u9635\u4e2d\u4f7f\u7528\u7684\u6570\u5b57\u548c\u5217\u8868\u7684\u6df7\u5408\u3002", "part.mcq.matrix wrong size": "\u77e9\u9635\u6709\u5927\u5c0f\u9519\u8bef\u3002", "part.mcq.correct choice": "\u60a8\u9009\u62e9\u4e86\u6b63\u786e\u7684\u7b54\u6848\u3002", "part.matrix.answer invalid": "\u60a8\u7684\u7b54\u6848\u65e0\u6548\u3002", "part.matrix.invalid cell": "\u60a8\u7684\u7b54\u6848\u4e2d\u7684\u4e00\u4e2a\u6216\u591a\u4e2a\u5355\u5143\u683c\u4e3a\u7a7a\u6216\u65e0\u6548\u3002", "part.matrix.some incorrect": "\u60a8\u7684\u7b54\u6848\u4e2d\u7684\u4e00\u4e2a\u6216\u591a\u4e2a\u5355\u5143\u683c\u4e0d\u6b63\u786e\uff0c\u4f46\u5176\u4f59\u90e8\u5206\u5219\u88ab\u6388\u4e88\u5206\u6570\u3002", "part.matrix.empty": "\u60a8\u5c1a\u672a\u8f93\u5165\u7b54\u6848\u3002", "part.matrix.empty cell": "\u60a8\u7684\u7b54\u6848\u4e2d\u7684\u4e00\u4e2a\u6216\u591a\u4e2a\u5355\u5143\u683c\u4e3a\u7a7a\u3002", "part.matrix.size mismatch": "\u95ee\u9898\u4f5c\u8005\u4e0d\u5141\u8bb8\u5b66\u751f\u51b3\u5b9a\u7b54\u6848\u7684\u7ef4\u5ea6\uff0c\u4f46\u662f\u6b63\u786e\u7684\u7b54\u6848\u662f{{correct_dimensions}}\uff0c\u800c\u7b54\u6848\u8f93\u5165\u662f{{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "\u90e8\u4ef6\u5c1a\u672a\u5b9e\u73b0<code>{{name}}</code>\u65b9\u6cd5\u3002", "question.loaded name mismatch": "\u65e0\u6cd5\u6062\u590d\u6b64\u5c1d\u8bd5-\u81ea\u4e0a\u6b21\u4f1a\u8bdd\u4ee5\u6765\uff0c\u7a0b\u5e8f\u5305\u5df2\u66f4\u6539\u3002", "question.error": "\u95ee\u9898{{number}}: {{-message}}", "question.preamble.error": "\u524d\u8a00\u4e2d\u7684\u9519\u8bef: {{-message}}", "question.preamble.syntax error": "\u524d\u8a00\u4e2d\u7684\u8bed\u6cd5\u9519\u8bef", "question.unsupported part type": "\u4e0d\u652f\u6301\u7684\u96f6\u4ef6\u7c7b\u578b", "question.header": "\u95ee\u9898{{number}}", "question.submit part": "\u63d0\u4ea4\u90e8\u5206", "question.show steps": "\u663e\u793a\u6b65\u9aa4", "question.show steps penalty": "\u60a8\u5c06\u4e22\u5931<strong>{{count,niceNumber}}</strong> $t(mark)\u3002", "question.show steps no penalty": "\u60a8\u7684\u5206\u6570\u4e0d\u4f1a\u53d7\u5230\u5f71\u54cd\u3002", "question.show steps already penalised": "\u60a8\u5df2\u7ecf\u663e\u793a\u4e86\u6b65\u9aa4\u3002 \u60a8\u53ef\u4ee5\u518d\u6b21\u65e0\u53d7\u60e9\u7f5a\u7684\u663e\u793a\u5b83\u4eec\u3002", "question.hide steps": "\u9690\u85cf\u6b65\u9aa4", "question.hide steps no penalty": "\u60a8\u7684\u5206\u6570\u4e0d\u4f1a\u53d7\u5230\u5f71\u54cd\u3002", "question.advice": "\u5efa\u8bae", "question.no such part": "\u627e\u4e0d\u5230\u90e8\u5206{{path}}", "question.can not submit": "\u65e0\u6cd5\u63d0\u4ea4\u7b54\u6848-\u68c0\u67e5\u9519\u8bef\u3002", "question.answer submitted": "\u7b54\u6848\u5df2\u63d0\u4ea4", "question.score feedback.show": "\u663e\u793a\u53cd\u9988", "question.score feedback.hide": "\u9690\u85cf\u53cd\u9988", "question.score feedback.answered total actual": "\u5f97\u5206: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}\u3002\u5df2\u56de\u7b54\u3002", "question.score feedback.answered actual": "\u5f97\u5206:  {{scoreString}}", "question.score feedback.answered": "\u5df2\u56de\u7b54", "question.score feedback.unanswered": "\u672a\u56de\u7b54", "question.score feedback.unanswered total": "{{marksString}}\u3002", "question.score feedback.correct": "\u4f60\u7684\u7b54\u6848\u662f\u6b63\u786e\u7684", "question.score feedback.partial": "\u60a8\u7684\u7b54\u6848\u90e8\u5206\u6b63\u786e", "question.score feedback.wrong": "\u4f60\u7684\u7b54\u6848\u4e0d\u6b63\u786e", "question.selector.unsubmitted changes": "\u672a\u63d0\u4ea4\u7684\u66f4\u6539\u3002", "timing.no accumulator": "\u6ca1\u6709\u8ba1\u65f6\u7d2f\u52a0\u5668{{name}}", "timing.time remaining": "\u5269\u4f59\u65f6\u95f4:", "xml.could not load": "\u65e0\u6cd5\u52a0\u8f7dXML\u6587\u6863: {{-message}}", "xml.property not number": "\u8282\u70b9{{element}}\u4e2d\u7684\u5c5e\u6027{{name}}\u5e94\u8be5\u662f\u6570\u5b57\uff0c\u4f46\u4e0d\u80fd\u662f ({{value}})\u3002", "xml.property not boolean": "\u8282\u70b9{{element}}\u4e2d\u7684\u5c5e\u6027{{name}}\u5e94\u8be5\u662f\u5e03\u5c14\u503c\uff0c\u800c\u4e0d\u662f ({{value}})", "xml.error in variable definition": "\u53d8\u91cf<code>{{name}}</code>\u7684\u5b9a\u4e49\u6709\u9519\u8bef", "scorm.error initialising": "\u521d\u59cb\u5316SCORM\u534f\u8bae\u65f6\u51fa\u9519\uff1a{{-message}}", "scorm.failed save": "<p>\u5c06\u6570\u636e\u4fdd\u5b58\u5230\u670d\u52a1\u5668\u7684\u8bf7\u6c42\u5931\u8d25\u3002 \u6309 <b>OK</b> \u5728\u7ee7\u7eed\u5c1d\u8bd5\u3002</p>\n<p>\u5982\u679c\u60a8\u53cd\u590d\u6536\u5230\u6b64\u6d88\u606f\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u4e92\u8054\u7f51\u8fde\u63a5\u6216\u4f7f\u7528\u5176\u4ed6\u8ba1\u7b97\u673a\u3002 \u60a8\u5148\u524d\u63d0\u4ea4\u7684\u7b54\u6848\u5df2\u6210\u529f\u4fdd\u5b58\uff0c\u5982\u679c\u60a8\u5728\u53e6\u4e00\u53f0\u8ba1\u7b97\u673a\u4e0a\u7ee7\u7eed\u8fdb\u884c\u6b64\u4f1a\u8bdd\uff0c\u5219\u5c06\u8fd8\u539f\u8be5\u7b54\u6848\u3002</p>\n<p>\u5982\u679c\u6b64\u6d88\u606f\u6301\u7eed\u51fa\u73b0\uff0c\u5e76\u4e14\u60a8\u65e0\u6cd5\u4fdd\u5b58<em>\u4efb\u4f55</em>\u7b54\u6848\uff0c\u8bf7\u4e0e\u60a8\u7684\u8bb2\u5e08\u6216\u8001\u5e08\u8054\u7cfb\u3002</p>", "scorm.no exam suspend data": "\u6062\u590d\u5931\u8d25: \u6ca1\u6709\u8003\u8bd5\u6682\u505c\u6570\u636e\u3002", "scorm.error loading suspend data": "\u52a0\u8f7d\u6682\u505c\u6570\u636e\u65f6\u51fa\u9519: {{-message}}", "scorm.error loading question": "\u52a0\u8f7d\u95ee\u9898{{number}}\u65f6\u51fa\u9519: {{-message}}", "scorm.no question suspend data": "\u6ca1\u6709\u95ee\u9898\u6682\u505c\u6570\u636e", "scorm.error loading part": "\u52a0\u8f7d\u90e8\u5206{{part}}\u65f6\u51fa\u9519: {{-message}}", "scorm.no part suspend data": "\u6ca1\u6709\u90e8\u5206\u6682\u505c\u6570\u636e", "util.product.non list": "\u5c06\u975e\u5217\u8868\u4f20\u9012\u7ed9<code>Numbas.util.product</code>", "mark": "\u5206\u6570", "was": "\u662f", "part": "\u90e8\u5206", "gap": "\u95f4\u9699", "step": "\u6b65\u9aa4", "jme.substituteTree.undefined variable": "\u672a\u5b9a\u4e49\u7684\u53d8\u91cf: <code>{{name}}</code>", "jme.user javascript.returned undefined": "\u7528\u6237\u5b9a\u4e49\u7684javascript\u51fd\u6570<code>{{name}}</code>\u8fd4\u56de\u4e86<code>undefined</code>\u3002", "part.marking.steps change": "\u60a8\u5bf9\u6b65\u9aa4\u7684\u56de\u7b54\u83b7\u5f97\u4e86<strong>{{count,niceNumber}}</strong> $t(mark)\u3002", "part.marking.revealed steps with penalty": "\u60a8\u900f\u9732\u4e86\u6b65\u9aa4\u3002 \u60a8\u53ef\u4ee5\u4e3a\u6b64\u90e8\u5206\u8bc4\u5206\u7684\u6700\u9ad8\u5206\u662f<strong>{{count,niceNumber}}</strong> $t(mark)\u3002 \u60a8\u7684\u5206\u6570\u5c06\u76f8\u5e94\u964d\u4f4e\u3002", "part.marking.total score": "\u60a8\u4e3a\u6b64\u90e8\u5206\u8bc4\u5206\u4e86<strong>{{count,niceNumber}}</strong> $t(mark)\u3002", "part.numberentry.precision type.dp": "\u5c0f\u6570\u4f4d", "part.numberentry.precision type.dp_plural": "\u5c0f\u6570\u4f4d", "part.numberentry.precision type.sigfig": "\u6709\u6548\u6570\u5b57", "part.numberentry.precision type.sigfig_plural": "\u6709\u6548\u6570\u5b57", "part.numberentry.give your answer to precision": "\u5c06\u7b54\u6848\u56db\u820d\u4e94\u5165\u5230{{count,niceNumber}} {{precisionType}}\u3002", "question.unsubmitted changes": "\u60a8\u5df2\u66f4\u6539\u7b54\u6848\uff0c\u4f46\u672a\u63d0\u4ea4\u3002 \u8bf7\u68c0\u67e5\u60a8\u7684\u7b54\u6848\uff0c\u7136\u540e\u6309 <strong>\u63d0\u4ea4\u7b54\u6848</strong>\u6309\u94ae\u3002", "question.unsubmitted changes_plural": "\u60a8\u5df2\u5bf9\u7b54\u6848\u8fdb\u884c\u4e86\u66f4\u6539\uff0c\u4f46\u672a\u63d0\u4ea4\u3002 \u8bf7\u68c0\u67e5\u6bcf\u4e2a\u90e8\u5206\u7684\u7b54\u6848\uff0c\u7136\u540e\u6309<strong>\u63d0\u4ea4\u6240\u6709\u90e8\u5206</strong>\u6309\u94ae\u3002", "util.equality not defined for type": "\u672a\u4e3a\u7c7b\u578b{{type}}\u5b9a\u4e49\u7684\u76f8\u7b49\u6027", "mark_plural": "\u5206\u6570", "was_plural": "\u662f", "die.script not loaded": "Numbas\u65e0\u6cd5\u542f\u52a8\uff0c\u56e0\u4e3a\u672a\u52a0\u8f7d\u6587\u4ef6<code>{{file}}</code>\u3002\u68c0\u67e5\u5b83\u662f\u5426\u5305\u542b\u5728<code>scripts.js</code>\u4e2d\u3002", "math.combinations.n less than zero": "\u65e0\u6cd5\u8ba1\u7b97\u7ec4\u5408: n\u5c0f\u4e8e\u96f6", "math.combinations.k less than zero": "\u65e0\u6cd5\u8ba1\u7b97\u7ec4\u5408: k\u5c0f\u4e8e\u96f6", "math.combinations.n less than k": "\u65e0\u6cd5\u8ba1\u7b97\u7ec4\u5408: n\u5c0f\u4e8ek", "math.permutations.n less than zero": "\u65e0\u6cd5\u8ba1\u7b97\u6392\u5217: n\u5c0f\u4e8e\u96f6", "math.permutations.k less than zero": "\u65e0\u6cd5\u8ba1\u7b97\u6392\u5217: k\u5c0f\u4e8e\u96f6", "math.permutations.n less than k": "\u65e0\u6cd5\u8ba1\u7b97\u6392\u5217: n\u5c0f\u4e8ek", "part.numberentry.give your answer to precision_0": "\u5c06\u7b54\u6848\u56db\u820d\u4e94\u5165\u5230\u6700\u63a5\u8fd1\u7684\u6574\u6570\u3002", "mathjax.error": "MathJax\u5904\u7406\u9519\u8bef\uff1a{{-message}}", "mathjax.error with context": "{{-context}}\u4e2d\u7684MathJax\u5904\u7406\u9519\u8bef\uff1a{{-message}}", "exam.introduction": "\u8003\u8bd5\u7b80\u4ecb", "exam.feedback": "\u8003\u8bd5\u53cd\u9988\u4fe1\u606f", "jme.tokenise.keypair key not a string": "\u5b57\u5178\u952e\u5e94\u4e3a\u5b57\u7b26\u4e32\uff0c\u800c\u4e0d\u662f{{type}}\u3002", "jme.shunt.list mixed argument types": "\u65e0\u6cd5\u89e3\u6790{{mode}}: \u5b57\u5178\u548c\u5217\u8868\u5143\u7d20\u7684\u6df7\u5408", "jme.func.listval.key not in dict": "\u5b57\u5178\u4e0d\u5305\u542bkey<code>{{key}}</code>", "part.prompt": "\u63d0\u793a", "part.feedback": "\u53cd\u9988", "part.numberentry.answer not reduced": "\u60a8\u7684\u56de\u7b54\u8fd8\u6ca1\u51cf\u5c11\u5230\u6700\u4f4e\u9650\u5ea6\u3002", "part.numberentry.give your answer as a reduced fraction": "\u5c06\u7b54\u6848\u964d\u4f4e\u5230\u6700\u4f4e\u9650\u5ea6\u3002", "part.numberentry.negative decimal places": "\u8bbe\u7f6e\u6b64\u90e8\u5206\u7684\u76ee\u7684\u662f\u5c06\u5b66\u751f\u7684\u7b54\u6848\u56db\u820d\u4e94\u5165\u4e3a\u8d1f\u7684\u5c0f\u6570\u4f4d\u6570\uff0c\u8fd9\u6ca1\u6709\u4efb\u4f55\u610f\u4e49\u3002", "part.mcq.choices": "\u9009\u62e9", "part.mcq.answers": "\u7b54\u6848", "part.mcq.matrix cell empty": "\u90e8\u5206{{part}}\u77e9\u9635\u5355\u5143\u683c ({{row}},{{column}})\u4e3a\u7a7a", "part.mcq.matrix jme error": "\u90e8\u5206{{part}}\u77e9\u9635\u5355\u5143\u683c ({{row}},{{column}})\u7ed9\u51fa\u4e86JME\u9519\u8bef\uff1a{{-error}}", "question.statement": "\u58f0\u660e", "ruleset.circular reference": "\u89c4\u5219\u96c6code>{{name}}</code>\u5b9a\u4e49\u4e2d\u7684\u5faa\u73af\u5f15\u7528", "ruleset.set not defined": "\u89c4\u5219\u96c6{{name}}\u5c1a\u672a\u5b9a\u4e49", "jme.evaluate.no scope given": "\u5fc5\u987b\u7ed9Numbas.jme.evaluate\u4e00\u4e2aScope", "answer.number.not a number": "\u60a8\u7684\u7b54\u6848\u65e0\u6548\u3002", "answer.number.fractions not allowed": "\u60a8\u4e0d\u80fd\u8f93\u5165\u5206\u6570\u3002", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "\u60a8\u4e0d\u80fd\u8f93\u5165\u5206\u6570\u3002", "answer.matrix.some cell not a number": "\u60a8\u7684\u7b54\u6848\u4e2d\u7684\u4e00\u4e2a\u6216\u591a\u4e2a\u5355\u5143\u683c\u4e0d\u662f\u6709\u6548\u6570\u5b57\u3002", "exam.enter password": "\u5bc6\u7801:", "exam.password.correct": "\u8be5\u5bc6\u7801\u6b63\u786e\u3002 \u60a8\u53ef\u4ee5\u5f00\u59cb\u8003\u8bd5\u3002", "exam.password.incorrect": "\u8be5\u5bc6\u7801\u9519\u8bef\u3002", "frontpage.scorm.lms not connected": "\u8be5\u8003\u8bd5\u4ee5\u72ec\u7acb\u6a21\u5f0f\u8fd0\u884c\u3002 \u60a8\u7684\u7b54\u6848\u548c\u6807\u8bb0\u5c06\u4e0d\u4f1a\u4fdd\u5b58\uff01", "result.question review": "\u56de\u987e", "control.confirm regen no marks": "\u60a8\u60f3\u91cd\u65b0\u968f\u673a\u5316\u8fd9\u4e2a\u95ee\u9898\u5417\uff1f", "control.confirm reveal no marks": "\u60a8\u60f3\u663e\u793a\u8fd9\u4e2a\u95ee\u9898\u7684\u7b54\u6848\u5417\uff1f", "jme.tokenise.invalid near": "\u65e0\u6548\u7684\u8868\u8fbe\u5f0f: <code>{{expression}}</code>\u9644\u8fd1\u7684\u4f4d\u7f6e{{position}}\u4e0a\u7684<code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "\u65e0\u6548\u7684\u5bf9\u8c61\u4f20\u9012\u7ed9\u6570\u5b57\u6784\u9020\u51fd\u6570\u3002", "jme.subvars.null substitution": "\u7a7a\u53d8\u91cf\u66ff\u6362: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "\u6570\u636e\u7c7b\u578b{{type}}\u5df2\u88ab\u6ce8\u518c\uff0c\u56e0\u6b64\u65e0\u6cd5\u518d\u6b21\u6ce8\u518c\u3002", "jme.type.no cast method": "\u65e0\u6cd5\u81ea\u52a8\u4ece{{from}}\u8f6c\u6362\u4e3a{{to}}\u3002", "jme.display.simplifyTree.empty expression": "\u8868\u8fbe\u5f0f\u4e3a\u7a7a", "jme.display.simplifyTree.stuck in a loop": "\u7b80\u5316\u7a0b\u5e8f\u9677\u5165\u5faa\u73af: <code>{{expr}}</code>", "math.niceNumber.undefined": "\u539f\u672c\u671f\u671b\u6709\u4e00\u4e2a\u6570\u5b57\uff0c\u4f46 <code>undefined</code>", "math.rangeToList.zero step size": "\u65e0\u6cd5\u5c06\u6b65\u957f\u4e3a\u96f6\u7684\u8303\u56f4\u8f6c\u6362\u4e3a\u5217\u8868\u3002", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "\u60a8\u663e\u793a\u4e86\u6b65\u9aa4\u3002", "part.marking.maximum scaled down": "\u60a8\u53ef\u4ee5\u4e3a\u6b64\u90e8\u5206\u8bc4\u5206\u7684\u6700\u9ad8\u5206\u662f<strong>{{count,niceNumber}}</strong> $t(mark)\u3002 \u60a8\u7684\u5206\u6570\u5c06\u76f8\u5e94\u964d\u4f4e\u3002", "part.marking.minimum score applied": "\u8fd9\u90e8\u5206\u7684\u6700\u4f4e\u5206\u6570\u662f<strong>{{score,niceNumber}}</strong>\u3002", "part.marking.maximum score applied": "\u8fd9\u90e8\u5206\u7684\u6700\u9ad8\u5206\u6570\u662f<strong>{{score,niceNumber}}</strong>\u3002", "part.marking.error in marking script": "\u8be5\u90e8\u5206\u7684\u8bc4\u5206\u7b97\u6cd5\u51fa\u9519\u3002 \u8bf7\u62a5\u544a\u6b64\u9519\u8bef\u3002 {{-message}}", "part.marking.no result after replacement": "\u65e0\u6cd5\u4f7f\u7528\u60a8\u5bf9\u5148\u524d\u90e8\u5206\u7684\u56de\u7b54\u6765\u8bc4\u5206\u8fd9\u90e8\u5206\u3002", "part.marking.missing required note": "\u8bc4\u5206\u7b97\u6cd5\u672a\u5b9a\u4e49\u6ce8\u91ca<code>{{note}}</code>", "marking.apply.not a list": " <code>apply</code>\u7684\u7b2c\u4e00\u4e2a\u53c2\u6570\u5fc5\u987b\u662f\u4e00\u4e2a\u5217\u8868\uff0c\u5e76\u4e14\u4e0d\u662f", "marking.apply marking script.script not found": "\u627e\u4e0d\u5230\u6807\u8bb0\u811a\u672c<code>{{name}}</code", "marking.note.compilation error": "\u7f16\u8bd1\u6ce8\u91ca<code>{{name}}</code>\u65f6\u51fa\u9519: {{-message}}\n", "marking.note.error evaluating note": "\u8bc4\u4f30\u7b14\u8bb0 <code>{{name}}</code> - {{-message}}\u65f6\u51fa\u9519", "marking.note.invalid definition": "\u65e0\u6548\u7684\u6ce8\u91ca\u5b9a\u4e49: <code>{{source}}</code>\u3002{{-hint}}", "marking.note.invalid definition.missing colon": "\u60a8\u53ef\u80fd\u4f1a\u5728\u540d\u79f0\u548c\u63cf\u8ff0\u540e\u9057\u6f0f\u4e00\u4e2a\u5192\u53f7", "marking.note.invalid definition.description missing closing bracket": "\u60a8\u53ef\u80fd\u7f3a\u5c11\u53f3\u62ec\u53f7", "marking.note.empty expression": "\u6ce8\u91ca<code>{{name}}</code>>\u4e3a\u7a7a\u3002", "marking.script.error parsing notes": "\u89e3\u6790\u6807\u8bb0\u811a\u672c\u65f6\u51fa\u9519: {{- message}}", "part.feedback out of date": "\u8be5\u53cd\u9988\u57fa\u4e8e\u60a8\u6700\u540e\u63d0\u4ea4\u7684\u7b54\u6848\u3002 \u63d0\u4ea4\u66f4\u6539\u7684\u7b54\u6848\u4ee5\u83b7\u53d6\u66f4\u65b0\u7684\u53cd\u9988\u3002", "part.jme.invalid value generator expression": "\u53d8\u91cf<code>{{name}}</code>\u7684\u65e0\u6548\u503c\u751f\u6210\u5668\u8868\u8fbe\u5f0f: {{-message}}", "part.mcq.incorrect choice": "\u60a8\u9009\u62e9\u4e86\u9519\u8bef\u7684\u7b54\u6848\u3002", "part.matrix.not all cells same precision": "\u60a8\u6ca1\u6709\u4ee5\u76f8\u540c\u7684\u7cbe\u5ea6\u7ed9\u51fa\u7b54\u6848\u4e2d\u7684\u6bcf\u4e2a\u5355\u5143\u683c\u3002", "part.gapfill.error marking gap": "\u8bc4\u5206{{name}}\u65f6\u51fa\u9519: {{-message}}", "part.custom.empty setting": "\u6ca1\u6709\u7ed9\u51fa\u503c\u3002", "part.custom.unrecognised input type": "\u65e0\u6cd5\u8bc6\u522b\u7684\u8bbe\u7f6e\u7c7b\u578b<code>{{input_type}}</code>", "part.custom.error evaluating input option": "\u8bc4\u4f30\u8f93\u5165\u9009\u9879<code>{{option}}</code>\u65f6\u51fa\u9519: {{-error}}", "part.custom.input option missing": "\u8f93\u5165\u9009\u9879 <code>{{option}}</code>\u7684\u5b9a\u4e49\u4e22\u5931\u3002", "part.custom.error evaluating setting": "\u8bc4\u4f30\u8bbe\u7f6e<code>{{setting}}</code>\u65f6\u51fa\u9519: {{-error}}", "question.error creating question": "\u521b\u5efa\u95ee\u9898{{number}}\u65f6\u51fa\u9519: {{-message}}", "question.score feedback.not marked": "\u672a\u8bc4\u5206", "question.score feedback.partially answered": "\u90e8\u5206\u56de\u7b54", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "\u5f97\u5206: {{scoreString}}", "question.score feedback.score total actual": "\u5f97\u5206: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "\u53d8\u91cf<code>{{name}}</code>\u7684\u5b9a\u4e49\u9519\u8bef", "left brace": "{", "right brace": "}", "extension.not found": "\u65e0\u6cd5\u52a0\u8f7d\u6269\u5c55\uff1a<code>{{name}}</code>\u3002", "control.toggle navigation menu": "\u5207\u6362\u5bfc\u822a\u83dc\u5355", "part.input title": "\u7b2c{{name}}\u6b65\u7b54\u6848", "part.correct answer title": "\u7b2c{{name}}\u6b65\u6b63\u786e\u7b54\u6848", "part.jme.must-match.failed": "\u7b54\u6848\u683c\u5f0f\u5b58\u5728\u9519\u8bef\u3002", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "\u8fd4\u56de\u83dc\u5355", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "\u95ee\u9898\u4e2d\u7684\u4e00\u4e2a\u53d8\u91cf\u8fd8\u6ca1\u88ab\u547d\u540d", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "\u9009\u62e9\u4e00\u4e2a\u95ee\u9898", "part.choose next part.answered": "\u60a8\u63a5\u4e0b\u6765\u60f3\u505a\u4ec0\u4e48 \uff1f", "part.choose next part.unanswered": "\u4e0d\u7136\uff0c \u60a8\u53ef\u4ee5\uff1a", "part.choose next part.will be locked": "\uff08\u8be5\u90e8\u5206\u5c06\u4f1a\u88ab\u9501\u5b9a\uff09", "part.reached dead end": "\u81ea\u6b64\u4ee5\u4e0b\uff0c\u672a\u6709\u53ef\u505a\u4e4b\u4e8b\u4e86", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "\u4f60\u7684\u7b54\u6848\u662f\u65e0\u6548\u7684\u3002\u4f60\u5fc5\u987b\u8f93\u5165\u4e00\u4e2a\u6574\u6570\u3001\u4e00\u4e2a\u5c0f\u6570\u6216\u4e00\u4e2a\u5206\u6570\u3002", "question": "\u9898\u76ee", "question.progress": "\u9898\u76ee\u8fdb\u5ea6 \uff1a", "question.score feedback.unattempted": "\u672a\u5c1d\u8bd5", "question.score feedback.attempted": "\u5df2\u5c1d\u8bd5", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "\u76ee\u6807", "question.penalties": "\u60e9\u7f5a", "question.back to previous part": "\u8fd4\u56de\u4e0a\u4e00\u90e8\u5206", "end.print": "\u6253\u5370\u60a8\u7684\u6210\u7ee9\u5355", "math.shuffle_together.lists not all the same length": "list\u957f\u5ea6\u672a\u5168\u90e8\u76f8\u540c\u3002", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "\u884c", "matrix input.columns": "\u5217", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "\u80cc\u666f\u8272", "modal.style.text colour": "\u5b57\u7b26\u989c\u8272", "modal.style.text size": "\u5b57\u7b26\u5927\u5c0f", "modal.style.explanation": "\u7528\u8fd9\u4e9b\u63a7\u5236\u6765\u6539\u53d8\u8003\u8bd5\u7684\u5916\u89c2\u3002", "modal.style.reset to defaults": "\u91cd\u7f6e", "modal.style.text size preview": "\u5927\u591a\u6570\u5b57\u7b26\u90fd\u5c06\u5982\u6b64\u5927\u5c0f", "control.style options": "\u663e\u793a\u9009\u9879", "part.marking.partially correct": "\u60a8\u7684\u7b54\u6848\u90e8\u5206\u6b63\u786e\u3002", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "\u5b66\u4e60\u76ee\u6807", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "\u60a8\u53ef\u80fd\u5728\u540d\u79f0\u548c\u63cf\u8ff0\u540e\u9762\u7f3a\u5c11\u4e00\u4e2a\u5192\u53f7", "jme.script.note.invalid definition.description missing closing bracket": "\u60a8\u53ef\u80fd\u6ca1\u6253\u62ec\u53f7\u7684\u540e\u4e00\u534a\u3002", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "\u8f6c\u5230\u4e0b\u4e00\u4e2a\u95ee\u9898", "diagnostic.use retry": "\u4f7f\u7528\u6389\u4e00\u6b21\u91cd\u8bd5\uff0c\u5e76\u518d\u5c1d\u8bd5\u4e00\u4e0b\u8fd9\u4e2a\u4e3b\u9898\u3002", "diagnostic.move to next topic": "\u8f6c\u5230\u4e0b\u4e00\u4e2a\u8bdd\u9898\u3002", "diagnostic.next step question": "\u60a8\u63a5\u4e0b\u6765\u60f3\u505a\u4ec0\u4e48\uff1f", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "\u60a8\u8fd8\u67091\u6b21\u91cd\u8bd5\u673a\u4f1a", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "\u6d4b\u8bd5\u7ed3\u675f\u3002", "diagnostic.passed all lo": "\u60a8\u5df2\u7ecf\u901a\u8fc7\u4e86\u6240\u6709\u7684\u5b66\u4e60\u76ee\u6807\u3002", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "\u8f6c\u8fdb\u5230\u4e3b\u9898\u4e2d\u7684\u4e0b\u4e00\u4e2a\u95ee\u9898\u3002", "diagnostic.complete": "\u5b8c\u6210\u4e86\uff01", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "\u60a8\u7684\u56de\u7b54\u6b63\u5728\u8bc4\u5206\u79cd\u3002\u8bf7\u7b49\u5f85\u3002", "diagnostic.end test": "\u7ed3\u675f\u6d4b\u8bd5", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "\u786e\u5b9a", "modal.alert": "\u8b66\u544a", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "\u6784\u5efa\u5411\u91cf\u65f6\u5019\u7528\u4e86\u4e0d\u6b63\u786e\u7684\u6570\u503c\u7c7b\u578b\u3002", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "\u505a\u51fa\u9009\u62e9", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "\u77e9\u9635\u7684\u884c\u5217\u6570\u76ee\u5fc5\u987b\u81f3\u5c11\u4e00\u81f4\u3002", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "\u5f85\u8f93\u51fa\u5f20\u6570", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "\u663e\u793a\u5377ID \uff1f", "worksheet.page break between questions": "\u60f3\u5728\u4e24\u9898\u4e4b\u95f4\u6362\u9875 \uff1f", "worksheet.page margins": "\u9875\u8fb9\u8ddd\uff08\u6beb\u7c73\uff09", "worksheet.text size": "\u5b57\u7b26\u5927\u5c0f\uff08\u78c5\uff09", "worksheet.generate": "\u8f93\u51fa", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "\u5377\u7f16\u53f7", "worksheet.print single": "\u6253\u5370\u8be5\u5377", "worksheet.print several": "\u6253\u5370\u8fd9\u4e9b\u5377", "worksheet.answer sheets": "\u7b54\u5377", "worksheet.question sheets": "\u8bd5\u5377", "worksheet.reconfigure": "\u8f93\u51fa\u4e0d\u540c\u5377", "worksheet.show sheet": "\u4e0e\u7f16\u53f7\u4e00\u8d77\u9884\u89c8\u8be5\u5377", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "ar-sa": {"translation": {"page.loading": "\u062a\u062d\u0645\u064a\u0644...\n\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0635\u0641\u062d\u0629", "page.saving": "<p>\u062d\u0641\u0638</p>\n<p>\u0642\u062f \u064a\u0633\u062a\u063a\u0631\u0642 \u0647\u0630\u0627 \u0628\u0636\u0639 \u062b\u0648\u0627\u0646</p>", "mathjax.math processing error": "\"{{-message}}\" \u0639\u0646\u062f \u062a\u062d\u062f\u064a\u062f <code> {{\u062a\u0639\u0628\u064a\u0631}} </ code>", "die.numbas failed": "\u0641\u0634\u0644 Numbas ", "die.sorry": "\u0639\u0630\u0631\u0627\u064b\u060c \u062d\u0635\u0644 \u062e\u0637\u0623 \u0641\u064a Numbas \u0648\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629.\n\u0641\u064a\u0645\u0627 \u064a\u0644\u064a \u0648\u0635\u0641 \u0627\u0644\u062e\u0637\u0623.", "die.error": "\u062e\u0637\u0623", "modal.ok": "\u0645\u0648\u0627\u0641\u0642", "modal.cancel": "\u0627\u0644\u063a\u0627\u0621", "exam.exam name": "\u0627\u0633\u0645 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646", "exam.random seed": "\u0631\u0642\u0645 \u0627\u0644\u062c\u0644\u0633\u0629", "exam.student name": "\u0627\u0633\u0645 \u0627\u0644\u0637\u0627\u0644\u0628", "exam.number of questions": "\u0639\u062f\u062f \u0627\u0644\u0623\u0633\u0626\u0644\u0629", "exam.marks available": "\u0627\u0644\u062f\u0631\u062c\u0627\u062a \u0627\u0644\u0645\u062a\u0627\u062d\u0629", "exam.pass percentage": "\u0646\u0633\u0628\u0629 \u0627\u0644\u0646\u062c\u0627\u062d", "exam.time allowed": "\u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u062a\u0627\u062d", "exam.passed": "\u0646\u062c\u0627\u062d", "exam.failed": "\u0641\u0634\u0644", "exam.review header": "\u0645\u0631\u0627\u062c\u0639\u0629", "frontpage.start": "\u0627\u0644\u0628\u062f\u0621", "suspend.paused header": "\u062a\u0648\u0642\u0641 \u0645\u0624\u0642\u062a", "suspend.exam suspended": "\u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646 \u0645\u062a\u0648\u0642\u0641 \u0628\u0634\u0643\u0644 \u0645\u0624\u0642\u062a. \u0627\u0636\u063a\u0637 <em>\u0645\u062a\u0627\u0628\u0639\u0629</em> \u0644\u0644\u0627\u0633\u062a\u0645\u0631\u0627\u0631.", "suspend.you can resume": "\u064a\u0645\u0643\u0646\u0643 \u0625\u0643\u0645\u0627\u0644 \u0627\u0644\u0625\u062e\u062a\u0628\u0627\u0631 \u0641\u064a \u0627\u0644\u0645\u0631\u0629 \u0627\u0644\u0642\u0627\u062f\u0645\u0629", "suspend.resume": "\u0645\u062a\u0627\u0628\u0639\u0629", "result.exit": "\u0645\u063a\u0627\u062f\u0631\u0629 \u0627\u0644\u0625\u062e\u062a\u0628\u0627\u0631", "result.print": "\u0637\u0628\u0627\u0639\u0629 \u0645\u0644\u062e\u0635 \u0627\u0644\u0646\u062a\u0627\u0626\u062c", "result.exam summary": "\u0645\u0644\u062e\u0635 \u0627\u0644\u0625\u062e\u062a\u0628\u0627\u0631", "result.performance summary": "\u0645\u0644\u062e\u0635 \u0627\u0644\u0623\u062f\u0627\u0621", "result.exam start": "\u0628\u062f\u0621 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646:", "result.exam stop": "\u0627\u0646\u062a\u0647\u0627\u0621 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646:", "result.time spent": "\u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u0646\u0642\u0636\u064a:", "result.questions attempted": "\u0639\u062f\u062f \u0627\u0644\u0623\u0633\u0626\u0644\u0629 \u0627\u0644\u062a\u064a \u062a\u0645\u062a \u0645\u062d\u0627\u0648\u0644\u062a\u0647\u0627", "result.score": "\u0627\u0644\u062f\u0631\u062c\u0629", "result.result": "\u0627\u0644\u0646\u062a\u064a\u062c\u0629:", "result.question number": "\u0631\u0642\u0645 \u0627\u0644\u0633\u0624\u0627\u0644", "result.question score": "\u0627\u0644\u062f\u0631\u062c\u0629", "result.question review title": "\u0645\u0631\u0627\u062c\u0639\u0629 \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644", "result.click a question to review": "\u0627\u0636\u063a\u0637 \u0639\u0644\u0649 \u0631\u0642\u0645 \u0633\u0624\u0627\u0644 \u0644\u0631\u0624\u064a\u0629 \u0643\u064a\u0641\u064a\u0629 \u062a\u0642\u064a\u064a\u0645 \u0627\u062c\u0627\u0628\u0627\u062a\u0643\u060c \u0648\u0627\u0644\u062d\u0644\u0648\u0644 \u0627\u0644\u0643\u0627\u0645\u0644\u0629 \u0627\u0644\u0645\u062a\u0648\u0641\u0631\u0629.", "end.exam has finished": "\u0647\u0630\u0627 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646 \u0642\u062f \u0627\u0646\u062a\u0647\u0649. \u0628\u0625\u0645\u0643\u0627\u0646\u0643 \u0627\u063a\u0644\u0627\u0642 \u0647\u0630\u0647 \u0627\u0644\u0646\u0627\u0641\u0630\u0629.", "control.confirm leave": "\u0644\u0645 \u062a\u0643\u0645\u0644 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646.", "control.not all questions answered": "\u0644\u0645 \u062a\u0643\u0645\u0644 \u0643\u0644 \u0623\u0633\u0626\u0644\u0629 \u0627\u0644\u0625\u0645\u062a\u062d\u0627\u0646.", "control.not all questions submitted": "\u0644\u0642\u062f \u0642\u0645\u062a \u0628\u062a\u0639\u062f\u064a\u0644 \u0625\u062c\u0627\u0628\u0629 \u0623\u0648 \u0623\u0643\u062b\u0631 \u0648\u0644\u0645 \u062a\u0642\u0645 \u0628\u062a\u0623\u0643\u064a\u062f \u0627\u0644\u062a\u0639\u062f\u064a\u0644. \u064a\u0631\u062c\u0649 \u062a\u0623\u0643\u064a\u062f \u062a\u0639\u062f\u064a\u0644\u0627\u062a\u0643 \u0644\u0643\u0644 \u0627\u0644\u0623\u0633\u0626\u0644\u0629.", "control.confirm end": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0627\u062e\u062a\u0628\u0627\u0631\u061f \u0628\u0639\u062f \u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0627\u062e\u062a\u0628\u0627\u0631 \u060c \u0644\u0646 \u062a\u062a\u0645\u0643\u0646 \u0645\u0646 \u062a\u063a\u064a\u064a\u0631 \u0623\u064a \u0645\u0646 \u0625\u062c\u0627\u0628\u0627\u062a\u0643.", "control.confirm regen": "\u0647\u0644 \u062a\u0631\u063a\u0628 \u0628\u0627\u0633\u062a\u0628\u062f\u0627\u0644 \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644 \u0639\u0634\u0648\u0627\u0626\u064a\u0627\u064b\u061f \u0625\u0630\u0627 \u0646\u0642\u0631\u062a \"\u0645\u0648\u0627\u0641\u0642\" \u0641\u0633\u0648\u0641 \u062a\u0641\u0642\u062f \u0643\u0627\u0641\u0629 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0648\u0639\u0644\u0627\u0645\u0627\u062a\u0643 \u0639\u0646 \u0627\u0644\u0633\u0624\u0627\u0644 \u0627\u0644\u062d\u0627\u0644\u064a", "control.confirm reveal": "\u0647\u0644 \u062a\u0631\u063a\u0628 \u0628\u0627\u0644\u0643\u0634\u0641 \u0639\u0646 \u0625\u062c\u0627\u0628\u0629 \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644\u061f \u0633\u064a\u062a\u0645 \u0625\u0642\u0641\u0627\u0644 \u0623\u064a \u0639\u0644\u0627\u0645\u0627\u062a \u062a\u0644\u0642\u064a\u062a\u0647\u0627 \u062d\u062a\u0649 \u0627\u0644\u0622\u0646 \u0648\u0644\u0646 \u062a\u062a\u0645\u0643\u0646 \u0645\u0646 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0639\u0644\u0649 \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644 \u0644\u0627\u062d\u0642\u064b\u0627.", "control.proceed anyway": "\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629 \u0639\u0644\u0649 \u0623\u064a \u062d\u0627\u0644", "control.regen": "\u0645\u062d\u0627\u0648\u0644\u0629 \u0633\u0624\u0627\u0644 \u0622\u062e\u0631 \u0645\u062b\u0644 \u0647\u0630\u0627", "control.submit answer": "\u0623\u0631\u0633\u0644 \u0627\u0644\u0625\u062c\u0627\u0628\u0629", "control.submit all parts": "\u0623\u0631\u0633\u0644 \u0643\u0644 \u0627\u0644\u0623\u062c\u0632\u0627\u0621", "control.submit again": "\u0623\u0631\u0633\u0644 \u0645\u0631\u0629 \u062b\u0627\u0646\u064a\u0629", "control.submit": "\u0623\u0631\u0633\u0644", "control.previous": "\u0627\u0644\u0633\u0627\u0628\u0642", "control.next": "\u0627\u0644\u062a\u0627\u0644\u064a", "control.advice": "\u0645\u0633\u0627\u0639\u062f\u0629", "control.reveal": "\u0643\u0634\u0641 \u0627\u0644\u0625\u062c\u0627\u0628\u0627\u062a", "control.total": "\u0627\u0644\u0645\u062c\u0645\u0648\u0639", "control.pause": "\u0625\u064a\u0642\u0627\u0641 \u0645\u0624\u0642\u062a", "control.end exam": "\u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646", "control.back to results": "\u0627\u0644\u0639\u0648\u062f\u0629 \u0625\u0644\u0649 \u0627\u0644\u0646\u062a\u0627\u0626\u062c", "display.part.jme.error making maths": "\u064a\u0648\u062c\u062f \u062e\u0637\u0623 \u0641\u064a \u0639\u0631\u0636 \u0627\u0644\u0635\u064a\u063a\u0629 \u0627\u0644\u0631\u064a\u0627\u0636\u064a\u0629", "exam.xml.bad root": "\u0627\u0644\u0639\u0646\u0635\u0631 \u0627\u0644\u0623\u0633\u0627\u0633\u064a \u0641\u064a \u0645\u0644\u0641 XML \u0627\u0644\u062e\u0627\u0635 \u0628\u0627\u0644\u0625\u0645\u062a\u062d\u0627\u0646\u060c \u064a\u062c\u0628 \u0623\u0646 \u064a\u0643\u0648\u0646 'exam'", "exam.changeQuestion.no questions": "\u0647\u0630\u0627 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646 \u0644\u0627 \u064a\u062d\u0648\u064a \u0623\u064a \u0623\u0633\u0626\u0644\u0629! \u062a\u0623\u0643\u062f \u0645\u0646 \u062e\u0644\u0648 \u0645\u0644\u0641 exam. \u0645\u0646 \u0627\u0644\u0623\u062e\u0637\u0627\u0621.", "feedback.you were awarded": "\n\u062a\u0645 \u0645\u0646\u062d\u0643 <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "\n\u062a\u0645 \u0633\u062d\u0628 <strong> {{count\u060c niceNumber}} </strong> $ t (\u0639\u0644\u0627\u0645\u0629) $ t (\u0639\u0644\u0627\u0645\u0629).", "jme.tokenise.invalid": "Invalid expression: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0648\u0633\u064a\u0637\u0627\u062a \u0643\u0627\u0641\u064a\u0629 \u0644\u0644\u0639\u0645\u0644\u064a\u0629 <code> {{op}} </code>", "jme.shunt.no left bracket in function": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0642\u0648\u0633 \u0623\u064a\u0633\u0631 \u0645\u0637\u0627\u0628\u0642 \u0641\u064a \u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0648\u0638\u064a\u0641\u0629 \u0623\u0648 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629", "jme.shunt.no left square bracket": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0642\u0648\u0633 \u0627\u0644\u0623\u064a\u0633\u0631", "jme.shunt.no left bracket": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0642\u0648\u0633 \u0627\u0644\u0623\u064a\u0633\u0631", "jme.shunt.no right bracket": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0642\u0648\u0633 \u0627\u0644\u0623\u064a\u0645\u0646", "jme.shunt.no right square bracket": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0642\u0648\u0633 \u0627\u0644\u0623\u064a\u0645\u0646 \u0644\u0625\u0646\u0647\u0627\u0621 \u0627\u0644\u0642\u0627\u0626\u0645\u0629", "jme.shunt.missing operator": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0642\u064a\u0645\u0629 \u0627\u0644\u062a\u0639\u0628\u064a\u0631 \u0627\u0644\u0631\u064a\u0627\u0636\u064a\u060c \u064a\u0648\u062c\u062f \u0645\u0624\u062b\u0631 \u0645\u0641\u0642\u0648\u062f", "jme.typecheck.function maybe implicit multiplication": "\n\u0627\u0644\u0648\u0638\u064a\u0641\u0629 <code> {{name}} </code> \u063a\u064a\u0631 \u0645\u062d\u062f\u062f\u0629. \u0647\u0644 \u062a\u0642\u0635\u062f <code> {{first}} * {{\u0627\u0644\u0645\u062d\u062a\u0645\u0644\u0629Op}} (...) </code>\u061f", "jme.typecheck.function not defined": "\u0627\u0644\u0648\u0638\u064a\u0641\u0629 <code> {{op}} </code> \u063a\u064a\u0631 \u0645\u062d\u062f\u062f\u0629. \u0647\u0644 <code> {{op}} </code> \u0645\u062a\u063a\u064a\u0631 \u060c \u0648\u0647\u0644 \u062a\u0642\u0635\u062f <code> {{Suggestion}} * (...) </code>\u061f", "jme.typecheck.op not defined": "\u0627\u0644\u0639\u0645\u0644\u064a\u0629 \"{{op}}\" \u063a\u064a\u0631 \u0645\u062d\u062f\u062f\u0629.", "jme.typecheck.no right type definition": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u062a\u0639\u0631\u064a\u0641 '{{op}}' \u0645\u0646 \u0627\u0644\u0646\u0648\u0639 \u0627\u0644\u0635\u062d\u064a\u062d.", "jme.typecheck.no right type unbound name": "\u0627\u0644\u0645\u062a\u063a\u064a\u0631 <code> {{name}} </code> \u063a\u064a\u0631 \u0645\u0639\u0631\u0641.", "jme.typecheck.map not on enumerable": "\u064a\u062c\u0628 \u0623\u0646 \u062a\u0639\u0645\u0644 \u0639\u0645\u0644\u064a\u0629 <code> map </code> \u0639\u0628\u0631 \u0642\u0627\u0626\u0645\u0629 \u0623\u0648 \u0646\u0637\u0627\u0642 \u060c \u0648\u0644\u064a\u0633 {{type}}", "jme.evaluate.undefined variable": "\u0627\u0644\u0645\u062a\u063a\u064a\u0631 {{\u0627\u0644\u0625\u0633\u0645}} \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f", "jme.thtml.not html": "\u062a\u0645\u0631\u064a\u0631 \u0642\u064a\u0645\u0629 \u063a\u064a\u0631 HTML \u0625\u0644\u0649 \u0645\u064f\u0646\u0634\u0626 THTML.", "jme.func.switch.no default case": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u062e\u062a\u064a\u0627\u0631 \u0642\u064a\u0645\u0629 \u0625\u0641\u062a\u0631\u0627\u0636\u064a\u0629 \u0644\u062c\u0645\u0644\u0629 \u0627\u0644\u062a\u062d\u0648\u064a\u0644 (Switch)", "jme.func.listval.invalid index": "\u0641\u0647\u0631\u0633 \u0642\u0627\u0626\u0645\u0629 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d {{index}} \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u062d\u062c\u0645 {{size}}", "jme.func.listval.not a list": "\u063a\u0631\u0636 \u063a\u064a\u0631 \u0642\u0627\u0628\u0644 \u0644\u0644\u0643\u062a\u0627\u0628\u0629", "jme.func.matrix.invalid row type": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0625\u0646\u0634\u0627\u0621 \u0645\u0635\u0641\u0648\u0641\u0629 \u0645\u0646 \u0623\u0633\u0637\u0631 \u0645\u0646 \u0627\u0644\u0646\u0648\u0639 {{type}}.", "jme.func.except.continuous range": "\u0644\u0627 \u064a\u0645\u0643\u0646\u0643 \u0627\u0633\u062a\u0639\u0645\u0627\u0644 \u0645\u0634\u063a\u0651\u0644 \u0637\u0645\u0627 \u0639\u062f\u0627\u0637 \u0641\u064a \u0627\u0644\u0646\u0637\u0627\u0642\u0627\u062a \u0627\u0644\u0645\u0633\u062a\u0645\u0631\u0629", "jme.matrix.reports bad size": "\u062a\u0634\u064a\u0631 \u0627\u0644\u0645\u0635\u0641\u0648\u0641\u0629 \u0625\u0644\u0649 \u062d\u062c\u0645\u0647\u0627 \u0628\u0634\u0643\u0644 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d - \u0644\u0627 \u0628\u062f \u0627\u0646\u0647 \u062e\u0637\u0623 \u0641\u064a \u062f\u0627\u0644\u0651\u0629 \u0627\u0644\u0645\u064f\u0646\u0634\u0626 (Constructor)", "jme.texsubvars.no right bracket": "\u0644\u0627 \u064a\u0648\u062c\u062f <code>] </code> \u0645\u0637\u0627\u0628\u0642 \u0641\u064a \u0648\u0633\u064a\u0637\u0627\u062a {{op}}.", "jme.texsubvars.missing parameter": "\u0627\u0644\u0645\u0639\u0644\u0645\u0629 \u0645\u0641\u0642\u0648\u062f\u0629 \u0641\u064a {{op}}: {{\u0645\u0639\u0644\u0645\u0629}}", "jme.texsubvars.no right brace": "\u0644\u0627 \u064a\u0648\u062c\u062f <code>} </code> \u0645\u0637\u0627\u0628\u0642 \u0641\u064a {{op}}", "jme.user javascript.error": "\u062e\u0637\u0623 \u0641\u064a \u0648\u0638\u064a\u0641\u0629 \u062c\u0627\u0641\u0627 \u0633\u0643\u0631\u064a\u0628\u062a \u0627\u0644\u0645\u0639\u0631\u0641\u0629 \u0645\u0646 \u0642\u0628\u0644 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 <code> {{name}} </code>: {{-message}", "jme.variables.error making function": "\u062e\u0637\u0623 \u0641\u064a \u0639\u0645\u0644 \u0627\u0644\u0648\u0638\u064a\u0641\u0629 <code> {{name}} </code>: {{-message}}", "jme.variables.syntax error in function definition": "\u062e\u0637\u0623 \u0641\u064a \u0643\u062a\u0627\u0628\u0629 \u0625\u0633\u0645 \u0627\u0644\u062f\u0627\u0644\u0629", "jme.variables.variable not defined": "\u0627\u0644\u0645\u062a\u063a\u064a\u0631 <code> {{name}} </code> \u063a\u064a\u0631 \u0645\u0639\u0631\u0641.", "jme.variables.empty definition": "\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 <code> {{name}} </code> \u0641\u0627\u0631\u063a.", "jme.variables.circular reference": "\u0645\u0631\u062c\u0639 \u0645\u062a\u063a\u064a\u0631 \u062f\u0627\u0626\u0631\u064a \u0641\u064a \u062a\u0639\u0631\u064a\u0641 <code> {{name}} </code>", "jme.variables.error computing dependency": "\u062d\u062f\u062b \u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u062d\u0648\u0633\u0628\u0629 \u064a\u0634\u064a\u0631 \u0625\u0644\u0649 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 <code> {{name}} </code>", "jme.variables.error evaluating variable": "\u062e\u0637\u0623 \u0641\u064a \u062a\u0642\u064a\u064a\u0645 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u0646\u0634\u0627\u0621 \u0645\u062c\u0645\u0648\u0639\u0629 \u0635\u0627\u0644\u062d\u0629 \u0645\u0646 \u0645\u062a\u063a\u064a\u0631\u0627\u062a \u0627\u0644\u0633\u0624\u0627\u0644 \u0641\u064a \u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u0646\u0627\u0633\u0628.", "jme.display.unknown token type": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u062f\u064a\u062f \u0646\u0648\u0639 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 {{type}}", "jme.display.collectRuleset.no sets": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u0633\u0644\u064a\u0645 \u0645\u062c\u0645\u0648\u0639\u0627\u062a \u0644 collectRuleset!", "jme.display.collectRuleset.set not defined": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u062f\u064a\u062f \u0645\u062c\u0645\u0648\u0639\u0629 \u0627\u0644\u0642\u0648\u0627\u0639\u062f {{name}}", "jme.display.simplifyTree.no scope given": "\u064a\u062c\u0628 \u0625\u0639\u0637\u0627\u0621 \u0645\u062c\u0627\u0644 \u0644 Numbas.jme.display.simplify \u0627\u0644\u0634\u062c\u0631\u0629", "math.precround.complex": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0642\u0631\u064a\u0628 \u0625\u0644\u0649 \u0639\u062f\u062f \u0645\u0639\u0642\u062f \u0645\u0646 \u0627\u0644\u0645\u0646\u0627\u0632\u0644 \u0627\u0644\u0639\u0634\u0631\u064a\u0629.", "math.siground.complex": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0642\u0631\u064a\u0628 \u0625\u0644\u0649 \u0639\u062f\u062f \u0645\u0639\u0642\u062f \u0645\u0646 \u0627\u0644\u0623\u0631\u0642\u0627\u0645 \u0627\u0644\u0645\u0647\u0645\u0629.", "math.combinations.complex": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u062a\u0648\u0644\u064a\u0641\u0627\u062a \u0644\u0623\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0631\u0643\u0628\u0629.", "math.permutations.complex": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u062a\u0628\u0627\u062f\u064a\u0644 \u0644\u0623\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0631\u0643\u0628\u0629.", "math.gcf.complex": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \"\u0627\u0644\u0639\u0627\u0645\u0644 \u0627\u0644\u0645\u0634\u062a\u0631\u0643 \u0627\u0644\u0623\u0643\u0628\u0631\" \u0644\u0623\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0631\u0643\u0628\u0629.", "math.lcm.complex": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \"\u0627\u0644\u0645\u0636\u0627\u0639\u0641 \u0627\u0644\u0645\u0634\u062a\u0631\u0643 \u0627\u0644\u0623\u0635\u063a\u0631\" \u0644\u0623\u0639\u062f\u0627\u062f \u0645\u0631\u0643\u0628\u0629.", "math.lt.order complex numbers": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062a\u0631\u062a\u064a\u0628 \u0627\u0644\u0623\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0631\u0643\u0628\u0629", "math.choose.empty selection": "\u062a\u0645 \u0625\u0639\u0637\u0627\u0621 \u0627\u0644\u062f\u0627\u0644\u0629 \u0627\u0644\u0639\u0634\u0648\u0627\u0626\u064a\u0629 \u0627\u062e\u062a\u064a\u0627\u0631 \u0641\u0627\u0631\u063a.", "matrixmath.abs.non-square": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0645\u062d\u062f\u062f\u0629 \u0645\u0635\u0641\u0648\u0641\u0629 \u0644\u064a\u0633\u062a \u0645\u0631\u0628\u0639\u0629.", "matrixmath.abs.too big": "\u0639\u0630\u0631\u064b\u0627 \u060c \u0644\u0627 \u064a\u0645\u0643\u0646 \u0641\u064a \u0627\u0644\u0648\u0642\u062a \u0627\u0644\u062d\u0627\u0644\u064a \u062d\u0633\u0627\u0628 \u0645\u062d\u062f\u062f\u0629 \u0645\u0635\u0641\u0648\u0641\u0629 \u0645\u0642\u0627\u064a\u064a\u0633\u0647\u0627 \u0623\u0643\u0628\u0631 \u0645\u0646 3x3.", "matrixmath.mul.different sizes": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062c\u062f\u0627\u0621 \u0645\u0635\u0641\u0648\u0641\u0627\u062a \u0645\u062e\u062a\u0644\u0641\u0629 \u0627\u0644\u0623\u062d\u062c\u0627\u0645.", "vectormath.cross.not 3d": "\u064a\u0645\u0643\u0646 \u0623\u062e\u0630 \u0627\u0644\u0645\u0646\u062a\u062c \u0627\u0644\u0645\u062a\u0642\u0627\u0637\u0639 \u0644\u0646\u0627\u0642\u0644\u0627\u062a \u062b\u0644\u0627\u062b\u064a\u0629 \u0627\u0644\u0623\u0628\u0639\u0627\u062f \u0641\u0642\u0637.", "vectormath.dot.matrix too big": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u0645\u0646\u062a\u062c \u0627\u0644\u0646\u0642\u0637\u064a \u0644\u0645\u0635\u0641\u0648\u0641\u0629 \u0644\u064a\u0633\u062a $ 1 \\ times N $ \u0623\u0648 $ N \\ \u0645\u0636\u0631\u0648\u0628\u0629 $ 1.", "vectormath.cross.matrix too big": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u0645\u0646\u062a\u062c \u0627\u0644\u0645\u062a\u0642\u0627\u0637\u0639 \u0644\u0645\u0635\u0641\u0648\u0641\u0629 \u0644\u064a\u0633\u062a $ 1 \\ times N $ \u0623\u0648 $ N \\ \u0645\u0636\u0631\u0648\u0628\u0629 $ 1.", "part.with steps answer prompt": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629:", "part.script.error": "\u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u062c\u0632\u0621 {{path}} \u0627\u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0646\u0635\u064a \u0627\u0644\u0645\u062e\u0635\u0635 {{script}}: {{-message}}", "part.marking.steps no matter": "\u0628\u0633\u0628\u0628 \u062d\u0635\u0648\u0644\u0643 \u0639\u0644\u0649 \u0639\u0644\u0627\u0645\u0627\u062a \u0643\u0627\u0645\u0644\u0629 \u0644\u0644\u062c\u0632\u0621\u060c \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0639\u0644\u0649 \u0627\u0644\u062e\u0637\u0648\u0627\u062a \u0644\u0646 \u062a\u062d\u062a\u0633\u0628.", "part.marking.revealed steps no penalty": "\u0642\u0645\u062a \u0628\u0627\u0644\u0643\u0634\u0641 \u0639\u0646 \u0647\u0630\u0647 \u0627\u0644\u062e\u0637\u0648\u0629", "part.marking.used variable replacements": "\u062a\u0645 \u062a\u0635\u062d\u064a\u062d \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644 \u0628\u0646\u0627\u0621 \u0639\u0644\u0649 \u0625\u062c\u0627\u0628\u062a\u0643 \u0641\u064a \u062c\u0632\u0621 \u0633\u0627\u0628\u0642", "part.marking.variable replacement part not answered": "\u064a\u062c\u0628 \u0639\u0644\u064a\u0643 \u0623\u0648\u0644\u0627 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0639\u0646 {{\u0642\u0633\u0645}}.", "part.marking.resubmit because of variable replacement": "\u0639\u0644\u0627\u0645\u0627\u062a \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u062a\u0639\u062a\u0645\u062f \u0639\u0644\u0649 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0639\u0644\u0649 \u0623\u062c\u0632\u0627\u0621 \u0623\u062e\u0631\u0649 \u0642\u0645\u062a \u0628\u062a\u063a\u064a\u064a\u0631\u0647\u0627. \u0623\u0631\u0633\u0644 \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649 \u0645\u0646 \u0623\u062c\u0644 \u062a\u062d\u062f\u064a\u062b \u062f\u0631\u062c\u0627\u062a\u0643.", "part.marking.not submitted": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0625\u062c\u0627\u0628\u0629", "part.marking.did not answer": "\u0644\u0645 \u062a\u0642\u0645 \u0628\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0639\u0644\u0649 \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644", "part.marking.nothing entered": "\u0644\u0645 \u062a\u0642\u0645 \u0628\u0625\u062f\u062e\u0627\u0644 \u0627\u0644\u0625\u062c\u0627\u0628\u0629", "part.marking.incorrect": "\u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629", "part.marking.correct": "\u0625\u062c\u0627\u0628\u0629 \u0635\u062d\u064a\u062d\u0629", "part.marking.uncaught error": "\u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u0639\u0644\u0627\u0645\u0627\u062a {{-\u0631\u0633\u0627\u0644\u0629}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0645\u062a\u0648\u0642\u0639\u0629: ", "part.missing type attribute": "{{part}}: \u0633\u0645\u0629 \u0646\u0648\u0639 \u0627\u0644\u062c\u0632\u0621 \u0645\u0641\u0642\u0648\u062f\u0629", "part.unknown type": "{{part}}: \u0646\u0648\u0639 \u062c\u0632\u0621 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641 {{type}}", "part.setting not present": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u0639\u064a\u064a\u0646 \u0627\u0644\u062e\u0627\u0635\u064a\u0629 \"{{property}}\"", "part.jme.answer missing": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629 \u0645\u0641\u0642\u0648\u062f\u0629", "part.jme.answer too long": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0623\u0637\u0648\u0644 \u0645\u0646 \u0627\u0644\u0645\u062a\u0648\u0642\u0639 ", "part.jme.answer too short": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0623\u0642\u0635\u0631\u0645\u0646 \u0627\u0644\u0645\u062a\u0648\u0642\u0639 ", "part.jme.answer invalid": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u064a\u0633\u062a \u062a\u0639\u0628\u064a\u0631\u064b\u0627 \u0631\u064a\u0627\u0636\u064a\u064b\u0627 \u0635\u0627\u0644\u062d\u064b\u0627. <br/> {{-message}}.", "part.jme.marking.correct": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629 \u0639\u062f\u062f\u064a\u0627", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "\u064a\u062c\u0628 \u0623\u0646 \u062a\u062d\u062a\u0648\u064a \u0625\u062c\u0627\u0628\u062a\u0643 \u0639\u0644\u0649: {{strings}}", "part.jme.must-have several": "\u0625\u062c\u0627\u0628\u062a\u0643 \u064a\u062c\u0628 \u0627\u0646 \u062a\u062d\u0648\u0644 \u0643\u0644 \u0627\u0644 {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u0627 \u062a\u062d\u062a\u0648\u064a \u0639\u0644\u0649 {{strings}}", "part.jme.not-allowed several": "\u064a\u062c\u0628 \u0623\u0644\u0627 \u062a\u062d\u062a\u0648\u064a \u0625\u062c\u0627\u0628\u062a\u0643 \u0639\u0644\u0649 \u0623\u064a \u0645\u0646: {{strings}}", "part.jme.unexpected variable name": "\u062a\u0645 \u062a\u0641\u0633\u064a\u0631 \u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0633\u0645 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 \u063a\u064a\u0631 \u0627\u0644\u0645\u062a\u0648\u0642\u0639 <code> {{name}} </code>.", "part.jme.unexpected variable name suggestion": "\u062a\u0645 \u062a\u0641\u0633\u064a\u0631 \u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0633\u0645 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 \u063a\u064a\u0631 \u0627\u0644\u0645\u062a\u0648\u0642\u0639 <code> {{name}} </code>. \u0647\u0644 \u062a\u0642\u0635\u062f <code> {{Suggestion}} </code>\u061f", "part.patternmatch.display answer missing": "\u0644\u0645 \u064a\u062a\u0645 \u0639\u0631\u0636 \u0627\u0644\u0646\u062a\u0627\u0626\u062c", "part.patternmatch.correct except case": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0635\u062d\u064a\u062d\u0629 \u060c \u0628\u0627\u0633\u062a\u062b\u0646\u0627\u0621 \u0627\u0644\u062d\u0627\u0644\u0629.", "part.numberentry.correct except decimal": "\u0625\u062c\u0627\u0628\u062a\u0643 \u062a\u0642\u0639 \u0636\u0645\u0646 \u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647 \u060c \u0648\u0644\u0643\u0646 \u0627\u0644\u0623\u0631\u0642\u0627\u0645 \u0627\u0644\u0639\u0634\u0631\u064a\u0629 \u063a\u064a\u0631 \u0645\u0633\u0645\u0648\u062d \u0628\u0647\u0627.", "part.numberentry.correct except fraction": "\u0625\u062c\u0627\u0628\u062a\u0643 \u062a\u0642\u0639 \u0636\u0645\u0646 \u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647 \u060c \u0648\u0644\u0643\u0646 \u0627\u0644\u0643\u0633\u0648\u0631 \u063a\u064a\u0631 \u0645\u0633\u0645\u0648\u062d \u0628\u0647\u0627.", "part.numberentry.answer invalid": "\u0644\u0645 \u062a\u0642\u0645 \u0628\u0625\u062f\u062e\u0627\u0644 \u0631\u0642\u0645 \u0635\u0627\u0644\u062d", "part.numberentry.answer not integer": "\u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629\u060c \u0642\u0645\u062a \u0628\u0625\u062f\u062e\u0627\u0644 \u0639\u062f\u062f \u0635\u062d\u064a\u062d \u0648 \u0644\u064a\u0633 \u0639\u062f\u062f \u0639\u0634\u0631\u064a", "part.numberentry.answer not integer or decimal": "\u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629. \u064a\u062c\u0628 \u0625\u062f\u062e\u0627\u0644 \u0639\u062f\u062f \u0635\u062d\u064a\u062d \u0623\u0648 \u0639\u062f\u062f \u0639\u0634\u0631\u064a.", "part.numberentry.zero sig fig": "\u062a\u0645 \u0625\u0639\u062f\u0627\u062f \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0644\u062a\u0642\u0631\u064a\u0628 \u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0637\u0627\u0644\u0628 \u0625\u0644\u0649 \u0635\u0641\u0631 \u0645\u0646 \u0627\u0644\u0623\u0631\u0642\u0627\u0645 \u0627\u0644\u0645\u0647\u0645\u0629 \u060c \u0648\u0627\u0644\u062a\u064a \u0644\u0627 \u0645\u0639\u0646\u0649 \u0644\u0647\u0627.", "part.mcq.options def not a list": "\u0627\u0644\u062a\u0639\u0628\u064a\u0631 \u0627\u0644\u0630\u064a \u064a\u0639\u0631\u0651\u0641 {{properties}} \u0644\u064a\u0633 \u0642\u0627\u0626\u0645\u0629.", "part.mcq.marking matrix string empty": "\u062a\u0639\u0628\u064a\u0631 \u0645\u0635\u0641\u0648\u0641\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u0645\u062e\u0635\u0635\u0629 \u0641\u0627\u0631\u063a .", "part.mcq.choices missing": "\u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0627\u062e\u062a\u064a\u0627\u0631\u0627\u062a \u0645\u0641\u0642\u0648\u062f", "part.mcq.matrix not a number": "\u0627\u0644\u062c\u0632\u0621 {{part}} \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0639\u0644\u0649 \u062e\u0644\u064a\u0629 \u0627\u0644\u0645\u0635\u0641\u0648\u0641\u0629 ({{row}} \u060c {{\u0627\u0644\u0639\u0645\u0648\u062f}}) \u0644\u0627 \u064a\u062a\u0645 \u062a\u0642\u064a\u064a\u0645\u0647 \u0625\u0644\u0649 \u0631\u0642\u0645", "part.mcq.wrong number of choices": "\u0627\u062e\u062a\u0631\u062a \u0639\u062f\u062f\u064b\u0627 \u062e\u0627\u0637\u0626\u064b\u0627 \u0645\u0646 \u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a.", "part.mcq.no choices selected": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0625\u062e\u062a\u064a\u0627\u0631 ", "part.mcq.matrix not a list": "\u0645\u0635\u0641\u0648\u0641\u0629 \u0627\u0644\u0648\u0633\u0645 \u060c \u0627\u0644\u0645\u0639\u0631\u0641\u0629 \u0628\u0648\u0627\u0633\u0637\u0629 \u062a\u0639\u0628\u064a\u0631 JME \u060c \u0644\u064a\u0633\u062a \u0642\u0627\u0626\u0645\u0629 \u0648\u0644\u0643\u0646 \u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0643\u0630\u0644\u0643.", "part.mcq.matrix wrong type": "\u0639\u0646\u0635\u0631 \u0645\u0646 \u0627\u0644\u0646\u0648\u0639 \u063a\u064a\u0631 \u0627\u0644\u0635\u0627\u0644\u062d '{{type}}' \u064a\u0633\u062a\u062e\u062f\u0645 \u0641\u064a \u0645\u0635\u0641\u0648\u0641\u0629 \u0627\u0644\u0648\u0633\u0645.", "part.mcq.matrix mix of numbers and lists": "\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u0632\u064a\u062c \u0645\u0646 \u0627\u0644\u0623\u0631\u0642\u0627\u0645 \u0648\u0627\u0644\u0642\u0648\u0627\u0626\u0645 \u0641\u064a \u0645\u0635\u0641\u0648\u0641\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0627\u062a.", "part.mcq.matrix wrong size": "\u062e\u0637\u0623 \u0641\u064a \u062d\u062c\u0645 \u0645\u0635\u0641\u0648\u0641\u0629 \u0627\u0644\u0639\u0644\u0627\u0645\u0627\u062a.", "part.mcq.correct choice": "\u0642\u0645\u062a \u0628\u0625\u062e\u062a\u064a\u0627\u0631 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629", "part.matrix.answer invalid": "\u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0645\u0642\u0628\u0648\u0644\u0629", "part.matrix.invalid cell": "\u0648\u0627\u062d\u062f\u0629 \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0627\u0644\u062e\u0644\u0627\u064a\u0627 \u0641\u064a \u0625\u062c\u0627\u0628\u062a\u0643 \u0641\u0627\u0631\u063a\u0629 \u0623\u0648 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629.", "part.matrix.some incorrect": "\u062e\u0644\u064a\u0629 \u0648\u0627\u062d\u062f\u0629 \u0623\u0648 \u0623\u0643\u062b\u0631 \u0641\u064a \u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629 \u060c \u0648\u0644\u0643\u0646 \u062a\u0645 \u0645\u0646\u062d\u0643 \u0639\u0644\u0627\u0645\u0627\u062a \u0644\u0644\u0628\u0642\u064a\u0629.", "part.matrix.empty": "\u0644\u0645 \u062a\u0642\u0645 \u0628\u0625\u062e\u062a\u064a\u0627\u0631 \u0623\u064a \u0625\u062c\u0627\u0628\u0629", "part.matrix.empty cell": "\u062e\u0644\u064a\u0629 \u0623\u0648 \u0623\u0643\u062b\u0631 \u0641\u064a \u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0645\u0643\u062a\u0645\u0644\u0629", "part.matrix.size mismatch": "\u0644\u0645 \u064a\u0633\u0645\u062d \u0645\u0624\u0644\u0641 \u0627\u0644\u0633\u0624\u0627\u0644 \u0644\u0644\u0637\u0627\u0644\u0628 \u0628\u062a\u062d\u062f\u064a\u062f \u0623\u0628\u0639\u0627\u062f \u0625\u062c\u0627\u0628\u062a\u0647 \u060c \u0644\u0643\u0646 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629 \u0647\u064a {{correct_dimensions}} \u0628\u064a\u0646\u0645\u0627 \u0625\u062f\u062e\u0627\u0644 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0647\u0648 {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "\u0644\u0645 \u064a\u0646\u0641\u0630 \u0627\u0644\u062c\u0632\u0621 \u0637\u0631\u064a\u0642\u0629 <code> {{name}} </code>.", "question.loaded name mismatch": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0633\u062a\u0626\u0646\u0627\u0641 \u0647\u0630\u0647 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 - \u0644\u0642\u062f \u062a\u063a\u064a\u0631\u062a \u0627\u0644\u062d\u0632\u0645\u0629 \u0645\u0646\u0630 \u0627\u0644\u062c\u0644\u0633\u0629 \u0627\u0644\u0623\u062e\u064a\u0631\u0629.", "question.error": "\u0627\u0644\u0633\u0624\u0627\u0644 {{number}}: {{-message}}", "question.preamble.error": "\n\u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u062f\u064a\u0628\u0627\u062c\u0629: {{-message}}", "question.preamble.syntax error": "\u062e\u0637\u0623 \u0641\u064a \u0628\u0646\u0627\u0621 \u0627\u0644\u062c\u0645\u0644\u0629 \u0641\u064a \u0627\u0644\u062f\u064a\u0628\u0627\u062c\u0629", "question.unsupported part type": "\u0646\u0648\u0639 \u062c\u0632\u0626\u064a \u063a\u064a\u0631 \u0645\u062f\u0639\u0648\u0645", "question.header": "\u0633\u0624\u0627\u0644 {{number}}", "question.submit part": "\u0625\u0631\u0633\u0644 \u0627\u0644\u0625\u062c\u0627\u0628\u0629", "question.show steps": "\u0623\u0638\u0647\u0631 \u0627\u0644\u062e\u0637\u0648\u0627\u062a", "question.show steps penalty": "\n\u0633\u062a\u0641\u0642\u062f <strong> {{count\u060c niceNumber}} </strong> $ t (\u0639\u0644\u0627\u0645\u0629).", "question.show steps no penalty": "\u0644\u0646 \u062a\u062a\u0623\u062b\u0631 \u0646\u062a\u064a\u062c\u062a\u0643 \u0628\u0630\u0644\u0643", "question.show steps already penalised": "\u0644\u0642\u062f \u0642\u0645\u062a \u0645\u0646 \u0642\u0628\u0644 \u0628\u0639\u0631\u0636 \u0627\u0644\u062e\u0637\u0648\u0627\u062a. \u064a\u0645\u0643\u0646\u0643 \u0625\u0639\u0627\u062f\u0629 \u0625\u0638\u0647\u0627\u0631\u0647\u0645 \u0628\u062f\u0648\u0646 \u0639\u0642\u0648\u0628\u0629 \u0625\u0636\u0627\u0641\u064a\u0629.", "question.hide steps": "\u0623\u062e\u0641\u064a \u0627\u0644\u062e\u0637\u0648\u0627\u062a", "question.hide steps no penalty": "\u0644\u0646 \u062a\u062a\u0623\u062b\u0631 \u0646\u062a\u064a\u062c\u062a\u0643", "question.advice": "\u0646\u0635\u064a\u062d\u0629", "question.no such part": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u0639\u062b\u0648\u0631 \u0639\u0644\u0649 \u0627\u0644\u062c\u0632\u0621 {{path}}", "question.can not submit": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0625\u0631\u0633\u0627\u0644 \u0625\u062c\u0627\u0628\u0629 - \u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u0644\u0623\u062e\u0637\u0627\u0621.", "question.answer submitted": "\u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0646\u062a\u0627\u0626\u062c", "question.score feedback.show": "\u0627\u0639\u0631\u0636 \u0627\u0644\u062a\u063a\u0630\u064a\u0629 \u0627\u0644\u0631\u0627\u062c\u0639\u0629", "question.score feedback.hide": "\u0623\u062e\u0641 \u0627\u0644\u062a\u063a\u0630\u064a\u0629 \u0627\u0644\u0631\u0627\u062c\u0639\u0629", "question.score feedback.ascore total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "\u0627\u0644\u0646\u062a\u064a\u062c\u0629: {{ScoreString}}", "question.score feedback.answered": "\u062a\u0645\u062a \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0639\u0644\u064a\u0647", "question.score feedback.unanswered": "\u0644\u0645 \u064a\u062a\u0645 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0639\u0644\u064a\u0647", "question.score feedback.correct": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0635\u062d\u064a\u062d\u0629", "question.score feedback.partial": "\u0625\u062c\u0627\u0628\u0629 \u0635\u062d\u064a\u062d\u0629 \u062c\u0632\u0626\u064a\u0627", "question.score feedback.wrong": "\u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629", "question.selector.unsubmitted changes": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u0631\u0633\u0627\u0644 \u0627\u0644\u0646\u062a\u0627\u0626\u062c", "timing.no accumulator": "\n\u0644\u0627 \u064a\u0648\u062c\u062f \u062a\u0631\u0627\u0643\u0645 \u062a\u0648\u0642\u064a\u062a {{name}}", "timing.time remaining": "\u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0645\u062a\u0628\u0642\u064a", "xml.could not load": "\u062a\u0639\u0630\u0631 \u062a\u062d\u0645\u064a\u0644 \u0645\u0633\u062a\u0646\u062f XML: {{-message}}", "xml.property not number": "\u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u062e\u0627\u0635\u064a\u0629 {{name}} \u0631\u0642\u0645\u064b\u0627 \u060c \u0648\u0644\u0643\u0646\u0647\u0627 \u0644\u064a\u0633\u062a ({{value}}) \u060c \u0641\u064a \u0627\u0644\u0639\u0642\u062f\u0629 {{element}}", "xml.property not boolean": "\u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u062e\u0627\u0635\u064a\u0629 {{name}} \u0645\u0646\u0637\u0642\u064a\u0629 \u060c \u0648\u0644\u0643\u0646\u0647\u0627 \u0644\u064a\u0633\u062a ({{value}}) \u060c \u0641\u064a \u0627\u0644\u0639\u0642\u062f\u0629 {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "\u062e\u0637\u0623 \u0641\u064a \u062a\u0647\u064a\u0626\u0629 \u0628\u0631\u0648\u062a\u0648\u0643\u0648\u0644 SCORM: {{-message}}", "scorm.failed save": "<p> \u0641\u0634\u0644 \u0637\u0644\u0628 \u062d\u0641\u0638 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0639\u0644\u0649 \u0627\u0644\u062e\u0627\u062f\u0645. \u0627\u0636\u063a\u0637 \u0639\u0644\u0649 <b> OK </b> \u0644\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649. </ p>\n<p> \u0625\u0630\u0627 \u062a\u0644\u0642\u064a\u062a \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0628\u0634\u0643\u0644 \u0645\u062a\u0643\u0631\u0631 \u060c \u0641\u062a\u062d\u0642\u0642 \u0645\u0646 \u0627\u062a\u0635\u0627\u0644 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0623\u0648 \u0627\u0633\u062a\u062e\u062f\u0645 \u062c\u0647\u0627\u0632 \u0643\u0645\u0628\u064a\u0648\u062a\u0631 \u0645\u062e\u062a\u0644\u0641\u064b\u0627. \u062a\u0645 \u062d\u0641\u0638 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0627\u0644\u0645\u0631\u0633\u0644\u0629 \u0645\u0633\u0628\u0642\u064b\u0627 \u0628\u0646\u062c\u0627\u062d \u0648\u0633\u062a\u062a\u0645 \u0627\u0633\u062a\u0639\u0627\u062f\u062a\u0647\u0627 \u0625\u0630\u0627 \u0627\u0633\u062a\u0623\u0646\u0641\u062a \u0647\u0630\u0647 \u0627\u0644\u062c\u0644\u0633\u0629 \u0639\u0644\u0649 \u062c\u0647\u0627\u0632 \u0643\u0645\u0628\u064a\u0648\u062a\u0631 \u0645\u062e\u062a\u0644\u0641. </ p>\n<p> \u0625\u0630\u0627 \u0638\u0647\u0631\u062a \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629 \u0628\u0627\u0633\u062a\u0645\u0631\u0627\u0631 \u0648\u0644\u0627 \u064a\u0645\u0643\u0646\u0643 \u062d\u0641\u0638 <em> \u0623\u064a \u0625\u062c\u0627\u0628\u0627\u062a </ em> \u060c \u0641\u064a\u0631\u062c\u0649 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0627\u0644\u0645\u062d\u0627\u0636\u0631 \u0623\u0648 \u200b\u200b\u0627\u0644\u0645\u0639\u0644\u0645. </ p>", "scorm.no exam suspend data": "\u0641\u0634\u0644\u062a \u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0625\u062e\u062a\u0628\u0627\u0631: \u0644\u0627\u064a\u0648\u062c\u062f \u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0639\u0644\u0642\u0629 \u0644\u0644\u0625\u062e\u062a\u0628\u0627\u0631", "scorm.error loading suspend data": "\u062e\u0637\u0623 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u062a\u0639\u0644\u064a\u0642: {{-message}}", "scorm.error loading question": "\n\u062e\u0637\u0623 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0633\u0624\u0627\u0644 {{number}}: {{-message}}", "scorm.no question suspend data": "\u0644\u0627\u064a\u0648\u062c\u062f \u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0639\u0644\u0642\u0629 \u0644\u0644\u0633\u0624\u0627\u0644", "scorm.error loading part": "\u062e\u0637\u0623 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u062c\u0632\u0621 {{part}}: {{-message}}", "scorm.no part suspend data": "\u0644\u0627\u064a\u0648\u062c\u062f \u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0639\u0644\u0642\u0629 \u0644\u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621", "util.product.non list": "\u0645\u0627 \u062a\u0645 \u062a\u0645\u0631\u064a\u0631\u0647 \u0644\u064a\u0633 \u0642\u0627\u0626\u0645\u0629 \u0625\u0644\u0649 <code> Numbas.util.product </code>", "mark": "\u062f\u0631\u062c\u0629", "was": "\u0643\u0627\u0646", "part": "\u062c\u0632\u0621", "gap": "\u0641\u0631\u0627\u063a", "step": "\u062e\u0637\u0648\u0629", "jme.substituteTree.undefined variable": "\u0645\u062a\u063a\u064a\u0631 \u063a\u064a\u0631 \u0645\u062d\u062f\u062f: <code> {{name}} </code>", "jme.user javascript.returned undefined": "\u062a\u0645 \u0625\u0631\u062c\u0627\u0639 \u0648\u0638\u064a\u0641\u0629 \u062c\u0627\u0641\u0627 \u0633\u0643\u0631\u064a\u0628\u062a \u0627\u0644\u0645\u0639\u0631\u0641\u0629 \u0628\u0648\u0627\u0633\u0637\u0629 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 <code> {{name}} </code> <code> undefined </code>.", "part.marking.steps change": "\u062a\u0645 \u0645\u0646\u062d\u0643 <strong> {{count\u060c niceNumber}} </strong> $ t (\u0639\u0644\u0627\u0645\u0629) \u0645\u0642\u0627\u0628\u0644 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0639\u0644\u0649 \u0627\u0644\u062e\u0637\u0648\u0627\u062a.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "\u0644\u0642\u062f \u0623\u062d\u0631\u0632\u062a <strong> {{count\u060c niceNumber}} </strong> $ t (\u0639\u0644\u0627\u0645\u0629) \u0641\u064a \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621.", "part.numberentry.precision type.dp": "\u062e\u0627\u0646\u0629 \u0639\u0634\u0631\u064a\u0629", "part.numberentry.precision type.dp_plural": "\u062e\u0627\u0646\u0627\u062a \u0639\u0634\u0631\u064a\u0629", "part.numberentry.precision type.sigfig": "\u0631\u0642\u0645 \u0630\u0648 \u0627\u0647\u0645\u064a\u0629", "part.numberentry.precision type.sigfig_plural": "\u0623\u0631\u0642\u0627\u0645 \u0645\u0647\u0645\u0629", "part.numberentry.give your answer to precision": "\u062a\u0642\u0631\u064a\u0628 \u0625\u062c\u0627\u0628\u062a\u0643 \u0625\u0644\u0649 {{count\u060c niceNumber}} {{\u0627\u0644\u062f\u0642\u0629Type}}.", "question.unsubmitted changes": "\u0644\u0642\u062f \u0642\u0645\u062a \u0628\u062a\u063a\u064a\u064a\u0631 \u0625\u062c\u0627\u0628\u062a\u0643 \u0648\u0644\u0643\u0646 \u0644\u0645 \u062a\u0631\u0633\u0644\u0647\u0627. \u064a\u0631\u062c\u0649 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0625\u062c\u0627\u0628\u062a\u0643 \u062b\u0645 \u0627\u0644\u0636\u063a\u0637 \u0639\u0644\u0649 \u0632\u0631 <strong> \u0625\u0631\u0633\u0627\u0644 \u0625\u062c\u0627\u0628\u0629 </ strong>.", "question.unsubmitted changes_plural": "\u0644\u0642\u062f \u0623\u062c\u0631\u064a\u062a \u062a\u063a\u064a\u064a\u0631\u0627\u062a \u0639\u0644\u0649 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0648\u0644\u0643\u0646 \u0644\u0645 \u062a\u0631\u0633\u0644\u0647\u0627. \u064a\u0631\u062c\u0649 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0644\u0643\u0644 \u062c\u0632\u0621 \u062b\u0645 \u0627\u0644\u0636\u063a\u0637 \u0639\u0644\u0649 \u0627\u0644\u0632\u0631 <strong> \u0625\u0631\u0633\u0627\u0644 \u062c\u0645\u064a\u0639 \u0627\u0644\u0623\u062c\u0632\u0627\u0621 </ strong>.", "util.equality not defined for type": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u062f\u064a\u062f \u0627\u0644\u0645\u0633\u0627\u0648\u0627\u0629 \u0644\u0644\u0646\u0648\u0639 {{type}}", "mark_plural": "\u062f\u0631\u062c\u0627\u062a", "was_plural": "\u0643\u0627\u0646\u062a", "die.script not loaded": "\u062a\u0639\u0630\u0631 \u0628\u062f\u0621 Numbas \u0644\u0623\u0646\u0647 \u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641 <code> {{file}} </code>. \u062a\u062d\u0642\u0642 \u0645\u0646 \u0623\u0646\u0647 \u0645\u062f\u0631\u062c \u0641\u064a <code> scripts.js </code>.", "math.combinations.n less than zero": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u062a\u0648\u0627\u0641\u064a\u0642 \u0644\u0623\u0646 \u0642\u064a\u0645\u0629 \u0646 \u0623\u0635\u063a\u0631 \u0645\u0646 \u0635\u0641\u0631", "math.combinations.k less than zero": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u062a\u0648\u0627\u0641\u064a\u0642 \u0644\u0623\u0646 \u0642\u064a\u0645\u0629 \u0643 \u0623\u0635\u063a\u0631 \u0645\u0646 \u0635\u0641\u0631", "math.combinations.n less than k": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u062a\u0648\u0627\u0641\u064a\u0642 \u0644\u0623\u0646 \u0642\u064a\u0645\u0629 \u0646 \u0623\u0635\u063a\u0631 \u0645\u0646 \u0643", "math.permutations.n less than zero": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u062a\u0628\u0627\u062f\u064a\u0644 \u0644\u0623\u0646 \u0642\u064a\u0645\u0629 \u0646 \u0623\u0635\u063a\u0631 \u0645\u0646 \u0635\u0641\u0631", "math.permutations.k less than zero": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u062a\u0628\u0627\u062f\u064a\u0644 \u0644\u0623\u0646 \u0642\u064a\u0645\u0629 \u0643 \u0623\u0635\u063a\u0631 \u0645\u0646 \u0635\u0641\u0631", "math.permutations.n less than k": "\u0644\u0627\u064a\u0645\u0643\u0646 \u062d\u0633\u0627\u0628 \u0627\u0644\u062a\u0628\u0627\u062f\u064a\u0644 \u0644\u0623\u0646 \u0642\u064a\u0645\u0629 \u0646 \u0623\u0635\u063a\u0631 \u0645\u0646 \u0643", "part.numberentry.give your answer to precision_0": "\u0642\u0631\u0628 \u0625\u062c\u0627\u0628\u062a\u0643 \u0625\u0644\u0649 \u0623\u0642\u0631\u0628 \u0639\u062f\u062f \u0635\u062d\u064a\u062d", "mathjax.error": "\u062e\u0637\u0623 \u0641\u064a \u0645\u0639\u0627\u0644\u062c\u0629 MathJax: {{-message}}", "mathjax.error with context": "\u062e\u0637\u0623 \u0641\u064a \u0645\u0639\u0627\u0644\u062c\u0629 MathJax \u0641\u064a {{-context}}: {{-message}}", "exam.introduction": "\u0645\u0642\u062f\u0645\u0629 \u0627\u0644\u0625\u062e\u062a\u0628\u0627\u0631", "exam.feedback": "\u0631\u0633\u0627\u0644\u0629 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0639\u0646 \u0627\u0644\u0627\u0645\u062a\u062d\u0627\u0646", "jme.tokenise.keypair key not a string": "\u064a\u062c\u0628 \u0623\u0646 \u064a\u0643\u0648\u0646 \u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0642\u0627\u0645\u0648\u0633 \u0633\u0644\u0633\u0644\u0629 \u0648\u0644\u064a\u0633 {{type}}.", "jme.shunt.list mixed argument types": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0644\u064a\u0644 {{mode}}: \u0645\u0632\u064a\u062c \u0645\u0646 \u0627\u0644\u0642\u0627\u0645\u0648\u0633 \u0648\u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0642\u0627\u0626\u0645\u0629", "jme.func.listval.key not in dict": "\u0644\u0627 \u064a\u062d\u062a\u0648\u064a \u0627\u0644\u0642\u0627\u0645\u0648\u0633 \u0639\u0644\u0649 \u0627\u0644\u0645\u0641\u062a\u0627\u062d <code> {{key}} </code>", "part.prompt": "\u0623\u0645\u0631", "part.feedback": "\u062a\u063a\u0630\u064a\u0629 \u0631\u0627\u062c\u0639\u0629", "part.numberentry.answer not reduced": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u064a\u0633\u062a \u0641\u064a \u0627\u0644\u0635\u064a\u063a\u0629 \u0627\u0644\u0645\u062e\u062a\u0635\u0631\u0629", "part.numberentry.give your answer as a reduced fraction": "\u0623\u0643\u062a\u0628 \u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0641\u064a \u0623\u0628\u0633\u0637 \u0635\u0648\u0631\u0629 \u0645\u0645\u0643\u0646\u0629", "part.numberentry.negative decimal places": "\u062a\u0645 \u0625\u0639\u062f\u0627\u062f \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0644\u064a\u0642\u0648\u0645 \u0628\u062a\u0642\u0631\u064a\u0628 \u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0637\u0627\u0644\u0628 \u0625\u0644\u0649 \u0639\u062f\u062f \u0633\u0627\u0644\u0628 \u0645\u0646 \u0627\u0644\u0645\u0646\u0627\u0632\u0644 \u0627\u0644\u0639\u0634\u0631\u064a\u0629\u060c \u0648\u0627\u0644\u062a\u064a \u0644\u0627 \u0645\u0639\u0646\u0649 \u0644\u0647\u0627.", "part.mcq.choices": "\u0625\u062e\u062a\u064a\u0627\u0631\u0627\u062a", "part.mcq.answers": "\u0625\u062c\u0627\u0628\u0627\u062a", "part.mcq.matrix cell empty": "\u0627\u0644\u062c\u0632\u0621 {{part}} \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0639\u0644\u0649 \u062e\u0644\u064a\u0629 \u0627\u0644\u0645\u0635\u0641\u0648\u0641\u0629 ({{row}} \u060c {{\u0639\u0645\u0648\u062f}}) \u0641\u0627\u0631\u063a", "part.mcq.matrix jme error": "\u064a\u0642\u062f\u0645 \u0627\u0644\u062c\u0632\u0621 {{part}} \u062e\u0644\u064a\u0629 \u0645\u0635\u0641\u0648\u0641\u0629 \u0648\u0633\u0645 ({{row}} \u060c {{\u0639\u0645\u0648\u062f}}) \u062e\u0637\u0623 JME: {{-error}}", "question.statement": "\u062c\u0645\u0644\u0629", "ruleset.circular reference": "\u0645\u0631\u062c\u0639 \u062f\u0627\u0626\u0631\u064a \u0641\u064a \u062a\u0639\u0631\u064a\u0641 \u0645\u062c\u0645\u0648\u0639\u0629 \u0627\u0644\u0642\u0648\u0627\u0639\u062f <code> {{name}} </code>", "ruleset.set not defined": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u062f\u064a\u062f \u0645\u062c\u0645\u0648\u0639\u0629 \u0627\u0644\u0642\u0648\u0627\u0639\u062f {{name}}", "jme.evaluate.no scope given": "\u064a\u062c\u0628 \u0625\u0639\u0637\u0627\u0621 Numbas.jme.evaluate \u0646\u0637\u0627\u0642\u064b\u0627\n", "question.score feedback.answered total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Answered.", "question.score feedback.answered actual": "Score: {{scoreString}}", "question.score feedback.unanswered total": "{{marksString}}.", "answer.number.not a number": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u064a\u0633\u062a \u0631\u0642\u0645\u064b\u0627 \u0635\u0627\u0644\u062d\u064b\u0627.", "answer.number.fractions not allowed": "\u0644\u0627 \u064a\u0645\u0643\u0646\u0643 \u0625\u062f\u062e\u0627\u0644 \u0643\u0633\u0631.", "answer.jme.invalid expression": "{{-message}}\n", "answer.matrix.fractions not allowed": "\u0644\u0627 \u064a\u0645\u0643\u0646\u0643 \u0625\u062f\u062e\u0627\u0644 \u0643\u0633\u0648\u0631.", "answer.matrix.some cell not a number": "\u0648\u0627\u062d\u062f\u0629 \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0627\u0644\u062e\u0644\u0627\u064a\u0627 \u0641\u064a \u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u0627 \u062a\u062d\u0645\u0644 \u0631\u0642\u0645\u064b\u0627 \u0635\u0627\u0644\u062d\u064b\u0627.", "exam.enter password": "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631:", "exam.password.correct": "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631 \u0635\u062d\u064a\u062d\u0629\u060c \u064a\u0645\u0643\u0646\u0643 \u0628\u062f\u0621 \u0627\u0644\u0625\u062e\u062a\u0628\u0627\u0631.", "exam.password.incorrect": "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631 \u0647\u0630\u0647 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629.", "frontpage.scorm.lms not connected": "\u0647\u0630\u0627 \u0627\u0644\u0627\u062e\u062a\u0628\u0627\u0631 \u064a\u0639\u0645\u0644 \u0627\u0644\u0622\u0646 \u0641\u064a \"\u0627\u0644\u0648\u0636\u0639\u064a\u0629 \u0627\u0644\u0645\u0633\u062a\u0642\u0644\u0629\". \u0644\u0646 \u064a\u062a\u0645 \u062d\u0641\u0638 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0648\u0639\u0644\u0627\u0645\u0627\u062a\u0643!", "result.question review": "\u0645\u0631\u0627\u062c\u0639\u0629", "control.confirm regen no marks": "\u0647\u0644 \u062a\u0631\u063a\u0628 \u0628\u0625\u0639\u0627\u062f\u0629 \u062e\u0644\u0637 \u0627\u0644\u0633\u0624\u0627\u0644 \u0639\u0634\u0648\u0627\u0626\u064a\u0627\u061f", "control.confirm reveal no marks": "\u0647\u0644 \u062a\u0631\u063a\u0628 \u0641\u064a \u0643\u0634\u0641 \u0625\u062c\u0627\u0628\u0629 \u0647\u0630\u0627 \u0627\u0644\u0633\u0624\u0627\u0644\u061f", "jme.tokenise.invalid near": "\u062a\u0639\u0628\u064a\u0631 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d: <code> {{\u062a\u0639\u0628\u064a\u0631}} </ code> \u0641\u064a \u0627\u0644\u0645\u0648\u0636\u0639 {{position}} \u0628\u0627\u0644\u0642\u0631\u0628 \u0645\u0646 <code> {{nearby}} </code>", "jme.tokenise.number.object not complex": "\u062a\u0645 \u062a\u0645\u0631\u064a\u0631 \u063a\u0631\u0636 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d \u0625\u0644\u0649 \u0645\u064f\u0646\u0634\u0626 \u0627\u0644\u0631\u0642\u0645.", "jme.subvars.null substitution": "\u0627\u0633\u062a\u0628\u062f\u0627\u0644 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 \u0627\u0644\u0641\u0627\u0631\u063a: <code> $ t (\u0642\u0648\u0633 \u0623\u064a\u0633\u0631) {{str}} $ t (\u0642\u0648\u0633 \u0623\u064a\u0645\u0646)", "jme.type.type already registered": "\u062a\u0645 \u062a\u0633\u062c\u064a\u0644 \u0646\u0648\u0639 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a {{type}} \u0628\u0627\u0644\u0641\u0639\u0644 \u060c \u0644\u0630\u0627 \u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0633\u062c\u064a\u0644 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.\n", "jme.type.no cast method": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u062d\u0648\u064a\u0644 \u062a\u0644\u0642\u0627\u0626\u064a\u064b\u0627 \u0645\u0646 {{from}} \u0625\u0644\u0649 {{to}}.", "jme.display.simplifyTree.empty expression": "\u0627\u0644\u062a\u0639\u0628\u064a\u0631 \u0641\u0627\u0631\u063a", "jme.display.simplifyTree.stuck in a loop": "\u0627\u0644\u0645\u0639\u0644\u0642 \u0639\u0627\u0644\u0642 \u0641\u064a \u062d\u0644\u0642\u0629: <code> {{expr}} </code>\n", "math.niceNumber.undefined": "\u0643\u0627\u0646 \u064a\u062a\u0648\u0642\u0639 \u0631\u0642\u0645\u064b\u0627 \u060c \u0648\u0644\u0643\u0646 \u062a\u0645 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 <code> \u063a\u064a\u0631 \u0645\u0639\u0631\u0641 </ code>", "math.rangeToList.zero step size": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0648\u064a\u0644 \u0646\u0637\u0627\u0642 \u062d\u062c\u0645 \u062e\u0637\u0648\u062a\u0647 \u0635\u0641\u0631\u060c \u0625\u0644\u0649 \u0642\u0627\u0626\u0645\u0629.", "part.error": "{{path}}: {{-message}}\n", "part.marking.revealed steps": "\u0642\u0645\u062a \u0628\u0627\u0644\u0643\u0634\u0641 \u0639\u0646 \u0627\u0644\u062e\u0637\u0648\u0627\u062a", "part.marking.maximum scaled down": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u0642\u0635\u0649 \u0627\u0644\u0630\u064a \u064a\u0645\u0643\u0646\u0643 \u062a\u0633\u062c\u064a\u0644\u0647 \u0644\u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0647\u0648 <strong> {{count\u060c niceNumber}} </strong> $ t (\u0639\u0644\u0627\u0645\u0629). \u0633\u064a\u062a\u0645 \u062a\u062e\u0641\u064a\u0636 \u062f\u0631\u062c\u0627\u062a\u0643 \u0648\u0641\u0642\u064b\u0627 \u0644\u0630\u0644\u0643.", "part.marking.minimum score applied": "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 \u0645\u0646 \u0627\u0644\u0646\u0642\u0627\u0637 \u0641\u064a \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0647\u0648 <strong> {{Score\u060c niceNumber}} </strong>.", "part.marking.maximum score applied": "\u0627\u0644\u062f\u0631\u062c\u0629 \u0627\u0644\u0642\u0635\u0648\u0649 \u0644\u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0647\u064a <strong> {{Score\u060c niceNumber}} </strong>.\n", "part.marking.error in marking script": "\u062d\u062f\u062b \u062e\u0637\u0623 \u0641\u064a \u062e\u0648\u0627\u0631\u0632\u0645\u064a\u0629 \u0648\u0636\u0639 \u0627\u0644\u0639\u0644\u0627\u0645\u0627\u062a \u0644\u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621. \u064a\u0631\u062c\u0649 \u0627\u0644\u0625\u0628\u0644\u0627\u063a \u0639\u0646 \u0647\u0630\u0627. {{-\u0631\u0633\u0627\u0644\u0629}}\n", "part.marking.no result after replacement": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u0645\u064a\u064a\u0632 \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0625\u062c\u0627\u0628\u0627\u062a\u0643 \u0639\u0644\u0649 \u0627\u0644\u0623\u062c\u0632\u0627\u0621 \u0627\u0644\u0633\u0627\u0628\u0642\u0629.\n", "part.marking.missing required note": "\u0644\u0627 \u062a\u062d\u062f\u062f \u062e\u0648\u0627\u0631\u0632\u0645\u064a\u0629 \u0648\u0636\u0639 \u0627\u0644\u0639\u0644\u0627\u0645\u0627\u062a \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 <code> {{note}} </code>", "marking.apply.not a list": "\u064a\u062c\u0628 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0627\u0644\u0648\u0633\u064a\u0637\u0629 \u0627\u0644\u0623\u0648\u0644\u0649 \u0644\u0640 <code> \u062a\u0637\u0628\u064a\u0642 </code> \u0642\u0627\u0626\u0645\u0629 \u0648\u0644\u064a\u0633\u062a \u0643\u0630\u0644\u0643", "marking.apply marking script.script not found": "\u0627\u0644\u0646\u0635 \u0627\u0644\u0628\u0631\u0645\u062c\u064a \u0644\u0644\u0639\u0644\u0627\u0645\u0629 <code> {{name}} </code> \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f", "marking.note.compilation error": "\u062e\u0637\u0623 \u0641\u064a \u062a\u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 <code> {{name}} </code>: {{-message}}", "marking.note.error evaluating note": "\u062e\u0637\u0623 \u0641\u064a \u062a\u0642\u064a\u064a\u0645 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 <code> {{name}} </code> - {{-message}}", "marking.note.invalid definition": "\u062a\u0639\u0631\u064a\u0641 \u0645\u0644\u0627\u062d\u0638\u0629 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d: <code> {{source}} </code>. {{-\u0645\u0644\u062d\u0648\u0638\u0629}}", "marking.note.invalid definition.missing colon": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0646\u0642\u0637\u062a\u064a\u0646 \u0628\u0639\u062f \u0627\u0644\u0625\u0633\u0645 \u0648\u0627\u0644\u0635\u0641\u0629", "marking.note.invalid definition.description missing closing bracket": "\u0642\u062f \u064a\u0643\u0648\u0646 \u0627\u062d\u062f \u0627\u0644\u0623\u0642\u0648\u0627\u0633 \u063a\u064a\u0631 \u0645\u063a\u0644\u0642.", "marking.note.empty expression": "\n\u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0629 <code> {{name}} </code> \u0641\u0627\u0631\u063a\u0629.", "marking.script.error parsing notes": "\u062e\u0637\u0623 \u0641\u064a \u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0646\u0635 \u0627\u0644\u0628\u0631\u0645\u062c\u064a \u0644\u0644\u062a\u0645\u064a\u064a\u0632: {{- message}}", "part.feedback out of date": "\u0647\u0630\u0627 \u0627\u0644\u0645\u0644\u0627\u062d\u0638\u0627\u062a \u062a\u0633\u062a\u0646\u062f \u0625\u0644\u0649 \u0625\u062c\u0627\u0628\u062a\u0643 \u0627\u0644\u0623\u062e\u064a\u0631\u0629 \u0627\u0644\u062a\u064a \u062a\u0645 \u0625\u0631\u0633\u0627\u0644\u0647\u0627. \u0623\u0631\u0633\u0644 \u0625\u062c\u0627\u0628\u062a\u0643 \u0627\u0644\u062a\u064a \u062a\u0645 \u062a\u063a\u064a\u064a\u0631\u0647\u0627 \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0645\u0644\u0627\u062d\u0638\u0627\u062a \u0645\u062d\u062f\u062b\u0629.", "part.jme.invalid value generator expression": "\u062a\u0639\u0628\u064a\u0631 \u0645\u0646\u0634\u0626 \u0642\u064a\u0645\u0629 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d \u0644\u0644\u0645\u062a\u063a\u064a\u0631 <code> {{name}} </code>: {{-message}}", "part.mcq.incorrect choice": "\u0642\u0645\u062a \u0628\u0627\u062e\u062a\u064a\u0627\u0631 \u0625\u062c\u0627\u0628\u0629 \u062e\u0627\u0637\u0626\u0629", "part.matrix.not all cells same precision": "\u062f\u0642\u0629 \u0627\u0644\u0623\u0639\u062f\u0627\u062f \u0627\u0644\u0645\u0642\u062f\u0645\u0629 (\u0639\u062f\u062f \u0627\u0644\u0623\u0631\u0642\u0627\u0645 \u0628\u0639\u062f \u0627\u0644\u0641\u0627\u0635\u0644\u0629) \u062a\u062e\u062a\u0644\u0641 \u0628\u064a\u0646 \u0627\u0644\u062e\u0644\u0627\u064a\u0627.", "part.gapfill.error marking gap": "\u062e\u0637\u0623 \u0641\u064a \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629 \u0639\u0644\u0649 {{name}}: {{-message}}", "part.custom.empty setting": "\u0644\u0645 \u064a\u062a\u0645 \u0625\u0639\u0637\u0627\u0621 \u0642\u064a\u0645\u0629.", "part.custom.unrecognised input type": "\u0646\u0648\u0639 \u0625\u0639\u062f\u0627\u062f \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641 <code> {{input_type}} </code>", "part.custom.error evaluating input option": "\u062e\u0637\u0623 \u0641\u064a \u062a\u0642\u064a\u064a\u0645 \u062e\u064a\u0627\u0631 \u0627\u0644\u0625\u062f\u062e\u0627\u0644 <code> {{option}} </code>: {{-error}}", "part.custom.input option missing": "\u062a\u0639\u0631\u064a\u0641 \u062e\u064a\u0627\u0631 \u0627\u0644\u0625\u062f\u062e\u0627\u0644 <code> {{option}} </code> \u0645\u0641\u0642\u0648\u062f.", "part.custom.error evaluating setting": "\u062e\u0637\u0623 \u0641\u064a \u062a\u0642\u064a\u064a\u0645 \u0627\u0644\u0625\u0639\u062f\u0627\u062f <code> {{setting}} </code>: {{-error}}", "question.error creating question": "\u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0625\u0646\u0634\u0627\u0621 \u0627\u0644\u0633\u0624\u0627\u0644 {{number}}: {{-message}}", "question.score feedback.not marked": "\u0644\u0645 \u064a\u062a\u0645 \u0648\u0636\u0639 \u0639\u0644\u0627\u0645\u0629", "question.score feedback.partially answered": "\u0625\u062c\u0627\u0628\u0629 \u062c\u0632\u0626\u064a\u0629", "question.score feedback.score total actual": "\u0627\u0644\u0646\u062a\u064a\u062c\u0629: {{Score\u060c niceNumber}} / {{\u0639\u0644\u0627\u0645\u0627\u062a \u060c niceNumber}}", "variable.error in variable definition": "\u062e\u0637\u0623 \u0641\u064a \u062a\u0639\u0631\u064a\u0641 \u0627\u0644\u0645\u062a\u063a\u064a\u0631 <code> {{name}} </code>", "left brace": "}", "right brace": "{", "extension.not found": "\u062a\u0639\u0630\u0631 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u062d\u0642 <code> {{name}} </code>.", "control.toggle navigation menu": "\u0642\u0645 \u0628\u0627\u0644\u062a\u0628\u062f\u064a\u0644 \u0641\u064a \u0642\u0627\u0626\u0645\u0629 \u0627\u0644\u0625\u0646\u062a\u0642\u0627\u0644", "part.input title": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0644\u0644\u062c\u0632\u0621 {{name}}", "part.correct answer title": "\u0627\u0644\u0625\u062c\u0627\u0628\u0629 \u0627\u0644\u0645\u062a\u0648\u0642\u0639\u0629 \u0644\u0644\u062c\u0632\u0621 {{name}}", "part.jme.must-match.failed": "\u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u064a\u0633\u062a \u0628\u0627\u0644\u0635\u064a\u063a\u0629 \u0627\u0644\u0635\u062d\u064a\u062d\u0629.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p> \u064a\u0639\u062a\u0645\u062f \u062c\u0632\u0621 \u0648\u0627\u062d\u062f \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0627\u0644\u0623\u062c\u0632\u0627\u0621 \u0627\u0644\u0644\u0627\u062d\u0642\u0629 \u0639\u0644\u0649 \u0625\u062c\u0627\u0628\u062a\u0643 \u0644\u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621. \u0633\u064a\u0624\u062f\u064a \u062a\u0642\u062f\u064a\u0645 \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649 \u0625\u0644\u0649 \u0625\u0628\u0637\u0627\u0644 \u062a\u0644\u0643 \u0627\u0644\u0623\u062c\u0632\u0627\u0621 \u0648\u0625\u0632\u0627\u0644\u062a\u0647\u0627 \u0645\u0646 \u0627\u0644\u0633\u0624\u0627\u0644. \u0647\u0630\u0627 \u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u0631\u0627\u062c\u0639 \u0639\u0646\u0647 </ p>\n<p> \u0647\u0644 \u062a\u0631\u063a\u0628 \u0641\u064a \u0625\u0631\u0633\u0627\u0644 \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649\u061f </ p>", "control.back to menu": "\u0627\u0631\u062c\u0639 \u0625\u0644\u0649 \u0627\u0644\u0642\u0627\u0626\u0645\u0629\n", "display.error making html": "\u062e\u0637\u0623 \u0641\u064a \u0639\u0645\u0644 HTML \u0641\u064a {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-\u0631\u0633\u0627\u0644\u0629}} \u0641\u064a <code> {{\u062a\u0639\u0628\u064a\u0631}} </ code>", "jme.variables.empty name": "\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u062f\u064a\u062f \u0627\u0633\u0645 \u0644\u0645\u062a\u063a\u064a\u0631 \u0627\u0644\u0633\u0624\u0627\u0644.\n", "jme.calculus.unknown derivative": "\u0644\u0627 \u062a\u0639\u0631\u0641 \u0643\u064a\u0641\u064a\u0629 \u0627\u0644\u062a\u0641\u0631\u064a\u0642 \u0628\u064a\u0646 <code> {{tree}} </code>", "math.order complex numbers": "\u0644\u0627 \u064a\u0645\u0643\u0646 \u0637\u0644\u0628 \u0623\u0631\u0642\u0627\u0645 \u0645\u0639\u0642\u062f\u0629", "menu.choose a question": "\u0627\u062e\u062a\u0631 \u0633\u0624\u0627\u0644\u0627.", "part.choose next part.answered": "\u0645\u0627\u0630\u0627 \u062a\u0631\u064a\u062f \u0627\u0646 \u062a\u0641\u0639\u0644 \u0644\u0627\u062d\u0642\u0627\u061f", "part.choose next part.unanswered": "\u0623\u0648 \u064a\u0645\u0643\u0646\u0643:", "part.choose next part.will be locked": "(\u0633\u064a\u062a\u0645 \u0642\u0641\u0644 \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621)", "part.reached dead end": "\u0644\u0627 \u064a\u0648\u062c\u062f \u0634\u064a\u0621 \u0622\u062e\u0631 \u064a\u0645\u0643\u0646 \u0627\u0644\u0642\u064a\u0627\u0645 \u0628\u0647 \u0645\u0646 \u0647\u0646\u0627.", "part.next part.penalty amount": "(\u062e\u0633\u0627\u0631\u0629 {{count}} $ t (\u0639\u0644\u0627\u0645\u0629))", "part.marking.counts towards objective": "\u064a\u062a\u0645 \u0627\u062d\u062a\u0633\u0627\u0628 \u0647\u0630\u0627 \u0627\u0644\u062c\u0632\u0621 \u0645\u0646 \u0627\u0644\u0647\u062f\u0641 <strong> \"{{target}}\" </strong>.", "part.numberentry.answer not integer or decimal or fraction": "\u0625\u062c\u0627\u0628\u062a\u0643 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d\u0629. \u064a\u062c\u0628 \u0625\u062f\u062e\u0627\u0644 \u0639\u062f\u062f \u0635\u062d\u064a\u062d \u0623\u0648 \u0639\u0634\u0631\u064a \u0623\u0648 \u0643\u0633\u0631.", "question": "\u0633\u0624\u0627\u0644", "question.progress": "\u062a\u0642\u062f\u0645 \u0627\u0644\u0633\u0624\u0627\u0644:\n", "question.score feedback.unattempted": "\u0644\u0645 \u062a\u062d\u0627\u0648\u0644", "question.score feedback.attempted": "\u062a\u0645\u062a \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629", "question.score feedback.score actual.plain": "\n{{ScoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "\u0627\u0644\u0623\u0647\u062f\u0627\u0641", "question.penalties": "\u0627\u0644\u0639\u0642\u0648\u0628\u0627\u062a", "question.back to previous part": "\u0639\u062f \u0625\u0644\u0649 \u0627\u0644\u062c\u0632\u0621 \u0627\u0644\u0633\u0627\u0628\u0642", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "pl-pl": {"translation": {"page.loading": "\u0141adowanie...", "page.saving": "<p>Zapisywanie.</p>\n<p>To mo\u017ce potrwa\u0107 par\u0119 sekund.</p>", "mathjax.math processing error": "\"{{-message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "Numbas has failed", "die.sorry": "Przepraszamy, Numbas napotka\u0142o b\u0142\u0105d, co znaczy, \u017ce nie mo\u017ce kontynuowa\u0107. Poni\u017cej jest opis b\u0142\u0119du.", "die.error": "B\u0142\u0105d", "modal.ok": "OK", "modal.cancel": "Anuluj", "exam.exam name": "Nazwa Egzaminu:", "exam.random seed": "Numer Sesji:", "exam.student name": "Imi\u0119 Ucznia:", "exam.number of questions": "Liczba Pyta\u0144:", "exam.marks available": "Dost\u0119pne Oceny:", "exam.pass percentage": "Pr\u00f3g procentowy:", "exam.time allowed": "Czas dozwolony:", "exam.passed": "Zdane", "exam.failed": "Niezdane", "exam.review header": "Review: ", "frontpage.start": "Rozpocznij", "suspend.paused header": "Wstrzymaj", "suspend.exam suspended": "Egzamin zosta\u0142 wstrzymany. Wci\u015bnij <em>Wzn\u00f3w</em> by kontynuowa\u0107 egzamin.", "suspend.you can resume": "B\u0119dziesz m\u00f3g\u0142/mog\u0142a wznowi\u0107 t\u0105 sesj\u0119, kiedy rozpoczniesz to zadanie nast\u0119pnym razem.", "suspend.resume": "Wzn\u00f3w", "result.exit": "Zako\u0144cz egzamin", "result.print": "Wydrukuj te podsumowanie wynik\u00f3w", "result.exam summary": "Podsumowanie Egzaminu", "result.performance summary": "Performance Summary", "result.exam start": "Rozpocznij Egzamin:", "result.exam stop": "Zako\u0144cz Egzamin:", "result.time spent": "Czas po\u015bwi\u0119cony:", "result.questions attempted": "Zadania podj\u0119te:", "result.score": "Liczba punkt\u00f3w:", "result.result": "Wynik:", "result.question number": "Numer pytania", "result.question score": "Liczba Punkt\u00f3w", "result.question review title": "Review this question", "result.click a question to review": "Kliknij na numer pytania, by zobaczy\u0107, jak Twoje odpowiedzi zosta\u0142y ocenione i, gdzie to mo\u017cliwe, modelowe odpowiedzi.", "end.exam has finished": "Egzamin zosta\u0142 zako\u0144czony. Mo\u017cesz zamkn\u0105\u0107 to okno.", "control.confirm leave": "Nie zako\u0144czy\u0142e\u015b/\u0142a\u015b jeszcze tego egzaminu.", "control.not all questions answered": "Nie odpowiedzia\u0142e\u015b/\u0142a\u015b na ka\u017cde pytanie w tym egzaminie.", "control.not all questions submitted": "Dokona\u0142e\u015b/\u0142a\u015b zmian w przynajmniej jednej ze swoich odpowiedzi, ale nie zatwierdzi\u0142e\u015b/\u0142a\u015b jej. Prosz\u0119 sprawd\u017a czy ka\u017cde zdanie zosta\u0142o zatwierdzone.", "control.confirm end": "Czy jeste\u015b pewny/na, \u017ce chcesz zako\u0144czy\u0107 egzamin? Po jego zako\u0144czeniu nie b\u0119dziesz m\u00f3g\u0142/mog\u0142a ju\u017c dokonywa\u0107 zmian na swoich odpowiedziach.", "control.confirm regen": "Czy chcia\u0142by\u015b/chcia\u0142aby\u015b wylosowa\u0107 nowe dane to tego zadania? Je\u015bli klikniesz OK, wszystkie Twoje odpowiedzi i punkty za to zadanie zostan\u0105 utracone.", "control.confirm reveal": "Czy chcesz zobaczy\u0107 odpowiedzi do tego zadania? Wszelkie punkty, kt\u00f3re uzyska\u0142e\u015b/\u0142a\u015b za to zadanie zostan\u0105 zablokowane i nie b\u0119dziesz ju\u017c p\u00f3\u017aniej m\u00f3g\u0142/mog\u0142a go wykona\u0107.", "control.proceed anyway": "Proceed anyway?", "control.regen": "Spr\u00f3buj podobne zadanie.", "control.submit answer": "Zatwierd\u017a odpowied\u017a", "control.submit all parts": "Zatwierd\u017a wszystkie cz\u0119\u015bci", "control.submit again": "Zatwierd\u017a ponownie", "control.submit": "Zatwierd\u017a", "control.previous": "Poprzednie", "control.next": "Nast\u0119pne", "control.advice": "Porada", "control.reveal": "Poka\u017c odpowiedzi", "control.total": "Suma", "control.pause": "Wstrzymaj", "control.end exam": "Zako\u0144cz Egzamin", "control.back to results": "Wr\u00f3\u0107 do wynik\u00f3w", "display.part.jme.error making maths": "Error making maths display", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "Ten egzamin nie zawiera \u017cadnych pyta\u0144! Sprawd\u017a plik .exam nie ma b\u0142\u0119d\u00f3w.", "feedback.you were awarded": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid": "Invalid expression: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Not enough arguments for operation <code>{{op}}</code>", "jme.shunt.no left bracket in function": "No matching left bracket in function application or tuple", "jme.shunt.no left square bracket": "Brak pasuj\u0105cego lewego nawiasu", "jme.shunt.no left bracket": "Brak pasuj\u0105cego lewego nawiasu", "jme.shunt.no right bracket": "Brak pasuj\u0105cego prawego nawiasu", "jme.shunt.no right square bracket": "Brak pasuj\u0105cego prawego kwadratowego nawiasu na ko\u0144cu tej listy.", "jme.shunt.missing operator": "Wyra\u017cenie nie mo\u017ce by\u0107 obliczone -- brakuje operatora.", "jme.typecheck.function maybe implicit multiplication": "Operacja {{name}} nie jest zdefiniowana. Czy mia\u0142e\u015b/\u0142a\u015b na my\u015bli <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Operacja {{op}} nie jest zdefiniowana. Czy mia\u0142e\u015b/\u0142a\u015b na my\u015bli <br/><code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operacja '{{op}}' nie jest zdefiniowana.", "jme.typecheck.no right type definition": "Nie zaleziono definicji '{{op}}' poprawnego typu.", "jme.typecheck.no right type unbound name": "Zmienna <code>{{name}}</code> nie jest zdefiniowana.", "jme.typecheck.map not on enumerable": "<code>map</code> operacja musi zosta\u0107 wykonana na li\u015bcie albo przedziale, a nie na {{type}}", "jme.evaluate.undefined variable": "Zmienna {{name}} nie jest zdefiniowana.", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Obiekt nie mo\u017ce mie\u0107 indeksu dolnego.", "jme.func.matrix.invalid row type": "Nie mo\u017cna skonstruowa\u0107 macierzy z rz\u0119d\u00f3w typu {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Brakuje parametra w {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Brak pasuj\u0105cego <code>}</code> w {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "B\u0142\u0105d tworzenia funkcji <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "B\u0142\u0105d sk\u0142adni w definicji funkcji", "jme.variables.variable not defined": "Zmienna <code>{{name}}</code> nie zosta\u0142a zdefiniowana.", "jme.variables.empty definition": "Definicja zmiennej <code>{{name}}</code> jest pusta.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Can't compute GCF of complex numbers", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.lt.order complex numbers": "Can't order complex numbers", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Can't compute the determinant of a matrix which isn't square.", "matrixmath.abs.too big": "Sorry, can't compute the determinant of a matrix bigger than 3x3 yet.", "matrixmath.mul.different sizes": "Nie mo\u017cna mno\u017cy\u0107 macierzy kt\u00f3re maj\u0105 inne rozmiary.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "Odpowied\u017a: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "Ta cz\u0119\u015b\u0107 by\u0142a oceniona u\u017cywaj\u0105c twoich odpowiedzi do poprzednich cz\u0119\u015bci.", "part.marking.variable replacement part not answered": "You must answer {{part}} first.", "part.marking.resubmit because of variable replacement": "Ocena tej cz\u0119\u015bci zale\u017cy na odpowiedzi do innych cz\u0119\u015bci, kt\u00f3re zosta\u0142y zmienione. Ponownie zatwierd\u017a t\u0105 cz\u0119\u015b\u0107 \u017ceby zaktualizowa\u0107 twoj\u0105 ocen\u0119.", "part.marking.not submitted": "No answer submitted.", "part.marking.did not answer": "You did not answer this question.", "part.marking.nothing entered": "You did not enter an answer.", "part.marking.incorrect": "Your answer is incorrect.", "part.marking.correct": "Your answer is correct.", "part.marking.uncaught error": "Error when marking: {{-message}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "Expected answer:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "Correct answer is missing", "part.jme.answer too long": "Your answer is too long.", "part.jme.answer too short": "Your answer is too short.", "part.jme.answer invalid": "Your answer is not a valid mathematical expression.<br/>{{-message}}.", "part.jme.marking.correct": "Your answer is numerically correct.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Your answer must contain: {{strings}}", "part.jme.must-have several": "Your answer must contain all of: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Your answer must not contain: {{strings}}", "part.jme.not-allowed several": "Your answer must not contain any of: {{strings}}", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "Your answer is within the allowed range, but decimal numbers are not allowed.", "part.numberentry.correct except fraction": "Your answer is within the allowed range, but fractions are not allowed.", "part.numberentry.answer invalid": "You did not enter a valid number.", "part.numberentry.answer not integer": "Your answer is invalid. You must enter a whole number, not a decimal.", "part.numberentry.answer not integer or decimal": "Your answer is invalid. You must enter an integer or a decimal.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "You selected the wrong number of choices.", "part.mcq.no choices selected": "No choices selected.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "You chose a correct answer.", "part.matrix.answer invalid": "Your answer is not valid.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.empty": "You have not entered an answer.", "part.matrix.empty cell": "One or more of the cells in your answer is empty.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Question {{number}}: {{-message}}", "question.preamble.error": "Error in preamble: {{-message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "Question {{number}}", "question.submit part": "Save answer", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.advice": "Advice", "question.no such part": "Can't find part {{path}}.", "question.can not submit": "Can not submit answer - check for errors.", "question.answer submitted": "Answer submitted", "question.score feedback.show": "Show feedback", "question.score feedback.hide": "Hide feedback", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.answered": "Answered", "question.score feedback.unanswered": "Unanswered", "question.score feedback.correct": "Your answer is correct", "question.score feedback.partial": "Your answer is partially correct", "question.score feedback.wrong": "Your answer is incorrect", "question.selector.unsubmitted changes": "Unsubmitted changes.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Time remaining:", "xml.could not load": "Couldn't load an XML document: {{-message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "Error initialising SCORM protocol: {{-message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{-message}}", "scorm.error loading question": "Error loading question {{number}}: {{-message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{-message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "mark", "was": "was", "part": "part", "gap": "gap", "step": "step", "jme.substituteTree.undefined variable": "Undefined variable: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "significant figure", "part.numberentry.precision type.sigfig_plural": "significant figures", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "You have made a change to your answer but not submitted it. Please check your answer and then press the <strong>Save answer</strong> button.", "question.unsubmitted changes_plural": "You have made changes to your answers but not submitted them. Please check your answers to each part and then press the <strong>Save all answers</strong> button.", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "marks", "was_plural": "were", "die.script not loaded": "Numbas couldn't start because the file <code>{{file}}</code> was not loaded. Check that it's included in <code>scripts.js</code>.", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "part.numberentry.give your answer to precision_0": "Round your answer to the nearest integer.", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "exam.introduction": "Exam introduction", "exam.feedback": "Exam feedback message", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "Feedback", "part.numberentry.answer not reduced": "Your answer is not reduced to lowest terms.", "part.numberentry.give your answer as a reduced fraction": "Reduce your answer to lowest terms.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "choices", "part.mcq.answers": "answers", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Statement", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "question.score feedback.answered total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Answered.", "question.score feedback.answered actual": "Score: {{scoreString}}", "question.score feedback.unanswered total": "{{marksString}}.", "answer.number.not a number": "Your answer is not a valid number.", "answer.number.fractions not allowed": "You may not enter a fraction.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "You may not enter fractions.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "exam.enter password": "Password:", "exam.password.correct": "This password is correct. You can start the exam.", "exam.password.incorrect": "This password is incorrect.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Review", "control.confirm regen no marks": "Would you like to re-randomise this question?", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Partially answered", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "it-it": {"translation": {"page.loading": "Sto caricando...", "page.saving": "<p>Salvataggio in corso.</p>\n<p>Potrebbe richiedere qualche secondo.</p>", "mathjax.math processing error": "\"{{-message}}\" mentre compila <code>{{expression}}</code>", "die.numbas failed": "Numbas ha fallito", "die.sorry": "Mi spiace. Nunbas ha incontrato un errore e non pu\u00f2 continuare. In basso troverai una descrizione.", "die.error": "Errore", "modal.ok": "OK", "modal.cancel": "Annulla", "exam.exam name": "Nome esame:", "exam.random seed": "ID sessione:", "exam.student name": "Nome studente:", "exam.number of questions": "Numero di domande:", "exam.marks available": "Punti disponibili:", "exam.pass percentage": "Percentuale superamento:", "exam.time allowed": "Tempo disponibile:", "exam.passed": "Superato", "exam.failed": "Non superato", "exam.review header": "Controllo: ", "frontpage.start": "Inizio", "suspend.paused header": "In pausa", "suspend.exam suspended": "Il test \u00e8 sospeso. Premi <em>Riprendi</em> per continuare", "suspend.you can resume": "Puoi riprendere la sessione la prossima volta che apri quest'attivit\u00e0.", "suspend.resume": "Riprendi", "result.exit": "Esci dall'esame", "result.print": "Stampa i risultati", "result.exam summary": "Argomenti del test", "result.performance summary": "Riepilogo delle prestazioni", "result.exam start": "Inizio test:", "result.exam stop": "Fine test:", "result.time spent": "Tempo impiegato:", "result.questions attempted": "Domande tentate:", "result.score": "Puteggio:", "result.result": "Risultato:", "result.question number": "Domanda numero", "result.question score": "Punteggio", "result.question review title": "Rivedi questa domanda", "result.click a question to review": "Clicca sul numero di una domanda per visualizzare che voti hanno ricevuto le tue risposte e, dove possibile, le soluzioni complete.", "end.exam has finished": "L'esame \u00e8 terminato. Adesso puoi chiudere questa finestra.", "control.confirm leave": "Non hai ancora terminato l'esame.", "control.not all questions answered": "Non hai completato tutte le domande di questo esame.", "control.not all questions submitted": "Hai fatto cambiamenti a una o pi\u00f9 risposte ma non le hai inviate. Per favore controlla che ogni risposta sia stata inviata.", "control.confirm end": "Sei sicuro di voler finire l'esame? Dopo che avrai finito l'esame, non sarai in grado di cambiare nessuna delle tue risposte.", "control.confirm regen": "Vorresti rigenerare questa domanda? Se clicchi OK, tutte le tue risposte e i punteggi per questa domanda saranno persi.", "control.confirm reveal": "Vuoi mostrare la risposta a questa domanda? I punti che hai ricevuto fino a ora saranno bloccati e non potrai rispondere a questa domanda pi\u00f9 tardi.", "control.proceed anyway": "Procedi in ogni caso?", "control.regen": "Prova un'altra domanda come questa.", "control.submit answer": "Invia la risposta", "control.submit all parts": "Invia tutte le parti", "control.submit again": "Reinvia", "control.submit": "Invia", "control.previous": "Precedente", "control.next": "Successivo", "control.advice": "Suggerimento", "control.reveal": "Mostra le risposte", "control.total": "Totale", "control.pause": "Pausa", "control.end exam": "Finisci l'esame", "control.back to results": "Torna ai risultati", "display.part.jme.error making maths": "Errore nel mostrare la matematica", "exam.xml.bad root": "L'elemento root dell'XML dell'esame dovrebbe essere 'exam'", "exam.changeQuestion.no questions": "Questo esame non contiene domande! Controlla gli errori nel file .exam.", "feedback.you were awarded": "Ti sono stati assegnati <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) tolto.", "jme.tokenise.invalid": "Invalid expression: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Non ci sono abbastanza argomenti per l'operazione <code>{{op}}</code>", "jme.shunt.no left bracket in function": "Manca la parentesi sinistra nell'applicare una funzione o in un vettore", "jme.shunt.no left square bracket": "Manca una parentesi quadra sinistra", "jme.shunt.no left bracket": "Manca una parentesi sinistra", "jme.shunt.no right bracket": "Manca una parentesi destra", "jme.shunt.no right square bracket": "Manca una parentesi quadra a destra per chiudere la lista", "jme.shunt.missing operator": "L'espressione non pu\u00f2 essere computata -- manca un operatore", "jme.typecheck.function maybe implicit multiplication": "La funzione <code>{{name}}</code> non \u00e8 definita. Volevi dire <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "La funzione <code>{{op}}</code> non \u00e8 definita. <code>{{op}}</code> \u00e8 una variabile, e intendevi <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "L'operazione '{{op}}' non \u00e8 definita.", "jme.typecheck.no right type definition": "Non ho trovato una definizione di '{{op}}' del tipo giusto.", "jme.typecheck.no right type unbound name": "La variabile <code>{{name}}</code> non \u00e8 definita.", "jme.typecheck.map not on enumerable": "L'operazione <code>map</code> deve funzionare con una lista o un intervallo, non {{type}}", "jme.evaluate.undefined variable": "La variabile {{name}} non \u00e8 definita", "jme.thtml.not html": "\u00c8 stato passato un valore non HTML nel costruttore THTML.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Indice delle liste {{index}} non valido in una lista di dimensione {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.matrix.invalid row type": "Non posso costruire una riga a partire da righe del tipo {{type}}", "jme.func.except.continuous range": "Non posso usare l'operatore 'eccetto' su intervalli continui.", "jme.matrix.reports bad size": "La matrice riporta la sua misura in modo incorretto - ci dev'essere un errore nella funzione di costruzione", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Parametro mancante in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Errore di sintassi nella definizione della funzione", "jme.variables.variable not defined": "La variabile <code>{{name}}</code> non \u00e8 definita.", "jme.variables.empty definition": "La definizione della variabile <code>{{name}}</code> \u00e8 vuota.", "jme.variables.circular reference": "Riferimento circolare alle variabili nella definizione di <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Non \u00e8 stato generato in tempo un insieme valido di variabili per la domanda.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Non posso arrotondare a un numero complesso di cifre decimali", "math.siground.complex": "Non posso arrotondare a un numero complesso di cifre significative", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Non posso calcolare l'MCD di numeri complessi", "math.lcm.complex": "Non posso calcolare l'mcm di numeri complessi", "math.lt.order complex numbers": "Non posso ordinare numeri complessi", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Non posso calcolare il determinante di una matrice non quadrata.", "matrixmath.abs.too big": "Mi dispiace, non so ancora calcolare il determinante di una matrice pi\u00f9 grande di 3x3.", "matrixmath.mul.different sizes": "Non posso moltiplicare matrici di dimensioni diverse.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "Risposta: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Poich\u00e9 hai avuto il punteggio pieno per la parte, le tue risposte ai passaggi non vengono contate.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "Questa parte \u00e8 stata valutata usando le tue risposte alle parti precedenti.", "part.marking.variable replacement part not answered": "Devi rispondere prima a {{part}}.", "part.marking.resubmit because of variable replacement": "Il voto di questa parte dipende dalle tue risposte alle altre parti, che hai cambiato. Invia di nuovo questa parte per aggiornare il tuo punteggio.", "part.marking.not submitted": "Non hai inviato una risposta.", "part.marking.did not answer": "Non hai risposto a questa domanda.", "part.marking.nothing entered": "Non hai inserito una risposta.", "part.marking.incorrect": "La tua risposta non \u00e8 corretta.", "part.marking.correct": "La tua risposta \u00e8 corretta.", "part.marking.uncaught error": "Errore nel valutare: {{-message}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "Risposta attesa:", "part.missing type attribute": "{{part}}: Manca l'attributo del tipo di parte", "part.unknown type": "{{part}}: Tipo di parte {{type}} non riconosciuto", "part.setting not present": "Propriet\u00e0 '{{property}}' non impostata", "part.jme.answer missing": "Manca la risposta corretta", "part.jme.answer too long": "La tua risposta \u00e8 troppo lunga.", "part.jme.answer too short": "La tua risposta \u00e8 troppo corta.", "part.jme.answer invalid": "La tua risposta non \u00e8 un'espressione matematica valida.<br/>{{-message}}.", "part.jme.marking.correct": "La tua risposta \u00e8 numericamente corretta.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>\n", "part.jme.must-have one": "La tua risposta deve contenere: {{strings}}", "part.jme.must-have several": "La tua risposta deve contenere tutti i seguenti elementi: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "La tua risposta non deve contenere:  {{strings}}", "part.jme.not-allowed several": "La tua risposta non deve contenere nessuno dei seguenti elementi: {{strings}}", "part.jme.unexpected variable name": "La tua risposta \u00e8 stata interpretata come utilizzante il nome della variabile inatteso <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "La tua risposta \u00e8 corretta, eccettuato il caso.", "part.numberentry.correct except decimal": "La tua risposta \u00e8 nell'intervallo permesso, ma non sono permessi numeri decimali.", "part.numberentry.correct except fraction": "La tua risposta \u00e8 nell'intervallo richiesto, ma le frazioni non sono permesse.", "part.numberentry.answer invalid": "Non hai inserito un numero valido.", "part.numberentry.answer not integer": "La tua risposta non \u00e8 valida. Devi inserire un numero intero, non un decimale.", "part.numberentry.answer not integer or decimal": "La tua risposta non \u00e8 valida. Devi inserire un intero o un decimale.", "part.numberentry.zero sig fig": "Questa parte \u00e8 impostata per arrotondare la risposta dello studente a zero cifre significative, il che non ha senso.", "part.mcq.options def not a list": "L'espressione che definisce le {{properties}} non \u00e8 una lista.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Manca la definizione delle scelte", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "Hai scelto il numero errato di scelte.", "part.mcq.no choices selected": "Nessuna scelta selezionata.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "Hai scelto una risposta corretta.", "part.matrix.answer invalid": "La tua risposta non \u00e8 valida.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.empty": "Non hai inserito una risposta.", "part.matrix.empty cell": "Uno o pi\u00f9 campi nella tua risposta sono vuoti.", "part.matrix.size mismatch": "L'autore della domanda non ha permesso allo studente di decidere le dimensioni della loro risposta, ma la risposta corretta \u00e8 {{correct_dimensions}} mentre l'input della risposta \u00e8 {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Domanda {{number}}: {{-message}}", "question.preamble.error": "Errore nel preambolo: {{-message}}", "question.preamble.syntax error": "Errore di sintassi nel preambolo", "question.unsupported part type": "Tipo di parte non supportato", "question.header": "Domanda {{number}}", "question.submit part": "Invia parte", "question.show steps": "Mostra i passaggi", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Il tuo punteggio non sar\u00e0 influenzato.", "question.show steps already penalised": "Hai gi\u00e0 mostrato i passaggi. Puoi mostrarli di nuovo senza ulteriori penalit\u00e0.", "question.hide steps": "Nascondi i passaggi", "question.hide steps no penalty": "Il tuo punteggio non sar\u00e0 influenzato.", "question.advice": "Soluzione", "question.no such part": "Non trovo la parte {{path}}", "question.can not submit": "Non posso inviare la risposta - controlla se hai fatto errori.", "question.answer submitted": "Risposta inviata", "question.score feedback.show": "Mostra il feedback", "question.score feedback.hide": "Nascondi il feedback", "question.score feedback.score total actual": "Punteggio: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Punteggio: {{scoreString}}", "question.score feedback.answered": "Answered", "question.score feedback.unanswered": "Unanswered", "question.score feedback.correct": "La tua risposta \u00e8 corretta", "question.score feedback.partial": "La tua risposta \u00e8 parzialmente corretta", "question.score feedback.wrong": "La tua risposta \u00e8 errata", "question.selector.unsubmitted changes": "Cambiamenti non inviati.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Tempo rimanente:", "xml.could not load": "Non ho potuto caricare un documento XML: {{-message}}", "xml.property not number": "La propriet\u00e0 {{name}} dovrebbe essere un numero, ma non \u00e8 ({{value}}), nel nodo {{element}}", "xml.property not boolean": "La propriet\u00e0 {{name}} dovrebbe essere una variabile booleana, ma non \u00e8 ({{value}}), nel nodo {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "Errore nell'inizializzazione del protocollo SCORM: {{-message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{-message}}", "scorm.error loading question": "Error loading question {{number}}: {{-message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{-message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "mark", "was": "was", "part": "parte", "gap": "gap", "step": "passaggio", "jme.substituteTree.undefined variable": "Variabile non definita: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.numberentry.precision type.dp": "posizione decimale", "part.numberentry.precision type.dp_plural": "posizioni decimali", "part.numberentry.precision type.sigfig": "cifra significativa", "part.numberentry.precision type.sigfig_plural": "cifre significative", "part.numberentry.give your answer to precision": "Arrotonda la tua risposta a {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Hai fatto un cambiamento alla tua risposta ma non l'hai inviato. Per favore controlla la risposta e poi premi il bottone <strong>Invia risposta</strong>", "question.unsubmitted changes_plural": "Hai fatto cambiamenti alle tue risposte ma non li hai inviati. Per favore controlla le tue risposte a ciascuna parte e poi premi il bottone <strong>Invia tutte le parti</strong>", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "marks", "was_plural": "were", "die.script not loaded": "Numbas couldn't start because the file <code>{{file}}</code> was not loaded. Check that it's included in <code>scripts.js</code>.", "math.combinations.n less than zero": "Non posso calcolare le combinazioni: n \u00e8 pi\u00f9 piccolo di zero", "math.combinations.k less than zero": "Non posso calcolare le combinazioni: k \u00e8 pi\u00f9 piccolo di zero", "math.combinations.n less than k": "Non posso calcolare le combinazioni: n \u00e8 pi\u00f9 piccolo di k", "math.permutations.n less than zero": "Non posso calcolare le permutazioni: n \u00e8 pi\u00f9 piccolo di zero", "math.permutations.k less than zero": "Non posso calcolare le permutazioni: k \u00e8 pi\u00f9 piccolo di zero", "math.permutations.n less than k": "Non posso calcolare le permutazioni: n \u00e8 pi\u00f9 piccolo di k", "part.numberentry.give your answer to precision_0": "Arrotonda la tua risposta all'intero pi\u00f9 vicino.", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "exam.introduction": "Introduzione all'esame", "exam.feedback": "Exam feedback message", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "Feedback", "part.numberentry.answer not reduced": "La tua risposta non \u00e8 ridotta ai minimi termini.", "part.numberentry.give your answer as a reduced fraction": "Riduci la tua risposta ai minimi termini.", "part.numberentry.negative decimal places": "Questa parte \u00e8 impostata per arrotondare la risposta dello studente a un numero negativo di cifre decimali, che non ha senso.", "part.mcq.choices": "opzioni", "part.mcq.answers": "risposta", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Enunciato", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "L'insieme di regole {{name}} non \u00e8 stato definito", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "question.score feedback.answered total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Answered.", "question.score feedback.answered actual": "Score: {{scoreString}}", "question.score feedback.unanswered total": "{{marksString}}.", "answer.number.not a number": "La risposta non \u00e8 valida", "answer.number.fractions not allowed": "Non puoi inserire una frazione.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Non puoi inserire frazioni.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "exam.enter password": "Password:", "exam.password.correct": "Questa password \u00e8 corretta. Puoi cominciare l'esame.", "exam.password.incorrect": "Questa password \u00e8 errata.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Revisione", "control.confirm regen no marks": "Vorresti ri-randomizzare questa domanda?", "control.confirm reveal no marks": "Vorresti rivelare la risposta a questa domanda?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Non posso convertire automaticamente da {{from}} a {{to}}.", "jme.display.simplifyTree.empty expression": "L'espressione \u00e8 vuota", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Hai mostrato i passaggi.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "Il punteggio minimo per questa parte \u00e8 <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "Non si \u00e8 potuto attribuire punteggio a questa parte utilizzando le tue risposte alle parti precedenti.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Potrebbe mancare una virgola dopo il nome e la descrizione", "marking.note.invalid definition.description missing closing bracket": "Potrebbe mancare una parentesi di chiusura", "marking.note.empty expression": "Il nodo <code>{{name}}</code> \u00e8 vuoto.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Hai scelto una risposta errata.", "part.matrix.not all cells same precision": "Non hai dato ogni cella nella tua risposta con la stessa precisione.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "Nessun valore dato.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Errore nella creazione della domanda {{number}}: {{-message}}", "question.score feedback.not marked": "Non valutato", "question.score feedback.partially answered": "Parzialmente risposto", "variable.error in variable definition": "Errore nella definizione della variabile <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Non sono riuscito a caricare l'estensione <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Risposta alla parte {{name}}", "part.correct answer title": "Risposta attesa alla parte {{name}}", "part.jme.must-match.failed": "La tua risposta non \u00e8 scritta nella forma corretta.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "nb-no": {"translation": {"page.loading": "Laster...", "page.saving": "<p>Lagrer.</p> <p>Dette kan ta noen sekunder.</p>", "mathjax.math processing error": "\"{{-message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "Numbas har feilet", "die.sorry": "Beklager, det har oppst\u00e5tt en feil, og Numbas kan ikke fortsette. Nedenfor er en beskrivelse av feilen.", "die.error": "Feil", "modal.ok": "OK", "modal.cancel": "Avbryt", "exam.exam name": "Eksamen navn:", "exam.random seed": "Sesjon ID:", "exam.student name": "Studentens navn:", "exam.number of questions": "Antall sp\u00f8rsm\u00e5l:", "exam.marks available": "Mulige poeng:", "exam.pass percentage": "Grense for best\u00e5tt:", "exam.time allowed": "Tillatt tid:", "exam.passed": "Best\u00e5tt", "exam.failed": "Ikke best\u00e5tt", "exam.review header": "Gjennomgang: ", "frontpage.start": "Start", "suspend.paused header": "Pause", "suspend.exam suspended": "Eksamen er avbrutt. Klikk Fortsett for \u00e5 g\u00e5 videre.", "suspend.you can resume": "Du kan fortsette eksamen neste gang du starter denne aktiviteten.", "suspend.resume": "Fortsett", "result.exit": "Avslutt eksamen", "result.print": "Skriv ut detaljert resultat", "result.exam summary": "Eksamen oversikt", "result.performance summary": "Resultatsammendrag", "result.exam start": "Eksamen start:", "result.exam stop": "Eksamen slutt:", "result.time spent": "Tidsbruk:", "result.questions attempted": "Antall besvarte sp\u00f8rsm\u00e5l:", "result.score": "Poengsum:", "result.result": "Resultat:", "result.question number": "Sp\u00f8rsm\u00e5l nummer", "result.question score": "Poengsum", "result.question review title": "G\u00e5 igjennom dette sp\u00f8rsm\u00e5let", "result.click a question to review": "Klikk p\u00e5 et sp\u00f8rsm\u00e5lnummer for \u00e5 se karaktersetting, og om mulig, fullstendig l\u00f8sning.", "end.exam has finished": "Eksamen er avsluttet. Du kan n\u00e5 lukke vinduet.", "control.confirm leave": "Du har ikke levert besvarelse.", "control.not all questions answered": "Du har ikke svart p\u00e5 alle sp\u00f8rsm\u00e5lene i denne eksamen.", "control.not all questions submitted": "Du har endret ett eller flere svar men ikke lagret dem. Vennligst se om svarene er lagret.", "control.confirm end": "Er du sikker p\u00e5 at du vil avslutte? Etter dette vil du ikke kunne endre p\u00e5 svarene dine..", "control.confirm regen": "Vil du lage nye tilfeldige tall i denne oppgaven? Hvis du klikker OK vil svarene og oppn\u00e5dde poeng bli annullert.", "control.confirm reveal": "Vil du se svaret p\u00e5 dette sp\u00f8rsm\u00e5let? Alle poeng du har f\u00e5tt hittil vil bli l\u00e5st - og du kan ikke besvare dette sp\u00f8rsm\u00e5let senere.", "control.proceed anyway": "Fortsett likevel?", "control.regen": "Pr\u00f8v et lignende sp\u00f8rsm\u00e5l", "control.submit answer": "Send inn svar", "control.submit all parts": "Send inn alle delsvar", "control.submit again": "Send inn p\u00e5 nytt", "control.submit": "Send inn", "control.previous": "Forrige", "control.next": "Neste", "control.advice": "Svarforslag", "control.reveal": "Vis svar", "control.total": "Totalt", "control.pause": "Pause", "control.end exam": "Avslutt eksamen", "control.back to results": "Go back to results", "display.part.jme.error making maths": "Feil i visning av matematisk uttrykk", "exam.xml.bad root": "Elementet p\u00e5 \u00f8verste niv\u00e5 i XML m\u00e5 v\u00e6re 'exam'", "exam.changeQuestion.no questions": "Eksamen inneholder ingen sp\u00f8rsm\u00e5l! Sjekk .exam-fila for feil.", "feedback.you were awarded": "Du oppn\u00e5dde {{count}} $t(mark).", "feedback.taken away": "<strong>{{count}}</strong> $t(mark) er trukket fra.", "jme.tokenise.invalid": "Ugyldig uttrykk: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Det mangler argumenter for \u00e5 utf\u00f8re {{op}}", "jme.shunt.no left bracket in function": "Venstre parentes mangler i funksjon eller tuppel", "jme.shunt.no left square bracket": "Venstre parentes mangler", "jme.shunt.no left bracket": "Venstre parentes mangler", "jme.shunt.no right bracket": "H\u00f8yre parentes mangler", "jme.shunt.no right square bracket": "H\u00f8yre parentes mangler i slutten av liste", "jme.shunt.missing operator": "Uttrykket kan ikke evalueres -- operator mangler.", "jme.typecheck.function maybe implicit multiplication": "Operasjon {{name}} er ikke definert. Mente du <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Operasjon {{op}} er ikke definert. Mente du <br/><code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operasjon '{{op}}' er ikke definert.", "jme.typecheck.no right type definition": "Finner ikke definisjon av '{{op}}' med korrekt type.", "jme.typecheck.no right type unbound name": "Variabel <code>{{name}}</code> er ikke definert.", "jme.typecheck.map not on enumerable": "<code>map</code> operasjonen m\u00e5 gjelde en liste eller range, ikke {{type}}", "jme.evaluate.undefined variable": "Variabel {{name}} er udefinert", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "Switch-setning mangler standardverdi", "jme.func.listval.invalid index": "Ugyldig listeindeks {{index}} for en liste med st\u00f8rrelse {{size}}", "jme.func.listval.not a list": "Objektet kan ikke indekseres", "jme.func.matrix.invalid row type": "Kan ikke danne matrise ut fra rader av type {{type}}", "jme.func.except.continuous range": "Kan ikke bruke operator 'except' p\u00e5 et kontinuerlig omr\u00e5de.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "Ingen samsvarende <code>]</code> i {{op}} argumenter.", "jme.texsubvars.missing parameter": "Manglende parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Ingen samsvarende <code>}</code> i {{op}}.", "jme.user javascript.error": "Feil i brukerdefinert javascript funksjon <code>{{name}}</code><br/>{{-message}}", "jme.variables.error making function": "Feil med funksjonskode <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Syntax feil i definisjonen av funksjon", "jme.variables.variable not defined": "Variabel <code>{{name}}</code> er ikke definert.", "jme.variables.empty definition": "Definisjonen av variabel {{name}} er tom.", "jme.variables.circular reference": "Sirkul\u00e6r referanse til variabel <code>{{name}}</code>", "jme.variables.error computing dependency": "Feil ved beregning av referert variabel <code>{{name}}</code>", "jme.variables.error evaluating variable": "Feil ved evaluering av variabel {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Et gyldig sett med variabler i sp\u00f8rsm\u00e5l ble ikke generert p\u00e5 normal tid", "jme.display.unknown token type": "Kan ikke lage tekstbilde av token type {{type}}", "jme.display.collectRuleset.no sets": "Ingen sett ble oppgitt til collectRuleset!", "jme.display.collectRuleset.set not defined": "Regelsett {{name}} er ikke definert", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree m\u00e5 f\u00e5 angitt et Scope", "math.precround.complex": "Kan ikke avrunde til antall desimaler gitt som komplekst tall", "math.siground.complex": "Kan ikke avrunde til antall signifikante siffer gitt som komplekst tall", "math.combinations.complex": "Kan ikke beregne kombinasjoner for komplekse tall", "math.permutations.complex": "Kan ikke beregne permutasjoner for komplekse tall", "math.gcf.complex": "Kan ikke beregne GCF for komplekse tall", "math.lcm.complex": "Kan ikke beregne LCM for komplekse tall", "math.lt.order complex numbers": "Kan ikke sortere komplekse tall", "math.choose.empty selection": "Slumpfunksjon har tomt tallomr\u00e5de", "matrixmath.abs.non-square": "Kan ikke beregne determinanten til en matrise som ikke er kvadratisk.", "matrixmath.abs.too big": "Kan ikke beregne determinanten til en matrise st\u00f8rre enn 3x3.", "matrixmath.mul.different sizes": "Kan ikke multiplisere matriser med ulike dimensjoner.", "vectormath.cross.not 3d": "Kan bare beregne kryssprodukt til 3-dimensjonale vektorer.", "vectormath.dot.matrix too big": "Kan ikke beregne prikkproduktet til en matrise som ikke er $1 \\times N$ eller $N \\times 1$.", "vectormath.cross.matrix too big": "Kan ikke beregne kryssproduktet til en matrise som ikke er $1 \\times N$ eller $N \\times 1$.", "part.with steps answer prompt": "Svar: ", "part.script.error": "Feil i del {{path}} brukerdefinert skript {{script}}: {{-message}}", "part.marking.steps no matter": "Ettersom du fikk alt riktig i oppgaven blir ikke delsvarene telt opp.", "part.marking.revealed steps no penalty": "Du valgte \u00e5 se svarforslag.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "Du m\u00e5 svare p\u00e5 {{part}} f\u00f8rst", "part.marking.resubmit because of variable replacement": "This part's marking depends on your answers to other parts, which you have changed. Save your answer to this part again to update your score.", "part.marking.not submitted": "Du svarte ikke", "part.marking.did not answer": "Du svarte ikke p\u00e5 dette sp\u00f8rsm\u00e5let.", "part.marking.nothing entered": "Du svarte ikke.", "part.marking.incorrect": "Svaret er feil.", "part.marking.correct": "Svaret er riktig.", "part.marking.uncaught error": "Error when marking: {{-message}}", "part.marking.no result": "Denne delen kunne ikke merkes.", "part.correct answer": "Riktig svar:", "part.missing type attribute": "Sp\u00f8rsm\u00e5lsdel har feil atributt", "part.unknown type": "Ukjent sp\u00f8rsm\u00e5lsdel {{type}}", "part.setting not present": "Egenskap '{{property}}' er ikke angitt", "part.jme.answer missing": "Korrekt svar for et JME felt mangler (%s)", "part.jme.answer too long": "Svaret er for langt.", "part.jme.answer too short": "Svaret er for kort.", "part.jme.answer invalid": "Svaret er ikke et gyldig matematisk uttrykk.<br/>{{-message}}", "part.jme.marking.correct": "Svaret er numerisk korrekt.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Svaret m\u00e5 inneholde: {{strings}}", "part.jme.must-have several": "Svaret m\u00e5 inneholde alle: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Svaret m\u00e5 ikke inneholde: {{strings}}", "part.jme.not-allowed several": "Svaret m\u00e5 ikke inneholde disse: {{strings}}", "part.jme.unexpected variable name": "Svaret ditt er tolket til \u00e5 bruke det uventede variabelnavnet <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Svaret ditt er tolket til \u00e5 bruke det uventede variabelnavnet <code>{{name}}</code>. Mente du <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing from a Pattern Match part (%s)", "part.patternmatch.correct except case": "Svaret er riktig, unntatt i dette tilfellet.", "part.numberentry.correct except decimal": "Svaret er i riktig intervall, men desimaltall er ikke tillatt.", "part.numberentry.correct except fraction": "Ditt svar er innenfor riktig omr\u00e5de, men br\u00f8k er ikke tillatt som svar.", "part.numberentry.answer invalid": "Du svarte ikke med et gyldig tall.", "part.numberentry.answer not integer": "Ditt svar er ikke gyldig. Tast inn et heltall, ikke desimaltall.", "part.numberentry.answer not integer or decimal": "Ditt svar er ikke gyldig. Tast inn et heltall eller et desimaltall.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Svarmuligheter mangler i flervalgstesten (%s)", "part.mcq.matrix not a number": "Del %s evaluering av matrisecelle {{row}},{{column}} gir ikke et tall", "part.mcq.wrong number of choices": "Du merket av feil antall valg.", "part.mcq.no choices selected": "Ingen av valgene er merket.", "part.mcq.matrix not a list": "Score matrise for flervalg oppgave definert som JME uttrykk er ikke en liste slik den skal v\u00e6re.", "part.mcq.matrix wrong type": "Element av ugyldig type '%s' er brukt i score matrise.", "part.mcq.matrix mix of numbers and lists": "En blanding av tall og lister er brukt i score matrise.", "part.mcq.matrix wrong size": "Score matrise er av feil dimensjon.", "part.mcq.correct choice": "Du valgte riktig svar.", "part.matrix.answer invalid": "Svaret ditt er ugyldig.", "part.matrix.invalid cell": "En eller flere av cellene i ditt svar er tomme eller ugyldige", "part.matrix.some incorrect": "En eller flere av cellene i ditt svar er ikke riktig besvart, men du har f\u00e5tt poeng for resten", "part.matrix.empty": "Du har ikke gitt noe svar.", "part.matrix.empty cell": "En eller flere av cellene i ditt svar er tomme.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>Boks {{index}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Kan ikke fortsette dette fors\u00f8ket - pakken er endret siden siste sesjon.", "question.error": "Sp\u00f8rsm\u00e5l {{number}}: {{-message}}", "question.preamble.error": "Feil i startkoden: {{-message}}", "question.preamble.syntax error": "Syntaks feil i startkoden", "question.unsupported part type": "Ikke gyldig svartype", "question.header": "Sp\u00f8rsm\u00e5l {{number}}", "question.submit part": "Send inn svar", "question.show steps": "Vis tips", "question.show steps penalty": "Du vil miste <strong>{{count}}</strong> $t(mark).", "question.show steps no penalty": "Din score vil ikke bli p\u00e5virket.", "question.show steps already penalised": "Du har allerede vist tips. Du kan se tips p\u00e5 nytt uten \u00e5 tape poeng.", "question.hide steps": "Skjul tips", "question.hide steps no penalty": "Din score vil ikke bli p\u00e5virket.", "question.advice": "Svarforslag", "question.no such part": "Finner ikke sp\u00f8rsm\u00e5l {{path}}", "question.can not submit": "Kan ikke sende inn svar - sjekk mulige feil.", "question.answer submitted": "Svaret er sendt inn", "question.score feedback.show": "Vis vurdering", "question.score feedback.hide": "Skjul vurdering", "question.score feedback.answered total actual": "Poengsum: {{score}}/{{marks}}", "question.score feedback.answered total": "{{marksString}}. Besvart.", "question.score feedback.answered actual": "Poengsum: {{scoreString}}", "question.score feedback.answered": "Besvart.", "question.score feedback.unanswered": "Ubesvart.", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Ditt svar er riktig", "question.score feedback.partial": "Ditt svar er delvis riktig", "question.score feedback.wrong": "Ditt svar er feil", "question.selector.unsubmitted changes": "Endringer som ikke er sendt inn.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Tid igjen: %s", "xml.could not load": "Kan ikke laste et XML dokument: {{-message}}", "xml.property not number": "Egenskap {{name}} m\u00e5 v\u00e6re et tall, men er ikke ({{value}}), i node {{element}}", "xml.property not boolean": "Egenskap {{name}} m\u00e5 v\u00e6re en boolsk verdi, men er ikke ({{value}}), i node {{element}}", "xml.error in variable definition": "Feil ved definisjon av variabel <code>{{name}}</code>", "scorm.error initialising": "Feil ved initiering av SCORM protokoll: {{-message}}", "scorm.failed save": "<p>Skriving av data til serveren feilet. Klikk <b>OK</b> og pr\u00f8v en gang til.</p>\n<p>Hvis denne feilen gjentar seg ofte b\u00f8r du sjekke forbindelsen til internet eller pr\u00f8ve en annen datamaskin. Dine tidligere innsendte svar er lagret og blir gjentatt hvis du fortsette \u00f8kten p\u00e5 en annen datamaskin.</p>\n<p> Hvis denne gjentar seg ofte og du kan ikke lagre <em>noen</em> svar b\u00f8r du ta kontakt med din l\u00e6rer.</p>", "scorm.no exam suspend data": "Kan ikke fortsette: finner ikke sesjonsdata.", "scorm.error loading suspend data": "Feil ved lasting av sesjonsdata: {{-message}}", "scorm.error loading question": "Feil ved lasting av sp\u00f8rsm\u00e5l {{number}}: {{-message}}", "scorm.no question suspend data": "Ingen sesjonsdata for sp\u00f8rsm\u00e5l", "scorm.error loading part": "Feil ved lasting av del {{part}}: {{-message}}", "scorm.no part suspend data": "Ingen sesjonsdata for delen", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "poeng", "was": "var", "part": "del", "gap": "mellomrom", "step": "steg", "jme.substituteTree.undefined variable": "Udefinert variabel: <code>{{name}}</code>", "jme.user javascript.returned undefined": "Brukerdefinert javascript funksjon <code>{{name}}</code> returnerte <code>undefined</code>.", "part.marking.steps change": "Du oppn\u00e5dde <strong>{{count}}</strong> $t(mark) for delsvarene", "part.marking.revealed steps with penalty": "Du valgte \u00e5 se svarforslag. Maksimal poengsum for denne oppgaven er <strong>{{count}}</strong> $t(mark). Din poengsum blir dermed redusert.", "part.marking.total score": "Du fikk <strong>{{count}}</strong> $t(mark) for denne oppgaven.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "signifikant siffer", "part.numberentry.precision type.sigfig_plural": "signifikante sifre", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Du har gjort endring i svaret, men ikke sendt det inn. Vennligst sjekk svaret og klikk <strong>Send inn svar</strong>.", "question.unsubmitted changes_plural": "Du har gjort endringer i dine svar, men ikke sendt de inn. Vennligst se over svarene og klikk <strong>Send inn alle delsvar</strong>.", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "poeng", "was_plural": "var", "die.script not loaded": "Numbas kunne ikke starte fordi filen <code>{{file}}</code> ikke ble lastet inn. Kontroller at den er inkludert i <code>scripts.js</code>", "math.combinations.n less than zero": "Kan ikke utregne kombinasjoner: n er mindre enn null", "math.combinations.k less than zero": "Kan ikke utregne kombinasjoner: k er mindre enn null", "math.combinations.n less than k": "Kan ikke utregne kombinasjoner: n er mindre enn k", "math.permutations.n less than zero": "Kan ikke utregne permutasjonene: n er mindre enn null", "math.permutations.k less than zero": "Kan ikke utregne permutasjonene: k er mindre enn null", "math.permutations.n less than k": "Kan ikke utregne permutasjonene: n er mindre enn k", "part.numberentry.give your answer to precision_0": "Avrund svaret ditt til n\u00e6rmeste heltall.", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "exam.introduction": "Exam introduction", "exam.feedback": "Exam feedback message", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "tilbakemelding", "part.numberentry.answer not reduced": "Svaret ditt er ikke forkortet mest mulig.", "part.numberentry.give your answer as a reduced fraction": "Forkort svaret mest mulig.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "valg", "part.mcq.answers": "svar", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Uttrykk", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "answer.number.not a number": "Svaret ditt er ikke et gyldig tall.", "answer.number.fractions not allowed": "Du kan ikke skrive en br\u00f8k.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Du kan ikke skrive br\u00f8ker.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "exam.enter password": "Passord:", "exam.password.correct": "Passordet er korrekt. Du kan starte eksamen.", "exam.password.incorrect": "Feil passord.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Review", "control.confirm regen no marks": "Would you like to re-randomise this question?", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Uttrykket er tomt", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Du avsl\u00f8rte stegene.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Mulig du mangler kolon etter navn og beskrivelse", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Du valgte et feil svar", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Partially answered", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Poengsum: {{scoreString}}", "question.score feedback.score total actual": "Poengsum: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "tr-tr": {"translation": {"page.loading": "Y\u00fckleniyor...", "page.saving": "<p>Kaydediliyor.</p>\n<p>Bu biraz vakit alabilir.</p>", "mathjax.math processing error": "\"{{-message}}\" metinle\u015ftirilirken <code>{{expression}}</code>", "die.numbas failed": "Numbas hata verdi", "die.sorry": "\u00dczg\u00fcn\u00fcz, Numbas devam edemiyecek seviyede bir hatayla kar\u015f\u0131la\u015ft\u0131. A\u015fa\u011f\u0131dan hatan\u0131n detaylar\u0131na ula\u015fabilirsiniz.", "die.error": "Hata", "modal.ok": "Tamam", "modal.cancel": "\u0130ptal", "exam.exam name": "S\u0131nav Ad\u0131:", "exam.random seed": "Oturum ID'si:", "exam.student name": "\u00d6\u011frenci Ad\u0131:", "exam.number of questions": "Soru adedi:", "exam.marks available": "\u0130\u015faretlenmi\u015f olanlar:", "exam.pass percentage": "Ge\u00e7me Oran\u0131:", "exam.time allowed": "Ge\u00e7en Zaman:", "exam.passed": "Ge\u00e7ti", "exam.failed": "Ba\u015far\u0131s\u0131z", "exam.review header": "\u0130nceleme: ", "frontpage.start": "Ba\u015fla", "suspend.paused header": "Durduruldu", "suspend.exam suspended": "S\u0131nav ask\u0131ya al\u0131nd\u0131. Devam etmek i\u00e7in <em>Devam</em>  tu\u015funa bas\u0131n\u0131z", "suspend.you can resume": "Daha sonra ba\u015flad\u0131\u011f\u0131n\u0131zda bu oturumdan devam edebilirsiniz", "suspend.resume": "Devam", "result.exit": "S\u0131navdan \u00c7\u0131k", "result.print": "Sonucun \u00f6zetini \u00e7\u0131kt\u0131 al", "result.exam summary": "S\u0131nav \u00d6zeti", "result.performance summary": "Performans \u00d6zeti", "result.exam start": "S\u0131nav Ba\u015flad\u0131:", "result.exam stop": "S\u0131nav Bitti:", "result.time spent": "Harcanan Zaman:", "result.questions attempted": "Yap\u0131lan Sorular:", "result.score": "Puan:", "result.result": "Sonu\u00e7:", "result.question number": "Soru Adedi", "result.question score": "Puan", "result.question review title": "Bu soruyu incele", "result.click a question to review": "Cevaplar\u0131n\u0131n nas\u0131l oldu\u011funu g\u00f6rmek i\u00e7in soru adedine t\u0131kla ve e\u011fer mevcutsa \u00e7\u00f6z\u00fcmlerini g\u00f6r", "end.exam has finished": "S\u0131nav bitti. Art\u0131k bu pencereyi kapatabilirsin.", "control.confirm leave": "S\u0131nav\u0131n\u0131 bitirmedin.", "control.not all questions answered": "Bu s\u0131navdaki t\u00fcm sorular\u0131 \u00e7\u00f6zmedin.", "control.not all questions submitted": "Bir veya daha fazla soruda de\u011fi\u015fiklik yapt\u0131n fakat g\u00f6ndermedin. L\u00fctfen her soruyu g\u00f6nderdi\u011finden emin ol.", "control.confirm end": "S\u0131nav\u0131 bitirmek istedi\u011fine emin misin? S\u0131nav\u0131 bitirdikten sonra cevaplar\u0131n\u0131 de\u011fi\u015ftiremezsin.", "control.confirm regen": "Would you like to re-randomise this question? If you click OK, all your answers and marks for the current question will be lost.", "control.confirm reveal": "Would you like to reveal the answer to this question? Any marks you have received so far will be locked and you will not be able to answer this question later.", "control.proceed anyway": "Proceed anyway?", "control.regen": "Bunun gibi ba\u015fka bir soru dene", "control.submit answer": "Cevab\u0131 g\u00f6nder", "control.submit all parts": "T\u00fcm par\u00e7alar\u0131 yolla", "control.submit again": "Tekrar G\u00f6nder", "control.submit": "G\u00f6nder", "control.previous": "\u00d6nceki", "control.next": "Sonraki", "control.advice": "Tavsiye", "control.reveal": "Cevaplar\u0131 \u0130ncele", "control.total": "Total", "control.pause": "Durdur", "control.end exam": "S\u0131nav\u0131 Bitir", "control.back to results": "Sonu\u00e7lara geri d\u00f6n", "display.part.jme.error making maths": "Matematik \u00c7\u0131kt\u0131lar Olu\u015fturulamad\u0131", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "This exam contains no questions! Check the .exam file for errors.", "feedback.you were awarded": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid": "Invalid expression: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Not enough arguments for operation <code>{{op}}</code>", "jme.shunt.no left bracket in function": "No matching left bracket in function application or tuple", "jme.shunt.no left square bracket": "No matching left bracket", "jme.shunt.no left bracket": "No matching left bracket", "jme.shunt.no right bracket": "No matching right bracket", "jme.shunt.no right square bracket": "No matching right square bracket to end list", "jme.shunt.missing operator": "Expression can't be evaluated -- missing an operator.", "jme.typecheck.function maybe implicit multiplication": "Function <code>{{name}}</code> is not defined. Did you mean <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Function <code>{{op}}</code> is not defined. Is <code>{{op}}</code> a variable, and did you mean <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operation '{{op}}' is not defined.", "jme.typecheck.no right type definition": "No definition of '{{op}}' of correct type found.", "jme.typecheck.no right type unbound name": "Variable <code>{{name}}</code> is not defined.", "jme.typecheck.map not on enumerable": "<code>map</code> operation must work over a list or a range, not {{type}}", "jme.evaluate.undefined variable": "Variable {{name}} is undefined", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.matrix.invalid row type": "Can't construct a matrix from rows of type {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Missing parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Syntax error in function definition", "jme.variables.variable not defined": "Variable <code>{{name}}</code> is not defined.", "jme.variables.empty definition": "Definition of variable <code>{{name}}</code> is empty.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Can't compute GCF of complex numbers", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.lt.order complex numbers": "Can't order complex numbers", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Can't compute the determinant of a matrix which isn't square.", "matrixmath.abs.too big": "Sorry, can't compute the determinant of a matrix bigger than 3x3 yet.", "matrixmath.mul.different sizes": "Can't multiply matrices of different sizes.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "Answer: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "You must answer {{part}} first.", "part.marking.resubmit because of variable replacement": "This part's marking depends on your answers to other parts, which you have changed. Save your answer to this part again to update your score.", "part.marking.not submitted": "No answer submitted.", "part.marking.did not answer": "You did not answer this question.", "part.marking.nothing entered": "You did not enter an answer.", "part.marking.incorrect": "Your answer is incorrect.", "part.marking.correct": "Your answer is correct.", "part.marking.uncaught error": "Error when marking: {{-message}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "Expected answer:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "Correct answer is missing", "part.jme.answer too long": "Your answer is too long.", "part.jme.answer too short": "Your answer is too short.", "part.jme.answer invalid": "Your answer is not a valid mathematical expression.<br/>{{-message}}.", "part.jme.marking.correct": "Your answer is numerically correct.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Your answer must contain: {{strings}}", "part.jme.must-have several": "Your answer must contain all of: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Your answer must not contain: {{strings}}", "part.jme.not-allowed several": "Your answer must not contain any of: {{strings}}", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "Your answer is within the allowed range, but decimal numbers are not allowed.", "part.numberentry.correct except fraction": "Your answer is within the allowed range, but fractions are not allowed.", "part.numberentry.answer invalid": "You did not enter a valid number.", "part.numberentry.answer not integer": "Your answer is invalid. You must enter a whole number, not a decimal.", "part.numberentry.answer not integer or decimal": "Your answer is invalid. You must enter an integer or a decimal.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "You selected the wrong number of choices.", "part.mcq.no choices selected": "No choices selected.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "You chose a correct answer.", "part.matrix.answer invalid": "Your answer is not valid.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.empty": "You have not entered an answer.", "part.matrix.empty cell": "One or more of the cells in your answer is empty.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Question {{number}}: {{-message}}", "question.preamble.error": "Error in preamble: {{-message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "Question {{number}}", "question.submit part": "Save answer", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.advice": "Advice", "question.no such part": "Can't find part {{path}}.", "question.can not submit": "Can not submit answer - check for errors.", "question.answer submitted": "Answer submitted", "question.score feedback.show": "Show feedback", "question.score feedback.hide": "Hide feedback", "question.score feedback.answered total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Answered.", "question.score feedback.answered actual": "Score: {{scoreString}}", "question.score feedback.answered": "Answered", "question.score feedback.unanswered": "Unanswered", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Your answer is correct", "question.score feedback.partial": "Your answer is partially correct", "question.score feedback.wrong": "Your answer is incorrect", "question.selector.unsubmitted changes": "Unsubmitted changes.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Time remaining:", "xml.could not load": "Couldn't load an XML document: {{-message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "Error initialising SCORM protocol: {{-message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{-message}}", "scorm.error loading question": "Error loading question {{number}}: {{-message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{-message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "mark", "was": "was", "part": "part", "gap": "gap", "step": "step", "jme.substituteTree.undefined variable": "Undefined variable: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "significant figure", "part.numberentry.precision type.sigfig_plural": "significant figures", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "You have made a change to your answer but not submitted it. Please check your answer and then press the <strong>Save answer</strong> button.", "question.unsubmitted changes_plural": "You have made changes to your answers but not submitted them. Please check your answers to each part and then press the <strong>Save all answers</strong> button.", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "marks", "was_plural": "were", "die.script not loaded": "Numbas couldn't start because the file <code>{{file}}</code> was not loaded. Check that it's included in <code>scripts.js</code>.", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "part.numberentry.give your answer to precision_0": "Round your answer to the nearest integer.", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "exam.introduction": "Exam introduction", "exam.feedback": "Exam feedback message", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "Feedback", "part.numberentry.answer not reduced": "Your answer is not reduced to lowest terms.", "part.numberentry.give your answer as a reduced fraction": "Reduce your answer to lowest terms.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "choices", "part.mcq.answers": "answers", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Statement", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "answer.number.not a number": "Your answer is not a valid number.", "answer.number.fractions not allowed": "You may not enter a fraction.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "You may not enter fractions.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "exam.enter password": "Password:", "exam.password.correct": "This password is correct. You can start the exam.", "exam.password.incorrect": "This password is incorrect.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Review", "control.confirm regen no marks": "Would you like to re-randomise this question?", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Partially answered", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "en-school": {"translation": {"page.loading": "Loading...", "page.saving": "<p>Saving.</p>\n<p>This might take a few seconds.</p>", "mathjax.math processing error": "MathJax processing error: \"{{message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "Numbas has failed", "die.sorry": "Sorry, Numbas has encountered an error which means it can't continue. Below is a description of the error.", "die.script not loaded": "Numbas couldn't start because the file <code>{{file}}</code> was not loaded. Check that it's included in <code>scripts.js</code>.", "die.error": "Error", "modal.ok": "OK", "modal.cancel": "Cancel", "exam.exam name": "Test Name:", "exam.random seed": "Session ID:", "exam.student name": "Student's Name:", "exam.number of questions": "Number of Questions:", "exam.marks available": "Marks Available:", "exam.pass percentage": "Pass Percentage:", "exam.time allowed": "Time Allowed:", "exam.passed": "Passed", "exam.failed": "Failed", "exam.review header": "Review: ", "frontpage.start": "Start the test", "suspend.paused header": "Paused", "suspend.exam suspended": "The test is paused. Press <em>Resume</em> to continue.", "suspend.you can resume": "You will be able to resume this session the next time you start this activity.", "suspend.resume": "Resume", "result.exit": "Exit Test", "result.print": "Print this results summary", "result.review": "Review", "result.exam summary": "Test Summary", "result.performance summary": "Performance Summary", "result.exam start": "Test Start:", "result.exam stop": "Test Stop:", "result.time spent": "Time Spent:", "result.questions attempted": "Questions Attempted:", "result.score": "Score:", "result.result": "Result:", "result.detailed question breakdown": "Detailed Question Breakdown and Feedback", "result.question number": "Question Number", "result.question score": "Score", "result.question review title": "Look at this question again", "result.click a question to review": "Click on a question number to see how your answers were marked.", "end.exam has finished": "The test has finished. You can close this window now.", "control.confirm leave": "You haven't finished the test.", "control.not all questions answered": "You have not completed every question in this test.", "control.not all questions submitted": "You have changed one of your answers but not submitted it. Please check each question has been submitted.", "control.confirm end": "Are you sure you want to end the test?<br>After you end the test, you will not be able to change any of your answers.", "control.confirm regen": "Would you like to re-randomise this question?<br> If you click OK, all your answers and marks for the current question will be lost.", "control.confirm reveal": "Would you like to reveal the answer to this question?<br>Any marks you have received so far will be locked and you will not be able to answer this question later.", "control.proceed anyway": "Proceed anyway?", "control.regen": "Try another question like this one", "control.submit answer": "Check my answer", "control.submit all parts": "Check every part", "control.submit again": "Check again", "control.submit": "Check", "control.previous": "Previous", "control.next": "Next", "control.advice": "Worked solution", "control.reveal": "Reveal answers", "control.total": "Total score", "control.pause": "Pause", "control.end exam": "End Test", "control.back to results": "Go back to results", "display.part.jme.error making maths": "Error making maths display", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "This test contains no questions! Check the .exam file for errors.", "feedback.you were awarded": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid": "Invalid expression: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Not enough arguments for operation {{op}}", "jme.shunt.no left bracket in function": "No matching left bracket in function application or tuple", "jme.shunt.no left square bracket": "No matching left bracket", "jme.shunt.no left bracket": "No matching left bracket", "jme.shunt.no right bracket": "No matching right bracket", "jme.shunt.no right square bracket": "No matching right square bracket to end list", "jme.shunt.missing operator": "Expression can't be evaluated -- missing an operator.", "jme.typecheck.function maybe implicit multiplication": "Operation {{name}} is not defined. Did you mean <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Operation {{op}} is not defined. Did you mean <br/><code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operation '{{op}}' is not defined.", "jme.typecheck.no right type definition": "No definition of '{{op}}' of correct type found.", "jme.typecheck.no right type unbound name": "Variable <code>{{name}}</code> is not defined.", "jme.typecheck.map not on enumerable": "<code>map</code> operation must work over a list or a range, not {{type}}", "jme.evaluate.undefined variable": "Variable {{name}} is undefined", "jme.substituteTree.undefined variable": "Undefined variable: <code>{{name}}</code>", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.matrix.invalid row type": "Can't construct a matrix from rows of type {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Missing parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{message}}", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{message}}", "jme.variables.syntax error in function definition": "Syntax error in function definition", "jme.variables.variable not defined": "Variable <code>{{name}}</code> is not defined.", "jme.variables.empty definition": "Definition of variable <code>{{name}}</code> is empty.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Can't compute GCF of complex numbers", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.lt.order complex numbers": "Can't order complex numbers", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Can't compute the determinant of a matrix which isn't square.", "matrixmath.abs.too big": "Sorry, can't compute the determinant of a matrix bigger than 3x3 yet.", "matrixmath.mul.different sizes": "Can't multiply matrices of different sizes.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "Answer: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "You must answer {{part}} first", "part.marking.resubmit because of variable replacement": "This part's marking depends on your answers to other parts, which you have changed. Submit this part again to update your score.", "part.marking.not submitted": "No answer submitted", "part.marking.did not answer": "You did not answer this question.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.marking.nothing entered": "You did not enter an answer.", "part.marking.incorrect": "Your answer is incorrect.", "part.marking.correct": "Your answer is correct.", "part.marking.uncaught error": "Error when marking {{part}}: {{message}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "Expected answer:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "Correct answer is missing", "part.jme.answer too long": "Your answer is too long.", "part.jme.answer too short": "Your answer is too short.", "part.jme.answer invalid": "Your answer is not a valid mathematical expression.<br/>{{message}}.", "part.jme.marking.correct": "Your answer is numerically correct.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Your answer must contain: {{strings}}", "part.jme.must-have several": "Your answer must contain all of: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Your answer must not contain: {{strings}}", "part.jme.not-allowed several": "Your answer must not contain any of: {{strings}}", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "Your answer is within the allowed range, but decimal numbers are not allowed.", "part.numberentry.correct except fraction": "Your answer is within the allowed range, but fractions are not allowed.", "part.numberentry.answer invalid": "You did not enter a valid number.", "part.numberentry.answer not integer": "Your answer is invalid. You must enter a whole number, not a decimal.", "part.numberentry.answer not integer or decimal": "Your answer is invalid. You must enter a whole number or a decimal.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "significant figure", "part.numberentry.precision type.sigfig_plural": "significant figures", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "part.numberentry.give your answer to precision_0": "Round your answer to the nearest integer.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error, {{error}}", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "You selected the wrong number of choices.", "part.mcq.no choices selected": "No choices selected.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "You chose the correct answer.", "part.matrix.answer invalid": "Your answer is not valid.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.empty": "You have not entered an answer.", "part.matrix.empty cell": "One or more of the cells in your answer is empty.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>Answer {{index}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Question {{number}}: {{message}}", "question.preamble.error": "Error in preamble: {{message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "Question {{number}}", "question.submit part": "Check answer", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.advice": "Advice", "question.no such part": "Can't find part {{path}}", "question.can not submit": "Can not submit answer - check for errors.", "question.answer submitted": "Answer submitted", "question.unsubmitted changes": "You have made a change to your answer but not submitted it. Please check your answer and then press the <strong>Submit answer</strong> button.", "question.unsubmitted changes_plural": "You have made changes to your answers but not submitted them. Please check your answers to each part and then press the <strong>Submit all parts</strong> button.", "question.score feedback.show": "Show feedback", "question.score feedback.hide": "Hide feedback", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score total": "{{marksString}}. Answered.", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.answered": "Answered.", "question.score feedback.unanswered": "Unanswered.", "question.score feedback.correct": "Your answer is correct", "question.score feedback.partial": "Your answer is partially correct", "question.score feedback.wrong": "Your answer is incorrect", "question.selector.unsubmitted changes": "Answer changed.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Time remaining:", "xml.could not load": "Couldn't load an XML document: {{message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "Error initialising SCORM protocol: {{message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{message}}", "scorm.error loading question": "Error loading question {{number}}: {{message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark": "mark", "mark_plural": "marks", "was": "was", "was_plural": "were", "part": "part", "gap": "gap", "step": "step"}}, "af-za": {"translation": {"page.loading": "Word gelaai...", "page.saving": "<p>Stoor tans.</p>\n<p>Dit kan 'n paar sekondes neem.</p>", "mathjax.math processing error": "\"{{-message}}\" wanneer <code>{{expression}}</code> na tex verwerk word", "die.numbas failed": "Numbas het gefaal", "die.sorry": "Jammer, Numbas het 'n fout te\u00ebgekom wat beteken dit kan nie voortgaan nie. Hieronder is 'n beskrywing van die fout.", "die.error": "Fout", "modal.ok": "OK", "modal.cancel": "Kanselleer", "exam.exam name": "Eksamen Naam:", "exam.random seed": "Sessie ID:", "exam.student name": "Student se naam:", "exam.number of questions": "Aantal vrae:", "exam.marks available": "Punte beskikbaar:", "exam.pass percentage": "Slaagpersentasie:", "exam.time allowed": "Tyd toegelaat:", "exam.passed": "Geslaag", "exam.failed": "Gedruip", "exam.review header": "Hersien: ", "frontpage.start": "Begin", "suspend.paused header": "Onderbreek", "suspend.exam suspended": "Die eksamen is opgeskort. Druk <em>Hervat</em> om voort te gaan.", "suspend.you can resume": "Jy sal hierdie sessie kan hervat wanneer jy hierdie aktiwiteit die volgende keer begin.", "suspend.resume": "Hervat", "result.exit": "Verlaat eksamen", "result.print": "Druk hierdie resultate-opsomming", "result.exam summary": "Eksamenopsomming", "result.performance summary": "Prestasie-opsomming", "result.exam start": "Eksamen begin:", "result.exam stop": "Eksamen eindig:", "result.time spent": "Tyd spandeer:", "result.questions attempted": "Vrae aangepak:", "result.score": "Telling:", "result.result": "Resultaat:", "result.question number": "Vraagnommer", "result.question score": "Telling", "result.question review title": "Hersien hierdie vraag", "result.click a question to review": "Klik op 'n vraagnommer om te sien hoe jou antwoorde gemerk is en, waar beskikbaar, volledige oplossings.", "end.exam has finished": "Die eksamen is klaar. Jy kan hierdie venster nou toemaak.", "control.confirm leave": "Jy het nie die eksamen voltooi nie.", "control.not all questions answered": "Jy het nie elke vraag in hierdie eksamen voltooi nie.", "control.not all questions submitted": "Jy het veranderinge aan een of meer antwoorde aangebring, maar dit nie ingedien nie. Kontroleer asseblief dat elke vraag ingedien is.", "control.confirm end": "Is jy seker jy wil die eksamen be\u00ebindig? Nadat jy die eksamen voltooi het, sal jy nie enige van jou antwoorde kan verander nie.", "control.confirm regen": "Wil jy waardes in hierdie vraag weer skommel? As jy OK klik, sal al jou antwoorde en punte vir die huidige vraag verlore gaan.", "control.confirm reveal": "Wil jy die antwoord op hierdie vraag ontbloot? Enige punte wat jy tot dusver ontvang het, sal vasgemaak word en jy sal nie later hierdie vraag kan beantwoord nie.", "control.proceed anyway": "Gaan in elk geval voort?", "control.regen": "Probeer 'n soortgelyke vraag", "control.submit answer": "Dien antwoord in", "control.submit all parts": "Dien al die afdelings in", "control.submit again": "Dien weer in", "control.submit": "Dien in", "control.previous": "Vorige", "control.next": "Volgende", "control.advice": "Raad", "control.reveal": "Wys antwoorde", "control.total": "Totaal", "control.pause": "Skort op", "control.end exam": "Eindig Eksamen", "control.back to results": "Gaan terug na resultate", "display.part.jme.error making maths": "Kon nie wiskunde vertoon nie", "exam.xml.bad root": "Wortelelement van eksamen-XML moet 'eksamen' wees", "exam.changeQuestion.no questions": "Hierdie eksamen bevat geen vrae nie! Gaan die .exam-l\u00eaer na vir foute.", "feedback.you were awarded": "<strong>{{count,niceNumber}}</strong> $t(mark) is aan jou toegeken.", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) weggeneem.", "jme.shunt.not enough arguments": "Nie genoeg argumente vir bewerking <code>{{op}}</code> nie", "jme.shunt.no left bracket in function": "Geen bypassende linkerhakie in funksietoepassing of tupel nie", "jme.shunt.no left square bracket": "Geen bypassende linkerhakie nie", "jme.shunt.no left bracket": "Geen bypassende linkerhakie nie", "jme.shunt.no right bracket": "Geen bypassende regterhakie nie", "jme.shunt.no right square bracket": "Geen bypassende regter, vierkantige hakie om die lys te sluit nie", "jme.shunt.missing operator": "Uitdrukking kan nie ge\u00ebvalueer word nie -- 'n operator ontbreek.", "jme.typecheck.function maybe implicit multiplication": "Funksie <code>{{name}}</code> is nie gedefinieer nie. Bedoel jy <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Funksie <code>{{op}}</code> is nie gedefinieer nie. Is <code>{{op}}</code> 'n veranderlike, en het jy bedoel <code>{{suggestion}}*(...)</code> bedoel?", "jme.typecheck.op not defined": "Operasie '{{op}}' is nie gedefinieer nie.", "jme.typecheck.no right type definition": "Geen definisie van '{{op}}' met korrekte tipe gevind nie.", "jme.typecheck.no right type unbound name": "Veranderlike <code>{{name}}</code> is nie gedefinieer nie.", "jme.typecheck.map not on enumerable": "<code>afbeeldings</code>-bewerking moet oor 'n lys of 'n reeks werk, nie {{type}} nie", "jme.evaluate.undefined variable": "Veranderlike {{name}} is ongedefinieerd", "jme.thtml.not html": "Het 'n nie-HTML-waarde na die THTML-konstruktor deurgegee.", "jme.func.switch.no default case": "Geen versteksaak vir Switch-stelling nie", "jme.func.listval.invalid index": "Ongeldige lysindeks {{index}} op lys van grootte {{size}}", "jme.func.listval.not a list": "Voorwerp is nie onderskryfbaar nie", "jme.func.matrix.invalid row type": "Kan nie 'n matriks konstrueer uit rye van tipe {{type}} nie", "jme.func.except.continuous range": "Kan nie die 'except'-operator op kontinue reekse gebruik nie.", "jme.matrix.reports bad size": "Matriks rapporteer sy grootte verkeerd - daar moet 'n fout in konstruktor funksie wees", "jme.texsubvars.no right bracket": "Geen ooreenstemmende <code>]</code> in {{op}} argumente.", "jme.texsubvars.missing parameter": "Ontbrekende parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Geen ooreenstemmende <code>}</code> in {{op}}", "jme.user javascript.error": "Fout in gebruikergedefinieerde javascript-funksie <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Kon nie funksie <code>{{name}}</code> skep nie: {{-message}}", "jme.variables.syntax error in function definition": "Sintaksisfout in funksiedefinisie", "jme.variables.variable not defined": "Veranderlike <code>{{name}}</code> is nie gedefinieer nie.", "jme.variables.empty definition": "Definisie van veranderlike <code>{{name}}</code> is leeg.", "jme.variables.circular reference": "Veranderlike se definisie verwys na self in <code>{{name}}</code>", "jme.variables.error computing dependency": "Fout met die berekening van verwysde veranderlike <code>{{name}}</code>", "jme.variables.error evaluating variable": "Kon nie veranderlike {{name}} evalueer nie: {{-message}}", "jme.variables.question took too many runs to generate variables": "'n Geldige stel vraagveranderlikes is nie betyds gegenereer nie.", "jme.display.unknown token type": "Kan nie teken tipe {{type}} in tex omskakel nie", "jme.display.collectRuleset.no sets": "Geen versamelings is aan collectRuleset gegee nie!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} is nie gedefinieer nie", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree moet 'n afbakening h\u00ea", "math.precround.complex": "Kan nie tot 'n komplekse getal desimale plekke afrond nie", "math.siground.complex": "Kan nie tot 'n komplekse getal beduidende syfers afrond nie", "math.combinations.complex": "Kan nie kombinasies van komplekse getalle bereken nie", "math.permutations.complex": "Kan nie permutasies van komplekse getalle bereken nie", "math.gcf.complex": "Kan nie GGD van komplekse getalle bereken nie", "math.lcm.complex": "Kan nie KGV van komplekse getalle bereken nie", "math.lt.order complex numbers": "Kan nie komplekse getalle orden nie", "math.choose.empty selection": "Le\u00eb seleksie gegee aan lukrake funksie", "matrixmath.abs.non-square": "Kan nie die determinant van 'n nie-vierkantige matriks bereken nie.", "matrixmath.abs.too big": "Jammer, kan nog nie die determinant van 'n matriks groter as 3x3 bereken nie.", "matrixmath.mul.different sizes": "Kan nie matrikse van verskillende groottes vermenigvuldig nie.", "vectormath.cross.not 3d": "Kan slegs die kruisproduk van 3-dimensionele vektore bereken.", "vectormath.dot.matrix too big": "Kan nie skalaarproduk van 'n matriks bereken wat nie $1 \\times N$ of $N \\times 1$ is nie.", "vectormath.cross.matrix too big": "Kan nie kruisproduk van 'n matriks bereken wat nie $1 \\times N$ of $N \\times 1$ is nie.", "part.with steps answer prompt": "Antwoord: ", "part.script.error": "Fout in deel {{path}} gepasmaakte skrif {{script}}: {{-message}}", "part.marking.steps no matter": "Omdat jy volpunte vir die afdeling gekry het, word jou antwoorde in die stappe afdeling nie getel nie.", "part.marking.used variable replacements": "Hierdie deel is gemerk deur jou antwoorde op vorige afdelings te gebruik.", "part.marking.variable replacement part not answered": "Jy moet eers {{part}} antwoord.", "part.marking.resubmit because of variable replacement": "Hierdie deel se nasien hang van jou antwoorde in ander afdelings af, wat jy verander het. Dien hierdie deel weer in om jou punt op te dateer.", "part.marking.not submitted": "Geen antwoord ingedien nie.", "part.marking.did not answer": "Jy het nie hierdie vraag beantwoord nie.", "part.marking.nothing entered": "Jy het nie 'n antwoord ingevoer nie.", "part.marking.incorrect": "Jou antwoord is verkeerd.", "part.marking.correct": "Jou antwoord is korrek.", "part.marking.uncaught error": "Nasienfout: {{-message}}", "part.correct answer": "Antwoord wat verwag is:", "part.missing type attribute": "{{part}}: Ontbrekende afdelingtipe-kenmerk", "part.unknown type": "{{part}}: Onbekende afdelingtipe {{type}}", "part.setting not present": "Eienskap '{{property}}' nie gestel nie", "part.jme.answer missing": "Korrekte antwoord ontbreek", "part.jme.answer too long": "Jou antwoord is te lank.", "part.jme.answer too short": "Jou antwoord is te kort.", "part.jme.answer invalid": "Jou antwoord is nie 'n geldige wiskundige uitdrukking nie.<br/>{{-message}}.", "part.jme.marking.correct": "Jou antwoord is numeries korrek.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Jou antwoord moet die volgende bevat: {{strings}}", "part.jme.must-have several": "Jou antwoord moet al die volgende bevat: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Jou antwoord moet die volgende bevat nie: {{strings}}", "part.jme.not-allowed several": "Jou antwoord moet nie enige van die volgende bevat nie: {{strings}}", "part.jme.unexpected variable name": "Jou antwoord is ge\u00efnterpreteer om die onverwagte veranderlike naam <code>{{name}}</code> te gebruik.", "part.jme.unexpected variable name suggestion": "Jou antwoord is ge\u00efnterpreteer om die onverwagte veranderlike naam <code>{{name}}</code> te gebruik. Bedoel jy <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Vertoonantwoord ontbreek", "part.patternmatch.correct except case": "Jou antwoord is korrek, behalwe vir die kas.", "part.numberentry.correct except decimal": "Jou antwoord is binne die toegelate reeks, maar desimale getalle word nie toegelaat nie.", "part.numberentry.correct except fraction": "Jou antwoord is binne die toegelate reeks, maar breuke word nie toegelaat nie.", "part.numberentry.answer invalid": "Jy het nie 'n geldige getal ingevoer nie.", "part.numberentry.answer not integer": "Jou antwoord is ongeldig. Jy moet 'n heelgetal invoer, nie 'n desimale getal nie.", "part.numberentry.answer not integer or decimal": "Jou antwoord is ongeldig. Jy moet 'n heelgetal of 'n desimale getal invoer.", "part.numberentry.zero sig fig": "Hierdie deel is opgestel om die student se antwoord af te rond tot nul beduidende syfers, wat geen betekenis het nie.", "part.mcq.options def not a list": "Die uitdrukking wat die {{properties}} definieer, is nie 'n lys nie.", "part.mcq.marking matrix string empty": "Die gepasmaakte merkmatriks-uitdrukking is leeg.", "part.mcq.choices missing": "Definisie van keuses ontbreek", "part.mcq.matrix not a number": "Afdeling {{part}} se merkmatrikssel ({{row}},{{column}}) evalueer nie na 'n getal nie", "part.mcq.wrong number of choices": "Jy het die verkeerde aantal keuses gekies.", "part.mcq.no choices selected": "Geen keuses gemaak nie.", "part.mcq.matrix not a list": "Merkmatriks, gedefinieer deur JME-uitdrukking, is nie 'n lys nie, maar dit behoort te wees.", "part.mcq.matrix wrong type": "Element van ongeldige tipe '{{type}}' word in merkmatriks gebruik.", "part.mcq.matrix mix of numbers and lists": "Mengsel van getalle en lyste word in die merkmatriks gebruik.", "part.mcq.matrix wrong size": "Merkmatriks is die verkeerde grootte.", "part.mcq.correct choice": "Jy het 'n korrekte antwoord gekies.", "part.matrix.answer invalid": "Jou antwoord is nie geldig nie.", "part.matrix.invalid cell": "Een of meer van die selle in jou antwoord is leeg of ongeldig.", "part.matrix.some incorrect": "Een of meer van die selle in jou antwoord is verkeerd, maar jy het punte vir die res gekry.", "part.matrix.empty": "Jy het nie 'n antwoord ingevoer nie.", "part.matrix.empty cell": "Een of meer van die selle in jou antwoord is leeg.", "part.matrix.size mismatch": "Die vraagskrywer het nie die student toegelaat om die afmetings van hul antwoord te bepaal nie, maar die korrekte antwoord is {{correct_dimensions}} terwyl die antwoordinvoer {{input_dimensions}} is", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Afdeling het nie die <code>{{name}}</code>-metode ge\u00efmplementeer nie.", "question.loaded name mismatch": "Kan nie hierdie poging hervat nie - die pakket het sedert die laaste sessie verander.", "question.error": "Antwoord {{number}}: {{-message}}", "question.preamble.error": "Fout in aanhef: {{-message}}", "question.preamble.syntax error": "Sintaksisfout in aanhef", "question.unsupported part type": "Ongesteunde afdelingtipe", "question.header": "Vraag {{number}}", "question.submit part": "Dien afdeling in", "question.show steps": "Wys stappe", "question.show steps penalty": "Jy sal <strong>{{count,niceNumber}}</strong> $t(mark) verloor.", "question.show steps no penalty": "Jou telling sal nie be\u00efnvloed word nie.", "question.show steps already penalised": "Jy het reeds stappe gewys. Jy kan hulle weer wys sonder enige verdere strafpunte.", "question.hide steps": "Verberg stappe", "question.hide steps no penalty": "Jou telling sal nie be\u00efnvloed word nie.", "question.advice": "Raad", "question.no such part": "Kan nie deel {{path}} vind nie", "question.can not submit": "Kan nie antwoord indien nie - kyk vir foute.", "question.answer submitted": "Antwoorde ingedien", "question.score feedback.show": "Wys terugvoer", "question.score feedback.hide": "Verberg terugvoer", "question.score feedback.answered": "Beantwoord", "question.score feedback.unanswered": "Ongeantwoord", "question.score feedback.correct": "Jou antwoord is reg", "question.score feedback.partial": "Jou antwoord is gedeeltelik reg", "question.score feedback.wrong": "Jou antwoord is verkeerd", "question.selector.unsubmitted changes": "Oningediende veranderinge.", "timing.no accumulator": "geen tydsberekening akkumulator {{name}}", "timing.time remaining": "Oorblywende tyd:", "xml.could not load": "Kon nie 'n XML-dokument laai nie: {{-message}}", "xml.property not number": "Eienskap {{name}} moet 'n getal wees, maar is nie ({{value}}), in nodus {{element}}", "xml.property not boolean": "Eienskap {{name}} moet 'n boolean wees, maar is nie ({{value}}), in nodus {{element}}", "scorm.error initialising": "Fout met inisiasie van SCORM-protokol: {{-message}}", "scorm.failed save": "<p>Die versoek om data op die bediener te stoor het misluk. Druk <b>OK</b> om weer te probeer.</p>\n<p>As jy hierdie boodskap herhaaldelik kry, toets jou internetverbinding of gebruik 'n ander rekenaar. Jou voorheen ingediende antwoorde is suksesvol gestoor en sal herstel word as jy hierdie sessie op 'n ander rekenaar hervat.</p>\n<p>As hierdie boodskap aanhoudend verskyn en jy nie <em>enige</em> antwoorde kan stoor nie, kontak asseblief jou dosent of onderwyser.</p>", "scorm.no exam suspend data": "Kon nie hervat nie: geen eksamenopskortingsdata nie.", "scorm.error loading suspend data": "Kon nie opskortdata laai nie: {{-message}}", "scorm.error loading question": "Kon nie vraag {{number}} laai nie: {{-message}}", "scorm.no question suspend data": "Geen vraagopskort data nie", "scorm.error loading part": "Kon nie deel {{part}} laai nie: {{-message}}", "scorm.no part suspend data": "Geen afdeling-opskortdata nie", "util.product.non list": "Het 'n nie-lys na <code>Numbas.util.product</code> deurgegee", "mark": "punt", "was": "was", "part": "afdeling", "gap": "gaping", "step": "stap", "jme.substituteTree.undefined variable": "Ongedefinieerde veranderlike: <code>{{name}}</code>", "jme.user javascript.returned undefined": "Gebruikergedefinieerde javascript-funksie <code>{{name}}</code> het <code>undefined</code> teruggestuur.", "part.marking.steps change": "<strong>{{count,niceNumber}}</strong> $t(mark) is aan jou toegeken vir jou antwoorde op die stappe.", "part.marking.total score": "Jy het <strong>{{count,niceNumber}}</strong> $t(mark) vir hierdie afdeling gekry.", "part.numberentry.precision type.dp": "desimale plek", "part.numberentry.precision type.dp_plural": "desimale plekke", "part.numberentry.precision type.sigfig": "beduidende syfer", "part.numberentry.precision type.sigfig_plural": "beduidende syfers", "part.numberentry.give your answer to precision": "Rond jou antwoord af tot {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Jy het jou antwoord verander, maar dit nie ingedien nie. Kontroleer asseblief jou antwoord en druk dan die <strong>Dien antwoord in</strong>-knoppie.", "question.unsubmitted changes_plural": "Jy het jou antwoorde verander, maar dit nie ingedien nie. Kontroleer asseblief jou antwoorde vir elke afdeling en druk dan die <strong>Dien alle dele in</strong>-knoppie.", "util.equality not defined for type": "Gelykgeid nie gedefinieer vir tipe {{type}} nie", "mark_plural": "punte", "was_plural": "was", "die.script not loaded": "Numbas kon nie begin nie omdat die l\u00eaer <code>{{file}}</code> nie gelaai is nie. Maak seker dat dit by <code>scripts.js</code> ingesluit is.", "math.combinations.n less than zero": "Kan nie kombinasies bereken nie: n is minder as nul", "math.combinations.k less than zero": "Kan nie kombinasies bereken nie: k is minder as nul", "math.combinations.n less than k": "Kan nie kombinasies bereken nie: n is minder as k", "math.permutations.n less than zero": "Kan nie permutasies bereken nie: n is minder as nul", "math.permutations.k less than zero": "Kan nie permutasies bereken nie: k is minder as nul", "math.permutations.n less than k": "Kan nie permutasies bereken nie: n is minder as k", "part.numberentry.give your answer to precision_0": "Rond jou antwoord af tot die naaste heelgetal.", "mathjax.error": "MathJax verwerkingsfout: {{-message}}", "mathjax.error with context": "MathJax-verwerkingsfout in {{-context}}: {{-message}}", "exam.introduction": "Eksamen inleiding", "exam.feedback": "Eksamen terugvoerboodskap", "jme.tokenise.keypair key not a string": "Woordeboeksleutel moet 'n string wees, nie {{type}} nie.", "jme.shunt.list mixed argument types": "Kan nie {{mode}} ontleed nie: mengsel van woordeboek- en lyselemente", "jme.func.listval.key not in dict": "Woordeboek bevat nie die sleutel <code>{{key}}</code> nie", "part.prompt": "vraagteks", "part.feedback": "terugvoer", "part.numberentry.answer not reduced": "Jou antwoord is nie tot die kleinste terme vereenvoudig nie.", "part.numberentry.give your answer as a reduced fraction": "Vereenvoudig jou antowwrd tot die kleinste terme.", "part.numberentry.negative decimal places": "Hierdie deel is opgestel om die student se antwoord af te rond tot 'n negatiewe aantal desimale plekke, wat geen betekenis het nie.", "part.mcq.choices": "keuses", "part.mcq.answers": "antwoorde", "part.mcq.matrix cell empty": "Deel {{part}} merkmatrikssel ({{row}},{{column}}) is leeg", "part.mcq.matrix jme error": "Deel {{part}} merkmatrikssel ({{row}},{{column}}) gee 'n JME-fout: {{-error}}", "question.statement": "Stelling", "ruleset.circular reference": "Selfverwysing in definisie van re\u00eblstel <code>{{name}}</code>", "ruleset.set not defined": "Re\u00eblstel {{name}} is nie gedefinieer nie", "jme.evaluate.no scope given": "Numbas.jme.evaluate moet 'n Afbakening gegee word", "answer.number.not a number": "Jou antwoord is nie 'n geldige nommer nie.", "answer.number.fractions not allowed": "Jy mag nie 'n breuk invoer nie.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Jy mag nie breuke invoer nie.", "answer.matrix.some cell not a number": "Een of meer van die selle in jou antwoord is nie 'n geldige getal nie.", "exam.enter password": "Wagwoord:", "exam.password.correct": "Hierdie wagwoord is korrek. Jy mag die eksamen begin.", "exam.password.incorrect": "Hierdie wagwoord is verkeerd.", "frontpage.scorm.lms not connected": "Hierdie eksamen verloop op sy eie. Jou antwoorde en punte sal nie gestoor word nie!", "result.question review": "Hersien", "control.confirm regen no marks": "Wil jy hierdie vraag se getalle weer kies?", "control.confirm reveal no marks": "Wil jy die antwoord op hierdie vraag blootstel?", "jme.tokenise.invalid near": "Ongeldige uitdrukking: <code>{{expression}}</code> by posisie {{position}} naby <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Ongeldige voorwerp deurgegee in getalkonstruktor.", "jme.subvars.null substitution": "Le\u00eb veranderlike vervanging: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Die datatipe {{type}} is reeds geregistreer en kan dus nie weer geregistreer word nie.", "jme.type.no cast method": "Kan nie outomaties van {{from}} na {{to}} omskakel nie.", "jme.display.simplifyTree.empty expression": "Uitdrukking is leeg", "jme.display.simplifyTree.stuck in a loop": "Vereenvoudiger is in 'n lus vasgevang: <code>{{expr}}</code>", "math.niceNumber.undefined": "Het 'n getal verwag, maar het <code>undefined</code> gekry", "math.rangeToList.zero step size": "Kan nie 'n reeks met stapgrootte nul na 'n lys omskakel nie.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Jy het die stappe ontbloot.", "part.marking.maximum scaled down": "Die maksimum punt wat jy vir hierdie afdeling kan kry is <strong>{{count,niceNumber}}</strong> $t(punt). Jou punte sal dienooreenkomstig afgeskaal word.", "part.marking.minimum score applied": "Die minimum telling vir hierdie deel is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "Die aksimum telling vir hierdie deel is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "Daar was 'n fout in hierdie deel se merkalgoritme. Rapporteer dit asseblief. {{-message}}", "part.marking.no result after replacement": "Hierdie deel kon nie nagesien word deur jou antwoorde by vorige afdelings te gebruik nie.", "part.marking.missing required note": "Die merkalgoritme definieer nie die nota <code>{{note}}</code> nie", "marking.apply.not a list": "Die eerste argument om <code>toe te pas</code> moet 'n lys wees, en is nie", "marking.apply marking script.script not found": "Merkskrif <code>{{name}}</code> nie gevind nie", "marking.note.compilation error": "Kon nie nota <code>{{name}}</code> saamstel nie: {{-message}}", "marking.note.error evaluating note": "Kon nie nota <code>{{name}}</code> evalueer nie - {{-message}}", "marking.note.invalid definition": "Ongeldige notadefinisie: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Jy kort dalk 'n dubbelpunt na die naam en beskrywing", "marking.note.invalid definition.description missing closing bracket": "Jy kort dalk 'n sluitingshakie", "marking.note.empty expression": "Die nota <code>{{name}}</code> is leeg.", "marking.script.error parsing notes": "Kon nie merkskrif ontleed nie: {{- message}}", "part.feedback out of date": "Hierdie terugvoer is gebaseer op jou laaste ingediende antwoord. Dien jou veranderde antwoord in om opgedateerde terugvoer te kry.", "part.jme.invalid value generator expression": "Ongeldige waardegenerator-uitdrukking vir veranderlike <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Jy het 'n verkeerde antwoord gekies.", "part.matrix.not all cells same precision": "Jy het nie elke sel in jou antwoord met dieselfde beduidende syfers gegee nie.", "part.gapfill.error marking gap": "Kon nie {{name}} merk nie: {{-message}}", "part.custom.empty setting": "Geen waarde gegee nie.", "part.custom.unrecognised input type": "Onherkende instellingtipe <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Fout met die evaluering van invoeropsie <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definisie van invoeropsie <code>{{option}}</code> ontbreek.", "part.custom.error evaluating setting": "Kon nie instelling <code>{{setting}}</code> evalueer: {{-error}}", "question.error creating question": "Kon nie vraag {{number}} skep nie: {{-message}}", "question.score feedback.not marked": "Nie gemerk nie", "question.score feedback.partially answered": "Deels beantwoord", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Punt: {{scoreString}}", "question.score feedback.score total actual": "Punt: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Fout in definisie van veranderlike <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Kon nie die uitbreiding <code>{{name}}</code> laai nie.", "control.toggle navigation menu": "Skakel die navigasiekieslys om", "part.input title": "Antwoord vir afdeling {{name}}", "part.correct answer title": "Verwagte antwoord vir afdeling {{name}}", "part.jme.must-match.failed": "Jou antwoord is nie in die regte formaat nie.", "control.submit part.confirm remove next parts": "<p>Een of meer opeenvolgende afdelings hang af van jou antwoord in hierdie deel. Deur hierdie deel weer in te dien, sal daardie afdelings ongeldig word en hulle word uit die vraag verwyder. Dit kan nie terug verander word nie.</p>\n<p>Wil jy hierdie deel weer indien?</p>", "control.back to menu": "Gaan terug na die kieslys", "display.error making html": "Kon nie HTML skep in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "'n Vraagveranderlike het nie 'n naam nie.", "jme.calculus.unknown derivative": "Weet nie hoe om <code>{{tree}}</code> te differensieer nie", "math.order complex numbers": "Kan nie komplekse getalle orden nie", "menu.choose a question": "Kies 'n vraag.", "part.choose next part.answered": "Wat vil jy volgende doen?", "part.choose next part.unanswered": "Of jy kan:", "part.choose next part.will be locked": "(Hierdie afdeling sal gesluit wees)", "part.reached dead end": "Daar is niks meer om verder te doen nie.", "part.next part.penalty amount": "(verloor {{count}} $t(mark))", "part.marking.counts towards objective": "Hierdie afdeling tel vir die doelwit <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Jou antwoord is ongeldig. Jy moet 'n heelgetal, 'n desimale getal of 'n breuk invoer.", "question": "Vraag", "question.progress": "Vordering met vraag:", "question.score feedback.unattempted": "Nie gepoog nie", "question.score feedback.attempted": "Gepoog", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Doelwitte", "question.penalties": "Strafpunte", "question.back to previous part": "Gaan terug na die vorige deel", "end.print": "Druk jou eksamenafskrif", "math.shuffle_together.lists not all the same length": "Nie alle lyste is ewe lank nie.", "jme.parse signature.invalid signature string": "Ongeldige funksie handtekeningstring: {{str}}", "part.custom.expected answer has wrong type": "Die verwagte antwoord vir hierdie deel het die verkeerde tipe. Dit moet <code>{{shouldbe}}</code> wees.", "part.custom.input option has wrong type": "Die antwoordinvoerstelling <code>{{option}}</code> het die verkeerde tipe. Dit moet <code>{{shouldbe}}</code> wees.", "matrix input.size control legend": "Grootte", "matrix input.rows": "Rye", "matrix input.columns": "Kolomme", "part.jme.error checking numerically": "Kon nie jou antwoord numeries nagaan nie: {{-message}}", "part.gapfill.cyclic adaptive marking": "Daar is 'n siklus in die aanpasbare merkswyse vir hierdie deel: <strong>{{name1}}</strong> maak staat op <strong>{{name2}}</strong>, wat uiteindelik staatmaak op <strong>{{name1} }</strong>.", "modal.style.background colour": "Agtergrond kleur", "modal.style.text colour": "Teks kleur", "modal.style.text size": "Teks grootte", "modal.style.explanation": "Gebruik hierdie kontroles om die voorkoms van die eksamen te verander.", "modal.style.reset to defaults": "Stel terug na verstekwaardes", "modal.style.text size preview": "Die meeste teks sal so groot wees.", "control.style options": "Vertoon opsies", "part.marking.partially correct": "Jou antwoord is gedeeltelik korrek.", "part.marking.error in adaptive marking": "Daar was 'n fout in die aanpasbare merkswyse vir hierdie afdeling. Rapporteer dit asseblief. {{-message}}", "page.skip to content": "Slaan oor na inhoud", "result.learning objective": "Leerdoelwit", "jme.interpreted as": "ge\u00efnterpreteer as", "jme.script.note.compilation error": "Kon nie nota <code>{{name}}</code> saamstel nie: {{-message}}", "jme.script.note.error evaluating note": "Kon nie nota <code>{{name}}</code> evalueer nie - {{-message}}", "jme.script.note.invalid definition": "Ongeldige notadefinisie: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "Dalk ontbreek 'n dubbelpunt na die naam en beskrywing", "jme.script.note.invalid definition.description missing closing bracket": "Jy kort dalk 'n sluitingshakie", "jme.script.note.empty expression": "Die nota <code>{{name}}</code> is leeg.", "jme.script.error parsing notes": "Kon nie merkskrif ontleed nie: {{- message}}", "matrix input.cell label": "Ry {{row}}, kolom {{column}}", "control.move to next question": "Gaan na die volgende vraag", "diagnostic.use retry": "Gebruik een herprobeerslag en doen hierdie onderwerp weer.", "diagnostic.move to next topic": "Gaan aan na die volgende onderwerp.", "diagnostic.next step question": "Wat wil jy volgende doen?", "diagnostic.now assessing topic": "{{current_topic}} word nou assesseer", "diagnostic.one retry left": "Jy het nog 1 herprobeerslag oor", "diagnostic.retries left": "Jy het nog {{retries}} herprobeerslae oor", "diagnostic.percentage completed": "Jy het <strong>{{percentage}}%</strong> van die toets voltooi.", "diagnostic.test is over": "Die toets is verby.", "diagnostic.passed all lo": "Jy het alle leerdoelwitte geslaag.", "diagnostic.more work on lo": "Jy moet nog aan die volgende leerdoelwitte werk: {{los}}.", "diagnostic.move to next question in topic": "Beweeg na die volgende vraag in die onderwerp.", "diagnostic.complete": "Voltooi!", "diagnostic.studying topic": "Bestudeer {{topic}}", "display.answer widget.unknown widget type": "Die antwoord legstuktipe <code>{{name}}</code> word nie herken nie.", "jme.shunt.expected argument before comma": "Het verwag om iets tussen die openinghakie en die komma te sien", "part.waiting for pre submit": "Jou antwoord word gemerk. Wag asseblief.", "diagnostic.end test": "Be\u00ebindig die toets.", "page.no stylesheet loaded": "Die bladsy se stylbladl\u00eaer is nie gelaai nie.", "modal.confirm": "Bevestig", "modal.alert": "Pasop", "suspend.resumed header": "Poging hervat", "jme.vector.value not an array of numbers": "Het probeer om 'n vektor te konstrueer deur 'n waarde te gebruik wat nie 'n reeks van getalle is nie.", "jme.matrix.value not the right type": "Het probeer om 'n vektor te konstrueer deur 'n waarde van die verkeerde tipe te gebruik.", "jme.subvars.html inserted twice": "'n HTML-waarde is twee keer ingebed. Oorweeg om 'n funksie te definieer om 'n nuwe waarde te genereer elke keer as dit gebruik word.", "jme.variables.invalid function language": "Die taal <code>{{language}}</code> is nie geldig nie.", "jme.variables.duplicate definition": "Daar is meer as een definisie van die veranderlike <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "Die grootte van die partisie moet tussen 1 en {{n}} wees.", "part.marking.parameter already in scope": "Daar is 'n veranderlike genaamd <code>{{name}}</code>, wat ook die naam van 'n merkparameter is. Hernoem asseblief die veranderlike.", "part.marking.adaptive variable replacement refers to self": "Hierdie deel verwys na self in 'n veranderlike vervanging vir aanpasbare merk.", "part.marking.adaptive variable replacement refers to nothing": "Hierdie deel bevat 'n ongeldige veranderlike vervanging vir aanpasbare merk.", "part.numberentry.display answer wrong type": "Die vertoonantwoord vir hierdie deel is 'n waarde van tipe <code>{{got_type}}</code>, maar moet 'n <code>{{want_type}}</code> wees.", "part.matrix.invalid type in prefilled": "Daar is 'n ongeldige waarde van tipe <code>{{n}}</code> in die reeks voorafgevulde selle.", "diagnostic.make a choice": "Maak 'n keuse", "matrixmath.not square": "Hierdie bewerking werk slegs op 'n vierkantige matriks.", "matrixmath.not invertible": "Hierdie bewerking werk slegs op 'n omkeerbare matriks.", "matrixmath.gauss-jordan elimination.not enough columns": "Daar moet ten minste soveel kolomme as rye wees.", "question.required extension not available": "Hierdie vraag vereis die uitbreiding <code>{{-extension}}</code> maar dit is nie beskikbaar nie.", "util.formatNumberNotation.unrecognised syntax": "Die getalformatering sintaksis <code>{{syntax}}</code> word nie herken nie.", "worksheet.number of exams to generate": "Aantal bladsye om te genereer", "worksheet.starting with id": "Begin met ID", "worksheet.show exam id": "Wys blad-ID?", "worksheet.page break between questions": "Bladsybreuke tussen vrae?", "worksheet.page margins": "Bladsykantlyne (mm)", "worksheet.text size": "Teksgrootte (pt)", "worksheet.generate": "Skep", "worksheet.generating exams": "Skep bladsye", "worksheet.sheet id": "Bladsy ID:", "worksheet.print single": "Druk hierdie bladsy", "worksheet.print several": "Deuk hierdie bladsye", "worksheet.answer sheets": "Antwoordbladsye", "worksheet.question sheets": "Vraebladsye", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Keer terug na die vraag", "control.show introduction": "Inleiding", "analysis.header": "Ontleed pogingdata", "analysis.help.upload files": "Laai pogingdata l\u00eaers op wat jou studente vir jou gegee het.", "analysis.help.file input label": "Kies studente pogingdata l\u00eaers, of sleep l\u00eaers na hierdie venster.", "analysis.table.total": "Eksamentotale", "analysis.table.question": "Eksamen- en vraagtotale", "analysis.table.all": "Alle besonderhede", "analysis.student name.anonymous": "Geen naam gegee nie", "analysis.expected": "Verwagte resultate", "analysis.start time": "Begin tyd", "analysis.maximum": "Maksimum punte", "analysis.file": "L\u00eaer", "analysis.download": "Laai af", "analysis.delete": "Vee uit", "analysis.view results": "Bekyk resultate", "analysis.upload files": "Laai l\u00eaers op", "analysis.upload more": "Laai meer l\u00eaers op", "analysis.attempt data": "Pogingdata", "analysis.select format": "Kies data om te wys", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "ko-kr": {"translation": {"page.loading": "\ubd88\ub7ec\uc624\ub294 \uc911...", "page.saving": "<p>\uc800\uc7a5 \uc911.</p>\n<p>\uc7a0\uc2dc\ub9cc \uae30\ub2e4\ub824 \uc8fc\uc2ed\uc2dc\uc624.</p>", "mathjax.math processing error": "\"{{message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "Numbas has failed", "die.sorry": "Sorry, Numbas has encountered an error which means it can't continue. Below is a description of the error.", "die.error": "\uc624\ub958", "modal.ok": "\ud655\uc778", "modal.cancel": "\ucde8\uc18c", "exam.exam name": "\uc2dc\ud5d8 \uc774\ub984:", "exam.random seed": "Session ID:", "exam.student name": "\ud559\uc0dd \uc774\ub984:", "exam.number of questions": "\uc9c8\ubb38\uc758 \uc218:", "exam.marks available": "Marks Available:", "exam.pass percentage": "\ud569\uaca9 \ud37c\uc13c\ud2b8:", "exam.time allowed": "Time Allowed:", "exam.passed": "\ud569\uaca9", "exam.failed": "\ubd88\ud569\uaca9", "exam.review header": "Review: ", "frontpage.start": "\uc2dc\uc791", "suspend.paused header": "\uc77c\uc2dc\uc815\uc9c0", "suspend.exam suspended": "\uc2dc\ud5d8\uc774 \uc77c\uc2dc\uc815\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. <em>\uacc4\uc18d\ud558\uae30</em>\ub97c \ub20c\ub7ec \uacc4\uc18d\ud558\uc2ed\uc2dc\uc624.", "suspend.you can resume": "You will be able to resume this session the next time you start this activity.", "suspend.resume": "\uc7ac\uac1c", "result.exit": "\uc2dc\ud5d8 \uc885\ub8cc", "result.print": "Print this results summary", "result.exam summary": "Exam Summary", "result.performance summary": "Performance Summary", "result.exam start": "Exam Start:", "result.exam stop": "Exam Stop:", "result.time spent": "Time Spent:", "result.questions attempted": "Questions Attempted:", "result.score": "\uc810\uc218:", "result.result": "\uacb0\uacfc:", "result.question number": "\ubb38\uc81c \ubc88\ud638", "result.question score": "\uc810\uc218", "result.question review title": "Review this question", "result.click a question to review": "Click on a question number to see how your answers were marked and, where available, full solutions.", "end.exam has finished": "The exam has finished. You may now close this window.", "control.confirm leave": "You haven't finished the exam.", "control.not all questions answered": "You have not completed every question in this exam.", "control.not all questions submitted": "You have made changes to one or more answers but not submitted them. Please check each question has been submitted.", "control.confirm end": "Are you sure you want to end the exam? After you end the exam, you will not be able to change any of your answers.", "control.confirm regen": "Would you like to re-randomise this question? If you click OK, all your answers and marks for the current question will be lost.", "control.confirm reveal": "Would you like to reveal the answer to this question? Any marks you have received so far will be locked and you will not be able to answer this question later.", "control.proceed anyway": "Proceed anyway?", "control.regen": "Try another question like this one", "control.submit answer": "Submit answer", "control.submit all parts": "Submit all parts", "control.submit again": "Submit again", "control.submit": "Submit", "control.previous": "Previous", "control.next": "Next", "control.advice": "Advice", "control.reveal": "Reveal answers", "control.total": "Total", "control.pause": "Pause", "control.end exam": "End Exam", "control.back to results": "Go back to results", "display.part.jme.error making maths": "Error making maths display", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "This exam contains no questions! Check the .exam file for errors.", "feedback.you were awarded": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid": "Invalid expression: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Not enough arguments for operation {{op}}", "jme.shunt.no left bracket in function": "No matching left bracket in function application or tuple", "jme.shunt.no left square bracket": "No matching left bracket", "jme.shunt.no left bracket": "No matching left bracket", "jme.shunt.no right bracket": "No matching right bracket", "jme.shunt.no right square bracket": "No matching right square bracket to end list", "jme.shunt.missing operator": "Expression can't be evaluated -- missing an operator.", "jme.typecheck.function maybe implicit multiplication": "Operation {{name}} is not defined. Did you mean <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Operation <code>{{op}}</code> is not defined. Is <code>{{op}}</code> a variable, and did you mean <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operation '{{op}}' is not defined.", "jme.typecheck.no right type definition": "No definition of '{{op}}' of correct type found.", "jme.typecheck.no right type unbound name": "Variable <code>{{name}}</code> is not defined.", "jme.typecheck.map not on enumerable": "<code>map</code> operation must work over a list or a range, not {{type}}", "jme.evaluate.undefined variable": "Variable {{name}} is undefined", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.matrix.invalid row type": "Can't construct a matrix from rows of type {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Missing parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{message}}", "jme.variables.syntax error in function definition": "Syntax error in function definition", "jme.variables.variable not defined": "Variable <code>{{name}}</code> is not defined.", "jme.variables.empty definition": "Definition of variable <code>{{name}}</code> is empty.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Can't compute GCF of complex numbers", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.lt.order complex numbers": "Can't order complex numbers", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Can't compute the determinant of a matrix which isn't square.", "matrixmath.abs.too big": "Sorry, can't compute the determinant of a matrix bigger than 3x3 yet.", "matrixmath.mul.different sizes": "Can't multiply matrices of different sizes.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "Answer: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "You must answer {{part}} first", "part.marking.resubmit because of variable replacement": "This part's marking depends on your answers to other parts, which you have changed. Submit this part again to update your score.", "part.marking.not submitted": "No answer submitted", "part.marking.did not answer": "You did not answer this question.", "part.marking.nothing entered": "You did not enter an answer.", "part.marking.incorrect": "Your answer is incorrect.", "part.marking.correct": "Your answer is correct.", "part.marking.uncaught error": "Error when marking {{part}}: {{message}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "Expected answer:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "Correct answer is missing", "part.jme.answer too long": "Your answer is too long.", "part.jme.answer too short": "Your answer is too short.", "part.jme.answer invalid": "Your answer is not a valid mathematical expression.<br/>{{message}}.", "part.jme.marking.correct": "Your answer is numerically correct.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Your answer must contain: {{strings}}", "part.jme.must-have several": "Your answer must contain all of: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Your answer must not contain: {{strings}}", "part.jme.not-allowed several": "Your answer must not contain any of: {{strings}}", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "Your answer is within the allowed range, but decimal numbers are not allowed.", "part.numberentry.correct except fraction": "Your answer is within the allowed range, but fractions are not allowed.", "part.numberentry.answer invalid": "You did not enter a valid number.", "part.numberentry.answer not integer": "Your answer is invalid. You must enter a whole number, not a decimal.", "part.numberentry.answer not integer or decimal": "Your answer is invalid. You must enter an integer or a decimal.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "You selected the wrong number of choices.", "part.mcq.no choices selected": "No choices selected.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "You chose the correct answer.", "part.matrix.answer invalid": "Your answer is not valid.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.empty": "You have not entered an answer.", "part.matrix.empty cell": "One or more of the cells in your answer is empty.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>Gap {{index}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Question {{number}}: {{message}}", "question.preamble.error": "Error in preamble: {{message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "Question {{number}}", "question.submit part": "Submit part", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.advice": "Advice", "question.no such part": "Can't find part {{path}}", "question.can not submit": "Can not submit answer - check for errors.", "question.answer submitted": "Answer submitted", "question.score feedback.show": "Show feedback", "question.score feedback.hide": "Hide feedback", "question.score feedback.ascore total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score total": "{{marksString}}. Answered.", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.answered": "Answered.", "question.score feedback.unanswered": "Unanswered.", "question.score feedback.correct": "Your answer is correct", "question.score feedback.partial": "Your answer is partially correct", "question.score feedback.wrong": "Your answer is incorrect", "question.selector.unsubmitted changes": "Unsubmitted changes.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Time remaining:", "xml.could not load": "Couldn't load an XML document: {{message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "Error initialising SCORM protocol: {{message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{message}}", "scorm.error loading question": "Error loading question {{number}}: {{message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "mark", "was": "was", "part": "part", "gap": "gap", "step": "step", "jme.substituteTree.undefined variable": "Undefined variable: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "significant figure", "part.numberentry.precision type.sigfig_plural": "significant figures", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "You have made a change to your answer but not submitted it. Please check your answer and then press the <strong>Submit answer</strong> button.", "question.unsubmitted changes_plural": "You have made changes to your answers but not submitted them. Please check your answers to each part and then press the <strong>Submit all parts</strong> button.", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "marks", "was_plural": "were", "die.script not loaded": "Numbas couldn't start because the file <code>{{file}}</code> was not loaded. Check that it's included in <code>scripts.js</code>.", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "part.numberentry.give your answer to precision_0": "Round your answer to the nearest integer.", "mathjax.error": "MathJax processing error: {{message}}", "mathjax.error with context": "MathJax processing error in {{context}}: {{message}}", "exam.introduction": "Exam introduction", "exam.feedback": "Exam feedback message", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "feedback", "part.numberentry.answer not reduced": "Your answer is not reduced to lowest terms.", "part.numberentry.give your answer as a reduced fraction": "Reduce your answer to lowest terms.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "choices", "part.mcq.answers": "answers", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{error}}", "question.statement": "Statement", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope"}}, "es-es": {"translation": {"page.loading": "Cargando...", "page.saving": "<p>Guardando.</p>\n<p>Esto podr\u00eda demorar unos segundos.</p>", "mathjax.math processing error": "Error de procesamiento en MathJax: \"{{-message}}\" al compilar <code>{{expression}}</code> ", "die.numbas failed": "Numbas ha fallado", "die.sorry": "Lo siento, Numbas ha encontrado un error, por lo tanto no puede continuar. A continuaci\u00f3n se muestra una descripci\u00f3n del error.", "die.error": "Error", "modal.ok": "OK", "modal.cancel": "Cancelar", "exam.exam name": "Nombre del examen:", "exam.random seed": "ID de la sesi\u00f3n:", "exam.student name": "Nombre del estudiante: ", "exam.number of questions": "N\u00famero de preguntas:", "exam.marks available": "Puntuaci\u00f3n m\u00e1xima:", "exam.pass percentage": "Porcentaje de aprobaci\u00f3n:", "exam.time allowed": "Tiempo permitido:", "exam.passed": "Aprobado... \u00a1Muy bien!", "exam.failed": "Reprobado... Int\u00e9ntelo otra vez", "exam.review header": "Revisi\u00f3n: ", "frontpage.start": "Comenzar examen", "suspend.paused header": "En pausa", "suspend.exam suspended": "Se ha detenido el examen. Presione <em>Reanudar</em> para continuar.", "suspend.you can resume": "Usted podr\u00e1 reanudar esta sesi\u00f3n la pr\u00f3xima vez que inicie esta actividad.", "suspend.resume": "Reanudar", "result.exit": "Salir del examen", "result.print": "Imprimir informe de resultados", "result.exam summary": "Informe del examen", "result.performance summary": "Informe de rendimiento", "result.exam start": "Inicio del examen:", "result.exam stop": "Termin\u00f3 el examen:", "result.time spent": "Tiempo invertido en resolver el examen:", "result.questions attempted": "Preguntas respondidas:", "result.score": "Puntuaci\u00f3n:", "result.result": "Resultado:", "result.question number": "N\u00famero de la pregunta", "result.question score": "Puntuaci\u00f3n", "result.question review title": "Revisar esta pregunta", "result.click a question to review": "Pulse sobre el n\u00famero de la pregunta para revisar sus respuestas y, si est\u00e1 disponible, la soluci\u00f3n al problema.", "end.exam has finished": "El examen ha finalizado. Ahora puede cerrar esta ventana.", "control.confirm leave": "Usted no ha terminado de responder su examen.", "control.not all questions answered": "Usted no ha contestado todas las preguntas de su examen.", "control.not all questions submitted": "Ha realizado cambios en una o m\u00e1s respuestas, pero no las ha enviado. Por favor, compruebe que se ha respondido cada pregunta.", "control.confirm end": "\u00bfEst\u00e1 seguro de que quiere terminar el examen? En cuanto lo finalice, ya no ser\u00e1 posible cambiar las respuestas.", "control.confirm regen": "\u00bfQuiere nuevos valores para la pregunta? Si usted pulsa sobre OK, todas sus respuestas y puntuaci\u00f3n para esta pregunta se perder\u00e1n.", "control.confirm reveal": "\u00bfQuiere mostrar las respuestas a esta pregunta? Perder\u00e1 la puntuaci\u00f3n recibida hasta ahora.", "control.proceed anyway": "\u00bfDesea continuar?", "control.regen": "Intentar una nueva versi\u00f3n de esta pregunta", "control.submit answer": "Enviar Respuesta", "control.submit all parts": "Enviar todas las partes de la pregunta", "control.submit again": "Enviar nuevamente", "control.submit": "Enviar", "control.previous": "Anterior", "control.next": "Siguiente", "control.advice": "Consejo", "control.reveal": "Mostrar las respuestas correctas", "control.total": "Total", "control.pause": "Pausa", "control.end exam": "Finalizar examen", "control.back to results": "Volver a los resultados", "display.part.jme.error making maths": "Error al desplegar escritura matem\u00e1tica", "exam.xml.bad root": "El elemento ra\u00edz de un examen XML deber\u00eda ser 'exam'", "exam.changeQuestion.no questions": "\u00a1Este examen no tiene preguntas! Revise el archivo con extensi\u00f3n .exam .", "feedback.you were awarded": "Ha logrado <strong>{{count}}</strong> $t(mark).", "feedback.taken away": "%s %s ha sido quitado.", "jme.tokenise.invalid": "Expresi\u00f3n inv\u00e1lida: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "No hay suficientes argumentos para la operaci\u00f3n {{op}}", "jme.shunt.no left bracket in function": "No concuerda el par\u00e9ntesis de la izquierda al aplicar la funci\u00f3n o tupla", "jme.shunt.no left square bracket": "No concuerda el corchete de la izquierda", "jme.shunt.no left bracket": "No concuerda el par\u00e9ntesis de la izquierda", "jme.shunt.no right bracket": "No concuerda el par\u00e9ntesis de la derecha", "jme.shunt.no right square bracket": "No concuerda el corchete de la derecha al final de la lista", "jme.shunt.missing operator": "La expresi\u00f3n no puede ser evaluada -- falta un operador.", "jme.typecheck.function maybe implicit multiplication": "La operaci\u00f3n {{name}} no est\u00e1 definida. \u00bfTal vez quiso decir <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "La operaci\u00f3n {{op}} no est\u00e1 definida. \u00bfTal vez quiso decir <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "La operaci\u00f3n '{{op}}' no est\u00e1 definida.", "jme.typecheck.no right type definition": "No se ha encontrado una definici\u00f3n para '{{op}}' del tipo correcto.\n", "jme.typecheck.no right type unbound name": "La variable <code>{{name}}</code> no est\u00e1 definida.", "jme.typecheck.map not on enumerable": "<code>map</code> la operaci\u00f3n funciona sobre una lista o un rango, no sobre{{type}}", "jme.evaluate.undefined variable": "La variable {{name}} no est\u00e1 definida", "jme.thtml.not html": "Se ha pasado un valor no HTML al constructor THTML.\n", "jme.func.switch.no default case": "No ha establecido una opci\u00f3n por defecto.", "jme.func.listval.invalid index": "\u00cdndice {{index}} de lista inv\u00e1lido en una lista de tama\u00f1o {{size}}", "jme.func.listval.not a list": "El objeto no se pude indizar", "jme.func.matrix.invalid row type": "No se puede construir una matriz a partir de filas de tipo {{type}}", "jme.func.except.continuous range": "No se puede usar el operador 'except' en intervalos continuos.", "jme.matrix.reports bad size": "No se puede construir una matriz a partir de filas de tipo {{type}}", "jme.texsubvars.no right bracket": "Falta <code>]</code> en los argumentos de {{op}}.", "jme.texsubvars.missing parameter": "Falta un par\u00e1metro en {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No corresponde <code>}</code> en {{op}}", "jme.user javascript.error": "Error en la funci\u00f3n javascript definida por el usuario <code> {{name}} </ code>: {{-message}}", "jme.variables.error making function": "Hubo un error en la construcci\u00f3n de la funci\u00f3n <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Error de sintaxis en la definici\u00f3n de la funci\u00f3n", "jme.variables.variable not defined": "La variable {{name}} no est\u00e1 definida.", "jme.variables.empty definition": "La variable definida <code>{{name}}</code> est\u00e1 vac\u00eda", "jme.variables.circular reference": "Hay una referencia circular en la definici\u00f3n de la variable <code>{{name}}</code>.", "jme.variables.error computing dependency": "Error al calcular la variable referenciada <code> {{name}} </ code>", "jme.variables.error evaluating variable": "Error al evaluar la variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Un conjunto v\u00e1lido de variables de pregunta no se gener\u00f3 a tiempo.", "jme.display.unknown token type": "No puedo pasar a texto la expresi\u00f3n de tipo {{type}}", "jme.display.collectRuleset.no sets": "No se han proporcionado conjuntos a collectRuleset!", "jme.display.collectRuleset.set not defined": "El conjunto de reglas para {{name}}, no ha sido definido.", "jme.display.simplifyTree.no scope given": "Hay que proporcionar un \u00e1mbito de actuaci\u00f3n a Numbas.jme.display.simplifyTree", "math.precround.complex": "No se puede aproximar un n\u00famero complejo a un n\u00famero de cifras decimales.", "math.siground.complex": "No se puede aproximar un n\u00famero complejo a un n\u00famero de cifras significativas.", "math.combinations.complex": "No se puede calcular combinaciones de n\u00fameros complejos.", "math.permutations.complex": "No se puede calcular permutaciones de n\u00fameros complejos.", "math.gcf.complex": "No se puede calcular MCD de n\u00fameros complejos.", "math.lcm.complex": "No se puede calcular MCM de n\u00fameros complejos.", "math.lt.order complex numbers": "No se pueden ordenar n\u00fameros complejos.", "math.choose.empty selection": "Se le ha proporcionado una selecci\u00f3n vac\u00eda a una funci\u00f3n aleatoria", "matrixmath.abs.non-square": "No se puede calcular el determinante de una matriz que no es cuadrada.", "matrixmath.abs.too big": "Lo sentimos, no se puede calcular el determinante de una matriz mayor que 3x3.", "matrixmath.mul.different sizes": "No se pueden multiplicar matrices de diferentes tama\u00f1os (columnas-filas).", "vectormath.cross.not 3d": "Solo puedo hacer el producto cruzado de vectores tridimensionales.", "vectormath.dot.matrix too big": "Solo puedo calcular el producto escalar de matrices que o bien tengan una sola fila o una sola columna.", "vectormath.cross.matrix too big": "Solo puedo calcular el producto cruzado de matrices que o bien tengan una sola fila o una sola columna.", "part.with steps answer prompt": "Respuesta.", "part.script.error": "Error en la parte {{path}} del script personalizado {{script}}: {{-message}}", "part.marking.steps no matter": "Como has recibido la m\u00e1xima puntuaci\u00f3n para esta parte, las respuestas de los pasos intermedios no se tienen en consideraci\u00f3n.", "part.marking.revealed steps no penalty": "Has revelado los pasos.", "part.marking.used variable replacements": "Esta parte ha sido puntuada usando tus respuestas a las partes anteriores.", "part.marking.variable replacement part not answered": "Debes responder primero a {{part}}", "part.marking.resubmit because of variable replacement": "La puntuaci\u00f3n de esta parte depende de las respuestas que has dado en partes anteriores, las cuales has cambiado. Por favor, env\u00eda de nuevo la respuesta de esta parte para actualizar tu nota.", "part.marking.not submitted": "No ha enviado respuesta", "part.marking.did not answer": "Usted no ha respondido esta pregunta.", "part.marking.nothing entered": "Usted no ha introducido una respuesta.", "part.marking.incorrect": "Su respuesta es incorrecta.", "part.marking.correct": "Su respuesta es correcta.", "part.marking.uncaught error": "Error al puntuar {{part}}: {{-message}}", "part.marking.no result": "Esta parte no se pudo puntuar.", "part.correct answer": "Respuesta correcta:", "part.missing type attribute": "{{part}}: falta atributo de tipo en la parte", "part.unknown type": "{{part}}: tipo {{type}} de parte no reconocido", "part.setting not present": "Propiedad '{{property}}' no establecida", "part.jme.answer missing": "Falta la respuesta correcta.", "part.jme.answer too long": "Su respuesta es demasiado larga.", "part.jme.answer too short": "Su respuesta es demasiado corta.", "part.jme.answer invalid": "Su respuesta no es una expresi\u00f3n matem\u00e1tica v\u00e1lida. <br/> {{-message}}.", "part.jme.marking.correct": "Su respuesta en num\u00e9ricamente correcta.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Su respuesta debe contener: {{strings}}", "part.jme.must-have several": "Su respuesta debe contener todas las cadenas: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Su respuesta no debe contener: {{strings}}", "part.jme.not-allowed several": "Su respuesta no debe contener: {{strings}}\n", "part.jme.unexpected variable name": "Se ha interpretado que tu respuesta usa el nombre de variable <code>{{name}}</code>, el cual es inapropiado.", "part.jme.unexpected variable name suggestion": "Se ha interpretado que tu respuesta usa el nombre de variable <code>{{name}}</code>, el cual es inapropiado. \u00bfQuiz\u00e1s quer\u00edas decir  <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Falta respuesta para mostrar.", "part.patternmatch.correct except case": "Su respuesta es correcta, excepto para el caso.", "part.numberentry.correct except decimal": "Su respuesta est\u00e1 dentro del rango permitido, pero los n\u00fameros decimales no est\u00e1n permitidos.", "part.numberentry.correct except fraction": "Su respuesta est\u00e1 dentro del rango permitido, pero no se permiten fracciones.", "part.numberentry.answer invalid": "No ha introducido un n\u00famero v\u00e1lido.", "part.numberentry.answer not integer": "Su respuesta no es v\u00e1lida. Debe ingresar un n\u00famero entero, no un decimal.", "part.numberentry.answer not integer or decimal": "Su respuesta no es v\u00e1lida. Debe ingresar un n\u00famero entero o decimal.", "part.numberentry.zero sig fig": "Esta parte est\u00e1 configurada para redondear la respuesta del estudiante a cero cifras significativas, lo cual no tiene sentido.", "part.mcq.options def not a list": "La expresi\u00f3n que define las {{properties}} no es una lista.", "part.mcq.marking matrix string empty": "La matriz de puntuaciones personalizada est\u00e1 vac\u00eda.", "part.mcq.choices missing": "Falta la definici\u00f3n de opciones", "part.mcq.matrix not a number": "La celda {{row}},{{column}} de la matriz de puntuaciones de la parte {{part}} no se evalua como un n\u00famero.", "part.mcq.wrong number of choices": "Ha seleccionado un n\u00famero incorrecto de opciones.", "part.mcq.no choices selected": "No hay opciones seleccionadas.", "part.mcq.matrix not a list": "La matriz de puntuaciones, definida por la expresi\u00f3n JME, no es una lista, y deber\u00eda serlo.", "part.mcq.matrix wrong type": "Se ha usado en la matriz de puntuaciones un elemento de tipo '{{type}}', el cual no es v\u00e1lido.", "part.mcq.matrix mix of numbers and lists": "Se ha usado una mezcla de listas y n\u00fameros en la matriz de puntuaciones.", "part.mcq.matrix wrong size": "La matriz de puntuaciones tiene un tama\u00f1o err\u00f3neo.", "part.mcq.correct choice": "Usted eligi\u00f3 la respuesta correcta.", "part.matrix.answer invalid": "Su respuesta no es v\u00e1lida.", "part.matrix.invalid cell": "Una o m\u00e1s de las celdas de la respuesta est\u00e1 vac\u00eda o es inv\u00e1lida.", "part.matrix.some incorrect": "Una o m\u00e1s celdas de tu respuesta son incorrectas, pero se te ha puntuado el resto.", "part.matrix.empty": "No ha ingresado una respuesta.", "part.matrix.empty cell": "Una o m\u00e1s de las celdas de la respuesta est\u00e1 vac\u00eda.", "part.matrix.size mismatch": "El autor de la pregunta no ha permitido al estudiante decidir la dimensiones de su respuesta; la respuesta correcta tiene {{correct_dimensions}} mientras que la entrada es {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Esta parte no tiene implementado el m\u00e9todo <code>{{name}}</code>.", "question.loaded name mismatch": "No se puede reanudar este intento - el paquete ha cambiado desde la \u00faltima sesi\u00f3n.", "question.error": "Pregunta {{number}}: {{-message}}", "question.preamble.error": "Error en el pre\u00e1mbulo: {{-message}}", "question.preamble.syntax error": "Error de sintaxis en el pre\u00e1mbulo", "question.unsupported part type": "Tipo de parte no soportado", "question.header": "Pregunta {{number}}", "question.submit part": "Enviar esta parte", "question.show steps": "Mostrar pasos", "question.show steps penalty": "Usted perder\u00e1 <strong>{{count}}</strong> $t(mark).", "question.show steps no penalty": "Su puntuaci\u00f3n no se ver\u00e1 afectada.", "question.show steps already penalised": "Ya se han mostrado los pasos, no tendr\u00e1 penalizaci\u00f3n adicional.", "question.hide steps": "Ocultar pasos", "question.hide steps no penalty": "Su puntuaci\u00f3n no se ver\u00e1 afectada.", "question.advice": "Soluci\u00f3n", "question.no such part": "No puedo encontrar la parte {{path}}", "question.can not submit": "No se puede enviar la respuesta - compruebe si hay errores.", "question.answer submitted": "Respuesta enviada", "question.score feedback.show": "Mostrar retroalimentaci\u00f3n", "question.score feedback.hide": "Ocultar retroalimentaci\u00f3n", "question.score feedback.answered total actual": "Puntuaci\u00f3n: {{score}}/{{marks}}", "question.score feedback.answered total": "{{marksString}}. Respondida.", "question.score feedback.answered actual": "Puntuaci\u00f3n: {{scoreString}}", "question.score feedback.answered": "Respondida.", "question.score feedback.unanswered": "No respondida.", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Su respuesta es correcta", "question.score feedback.partial": "Su respuesta es parcialmente correcta", "question.score feedback.wrong": "Su respuesta es incorrecta", "question.selector.unsubmitted changes": "Cambios no enviados.", "timing.no accumulator": "no hay acumulador de tiempo {{name}}", "timing.time remaining": "Tiempo restante: %s", "xml.could not load": "No se pudo cargar un documento XML: {{-message}}.", "xml.property not number": "La propiedad {{name}} en el nodo {{element}} deber\u00eda ser un numero, pero no es ({{value}})", "xml.property not boolean": "La propiedad {{name}} en el nodo {{element}} deber\u00eda ser un valor booleano, pero no es ({{value}})", "xml.error in variable definition": "Error en la definici\u00f3n de la variable <code>{{name}}</code>", "scorm.error initialising": "Error al inicializar el protocolo SCORM: {{-message}}.", "scorm.failed save": "<p>Ha fallado la petici\u00f3n de guardar los datos en el servidor. Pulsa  <b>OK</b> para intentarlo de nuevo.</p>\n<p>Si este mensaje aparece a menudo, comprueba tu conexi\u00f3n a internet o utiliza otro ordenador. Las respuestas enviadas con anterioridad han sido guardadas y se restaurar\u00e1n cuando restablezcas esta sesi\u00f3n en otro ordenador.</p>\n<p>Si persiste este mensaje y no puedes guardar <em>ninguna</em> respuesta, por favor, contacta con tu profesor.</p> ", "scorm.no exam suspend data": "No se ha podido reanudar: no hay datos del examen suspendido.", "scorm.error loading suspend data": "Error al cargar datos suspendidos: {{-message}}", "scorm.error loading question": "Error al cargar la pregunta {{number}}: {{-message}}", "scorm.no question suspend data": "No hay datos de la pregunta suspendida", "scorm.error loading part": "Error ala cargar la parte {{part}}: {{-message}}", "scorm.no part suspend data": "No hay datos suspendidos de esta parte", "util.product.non list": "A <code>Numbas.util.product</code> se le a pasado un argumento que no es una lista", "mark": "punto", "was": "fue", "part": "Parte.", "gap": "casilla", "step": "Paso.", "jme.substituteTree.undefined variable": "Variable no definida: <code> {{name}} </ code>", "jme.user javascript.returned undefined": "Funci\u00f3n javascript <code>{{name}}</code>, definida por el usuario, devolvi\u00f3 <code>undefined</code>.", "part.marking.steps change": "Has ganado <strong>{{count,niceNumber}}</strong> $t(mark) por tus respuestas a los pasos.", "part.marking.revealed steps with penalty": "Has revelado los pasos. Lo m\u00e1ximo que puedes puntear para esta parte es <strong>{{count,niceNumber}}</strong> $t(mark). Su puntuaci\u00f3n ser\u00e1 reducida en consecuencia.", "part.marking.total score": "Has obtenido <strong>{{count,niceNumber}}</strong> $t(mark) en esta parte.", "part.numberentry.precision type.dp": "posici\u00f3n decimal", "part.numberentry.precision type.dp_plural": "N\u00famero de decimales.", "part.numberentry.precision type.sigfig": "cifra significativa", "part.numberentry.precision type.sigfig_plural": "cifras significativas", "part.numberentry.give your answer to precision": "Redondea tu respuesta a {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Has realizado un cambio en tu respuesta, pero no la has enviado a\u00fan. Por favor, revisa tu respuesta y luego presiona el bot\u00f3n <strong>Enviar respuesta</strong>.", "question.unsubmitted changes_plural": "Has realizado cambios a tus respuestas, pero no las has enviado. Por favor, revisa tus respuestas a cada parte y luego presiona el bot\u00f3n  <strong>Enviar todas las partes </strong>. ", "util.equality not defined for type": "Igualdad no definida para el tipo {{type}} ", "mark_plural": "Puntos", "was_plural": "fueron", "die.script not loaded": "Numbas no pudo iniciar porque el archivo <code>{{file}}</code> no carg\u00f3. Compruebe que est\u00e1 incluido en <code>scripts.js</code>. ", "math.combinations.n less than zero": "No se puede calcular combinaciones: n es menor que cero.", "math.combinations.k less than zero": "No se puede calcular combinaciones: k es menor que cero.", "math.combinations.n less than k": "No se puede calcular combinaciones: n es menor que k.", "math.permutations.n less than zero": "No se puede calcular permutaciones: n es menor que cero.", "math.permutations.k less than zero": "No se puede calcular permutacione: k es menor que cero.", "math.permutations.n less than k": "No se puede calcular permutaciones: n es menor que k.", "part.numberentry.give your answer to precision_0": "Redondea tu respuesta al entero m\u00e1s cercano.", "mathjax.error": "MathJax procesando error: {{-message}}", "mathjax.error with context": "MathJax procesando error en {{context}}: {{-message}}", "exam.introduction": "Introducci\u00f3n al examen", "exam.feedback": "Mensaje de retroalimentaci\u00f3n del examen", "jme.tokenise.keypair key not a string": "La clave del diccionario tiene que ser una cadena, no {{type}}", "jme.shunt.list mixed argument types": "No puede analizar gram\u00e1ticamente {{mode}}: mezcla de diccionario y lista de elementos", "jme.func.listval.key not in dict": "El diccionario no contiene la clave <code>{{key}}</code>", "part.prompt": "Indicaci\u00f3n", "part.feedback": "Retroalimentaci\u00f3n", "part.numberentry.answer not reduced": "Su respuesta no est\u00e1 reducida a su menor expresi\u00f3n.", "part.numberentry.give your answer as a reduced fraction": "Reduzca su respuesta a la menor expresi\u00f3n.", "part.numberentry.negative decimal places": "Esta parte est\u00e1 configurada para redondear la respuesta del estudiante a un n\u00famero negativo de posiciones decimales, lo cual no tiene sentido.", "part.mcq.choices": "opciones", "part.mcq.answers": "respuestas", "part.mcq.matrix cell empty": "La parte {{part}} que marca la celda de una matriz ({{row}},{{column}}) est\u00e1 vac\u00eda", "part.mcq.matrix jme error": "La parte {{part}} que marca la celda de una matriz ({{row}}.{{column}}) produce un error JME: {{error}}", "question.statement": "Declaraci\u00f3n", "ruleset.circular reference": "Referencia circular en la definici\u00f3n de ruleset <code>{{name}}</code>", "ruleset.set not defined": "El conjunto de reglas {{name}} no ha sido definido", "jme.evaluate.no scope given": "Hay que darle a Numbas.jme.evaluate un Scope\n", "answer.number.not a number": "La respuesta no es v\u00e1lida", "answer.number.fractions not allowed": "La entrada no tiene que ser una fracci\u00f3n", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "No puedes introducir fracciones.", "answer.matrix.some cell not a number": "Una o m\u00e1s celdas en tu respuesta no es un n\u00famero v\u00e1lido", "exam.enter password": "Contrase\u00f1a", "exam.password.correct": "La contrase\u00f1a es correcta. Ud. puede comenzar el examen.", "exam.password.incorrect": "La contrase\u00f1a es incorrecta.", "frontpage.scorm.lms not connected": "Este examen se est\u00e1 desarrollando en modo independiente. \u00a1Sus respuestas y calificaciones no se guardar\u00e1n!", "result.question review": "Revisar", "control.confirm regen no marks": "\u00bfDesea elegir otra pregunta aleatoria?", "control.confirm reveal no marks": "\u00bfDesea que se le revele la respuesta?", "jme.tokenise.invalid near": "Expresi\u00f3n inv\u00e1lida: <code>{{expression}}</code> en la posici\u00f3n {{position}} cerca de <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Se ha pasado un objeto inv\u00e1lido al constructor num\u00e9rico.", "jme.subvars.null substitution": "Sustituci\u00f3n de una variable vac\u00eda: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Ya se ha registrado el tipo de dato {{type}}, por lo que no se puede registrar de nuevo.", "jme.type.no cast method": "No se puede convertir autom\u00e1ticamente de {{from}} en {{}}.", "jme.display.simplifyTree.empty expression": "La expresi\u00f3n est\u00e1 vac\u00eda.", "jme.display.simplifyTree.stuck in a loop": "El simplificador se ha bloqueado en la iteraci\u00f3n: <code>{{expr}}</code>", "math.niceNumber.undefined": "Se esperaba un n\u00famero, pero se obtuvo <code>undefined</code>", "math.rangeToList.zero step size": "No es posible convertir un intervalo con longitud de paso nula a una lista.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Ud. ha revelado los pasos.", "part.marking.maximum scaled down": "El puntaje m\u00e1ximo para esta parte es <strong>{{count,niceNumber}}</strong> $t(mark). El puntaje se disminuir\u00e1 acordemente.", "part.marking.minimum score applied": "El puntaje m\u00ednimo para esta parte es <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "El puntaje m\u00e1ximo para esta parte es <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "Se encontr\u00f3 un error en el algoritmo de puntuaci\u00f3n. Por favor rep\u00f3rtelo. {{-message}}", "part.marking.no result after replacement": "No se pudo puntuar esta parte usando las repuestas a las partes anteriores.", "part.marking.missing required note": "El algoritmo de puntuaci\u00f3n no define la nota <code>{{note}}</code>", "marking.apply.not a list": "El primer argumento para <code>apply</code> debe ser una lista, y no lo es", "marking.apply marking script.script not found": "No se encontr\u00f3 el algoritmo de puntuaci\u00f3n <code>{{name}}</code>.", "marking.note.compilation error": "Error al compilar la nota <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error al evaluar la nota <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Definici\u00f3n de nota inv\u00e1lida: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Ud. parece haber olvidado los dos puntos despu\u00e9s del nombre y la descripci\u00f3n.", "marking.note.invalid definition.description missing closing bracket": "Ud. parece haber olvidado el corchete", "marking.note.empty expression": "La nota <code>{{name}}</code> est\u00e1 vac\u00eda.", "marking.script.error parsing notes": "Ud. parece haber olvidado el corchete del cierre.", "part.feedback out of date": "Esta retroalimentaci\u00f3n se basa en la \u00faltima respuesta enviada. Env\u00ede la respuesta cambiada para obtener una retroalimentaci\u00f3n actualizada.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "La respuesta seleccionada es incorrecta.", "part.matrix.not all cells same precision": "Todas las celdas en su respuesta no tienen la misma precisi\u00f3n.", "part.gapfill.error marking gap": "Error al puntuar {{name}}: {{-message}}", "part.custom.empty setting": "Ning\u00fan valor enviado.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error al evaluar la opci\u00f3n de entrada <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error al evaluar el ajuste <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error mientras se genera la pregunta {{number}}: {{-message}}", "question.score feedback.not marked": "No anotado.", "question.score feedback.partially answered": "Respuesta parcial.", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Puntuaci\u00f3n: {{scoreString}}", "question.score feedback.score total actual": "Puntuaci\u00f3n: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Error en la definici\u00f3n de la variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "No se pudo cargar la extensi\u00f3n <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Respuesta para parte {{name}}", "part.correct answer title": "Respuesta esperada para parte {{name}}", "part.jme.must-match.failed": "Su respuesta no est\u00e1 escrita en la forma correcta", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Volver al men\u00fa", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "Una variable de la pregunta no ha sido nombrada.", "jme.calculus.unknown derivative": "No s\u00e9 diferenciar <code>{{tree}}</code>", "math.order complex numbers": "No puede ordenar n\u00fameros complejos", "menu.choose a question": "Escoja una pregunta", "part.choose next part.answered": "\u00bfQu\u00e9 quiere hacer ahora?", "part.choose next part.unanswered": "O podr\u00eda:", "part.choose next part.will be locked": "(Esta parte estar\u00e1 bloqueada)", "part.reached dead end": "No hay m\u00e1s por hacer.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Su pregunta es inv\u00e1lida. Debe ingresar un entero, un decimal o una fracci\u00f3n.", "question": "Pregunta", "question.progress": "Progreso de la pregunta:", "question.score feedback.unattempted": "No resuelta", "question.score feedback.attempted": "Resuelta", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objetivos", "question.penalties": "Penalties", "question.back to previous part": "Retorne a la parte previa", "end.print": "Imprima la transcripci\u00f3n de su examen", "math.shuffle_together.lists not all the same length": "No todas las listas tienen la misma longitud.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "La respuesta esperada para esta parte es de un tipo incorrecto. Deber\u00eda ser <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Dimensiones", "matrix input.rows": "Filas", "matrix input.columns": "Columnas", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Color de fondo", "modal.style.text colour": "Color de texto", "modal.style.text size": "Tama\u00f1o de texto", "modal.style.explanation": "Use estos controles para cambiar la apariencia del examen.", "modal.style.reset to defaults": "Volver a los valores por defecto", "modal.style.text size preview": "La mayor\u00eda del texto tendr\u00e1 este tama\u00f1o.", "control.style options": "Opciones de visualizaci\u00f3n", "part.marking.partially correct": "Tu respuesta es parcialmente correcta.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Ir al contenido", "result.learning objective": "Objetivo del aprendizaje", "jme.interpreted as": "Interpretado como", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Ir al siguiente tema.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "Te queda 1 intento", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "La prueba ha terminado.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Toma una decisi\u00f3n", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "he-il": {"translation": {"page.loading": "\u05d4\u05d3\u05e3 \u05d1\u05d8\u05e2\u05d9\u05e0\u05d4", "page.saving": "<p>\u05d4\u05d3\u05e3 \u05d1\u05e9\u05de\u05d9\u05e8\u05d4</p>\n<p>\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05e2\u05e9\u05d5\u05d9\u05d4 \u05dc\u05e7\u05d7\u05ea \u05de\u05e1\u05e4\u05e8 \u05e9\u05e0\u05d9\u05d5\u05ea</p>", "mathjax.math processing error": "\"{{-message}}\" \u05db\u05d0\u05e9\u05e8 \u05de\u05e2\u05d1\u05d3 \u05d0\u05ea <code>{{expression}}</code>", "die.numbas failed": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4", "die.sorry": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e0\u05ea\u05e7\u05dc\u05d4 \u05d1\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d5\u05dc\u05d0 \u05d9\u05db\u05d5\u05dc\u05d4 \u05dc\u05d4\u05de\u05e9\u05d9\u05da. \u05ea\u05d0\u05d5\u05e8 \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05de\u05d5\u05e4\u05d9\u05e2 \u05de\u05d8\u05d4.", "die.error": "\u05e9\u05d2\u05d9\u05d0\u05d4", "modal.ok": "OK", "modal.cancel": "Cancel", "exam.exam name": "\u05e9\u05dd \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea:", "exam.random seed": "\u05de\u05e1\u05e4\u05e8 \u05de\u05e4\u05d2\u05e9:", "exam.student name": "\u05e9\u05dd \u05d4\u05ea\u05dc\u05de\u05d9\u05d3:", "exam.number of questions": "\u05de\u05e1\u05e4\u05e8 \u05d4\u05e9\u05d0\u05dc\u05d5\u05ea:", "exam.marks available": "\u05e6\u05d9\u05d5\u05e0\u05d9\u05dd:", "exam.pass percentage": "\u05d0\u05d7\u05d5\u05d6 \u05d4\u05e6\u05dc\u05d7\u05d4:", "exam.time allowed": "\u05d6\u05de\u05df \u05de\u05d5\u05e7\u05e6\u05d4:", "exam.passed": "\u05e2\u05d1\u05e8", "exam.failed": "\u05e0\u05db\u05e9\u05dc", "exam.review header": "\u05e1\u05e7\u05d9\u05e8\u05d4: ", "frontpage.start": "\u05d4\u05ea\u05d7\u05dc", "suspend.paused header": "\u05d4\u05d5\u05e4\u05e1\u05e7", "suspend.exam suspended": "\n\u05d4\u05de\u05d1\u05d3\u05e7 \u05d4\u05d5\u05e4\u05e1\u05e7. \u05dc\u05d7\u05e5 \u05e2\u05dc  <em>\u05d4\u05de\u05e9\u05da</em> \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05d4\u05de\u05e9\u05d9\u05da.", "suspend.you can resume": "\u05ea\u05d5\u05db\u05dc \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05d0\u05ea \u05d4\u05de\u05e4\u05d2\u05e9 \u05d1\u05e4\u05e2\u05dd \u05d4\u05d1\u05d0\u05d4 \u05e9\u05ea\u05ea\u05d7\u05d9\u05dc \u05d0\u05ea \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea \u05d4\u05d6\u05d5.", "suspend.resume": "\u05d4\u05de\u05e9\u05da", "result.exit": "\u05e1\u05d9\u05d9\u05dd \u05e4\u05e2\u05d9\u05dc\u05d5\u05ea", "result.print": "\u05d4\u05d3\u05e4\u05e1 \u05d0\u05ea \u05d3\u05e3 \u05d4\u05ea\u05d5\u05e6\u05d0\u05d5\u05ea", "result.exam summary": "\u05d3\u05e3 \u05ea\u05d5\u05e6\u05d0\u05d5\u05ea", "result.performance summary": "\u05e1\u05d9\u05db\u05d5\u05dd \u05d1\u05d9\u05e6\u05d5\u05e2 \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea", "result.exam start": "\u05d4\u05ea\u05d7\u05dc\u05ea \u05e4\u05e2\u05d9\u05dc\u05d5\u05ea", "result.exam stop": "\u05e1\u05d9\u05d5\u05dd \u05e4\u05e2\u05d9\u05dc\u05d5\u05ea:", "result.time spent": "\u05d6\u05de\u05df \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea:", "result.questions attempted": "\u05e9\u05d0\u05dc\u05d5\u05ea \u05e9\u05e0\u05d9\u05e1\u05d9\u05ea:", "result.score": "\u05e6\u05d9\u05d5\u05df:", "result.result": "\u05ea\u05d5\u05e6\u05d0\u05d5\u05ea:", "result.question number": "\u05de\u05e1\u05e4\u05e8 \u05e9\u05d0\u05dc\u05d4:", "result.question score": "\u05e6\u05d9\u05d5\u05df", "result.question review title": "\u05e6\u05e4\u05d4 \u05d1\u05e9\u05d0\u05dc\u05d4", "result.click a question to review": "\u05d4\u05e7\u05dc\u05e7 \u05e2\u05dc \u05de\u05e1\u05e4\u05e8 \u05d4\u05e9\u05d0\u05dc\u05d4 \u05db\u05d3\u05d9 \u05dc\u05e8\u05d0\u05d5\u05ea \u05d0\u05ea \u05d4\u05e6\u05d9\u05d5\u05df \u05dc\u05ea\u05e9\u05d5\u05d1\u05d5\u05ea \u05e9\u05e0\u05ea\u05ea \u05d5\u05d0\u05ea \u05d4\u05e4\u05ea\u05e8\u05d5\u05df \u05d4\u05de\u05dc\u05d0 (\u05d1\u05de\u05d9\u05d3\u05d4 \u05d5\u05e7\u05d9\u05d9\u05dd).", "end.exam has finished": "\u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea \u05d4\u05e1\u05ea\u05d9\u05d9\u05de\u05d4. \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e1\u05d2\u05d5\u05e8 \u05d0\u05ea \u05d4\u05d7\u05dc\u05d5\u05df.", "control.confirm leave": "\u05dc\u05d0 \u05e1\u05d9\u05d9\u05de\u05ea \u05d0\u05ea \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea.", "control.not all questions answered": "\u05dc\u05d0 \u05e1\u05d9\u05d9\u05de\u05ea \u05d0\u05ea \u05db\u05dc \u05d4\u05e9\u05d0\u05dc\u05d5\u05ea \u05d1\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea.", "control.not all questions submitted": "\u05d1\u05d9\u05e6\u05e2\u05ea \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05dc\u05ea\u05e9\u05d5\u05d1\u05d4 \u05d0\u05d7\u05ea \u05d0\u05d5 \u05d9\u05d5\u05ea\u05e8, \u05d0\u05d1\u05dc \u05dc\u05d0 \u05d4\u05d2\u05e9\u05ea \u05d0\u05d5\u05ea\u05dd. \u05d0\u05e0\u05d0 \u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05dc \u05ea\u05e9\u05d5\u05d1\u05d5\u05ea\u05d9\u05da \u05d4\u05d5\u05d2\u05e9\u05d5.", "control.confirm end": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05d1\u05d8\u05d5\u05d7 \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05e1\u05d9\u05d9\u05dd \u05d0\u05ea \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea? \u05d0\u05d7\u05e8\u05d9 \u05e1\u05d9\u05d5\u05dd \u05d4\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea, \u05dc\u05d0 \u05ea\u05d5\u05db\u05dc \u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05ea\u05e9\u05d5\u05d1\u05d5\u05ea\u05d9\u05da. ", "control.confirm regen": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e2\u05d5\u05e0\u05d9\u05d9\u05df \u05dc\u05d1\u05d7\u05d5\u05e8 \u05e2\u05e8\u05db\u05d9\u05dd \u05d7\u05d3\u05e9\u05d9\u05dd \u05dc\u05e9\u05d0\u05dc\u05d4? \u05d0\u05dd \u05db\u05df, \u05d4\u05ea\u05e9\u05d5\u05d1\u05d5\u05ea \u05d5\u05d4\u05e6\u05d9\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05e9\u05d0\u05dc\u05d4 \u05d6\u05d5 \u05d9\u05de\u05d7\u05e7\u05d5.", "control.confirm reveal": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e2\u05d5\u05e0\u05d9\u05d9\u05df \u05dc\u05e8\u05d0\u05d5\u05ea \u05d0\u05ea \u05d4\u05e4\u05ea\u05e8\u05d5\u05df \u05e9\u05dc \u05d4\u05e9\u05d0\u05dc\u05d4? \u05d0\u05dd \u05db\u05df, \u05d4\u05ea\u05e9\u05d5\u05d1\u05d5\u05ea \u05d5\u05d4\u05e6\u05d9\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05e9\u05d0\u05dc\u05d4 \u05d6\u05d5 \u05d9\u05e0\u05e2\u05dc\u05d5 \u05d5\u05dc\u05d0 \u05ea\u05d5\u05db\u05dc \u05dc\u05e2\u05e0\u05d5\u05ea \u05e2\u05dc\u05d9\u05d4 \u05e9\u05d5\u05d1.", "control.proceed anyway": "\u05dc\u05d4\u05de\u05e9\u05d9\u05da?", "control.regen": "\u05d1\u05d7\u05e8 \u05e2\u05e8\u05db\u05d9\u05dd \u05d7\u05d3\u05e9\u05d9\u05dd \u05dc\u05e9\u05d0\u05dc\u05d4 \u05d6\u05d5", "control.submit answer": "\u05d4\u05d2\u05e9 \u05d0\u05ea \u05d4\u05ea\u05e9\u05d5\u05d1\u05d4", "control.submit all parts": "\u05d4\u05d2\u05e9 \u05d0\u05ea \u05db\u05dc \u05d7\u05dc\u05e7\u05d9 \u05d4\u05ea\u05e9\u05d5\u05d1\u05d4", "control.submit again": "\u05d4\u05d2\u05e9 \u05de\u05d7\u05d3\u05e9", "control.submit": "\u05d4\u05d2\u05e9", "control.previous": "\u05d4\u05e7\u05d5\u05d3\u05dd", "control.next": "\u05d4\u05d1\u05d0", "control.advice": "\u05e8\u05de\u05d6", "control.reveal": "\u05d2\u05dc\u05d4 \u05d0\u05ea \u05d4\u05ea\u05e9\u05d5\u05d1\u05d5\u05ea", "control.total": "\u05e1\u05d9\u05db\u05d5\u05dd", "control.pause": "\u05d4\u05e4\u05e1\u05e7", "control.end exam": "\u05e1\u05d9\u05d9\u05dd \u05e4\u05e2\u05d9\u05dc\u05d5\u05ea", "control.back to results": "\u05d7\u05d6\u05d5\u05e8 \u05dc\u05ea\u05d5\u05e6\u05d0\u05d5\u05ea", "display.part.jme.error making maths": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05ea \u05d4\u05e6\u05d2\u05ea \u05e0\u05ea\u05d5\u05e0\u05d9\u05dd", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "\u05d1\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea \u05d6\u05d5 \u05d0\u05d9\u05df \u05e9\u05d0\u05dc\u05d5\u05ea! (\u05d7\u05e4\u05e9 \u05e9\u05d2\u05d9\u05d0\u05d5\u05ea \u05d1\u05e7\u05d5\u05d1\u05e5 exam)", "feedback.you were awarded": "\n\u05e7\u05d9\u05d1\u05dc\u05ea <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "\n<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) \u05e0\u05dc\u05e7\u05d7\u05d5.", "jme.shunt.not enough arguments": "Not enough arguments for operation <code>{{op}}</code>", "jme.shunt.no left bracket in function": "No matching left bracket in function application or tuple", "jme.shunt.no left square bracket": "No matching left bracket", "jme.shunt.no left bracket": "No matching left bracket", "jme.shunt.no right bracket": "No matching right bracket", "jme.shunt.no right square bracket": "No matching right square bracket to end list", "jme.shunt.missing operator": "Expression can't be evaluated -- missing an operator.", "jme.typecheck.function maybe implicit multiplication": "Function <code>{{name}}</code> is not defined. Did you mean <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Function <code>{{op}}</code> is not defined. Is <code>{{op}}</code> a variable, and did you mean <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operation '{{op}}' is not defined.", "jme.typecheck.no right type definition": "No definition of '{{op}}' of correct type found.", "jme.typecheck.no right type unbound name": "Variable <code>{{name}}</code> is not defined.", "jme.typecheck.map not on enumerable": "<code>map</code> operation must work over a list or a range, not {{type}}", "jme.evaluate.undefined variable": "Variable {{name}} is undefined", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.matrix.invalid row type": "Can't construct a matrix from rows of type {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Missing parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Syntax error in function definition", "jme.variables.variable not defined": "Variable <code>{{name}}</code> is not defined.", "jme.variables.empty definition": "Definition of variable <code>{{name}}</code> is empty.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Can't compute GCF of complex numbers", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.lt.order complex numbers": "Can't order complex numbers", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Can't compute the determinant of a matrix which isn't square.", "matrixmath.abs.too big": "Sorry, can't compute the determinant of a matrix bigger than 3x3 yet.", "matrixmath.mul.different sizes": "Can't multiply matrices of different sizes.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "Answer: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "You must answer {{part}} first.", "part.marking.resubmit because of variable replacement": "This part's marking depends on your answers to other parts, which you have changed. Save your answer to this part again to update your score.", "part.marking.not submitted": "No answer submitted.", "part.marking.did not answer": "You did not answer this question.", "part.marking.nothing entered": "You did not enter an answer.", "part.marking.incorrect": "Your answer is incorrect.", "part.marking.correct": "Your answer is correct.", "part.marking.uncaught error": "Error when marking: {{-message}}", "part.correct answer": "Expected answer:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "Correct answer is missing", "part.jme.answer too long": "Your answer is too long.", "part.jme.answer too short": "Your answer is too short.", "part.jme.answer invalid": "Your answer is not a valid mathematical expression.<br/>{{-message}}.", "part.jme.marking.correct": "Your answer is numerically correct.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Your answer must contain: {{strings}}", "part.jme.must-have several": "Your answer must contain all of: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Your answer must not contain: {{strings}}", "part.jme.not-allowed several": "Your answer must not contain any of: {{strings}}", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "Your answer is within the allowed range, but decimal numbers are not allowed.", "part.numberentry.correct except fraction": "Your answer is within the allowed range, but fractions are not allowed.", "part.numberentry.answer invalid": "You did not enter a valid number.", "part.numberentry.answer not integer": "Your answer is invalid. You must enter a whole number, not a decimal.", "part.numberentry.answer not integer or decimal": "Your answer is invalid. You must enter an integer or a decimal.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "You selected the wrong number of choices.", "part.mcq.no choices selected": "No choices selected.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "You chose a correct answer.", "part.matrix.answer invalid": "Your answer is not valid.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.empty": "You have not entered an answer.", "part.matrix.empty cell": "One or more of the cells in your answer is empty.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Question {{number}}: {{-message}}", "question.preamble.error": "Error in preamble: {{-message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "Question {{number}}", "question.submit part": "Save answer", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.advice": "Advice", "question.no such part": "Can't find part {{path}}.", "question.can not submit": "Can not submit answer - check for errors.", "question.answer submitted": "Answer submitted", "question.score feedback.show": "Show feedback", "question.score feedback.hide": "Hide feedback", "question.score feedback.answered": "Answered", "question.score feedback.unanswered": "Unanswered", "question.score feedback.correct": "Your answer is correct", "question.score feedback.partial": "Your answer is partially correct", "question.score feedback.wrong": "Your answer is incorrect", "question.selector.unsubmitted changes": "Unsubmitted changes.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "\u05d4\u05d6\u05de\u05df \u05d4\u05e0\u05d5\u05ea\u05e8:", "xml.could not load": "Couldn't load an XML document: {{-message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "scorm.error initialising": "Error initialising SCORM protocol: {{-message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{-message}}", "scorm.error loading question": "Error loading question {{number}}: {{-message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{-message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "mark", "was": "\u05d4\u05d9\u05d4", "part": "part", "gap": "\u05de\u05e8\u05d5\u05d5\u05d7", "step": "\u05e6\u05e2\u05d3", "jme.substituteTree.undefined variable": "Undefined variable: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "significant figure", "part.numberentry.precision type.sigfig_plural": "significant figures", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "You have made a change to your answer but not submitted it. Please check your answer and then press the <strong>Save answer</strong> button.", "question.unsubmitted changes_plural": "You have made changes to your answers but not submitted them. Please check your answers to each part and then press the <strong>Save all answers</strong> button.", "util.equality not defined for type": "Equality not defined for type {{type}}", "mark_plural": "marks", "was_plural": "\u05d4\u05d9\u05d5", "die.script not loaded": "\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05dc\u05d0 \u05d9\u05db\u05d5\u05dc\u05d4 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05e7\u05d5\u05d1\u05e5 <code>{{file}}</code> \u05dc\u05d0 \u05e0\u05d8\u05e2\u05df. \u05d1\u05d3\u05d5\u05e7 \u05e9\u05d4\u05e7\u05d5\u05d1\u05e5 \u05e0\u05db\u05dc\u05dc \u05d1 <code>scripts.js</code>.", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "part.numberentry.give your answer to precision_0": "Round your answer to the nearest integer.", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "exam.introduction": "\u05d4\u05e7\u05d3\u05de\u05d4 \u05dc\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea", "exam.feedback": "\u05de\u05e9\u05d5\u05d1 \u05dc\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "Feedback", "part.numberentry.answer not reduced": "Your answer is not reduced to lowest terms.", "part.numberentry.give your answer as a reduced fraction": "Reduce your answer to lowest terms.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "choices", "part.mcq.answers": "answers", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Statement", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "answer.number.not a number": "\u05ea\u05e9\u05d5\u05d1\u05ea\u05da \u05d0\u05d9\u05e0\u05d4 \u05de\u05e1\u05e4\u05e8 \u05d7\u05d5\u05e7\u05d9.", "answer.number.fractions not allowed": "\u05d0\u05d9\u05e0\u05da \u05e8\u05e9\u05d0\u05d9 \u05dc\u05d4\u05e7\u05dc\u05d9\u05d3 \u05e9\u05d1\u05e8.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "\u05d0\u05d9\u05e0\u05da \u05e8\u05e9\u05d0\u05d9 \u05dc\u05d4\u05e7\u05dc\u05d9\u05d3 \u05e9\u05d1\u05e8\u05d9\u05dd.", "answer.matrix.some cell not a number": "\u05d0\u05d7\u05d3 \u05d0\u05d5 \u05d9\u05d5\u05ea\u05e8 \u05de\u05d4\u05ea\u05d0\u05d9\u05dd \u05d1\u05ea\u05e9\u05d5\u05d1\u05ea\u05da \u05d0\u05d9\u05e0\u05d5 \u05de\u05e1\u05e4\u05e8 \u05d7\u05d5\u05e7\u05d9.", "exam.enter password": "\u05e1\u05d9\u05e1\u05de\u05d4:", "exam.password.correct": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e0\u05db\u05d5\u05e0\u05d4. \u05d0\u05ea\u05d4 \u05e8\u05e9\u05d0\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea.", "exam.password.incorrect": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4.", "frontpage.scorm.lms not connected": "\u05e4\u05e2\u05d9\u05dc\u05d5\u05ea \u05d6\u05d5 \u05dc\u05dc\u05d0 \u05d7\u05d9\u05d1\u05d5\u05e8 \u05dc\u05e9\u05e8\u05ea. \u05d4\u05ea\u05e9\u05d5\u05d1\u05d5\u05ea \u05d5\u05d4\u05e6\u05d9\u05d5\u05e0\u05d9\u05dd \u05dc\u05d0 \u05d9\u05e9\u05de\u05e8\u05d5. ", "result.question review": "Review", "control.confirm regen no marks": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e2\u05d5\u05e0\u05d9\u05d9\u05df \u05dc\u05d1\u05d7\u05d5\u05e8 \u05e2\u05e8\u05db\u05d9\u05dd \u05d7\u05d3\u05e9\u05d9\u05dd \u05dc\u05e9\u05d0\u05dc\u05d4?", "control.confirm reveal no marks": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e2\u05d5\u05e0\u05d9\u05d9\u05df \u05dc\u05e8\u05d0\u05d5\u05ea \u05d0\u05ea \u05d4\u05e4\u05ea\u05e8\u05d5\u05df \u05e9\u05dc \u05d4\u05e9\u05d0\u05dc\u05d4?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Partially answered", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea \u05ea\u05e4\u05e8\u05d9\u05d8 \u05d4\u05e0\u05d9\u05d5\u05d5\u05d8", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "en-gb": {"translation": {"accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "answer.number.not a number": "Your answer is not a valid number.", "answer.number.fractions not allowed": "You may not enter a fraction.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "You may not enter fractions.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "page.loading": "Loading...", "page.saving": "<p>Saving.</p>\n<p>This might take a few seconds.</p>", "page.skip to content": "Skip to content", "mathjax.math processing error": "\"{{-message}}\" when texifying <code>{{expression}}</code>", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "die.numbas failed": "Numbas has failed", "die.sorry": "Sorry, Numbas has encountered an error which means it can't continue. Below is a description of the error.", "die.script not loaded": "Numbas couldn't start because the file <code>{{file}}</code> was not loaded. Check that it's included in <code>scripts.js</code>.", "die.error": "Error", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "modal.confirm": "Confirm", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "modal.alert": "Alert", "modal.ok": "OK", "modal.cancel": "Cancel", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.invert images in dark mode": "Invert images in dark mode", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "lightbox.zoom in on image": "Zoom in on this image", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "exam.enter password": "Password:", "exam.password.correct": "This password is correct. You can start the exam.", "exam.password.incorrect": "This password is incorrect.", "exam.exam name": "Exam Name:", "exam.introduction": "Exam introduction", "exam.feedback": "Exam feedback message", "exam.random seed": "Session ID:", "exam.student name": "Student's Name:", "exam.number of questions": "Number of Questions:", "exam.marks available": "Marks Available:", "exam.pass percentage": "Pass Percentage:", "exam.time allowed": "Time Allowed:", "exam.passed": "Passed", "exam.failed": "Failed", "exam.review header": "Review: ", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "frontpage.start": "Start", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "suspend.paused header": "Paused", "suspend.resumed header": "Attempt resumed", "suspend.exam suspended": "The Exam has been suspended. Press <em>Resume</em> to continue.", "suspend.you can resume": "You will be able to resume this session the next time you start this activity.", "suspend.resume": "Resume", "result.exit": "Exit Exam", "result.print": "Print this results summary", "result.exam summary": "Exam Summary", "result.performance summary": "Performance Summary", "result.exam start": "Exam Start:", "result.exam stop": "Exam Stop:", "result.time spent": "Time Spent:", "result.questions attempted": "Questions Attempted:", "result.score": "Score:", "result.result": "Result:", "result.question number": "Question Number", "result.learning objective": "Learning objective", "result.question score": "Score", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "result.question review title": "Review this question", "result.question review": "Review", "result.click a question to review": "Click on a question number to see how your answers were marked and, where available, full solutions.", "result.download exam object": "Download your exam data", "end.exam has finished": "The exam has finished. You may now close this window.", "end.print": "Print your exam transcript", "control.confirm leave": "You haven't finished the exam.", "control.not all questions answered": "You have not completed every question in this exam.", "control.not all questions submitted": "You have made changes to one or more answers but not submitted them. Please check each question has been submitted.", "control.confirm end": "Are you sure you want to end the exam? After you end the exam, you will not be able to change any of your answers.", "control.confirm regen": "Would you like to re-randomise this question? If you click OK, all your answers and marks for the current question will be lost.", "control.confirm regen no marks": "Would you like to re-randomise this question?", "control.confirm reveal": "Would you like to reveal the answer to this question? Any marks you have received so far will be locked and you will not be able to answer this question later.", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.proceed anyway": "Proceed anyway?", "control.regen": "Try another question like this one", "control.submit answer": "Save answer", "control.submit all parts": "Save all answers", "control.previous": "Previous", "control.next": "Next", "control.advice": "Advice", "control.reveal": "Reveal answers", "control.total": "Total", "control.pause": "Pause", "control.end exam": "End Exam", "control.back to results": "Go back to results", "control.back to menu": "Go back to the menu", "control.toggle navigation menu": "Toggle the navigation menu", "control.return to question": "Return to the question", "control.style options": "Display options", "control.move to next question": "Move to the next question", "control.show introduction": "Introduction", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "display.part.jme.error making maths": "Error making maths display", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "exam.xml.bad root": "Root element of exam XML should be 'exam'", "exam.changeQuestion.no questions": "This exam contains no questions! Check the .exam file for errors.", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "feedback.you were awarded": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) taken away.", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.shunt.not enough arguments": "Not enough arguments for operation <code>{{op}}</code>", "jme.shunt.no left bracket in function": "No matching left bracket in function application or tuple", "jme.shunt.no left square bracket": "No matching left bracket", "jme.shunt.no left bracket": "No matching left bracket", "jme.shunt.no right bracket": "No matching right bracket", "jme.shunt.no right square bracket": "No matching right square bracket to end list", "jme.shunt.missing operator": "Expression can't be evaluated -- missing an operator.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "jme.typecheck.function maybe implicit multiplication": "Function <code>{{name}}</code> is not defined. Did you mean <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Function <code>{{op}}</code> is not defined. Is <code>{{op}}</code> a variable, and did you mean <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operation '{{op}}' is not defined.", "jme.typecheck.no right type definition": "No definition of '{{op}}' of correct type found.", "jme.typecheck.no right type unbound name": "Variable <code>{{name}}</code> is not defined.", "jme.typecheck.map not on enumerable": "<code>map</code> operation must work over a list or a range, not {{type}}", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "jme.evaluate.undefined variable": "Variable {{name}} is undefined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "jme.substituteTree.undefined variable": "Undefined variable: <code>{{name}}</code>", "jme.thtml.not html": "Passed a non-HTML value into the THTML constructor.", "jme.func.switch.no default case": "No default case for Switch statement", "jme.func.listval.invalid index": "Invalid list index {{index}} on list of size {{size}}", "jme.func.listval.not a list": "Object is not subscriptable", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "jme.func.matrix.invalid row type": "Can't construct a matrix from rows of type {{type}}", "jme.func.except.continuous range": "Can't use the 'except' operator on continuous ranges.", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Missing parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.syntax error in function definition": "Syntax error in function definition", "jme.variables.variable not defined": "Variable <code>{{name}}</code> is not defined.", "jme.variables.empty definition": "Definition of variable <code>{{name}}</code> is empty.", "jme.variables.empty name": "A question variable has not been given a name.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "jme.interpreted as": "interpreted as", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Can't compute GCF of complex numbers", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.order complex numbers": "Can't order complex numbers", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.choose.empty selection": "Empty selection given to random function", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "matrixmath.abs.non-square": "Can't compute the determinant of a matrix which isn't square.", "matrixmath.abs.too big": "Sorry, can't compute the determinant of a matrix bigger than 3x3 yet.", "matrixmath.mul.different sizes": "Can't multiply matrices of different sizes.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "menu.choose a question": "Choose a question.", "part.error": "{{path}}: {{-message}}", "part.with steps answer prompt": "Answer: ", "part.prompt": "prompt", "part.feedback": "Feedback", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.next part.confirm penalty": "Would you like to take this option? You will lose {{count,niceNumber}} $t(mark).", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Because you received full marks for the part, your answers to the steps aren't counted.", "part.marking.steps change": "You were awarded <strong>{{count,niceNumber}}</strong> $t(mark) for your answers to the steps.", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.used variable replacements": "This part was marked using your answers to previous parts.", "part.marking.variable replacement part not answered": "You must answer {{part}} first.", "part.marking.resubmit because of variable replacement": "This part's marking depends on your answers to other parts, which you have changed. Save your answer to this part again to update your score.", "part.marking.not submitted": "No answer submitted.", "part.marking.did not answer": "You did not answer this question.", "part.marking.total score": "You scored <strong>{{count,niceNumber}}</strong> $t(mark) for this part.", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.nothing entered": "You did not enter an answer.", "part.marking.incorrect": "Your answer is incorrect.", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.correct": "Your answer is correct.", "part.marking.uncaught error": "Error when marking: {{-message}}", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.correct answer": "Expected answer:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.there is new feedback": "The feedback has changed.", "part.input title": "Answer for part {{name}}", "part.feedback title": "Feedback for {{name}}.", "part.correct answer title": "Expected answer for part {{name}}", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "part.jme.answer missing": "Correct answer is missing", "part.jme.answer too long": "Your answer is too long.", "part.jme.answer too short": "Your answer is too short.", "part.jme.answer invalid": "Your answer is not a valid mathematical expression.<br/>{{-message}}.", "part.jme.marking.correct": "Your answer is numerically correct.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Your answer must contain: {{strings}}", "part.jme.must-have several": "Your answer must contain all of: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Your answer must not contain: {{strings}}", "part.jme.not-allowed several": "Your answer must not contain any of: {{strings}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.jme.unexpected variable name": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Your answer was interpreted to use the unexpected variable name <code>{{name}}</code>. Did you mean <code>{{suggestion}}</code>?", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.patternmatch.display answer missing": "Display answer is missing", "part.patternmatch.correct except case": "Your answer is correct, except for the case.", "part.numberentry.correct except decimal": "Your answer is within the allowed range, but decimal numbers are not allowed.", "part.numberentry.correct except fraction": "Your answer is within the allowed range, but fractions are not allowed.", "part.numberentry.answer invalid": "You did not enter a valid number.", "part.numberentry.answer not reduced": "Your answer is not reduced to lowest terms.", "part.numberentry.give your answer as a reduced fraction": "Reduce your answer to lowest terms.", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "part.numberentry.answer not integer": "Your answer is invalid. You must enter a whole number, not a decimal.", "part.numberentry.answer not integer or decimal": "Your answer is invalid. You must enter an integer or a decimal.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "part.numberentry.zero sig fig": "This part is set up to round the student's answer to zero significant figures, which has no meaning.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.numberentry.precision type.dp": "decimal place", "part.numberentry.precision type.dp_plural": "decimal places", "part.numberentry.precision type.sigfig": "significant figure", "part.numberentry.precision type.sigfig_plural": "significant figures", "part.numberentry.give your answer to precision": "Round your answer to {{count,niceNumber}} {{precisionType}}.", "part.numberentry.give your answer to precision_0": "Round your answer to the nearest integer.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.mcq.choices": "choices", "part.mcq.answers": "answers", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "You selected the wrong number of choices.", "part.mcq.no choices selected": "No choices selected.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "You chose a correct answer.", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.answer invalid": "Your answer is not valid.", "part.matrix.invalid cell": "One or more of the cells in your answer is empty or invalid.", "part.matrix.some incorrect": "One or more of the cells in your answer is incorrect, but you have been awarded marks for the rest.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.matrix.empty": "You have not entered an answer.", "part.matrix.empty cell": "One or more of the cells in your answer is empty.", "part.matrix.size mismatch": "The question author hasn't allowed the student to decide the dimensions of their answer, but the correct answer is {{correct_dimensions}} while the answer input is {{input_dimensions}}", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "part.extension.not implemented": "Part hasn't implemented the <code>{{name}}</code> method.", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "question": "Question", "question.nav.label": "Question controls", "question.loaded name mismatch": "Can't resume this attempt - the package has changed since the last session.", "question.error": "Question {{number}}: {{-message}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.preamble.error": "Error in preamble: {{-message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "question.unsupported part type": "Unsupported part type", "question.header": "Question {{number}}", "question.submit part": "Save answer", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "question.show steps": "Show steps", "question.show steps penalty": "You will lose <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Your score will not be affected.", "question.show steps already penalised": "You have already shown steps. You can show them again with no further penalty.", "question.hide steps": "Hide steps", "question.hide steps no penalty": "Your score will not be affected.", "question.statement": "Statement", "question.advice": "Advice", "question.progress": "Question progress", "question.no such part": "Can't find part {{path}}.", "question.can not submit": "Can not submit answer - check for errors.", "question.answer submitted": "Answer submitted", "question.unsubmitted changes": "You have made a change to your answer but not submitted it. Please check your answer and then press the <strong>Save answer</strong> button.", "question.unsubmitted changes_plural": "You have made changes to your answers but not submitted them. Please check your answers to each part and then press the <strong>Save all answers</strong> button.", "question.score feedback.show": "Show feedback", "question.score feedback.hide": "Hide feedback", "question.score feedback.not marked": "Not marked", "question.score feedback.unanswered": "Unanswered", "question.score feedback.partially answered": "Partially answered", "question.score feedback.answered": "Answered", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.correct": "Your answer is correct", "question.score feedback.partial": "Your answer is partially correct", "question.score feedback.wrong": "Your answer is incorrect", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.selector.unsubmitted changes": "Unsubmitted changes.", "question.back to previous part": "Go back to the previous part", "question.explore.no parts defined": "There are no parts defined in this question.", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Time remaining:", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "xml.could not load": "Couldn't load an XML document: {{-message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "scorm.error initialising": "Error initialising SCORM protocol: {{-message}}", "scorm.failed save": "<p>The request to save data to the server failed. Press <b>OK</b> to try again.</p>\n<p>If you get this message repeatedly, check your internet connection or use a different computer. Your previously submitted answers have been successfully saved and will be restored if you resume this session on a different computer.</p>\n<p>If this message appears persistently and you can't save <em>any</em> answers, please contact your lecturer or teacher.</p>", "scorm.no exam suspend data": "Failed to resume: no exam suspend data.", "scorm.error loading suspend data": "Error loading suspend data: {{-message}}", "scorm.error loading question": "Error loading question {{number}}: {{-message}}", "scorm.no question suspend data": "No question suspend data", "scorm.error loading part": "Error loading part {{part}}: {{-message}}", "scorm.no part suspend data": "No part suspend data", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "util.equality not defined for type": "Equality not defined for type {{type}}", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "mark": "mark", "mark_plural": "marks", "answer": "answer", "was": "was", "was_plural": "were", "part": "part", "gap": "gap", "step": "step", "left brace": "{", "right brace": "}", "math.lt.order complex numbers": "Can't order complex numbers", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "matrix input.cell label": "Row {{row}}, column {{column}}", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.end test": "End the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.make a choice": "Make a choice", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "worksheet.answersheet show question content": "Show question content in answer sheets?", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right", "modal.style.text size preview": "Most text will be this big.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback"}}, "sq-al": {"translation": {"page.loading": "Duke u ngarkuar ...", "page.saving": "<p>Duke ruajtur.</p>\n<p>Kjo mund t\u00eb k\u00ebrkoj\u00eb disa seconda.</p>", "mathjax.math processing error": "\"{{-message}}\" gjat\u00eb tex-ifikimit t\u00eb <code>{{expression}}</code>", "die.numbas failed": "Numbas d\u00ebshtoi", "die.sorry": "Na vjen keq, Numbas ka hasur n\u00eb nj\u00eb gabim q\u00eb do t\u00eb thot\u00eb se nuk mund t\u00eb vazhdoj\u00eb. M\u00eb posht\u00eb \u00ebsht\u00eb nj\u00eb p\u00ebrshkrim i gabimit.", "die.error": "Gabim", "modal.ok": "OK", "modal.cancel": "Anulo", "exam.exam name": "Emri i Provimit:", "exam.random seed": "ID e Sesionit:", "exam.student name": "Emri i Studentit:", "exam.number of questions": "Numri i Pyetjeve:", "exam.marks available": "Vler\u00ebsime n\u00eb dispozicion:", "exam.pass percentage": "P\u00ebrqindja p\u00ebr t\u00eb kaluar:", "exam.time allowed": "Koha e lejuar:", "exam.passed": "Pranohet", "exam.failed": "Nuk pranohet", "exam.review header": "Koment: ", "frontpage.start": "Fillo", "suspend.paused header": "I nd\u00ebrprer\u00eb p\u00ebrkoh\u00ebsisht", "suspend.exam suspended": "Provimi \u00ebsht\u00eb nd\u00ebrprer\u00eb p\u00ebrkoh\u00ebsisht. Shtypni <em>Vazhdo</em> p\u00ebr t\u00eb vazhduar.", "suspend.you can resume": "Ju mund ta vazhdoni k\u00ebt\u00eb sesion aty ku e keni l\u00ebn\u00eb, kur t\u00eb filloni k\u00ebt\u00eb aktivitet her\u00ebn tjet\u00ebr.", "suspend.resume": "Vazhdo", "result.exit": "Dil nga Provimi", "result.print": "Printoni k\u00ebt\u00eb p\u00ebrmbledhje t\u00eb rezultateve", "result.exam summary": "P\u00ebrmbledhje e provimit", "result.performance summary": "P\u00ebrmbledhje e rezultateve", "result.exam start": "Fillimi i Provimit:", "result.exam stop": "Mbarimi i Provimit:", "result.time spent": "Koha e p\u00ebrdorur:", "result.questions attempted": "Pyetjte t\u00eb tratjuara:", "result.score": "Vler\u00ebsimi:", "result.result": "Rezultati:", "result.question number": "Pyetja Nr", "result.question score": "Vler\u00ebsimi", "result.question review title": "Komento k\u00ebt\u00eb pyetje", "result.click a question to review": "Kliko mbi nj\u00eb num\u00ebr pyetjeje p\u00ebr t\u00eb par\u00eb se si jan\u00eb vler\u00ebsuar p\u00ebrgjigjet tuaja dhe kur \u00ebsht\u00eb e mundur, zgjidhjet e plota.", "end.exam has finished": "Provimi ka mbaruar. Tani mund ta mbyllni k\u00ebt\u00eb dritare.", "control.confirm leave": "Akoma nuk e keni mbaruar provimin.", "control.not all questions answered": "Nuk i keni trajtuar t\u00eb gjitha pyetjet e provimit.", "control.not all questions submitted": "Ju keni b\u00ebr\u00eb ndryshime n\u00eb nj\u00eb ose m\u00eb shum\u00eb p\u00ebrgjigje, por nuk i keni dor\u00ebzuar ato. Ju lutemi kontrolloni q\u00eb \u00e7do pyetje t\u00eb jet\u00eb dor\u00ebzuar.", "control.confirm end": "Jeni t\u00eb sigurt q\u00eb doni t\u00eb p\u00ebrfundoni provimin? Pas k\u00ebsaj, nuk do t\u00eb jeni n\u00eb gjendje t\u00eb ndryshoni asnj\u00eb nga p\u00ebrgjigjet tuaja.", "control.confirm regen": "D\u00ebshironi t\u00eb ri-krijoni k\u00ebt\u00eb pyetje? N\u00ebse klikoni OK, t\u00eb gjitha p\u00ebrgjigjet dhe vler\u00ebsimet tuaja p\u00ebr pyetjen aktuale do t\u00eb humbasin.", "control.confirm reveal": "D\u00ebshironi t\u00eb zbuloni p\u00ebrgjigjen p\u00ebr k\u00ebt\u00eb pyetje? \u00c7do vler\u00ebsim q\u00eb keni marr\u00eb deri tani do t\u00eb bllokohet dhe nuk do t\u00eb jeni n\u00eb gjendje t'i p\u00ebrgjigjeni k\u00ebsaj pyetjeje m\u00eb von\u00eb.", "control.proceed anyway": "Vazhdo gjith\u00ebsesi?", "control.regen": "Provo nj\u00eb pyetje tjet\u00ebr t\u00eb ngjashme", "control.submit answer": "Dor\u00ebzo p\u00ebrgjigjen", "control.submit all parts": "Dor\u00ebzo t\u00eb gjitha", "control.submit again": "Dor\u00ebzo p\u00ebrs\u00ebri", "control.submit": "Dor\u00ebzo", "control.previous": "Paraardh\u00ebs", "control.next": "Pasardh\u00ebs", "control.advice": "Udh\u00ebzim", "control.reveal": "Zbulo p\u00ebrgjigjet", "control.total": "Total", "control.pause": "Nd\u00ebrprit p\u00ebrkoh\u00ebsisht", "control.end exam": "P\u00ebrfundo Provimin", "control.back to results": "Kthehu tek rezultatet", "display.part.jme.error making maths": "Gabim n\u00eb paraqitjen e shprehjeve matematikore", "exam.xml.bad root": "Elementi rr\u00ebnj\u00eb i provimit XML duhet t\u00eb jet\u00eb 'exam'", "exam.changeQuestion.no questions": "Provimi nuk p\u00ebrmban asnj\u00eb pyetje! Kontrolloni skedarin .exam p\u00ebr gabime.", "feedback.you were awarded": "Jeni vler\u00ebsuar me <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) zbritur.", "jme.tokenise.invalid": "Shprehje e gabuar: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Nuk ka argumente t\u00eb mjaftuesh\u00ebm p\u00ebr operatorin <code>{{op}}</code>", "jme.shunt.no left bracket in function": "Nuk kombinon kllapa majtas n\u00eb aplikimin e funksionit ose tek sistemi i renditur", "jme.shunt.no left square bracket": "Nuk kombinon kllapa majtas", "jme.shunt.no left bracket": "Nuk kombinon kllapa majtas", "jme.shunt.no right bracket": "Nuk kombinon kllapa djathtas", "jme.shunt.no right square bracket": "Nuk kombinon kllapa djathtas p\u00ebr t\u00eb mbyllur list\u00ebn", "jme.shunt.missing operator": "Shprehja nuk mund t\u00eb vler\u00ebsohet - mungon nj\u00eb operator.", "jme.typecheck.function maybe implicit multiplication": "Funksioni <code>{{name}}</code> nuk \u00ebsht\u00eb i p\u00ebrcaktuar. Mos keni dashur:<br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Funksioni <code>{{op}}</code> nuk \u00ebsht\u00eb i p\u00ebrcaktuar. A \u00ebsht\u00eb <code>{{op}}</code> nj\u00eb variab\u00ebl dhe mos keni dashur: <br/><code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Veprimi '{{op}}' nuk \u00ebsht\u00eb i p\u00ebrcaktuar.", "jme.typecheck.no right type definition": "Nuk ka nj\u00eb p\u00ebrcaktim t\u00eb '{{op}}' p\u00ebr tipin e gjetur.", "jme.typecheck.no right type unbound name": "Variabli <code>{{name}}</code> nuk \u00ebsht\u00eb i p\u00ebrcaktuar.", "jme.typecheck.map not on enumerable": "veprimi <code>map</code> duhet t\u00eb p\u00ebrdoret p\u00ebr nj\u00eb list\u00eb ose zon\u00eb, interval, jo p\u00ebr {{type}}", "jme.evaluate.undefined variable": "Variabli {{name}} nuk \u00ebsht\u00eb i p\u00ebrcaktuar", "jme.thtml.not html": "Konstruktorit THTML i \u00ebsht\u00eb kaluar nj\u00eb vler\u00eb jo-HTML.", "jme.func.switch.no default case": "Asnj\u00eb rast i parazgjedhur p\u00ebr deklarat\u00ebn Switch", "jme.func.listval.invalid index": "Indeks liste i gabuar {{index}} n\u00eb list\u00ebn me madh\u00ebsi {{size}}", "jme.func.listval.not a list": "Objekti nuk \u00ebsht\u00eb i p\u00ebrshkruesh\u00ebm", "jme.func.matrix.invalid row type": "Nuk mund t\u00eb krijohet nj\u00eb matric\u00eb me rreshta t\u00eb tipit {{type}}", "jme.func.except.continuous range": "Nuk mund t\u00eb p\u00ebrdoret operatori 'except' n\u00eb intervale t\u00eb vazhduar.", "jme.matrix.reports bad size": "Matrix reports its size incorrectly - must be an error in constructor function", "jme.texsubvars.no right bracket": "No matching <code>]</code> in {{op}} arguments.", "jme.texsubvars.missing parameter": "Mungon parametri n\u00eb {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "No matching <code>}</code> in {{op}}", "jme.user javascript.error": "Error in user-defined javascript function <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Error making function <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Gabim sintaksor\u00eb gjat\u00eb definimit t\u00eb funksionit", "jme.variables.variable not defined": "Variabli <code>{{name}}</code> nuk \u00ebsht\u00eb i p\u00ebrcaktuar.", "jme.variables.empty definition": "P\u00ebrcaktimi i variablit <code>{{name}}</code> \u00ebsht\u00eb bosh.", "jme.variables.circular reference": "Referenc\u00eb rrethore n\u00eb p\u00ebrcaktimin e variablit <code>{{name}}</code>", "jme.variables.error computing dependency": "Error computing referenced variable <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error evaluating variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "A valid set of question variables was not generated in time.", "jme.display.unknown token type": "Can't texify token type {{type}}", "jme.display.collectRuleset.no sets": "No sets given to collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} has not been defined", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree must be given a Scope", "math.precround.complex": "Can't round to a complex number of decimal places", "math.siground.complex": "Can't round to a complex number of sig figs", "math.combinations.complex": "Can't compute combinations of complex numbers", "math.permutations.complex": "Can't compute permutations of complex numbers", "math.gcf.complex": "Smund t\u00eb llogaritet GCF i numrave kompleks", "math.lcm.complex": "Can't compute LCM of complex numbers", "math.lt.order complex numbers": "Can't order complex numbers", "math.choose.empty selection": "Empty selection given to random function", "matrixmath.abs.non-square": "Nuk mund t\u00eb llogaritet p\u00ebrcaktori i nj\u00eb matrice jo katrore.", "matrixmath.abs.too big": "Na vjen keq, akoma nuk mund t\u00eb llogarisim p\u00ebrcaktorin e nj\u00eb matrice m\u00eb t\u00eb madhe se 3x3.", "matrixmath.mul.different sizes": "Nuk mund t\u00eb shum\u00ebzohen dy matrica me p\u00ebrmasa t\u00eb ndryshme.", "vectormath.cross.not 3d": "Can only take the cross product of 3-dimensional vectors.", "vectormath.dot.matrix too big": "Can't calculate dot product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "vectormath.cross.matrix too big": "Can't calculate cross product of a matrix which isn't $1 \\times N$ or $N \\times 1$.", "part.with steps answer prompt": "P\u00ebrgjigje: ", "part.script.error": "Error in part {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Meqen\u00ebse keni marr\u00eb t\u00eb gjtha pik\u00ebt p\u00ebr pjes\u00ebn, p\u00ebrgjigjet p\u00ebr hapat nuk do t\u00eb llogariten.", "part.marking.revealed steps no penalty": "Keni zbuluar hapat.", "part.marking.used variable replacements": "Kjo k\u00ebrkes\u00eb \u00ebsht\u00eb vler\u00ebsuar duke p\u00ebrdorur p\u00ebrgjigjet e dh\u00ebna n\u00eb k\u00ebrkesat paraardh\u00ebse.", "part.marking.variable replacement part not answered": "Duhet ti p\u00ebrgjigjeni {{part}} m\u00eb par\u00eb.", "part.marking.resubmit because of variable replacement": "Vler\u00ebsimi i k\u00ebsaj k\u00ebrkese varet nga p\u00ebrgjigjet e k\u00ebrkesave t\u00eb tjera, t\u00eb cilat ju i keni ndryshuar. Dor\u00ebzoni p\u00ebrs\u00ebri p\u00ebrgjigjen p\u00ebr k\u00ebt\u00eb k\u00ebrkes\u00eb p\u00ebr t\u00eb azhornuar rezultatin.", "part.marking.not submitted": "Nuk \u00ebsht\u00eb dor\u00ebzuar p\u00ebrgjigje.", "part.marking.did not answer": "Nuk i jeni p\u00ebrgjigjur k\u00ebsaj pyetjeje.", "part.marking.nothing entered": "Nuk keni dh\u00ebn\u00eb nj\u00eb p\u00ebrgjigje.", "part.marking.incorrect": "P\u00ebrgjigja juaj \u00ebsht\u00eb e pasakt\u00eb.", "part.marking.correct": "P\u00ebrgjigja juaj \u00ebsht\u00eb e sakt\u00eb.", "part.marking.uncaught error": "Gabim n\u00eb vler\u00ebsim: {{-message}}", "part.marking.no result": "Kjo k\u00ebrkes\u00eb nuk mund t\u00eb vler\u00ebsohet.", "part.correct answer": "P\u00ebrgjigja e pritshme:", "part.missing type attribute": "{{part}}: Missing part type attribute", "part.unknown type": "{{part}}: Unrecognised part type {{type}}", "part.setting not present": "Property '{{property}}' not set", "part.jme.answer missing": "Mungon p\u00ebrgjigja e sakt\u00eb", "part.jme.answer too long": "P\u00ebrgjigja juaj \u00ebsht\u00eb shum\u00eb e gjat\u00eb.", "part.jme.answer too short": "P\u00ebrgjigja juaj \u00ebsht\u00eb shum\u00eb e shkurt\u00eb.", "part.jme.answer invalid": "P\u00ebrgjigja juaj nuk \u00ebsht\u00eb nj\u00eb shprehje e vlefshme matematike.<br/>{{-message}}.", "part.jme.marking.correct": "P\u00ebrgjigja juaj \u00ebsht\u00eb numerikisht e sakt\u00eb.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "P\u00ebrgjigja juaj duhet t\u00eb p\u00ebrmbaj\u00eb: {{strings}}", "part.jme.must-have several": "P\u00ebrgjigja juaj duhet t\u00eb p\u00ebrmbaj\u00eb t\u00eb gjitha fjal\u00ebt: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "P\u00ebrgjigja juaj nuk duhet t\u00eb p\u00ebrmbaj\u00eb: {{strings}}", "part.jme.not-allowed several": "P\u00ebrgjigja juaj nuk duhet t\u00eb p\u00ebrmbaj\u00eb asnj\u00eb nga fjal\u00ebt: {{strings}}", "part.jme.unexpected variable name": "P\u00ebrgjigja juaj \u00ebsht\u00eb interpretuar sikur p\u00ebrdor variablin e pap\u00ebrcaktuar <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "P\u00ebrgjigja juaj \u00ebsht\u00eb interpretuar sikur p\u00ebrdor variablin e pap\u00ebrcaktuar <code>{{name}}</code>. Ndoshta keni dashur <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Paraqitja e p\u00ebrgjigjes mungon", "part.patternmatch.correct except case": "P\u00ebrgjigja juaj \u00ebsht\u00eb e sakt\u00eb, p\u00ebrve\u00e7 k\u00ebtij rasti.", "part.numberentry.correct except decimal": "P\u00ebrgjigja juaj \u00ebsht\u00eb brenda intervalit t\u00eb lejuar, por nuk pranohen numrat dhjetor\u00eb.", "part.numberentry.correct except fraction": "P\u00ebrgjigja juaj \u00ebsht\u00eb brenda intervalit t\u00eb lejuar, por nuk pranohen thyesat.", "part.numberentry.answer invalid": "Nuk keni shkruar nj\u00eb num\u00ebr.", "part.numberentry.answer not integer": "P\u00ebrgjigja jua \u00ebsht\u00eb e gabuar. Duhet t\u00eb shkruani nj\u00eb num\u00ebr t\u00eb plot\u00eb dhe jo nj\u00eb num\u00ebr dhjetor.", "part.numberentry.answer not integer or decimal": "P\u00ebrgjigja jua \u00ebsht\u00eb e gabuar. Duhet t\u00eb shkruani nj\u00eb num\u00ebr t\u00eb plot\u00eb ose dhjetor.", "part.numberentry.zero sig fig": "Kjo k\u00ebrkes\u00eb \u00ebsht\u00eb nd\u00ebrtuar n\u00eb m\u00ebnyr\u00eb q\u00eb t\u00eb rrumbullakos\u00eb p\u00ebrgjigjen e studentit me zero shifra me vlere, gj\u00eb q\u00eb nuk ka kuptim.", "part.mcq.options def not a list": "The expression defining the {{properties}} is not a list.", "part.mcq.marking matrix string empty": "The custom marking matrix expression is empty.", "part.mcq.choices missing": "Definition of choices is missing", "part.mcq.matrix not a number": "Part {{part}} marking matrix cell ({{row}},{{column}}) does not evaluate to a number", "part.mcq.wrong number of choices": "Keni zgjedhur numrin e gabuar t\u00eb opsioneve.", "part.mcq.no choices selected": "Nuk \u00ebsht\u00eb b\u00ebr\u00eb asnj\u00eb zgjedhje.", "part.mcq.matrix not a list": "Marking matrix, defined by JME expression, is not a list but it should be.", "part.mcq.matrix wrong type": "Element of invalid type '{{type}}' used in marking matrix.", "part.mcq.matrix mix of numbers and lists": "Mix of numbers and lists used in marking matrix.", "part.mcq.matrix wrong size": "Marking matrix is the wrong size.", "part.mcq.correct choice": "Keni zgjedhur nj\u00eb p\u00ebrgjigje t\u00eb sakt\u00eb.", "part.matrix.answer invalid": "P\u00ebrgjigja juaj nuk \u00ebsht\u00eb e sakt\u00eb.", "part.matrix.invalid cell": "Nj\u00eb ose m\u00eb shum\u00eb qeliza n\u00eb p\u00ebrgjigjen tuaj jan\u00eb bosh ose t\u00eb pavlefshme.", "part.matrix.some incorrect": "Nj\u00eb ose m\u00eb shum\u00eb qeliza n\u00eb p\u00ebrgjigjen tuaj jan\u00eb jo t\u00eb sakta, por jeni vler\u00ebsuar mbi pjes\u00ebn tjet\u00ebr.", "part.matrix.empty": "Nuk keni shkruar nj\u00eb p\u00ebrgjigje.", "part.matrix.empty cell": "Nj\u00eb ose m\u00eb shum\u00eb qeliza n\u00eb p\u00ebrgjigjen tuaj jan\u00eb bosh.", "part.matrix.size mismatch": "Autori i pyetjes nuk i ka lejuar studentit t\u00eb vendos\u00eb dimensionin e p\u00ebrgjgigjes, por p\u00ebrgjigja e sakt\u00eb \u00ebsht\u00eb me {{correct_dimensions}} nd\u00ebrsa p\u00ebrgjigja e dh\u00ebn\u00eb \u00ebsht\u00eb me {{input_dimensions}}", "part.gapfill.feedback header": "<strong>Hap\u00ebsir\u00eb {{name}}</strong>", "part.extension.not implemented": "K\u00ebrkesa nuk ka implementuar metod\u00ebn <code>{{name}}</code>.", "question.loaded name mismatch": "Nuk mund t\u00eb vazhdohet ky sesion, sepse aplikacioni ka ndryshuar.", "question.error": "Pyetja {{number}}: {{-message}}", "question.preamble.error": "Error in preamble: {{-message}}", "question.preamble.syntax error": "Syntax error in preamble", "question.unsupported part type": "Unsupported part type", "question.header": "Pyetja {{number}}", "question.submit part": "Dor\u00ebzo k\u00ebt\u00eb p\u00ebrgjigje", "question.show steps": "Paraqit hapat", "question.show steps penalty": "Do ju zbriten <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Vler\u00ebsimi nuk do t\u00eb ndikohet.", "question.show steps already penalised": "Tashm\u00eb ju i kani paraqitur hapat. Paraqitja e tyre p\u00ebrs\u00ebri nuk ju penalizon.", "question.hide steps": "Fshih hapat", "question.hide steps no penalty": "Vler\u00ebsimi nuk do t\u00eb ndikohet.", "question.advice": "Udh\u00ebzim", "question.no such part": "K\u00ebrkesa {{path}} nuk mund t\u00eb gjendet", "question.can not submit": "P\u00ebrgjigja nuk mund t\u00eb dor\u00ebzohet - kontrolloni p\u00ebr gabime.", "question.answer submitted": "P\u00ebrgjigja u dor\u00ebzua", "question.score feedback.show": "Paraqit vler\u00ebsim", "question.score feedback.hide": "Fshih vler\u00ebsim", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.answered": "Me p\u00ebrgjigje", "question.score feedback.unanswered": "Pa p\u00ebrgjigje", "question.score feedback.correct": "P\u00ebrgjigja juaj \u00ebsht\u00eb e sakt\u00eb", "question.score feedback.partial": "P\u00ebrgjigja juaj \u00ebsht\u00eb pjes\u00ebrisht e sakt\u00eb", "question.score feedback.wrong": "P\u00ebrgjigja juaj \u00ebsht\u00eb e gabuar", "question.selector.unsubmitted changes": "Ndryshime t\u00eb pa dor\u00ebzuara.", "timing.no accumulator": "nuk ka akumulator kohor {{name}}", "timing.time remaining": "Koha e mbetur:", "xml.could not load": "Nuk mund t\u00eb ngarkohet dokumenti XML: {{-message}}", "xml.property not number": "Property {{name}} should be a number, but isn't ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} should be a boolean, but isn't ({{value}}), in node {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "Error initialising SCORM protocol: {{-message}}", "scorm.failed save": "<p>K\u00ebrkesa p\u00ebr t\u00eb ruajtur t\u00eb dh\u00ebnat n\u00eb server d\u00ebshtoi. Klikoni <b>OK</b> p\u00ebr ta provuar p\u00ebrs\u00ebri.</p>\n<p>N\u00ebse ky mesazh p\u00ebrs\u00ebritet disa her\u00eb, kontrolloni lidhjen tuaj me internetin ose p\u00ebrdorni nj\u00eb kompjuter tjet\u00ebr. P\u00ebrgjigjet tuaja t\u00eb m\u00ebparshme jan\u00eb ruajtur me sukses dhe do t\u00eb ringarkohen n\u00ebse e vazhdoni k\u00ebt\u00eb sesion n\u00eb nj\u00eb kompjuter tjet\u00ebr.</p>\n<p>N\u00ebse ky mesazh shafet vazhdimisht dhe nuk mund t\u00eb ruani <em>asnj\u00eb</em> p\u00ebrgjigje, ju lutem kontaktoni pedagogun ose m\u00ebsuesin.</p>", "scorm.no exam suspend data": "Vazhdimi i sesionit d\u00ebshtoi: nuk ka t\u00eb dh\u00ebna mbi provimin.", "scorm.error loading suspend data": "Gabim n\u00eb ngarkimin e t\u00eb dh\u00ebnave t\u00eb sesionit: {{-message}}", "scorm.error loading question": "Gabim n\u00eb ngarkimin e pyetjes {{number}}: {{-message}}", "scorm.no question suspend data": "Nuk ka t\u00eb dh\u00ebna t\u00eb pezulluara p\u00ebr pyetjen", "scorm.error loading part": "Gabim n\u00eb ngarkimin e k\u00ebrkes\u00ebs {{part}}: {{-message}}", "scorm.no part suspend data": "Nuk ka t\u00eb dh\u00ebna t\u00eb pezulluara p\u00ebr k\u00ebrkes\u00ebn", "util.product.non list": "Passed a non-list to <code>Numbas.util.product</code>", "mark": "pik\u00eb", "was": "ishte", "part": "k\u00ebrkes\u00eb", "gap": "hap\u00ebsir\u00eb", "step": "hap", "jme.substituteTree.undefined variable": "Variab\u00ebl i pap\u00ebrcaktuar: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "Jeni vler\u00ebsuar me <strong>{{count,niceNumber}}</strong> $t(mark) p\u00ebr p\u00ebrgjigjet e dh\u00ebna tek hapat.", "part.marking.revealed steps with penalty": "Keni paraqitur hapat. Maksimumi i vler\u00ebsimit p\u00ebr k\u00ebt\u00eb k\u00ebrkes\u00eb \u00ebsht\u00eb <strong>{{count,niceNumber}}</strong> $t(mark). Vler\u00ebsimi juaj do t\u00eb ulet bazuar mbi k\u00ebt\u00eb.", "part.marking.total score": "Jeni vler\u00ebsuar me <strong>{{count,niceNumber}}</strong> $t(mark) p\u00ebr k\u00ebt\u00eb k\u00ebrkes\u00eb.", "part.numberentry.precision type.dp": "shif\u00ebr dhjetore", "part.numberentry.precision type.dp_plural": "shifra dhjetore", "part.numberentry.precision type.sigfig": "shif\u00ebr me vler\u00eb", "part.numberentry.precision type.sigfig_plural": "shifra me vler\u00eb", "part.numberentry.give your answer to precision": "Rrumbullakoseni p\u00ebrgjigjen me {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Keni ndryshuar p\u00ebrgjigjen por nuk e keni dor\u00ebzuar. Ju lutemi kontrolloni p\u00ebrgjigjen dhe klikoni butonin <strong>Dor\u00ebzo p\u00ebrgjigje</strong>.", "question.unsubmitted changes_plural": "Keni ndryshuar p\u00ebrgjigjet por nuk i keni dor\u00ebzuar. Ju lutemi kontrolloni p\u00ebrgjigjet dhe klikoni butonin <strong>Dor\u00ebzo t\u00eb gjitha p\u00ebrgjigjet</strong>.", "util.equality not defined for type": "Barazim i pa definuar p\u00ebr tipin {{type}}", "mark_plural": "pik\u00eb", "was_plural": "jan\u00eb", "die.script not loaded": "Numbas nuk mund t\u00eb filloj\u00eb pasi skedari <code>{{file}}</code> nuk \u00ebsht\u00eb ngarkuar. Sigurohuni q\u00eb ai t\u00eb jet\u00eb i p\u00ebrfshir\u00eb n\u00eb <code>scripts.js</code>.", "math.combinations.n less than zero": "Can't compute combinations: n is less than zero", "math.combinations.k less than zero": "Can't compute combinations: k is less than zero", "math.combinations.n less than k": "Can't compute combinations: n is less than k", "math.permutations.n less than zero": "Can't compute permutations: n is less than zero", "math.permutations.k less than zero": "Can't compute permutations: k is less than zero", "math.permutations.n less than k": "Can't compute permutations: n is less than k", "part.numberentry.give your answer to precision_0": "Rrumbullakoseni p\u00ebrgjigjen n\u00eb numrin e plot\u00eb m\u00eb t\u00eb af\u00ebrt.", "mathjax.error": "MathJax processing error: {{-message}}", "mathjax.error with context": "MathJax processing error in {{-context}}: {{-message}}", "exam.introduction": "Prezantim i provimit", "exam.feedback": "Mesazhi i vler\u00ebsimit t\u00eb provimit", "jme.tokenise.keypair key not a string": "Dictionary key should be a string, not {{type}}.", "jme.shunt.list mixed argument types": "Can't parse {{mode}}: mix of dictionary and list elements", "jme.func.listval.key not in dict": "Dictionary does not contain the key <code>{{key}}</code>", "part.prompt": "prompt", "part.feedback": "vler\u00ebsim", "part.numberentry.answer not reduced": "P\u00ebrgjigja juaj nuk \u00ebsht\u00eb thjeshtuar n\u00eb termat m\u00eb t\u00eb vegj\u00ebl.", "part.numberentry.give your answer as a reduced fraction": "Thjeshtoni p\u00ebrgjigjen n\u00eb termat m\u00eb t\u00eb vegj\u00ebl.", "part.numberentry.negative decimal places": "This part is set up to round the student's answer to a negative number of decimal places, which has no meaning.", "part.mcq.choices": "opsione", "part.mcq.answers": "p\u00ebrgjigje", "part.mcq.matrix cell empty": "Part {{part}} marking matrix cell ({{row}},{{column}}) is empty", "part.mcq.matrix jme error": "Part {{part}} marking matrix cell ({{row}},{{column}}) gives a JME error: {{-error}}", "question.statement": "Formulimi", "ruleset.circular reference": "Circular reference in definition of ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} has not been defined", "jme.evaluate.no scope given": "Numbas.jme.evaluate must be given a Scope", "question.score feedback.answered total actual": "Vler\u00ebsimi: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Me p\u00ebrgjigje.", "question.score feedback.answered actual": "Vler\u00ebsimi: {{scoreString}}", "question.score feedback.unanswered total": "{{marksString}}.", "answer.number.not a number": "P\u00ebrgjigja juaj nuk \u00ebsht\u00eb num\u00ebr.", "answer.number.fractions not allowed": "Nuk mund t\u00eb shkruani thyes\u00eb.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Nuk mund t\u00eb shkruani thyesa.", "answer.matrix.some cell not a number": "Vlerate n\u00eb nj\u00eb ose disa qeliza t\u00eb p\u00ebrgjigjes nuk jan\u00eb numra.", "exam.enter password": "Fjal\u00ebkalimi:", "exam.password.correct": "Fjal\u00ebkalimi \u00ebsht\u00eb i sakt\u00eb. Ju mund t\u00eb filloni provimin.", "exam.password.incorrect": "Fjal\u00ebkalimi nuk \u00ebsht\u00eb i sakt\u00eb.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Review", "control.confirm regen no marks": "Doni ta ri-randomizoni k\u00ebt\u00eb pyetje?", "control.confirm reveal no marks": "Doni ti shfaqni p\u00ebrgjigjet e k\u00ebsaj pyetjeje?", "jme.tokenise.invalid near": "Shprehje e gabuar: <code>{{expression}}</code> n\u00eb pozicionin {{position}} pran\u00eb <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Nuk mund t\u00eb b\u00ebhet konvertim automatik nga {{from}} n\u00eb {{to}}.", "jme.display.simplifyTree.empty expression": "Shprehja \u00ebsht\u00eb bosh", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "Ky vler\u00ebsim bazohet n\u00eb p\u00ebrgjigjen e fundit t\u00eb dor\u00ebzuar nga ju. Dor\u00ebzoni p\u00ebrgjigjen e ndryshuar p\u00ebr t\u00eb marr\u00eb nj\u00eb vleresim t\u00eb p\u00ebrdit\u00ebsuar.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Keni zgjedhur nj\u00eb p\u00ebrgjigje jo t\u00eb sakt\u00eb.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "E pa vler\u00ebsuar", "question.score feedback.partially answered": "P\u00ebrgjigjur pjes\u00ebrisht", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Paraqit menun\u00eb e navigimit", "part.input title": "Answer for part {{name}}", "part.correct answer title": "P\u00ebrgjigja e pritshme e k\u00ebrkes\u00ebs {{name}}", "part.jme.must-match.failed": "P\u00ebrgjigja juaj nuk \u00ebsht\u00eb n\u00eb form\u00ebn e duhur.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>Nj\u00eb ose disa nga k\u00ebrkesat vijuese varen nga p\u00ebrgjigjet e k\u00ebsaj k\u00ebrkese. Duke e dor\u00ebzuar edhe nj\u00eb her\u00eb k\u00ebt\u00eb k\u00ebrkes\u00eb do t\u00eb anulohen p\u00ebrgjigjet e tyre, dhe do t\u00eb hiqen nga pyetja. Ky verpim nuk mund t\u00eb kthehet.</p>\n<p>Doni ta dor\u00ebzoni k\u00ebt\u00eb k\u00ebrkes\u00eb p\u00ebrs\u00ebri?</p>", "control.back to menu": "Kthehu pas tek menuja", "display.error making html": "Gabim n\u00eb krijimin e HTML n\u00eb {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} n\u00eb <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Zgjidhni nj\u00eb pyetje.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "P\u00ebrgjigja jua \u00ebsht\u00eb e gabuar. Duhet t\u00eb shkruani nj\u00eb num\u00ebr t\u00eb plot\u00eb, dhjetor ose thyes\u00eb.", "question": "Pyetje", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Kthehu tek k\u00ebrkesa paraardh\u00ebse", "end.print": "Printo transkriptin e provimit", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Madh\u00ebsia", "matrix input.rows": "Rreshta", "matrix input.columns": "Kolona", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Opsione t\u00eb paraqitjes", "part.marking.partially correct": "P\u00ebrgjigja \u00ebsht\u00eb pjes\u00ebrisht e sakt\u00eb.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "de-de": {"translation": {"page.loading": "Wird geladen...", "page.saving": "<p>Wird gespeichert.</p>\n<p>Dies kann einen Moment dauern.</p>", "mathjax.math processing error": "\"{{-message}}\" beim TeXen von <code>{{expression}}</code>", "die.numbas failed": "Ausf\u00fchren von Numbas ist gescheitert", "die.sorry": "Entschuldigung, Numbas hat einen Fehler festgestellt, der eine weitere Bearbeitung unm\u00f6glich macht. Es folgt eine Beschreibung des Fehlers.", "die.error": "Fehler", "modal.ok": "OK", "modal.cancel": "Abbrechen", "exam.exam name": "Testname:", "exam.random seed": "Session ID:", "exam.student name": "Name der/s Teilnehmer/in:", "exam.number of questions": "Anzahl der Aufgaben:", "exam.marks available": "Erreichbare Punkte:", "exam.pass percentage": "Notwendige Punktzahl (in Prozent):", "exam.time allowed": "Erlaubte Zeit:", "exam.passed": "Bestanden", "exam.failed": "Nicht bestanden", "exam.review header": "\u00dcberblick: ", "frontpage.start": "Start", "suspend.paused header": "Angehalten", "suspend.exam suspended": "Der Test wurde unterbrochen. Klicken Sie <em>Fortfahren</em> um weiterzumachen.", "suspend.you can resume": "Sie k\u00f6nnen diese Sitzung beim n\u00e4chsten Start fortsetzen.", "suspend.resume": "Fortfahren", "result.exit": "Test beenden", "result.print": "Ergebnisse ausdrucken", "result.exam summary": "Zusammenfassung des Tests", "result.performance summary": "Zusammenfassung der Leistung", "result.exam start": "Test begonnen:", "result.exam stop": "Test beendet:", "result.time spent": "Vergangene Zeit:", "result.questions attempted": "Bearbeitete Aufgaben:", "result.score": "Punktzahl:", "result.result": "Ergebnis:", "result.question number": "Aufgabennummer", "result.question score": "Punkte", "result.question review title": "Aufgabe nochmals durchsehen", "result.click a question to review": "Klicken Sie auf eine Aufgabennummer, um die Bewertung Ihrer Antworten und, wenn vorhanden, die L\u00f6sungen zu sehen.", "end.exam has finished": "Der Test wurde beendet. Sie k\u00f6nnen das Fenster nun schlie\u00dfen.", "control.confirm leave": "Sie haben den Test noch nicht beendet.", "control.not all questions answered": "Sie haben nicht alle Aufgaben in diesem Test bearbeitet.", "control.not all questions submitted": "Sie haben eine oder mehrere Antworten ge\u00e4ndert, aber nicht eingereicht. \u00dcberpr\u00fcfen Sie bitte, dass alle Antworten eingereicht wurden.", "control.confirm end": "Sind Sie sich sicher den Test zu beenden? Nach Beendigung des Tests k\u00f6nnen Sie ihre Antworten nicht mehr \u00e4ndern.", "control.confirm regen": "Wollen Sie diese Aufgabe mit neuen zuf\u00e4lligen Werten bearbeiten? Wenn Sie OK klicken, gehen alle ihre Antworten und Punkte f\u00fcr die aktuelle Aufgabe verloren.", "control.confirm reveal": "Wollen Sie die L\u00f6sung zu dieser Aufgabe ansehen? Alle bisher erreichten Punkte bleiben erhalten und sie k\u00f6nnen diese Aufgabe sp\u00e4ter nicht nochmal bearbeiten.", "control.proceed anyway": "Dennoch fortfahren?", "control.regen": "Eine andere Aufgabe von dieser Art probieren", "control.submit answer": "Antwort einreichen", "control.submit all parts": "Alle Abschnitte einreichen", "control.submit again": "Erneut einreichen", "control.submit": "Einreichen", "control.previous": "Vorhergehend", "control.next": "N\u00e4chste", "control.advice": "Hinweis", "control.reveal": "Antworten aufdecken", "control.total": "Gesamtergebnis", "control.pause": "Pause", "control.end exam": "Test beenden", "control.back to results": "Zur\u00fcck zu den Ergebnissen", "display.part.jme.error making maths": "Fehler bei der mathematischen Darstellung", "exam.xml.bad root": "Grundelement des Test XML sollte 'exam' sein", "exam.changeQuestion.no questions": "Dieser Test enth\u00e4lt keine Aufgaben! \u00dcberpr\u00fcfen sie die .exam Datei auf Fehler.", "feedback.you were awarded": "Sie erhalten <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) abgezogen.", "jme.tokenise.invalid": "Ung\u00fcltiger Ausdruck: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Nicht gen\u00fcgend Argumente f\u00fcr die Funktion <code>{{op}}</code>", "jme.shunt.no left bracket in function": "Fehlende linke Klammer in der Funktion oder dem Tupel", "jme.shunt.no left square bracket": "Keine passende linke eckige Klammer", "jme.shunt.no left bracket": "Keine passende linke Klammer", "jme.shunt.no right bracket": "Keine passende rechte Klammer", "jme.shunt.no right square bracket": "Keine passende rechte eckige Klammer", "jme.shunt.missing operator": "Ausdruck kann nicht ausgewertet werden - ein Operator fehlt.", "jme.typecheck.function maybe implicit multiplication": "Funktion <code>{{name}}</code> ist nicht definiert. Meinen Sie <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Funktion <code>{{op}}</code> ist nicht definiert. Ist <code>{{op}}</code> eine Variable, und meinen Sie <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operation '{{op}}' ist nicht definiert.", "jme.typecheck.no right type definition": "Keine Definition von '{{op}}' mit korrektem Typ gefunden.", "jme.typecheck.no right type unbound name": "Variable <code>{{name}}</code> ist nicht definiert.", "jme.typecheck.map not on enumerable": "<code>map</code> operiert auf einer Liste oder Bereich nicht auf {{type}}", "jme.evaluate.undefined variable": "Variable {{name}} ist nicht definiert", "jme.thtml.not html": "Ein nicht-HTML Wert wurde an einen THTML Konstruktor \u00fcbergeben.", "jme.func.switch.no default case": "Kein Standardwert f\u00fcr Switch Anweisung", "jme.func.listval.invalid index": "Ung\u00fcltiger Listenindex {{index}} f\u00fcr eine Liste der Gr\u00f6\u00dfe {{size}}", "jme.func.listval.not a list": "Objekt ist nicht indizierbar", "jme.func.matrix.invalid row type": "Matrix kann nicht mit Zeilen vom Typ {{type}} erstellt werden", "jme.func.except.continuous range": "'Exklusiv' Operator kann nicht auf kontinuierliche Bereiche angewendet werden.", "jme.matrix.reports bad size": "Matrix meldet ihre Gr\u00f6\u00dfe falsch - es muss ein Fehler im Konstruktor vorliegen", "jme.texsubvars.no right bracket": "Keine passende <code>]</code> in {{op}} Argumenten.", "jme.texsubvars.missing parameter": "Fehlende Parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Keine passende <code>}</code> in {{op}}", "jme.user javascript.error": "Fehler in Benutzer-definierter Javascript Funktion <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Fehler beim Erstellen der Funktion <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Syntaxfehler in der Definition der Funktion", "jme.variables.variable not defined": "Variable  <code>{{name}}</code> ist nicht definiert.", "jme.variables.empty definition": "Definition der Variable  <code>{{name}}</code> ist leer.", "jme.variables.circular reference": "Zirkul\u00e4re Definition der Variable <code>{{name}}</code>", "jme.variables.error computing dependency": "Fehler bei der Berechnung der referenzierten Variablen <code>{{name}}</code>", "jme.variables.error evaluating variable": "Fehler beim Auswerten der Variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Es konnte kein g\u00fcltiger Variablensatz in angemessener Zeit gebildet werden.", "jme.display.unknown token type": "Token vom Typ {{type}} konnte nicht mit TeX dargestellt werden", "jme.display.collectRuleset.no sets": "Keine Daten gegeben zum Erstellen von Regels\u00e4tzen!", "jme.display.collectRuleset.set not defined": "Regelsatz {{name}} wurde nicht definiert", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree muss ein Geltungsbereich \u00fcbergeben werden", "math.precround.complex": "Beim Runden darf die Anzahl der Dezimalstellen nicht komplex sein", "math.siground.complex": "Beim Runden darf die Anzahl der signifikanten Ziffern nicht komplex sein", "math.combinations.complex": "Kombinationen k\u00f6nnen nicht von komplexen Zahlen berechnet werden", "math.permutations.complex": "Permutationen k\u00f6nnen nicht von komplexen Zahlen angegeben werden", "math.gcf.complex": "ggT kann nicht von komplexen Zahlen berechnet werden", "math.lcm.complex": "kgV kann nicht von komplexen Zahlen berechnet werden", "math.lt.order complex numbers": "Komplexe Zahlen k\u00f6nnen nicht sortiert werden", "math.choose.empty selection": "Leere Auswahl wurde der Zufallsfunktion \u00fcbergeben", "matrixmath.abs.non-square": "Determinanten k\u00f6nnen nur von quadratischen Matrizen berechnet werden.", "matrixmath.abs.too big": "Determinanten k\u00f6nnen im Moment leider nicht f\u00fcr Matrizen gr\u00f6\u00dfer als 3x3 bestimmt werden.", "matrixmath.mul.different sizes": "Matrizen mit verschiedenen Gr\u00f6\u00dfen k\u00f6nnen nicht multipliziert werden.", "vectormath.cross.not 3d": "Nur Kreuzprodukte von 3-dimensionalen Vektoren m\u00f6glich.", "vectormath.dot.matrix too big": "Das Skalarprodukt von Matrizen ist nur f\u00fcr  $1 \\times N$ or $N \\times 1$ definiert.", "vectormath.cross.matrix too big": "Das Kreuzprodukt von Matrizen ist nur f\u00fcr  $1 \\times N$ or $N \\times 1$ definiert.", "part.with steps answer prompt": "Antwort: ", "part.script.error": "Fehler im Abschnitt {{path}} des Benutzerscripts {{script}}: {{-message}}", "part.marking.steps no matter": "Da Sie die volle Punktzahl f\u00fcr diesen Abschnitt erhalten haben, werden ihre Antworten zu den Teilschritten nicht gez\u00e4hlt.", "part.marking.revealed steps no penalty": "Sie haben die Tipps aufgedeckt.", "part.marking.used variable replacements": "Dieser Abschnitt wurde anhand Ihrer Antworten in vorhergehenden Abschnitten bewertet.", "part.marking.variable replacement part not answered": "Sie m\u00fcssen zuerst {{part}} beantworten.", "part.marking.resubmit because of variable replacement": "Die Bewertung dieses Abschnitts h\u00e4ngt von ihren Antworten aus anderen Abschnitten, die sie g\u00e4ndert haben, ab. Reichen sie diesen Abschnitt erneut ein, um ihren Punktestand zu aktualisieren.", "part.marking.not submitted": "Keine Antwort eingereicht.", "part.marking.did not answer": "Sie haben diese Aufgabe nicht beantwortet.", "part.marking.nothing entered": "Sie haben keine Antwort eingegeben.", "part.marking.incorrect": "Ihre Antwort ist nicht richtig.", "part.marking.correct": "Ihre Antwort ist richtig.", "part.marking.uncaught error": "Fehler bei der Bewertung von: {{-message}}", "part.marking.no result": "Dieser Abschnitt kann nicht bewertet werden.", "part.correct answer": "Erwartete Antwort:", "part.missing type attribute": "{{part}}: Fehlendes Attribut f\u00fcr den Typ des Abschnitts", "part.unknown type": "{{part}}: Unbekanntet Typ des Abschnitts {{type}}", "part.setting not present": "Einstellung '{{property}}' nicht gesetzt", "part.jme.answer missing": "Korrekte Antwort fehlt", "part.jme.answer too long": "Ihre Antwort ist zu lang.", "part.jme.answer too short": "Ihre Antwort ist zu kurz.", "part.jme.answer invalid": "Ihre Antwort ist kein g\u00fcltiger mathematischer Ausdruck.<br/>{{-message}}.", "part.jme.marking.correct": "Ihre Antwort ist numerisch korrekt.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Ihre Antwort muss enthalten: {{strings}}", "part.jme.must-have several": "Ihre Antwort muss alle folgenden Teile enthalten: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Ihre Antwort darf nicht enthalten: {{strings}}", "part.jme.not-allowed several": "Ihre Antwort darf keinen der folgenden Teile enthalten: {{strings}}", "part.jme.unexpected variable name": "In ihrer Antwort tritt der unerwartete Variablennamen <code>{{name}}</code> auf.", "part.jme.unexpected variable name suggestion": "In ihrer Antwort tritt der unerwartete Variablennamen <code>{{name}}</code> auf. Meinen sie <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Anzeigeantwort fehlt", "part.patternmatch.correct except case": "Ihre Antwort ist bis auf Gro\u00df-/Kleinschreibung richtig.", "part.numberentry.correct except decimal": "Ihre Antwort ist im erlaubten Intervall, aber Dezimalzahlen sind nicht erlaubt.", "part.numberentry.correct except fraction": "Ihre Antwort ist im erlaubten Intervall, aber Br\u00fcche sind nicht erlaubt.", "part.numberentry.answer invalid": "Sie haben keine g\u00fcltige Zahl eingegeben.", "part.numberentry.answer not integer": "Ihre Antwort ist ung\u00fcltig. Sie m\u00fcssen eine ganze Zahl, nicht eine Dezimalzahl angeben.", "part.numberentry.answer not integer or decimal": "Ihre Antwort ist ung\u00fcltig. Sie m\u00fcssen eine ganze oder eine Dezimalzahl angeben.", "part.numberentry.zero sig fig": "Dieser Abschnitt ist so eingestellt, dass die Antwort der Studenten auf null signifikante Stellen gerundet werden. Dies ergibt keinen Sinn.", "part.mcq.options def not a list": "Der Ausdruck, der {{properties}} definiert, ist keine Liste.", "part.mcq.marking matrix string empty": "Die angepasste Bewertungsmatrix ist leer.", "part.mcq.choices missing": "Definition der Auswahlm\u00f6glichkeiten fehlt", "part.mcq.matrix not a number": "Abschnitt {{part}} Zelle ({{row}},{{column}}) der Bewertungsmatrix wird nicht mit einer Zahl belegt", "part.mcq.wrong number of choices": "Sie haben die falsche Anzahl von Auswahlm\u00f6glichkeiten gew\u00e4hlt.", "part.mcq.no choices selected": "Keine Auswahl get\u00e4tigt.", "part.mcq.matrix not a list": "Die Bewertungsmatrix, definiert durch einen JME-Ausdruck, ist keine Liste, was sie sein sollte.", "part.mcq.matrix wrong type": "Element mit ung\u00fcltigem Typ '{{type}}' in Bewertungsmatrix verwendet.", "part.mcq.matrix mix of numbers and lists": "Mix von Zahlen und Listen in Bewertungsmatrix verwendet.", "part.mcq.matrix wrong size": "Bewertungsmatrix hat falsche Gr\u00f6\u00dfe.", "part.mcq.correct choice": "Sie haben eine richtige Antwort gew\u00e4hlt.", "part.matrix.answer invalid": "Ihre Antwort ist nicht g\u00fcltig.", "part.matrix.invalid cell": "Eine oder mehrere Zellen in ihren Antworten sind leer oder ung\u00fcltig.", "part.matrix.some incorrect": "Eine oder mehrere Zellen in ihrer Antwort sind nicht richtig, aber sie haben f\u00fcr den Rest Punkte erhalten.", "part.matrix.empty": "Sie haben keine Antwort angegeben.", "part.matrix.empty cell": "Eine oder mehrere Zellen in Ihrer Antwort sind leer.", "part.matrix.size mismatch": "Der Aufgabensteller erwartet eine bestimmte Dimension der L\u00f6sung. Die korrekte Dimension ist {{correct_dimensions}}, aber Ihre Antwort hat die Dimension {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Abschnitt hat nicht die Methode <code>{{name}}</code> implementiert.", "question.loaded name mismatch": "Der L\u00f6sungsversuch kann nicht wieder aufgenommen werden - das Paket hat sich seit der letzten Sitzung ge\u00e4ndert.", "question.error": "Aufgabe {{number}}: {{-message}}", "question.preamble.error": "Fehler in der Pr\u00e4ambel : {{-message}}", "question.preamble.syntax error": "Syntaxfehler in der Pr\u00e4ambel", "question.unsupported part type": "Nicht unterst\u00fctzer Abschnittstyp", "question.header": "Aufgabe {{number}}", "question.submit part": "Abschnitt einreichen", "question.show steps": "Zeige Tipps", "question.show steps penalty": "Sie verlieren <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Ihr Punktestand wird nicht ver\u00e4ndert.", "question.show steps already penalised": "Sie haben die Tipps schon gesehen. Sie k\u00f6nnen sie sich noch einmal ohne weiteren Punktabzug ansehen.", "question.hide steps": "Verberge Tipps", "question.hide steps no penalty": "Ihr Punktestand wird nicht ver\u00e4ndert.", "question.advice": "Hinweise", "question.no such part": "Abschnitt {{path}} kann nicht gefunden werden", "question.can not submit": "Antwort kann nicht eingereicht werden - bitte auf Fehler \u00fcberpr\u00fcfen.", "question.answer submitted": "Antwort eingereicht", "question.score feedback.show": "Zeige Wertung", "question.score feedback.hide": "Blende Wertung aus", "question.score feedback.answered total actual": "Punktestand: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Beantwortet.", "question.score feedback.answered actual": "Punktestand: {{scoreString}}", "question.score feedback.answered": "Beantwortet", "question.score feedback.unanswered": "Unbeantwortet", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Ihre Antwort ist richtig", "question.score feedback.partial": "Ihre Antwort ist teilweise richtig", "question.score feedback.wrong": "Ihre Antwort ist nicht richtig", "question.selector.unsubmitted changes": "Nicht eingereichte \u00c4nderungen.", "timing.no accumulator": "kein Zeitregister {{name}}", "timing.time remaining": "Verbleibende Zeit:", "xml.could not load": "Ein XML Dokument konnte nicht geladen werden: {{-message}}", "xml.property not number": "Einstellung {{name}} sollte eine Zahl sein, aber ist es nicht ({{value}}), im Knoten {{element}}", "xml.property not boolean": "Einstellung {{name}} sollte ein boolescher Wert sein, aber ist es nicht ({{value}}), im Knoten {{element}}", "xml.error in variable definition": "Fehler in der Definition der Variablen <code>{{name}}</code> ", "scorm.error initialising": "Fehler bei der Initialisierung des SCORM-Protokolls: {{-message}}", "scorm.failed save": "<p>Das Speichern der Daten auf dem Server schlug fehl. Klicken Sie <b>OK</b>, um es nochmal zu versuchen.</p>\n<p>Wenn Sie diese Meldung wiederholt erhalten, pr\u00fcfen Sie die Internetverbindung oder benutzen Sie einen anderen Computer. Ihre vorher eingereichte Antwort wurde erfolgreich gespeichert und wird wierderhergestellt, wenn Sie die Sitzung an einem anderen Computer fortsetzen.</p>\n<p>Wenn diese Meldung dauerhaft erscheint und Sie <em>gar keine</em> Antworten speichern k\u00f6nnen, nehmen Sie bitte mit Ihrer Dozent*in oder Lehrer*in Kontakt auf.</p>", "scorm.no exam suspend data": "Fortsetzung fehlgeschlagen: keine Daten vom unterbrochenen Test.", "scorm.error loading suspend data": "Fehler beim Laden der eingefrorenen Daten: {{-message}}", "scorm.error loading question": "Fehler beim Laden der Aufgabe {{number}}: {{-message}}", "scorm.no question suspend data": "Keine Daten zur unterbrochenen Aufgabe", "scorm.error loading part": "Fehler beim Laden des Abschnitts {{part}}: {{-message}}", "scorm.no part suspend data": "Keine Daten von der Unterbrechung des Abschnitts", "util.product.non list": "Keine Liste \u00fcbergeben an <code>Numbas.util.product</code>", "mark": "Punkt", "was": "wurde", "part": "Abschnitt", "gap": "L\u00fccke", "step": "Schritt", "jme.substituteTree.undefined variable": "Nicht definierte Variable: <code>{{name}}</code>", "jme.user javascript.returned undefined": "Benutzerdefinierte Javascriptfunktion <code>{{name}}</code> liefert <code>undefined</code>.", "part.marking.steps change": "Sie erhielten <strong>{{count,niceNumber}}</strong> $t(mark) f\u00fcr ihre Antworten in den L\u00f6sungsschritten.", "part.marking.revealed steps with penalty": "Sie haben sich Tipps anzeigen lassen. F\u00fcr diesen Abschnitt k\u00f6nnten sie <strong>{{count,niceNumber}}</strong> $t(mark) erhalten. Ihr Punktestand wird entsprechend reduziert.", "part.marking.total score": "Sie erhalten <strong>{{count,niceNumber}}</strong> $t(mark) f\u00fcr diesen Abschnitt.", "part.numberentry.precision type.dp": "Dezimalstelle", "part.numberentry.precision type.dp_plural": "Dezimalstellen", "part.numberentry.precision type.sigfig": "signifikante Stelle", "part.numberentry.precision type.sigfig_plural": "signifikante Stellen", "part.numberentry.give your answer to precision": "Runden Sie ihre Antwort auf {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Sie haben ihre Antwort ge\u00e4ndert ohne sie einzureichen. Bitte pr\u00fcfen Sie Ihre Antwort und klicken Sie <strong>Antwort einreichen</strong>.", "question.unsubmitted changes_plural": "Sie haben \u00c4nderungen an ihren Antworten vorgenommen ohne sie einzureichen. Bitte pr\u00fcfen Sie ihre Antworten und klicken Sie <strong>Alle Abschnitte einreichen</strong>.", "util.equality not defined for type": "Der Gleichheitsoperator ist f\u00fcr den Typ {{type}} nicht definiert", "mark_plural": "Punkte", "was_plural": "wurden", "die.script not loaded": "Numbas konnte nicht starten, da die Datei <code>{{file}}</code> nicht geladen wurde. \u00dcberpr\u00fcfen sie, dass sie in <code>scripts.js</code> enthalten ist.", "math.combinations.n less than zero": "Kombinationen konnten nicht berechnet werden: n ist kleiner als null", "math.combinations.k less than zero": "Kombinationen konnten nicht berechnet werden: k ist kleiner als null", "math.combinations.n less than k": "Kombinationen konnten nicht berechnet werden: n ist kleiner als k", "math.permutations.n less than zero": "Permutationen konnten nicht berechnet werden: n ist kleiner als null", "math.permutations.k less than zero": "Permutationen konnten nicht berechnet werden: k ist kleiner als null", "math.permutations.n less than k": "Permutationen konnten nicht berechnet werden: n ist kleiner als k", "part.numberentry.give your answer to precision_0": "Runden sie ihre Antwort auf die n\u00e4chste ganze Zahl.", "mathjax.error": "MathJax Verarbeitungsfehler: {{-message}}", "mathjax.error with context": "MathJax Verarbeitungsfehler bei {{-context}}: {{-message}}", "exam.introduction": "Test Einf\u00fchrung", "exam.feedback": "Test Bewertungsnachricht", "jme.tokenise.keypair key not a string": "Dictionary key muss eine Zeichenkette sein, nicht {{type}}.", "jme.shunt.list mixed argument types": "Kann {{mode}} nicht parsen: Mischung von Dictionary- und Listenelementen", "jme.func.listval.key not in dict": "Dictionary enth\u00e4lt keinen key <code>{{key}}</code>", "part.prompt": "Aufforderung", "part.feedback": "Wertung", "part.numberentry.answer not reduced": "Ihre Antwort ist nicht vollst\u00e4ndig vereinfacht.", "part.numberentry.give your answer as a reduced fraction": "Vereinfachen Sie ihre Antwort vollst\u00e4ndig.", "part.numberentry.negative decimal places": "Dieser Abschnitt ist so eingestellt, dass die Antwort der Studenten auf eine negative Zahl von Dezimalstellen gerundet wird. Dies ergibt keinen Sinn.", "part.mcq.choices": "M\u00f6glichkeiten", "part.mcq.answers": "Antworten", "part.mcq.matrix cell empty": "Abschnitt {{part}} Bewertungsmatrix Zelle ({{row}},{{column}}) ist leer", "part.mcq.matrix jme error": "Abschnitt {{part}} Bewertungsmatrix Zelle ({{row}},{{column}}) f\u00fchrt zu einem JME Fehler: {{-error}}", "question.statement": "Beschreibung", "ruleset.circular reference": "Zirkul\u00e4re Referenz in der Definition des Regelsatzes <code>{{name}}</code>", "ruleset.set not defined": "Regelsatz {{name}} wurde nicht definiert", "jme.evaluate.no scope given": "Numbas.jme.evaluate muss ein Geltungsbereich \u00fcbergeben werden", "answer.number.not a number": "Sie haben keine g\u00fcltige Zahl eingegeben.", "answer.number.fractions not allowed": "Sie d\u00fcrfen keine Bruchzahl eingeben.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Sie d\u00fcrfen keine Bruchzahlen eingeben.", "answer.matrix.some cell not a number": "Eine oder mehrere der Zellen in ihrer Antwort enthalten keine g\u00fcltige(n) Zahle(n).", "exam.enter password": "Passwort:", "exam.password.correct": "Dieses Passwort ist korrekt. Sie k\u00f6nnen den Test beginnen.", "exam.password.incorrect": "Dieses Passwort ist falsch.", "frontpage.scorm.lms not connected": "Der Test l\u00e4uft im Standalone-Modus. Ihre Antworten und Bewertungen werden nicht gespeichert!", "result.question review": "\u00dcberblick", "control.confirm regen no marks": "Wollen Sie diese Aufgabe mit neuen zuf\u00e4lligen Werten bearbeiten?", "control.confirm reveal no marks": "Wollen Sie die L\u00f6sung zu dieser Aufgabe ansehen?", "jme.tokenise.invalid near": "Ung\u00fcltiger Ausdruck: <code>{{expression}}</code> an Position {{position}} nahe bei <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Ung\u00fcltiges Objekt an die Zahlerzeugung \u00fcbergeben.", "jme.subvars.null substitution": "Leere Variablen-Substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Der Daten-Typ {{type}} wurde bereits vergeben und kann nicht erneut vergeben werden.", "jme.type.no cast method": "Kann nicht automatisch von {{from}} nach {{to}} konvertieren.", "jme.display.simplifyTree.empty expression": "Ausdruck ist leer", "jme.display.simplifyTree.stuck in a loop": "Vereinfachung h\u00e4ngt in Schleife: <code>{{expr}}</code>", "math.niceNumber.undefined": "Zahl erwartet, aber <code>undefined</code> erhalten", "math.rangeToList.zero step size": "Kann einen Bereich mit Schrittanzahl Null nicht in eine Liste konvertieren", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Sie haben sich die Schritte anzeigen lassen.", "part.marking.maximum scaled down": "Die maximal erzielbare Punktzahl in diesem Teil betr\u00e4gt <strong>{{count,niceNumber}}</strong> $t(mark). Ihre Punktzahl wird entsprechend verringert.", "part.marking.minimum score applied": "Die Mindestpunktzahl f\u00fcr diesen Teil betr\u00e4gt <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "Die H\u00f6chstpunktzahl f\u00fcr diesen Teil betr\u00e4gt <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "Ein Fehler im Bewertungs-Algorithmus f\u00fcr diesen Teil ist aufgetreten. Bitte melden Sie dies. {{-message}}", "part.marking.no result after replacement": "Dieser Abschnitt konnte nicht unter Verwendung ihrer Antworten zu den vorigen Abschnitten bewertet werden.", "part.marking.missing required note": "Der Bewertungs-Algorithmus definiert die marking note <code>{{note}}</code> nicht", "marking.apply.not a list": "Das erste Argument in <code>apply</code> muss vom Typ Liste sein, ist es aber nicht", "marking.apply marking script.script not found": "Beurteilungs-Skript <code>{{name}}</code> nicht gefunden", "marking.note.compilation error": "Kompilieren der marking note <code>{{name}}</code> fehlgeschlagen: {{-message}}", "marking.note.error evaluating note": "Auswertung der marking note <code>{{name}}</code> fehlgeschlagen - {{-message}}", "marking.note.invalid definition": "Ung\u00fcltig definierte marking note:<code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "M\u00f6glicherweise fehlt ein Doppelpunkt nach dem Namen und der Beschreibung", "marking.note.invalid definition.description missing closing bracket": "M\u00f6glicherweise fehlt eine schlie\u00dfende Klammer", "marking.note.empty expression": "Die marking note <code>{{name}}</code> ist leer.", "marking.script.error parsing notes": "Fehler beim Parsen des Bewertungsskripts: {{- message}}", "part.feedback out of date": "Das Feedback basiert auf ihrer zuletzt eingereichten Antwort. Reichen Sie ihre ver\u00e4nderte Antwort ein um ein aktualisiertes Feedback zu erhalten.", "part.jme.invalid value generator expression": "Ung\u00fcltiger Ausdruck zur Erzeugung von Werten f\u00fcr die Variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Sie haben eine falsche Antwort ausgew\u00e4hlt.", "part.matrix.not all cells same precision": "Sie haben nicht jede Zelle ihrer Antwort mit derselben Genauigkeit angegeben.", "part.gapfill.error marking gap": "Bewertungsfehler bei {{name}}: {{-message}}", "part.custom.empty setting": "Kein Wert angegeben.", "part.custom.unrecognised input type": "Nicht erkannter Einstellungs-Typ <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Fehler beim Auswerten der Eingabe-Option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition der Eingabe-Option <code>{{option}}</code> fehlt.", "part.custom.error evaluating setting": "Fehler beim Auswerten der Einstellung <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Fehler beim Erzeugen der Frage {{number}}: {{-message}}", "question.score feedback.not marked": "Nicht bewertet", "question.score feedback.partially answered": "Teilweise beantwortet", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Punktestand: {{scoreString}}", "question.score feedback.score total actual": "Punktestand: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Fehler in der Definition der Variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Erweiterung <code>{{name}}</code> konnte nicht geladen werden.", "control.toggle navigation menu": "Navigationsmen\u00fc ein-/ausschalten", "part.input title": "Antwort zum Teil {{name}}", "part.correct answer title": "Erwartete Antwort zum Teil {{name}}", "part.jme.must-match.failed": "Ihre Antwort ist nicht im richtigen Format.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>Einer oder mehrere der folgenden Teile h\u00e4ngen von Ihrer Antwort in diesem Abschnitt ab. Wenn Sie diesen Abschnitt erneut einreichen, werden diese folgenden Teile ung\u00fcltig und entfernt. Dies kann nicht r\u00fcckg\u00e4ngig gemacht werden.</p>\n<p>M\u00f6chten Sie diesen Abschnitt erneut einreichen?</p>", "control.back to menu": "Zur\u00fcck zum Menu", "display.error making html": "Fehler beim Erzeugen von HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "Einer Fragevariable wurde kein Name zugewiesen.", "jme.calculus.unknown derivative": "Kann <code>{{tree}}</code> nicht ableiten.", "math.order complex numbers": "Kann komplexe Zahlen nicht anordnen", "menu.choose a question": "W\u00e4hlen Sie eine Frage aus.", "part.choose next part.answered": "Was m\u00f6chten Sie als n\u00e4chstes machen?", "part.choose next part.unanswered": "Sie k\u00f6nnten auch:", "part.choose next part.will be locked": "(Dieser Teil wird gesperrt werden)", "part.reached dead end": "An diese Stelle gibt es nichts mehr zu tun.", "part.next part.penalty amount": "(Abzug von {{count}} $t(mark))", "part.marking.counts towards objective": "Dieser Abschnitt z\u00e4hlt f\u00fcr das Ziel <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Ihre Antwort ist ung\u00fcltig. Sie m\u00fcssen eine ganze Zahl, eine Dezimalzahl oder eine Bruchzahl eingeben.", "question": "Frage", "question.progress": "Fortschritt bei dieser Frage:", "question.score feedback.unattempted": "Kein Versuch unternommen", "question.score feedback.attempted": "Versuch unternommen", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Ziele", "question.penalties": "Abz\u00fcge", "question.back to previous part": "Zur\u00fcck zum vorherigen Teil", "end.print": "Drucke eine Abschrift des Tests", "math.shuffle_together.lists not all the same length": "Nicht alle Listen haben die gleiche L\u00e4nge.", "jme.parse signature.invalid signature string": "Ung\u00fcltiger Signaturstring der Funktion: {{str}}", "part.custom.expected answer has wrong type": "Die erwartete Antwort f\u00fcr diesen Abschnitt hat den falschen Typ. Es sollte <code>{{shouldbe}}</code> sein.", "part.custom.input option has wrong type": "Die Eingabe-Einstellung <code>{{option}}</code> hat den falschen Typ. Es sollte <code>{{shouldbe}}</code> sein.", "matrix input.size control legend": "Gr\u00f6\u00dfe", "matrix input.rows": "Zeilen", "matrix input.columns": "Spalten", "part.jme.error checking numerically": "Beim numerischen \u00dcberpr\u00fcfen der Antwort ist ein Fehler aufgetreten: {{-message}}", "part.gapfill.cyclic adaptive marking": "Es gibt eine zirkul\u00e4re Referenz in der adaptiven Bewertung dieses Abschnitts: <strong>{{name1}}</strong> ben\u00f6tigt <strong>{{name2}}</strong>, was sich letztlich auf <strong>{{name1}}</strong> bezieht.", "modal.style.background colour": "Hintergrundfarbe", "modal.style.text colour": "Schriftfarbe", "modal.style.text size": "Schriftgr\u00f6\u00dfe", "modal.style.explanation": "Benutzen Sie diese Einstellungen, um das Erscheinungsbild des Tests zu ver\u00e4ndern.", "modal.style.reset to defaults": "Auf die Vorgabewerte zur\u00fccksetzen", "modal.style.text size preview": "Der Text wird meist diese Gr\u00f6\u00dfe haben.", "control.style options": "Anzeigeeinstellungen", "part.marking.partially correct": "Ihre Antwort ist teilweise korrekt.", "part.marking.error in adaptive marking": "Fehler bei der adaptiven Bewertung f\u00fcr diesen Abschnitt. Bitte melden Sie dies. {{-message}}", "page.skip to content": "Weiter zum Inhalt", "result.learning objective": "Lernziel", "jme.interpreted as": "interpretiert als", "jme.script.note.compilation error": "Fehler beim Kompilieren, <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Fehler beim Auswerten, <code>{{name}}</code>: {{-message}}", "jme.script.note.invalid definition": "Ung\u00fcltige Definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "Vielleicht fehlt ein Doppelpunkt nach Name und Beschreibung", "jme.script.note.invalid definition.description missing closing bracket": "Vielleicht fehlt eine schlie\u00dfende Klammer", "jme.script.note.empty expression": "Die marking note <code>{{name}}</code> ist leer.", "jme.script.error parsing notes": "Fehler beim Parsen des Bewertungsskripts: {{- message}}", "matrix input.cell label": "Zeile {{row}}, Spalte {{column}}", "control.move to next question": "Weiter zur n\u00e4chsten Frage", "diagnostic.use retry": "Nutze einen Versuch und probiere das Thema noch einmal.", "diagnostic.move to next topic": "Weiter zum n\u00e4chsten Thema.", "diagnostic.next step question": "Was m\u00f6chtest du als n\u00e4chstes machen?", "diagnostic.now assessing topic": "{{current_topic}} wird gepr\u00fcft", "diagnostic.one retry left": "Du hast noch einen Versuch \u00fcbrig", "diagnostic.retries left": "Du hast noch {{retries}} Versuche \u00fcbrig.", "diagnostic.percentage completed": "Du hast bereits <strong>{{percentage}}%</strong> des Tests geschafft.", "diagnostic.test is over": "Die Pr\u00fcfung ist beendet.", "diagnostic.passed all lo": "Du hast alle Lernziele erreicht.", "diagnostic.more work on lo": "Du solltest noch folgende Lernziele weiter \u00fcben: {{los}}.", "diagnostic.move to next question in topic": "Weiter zur n\u00e4chsten Frage im Thema.", "diagnostic.complete": "Geschafft!", "diagnostic.studying topic": "Aktuelles Thema: {{topic}}", "display.answer widget.unknown widget type": "Der Antwort-Widget-Typ <code>{{name}}</code> wurde nicht erkannt.", "jme.shunt.expected argument before comma": "Eintrag zwischen \u00f6ffnender Klammer und Komma erwartet", "part.waiting for pre submit": "Die Antwort wird jetzt bewertet. Bitte warten.", "diagnostic.end test": "Den Test beenden.", "page.no stylesheet loaded": "Die Stylesheet-Datei der Seite wurde nicht geladen.", "modal.confirm": "Best\u00e4tigen", "modal.alert": "Achtung", "suspend.resumed header": "Versuch wieder aufgenommen", "jme.vector.value not an array of numbers": "Ein Vektor kann nur aus einem Array von Zahlen konstruiert werden.", "jme.matrix.value not the right type": "Falscher Typ, um einen Vektor zu konstruieren.", "jme.subvars.html inserted twice": "Ein HTML-Wert wurde zweifach eingebettet. Definieren Sie gegebenenfalls eine Funktion, um jeweils einen neuen Wert zu erzeugen.", "jme.variables.invalid function language": "Die Sprache <code>{{language}}</code> ist ung\u00fcltig.", "jme.variables.duplicate definition": "Es gibt mehrere Definitionen der Variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "Die Gr\u00f6\u00dfe der Partition muss zwischen 1 und {{n}} liegen.", "part.marking.parameter already in scope": "Es gibt eine Variable <code>{{name}}</code>, was auch die Bezeichnung eines Bewertungsparameters ist. Bitte benennen Sie die Variable um.", "part.marking.adaptive variable replacement refers to self": "Dieser Teil enth\u00e4lt eine selbstbez\u00fcgliche Variablenersetzung f\u00fcr adaptive Bewertung.", "part.marking.adaptive variable replacement refers to nothing": "Dieser Teil enth\u00e4lt eine ung\u00fcltige Variablenersetzung f\u00fcr adaptive Bewertung.", "part.numberentry.display answer wrong type": "Die angezeigte Antwort f\u00fcr diesen Teil ist vom Typ <code>{{got_type}}</code> statt vom Typ <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "Das Array der vorausgef\u00fcllten Zellen enth\u00e4lt einen ung\u00fcltigen Wert vom Typ <code>{{n}}</code>.", "diagnostic.make a choice": "Auswahl treffen", "matrixmath.not square": "Diese Operation kann nur auf eine quadratische Matrix angewendet werden.", "matrixmath.not invertible": "Diese Operation kann nur auf eine invertierbare Matrix angewendet werden.", "matrixmath.gauss-jordan elimination.not enough columns": "Es muss mindestens so viele Spalten wie Zeilen geben.", "question.required extension not available": "Diese Frage ben\u00f6tigt die Erweiterung <code>{{-extension}}</code>, aber diese ist nicht verf\u00fcgbar.", "util.formatNumberNotation.unrecognised syntax": "Die Formatierungssyntax <code>{{syntax}}</code> wurde nicht erkannt.", "worksheet.number of exams to generate": "Anzahl von zu erzeugenden Bl\u00e4ttern", "worksheet.starting with id": "Beginne mit ID", "worksheet.show exam id": "Zeige Blatt-ID?", "worksheet.page break between questions": "Seitenumbruch zwischen Fragen?", "worksheet.page margins": "Seitenr\u00e4nder (mm)", "worksheet.text size": "Schriftgr\u00f6\u00dfe (pt)", "worksheet.generate": "Erzeugen", "worksheet.generating exams": "Erzeuge Bl\u00e4tter", "worksheet.sheet id": "Blatt-ID:", "worksheet.print single": "Dieses Blatt drucken", "worksheet.print several": "Diese Bl\u00e4tter drucken", "worksheet.answer sheets": "Antwortbl\u00e4tter", "worksheet.question sheets": "Aufgabenbl\u00e4tter", "worksheet.reconfigure": "Erzeuge andere Bl\u00e4tter", "worksheet.show sheet": "Vorschau f\u00fcr Blatt ID:", "accessibility statement": "Barrierefreiheit und Anpassungsm\u00f6glichkeiten von Numbas.", "exam.enter your name": "Ihr Name:", "exam.attempt download security warning": "Dieser Test ist zwar so eingerichtet, dass Sie Ihre Daten herunterladen k\u00f6nnen, aber l\u00e4uft nicht in einem sicheren Browser-Kontext. Sie k\u00f6nnen daher Ihre Testdaten nicht herunterladen. Nehmen Sie gegebenenfalls mit Ihrer Dozent*in Kontakt auf.", "result.download exam object": "Testdaten herunterladen", "control.return to question": "Zur Frage zur\u00fcckkehren", "control.show introduction": "Einf\u00fchrung", "analysis.header": "Versuchsdaten analysieren", "analysis.help.upload files": "Versuchsdaten-Dateien Ihrer Studierenden hochladen.", "analysis.help.file input label": "W\u00e4hlen Sie Versuchsdaten-Dateien aus, oder ziehen Sie Dateien hierher.", "analysis.table.total": "Punktzahl des Tests", "analysis.table.question": "Punktzahlen des Tests und der Fragen", "analysis.table.all": "Alle Details", "analysis.student name.anonymous": "Kein Name angegeben", "analysis.expected": "Erwartete Ergebnisse", "analysis.start time": "Startzeitpunkt", "analysis.maximum": "Maximale Punktzahl", "analysis.file": "Datei", "analysis.download": "Download", "analysis.delete": "L\u00f6schen", "analysis.view results": "Ergebnisse anzeigen", "analysis.upload files": "Dateien hochladen", "analysis.upload more": "Weitere Dateien hochladen", "analysis.attempt data": "Daten des Versuchs", "analysis.select format": "Daten zur Anzeige ausw\u00e4hlen", "analysis.download this table": "Diese Tabelle herunterladen", "analysis.student": "Ergebnisse der Studierenden", "analysis.question key": "Frage-Schl\u00fcssel", "analysis.question name": "Name der Frage", "analysis.group": "Gruppe", "analysis.question": "Frage", "analysis.part": "Teil", "analysis.gap": "L\u00fccke", "analysis.record type": "Eintragstyp", "analysis.score": "Punktzahl", "analysis.marks available": "Bewertung verf\u00fcgbar", "analysis.percentage": "Prozentsatz", "analysis.answer": "Antwort", "analysis.student name": "Name des Studierenden", "analysis.summary.no files": "Sie haben noch keine Dateien hochgeladen.", "analysis.summary.no decrypted files": "Sie haben noch keine g\u00fcltigen Dateien hochgeladen.", "analysis.summary.one file": "Ein Versuch.", "analysis.summary.several files": "{{num_files,niceNumber}} Versuche.", "analysis.not secure context": "Diese Seite muss \u00fcber eine HTTPS-Verbindung oder als Datei auf Ihrem eigenen Ger\u00e4t geladen werden.", "jme.shunt.pipe right hand takes no arguments": "Der Ausdruck auf der rechten Seite des Pipe-Operators muss eine Funktion sein.", "question.explore.no parts defined": "In dieser Frage wurden keine Teile angelegt.", "answer": "Antwort", "worksheet.answersheet show question content": "Inhalt der Frage in Antwortbl\u00e4ttern zeigen?", "modal.confirm end exam": "Schreiben Sie zur Best\u00e4tigung <code>{{endConfirmation}}</code> in die Box:", "modal.end exam button": "Test beenden", "lightbox.zoom in on image": "Hereinzoomen", "exam.progress": "Fortschritt", "exam.questions answered": "{{numAnsweredQuestions}} von {{numQuestions}} Fragen beantwortet.", "result.question marks available": "Bewertungen verf\u00fcgbar", "result.question answered": "Beantwortet?", "control.confirm end.correct": "Sie k\u00f6nnen den Test nun beenden.", "control.confirm end.incorrect": "Dies ist nicht der erwartete Text.", "control.confirm end.password": "Beenden", "jme.typecheck.for in name wrong type": "Der Name in einem <code>for</code>-Statement muss ein Name oder eine Liste von Namen sein, nicht {{type}}.", "jme.makeFast.no fast definition of function": "Die Funktion <code>{{name}}</code> hier ist nicht so definiert, dass sie schnell ausgef\u00fchrt werden kann.", "part.show feedback": "Feedback anzeigen", "part.hide feedback": "Feedback verbergen", "part.feedback title": "Feedback f\u00fcr {{name}}.", "part.jme.must-match.warning": "Ihre Antwort hat nicht die erwartete Form: {{-message}}", "part.numberentry.write your answer as a fraction": "Schreiben Sie Ihre Antwort als Bruchzahl.", "question.nav.label": "Fragen-Navigation", "question.answer saved": "Antwort gespeichert", "question.all answers saved": "Alle Antworten gespeichert", "analysis.back to results": "Zur\u00fcck zu den Ergebnissen", "analysis.review": "\u00dcberpr\u00fcfung", "analysis.review this": "Versuch \u00fcberpr\u00fcfen", "analysis.reviewing attempt": "\u00dcberpr\u00fcfung: Versuch von {{student_name}}.", "part.there is new feedback": "Die R\u00fcckmeldung wurde ge\u00e4ndert.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "in-id": {"translation": {"page.loading": "Memuat...", "page.saving": "<p>Menyimpan.</p>\n<p>Hal ini akan memerlukan waktu beberapa detik.</p>", "mathjax.math processing error": "Terjadi kesalahan \"{{-message}}\" ketika membuat teks dari sintaks <code>{{expression}}</code>", "die.numbas failed": "Numbas telah gagal", "die.sorry": "Maaf, Numbas mengalami masalah sehingga proses tidak dapat dilanjutkan. Berikut ini adalah deskripsi dari masalah tersebut.", "die.error": "Kesalahan", "modal.ok": "Ok", "modal.cancel": "Batalkan", "exam.exam name": "Nama Ujian:", "exam.random seed": "ID Sesi:", "exam.student name": "Nama Siswa:", "exam.number of questions": "Jumlah Soal:", "exam.marks available": "Total Skor:", "exam.pass percentage": "Persentase Kelulusan:", "exam.time allowed": "Waktu yang Diizinkan:", "exam.passed": "Lulus", "exam.failed": "Gagal", "exam.review header": "Review: ", "frontpage.start": "Mulai", "suspend.paused header": "Terhenti Sementara", "suspend.exam suspended": "Ujian ditangguhkan. Tekan <em>Lanjutkan</em> untuk melanjutkan.", "suspend.you can resume": "Anda akan bisa melanjutkan sesi ini setelah Anda memulai aktivitas ini.", "suspend.resume": "Lanjutkan", "result.exit": "Keluar Ujian", "result.print": "Cetak ringkasan hasil ujian ini", "result.exam summary": "Ringkasan Ujian", "result.performance summary": "Ulasan Kinerja", "result.exam start": "Ujian Mulai:", "result.exam stop": "Ujian Berhenti:", "result.time spent": "Waktu yang Terpakai:", "result.questions attempted": "Soal yang Dicoba:", "result.score": "Nilai:", "result.result": "Hasil:", "result.question number": "Nomor Soal", "result.question score": "Skor", "result.question review title": "Ulas soal ini", "result.click a question to review": "Klik pada nomor soal untuk melihat bagaimana jawaban Anda dinilai, dan jika tersedia, solusi lengkapnya juga.", "end.exam has finished": "Ujian telah selesai. Anda dapat menutup jendela ini.", "control.confirm leave": "Anda belum menyelesaikan ujian.", "control.not all questions answered": "Anda belum menjawab setiap soal pada ujian ini.", "control.not all questions submitted": "Anda telah mengubah satu atau lebih jawaban namun belum menyerahkannya. Silakan periksa dan serahkan tiap soal.", "control.confirm end": "Apakah Anda yakin ingin mengakhiri ujian? Setelah mengakhiri, Anda tidak dapat mengubah satu pun jawaban.", "control.confirm regen": "Apakah Anda ingin mengacak ulang soal ini? Jika Anda klik OK, semua jawaban dan nilai untuk soal ini akan hilang.", "control.confirm reveal": "Apakah Anda ingin melihat jawaban dari soal ini? Semua nilai yang telah Anda terima sebelumnya akan dikunci dan Anda tidak bisa menjawab soal ini setelahnya.", "control.proceed anyway": "Tetap lanjutkan?", "control.regen": "Coba soal lain yang mirip dengan soal ini", "control.submit answer": "Kirim jawaban", "control.submit all parts": "Kirim semua bagian", "control.submit again": "Kirim lagi", "control.submit": "Kirim", "control.previous": "Sebelumnya", "control.next": "Selanjutnya", "control.advice": "Saran", "control.reveal": "Tunjukkan jawaban", "control.total": "Total", "control.pause": "Hentikan sementara", "control.end exam": "Selesaikan Ujian", "control.back to results": "Kembali ke hasil ujian", "display.part.jme.error making maths": "Kesalahan dalam membuat tampilan matematika", "exam.xml.bad root": "Elemen dasar XML ujian haruslah 'exam'", "exam.changeQuestion.no questions": "Ujian ini tidak mengandung soal! Cek kesalahan pada file .exam.", "feedback.you were awarded": "Anda mendapatkan <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) telah diambil.", "jme.shunt.not enough arguments": "Tidak cukup argumen untuk operasi <code>{{op}}</code>", "jme.shunt.no left bracket in function": "Tidak ada kurung buka yang sesuai dalam aplikasi fungsi atau tupel", "jme.shunt.no left square bracket": "Tidak ada kurung buka yang sesuai", "jme.shunt.no left bracket": "Tidak ada kurung buka yang sesuai", "jme.shunt.no right bracket": "Tidak ada kurung tutup yang sesuai", "jme.shunt.no right square bracket": "Tidak ada kurung siku tutup yang sesuai untuk mengakhiri daftar", "jme.shunt.missing operator": "Ekspresi tidak bisa diproses -- kehilangan sebuah operator.", "jme.typecheck.function maybe implicit multiplication": "Fungsi <code>{{name}}</code> tidak didefinisikan. Apakah maksud Anda adalah <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Fungsi <code>{{op}}</code> tidak didefinisikan. Apakah <code>{{op}}</code> suatu variabel sehingga maksud Anda adalah <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operasi '{{op}}' tidak terdefinisi.", "jme.typecheck.no right type definition": "Tidak ada definisi  '{{op}}' dari tipe yang benar.", "jme.typecheck.no right type unbound name": "Variabel <code>{{name}}</code> tidak terdefinisi.", "jme.typecheck.map not on enumerable": "Operasi <code>map</code> harus bekerja pada list atau range, bukan {{type}}", "jme.evaluate.undefined variable": "Variabel {{name}} tidak didefinisikan", "jme.thtml.not html": "Meneruskan nilai non-HTML ke konstruktor THTML.", "jme.func.switch.no default case": "Tidak ada default case untuk Switch statement", "jme.func.listval.invalid index": "Indeks list {{index}} tidak valid untuk list dengan ukuran {{size}}", "jme.func.listval.not a list": "Objek tidak dapat disubskribkan", "jme.func.matrix.invalid row type": "Tidak dapat membuat matriks dari baris bertipe {{type}}", "jme.func.except.continuous range": "Tidak dapat menggunakan operator 'except' untuk range kontinu.", "jme.matrix.reports bad size": "Ukuran matriks salah - pasti terdapat kesalahan pada fungsi konstruktor", "jme.texsubvars.no right bracket": "Tidak ada <code>]</code> dalam argumen {{op}}.", "jme.texsubvars.missing parameter": "Parameter hilang dalam {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Tidak ada <code>}</code> dalam {{op}}", "jme.user javascript.error": "Kesalahan di fungsi javascript buatan pengguna <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Kesalahan dalam membuat fungsi <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Kesalahan sintaks pada definisi fungsi", "jme.variables.variable not defined": "Variabel <code>{{name}}</code> tidak didefinisikan.", "jme.variables.empty definition": "Definisi dari variabel <code>{{name}}</code> kosong.", "jme.variables.circular reference": "Referensi variabel sirkular dalam definisi <code>{{name}}</code>", "jme.variables.error computing dependency": "Error dalam menghitung variabel referensi <code>{{name}}</code>", "jme.variables.error evaluating variable": "Error dalam mengevaluasi variabel {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Serangkaian variabel pertanyaan yang valid tidak dibuat tepat waktu.", "jme.display.unknown token type": "Tidak dapat membuat teks dari token bertipe {{type}}", "jme.display.collectRuleset.no sets": "Tidak ada set yang diberikan ke collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} tidak didefinisikan", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree harus diberi Scope", "math.precround.complex": "Tidak dapat melakukan pembulatan ke bilangan kompleks dengan tempat desimal ini", "math.siground.complex": "Tidak dapat melakukan pembulatan ke bilangan kompleks dengan angka penting ini", "math.combinations.complex": "Tidak dapat menghitung kombinasi dari bilangan kompleks", "math.permutations.complex": "Tidak dapat menghitung permutasi dari bilangan kompleks", "math.gcf.complex": "Tidak dapat menghitung FPB dari bilangan kompleks", "math.lcm.complex": "Tidak dapat menghitung KPK dari bilangan kompleks", "math.lt.order complex numbers": "Tidak dapat mengurutkan bilangan kompleks", "math.choose.empty selection": "Pilihan kosong diberikan ke fungsi random", "matrixmath.abs.non-square": "Tidak dapat menghitung determinan matriks yang tidak berbentuk persegi.", "matrixmath.abs.too big": "Maaf, belum bisa menghitung determinan matriks berukuran lebih besar dari 3x3.", "matrixmath.mul.different sizes": "Tidak dapat mengalikan matriks dengan ukuran berbeda.", "vectormath.cross.not 3d": "Hanya bisa menentukan perkalian silang untuk vektor berdimensi tiga.", "vectormath.dot.matrix too big": "Tidak dapat menghitung hasil kali titik untuk matriks yang tidak berukuran $ 1 \\times N $ atau $ N \\times 1 $.", "vectormath.cross.matrix too big": "Tidak dapat menghitung hasil kali silang untuk matriks yang tidak berukuran $1 \\times N$ atau $N \\times 1$.", "part.with steps answer prompt": "Jawaban: ", "part.script.error": "Kesalahan pada bagian {{path}} skrip kustom {{script}}: {{-message}}", "part.marking.steps no matter": "Karena Anda menerima nilai penuh untuk bagian tersebut, jawaban Anda atas langkah-langkah tersebut tidak dihitung.", "part.marking.used variable replacements": "Bagian ini dinilai menggunakan jawaban Anda pada bagian sebelumnya.", "part.marking.variable replacement part not answered": "Anda harus menjawab {{part}} terlebih dahulu.", "part.marking.resubmit because of variable replacement": "Penilaian pada bagian ini bergantung pada jawaban Anda untuk bagian lain, yang telah Anda ubah. Kirimkan bagian ini lagi untuk memperbarui skor Anda.", "part.marking.not submitted": "Tidak ada jawaban yang terkirim.", "part.marking.did not answer": "Anda tidak menjawab pertanyaan ini.", "part.marking.nothing entered": "Anda tidak memasukkan jawaban.", "part.marking.incorrect": "Jawaban Anda salah.", "part.marking.correct": "Jawaban Anda benar.", "part.marking.uncaught error": "Kesalahan ketika menilai: {{-message}}", "part.correct answer": "Jawaban yang diharapkan:", "part.missing type attribute": "{{part}}: Atribut tipe bagian hilang", "part.unknown type": "{{part}}: Tipe bagian {{type}} tidak dikenal", "part.setting not present": "Properti '{{property}}' tidak diatur", "part.jme.answer missing": "Jawaban yang benar tidak ditemukan", "part.jme.answer too long": "Jawaban Anda terlalu panjang.", "part.jme.answer too short": "Jawaban Anda terlalu pendek.", "part.jme.answer invalid": "Jawaban Anda bukanlah ekspresi matematika yang valid.<br/>{{-message}}.", "part.jme.marking.correct": "Jawaban Anda benar secara numerik.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Jawaban Anda harus mengandung: {{strings}}", "part.jme.must-have several": "Jawaban Anda harus mengandung seluruhnya dari: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Jawaban Anda tidak boleh mengandung: {{strings}}", "part.jme.not-allowed several": "Jawaban Anda tidak boleh mengandung salah satu dari: {{strings}}", "part.jme.unexpected variable name": "Jawaban Anda diinterpretasikan dengan menggunakan nama variabel <code>{{name}}</code> yang tidak diharapkan.", "part.jme.unexpected variable name suggestion": "Jawaban Anda diinterpretasikan dengan menggunakan nama variabel <code>{{name}}</code> yang tidak diharapkan. Apakah maksud Anda adalah <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Jawaban tampilan hilang", "part.patternmatch.correct except case": "Jawaban Anda benar, kecuali besar kecilnya huruf.", "part.numberentry.correct except decimal": "Jawaban Anda berada dalam rentang yang benar, namun bilangan desimal tidak diperbolehkan.", "part.numberentry.correct except fraction": "Jawaban Anda berada dalam rentang yang benar, namun bentuk pecahan tidak dibolehkan.", "part.numberentry.answer invalid": "Anda tidak memasukkan angka yang sesuai.", "part.numberentry.answer not integer": "Jawaban Anda tidak sesuai. Anda harus memasukkan bilangan bulat, bukan desimal.", "part.numberentry.answer not integer or decimal": "Jawaban Anda tidak sesuai. Anda harus memasukkan bilangan asli atau desimal.", "part.numberentry.zero sig fig": "Bagian ini diatur untuk membulatkan jawaban siswa menjadi nol angka penting, yang tidak ada artinya.", "part.mcq.options def not a list": "Ekspresi yang mendefinisikan {{properties}} bukan merupakan suatu list.", "part.mcq.marking matrix string empty": "Ekspresi matriks penilaian kustom kosong.", "part.mcq.choices missing": "Definisi dari pilihan-pilihan tidak ada", "part.mcq.matrix not a number": "Bagian {{part}} pada sel matriks penilaian ({{row}},{{column}}) bukan merupakan angka", "part.mcq.wrong number of choices": "Anda memilih jumlah pilihan yang salah.", "part.mcq.no choices selected": "Tidak ada pilihan yang terpilih.", "part.mcq.matrix not a list": "Matriks penilaian yang didefinisikan oleh ekspresi JME bukan merupakan suatu list, dan seharusnya merupakan list.", "part.mcq.matrix wrong type": "Elemen bertipe '{{type}}' yang tidak valid digunakan dalam matriks penilaian.", "part.mcq.matrix mix of numbers and lists": "Campuran dari angka dan list digunakan dalam matriks penilaian.", "part.mcq.matrix wrong size": "Matriks penilaian memiliki ukuran yang salah.", "part.mcq.correct choice": "Anda telah memilih jawaban yang benar.", "part.matrix.answer invalid": "Jawaban Anda tidak sesuai.", "part.matrix.invalid cell": "Satu atau lebih sel pada jawaban Anda kosong atau tidak sesuai.", "part.matrix.some incorrect": "Satu atau lebih sel pada jawaban Anda salah, namun Anda telah mendapatkan nilai untuk sel lainnya.", "part.matrix.empty": "Anda belum memasukkan jawaban.", "part.matrix.empty cell": "Satu atau lebih sel pada jawaban Anda kosong.", "part.matrix.size mismatch": "Penulis soal belum mengizinkan siswa untuk menentukan dimensi jawaban mereka, tetapi jawaban yang benar adalah {{correct_dimensions}} sedangkan input jawaban adalah {{input_dimensions}}", "part.gapfill.feedback header": "<strong>{{name}}</strong>", "part.extension.not implemented": "Bagian ini belum mengimplementasikan metode <code>{{name}}</code>.", "question.loaded name mismatch": "Tidak dapat melanjutkan upaya ini - paket telah berubah sejak sesi terakhir.", "question.error": "Soal {{number}}: {{-message}}", "question.preamble.error": "Kesalahan dalam pembukaan: {{-message}}", "question.preamble.syntax error": "Kesalahan sintaks pada pembukaan", "question.unsupported part type": "Tipe bagian tidak didukung", "question.header": "Soal {{number}}", "question.submit part": "Kirim bagian", "question.show steps": "Tunjukkan langkah", "question.show steps penalty": "Anda akan kehilangan <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Skor Anda tidak akan terpengaruh.", "question.show steps already penalised": "Anda telah melihat langkah penyelesaian. Anda dapat melihatnya lagi tanpa hukuman lebih lanjut.", "question.hide steps": "Sembunyikan langkah", "question.hide steps no penalty": "Skor Anda tidak akan terpengaruh.", "question.advice": "Saran", "question.no such part": "Tidak dapat menemukan bagian {{path}}", "question.can not submit": "Tidak dapat mengirim jawaban - periksa bila ada kesalahan.", "question.answer submitted": "Jawaban terkirim", "question.score feedback.show": "Tunjukkan umpan balik", "question.score feedback.hide": "Sembunyikan umpan balik", "question.score feedback.answered": "Terjawab", "question.score feedback.unanswered": "Tidak terjawab", "question.score feedback.correct": "Jawaban Anda benar", "question.score feedback.partial": "Jawaban Anda benar sebagian", "question.score feedback.wrong": "Jawaban Anda salah", "question.selector.unsubmitted changes": "Perubahan belum dikirim.", "timing.no accumulator": "tidak ada akumulator waktu {{name}}", "timing.time remaining": "Waktu tersisa:", "xml.could not load": "Tidak dapat memuat dokumen XML: {{-message}}", "xml.property not number": "Properti {{name}} harus merupakan angka, tetapi bukan ({{value}}), pada node {{element}}", "xml.property not boolean": "Properti {{name}} harus merupakan boolean, tetapi bukan ({{value}}), pada node {{element}}", "scorm.error initialising": "Kesalahan dalam menginisialisasi protokol SCORM: {{-message}}", "scorm.failed save": "<p>Permintaan untuk menyimpan data ke server gagal. Tekan <b>OK</b> untuk mencoba lagi.</p>\n<p>Jika Anda mendapatkan pesan ini berulang kali, periksa koneksi internet Anda atau gunakan perangkat yang lain. Jawaban yang Anda kirimkan sebelumnya telah berhasil disimpan dan akan dipulihkan jika Anda melanjutkan sesi ini di perangkat lain.</p>\n<p>Jika pesan ini terus muncul dan Anda tidak dapat menyimpan <em>jawaban</em> apa pun, harap hubungi dosen atau guru Anda.</p>", "scorm.no exam suspend data": "Gagal untuk melanjutkan: tidak ada data penundaan ujian.", "scorm.error loading suspend data": "Terjadi kesalahan saat memuat data penangguhan: {{-message}}", "scorm.error loading question": "Terjadi kesalahan saat memuat soal {{number}}: {{-message}}", "scorm.no question suspend data": "Tidak ada pertanyaan yang menangguhkan data", "scorm.error loading part": "Terjadi kesalahan saat memuat bagian {{part}}: {{-message}}", "scorm.no part suspend data": "Tidak ada bagian yang menangguhkan data", "util.product.non list": "Meneruskan non-list ke <code>Numbas.util.product</code>", "mark": "nilai", "was": "adalah", "part": "bagian", "gap": "celah", "step": "langkah", "jme.substituteTree.undefined variable": "Variabel tidak didefinisikan: <code>{{name}}</code>", "jme.user javascript.returned undefined": "Fungsi javascript buatan pengguna <code>{{name}}</code> mengembalikan <code>undefined</code>.", "part.marking.steps change": "Anda mendapatkan <strong>{{count,niceNumber}}</strong> $t(mark) atas jawaban Anda untuk langkah-langkah tersebut.", "part.marking.total score": "Anda mendapatkan <strong>{{count,niceNumber}}</strong> $t(mark) untuk bagian ini.", "part.numberentry.precision type.dp": "angka di belakang koma", "part.numberentry.precision type.dp_plural": "angka di belakang koma", "part.numberentry.precision type.sigfig": "angka penting", "part.numberentry.precision type.sigfig_plural": "angka penting", "part.numberentry.give your answer to precision": "Bulatkan jawaban Anda ke {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Anda telah mengubah jawaban Anda, tetapi belum mengirimkannya. Harap periksa jawaban Anda, lalu tekan tombol <strong>Kirim jawaban</strong>.", "question.unsubmitted changes_plural": "Anda telah mengubah jawaban Anda, tetapi belum mengirimkannya. Silakan periksa jawaban Anda untuk setiap bagian dan kemudian tekan tombol <strong>Kirim semua bagian</strong>.", "util.equality not defined for type": "Kesamaan tidak didefinisikan untuk tipe {{type}}", "mark_plural": "nilai", "was_plural": "adalah", "die.script not loaded": "Numbas tidak dapat memulai karena file <code>{{file}}</code> tidak dimuat. Periksa apakah file tersebut termasuk dalam <code>scripts.js</code>.", "math.combinations.n less than zero": "Tidak bisa menghitung kombinasi: n lebih kecil dari nol", "math.combinations.k less than zero": "Tidak bisa menghitung kombinasi: k lebih kecil dari nol", "math.combinations.n less than k": "Tidak bisa menghitung kombinasi: n lebih kecil dari k", "math.permutations.n less than zero": "Tidak bisa menghitung permutasi: n lebih kecil dari nol", "math.permutations.k less than zero": "Tidak bisa menghitung permutasi: k lebih kecil dari nol", "math.permutations.n less than k": "Tidak bisa menghitung permutasi: n lebih kecil dari k", "part.numberentry.give your answer to precision_0": "Bulatkan jawaban Anda ke bilangan bulat terdekat.", "mathjax.error": "Kesalahan pemrosesan MathJax: {{-message}}", "mathjax.error with context": "Kesalahan pemrosesan MathJax dalam {{-context}}: {{-message}}", "exam.introduction": "Pengantar ujian", "exam.feedback": "Pesan umpan balik ujian", "jme.tokenise.keypair key not a string": "Key dari dictionary haruslah berupa string, bukan {{type}}.", "jme.shunt.list mixed argument types": "Tidak dapat mem-parse {{mode}}: campuran dari dictionary dan list element", "jme.func.listval.key not in dict": "Dictionary tidak mengandung key <code>{{key}}</code>", "part.prompt": "segera", "part.feedback": "umpan balik", "part.numberentry.answer not reduced": "Jawaban Anda tidak dalam bentuk paling sederhana.", "part.numberentry.give your answer as a reduced fraction": "Buat jawaban Anda ke dalam bentuk paling sederhana.", "part.numberentry.negative decimal places": "Bagian ini disiapkan untuk membulatkan jawaban siswa ke-n angka di belakang koma, dengan n bilangan negatif, yang tidak ada artinya.", "part.mcq.choices": "pilihan", "part.mcq.answers": "jawaban", "part.mcq.matrix cell empty": "Bagian {{part}} pada sel matriks penilaian ({{row}},{{column}}) kosong", "part.mcq.matrix jme error": "Bagian {{part}} pada sel matriks penilaian ({{row}},{{column}}) memberikan error JME: {{-error}}", "question.statement": "Pernyataan", "ruleset.circular reference": "Referensi sirkular dalam definisi ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} tidak didefinisikan", "jme.evaluate.no scope given": "Numbas.jme.evaluate harus diberikan Scope", "answer.number.not a number": "Jawaban Anda bukan angka yang valid.", "answer.number.fractions not allowed": "Anda tidak diperbolehkan memasukkan pecahan.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Anda tidak diperbolehkan memasukkan pecahan.", "answer.matrix.some cell not a number": "Satu atau lebih sel pada jawaban Anda bukan angka yang valid.", "exam.enter password": "Kata Sandi:", "exam.password.correct": "Kata sandi benar. Anda dapat memulai ujian.", "exam.password.incorrect": "Kata sandi salah.", "frontpage.scorm.lms not connected": "Ujian ini berjalan di moda mandiri. Jawaban dan nilai Anda tidak akan tersimpan!", "result.question review": "Ulasan", "control.confirm regen no marks": "Apakah Anda ingin mengacak ulang soal ini?", "control.confirm reveal no marks": "Apakah Anda ingin melihat jawaban dari soal ini?", "jme.tokenise.invalid near": "Ekspresi tidak valid: <code>{{expression}}</code> pada posisi {{position}} dekat <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Objek yang tidak valid diteruskan ke konstruktor bilangan.", "jme.subvars.null substitution": "Substitusi variabel kosong: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Tipe data {{type}} sudah terdaftar sehingga tidak dapat didaftarkan lagi.", "jme.type.no cast method": "Tidak dapat mengonversi dari {{from}} ke {{to}}.", "jme.display.simplifyTree.empty expression": "Pernyataan kosong", "jme.display.simplifyTree.stuck in a loop": "Penyederhana terjebak dalam loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Mengharapkan suatu bilangan, tetapi mendapatkan <code>undefined</code>", "math.rangeToList.zero step size": "Tidak dapat mengonversi range dengan step berukuran nol ke suatu list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Anda membuka langkahnya.", "part.marking.maximum scaled down": "Skor maksimum yang bisa Anda dapatkan pada bagian ini adalah <strong>{{count,niceNumber}}</strong> $t(mark). Skor Anda akan diskalakan ke bawah.", "part.marking.minimum score applied": "Skor minimum untuk bagian ini adalah <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "Skor maksimum untuk bagian ini adalah <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "Terdapat kesalahan algoritma penilaian pada bagian ini. Harap laporkan hal ini. {{-message}}", "part.marking.no result after replacement": "Bagian ini tidak bisa dinilai dari jawaban Anda pada bagian sebelumnya.", "part.marking.missing required note": "Algoritma penilaian tidak mendefinisikan catatan <code>{{note}}</code>", "marking.apply.not a list": "Argumen pertama untuk <code>apply</code> haruslah merupakan list, dan bukan", "marking.apply marking script.script not found": "Skrip penilaian <code>{{name}}</code> tidak ditemukan", "marking.note.compilation error": "Kesalahan dalam mengompilasi catatan <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Kesalahan dalam mengevaluasi catatan <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Definisi catatan tidak valid: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Anda mungkin lupa titik dua setelah nama dan deskripsi", "marking.note.invalid definition.description missing closing bracket": "Anda mungkin lupa kurung tutup", "marking.note.empty expression": "Catatan <code>{{name}}</code> kosong.", "marking.script.error parsing notes": "Kesalahan dalam mem-parse skrip penilaian: {{-message}}", "part.feedback out of date": "Umpan balik ini berdasarkan jawaban yang terakhir dikirim. Kirim jawaban yang telah diubah untuk mendapatkan umpan balik baru.", "part.jme.invalid value generator expression": "Ekspresi pembuat nilai tidak valid untuk variabel <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Anda telah memilih jawaban yang salah.", "part.matrix.not all cells same precision": "Anda belum memberikan presisi yang sama untuk tiap sel pada jawaban Anda.", "part.gapfill.error marking gap": "Kesalahan dalam menilai {{name}}: {{-message}}", "part.custom.empty setting": "Tidak ada nilai yang diberikan.", "part.custom.unrecognised input type": "Jenis pengaturan <code>{{input_type}}</code> tidak dikenal", "part.custom.error evaluating input option": "Kesalahan dalam mengevaluasi opsi input <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definisi dari opsi input <code>{{option}}</code> hilang.", "part.custom.error evaluating setting": "Kesalahan dalam mengevaluasi pengaturan <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Kesalahan dalam membuat pertanyaan {{number}}: {{-message}}", "question.score feedback.not marked": "Tidak dinilai", "question.score feedback.partially answered": "Dijawab sebagian", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Skor: {{scoreString}}", "question.score feedback.score total actual": "Skor: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Kesalahan dalam definisi variabel <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Tidak dapat memuat ekstensi<code>{{name}}</code>.", "control.toggle navigation menu": "Ganti menu navigasi", "part.input title": "Jawaban untuk bagian {{name}}", "part.correct answer title": "Jawaban yang diharapkan untuk bagian {{name}}", "part.jme.must-match.failed": "Jawaban Anda tidak dalam bentuk yang tepat.", "control.submit part.confirm remove next parts": "<p>Satu atau lebih bagian berikutnya bergantung pada jawaban Anda untuk bagian ini. Mengirimkan bagian ini lagi akan membatalkan bagian-bagian tersebut dan menghapusnya dari pertanyaan terkait. Hal ini tidak dapat dibatalkan.</p>\n<p> Apakah Anda ingin mengirimkan bagian ini lagi? </p>", "control.back to menu": "Kembali ke menu", "display.error making html": "Kesalahan membuat HTML di {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} pada <code>{{expression}}</code>", "jme.variables.empty name": "Variabel soal belum diberi nama.", "jme.calculus.unknown derivative": "Tidak mengetahui bagaimana membedakan <code>{{tree}}</code>", "math.order complex numbers": "Tidak dapat mengurutkan bilangan kompleks", "menu.choose a question": "Pilih soal.", "part.choose next part.answered": "Apa yang akan Anda lakukan selanjutnya?", "part.choose next part.unanswered": "Atau, Anda bisa:", "part.choose next part.will be locked": "(Bagian ini akan dikunci)", "part.reached dead end": "Tidak ada lagi yang bisa dilakukan sekarang.", "part.next part.penalty amount": "(kehilangan {{count}} $t(mark))", "part.marking.counts towards objective": "Bagian ini diperhitungkan dalam tujuan <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Jawaban Anda tidak sesuai. Anda harus memasukkan bilangan asli, desimal, atau pecahan.", "question": "Soal", "question.progress": "Perkembangan soal:", "question.score feedback.unattempted": "Tidak dicoba", "question.score feedback.attempted": "Dicoba", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Tujuan", "question.penalties": "Hukuman", "question.back to previous part": "Kembali ke bagian sebelumnya", "end.print": "Cetak transkrip ujian Anda", "math.shuffle_together.lists not all the same length": "Tidak semua list memiliki panjang sama.", "jme.parse signature.invalid signature string": "String untuk signature fungsi tidak valid: {{str}}", "part.custom.expected answer has wrong type": "Jawaban yang diharapkan untuk bagian ini memiliki tipe yang salah. Seharusnya <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "Jenis setelan input jawaban <code>{{option}}</code> salah. Seharusnya <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Ukuran", "matrix input.rows": "Baris", "matrix input.columns": "Kolom", "part.jme.error checking numerically": "Terdapat kesalahan saat memeriksa jawaban Anda secara numerik: {{-message}}", "part.gapfill.cyclic adaptive marking": "Terdapat cycle dalam penilaian adaptif untuk bagian ini: <strong>{{name1}}</strong> bergantung pada <strong>{{name2}}</strong>, yang pada akhirnya bergantung lagi pada <strong>{{name1}}</strong>.", "modal.style.background colour": "Warna latar belakang", "modal.style.text colour": "Warna teks", "modal.style.text size": "Ukuran teks", "modal.style.explanation": "Gunakan kontrol ini untuk mengubah tampilan ujian.", "modal.style.reset to defaults": "Reset ke default", "modal.style.text size preview": "Mayoritas teks akan sebesar ini.", "control.style options": "Pengaturan Tampilan", "part.marking.partially correct": "Jawaban Anda benar sebagian.", "part.marking.error in adaptive marking": "Terdapat kesalahan dalam penilaian adaptif pada bagian ini. Harap laporkan hal ini. {{-message}}", "page.skip to content": "Lewati ke konten", "result.learning objective": "Tujuan pembelajaran", "jme.interpreted as": "diinterpretasikan sebagai", "jme.script.note.compilation error": "Kesalahan dalam mengompilasi catatan <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Kesalahan dalam mengevaluasi catatan <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Definisi catatan tidak benar: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "Anda mungkin melupakan titik dua setelah nama dan deskripsi", "jme.script.note.invalid definition.description missing closing bracket": "Anda mungkin melupakan kurung tutup", "jme.script.note.empty expression": "Catatan <code>{{name}}</code> kosong.", "jme.script.error parsing notes": "Kesalahan dalam mem-parse skrip penilaian: {{-message}}", "matrix input.cell label": "Baris {{row}}, kolom {{column}}", "control.move to next question": "Beralih ke soal selanjutnya", "diagnostic.use retry": "Gunakan satu percobaan dan coba topik ini lagi.", "diagnostic.move to next topic": "Beralih ke topik selanjutnya.", "diagnostic.next step question": "Apa yang ingin Anda lakukan selanjutnya?", "diagnostic.now assessing topic": "Sekarang melakukan asesmen {{current_topic}}", "diagnostic.one retry left": "Anda memiliki 1 percobaan lagi", "diagnostic.retries left": "Anda memiliki {{retries}} percobaan lagi.", "diagnostic.percentage completed": "Anda menyelesaikan <strong>{{percentage}}%</strong> dari ujian.", "diagnostic.test is over": "Ujian berakhir.", "diagnostic.passed all lo": "Anda lulus seluruh tujuan pembelajaran.", "diagnostic.more work on lo": "Anda perlu melakukan beberapa pekerjaan lagi pada tujuan pembelajaran berikut: {{los}}.", "diagnostic.move to next question in topic": "Beralih pada pertanyaan selanjutnya dalam topik ini.", "diagnostic.complete": "Selesai!", "diagnostic.studying topic": "Mempelajari {{topic}}", "display.answer widget.unknown widget type": "Jenis widget jawaban <code>{{name}}</code> tidak dikenali.", "jme.shunt.expected argument before comma": "Diharapkan untuk melihat sesuatu di antara kurung buka dan tanda koma", "part.waiting for pre submit": "Jawaban Anda sedang dinilai. Mohon tunggu.", "diagnostic.end test": "Akhir ujian.", "page.no stylesheet loaded": "File stylesheet halaman belum dimuat.", "modal.confirm": "Konfirmasi", "modal.alert": "Peringatan", "suspend.resumed header": "Upaya dilanjutkan", "jme.vector.value not an array of numbers": "Mencoba membuat vektor menggunakan nilai yang bukan merupakan array dari bilangan.", "jme.matrix.value not the right type": "Mencoba membuat vektor menggunakan nilai dengan tipe yang salah.", "jme.subvars.html inserted twice": "Suatu nilai HTML disematkan dua kali. Pertimbangkan untuk mendefinisikan fungsi untuk menghasilkan nilai baru setiap kali digunakan.", "jme.variables.invalid function language": "Bahasa <code>{{language}}</code> tidak valid.", "jme.variables.duplicate definition": "Terdapat lebih dari satu definisi untuk variabel <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "Ukuran partisi harus berada di antara 1 dan {{n}}.", "part.marking.parameter already in scope": "Terdapat variabel bernama <code>{{name}}</code>, yang juga merupakan nama dari suatu parameter penilaian. Harap namai ulang variabel tersebut.", "part.marking.adaptive variable replacement refers to self": "Bagian ini mengacu pada dirinya sendiri dalam pengganti variabel untuk penilaian adaptif.", "part.marking.adaptive variable replacement refers to nothing": "Bagian ini mengandung pengganti variabel yang tidak valid untuk penilaian adaptif.", "part.numberentry.display answer wrong type": "Tampilan jawaban untuk bagian ini merupakan nilai bertipe <code>{{got_type}}</code>, yang seharusnya <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "Terdapat nilai tidak valid bertipe <code>{{n}}</code> dalam array sel yang diisi sebelumnya.", "diagnostic.make a choice": "Silakan pilih", "matrixmath.not square": "Operasi ini hanya berlaku untuk matriks persegi.", "matrixmath.not invertible": "Operasi ini hanya berlaku untuk matriks terinverskan.", "matrixmath.gauss-jordan elimination.not enough columns": "Setidaknya banyaknya kolom harus sebanyak baris.", "question.required extension not available": "Soal ini memerlukan ekstensi <code>{{-extension}}</code> yang tidak tersedia.", "util.formatNumberNotation.unrecognised syntax": "Sintaks pemformatan bilangan <code>{{syntax}}</code> tidak dikenal.", "worksheet.number of exams to generate": "Banyaknya lembar kerja yang akan dibuat", "worksheet.starting with id": "Dimulai dengan ID", "worksheet.show exam id": "Tampilkan ID lembar kerja?", "worksheet.page break between questions": "Pisah halaman antar soal?", "worksheet.page margins": "Margin halaman (mm)", "worksheet.text size": "Ukuran teks (pt)", "worksheet.generate": "Buat", "worksheet.generating exams": "Membuat lembar kerja", "worksheet.sheet id": "ID Lembar Kerja:", "worksheet.print single": "Print lembar kerja ini", "worksheet.print several": "Print lembar kerja ini", "worksheet.answer sheets": "Lembar kerja untuk jawaban", "worksheet.question sheets": "Lembar kerja untuk soal", "worksheet.reconfigure": "Buat lembar kerja berbeda", "worksheet.show sheet": "Pratinjau lembar kerja dengan ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "fr-fr": {"translation": {"page.loading": "Chargement...", "page.saving": "<p>Sauvegarde en cours.</p>\n<p>Cela peut prendre quelques instants.</p>", "mathjax.math processing error": "\"{{-message}}\" lors du rendu tex de <code>{{expression}}</code>", "die.numbas failed": "Numbas a \u00e9chou\u00e9", "die.sorry": "D\u00e9sol\u00e9, Numbas a rencontr\u00e9 une erreur fatale et ne peut continuer. Une description de l'erreur suit ci-dessous.", "die.error": "Erreur", "modal.ok": "OK", "modal.cancel": "Annuler", "exam.exam name": "Nom de l'examen:", "exam.random seed": "Identificateur de session:", "exam.student name": "Nom de l'\u00e9tudiant:", "exam.number of questions": "Nombre de questions:", "exam.marks available": "Points disponibles:", "exam.pass percentage": "Pourcentage de r\u00e9ussite:", "exam.time allowed": "Temps accord\u00e9:", "exam.passed": "R\u00e9ussi", "exam.failed": "\u00c9chou\u00e9", "exam.review header": "R\u00e9vision:", "frontpage.start": "D\u00e9marrer", "suspend.paused header": "En pause", "suspend.exam suspended": "L'examen a \u00e9t\u00e9 suspendu. Appuyez sur <em>Reprendre</em> pour continuer.", "suspend.you can resume": "Vous pourrez reprendre cette session la prochaine fois que vous d\u00e9marrerez cette activit\u00e9.", "suspend.resume": "Reprendre", "result.exit": "Quitter l'examen", "result.print": "Imprimer le r\u00e9sum\u00e9 des notes", "result.exam summary": "Sommaire de l'examen", "result.performance summary": "Sommaire du rendement", "result.exam start": "D\u00e9but de l'examen:", "result.exam stop": "Fin de l'examen:", "result.time spent": "Temps \u00e9coul\u00e9:", "result.questions attempted": "Questions essay\u00e9es:", "result.score": "Note:", "result.result": "R\u00e9sultat:", "result.question number": "Num\u00e9ro de la question", "result.question score": "Note", "result.question review title": "R\u00e9viser cette question", "result.click a question to review": "Cliquez sur le num\u00e9ro d'une question pour voir comment vos r\u00e9ponses ont \u00e9t\u00e9 not\u00e9es et, le cas \u00e9ch\u00e9ant, les solutions compl\u00e8tes.", "end.exam has finished": "L'examen est termin\u00e9. Vous pouvez d\u00e9sormais fermer cette fen\u00eatre.", "control.confirm leave": "Vous n'avez pas termin\u00e9 l'examen.", "control.not all questions answered": "Vous n'avez pas compl\u00e9t\u00e9 toutes les questions de cet examen.", "control.not all questions submitted": "Vous avez modifi\u00e9 une ou plusieurs r\u00e9ponses et ne les avez pas soumises. S.V.P., v\u00e9rifiez que chaque question a \u00e9t\u00e9 soumise.", "control.confirm end": "\u00cates-vous certain de vouloir terminer l'examen? Apr\u00e8s avoir termin\u00e9 l'examen, vous ne pourrez plus changer vos r\u00e9ponses.", "control.confirm regen": "Voulez-vous \u00e0 nouveau r\u00e9partir au hasard cette question? Si vous cliquez OK, toutes vos r\u00e9ponses et vos r\u00e9sultats pour la question en cours seront perdus.", "control.confirm reveal": "Voulez-vous r\u00e9v\u00e9ler la r\u00e9ponse \u00e0 cette question? Tous les points re\u00e7us jusqu'\u00e0 maintenant seront verrouill\u00e9s et vous ne pourrez pas r\u00e9pondre \u00e0 cette question plus tard.", "control.proceed anyway": "Continuer malgr\u00e9 tout?", "control.regen": "Essayer une autre question comme celle-ci", "control.submit answer": "Soumettre la r\u00e9ponse", "control.submit all parts": "Soumettre toutes les parties", "control.submit again": "Soumettre \u00e0 nouveau", "control.submit": "Soumettre", "control.previous": "Pr\u00e9c\u00e9dent", "control.next": "Suivant", "control.advice": "Conseil", "control.reveal": "R\u00e9v\u00e9ler les r\u00e9ponses", "control.total": "Total", "control.pause": "Pause", "control.end exam": "Fin de l'examen", "control.back to results": "Retourner aux r\u00e9sultats", "display.part.jme.error making maths": "Erreur lors de l'affichage des math\u00e9matiques", "exam.xml.bad root": "L'\u00e9l\u00e9ment racine de l'examen XML devrait \u00eatre 'exam'", "exam.changeQuestion.no questions": "Cet examen ne contient aucune question! V\u00e9rifiez s'il y a des erreurs dans le fichier .exam.", "feedback.you were awarded": "Vous avez re\u00e7u <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) retir\u00e9(s).", "jme.tokenise.invalid": "Expression invalide: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Pas assez d'arguments pour l'op\u00e9ration {{op}}", "jme.shunt.no left bracket in function": "Aucun crochet ouvrant correspondant dans la fonction ou le tuple", "jme.shunt.no left square bracket": "Aucun crochet ouvrant correspondant", "jme.shunt.no left bracket": "Aucun crochet ouvrant correspondant", "jme.shunt.no right bracket": "Aucun crochet fermant correspondant", "jme.shunt.no right square bracket": "Aucun crochet fermant correspondant pour fermer la liste", "jme.shunt.missing operator": "L'expression ne peut \u00eatre \u00e9valu\u00e9e -- il manque un op\u00e9rateur.", "jme.typecheck.function maybe implicit multiplication": "L'op\u00e9ration {{name}} n'est pas d\u00e9finie. Voulez-vous dire <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "L'op\u00e9ration <code>{{op}}</code> n'est pas d\u00e9finie. Est-ce que <code>{{op}}</code> est une variable et voulez-vous dire <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "L'op\u00e9ration '{{op}}' n'est pas d\u00e9finie.", "jme.typecheck.no right type definition": "Aucune d\u00e9finition du bon type trouv\u00e9e pour '{{op}}'.", "jme.typecheck.no right type unbound name": "La variable <code>{{name}}</code> n'est pas d\u00e9finie.", "jme.typecheck.map not on enumerable": "L'op\u00e9ration <code>map</code> doit travailler sur une liste ou une plage, pas sur {{type}}", "jme.evaluate.undefined variable": "La variable {{name}} n'est pas d\u00e9finie", "jme.thtml.not html": "Valeur non-HTML pass\u00e9e dans le constructeur THTML.", "jme.func.switch.no default case": "Aucun cas par d\u00e9faut pour l'instruction Switch.", "jme.func.listval.invalid index": "Indice de liste {{index}} invalide sur une liste de taille {{size}}", "jme.func.listval.not a list": "L'objet n'est pas indi\u00e7able\n", "jme.func.matrix.invalid row type": "Impossible de construire une matrice \u00e0 partir de rang\u00e9es de type {{type}}", "jme.func.except.continuous range": "Impossible d'utiliser l'op\u00e9rateur 'except' sur une plage continue.", "jme.matrix.reports bad size": "La matrice d\u00e9clare sa taille incorrectement - doit \u00eatre une erreur dans la fonction du constructeur", "jme.texsubvars.no right bracket": "Aucun <code>]</code> dans les arguments {{op}}.", "jme.texsubvars.missing parameter": "Param\u00e8tre manquant dans {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Aucun <code>}</code> dans {{op}}", "jme.user javascript.error": "Erreur dans la fonction javascript d\u00e9finie par l'utilisateur <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Erreur lors de la cr\u00e9ation de la fonction <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Erreur de syntaxe dans la d\u00e9finition de la fonction", "jme.variables.variable not defined": "La variable <code>{{name}}</code> n'est pas d\u00e9finie.", "jme.variables.empty definition": "La d\u00e9finition de la variable <code>{{name}}</code> est vide.", "jme.variables.circular reference": "R\u00e9f\u00e9rence circulaire \u00e0 une variable dans la d\u00e9finition de <code>{{name}}</code>", "jme.variables.error computing dependency": "Erreur lors du calcul de la variable r\u00e9f\u00e9renc\u00e9e <code>{{name}}</code>", "jme.variables.error evaluating variable": "Erreur lors de l'\u00e9valuation de la variable {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Un ensemble valide de variables de question n'a pas \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9 \u00e0 temps.", "jme.display.unknown token type": "Impossible de textifier le jeton de type {{type}}", "jme.display.collectRuleset.no sets": "Aucun ensemble n'a \u00e9t\u00e9 fourni \u00e0 collectRuleset!", "jme.display.collectRuleset.set not defined": "L'ensemble de r\u00e8gles {{name}} n'a pas \u00e9t\u00e9 d\u00e9fini", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree doit avoir une port\u00e9e", "math.precround.complex": "Impossible d'arrondir un nombre complexe de d\u00e9cimales", "math.siground.complex": "Impossible d'arrondir \u00e0 un nombre complexe de chiffres significatifs", "math.combinations.complex": "Impossible de calculer des combinaisons de nombres complexes", "math.permutations.complex": "Impossible de calculer des permutation de nombres complexes.", "math.gcf.complex": "Impossible de calculer le plus grand facteur commun de nombres complexes.", "math.lcm.complex": "Impossible de calculer le plus petit commun multiple de nombres complexes.", "math.lt.order complex numbers": "Impossible de trier des nombres complexes.", "math.choose.empty selection": "S\u00e9lection vide donn\u00e9e \u00e0 la fonction al\u00e9atoire", "matrixmath.abs.non-square": "Impossible de calculer le d\u00e9terminant d'une matrice qui n'est pas carr\u00e9e.", "matrixmath.abs.too big": "D\u00e9sol\u00e9, impossible pour l'instant de calculer le d\u00e9terminant d'une matrice plus grande que 3x3.", "matrixmath.mul.different sizes": "Impossible de multiplier des matrices de tailles diff\u00e9rentes.", "vectormath.cross.not 3d": "Ne peut prendre que le produit vectoriel de vecteurs tridimensionnels.", "vectormath.dot.matrix too big": "Impossible de calculer le produit scalaire d'une matrice qui n'est pas $1 \\times N$ ou $N \\times 1$.", "vectormath.cross.matrix too big": "Impossible de calculer le produit vectoriel d'une matrice qui n'est pas $1 \\times N$ ou $N \\times 1$.", "part.with steps answer prompt": "R\u00e9ponse: ", "part.script.error": "Erreur dans {{path}} du script personnalis\u00e9 {{script}}: {{-message}}\n", "part.marking.steps no matter": "Parce que vous avez re\u00e7u toutes les points pour la partie, vos r\u00e9ponses aux \u00e9tapes ne sont pas compt\u00e9es.", "part.marking.revealed steps no penalty": "Vous avez r\u00e9v\u00e9l\u00e9 les \u00e9tapes.", "part.marking.used variable replacements": "Cette partie a \u00e9t\u00e9 not\u00e9e en utilisant vos r\u00e9ponses aux parties pr\u00e9c\u00e9dentes.", "part.marking.variable replacement part not answered": "Vous devez r\u00e9pondre \u00e0 {{part}} en premier.", "part.marking.resubmit because of variable replacement": "Le note de cette partie d\u00e9pend de vos r\u00e9ponses \u00e0 d'autres parties que vous avez modifi\u00e9es. Soumettez \u00e0 nouveau cette partie pour mettre \u00e0 jour votre r\u00e9sultat.", "part.marking.not submitted": "Aucune r\u00e9ponse soumise", "part.marking.did not answer": "Vous n'avez pas r\u00e9pondu \u00e0 cette question.", "part.marking.nothing entered": "Vous n'avez pas entr\u00e9 de r\u00e9ponse.", "part.marking.incorrect": "Votre r\u00e9ponse est incorrecte.", "part.marking.correct": "Votre r\u00e9ponse est correcte.", "part.marking.uncaught error": "Erreur lors de l'\u00e9valuation de la {{part}}: {{-message}}", "part.marking.no result": "Cette partie n'a pas pu \u00eatre \u00e9valu\u00e9e.", "part.correct answer": "R\u00e9ponse attendue:", "part.missing type attribute": "{{part}}: Attribut de type de partie manquant", "part.unknown type": "{{part}}: Type de partie {{type}} non reconnu", "part.setting not present": "Propri\u00e9t\u00e9 '{{property}}' non d\u00e9finie", "part.jme.answer missing": "R\u00e9ponse correcte manquante", "part.jme.answer too long": "Votre r\u00e9ponse est trop longue.", "part.jme.answer too short": "Votre r\u00e9ponse est trop courte.", "part.jme.answer invalid": "Votre r\u00e9ponse n'est pas une expression math\u00e9matique valide.<br/>{{-message}}.", "part.jme.marking.correct": "Votre r\u00e9ponse est num\u00e9riquement correcte.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Votre r\u00e9ponse doit contenir: {{strings}}", "part.jme.must-have several": "Votre r\u00e9ponse doit contenir tous les \u00e9l\u00e9ments suivants: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Votre r\u00e9ponse ne doit pas contenir: {{strings}}", "part.jme.not-allowed several": "Votre r\u00e9ponse ne doit pas contenir: {{strings}}", "part.jme.unexpected variable name": "Votre r\u00e9ponse a \u00e9t\u00e9 interpr\u00e9t\u00e9e pour utiliser le nom de variable inattendue <code> {{name}} </ code>.", "part.jme.unexpected variable name suggestion": "Votre r\u00e9ponse a \u00e9t\u00e9 interpr\u00e9t\u00e9e pour utiliser le nom de variable inattendue <code> {{name}} </ code>. Voulez-vous dire <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "L'affichage de la r\u00e9ponse est manquant", "part.patternmatch.correct except case": "Votre r\u00e9ponse est correcte, \u00e0 l\u2019exception des majuscules et minuscules.", "part.numberentry.correct except decimal": "Votre r\u00e9ponse se situe dans la plage autoris\u00e9e, mais les nombres d\u00e9cimaux ne sont pas autoris\u00e9s.", "part.numberentry.correct except fraction": "Votre r\u00e9ponse se situe dans la plage autoris\u00e9e, mais les fractions ne sont pas autoris\u00e9es.", "part.numberentry.answer invalid": "Vous n'avez pas entr\u00e9 un nombre valide.", "part.numberentry.answer not integer": "Votre r\u00e9ponse est invalide. Vous devez entrer un nombre entier, pas un nombre d\u00e9cimal.", "part.numberentry.answer not integer or decimal": "Votre r\u00e9ponse est invalide. Vous devez entrer un nombre entier ou un nombre d\u00e9cimal.", "part.numberentry.zero sig fig": "Cette partie est r\u00e9gl\u00e9e pour arrondir la r\u00e9ponse de l'\u00e9tudiant \u00e0 z\u00e9ro chiffre significatif, ce qui n'a aucun sens.", "part.mcq.options def not a list": "L'expression d\u00e9finissant les {{properties}} n'est pas une liste.", "part.mcq.marking matrix string empty": "L'expression de la matrice d'\u00e9valuation personnalis\u00e9e est vide.", "part.mcq.choices missing": "La d\u00e9finition des choix est manquante", "part.mcq.matrix not a number": "Partie {{part}} de la cellule ({{row}},{{column}}) de la matrice d'\u00e9valuation ne donne pas un nombre", "part.mcq.wrong number of choices": "Vous avez s\u00e9lectionn\u00e9 le mauvais nombre de choix.", "part.mcq.no choices selected": "Aucun choix s\u00e9lectionn\u00e9.", "part.mcq.matrix not a list": "La matrice d'\u00e9valuation, d\u00e9finie par l'expression JME, n'est pas une liste mais elle devrait l'\u00eatre.", "part.mcq.matrix wrong type": "\u00c9l\u00e9ment de type '{{type}}' invalide utilis\u00e9 dans la matrice d'\u00e9valuation.\n", "part.mcq.matrix mix of numbers and lists": "M\u00e9lange de nombres et de listes utilis\u00e9s dans la matrice d'\u00e9valuation.", "part.mcq.matrix wrong size": "La matrice d'\u00e9valuation est de la mauvaise taille.", "part.mcq.correct choice": "Vous avez choisi la bonne r\u00e9ponse.", "part.matrix.answer invalid": "Votre r\u00e9ponse n'est pas valide.", "part.matrix.invalid cell": "Une ou plusieurs cellules de votre r\u00e9ponse sont vides ou invalides.", "part.matrix.some incorrect": "Une ou plusieurs cellules de votre r\u00e9ponse sont incorrectes, mais vous avez re\u00e7u des points pour le reste.", "part.matrix.empty": "Vous n'avez pas entr\u00e9 de r\u00e9ponse.", "part.matrix.empty cell": "Une ou plusieurs cellules de votre r\u00e9ponse sont vides.\n", "part.matrix.size mismatch": "L'auteur de la question n'a pas permis \u00e0 l'\u00e9tudiant de d\u00e9cider des dimensions de sa r\u00e9ponse, mais la bonne r\u00e9ponse est {{correct_dimensions}} alors que la r\u00e9ponse est {{input_dimensions}}", "part.gapfill.feedback header": "<strong>\u00c9cart {{index}}</strong>", "part.extension.not implemented": "La partie n'a pas impl\u00e9ment\u00e9 la m\u00e9thode <code>{{name}}</code>.", "question.loaded name mismatch": "Impossible de reprendre cette tentative - le progiciel a chang\u00e9 depuis la derni\u00e8re session.", "question.error": "Question {{number}}: {{-message}}", "question.preamble.error": "Erreur dans le pr\u00e9ambule: {{-message}}", "question.preamble.syntax error": "Erreur de syntaxe dans le pr\u00e9ambule", "question.unsupported part type": "Type de partie non support\u00e9", "question.header": "Question {{number}}", "question.submit part": "Soumettre la partie", "question.show steps": "Afficher les \u00e9tapes", "question.show steps penalty": "Vous allez perdre <strong>{{count,niceNumber}}</strong> $t(mark).\n", "question.show steps no penalty": "Votre r\u00e9sultat ne sera pas influenc\u00e9.", "question.show steps already penalised": "Vous avez d\u00e9j\u00e0 affich\u00e9 les \u00e9tapes. Vous pouvez les afficher de nouveau sans p\u00e9nalit\u00e9s.", "question.hide steps": "Masquer les \u00e9tapes", "question.hide steps no penalty": "Votre r\u00e9sultat ne sera pas influenc\u00e9.", "question.advice": "Conseil", "question.no such part": "Impossible de trouver la partie {{path}}", "question.can not submit": "Impossible de soumettre la r\u00e9ponse - v\u00e9rifiez s'il y a des erreurs.", "question.answer submitted": "R\u00e9ponse envoy\u00e9e.", "question.score feedback.show": "Afficher la r\u00e9troaction", "question.score feedback.hide": "Masquer la r\u00e9troaction", "question.score feedback.answered total actual": "R\u00e9sultat: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. R\u00e9pondu.\n", "question.score feedback.answered actual": "R\u00e9sultat: {{scoreString}}", "question.score feedback.answered": "R\u00e9pondu.", "question.score feedback.unanswered": "Sans r\u00e9ponse.", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Votre r\u00e9ponse est correcte.", "question.score feedback.partial": "Votre r\u00e9ponse est partiellement correcte", "question.score feedback.wrong": "Votre r\u00e9ponse est incorrecte", "question.selector.unsubmitted changes": "Modifications non soumises.", "timing.no accumulator": "pas d'accumulateur de chronom\u00e9trage {{name}}", "timing.time remaining": "Temps restant:", "xml.could not load": "Impossible de charger un document XML: {{-message}}", "xml.property not number": "La propri\u00e9t\u00e9 {{name}} devrait \u00eatre un nombre, mais n'en est pas un ({{value}}), dans le noeud {{element}}", "xml.property not boolean": "La propri\u00e9t\u00e9 {{name}} devrait \u00eatre un bool\u00e9en, mais n'en est pas un ({{value}}), dans le noeud {{element}}", "xml.error in variable definition": "Erreur dans la d\u00e9finition de la variable <code>{{name}}</code>", "scorm.error initialising": "Erreur lord de l'initialisation du protocole SCORM: {{-message}}", "scorm.failed save": "<p>La requ\u00eate pour sauvegarder les donn\u00e9es sur le serveur a \u00e9chou\u00e9. Appuyez sur <b>OK</b> pour essayer \u00e0 nouveau.</p>\n<p>Si vous rencontrez ce message de fa\u00e7on r\u00e9p\u00e9t\u00e9e, v\u00e9rifiez votre connexion internet ou utilisez un autre ordinateur. Les r\u00e9ponses que vous avez envoy\u00e9es pr\u00e9c\u00e9demment ont \u00e9t\u00e9 sauvegard\u00e9es avec succ\u00e8s et seront restaur\u00e9es si vous reprenez cette session sur un autre ordinateur.</p>\n<p>Si ce message apparait constamment et que vous ne pouvez sauvegarder <em>aucune</em> de vos r\u00e9ponses, merci de contacter votre professeur.</p>", "scorm.no exam suspend data": "Impossible de reprendre: pas de donn\u00e9es d'examen en suspend.", "scorm.error loading suspend data": "Erreur lors du chargement des donn\u00e9es en suspend: {{-message}}", "scorm.error loading question": "Erreur lors du chargement de la question {{number}}: {{-message}}", "scorm.no question suspend data": "Pas de donn\u00e9es en suspend pour la question", "scorm.error loading part": "Erreur lors du chargement de la partie {{part}}: {{-message}}", "scorm.no part suspend data": "Pas de donn\u00e9es en suspend pour la partie", "util.product.non list": "Un \u00e9l\u00e9ment qui n'est pas une liste a \u00e9t\u00e9 pass\u00e9 \u00e0 <code>Numbas.util.product</code>\n", "mark": "point", "was": "\u00e9tait", "part": "partie", "gap": "\u00e9cart", "step": "\u00e9tape", "jme.substituteTree.undefined variable": "Variable non d\u00e9finie: <code>{{name}}</code>", "jme.user javascript.returned undefined": "La fonction javascript d\u00e9finie par l'utilisateur <code>{{name}}</ code> a renvoy\u00e9 <code>undefined</ code>.", "part.marking.steps change": "Vous avez re\u00e7u <strong>{{count,niceNumber}}</strong> $t(mark) pour vos r\u00e9ponses aux \u00e9tapes.", "part.marking.revealed steps with penalty": "Vous avez affich\u00e9 les \u00e9tapes. Le r\u00e9sultat maximum pour cette partie est <strong>{{count,niceNumber}}</strong> $t(mark). Vos r\u00e9sultats seront r\u00e9duits en cons\u00e9quence.", "part.marking.total score": "Vous avez obtenu <strong>{{count,niceNumber}}</strong> $t(mark) pour cette partie.", "part.numberentry.precision type.dp": "d\u00e9cimale", "part.numberentry.precision type.dp_plural": "d\u00e9cimales", "part.numberentry.precision type.sigfig": "Chiffre significatif", "part.numberentry.precision type.sigfig_plural": "Chiffres significatifs", "part.numberentry.give your answer to precision": "Arrondissez votre r\u00e9ponse \u00e0 {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Vous avez apport\u00e9 une modification \u00e0 votre r\u00e9ponse mais vous ne l'avez pas soumise. Veuillez v\u00e9rifier votre r\u00e9ponse, puis appuyez sur le bouton <strong>Envoyer la r\u00e9ponse</strong>.", "question.unsubmitted changes_plural": "Vous avez apport\u00e9 des modifications \u00e0 vos r\u00e9ponses, mais vous ne les avez pas soumises. Veuillez v\u00e9rifier vos r\u00e9ponses \u00e0 chaque partie, puis appuyez sur le bouton <strong>Envoyer toutes les parties</strong>.", "util.equality not defined for type": "L'\u00e9galit\u00e9 n'est pas d\u00e9finie pour le {{type}}", "mark_plural": "points", "was_plural": "\u00e9taient", "die.script not loaded": "Numbas n'a pas pu d\u00e9marrer car le fichier <code>{{file}}</code> n'a pas \u00e9t\u00e9 charg\u00e9. V\u00e9rifiez qu'il est inclus dans <code>scripts.js</code>.\n", "math.combinations.n less than zero": "Impossible de calculer les combinaisons: n est plus petit que z\u00e9ro", "math.combinations.k less than zero": "Impossible de calculer les combinaisons: k est plus petit que z\u00e9ro", "math.combinations.n less than k": "Impossible de calculer les combinaisons: n est plus petit que k", "math.permutations.n less than zero": "Impossible de calculer les permutations: n est plus petit que z\u00e9ro", "math.permutations.k less than zero": "Impossible de calculer les permutations: k est plus petit que z\u00e9ro", "math.permutations.n less than k": "Impossible de calculer les permutations: n est plus petit k", "part.numberentry.give your answer to precision_0": "Arrondissez votre r\u00e9ponse \u00e0 l'entier le plus pr\u00e8s.", "mathjax.error": "Erreur de traitement MathJax: {{-message}}", "mathjax.error with context": "Erreur de traitement MathJax dans {{context}}: {{-message}}", "exam.introduction": "Introduction \u00e0 l'examen", "exam.feedback": "Message de r\u00e9troaction \u00e0 l'examen", "jme.tokenise.keypair key not a string": "La cl\u00e9 du dictionnaire doit \u00eatre une cha\u00eene, pas {{type}}.\n", "jme.shunt.list mixed argument types": "Impossible d'analyser {{mode}}: m\u00e9lange de dictionnaire et d'\u00e9l\u00e9ments de liste", "jme.func.listval.key not in dict": "Le dictionnaire ne contient pas la cl\u00e9 <code>{{key}}</code>", "part.prompt": "invite", "part.feedback": "r\u00e9troaction", "part.numberentry.answer not reduced": "Votre r\u00e9ponse n'est pas simplifi\u00e9e.", "part.numberentry.give your answer as a reduced fraction": "Simplifiez votre r\u00e9ponse.", "part.numberentry.negative decimal places": "Cette partie est r\u00e9gl\u00e9e pour arrondir la r\u00e9ponse de l'\u00e9tudiant \u00e0 un nombre n\u00e9gatif de d\u00e9cimales, ce qui n'a aucun sens.", "part.mcq.choices": "choix", "part.mcq.answers": "r\u00e9ponses", "part.mcq.matrix cell empty": "La partie {{part}} de la cellule ({{row}},{{column}}) de la matrice d'\u00e9valuation est vide", "part.mcq.matrix jme error": "La partie {{part}} de la cellule ({{row}},{{column}}) de la matrice d'\u00e9valuation retourne une erreur JME: {{error}}\n", "question.statement": "D\u00e9claration", "ruleset.circular reference": "R\u00e9f\u00e9rence circulaire dans la d\u00e9finition du jeu de r\u00e8gles <code>{{name}}</code>", "ruleset.set not defined": "Le jeu de r\u00e8gles {{name}} n'a pas \u00e9t\u00e9 d\u00e9fini", "jme.evaluate.no scope given": "Numbas.jme.evaluate doit avoir une port\u00e9e", "answer.number.not a number": "La r\u00e9ponse doit \u00eatre un nombre.", "answer.number.fractions not allowed": "La r\u00e9ponse ne doit pas \u00eatre une fraction.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "La r\u00e9ponse ne doit pas \u00eatre une fraction.", "answer.matrix.some cell not a number": "Une ou plusieurs cellule ne contient pas un nombre valide.", "exam.enter password": "Mot de passe:", "exam.password.correct": "Mot de passe correct. Vous pouvez commencer l'examen.", "exam.password.incorrect": "Mot de passe incorrect.", "frontpage.scorm.lms not connected": "L'examen est en mode d\u00e9connect\u00e9. Les r\u00e9ponses et les notes ne seront pas sauvegard\u00e9es ! ", "result.question review": "R\u00e9viser", "control.confirm regen no marks": "Souhaitez-vous recommencer cette question avec d'autres valeurs al\u00e9atoires ?", "control.confirm reveal no marks": "Souhaitez vous d\u00e9voiler la r\u00e9ponse de cette question ?", "jme.tokenise.invalid near": "expression invalide:  <code>{{expression}}</code> \u00e0  la position {{position}} pr\u00e8s de <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Objet invalide a \u00e9t\u00e9 pass\u00e9 \u00e0 le constructeur de nombres.", "jme.subvars.null substitution": "Variable vide substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Le type de donn\u00e9e {{type}} a d\u00e9j\u00e0 \u00e9t\u00e9 enregistr\u00e9, alors il ne peut pas \u00eatre r\u00e9enregistr\u00e9.", "jme.type.no cast method": "Impossible de convertir automatiquement {{from}} en {{to}}.", "jme.display.simplifyTree.empty expression": "Expression vide", "jme.display.simplifyTree.stuck in a loop": "Simplifier est coinc\u00e9 dans une boucle : <code>{{expr}}</code>", "math.niceNumber.undefined": "Un nombre \u00e9tait attendu, mais on a eu <code>undefined</code>", "math.rangeToList.zero step size": "Ne peut pas convertir un intervalle avec une taille de pas de z\u00e9ro en une liste.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Vous avez affich\u00e9 les indices.", "part.marking.maximum scaled down": "Le score maximal de cette partie est <strong>{{count,niceNumber}}</strong> $t(mark). Vos r\u00e9sultats vont \u00eatre abaiss\u00e9 en cons\u00e9quence.", "part.marking.minimum score applied": "Le score minimal de cette partie est <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "Le score maximal de cette partie est <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "Il y a une erreur dans l'algorithme de notation de cette partie. Merci de le signaler. {{-message}}", "part.marking.no result after replacement": "Cette partie ne peut pas \u00eatre \u00e9valu\u00e9e avec les r\u00e9ponses des pr\u00e9c\u00e9dentes parties.", "part.marking.missing required note": "L'algorithme de notation ne d\u00e9finit pas la note <code>{{note}}</code>", "marking.apply.not a list": "Le premier argument \u00e0 <code>appliquer</code> doit \u00eatre une liste, mais il n'est pas", "marking.apply marking script.script not found": "Le script de marquage <code>{{name}}</code> est introuvable", "marking.note.compilation error": "Erreur lors de la compilation de la note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Erreur lors de l'\u00e9valuation de la note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "D\u00e9finition de note invalide: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Il vous manque peut-\u00eatre deux points apr\u00e8s le nom et la description", "marking.note.invalid definition.description missing closing bracket": "Il vous manque peut-\u00eatre une parenth\u00e8se fermante", "marking.note.empty expression": "La note <code>{{name}}</code> est vide.", "marking.script.error parsing notes": "Erreur lors de l'analyse du script de marquage: {{- message}}", "part.feedback out of date": "Cette r\u00e9action est bas\u00e9e sur votre derni\u00e8re r\u00e9ponse soumise. Soumettez votre r\u00e9ponse modifi\u00e9e pour obtenir une nouvelle r\u00e9action.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Vous avez choisi une mauvaise r\u00e9ponse.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Erreur lors du marquage de {{name}}\u00a0: {{-message}}", "part.custom.empty setting": "Aucune valeur donn\u00e9e.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Non not\u00e9", "question.score feedback.partially answered": "R\u00e9ponse incompl\u00e8te", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Note: {{scoreString}}", "question.score feedback.score total actual": "Note: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Erreur dans la d\u00e9finition de la variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Impossible de charger l'extension <code>{{name}}</code>.", "control.toggle navigation menu": "Basculer sur le menu de navigation", "part.input title": "R\u00e9ponse pour la partie {{name}}", "part.correct answer title": "R\u00e9ponse attendue pour la partie {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Retour au menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} dans <code>{{expression}}</code>", "jme.variables.empty name": "Une variable de la question n'a pas de nom.", "jme.calculus.unknown derivative": "Impossible de diff\u00e9rencier <code>{{tree}}</code>", "math.order complex numbers": "Impossible d'ordonner des nombres complexes", "menu.choose a question": "Choisissez une question.", "part.choose next part.answered": "Que souhaitez-vous faire \u00e0 pr\u00e9sent ?", "part.choose next part.unanswered": "Sinon, vous pourriez : ", "part.choose next part.will be locked": "(Cette partie sera verrouill\u00e9e)", "part.reached dead end": "Il n'y a rien de plus \u00e0 faire ici.", "part.next part.penalty amount": "(perdre {{count}}\u00a0$t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Votre r\u00e9ponse n'est pas valide. Il faut saisir un nombre entier, un nombre d\u00e9cimal ou une fraction.", "question": "Question", "question.progress": "Avancement :", "question.score feedback.unattempted": "Non tent\u00e9", "question.score feedback.attempted": "Tent\u00e9", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectifs", "question.penalties": "P\u00e9nalit\u00e9s", "question.back to previous part": "Revenir \u00e0 la partie pr\u00e9c\u00e9dente", "end.print": "Imprimez votre relev\u00e9 de notes d'examen", "math.shuffle_together.lists not all the same length": "Toutes les listes n'ont pas la m\u00eame longueur.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Taille", "matrix input.rows": "Lignes", "matrix input.columns": "Colonnes", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Couleur de l'arri\u00e8re-plan", "modal.style.text colour": "Couleur du texte", "modal.style.text size": "Taille du texte", "modal.style.explanation": "Utilisez ces bouton pour changer l'apparence du test.", "modal.style.reset to defaults": "Remettre aux valeurs par d\u00e9faut.", "modal.style.text size preview": "La plupart des textes seront de cette taille.", "control.style options": "Options d'affichage", "part.marking.partially correct": "Votre r\u00e9ponse est en partie juste.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Aller au contenu", "result.learning objective": "Objectif d'apprentissage", "jme.interpreted as": "interpr\u00e9t\u00e9 comme", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "Il doit manquer une parenth\u00e8se fermante ", "jme.script.note.empty expression": "La note <code>{{name}}</code> est vide.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Ligne {{row}}, colonne {{column}}", "control.move to next question": "Passer \u00e0 la question suivante", "diagnostic.use retry": "Utilisez une nouvelle tentative et essayez \u00e0 nouveau ce sujet.", "diagnostic.move to next topic": "Passez au sujet suivant.", "diagnostic.next step question": "Que voulez-vous faire ensuite ?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "Il vous reste 1\u00a0essai", "diagnostic.retries left": "Il vous reste {{retries}} essais.", "diagnostic.percentage completed": "Vous avez termin\u00e9 <strong>{{percentage}}\u00a0%</strong> de l'\u00e9preuve.", "diagnostic.test is over": "L'\u00e9preuve est termin\u00e9e.", "diagnostic.passed all lo": "Vous avez r\u00e9ussi tous les objectifs d'apprentissage.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Passez \u00e0 la question suivante du sujet.", "diagnostic.complete": "Termin\u00e9 !", "diagnostic.studying topic": "\u00c9tudier {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Votre r\u00e9ponse est en cours d'\u00e9valuation. Patientez s'il vous pla\u00eet.", "diagnostic.end test": "Terminez l'\u00e9preuve.", "page.no stylesheet loaded": "Le fichier de feuille de style de la page n'a pas \u00e9t\u00e9 charg\u00e9.", "modal.confirm": "Confirmer", "modal.alert": "Alerte", "suspend.resumed header": "La tentative a repris", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Faites un choix", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Nombre de feuilles \u00e0 g\u00e9n\u00e9rer", "worksheet.starting with id": "Commen\u00e7ant par ID", "worksheet.show exam id": "Afficher l'ID de la feuille\u00a0?", "worksheet.page break between questions": "Sauts de page entre les questions ?", "worksheet.page margins": "Marges de la page (mm)", "worksheet.text size": "Taille du texte (pt)", "worksheet.generate": "G\u00e9n\u00e9rer", "worksheet.generating exams": "G\u00e9n\u00e9ration des feuilles", "worksheet.sheet id": "Identifiant de la feuille\u00a0ID:", "worksheet.print single": "Imprimer cette feuille", "worksheet.print several": "Imprimez ces feuilles", "worksheet.answer sheets": "Feuilles-r\u00e9ponses", "worksheet.question sheets": "Feuilles-questions", "worksheet.reconfigure": "G\u00e9n\u00e9rer des feuilles diff\u00e9rentes", "worksheet.show sheet": "Pr\u00e9visualisez la feuille avec ID\u00a0:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "vi-vn": {"translation": {"page.loading": "\u0110ang t\u1ea3i...", "page.saving": "<p>\u0110ang l\u01b0u.</p>\n<p>Vi\u1ec7c n\u00e0y c\u00f3 th\u1ec3 m\u1ea5t v\u00e0i gi\u00e2y.</p>", "mathjax.math processing error": "\"{{-message}}\" khi hi\u1ec3n th\u1ecb c\u00f4ng th\u1ee9c to\u00e1n <code>{{expression}}</code>", "die.numbas failed": "Numbas b\u1ecb s\u1eadp", "die.sorry": "Xin l\u1ed7i, Numbas \u0111\u00e3 g\u1eb7p ph\u1ea3i m\u1ed9t l\u1ed7i n\u00ean n\u00f3 kh\u00f4ng th\u1ec3 ti\u1ebfp t\u1ee5c. L\u1ed7i n\u00e0y \u0111\u01b0\u1ee3c m\u00f4 t\u1ea3 nh\u01b0 d\u01b0\u1edbi \u0111\u00e2y.", "die.error": "L\u1ed7i", "modal.ok": "OK", "modal.cancel": "H\u1ee7y b\u1ecf", "exam.exam name": "T\u00ean b\u00e0i ki\u1ec3m tra:", "exam.random seed": "ID c\u1ee7a phi\u00ean:", "exam.student name": "T\u00ean h\u1ecdc vi\u00ean:", "exam.number of questions": "S\u1ed1 l\u01b0\u1ee3ng c\u00e2u h\u1ecfi:", "exam.marks available": "\u0110i\u1ec3m thi c\u00f3 s\u1eb5n:", "exam.pass percentage": "T\u1ec9 l\u1ec7 \u0111\u1ed7 (%):", "exam.time allowed": "Th\u1eddi gian cho ph\u00e9p:", "exam.passed": "\u0110\u1eadu", "exam.failed": "Tr\u01b0\u1ee3t", "exam.review header": "Xem l\u1ea1i: ", "frontpage.start": "B\u1eaft \u0111\u1ea7u", "suspend.paused header": "T\u1ea1m d\u1eebng", "suspend.exam suspended": "B\u00e0i thi b\u1ecb t\u1ea1m ng\u01b0ng. Nh\u1ea5n <em>Ti\u1ebfp t\u1ee5c</em> \u0111\u1ec3 ti\u1ebfp t\u1ee5c.", "suspend.you can resume": "B\u1ea1n s\u1ebd c\u00f3 th\u1ec3 ti\u1ebfp t\u1ee5c phi\u00ean n\u00e0y khi b\u1ea1n ti\u1ebfp t\u1ee5c ho\u1ea1t \u0111\u1ed9ng n\u00e0y v\u00e0o l\u1ea7n t\u1edbi.", "suspend.resume": "Ti\u1ebfp t\u1ee5c", "result.exit": "Tho\u00e1t kh\u1ecfi b\u00e0i ki\u1ec3m tra", "result.print": "In b\u1ea3n t\u00f3m t\u1eaft k\u1ebft qu\u1ea3 n\u00e0y", "result.exam summary": "T\u00f3m t\u1eaft b\u00e0i ki\u1ec3m tra", "result.performance summary": "T\u00f3m t\u1eaft th\u00e0nh t\u00edch", "result.exam start": "Th\u1eddi gian b\u1eaft \u0111\u1ea7u:", "result.exam stop": "Th\u1eddi gian k\u1ebft th\u00fac:", "result.time spent": "Th\u1eddi gian l\u00e0m b\u00e0i:", "result.questions attempted": "C\u00e2u h\u1ecfi \u0111\u00e3 l\u00e0m:", "result.score": "\u0110i\u1ec3m:", "result.result": "K\u1ebft qu\u1ea3:", "result.question number": "C\u00e2u h\u1ecfi s\u1ed1", "result.question score": "\u0110i\u1ec3m", "result.question review title": "Xem l\u1ea1i c\u00e2u h\u1ecfi n\u00e0y", "result.click a question to review": "B\u1ea5m v\u00e0o c\u00e2u h\u1ecfi \u0111\u1ec3 xem c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111\u01b0\u1ee3c ch\u1ea5m nh\u01b0 th\u1ebf n\u00e0o v\u00e0 l\u1eddi gi\u1ea3i \u0111\u1ea7y \u0111\u1ee7 (n\u1ebfu c\u00f3).", "end.exam has finished": "B\u00e0i ki\u1ec3m tra \u0111\u00e3 k\u1ebft th\u00fac. B\u00e2y gi\u1edd b\u1ea1n c\u00f3 th\u1ec3 \u0111\u00f3ng c\u1eeda s\u1ed5 n\u00e0y.", "control.confirm leave": "B\u1ea1n ch\u01b0a ho\u00e0n th\u00e0nh b\u00e0i ki\u1ec3m tra.", "control.not all questions answered": "B\u1ea1n ch\u01b0a ho\u00e0n th\u00e0nh m\u1ecdi c\u00e2u h\u1ecfi trong b\u00e0i ki\u1ec3m tra n\u00e0y.", "control.not all questions submitted": "B\u1ea1n \u0111\u00e3 thay \u0111\u1ed5i m\u1ed9t ho\u1eb7c nhi\u1ec1u c\u00e2u tr\u1ea3 l\u1eddi nh\u01b0ng kh\u00f4ng g\u1eedi ch\u00fang. Vui l\u00f2ng ki\u1ec3m tra t\u1eebng c\u00e2u h\u1ecfi \u0111\u00e3 \u0111\u01b0\u1ee3c g\u1eedi.", "control.confirm end": "B\u1ea1n c\u00f3 ch\u1eafc ch\u1eafn mu\u1ed1n k\u1ebft th\u00fac b\u00e0i ki\u1ec3m tra? Sau khi b\u1ea1n k\u1ebft th\u00fac b\u00e0i ki\u1ec3m tra, b\u1ea1n s\u1ebd kh\u00f4ng th\u1ec3 thay \u0111\u1ed5i b\u1ea5t k\u1ef3 c\u00e2u tr\u1ea3 l\u1eddi n\u00e0o c\u1ee7a m\u00ecnh.", "control.confirm regen": "B\u1ea1n c\u00f3 mu\u1ed1n th\u1eed m\u1ed9t phi\u00ean b\u1ea3n m\u1edbi (ng\u1eabu nhi\u00ean) c\u1ee7a c\u00e2u h\u1ecfi n\u00e0y? N\u1ebfu b\u1ea1n b\u1ea5m v\u00e0o OK, t\u1ea5t c\u1ea3 c\u00e1c c\u00e2u tr\u1ea3 l\u1eddi v\u00e0 \u0111i\u1ec3m cho c\u00e2u h\u1ecfi hi\u1ec7n t\u1ea1i c\u1ee7a b\u1ea1n s\u1ebd b\u1ecb m\u1ea5t.", "control.confirm reveal": "B\u1ea1n c\u00f3 mu\u1ed1n xem c\u00e2u tr\u1ea3 l\u1eddi cho c\u00e2u h\u1ecfi n\u00e0y? T\u1ea5t c\u1ea3 s\u1ed1 \u0111i\u1ec3m b\u1ea1n nh\u1eadn \u0111\u01b0\u1ee3c cho \u0111\u1ebfn nay s\u1ebd b\u1ecb kh\u00f3a v\u00e0 b\u1ea1n s\u1ebd kh\u00f4ng th\u1ec3 tr\u1ea3 l\u1eddi c\u00e2u h\u1ecfi n\u00e0y v\u1ec1 sau.", "control.proceed anyway": "D\u00f9 sao v\u1eabn ti\u1ebfn h\u00e0nh?", "control.regen": "H\u00e3y th\u1eed m\u1ed9t c\u00e2u h\u1ecfi kh\u00e1c nh\u01b0 c\u00e2u n\u00e0y", "control.submit answer": "G\u1eedi c\u00e2u tr\u1ea3 l\u1eddi", "control.submit all parts": "G\u1eedi t\u1ea5t c\u1ea3 c\u00e1c ph\u1ea7n", "control.submit again": "G\u1eedi l\u1ea1i", "control.submit": "G\u1eedi", "control.previous": "Tr\u01b0\u1edbc", "control.next": "Sau", "control.advice": "G\u1ee3i \u00fd", "control.reveal": "Xem c\u00e2u tr\u1ea3 l\u1eddi", "control.total": "T\u1ed5ng \u0111i\u1ec3m", "control.pause": "T\u1ea1m d\u1eebng", "control.end exam": "K\u1ebft th\u00fac b\u00e0i ki\u1ec3m tra", "control.back to results": "Quay tr\u1edf l\u1ea1i k\u1ebft qu\u1ea3", "display.part.jme.error making maths": "L\u1ed7i hi\u1ec3n th\u1ecb to\u00e1n h\u1ecdc", "exam.xml.bad root": "Ph\u1ea7n t\u1eed g\u1ed1c c\u1ee7a b\u00e0i ki\u1ec3m tra XML ph\u1ea3i l\u00e0 'exam'", "exam.changeQuestion.no questions": "B\u00e0i ki\u1ec3m tra n\u00e0y kh\u00f4ng c\u00f3 c\u00e2u h\u1ecfi! Ki\u1ec3m tra t\u1ec7p .exam \u0111\u1ec3 xem l\u1ed7i.", "feedback.you were awarded": "B\u1ea1n \u0111\u01b0\u1ee3c <strong>{{count,niceNumber}}</strong> $t(\u0111i\u1ec3m).", "feedback.taken away": "B\u1ea1n b\u1ecb tr\u1eeb <strong>{{count,niceNumber}}</strong> $t(\u0111i\u1ec3m).", "jme.tokenise.invalid": "Bi\u1ec3u th\u1ee9c kh\u00f4ng h\u1ee3p l\u1ec7: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Kh\u00f4ng \u0111\u1ee7 \u0111\u1ed1i s\u1ed1 cho ph\u00e9p to\u00e1n <code>{{op}}</code>", "jme.shunt.no left bracket in function": "Kh\u00f4ng c\u00f3 d\u1ea5u ngo\u1eb7c tr\u00e1i trong h\u00e0m \u1ee9ng d\u1ee5ng ho\u1eb7c b\u1ed9 d\u1eef li\u1ec7u", "jme.shunt.no left square bracket": "Kh\u00f4ng c\u00f3 d\u1ea5u ngo\u1eb7c tr\u00e1i", "jme.shunt.no left bracket": "Kh\u00f4ng c\u00f3 d\u1ea5u ngo\u1eb7c tr\u00e1i", "jme.shunt.no right bracket": "Kh\u00f4ng c\u00f3 d\u1ea5u ngo\u1eb7c ph\u1ea3i", "jme.shunt.no right square bracket": "Kh\u00f4ng c\u00f3 d\u1ea5u ngo\u1eb7c vu\u00f4ng ph\u1ea3i \u1edf cu\u1ed1i danh s\u00e1ch", "jme.shunt.missing operator": "Bi\u1ec3u th\u1ee9c kh\u00f4ng th\u1ec3 t\u00ednh to\u00e1n -- thi\u1ebfu m\u1ed9t ph\u00e9p to\u00e1n.", "jme.typecheck.function maybe implicit multiplication": "H\u00e0m <code>{{name}}</code> kh\u00f4ng \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a. \n\u00dd b\u1ea1n l\u00e0 <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "H\u00e0m <code>{{op}}</code> kh\u00f4ng \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a. C\u00f3 ph\u1ea3i <code>{{op}}</code> l\u00e0 bi\u1ebfn kh\u00f4ng, v\u00e0 \u00fd b\u1ea1n l\u00e0 <code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Ph\u00e9p to\u00e1n '{{op}}' ch\u01b0a \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a", "jme.typecheck.no right type definition": "Kh\u00f4ng t\u00ecm th\u1ea5y \u0111\u1ecbnh ngh\u0129a '{{op}}' \u0111\u00fang ki\u1ec3u.", "jme.typecheck.no right type unbound name": "Bi\u1ebfn <code>{{name}}</code> kh\u00f4ng \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a.", "jme.typecheck.map not on enumerable": "<code>map</code> ph\u00e9p to\u00e1n ph\u1ea3i \u0111\u01b0\u1ee3c ho\u1ea1t \u0111\u1ed9ng tr\u00ean m\u1ed9t danh s\u00e1ch ho\u1eb7c m\u1ed9t mi\u1ec1n, kh\u00f4ng tr\u00ean {{type}}", "jme.evaluate.undefined variable": "Bi\u1ebfn {{name}} kh\u00f4ng \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a", "jme.thtml.not html": "\u0110\u00e3 chuy\u1ec3n m\u1ed9t gi\u00e1 tr\u1ecb kh\u00f4ng ph\u1ea3i HTML v\u00e0o h\u00e0m t\u1ea1o THTML.", "jme.func.switch.no default case": "Kh\u00f4ng c\u00f3 tr\u01b0\u1eddng h\u1ee3p m\u1eb7c \u0111\u1ecbnh cho c\u00e2u l\u1ec7nh Switch", "jme.func.listval.invalid index": "Index c\u1ee7a danh s\u00e1ch kh\u00f4ng h\u1ee3p l\u1ec7 {{index}} trong k\u00edch th\u01b0\u1edbc {{size}}", "jme.func.listval.not a list": "\u0110\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng \u0111\u01b0\u1ee3c \u0111\u0103ng k\u00fd", "jme.func.matrix.invalid row type": "Kh\u00f4ng th\u1ec3 t\u1ea1o m\u1ed9t ma tr\u1eadn t\u1eeb nh\u1eefng h\u00e0ng thu\u1ed9c ki\u1ec3u {{type}}", "jme.func.except.continuous range": "Kh\u00f4ng th\u1ec3 s\u1eed d\u1ee5ng to\u00e1n t\u1eed 'except' trong ph\u1ea1m vi li\u00ean t\u1ee5c.", "jme.matrix.reports bad size": "K\u00edch th\u01b0\u1edbc c\u1ee7a ma tr\u1eadn kh\u00f4ng ch\u00ednh x\u00e1c - c\u00f3 m\u1ed9t l\u1ed7i trong h\u00e0m x\u00e2y d\u1ef1ng", "jme.texsubvars.no right bracket": "Kh\u00f4ng kh\u1edbp <code>]</code> trong c\u00e1c \u0111\u1ed1i s\u1ed1 {{op}}.", "jme.texsubvars.missing parameter": "Thi\u1ebfu tham s\u1ed1 trong {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Kh\u00f4ng kh\u1edbp <code>]</code> trong {{op}}", "jme.user javascript.error": "L\u1ed7i trong h\u00e0m javascript do ng\u01b0\u1eddi d\u00f9ng \u0111\u1ecbnh ngh\u0129a <code> {{name}} </code>: {{-message}}", "jme.variables.error making function": "L\u1ed7i t\u1ea1o h\u00e0m <code> {{name}} </code>: {{-message}}", "jme.variables.syntax error in function definition": "L\u1ed7i c\u00fa ph\u00e1p trong \u0111\u1ecbnh ngh\u0129a h\u00e0m", "jme.variables.variable not defined": "Bi\u1ebfn <code>{{name}}</code> ch\u01b0a \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a.", "jme.variables.empty definition": "\u0110\u1ecbnh ngh\u0129a c\u1ee7a bi\u1ebfn <code>{{name}}</code> c\u00f2n tr\u1ed1ng.", "jme.variables.circular reference": "Tham chi\u1ebfu v\u00f2ng tr\u00f2n c\u00e1c bi\u1ebfn theo \u0111\u1ecbnh ngh\u0129a c\u1ee7a <code> {{name}} </code>", "jme.variables.error computing dependency": "L\u1ed7i t\u00ednh to\u00e1n bi\u1ebfn tham chi\u1ebfu <code> {{name}} </code>", "jme.variables.error evaluating variable": "L\u1ed7i t\u00ednh gi\u00e1 tr\u1ecb bi\u1ebfn {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "M\u1ed9t t\u1eadp h\u1ee3p c\u00e1c c\u00e2u h\u1ecfi h\u1ee3p l\u1ec7 kh\u00f4ng \u0111\u01b0\u1ee3c t\u1ea1o ra k\u1ecbp th\u1eddi.", "jme.display.unknown token type": "Kh\u00f4ng th\u1ec3 x\u00e1c \u0111\u1ecbnh lo\u1ea1i m\u00e3 th\u00f4ng b\u00e1o {{type}}", "jme.display.collectRuleset.no sets": "Kh\u00f4ng c\u00f3 t\u1eadp n\u00e0o \u0111\u01b0\u1ee3c \u0111\u01b0a ra \u0111\u1ec3 collectRuleset!", "jme.display.collectRuleset.set not defined": "B\u1ed9 quy t\u1eafc {{name}} ch\u01b0a \u0111\u01b0\u1ee3c x\u00e1c \u0111\u1ecbnh", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree ph\u1ea3i \u0111\u01b0\u1ee3c cung c\u1ea5p Scope", "math.precround.complex": "Kh\u00f4ng th\u1ec3 l\u00e0m tr\u00f2n s\u1ed1 ph\u1ee9c", "math.siground.complex": "Kh\u00f4ng th\u1ec3 l\u00e0m tr\u00f2n m\u1ed9t s\u1ed1 ph\u1ee9c", "math.combinations.complex": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 t\u1ed5 h\u1ee3p c\u1ee7a c\u00e1c s\u1ed1 ph\u1ee9c", "math.permutations.complex": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 ho\u00e1n v\u1ecb c\u1ee7a s\u1ed1 ph\u1ee9c", "math.gcf.complex": "Kh\u00f4ng th\u1ec3 t\u00ecm UCLN c\u1ee7a s\u1ed1 ph\u1ee9c", "math.lcm.complex": "Kh\u00f4ng th\u1ec3 t\u00ecm b\u1ed9i chung nh\u1ecf nh\u1ea5t c\u1ee7a c\u00e1c s\u1ed1 ph\u1ee9c", "math.lt.order complex numbers": "Kh\u00f4ng th\u1ec3 s\u1eafp x\u1ebfp th\u1ee9 t\u1ef1 c\u00e1c s\u1ed1 ph\u1ee9c", "math.choose.empty selection": "L\u1ef1a ch\u1ecdn tr\u1ed1ng cho h\u00e0m ng\u1eabu nhi\u00ean", "matrixmath.abs.non-square": "Kh\u00f4ng th\u1ec3 t\u00ednh \u0111\u1ecbnh th\u1ee9c c\u1ee7a m\u1ed9t ma tr\u1eadn kh\u00f4ng vu\u00f4ng.", "matrixmath.abs.too big": "Xin l\u1ed7i, kh\u00f4ng th\u1ec3 t\u00ednh \u0111\u1ecbnh th\u1ee9c c\u1ee7a m\u1ed9t ma tr\u1eadn c\u00f3 k\u00edch th\u01b0\u1edbc l\u1edbn h\u01a1n 3x3.", "matrixmath.mul.different sizes": "Kh\u00f4ng th\u1ec3 nh\u00e2n c\u00e1c ma tr\u1eadn c\u00f3 k\u00edch th\u01b0\u1edbc kh\u00e1c nhau.", "vectormath.cross.not 3d": "Ch\u1ec9 c\u00f3 th\u1ec3 l\u1ea5y t\u00edch ch\u00e9o c\u1ee7a c\u00e1c v\u00e9c-t\u01a1 3 chi\u1ec1u.", "vectormath.dot.matrix too big": "Kh\u00f4ng th\u1ec3 t\u00ednh t\u00edch v\u00f4 h\u01b0\u1edbng c\u1ee7a ma tr\u1eadn kh\u00f4ng ph\u1ea3i l\u00e0 $ 1 \\times N $ ho\u1eb7c $ N \\times 1 $.", "vectormath.cross.matrix too big": "Kh\u00f4ng th\u1ec3 t\u00ednh t\u00edch ch\u00e9o c\u1ee7a ma tr\u1eadn kh\u00f4ng ph\u1ea3i l\u00e0 $ 1 \\times N $ ho\u1eb7c $ N \\times 1 $.", "part.with steps answer prompt": "Tr\u1ea3 l\u1eddi:", "part.script.error": "L\u1ed7i trong ph\u1ea7n {{path}} t\u1eadp l\u1ec7nh t\u00f9y ch\u1ec9nh {{script}}: {{-message}}", "part.marking.steps no matter": "V\u00ec b\u1ea1n \u0111\u00e3 nh\u1eadn \u0111\u01b0\u1ee3c \u0111\u1ee7 \u0111i\u1ec3m cho ph\u1ea7n n\u00e0y, c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n cho c\u00e1c b\u01b0\u1edbc kh\u00f4ng \u0111\u01b0\u1ee3c t\u00ednh.", "part.marking.revealed steps no penalty": "You revealed the steps.", "part.marking.used variable replacements": "Ph\u1ea7n \u0111\u01b0\u1ee3c \u0111\u00e1nh d\u1ea5u s\u1eed d\u1ee5ng c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a ph\u1ea7n tr\u01b0\u1edbc.", "part.marking.variable replacement part not answered": "B\u1ea1n ph\u1ea3i tr\u1ea3 l\u1eddi {{part}} tr\u01b0\u1edbc.", "part.marking.resubmit because of variable replacement": "Vi\u1ec7c ch\u1ea5m \u0111i\u1ec3m ph\u1ea7n n\u00e0y ph\u1ee5 thu\u1ed9c v\u00e0o c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n cho c\u00e1c ph\u1ea7n kh\u00e1c m\u00e0 b\u1ea1n \u0111\u00e3 thay \u0111\u1ed5i. G\u1eedi ph\u1ea7n n\u00e0y m\u1ed9t l\u1ea7n n\u1eefa \u0111\u1ec3 c\u1eadp nh\u1eadt \u0111i\u1ec3m s\u1ed1 c\u1ee7a b\u1ea1n.", "part.marking.not submitted": "Kh\u00f4ng c\u00e2u tr\u1ea3 l\u1eddi n\u00e0o \u0111\u01b0\u1ee3c g\u1eedi.", "part.marking.did not answer": "B\u1ea1n ch\u01b0a tr\u1ea3 l\u1eddi c\u00e2u h\u1ecfi n\u00e0y.", "part.marking.nothing entered": "B\u1ea1n ch\u01b0a nh\u1eadp c\u00e2u tr\u1ea3 l\u1eddi.", "part.marking.incorrect": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ch\u01b0a \u0111\u00fang.", "part.marking.correct": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111\u00fang.", "part.marking.uncaught error": "L\u1ed7i khi \u0111\u00e1nh d\u1ea5u: {{-message}}", "part.marking.no result": "This part could not be marked.", "part.correct answer": "C\u00e2u tr\u1ea3 l\u1eddi d\u1ef1 ki\u1ebfn:", "part.missing type attribute": "{{part}}: Thi\u1ebfu thu\u1ed9c t\u00ednh ki\u1ec3u", "part.unknown type": "{{part}}: Kh\u00f4ng nh\u1eadn d\u1ea1ng \u0111\u01b0\u1ee3c ki\u1ec3u {{type}}", "part.setting not present": "Thu\u1ed9c t\u00ednh '{{property}}' ch\u01b0a \u0111\u01b0\u1ee3c \u0111\u1eb7t", "part.jme.answer missing": "C\u00e2u tr\u1ea3 l\u1eddi \u0111\u00fang b\u1ecb thi\u1ebfu", "part.jme.answer too long": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n qu\u00e1 d\u00e0i.", "part.jme.answer too short": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n qu\u00e1 ng\u1eafn.", "part.jme.answer invalid": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng ph\u1ea3i l\u00e0 bi\u1ec3u th\u1ee9c to\u00e1n h\u1ecdc h\u1ee3p l\u1ec7. <br/> {{-message}}.", "part.jme.marking.correct": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n c\u00f3 gi\u00e1 tr\u1ecb \u0111\u00fang.", "part.jme.must-have bits": "<span class = \"monospace\"> {{string}} </span>", "part.jme.must-have one": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ph\u1ea3i c\u00f3: {{strings}}", "part.jme.must-have several": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ph\u1ea3i ch\u1ee9a t\u1ea5t c\u1ea3: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng \u0111\u01b0\u1ee3c ch\u1ee9a: {{strings}}", "part.jme.not-allowed several": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng \u0111\u01b0\u1ee3c ch\u1ee9a b\u1ea5t k\u1ef3 t\u1eeb n\u00e0o trong: {{strings}}", "part.jme.unexpected variable name": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111\u00e3 \u0111\u01b0\u1ee3c di\u1ec5n gi\u1ea3i \u0111\u1ec3 s\u1eed d\u1ee5ng t\u00ean bi\u1ebfn kh\u00f4ng mong mu\u1ed1n <code> {{name}} </code>.", "part.jme.unexpected variable name suggestion": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111\u00e3 \u0111\u01b0\u1ee3c di\u1ec5n gi\u1ea3i \u0111\u1ec3 s\u1eed d\u1ee5ng t\u00ean bi\u1ebfn kh\u00f4ng mong mu\u1ed1n <code> {{name}} </code>. \u00dd c\u1ee7a b\u1ea1n l\u00e0 <code> {{suggest}} </code>?", "part.patternmatch.display answer missing": "Hi\u1ec3n th\u1ecb c\u00e2u tr\u1ea3 l\u1eddi thi\u1ebfu.", "part.patternmatch.correct except case": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n l\u00e0 ch\u00ednh x\u00e1c, ngo\u1ea1i tr\u1eeb tr\u01b0\u1eddng h\u1ee3p.", "part.numberentry.correct except decimal": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n n\u1eb1m trong ph\u1ea1m vi cho ph\u00e9p, nh\u01b0ng s\u1ed1 th\u1eadp ph\u00e2n kh\u00f4ng \u0111\u01b0\u1ee3c ch\u1ea5p nh\u1eadn.", "part.numberentry.correct except fraction": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n n\u1eb1m trong ph\u1ea1m vi cho ph\u00e9p, nh\u01b0ng ph\u00e2n s\u1ed1 kh\u00f4ng \u0111\u01b0\u1ee3c ch\u1ea5p nh\u1eadn.", "part.numberentry.answer invalid": "B\u1ea1n ch\u01b0a nh\u1eadp m\u1ed9t s\u1ed1 h\u1ee3p l\u1ec7.", "part.numberentry.answer not integer": "C\u00e2u tr\u1ea3 l\u1eddi kh\u00f4ng h\u1ee3p l\u1ec7. B\u1ea1n ph\u1ea3i nh\u1eadp m\u1ed9t s\u1ed1 nguy\u00ean, kh\u00f4ng ph\u1ea3i m\u1ed9t s\u1ed1 th\u1eadp ph\u00e2n.", "part.numberentry.answer not integer or decimal": "C\u00e2u tr\u1ea3 l\u1eddi kh\u00f4ng h\u1ee3p l\u1ec7. B\u1ea1n ph\u1ea3i nh\u1eadp m\u1ed9t s\u1ed1 nguy\u00ean ho\u1eb7c m\u1ed9t s\u1ed1 th\u1eadp ph\u00e2n.", "part.numberentry.zero sig fig": "Ph\u1ea7n n\u00e0y \u0111\u01b0\u1ee3c thi\u1ebft l\u1eadp \u0111\u1ec3 l\u00e0m tr\u00f2n c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a h\u1ecdc sinh th\u00e0nh c\u00e1c s\u1ed1 nguy\u00ean.", "part.mcq.options def not a list": "Bi\u1ec3u th\u1ee9c x\u00e1c \u0111\u1ecbnh {{property}} kh\u00f4ng ph\u1ea3i l\u00e0 danh s\u00e1ch.", "part.mcq.marking matrix string empty": "Bi\u1ec3u th\u1ee9c ma tr\u1eadn \u0111\u00e1nh d\u1ea5u t\u00f9y ch\u1ec9nh c\u00f2n tr\u1ed1ng.", "part.mcq.choices missing": "\u0110\u1ecbnh ngh\u0129a c\u1ee7a s\u1ef1 l\u1ef1a ch\u1ecdn \u0111ang thi\u1ebfu", "part.mcq.matrix not a number": "Ph\u1ea7n {{part}} \u00f4 ma tr\u1eadn \u0111\u00e1nh d\u1ea5u ({{row}}, {{col}}) kh\u00f4ng c\u00f3 k\u1ebft qu\u1ea3 l\u00e0 m\u1ed9t s\u1ed1", "part.mcq.wrong number of choices": "B\u1ea1n \u0111\u00e3 ch\u1ecdn sai s\u1ed1 l\u01b0\u1ee3ng c\u00e2u tr\u1ea3 l\u1eddi.", "part.mcq.no choices selected": "Kh\u00f4ng c\u00f3 c\u00e2u tr\u1ea3 l\u1eddi n\u00e0o \u0111\u01b0\u1ee3c ch\u1ecdn.", "part.mcq.matrix not a list": "Ma tr\u1eadn \u0111\u00e1nh d\u1ea5u, \u0111\u01b0\u1ee3c x\u00e1c \u0111\u1ecbnh b\u1edfi bi\u1ec3u th\u1ee9c JME, kh\u00f4ng ph\u1ea3i l\u00e0 m\u1ed9t danh s\u00e1ch nh\u01b0ng n\u00f3 n\u00ean nh\u01b0 v\u1eady.", "part.mcq.matrix wrong type": "Ph\u1ea7n t\u1eed c\u1ee7a lo\u1ea1i kh\u00f4ng h\u1ee3p l\u1ec7 '{{type}}' \u0111\u01b0\u1ee3c s\u1eed d\u1ee5ng trong ma tr\u1eadn \u0111\u00e1nh d\u1ea5u.", "part.mcq.matrix mix of numbers and lists": "Tr\u1ed9n l\u1eabn s\u1ed1 v\u00e0 danh s\u00e1ch \u0111ang \u0111\u01b0\u1ee3c s\u1eed d\u1ee5ng trong ma tr\u1eadn \u0111\u00e1nh d\u1ea5u.", "part.mcq.matrix wrong size": "Ma tr\u1eadn \u0111\u00e1nh d\u1ea5u c\u00f3 k\u00edch th\u01b0\u1edbc sai.", "part.mcq.correct choice": "B\u1ea1n \u0111\u00e3 ch\u1ecdn m\u1ed9t c\u00e2u tr\u1ea3 l\u1eddi \u0111\u00fang.", "part.matrix.answer invalid": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng h\u1ee3p l\u1ec7.", "part.matrix.invalid cell": "M\u1ed9t ho\u1eb7c nhi\u1ec1u \u00f4 trong c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n tr\u1ed1ng ho\u1eb7c kh\u00f4ng h\u1ee3p l\u1ec7.", "part.matrix.some incorrect": "M\u1ed9t ho\u1eb7c nhi\u1ec1u \u00f4 trong c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n l\u00e0 kh\u00f4ng ch\u00ednh x\u00e1c, nh\u01b0ng b\u1ea1n \u0111\u00e3 \u0111\u01b0\u1ee3c ch\u1ea5m \u0111i\u1ec3m cho ph\u1ea7n c\u00f2n l\u1ea1i.", "part.matrix.empty": "B\u1ea1n ch\u01b0a nh\u1eadp c\u00e2u tr\u1ea3 l\u1eddi.", "part.matrix.empty cell": "M\u1ed9t ho\u1eb7c nhi\u1ec1u \u00f4 trong c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n tr\u1ed1ng.", "part.matrix.size mismatch": "T\u00e1c gi\u1ea3 c\u00e2u h\u1ecfi \u0111\u00e3 kh\u00f4ng cho ph\u00e9p h\u1ecdc sinh quy\u1ebft \u0111\u1ecbnh k\u00edch th\u01b0\u1edbc c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a h\u1ecd, nh\u01b0ng c\u00e2u tr\u1ea3 l\u1eddi \u0111\u00fang l\u00e0 {{correct_dimensions}} trong khi \u0111\u1ea7u v\u00e0o c\u00e2u tr\u1ea3 l\u1eddi l\u00e0 {{input_dimensions}}", "part.gapfill.feedback header": "<strong> {{name}} </strong>", "part.extension.not implemented": "M\u1ed9t ph\u1ea7n ch\u01b0a \u0111\u01b0\u1ee3c tri\u1ec3n khai theo c\u00e1ch <code> {{name}} </code>.", "question.loaded name mismatch": "Kh\u00f4ng th\u1ec3 ti\u1ebfp t\u1ee5c n\u1ed7 l\u1ef1c n\u00e0y - th\u01b0 vi\u1ec7n \u0111\u00e3 thay \u0111\u1ed5i k\u1ec3 t\u1eeb phi\u00ean tr\u01b0\u1edbc.", "question.error": "C\u00e2u h\u1ecfi {{number}}: {{-message}}", "question.preamble.error": "L\u1ed7i trong preamble: {{-message}}", "question.preamble.syntax error": "L\u1ed7i c\u00fa ph\u00e1p trong preamble", "question.unsupported part type": "Ki\u1ec3u kh\u00f4ng \u0111\u01b0\u1ee3c h\u1ed7 tr\u1ee3", "question.header": "C\u00e2u h\u1ecfi {{number}}", "question.submit part": "G\u1eedi m\u1ed9t ph\u1ea7n", "question.show steps": "Xem c\u00e1c b\u01b0\u1edbc", "question.show steps penalty": "B\u1ea1n s\u1ebd m\u1ea5t <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "\u0110i\u1ec3m c\u1ee7a b\u1ea1n s\u1ebd kh\u00f4ng b\u1ecb \u1ea3nh h\u01b0\u1edfng.", "question.show steps already penalised": "B\u1ea1n \u0111\u00e3 hi\u1ec3n th\u1ecb c\u00e1c b\u01b0\u1edbc. B\u1ea1n c\u00f3 th\u1ec3 hi\u1ec3n th\u1ecb l\u1ea1i ch\u00fang m\u00e0 kh\u00f4ng b\u1ecb ph\u1ea1t th\u00eam.", "question.hide steps": "\u1ea8n c\u00e1c b\u01b0\u1edbc", "question.hide steps no penalty": "\u0110i\u1ec3m c\u1ee7a b\u1ea1n s\u1ebd kh\u00f4ng b\u1ecb \u1ea3nh h\u01b0\u1edfng.", "question.advice": "G\u1ee3i \u00fd", "question.no such part": "Kh\u00f4ng t\u00ecm th\u1ea5y ph\u1ea7n {{path}}", "question.can not submit": "Kh\u00f4ng th\u1ec3 g\u1eedi c\u00e2u tr\u1ea3 l\u1eddi - ki\u1ec3m tra l\u1ed7i.", "question.answer submitted": "C\u00e2u tr\u1ea3 l\u1eddi \u0111\u00e3 \u0111\u01b0\u1ee3c g\u1eedi", "question.score feedback.show": "Xem ph\u1ea3n h\u1ed3i", "question.score feedback.hide": "\u1ea8n ph\u1ea3n h\u1ed3i", "question.score feedback.answered total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Answered.", "question.score feedback.answered actual": "Score: {{scoreString}}", "question.score feedback.answered": "\u0110\u00e3 tr\u1ea3 l\u1eddi", "question.score feedback.unanswered": "Ch\u01b0a tr\u1ea3 l\u1eddi", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ch\u01b0a \u0111\u00fang", "question.score feedback.partial": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111\u00fang m\u1ed9t ph\u1ea7n", "question.score feedback.wrong": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ch\u01b0a \u0111\u00fang", "question.selector.unsubmitted changes": "Thay \u0111\u1ed5i ch\u01b0a \u0111\u01b0\u1ee3c g\u1eedi \u0111i.", "timing.no accumulator": "kh\u00f4ng c\u00f3 b\u1ed9 \u0111\u1ebfm th\u1eddi gian {{name}}", "timing.time remaining": "Th\u1eddi gian c\u00f2n l\u1ea1i:", "xml.could not load": "Kh\u00f4ng th\u1ec3 n\u1ea1p t\u1ec7p XML: {{-message}}", "xml.property not number": "Thu\u1ed9c t\u00ednh {{name}} ph\u1ea3i l\u00e0 m\u1ed9t s\u1ed1, nh\u01b0ng kh\u00f4ng ph\u1ea3i ({{value}}), trong n\u00fat {{element}}", "xml.property not boolean": "Thu\u1ed9c t\u00ednh {{name}} ph\u1ea3i l\u00e0 ki\u1ec3u boolean, nh\u01b0ng kh\u00f4ng ph\u1ea3i ({{value}}), trong n\u00fat {{element}}", "xml.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "scorm.error initialising": "L\u1ed7i kh\u1edfi t\u1ea1o giao th\u1ee9c SCORM: {{-message}}", "scorm.failed save": "<p> Y\u00eau c\u1ea7u l\u01b0u d\u1eef li\u1ec7u v\u00e0o m\u00e1y ch\u1ee7 kh\u00f4ng th\u00e0nh c\u00f4ng. Nh\u1ea5n <b> OK </b> \u0111\u1ec3 th\u1eed l\u1ea1i. </P>\n<p> N\u1ebfu b\u1ea1n nh\u1eadn \u0111\u01b0\u1ee3c th\u00f4ng b\u00e1o n\u00e0y nhi\u1ec1u l\u1ea7n, h\u00e3y ki\u1ec3m tra k\u1ebft n\u1ed1i internet c\u1ee7a b\u1ea1n ho\u1eb7c s\u1eed d\u1ee5ng m\u00e1y t\u00ednh kh\u00e1c. C\u00e1c c\u00e2u tr\u1ea3 l\u1eddi \u0111\u00e3 g\u1eedi tr\u01b0\u1edbc \u0111\u00f3 c\u1ee7a b\u1ea1n \u0111\u00e3 \u0111\u01b0\u1ee3c l\u01b0u th\u00e0nh c\u00f4ng v\u00e0 s\u1ebd \u0111\u01b0\u1ee3c kh\u00f4i ph\u1ee5c n\u1ebfu b\u1ea1n ti\u1ebfp t\u1ee5c phi\u00ean n\u00e0y tr\u00ean m\u1ed9t m\u00e1y t\u00ednh kh\u00e1c. </P>\n<p> N\u1ebfu th\u00f4ng b\u00e1o n\u00e0y xu\u1ea5t hi\u1ec7n li\u00ean t\u1ee5c v\u00e0 b\u1ea1n kh\u00f4ng th\u1ec3 l\u01b0u <em> b\u1ea5t k\u1ef3 c\u00e2u tr\u1ea3 l\u1eddi </em> n\u00e0o, vui l\u00f2ng li\u00ean h\u1ec7 v\u1edbi gi\u1ea3ng vi\u00ean ho\u1eb7c gi\u00e1o vi\u00ean c\u1ee7a b\u1ea1n. </p>", "scorm.no exam suspend data": "Kh\u00f4ng th\u1ec3 ti\u1ebfp t\u1ee5c: kh\u00f4ng c\u00f3 b\u00e0i thi n\u00e0o \u0111ang l\u00e0m d\u1edf.", "scorm.error loading suspend data": "L\u1ed7i khi t\u1ea3i d\u1eef li\u1ec7u t\u1ea1m ng\u01b0ng: {{-message}}", "scorm.error loading question": "L\u1ed7i khi n\u1ea1p c\u00e2u h\u1ecfi {{number}}: {{-message}}", "scorm.no question suspend data": "Kh\u00f4ng c\u00f3 c\u00e2u h\u1ecfi \u0111ang l\u00e0m d\u1edf", "scorm.error loading part": "L\u1ed7i khi t\u1ea3i ph\u1ea7n {{part}}: {{-message}}", "scorm.no part suspend data": "Kh\u00f4ng c\u00f3 ph\u1ea7n d\u1eef li\u1ec7u \u0111ang l\u00e0m d\u1edf", "util.product.non list": "\u0110\u00e3 v\u01b0\u1ee3t qua m\u1ed9t danh s\u00e1ch kh\u00f4ng ph\u1ea3i l\u00e0 <code> Numbas.util.product </code>", "mark": "\u0111\u00e1nh d\u1ea5u", "was": "\u0111\u00e3 l\u00e0", "part": "ph\u1ea7n", "gap": "l\u1ed7 h\u1ed5ng", "step": "b\u01b0\u1edbc", "jme.substituteTree.undefined variable": "Bi\u1ebfn kh\u00f4ng x\u00e1c \u0111\u1ecbnh <code>{{name}}</code>", "jme.user javascript.returned undefined": "H\u00e0m javascript do ng\u01b0\u1eddi d\u00f9ng \u0111\u1ecbnh ngh\u0129a <code> {{name}} </code> \u0111\u01b0\u1ee3c tr\u1ea3 v\u1ec1 <code> kh\u00f4ng x\u00e1c \u0111\u1ecbnh </code>.", "part.marking.steps change": "B\u1ea1n \u0111\u00e3 \u0111\u01b0\u1ee3c trao <strong> {{Count, beautifulNumber}} </strong> $ t (\u0111\u00e1nh d\u1ea5u) cho c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n cho c\u00e1c b\u01b0\u1edbc.", "part.marking.revealed steps with penalty": "You revealed the steps. The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.total score": "B\u1ea1n \u0111\u00e3 ghi <strong> {{Count, beautifulNumber}} </strong> $ t (mark) cho ph\u1ea7n n\u00e0y.", "part.numberentry.precision type.dp": "v\u1ecb tr\u00ed d\u1ea5u th\u1eadp ph\u00e2n", "part.numberentry.precision type.dp_plural": "d\u1ea5u ph\u1ea3y th\u1eadp ph\u00e2n", "part.numberentry.precision type.sigfig": "ch\u1eef s\u1ed1 c\u00f3 ngh\u0129a", "part.numberentry.precision type.sigfig_plural": "ch\u1eef s\u1ed1 c\u00f3 ngh\u0129a", "part.numberentry.give your answer to precision": "L\u00e0m tr\u00f2n k\u1ebft qu\u1ea3 \u0111\u1ebfn {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "B\u1ea1n \u0111\u00e3 th\u1ef1c hi\u1ec7n m\u1ed9t thay \u0111\u1ed5i cho c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n nh\u01b0ng ch\u01b0a g\u1eedi n\u00f3. Vui l\u00f2ng ki\u1ec3m tra c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n v\u00e0 sau \u0111\u00f3 nh\u1ea5n n\u00fat <strong> G\u1eedi c\u00e2u tr\u1ea3 l\u1eddi </strong>.", "question.unsubmitted changes_plural": "B\u1ea1n \u0111\u00e3 th\u1ef1c hi\u1ec7n thay \u0111\u1ed5i cho c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n nh\u01b0ng ch\u01b0a g\u1eedi ch\u00fang. Vui l\u00f2ng ki\u1ec3m tra c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n cho t\u1eebng ph\u1ea7n v\u00e0 sau \u0111\u00f3 nh\u1ea5n n\u00fat <strong> G\u1eedi t\u1ea5t c\u1ea3 c\u00e1c ph\u1ea7n </strong>.", "util.equality not defined for type": "So s\u00e1nh ch\u01b0a \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a cho ki\u1ec3u {{type}}", "mark_plural": "\u0111\u00e1nh d\u1ea5u", "was_plural": "\u0111\u00e3 l\u00e0", "die.script not loaded": "Numbas kh\u00f4ng th\u1ec3 b\u1eaft \u0111\u1ea7u v\u00ec t\u1ec7p <code> {{file}} </code> kh\u00f4ng \u0111\u01b0\u1ee3c t\u1ea3i. Ki\u1ec3m tra xem n\u00f3 c\u00f3 trong <code>scripts.js</code> kh\u00f4ng.", "math.combinations.n less than zero": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 t\u1ed5 h\u1ee3p: n \u0111ang nh\u1ecf h\u01a1n 0", "math.combinations.k less than zero": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 t\u1ed5 h\u1ee3p: k \u0111ang nh\u1ecf h\u01a1n 0", "math.combinations.n less than k": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 t\u1ed5 h\u1ee3p: n \u0111ang nh\u1ecf h\u01a1n k", "math.permutations.n less than zero": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 ho\u00e1n v\u1ecb: n \u0111ang nh\u1ecf h\u01a1n 0", "math.permutations.k less than zero": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 ho\u00e1n v\u1ecb: k \u0111ang nh\u1ecf h\u01a1n 0", "math.permutations.n less than k": "Kh\u00f4ng th\u1ec3 t\u00ednh s\u1ed1 ho\u00e1n v\u1ecb: n \u0111ang nh\u1ecf h\u01a1n k", "part.numberentry.give your answer to precision_0": "L\u00e0m tr\u00f2n k\u1ebft qu\u1ea3 \u0111\u1ebfn s\u1ed1 nguy\u00ean g\u1ea7n nh\u1ea5t", "mathjax.error": "L\u1ed7i khi th\u1ef1c thi MathJax: {{-message}}", "mathjax.error with context": "L\u1ed7i th\u1ef1c thi MathJax trong {{-context}}: {{-message}}", "exam.introduction": "Gi\u1edbi thi\u1ec7u b\u00e0i thi", "exam.feedback": "Th\u00f4ng b\u00e1o ph\u1ea3n h\u1ed3i b\u00e0i ki\u1ec3m tra", "jme.tokenise.keypair key not a string": "Dictionary key ph\u1ea3i l\u00e0 string, kh\u00f4ng th\u1ec3 l\u00e0 {{type}}.", "jme.shunt.list mixed argument types": "Kh\u00f4ng th\u1ec3 th\u1ef1c thi {{mode}}: k\u1ebft h\u1ee3p c\u00e1c y\u1ebfu t\u1ed1 t\u1eeb \u0111i\u1ec3n v\u00e0 danh s\u00e1ch", "jme.func.listval.key not in dict": "T\u1eeb \u0111i\u1ec3n kh\u00f4ng ch\u1ee9a kh\u00f3a <code> {{key}} </code>", "part.prompt": "l\u1eddi nh\u1eafc", "part.feedback": "ph\u1ea3n h\u1ed3i", "part.numberentry.answer not reduced": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ch\u01b0a \u0111\u01b0\u1ee3c r\u00fat g\u1ecdn", "part.numberentry.give your answer as a reduced fraction": "R\u00fat g\u1ecdn c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n", "part.numberentry.negative decimal places": "Ph\u1ea7n n\u00e0y \u0111\u01b0\u1ee3c thi\u1ebft l\u1eadp \u0111\u1ec3 l\u00e0m tr\u00f2n c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a h\u1ecdc sinh cho m\u1ed9t s\u1ed1 th\u1eadp ph\u00e2n \u00e2m, kh\u00f4ng c\u00f3 ngh\u0129a.", "part.mcq.choices": "c\u00e1c l\u1ef1a ch\u1ecdn", "part.mcq.answers": "c\u00e1c c\u00e2u tr\u1ea3 l\u1eddi", "part.mcq.matrix cell empty": "Ph\u1ea7n {{part}} \u00f4 ma tr\u1eadn \u0111\u00e1nh d\u1ea5u ({{row}}, {{col}}) tr\u1ed1ng", "part.mcq.matrix jme error": "Ph\u1ea7n {{part}} \u00f4 ma tr\u1eadn \u0111\u00e1nh d\u1ea5u ({{row}}, {{col}}) \u0111\u01b0a ra l\u1ed7i JME: {{-error}}", "question.statement": "Tuy\u00ean b\u1ed1", "ruleset.circular reference": "Tham chi\u1ebfu tr\u00f2n trong \u0111\u1ecbnh ngh\u0129a c\u1ee7a quy t\u1eafc <code> {{name}} </code>", "ruleset.set not defined": "Quy t\u1eafc {{name}} ch\u01b0a \u0111\u01b0\u1ee3c x\u00e1c \u0111\u1ecbnh", "jme.evaluate.no scope given": "Numbas.jme.evalu ph\u1ea3i \u0111\u01b0\u1ee3c cung c\u1ea5p Scope", "answer.number.not a number": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng ph\u1ea3i l\u00e0 m\u1ed9t s\u1ed1 h\u1ee3p l\u1ec7.", "answer.number.fractions not allowed": "B\u1ea1n kh\u00f4ng th\u1ec3 nh\u1eadp m\u1ed9t ph\u00e2n s\u1ed1.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "B\u1ea1n kh\u00f4ng th\u1ec3 nh\u1eadp m\u1ed9t ph\u00e2n s\u1ed1.", "answer.matrix.some cell not a number": "M\u1ed9t ho\u1eb7c nhi\u1ec1u \u00f4 trong c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng ph\u1ea3i l\u00e0 s\u1ed1 h\u1ee3p l\u1ec7.", "exam.enter password": "M\u1eadt kh\u1ea9u:", "exam.password.correct": "M\u1eadt kh\u1ea9u \u0111\u00fang. B\u1ea1n c\u00f3 th\u1ec3 b\u1eaft \u0111\u1ea7u thi.", "exam.password.incorrect": "M\u1eadt kh\u1ea9u ch\u01b0a \u0111\u00fang.", "frontpage.scorm.lms not connected": "B\u00e0i ki\u1ec3m tra n\u00e0y \u0111ang ch\u1ea1y trong ch\u1ebf \u0111\u1ed9 \u0111\u1ed9c l\u1eadp. C\u00e2u tr\u1ea3 l\u1eddi v\u00e0 \u0111i\u1ec3m c\u1ee7a b\u1ea1n s\u1ebd kh\u00f4ng \u0111\u01b0\u1ee3c l\u01b0u!", "result.question review": "Xem l\u1ea1i", "control.confirm regen no marks": "B\u1ea1n c\u00f3 mu\u1ed1n t\u1ea1o ng\u1eabu nhi\u00ean l\u1ea1i c\u00e2u h\u1ecfi n\u00e0y?", "control.confirm reveal no marks": "B\u1ea1n c\u00f3 mu\u1ed1n xem c\u00e2u tr\u1ea3 l\u1eddi cho c\u00e2u h\u1ecfi n\u00e0y?", "jme.tokenise.invalid near": "Bi\u1ec3u th\u1ee9c kh\u00f4ng h\u1ee3p l\u1ec7: <code> {{bi\u1ec3u th\u1ee9c}} </code> t\u1ea1i v\u1ecb tr\u00ed {{v\u1ecb tr\u00ed}} g\u1ea7n <code> {{l\u00e2n c\u1eadn}} </code>", "jme.tokenise.number.object not complex": "M\u1ed9t \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7 \u0111\u00e3 \u0111\u01b0\u1ee3c chuy\u1ec3n v\u00e0o h\u00e0m t\u1ea1o s\u1ed1.", "jme.subvars.null substitution": "Thay th\u1ebf bi\u1ebfn r\u1ed7ng: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Ki\u1ec3u d\u1eef li\u1ec7u {{type}} \u0111\u00e3 \u0111\u01b0\u1ee3c \u0111\u0103ng k\u00fd n\u00ean kh\u00f4ng th\u1ec3 \u0111\u0103ng k\u00fd l\u1ea1i.", "jme.type.no cast method": "Kh\u00f4ng th\u1ec3 t\u1ef1 \u0111\u1ed9ng chuy\u1ec3n \u0111\u1ed5i t\u1eeb {{from}} sang {{to}}.", "jme.display.simplifyTree.empty expression": "Bi\u1ec3u th\u1ee9c \u0111ang tr\u1ed1ng", "jme.display.simplifyTree.stuck in a loop": "B\u1ed9 khu\u1ebfch \u0111\u1ea1i b\u1ecb k\u1eb9t trong m\u1ed9t v\u00f2ng l\u1eb7p: <code> {{expr}} </code>", "math.niceNumber.undefined": "\u0110\u00e3 mong \u0111\u1ee3i m\u1ed9t s\u1ed1, nh\u01b0ng c\u00f3 <code> kh\u00f4ng x\u00e1c \u0111\u1ecbnh </code>", "math.rangeToList.zero step size": "Kh\u00f4ng th\u1ec3 chuy\u1ec3n \u0111\u1ed5i m\u1ed9t kho\u1ea3ng v\u1edbi b\u01b0\u1edbc nh\u1ea3y b\u1eb1ng 0 th\u00e0nh m\u1ed9t danh s\u00e1ch.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "B\u1ea1n \u0111\u00e3 xem c\u00e1c b\u01b0\u1edbc gi\u1ea3i.", "part.marking.maximum scaled down": "M\u1ee9c t\u1ed1i \u0111a b\u1ea1n c\u00f3 th\u1ec3 ghi cho ph\u1ea7n n\u00e0y l\u00e0 <strong> {{Count, beautifulNumber}} </strong> $ t (mark). \u0110i\u1ec3m s\u1ed1 c\u1ee7a b\u1ea1n s\u1ebd \u0111\u01b0\u1ee3c thu nh\u1ecf l\u1ea1i cho ph\u00f9 h\u1ee3p.", "part.marking.minimum score applied": "\u0110i\u1ec3m t\u1ed1i thi\u1ec3u cho ph\u1ea7n n\u00e0y l\u00e0 <strong> {{\u0111i\u1ec3m, NiceNumber}} </strong>.", "part.marking.maximum score applied": "\u0110i\u1ec3m t\u1ed1i \u0111a cho ph\u1ea7n n\u00e0y l\u00e0 <strong> {{\u0111i\u1ec3m, NiceNumber}} </strong>.", "part.marking.error in marking script": "C\u00f3 m\u1ed9t l\u1ed7i trong thu\u1eadt to\u00e1n \u0111\u00e1nh d\u1ea5u c\u1ee7a ph\u1ea7n n\u00e0y. H\u00e3y b\u00e1o c\u00e1o \u0111i\u1ec1u n\u00e0y. {{-message}}", "part.marking.no result after replacement": "Ph\u1ea7n n\u00e0y kh\u00f4ng th\u1ec3 \u0111\u01b0\u1ee3c \u0111\u00e1nh d\u1ea5u b\u1eb1ng c\u00e1ch s\u1eed d\u1ee5ng c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n cho c\u00e1c ph\u1ea7n tr\u01b0\u1edbc.", "part.marking.missing required note": "Thu\u1eadt to\u00e1n \u0111\u00e1nh d\u1ea5u kh\u00f4ng x\u00e1c \u0111\u1ecbnh ghi ch\u00fa <code> {{note}} </code>", "marking.apply.not a list": "\u0110\u1ed1i s\u1ed1 \u0111\u1ea7u ti\u00ean \u0111\u1ec3 <code> \u00e1p d\u1ee5ng </code> ph\u1ea3i l\u00e0 m\u1ed9t danh s\u00e1ch v\u00e0 kh\u00f4ng", "marking.apply marking script.script not found": "Kh\u00f4ng t\u00ecm th\u1ea5y t\u1eadp l\u1ec7nh \u0111\u00e1nh d\u1ea5u <code> {{name}} </code>", "marking.note.compilation error": "L\u1ed7i bi\u00ean d\u1ecbch ghi ch\u00fa <code> {{name}} </code>: {{-message}}", "marking.note.error evaluating note": "L\u1ed7i \u0111\u00e1nh gi\u00e1 ghi ch\u00fa <code> {{name}} </code> - {{-message}}", "marking.note.invalid definition": "\u0110\u1ecbnh ngh\u0129a ghi ch\u00fa kh\u00f4ng h\u1ee3p l\u1ec7: <code> {{source}} </code>. {{-d\u1ea5u}}", "marking.note.invalid definition.missing colon": "B\u1ea1n c\u00f3 th\u1ec3 thi\u1ebfu d\u1ea5u hai ch\u1ea5m sau t\u00ean v\u00e0 m\u00f4 t\u1ea3", "marking.note.invalid definition.description missing closing bracket": "B\u1ea1n c\u00f3 th\u1ec3 \u0111ang thi\u1ebfu m\u1ed9t d\u1ea5u \u0111\u00f3ng ngo\u1eb7c", "marking.note.empty expression": "Ghi ch\u00fa <code> {{name}} </code> tr\u1ed1ng.", "marking.script.error parsing notes": "L\u1ed7i ph\u00e2n t\u00edch c\u00fa ph\u00e1p \u0111\u00e1nh d\u1ea5u t\u1eadp l\u1ec7nh: {{- message}}", "part.feedback out of date": "Ph\u1ea3n h\u1ed3i n\u00e0y d\u1ef1a tr\u00ean c\u00e2u tr\u1ea3 l\u1eddi cu\u1ed1i c\u00f9ng c\u1ee7a b\u1ea1n. G\u1eedi c\u00e2u tr\u1ea3 l\u1eddi m\u1edbi c\u1ee7a b\u1ea1n \u0111\u1ec3 nh\u1eadn \u0111\u01b0\u1ee3c ph\u1ea3n h\u1ed3i c\u1eadp nh\u1eadt.", "part.jme.invalid value generator expression": "Bi\u1ec3u th\u1ee9c t\u1ea1o gi\u00e1 tr\u1ecb kh\u00f4ng h\u1ee3p l\u1ec7 cho bi\u1ebfn <code> {{name}} </code>: {{-message}}", "part.mcq.incorrect choice": "B\u1ea1n \u0111\u00e3 ch\u1ecdn m\u1ed9t c\u00e2u tr\u1ea3 l\u1eddi kh\u00f4ng ch\u00ednh x\u00e1c.", "part.matrix.not all cells same precision": "C\u00e1c \u00f4 trong c\u00e2u tr\u1ea3 l\u1eddi ch\u01b0a c\u00f3 c\u00f9ng \u0111\u1ed9 ch\u00ednh x\u00e1c.", "part.gapfill.error marking gap": "L\u1ed7i \u0111\u00e1nh d\u1ea5u {{name}}: {{-message}}", "part.custom.empty setting": "Kh\u00f4ng c\u00f3 gi\u00e1 tr\u1ecb \u0111\u01b0\u1ee3c \u0111\u01b0a ra.", "part.custom.unrecognised input type": "Lo\u1ea1i c\u00e0i \u0111\u1eb7t kh\u00f4ng \u0111\u01b0\u1ee3c nh\u1eadn d\u1ea1ng <code> {{input_type}} </code>", "part.custom.error evaluating input option": "L\u1ed7i \u0111\u00e1nh gi\u00e1 t\u00f9y ch\u1ecdn \u0111\u1ea7u v\u00e0o <code> {{t\u00f9y ch\u1ecdn}} </code>: {{-error}}", "part.custom.input option missing": "\u0110\u1ecbnh ngh\u0129a t\u00f9y ch\u1ecdn \u0111\u1ea7u v\u00e0o <code> {{t\u00f9y ch\u1ecdn}} </code> b\u1ecb thi\u1ebfu.", "part.custom.error evaluating setting": "L\u1ed7i \u0111\u00e1nh gi\u00e1 c\u00e0i \u0111\u1eb7t <code> {{settings}} </code>: {{-error}}", "question.error creating question": "L\u1ed7i khi t\u1ea1o c\u00e2u h\u1ecfi {{number}}: {{-message}}", "question.score feedback.not marked": "Ch\u01b0a \u0111\u01b0\u1ee3c \u0111\u00e1nh d\u1ea5u.", "question.score feedback.partially answered": "\u0110\u01b0\u1ee3c tr\u1ea3 l\u1eddi m\u1ed9t ph\u1ea7n", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "\u0110i\u1ec3m: {{scoreString}}", "question.score feedback.score total actual": "\u0110i\u1ec3m: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "L\u1ed7i trong \u0111\u1ecbnh ngh\u0129a bi\u1ebfn <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Kh\u00f4ng th\u1ec3 t\u1ea3i ti\u1ec7n \u00edch m\u1edf r\u1ed9ng <code> {{name}} </code>.", "control.toggle navigation menu": "\u1ea8n hi\u1ec7n menu \u0111i\u1ec1u h\u01b0\u1edbng", "part.input title": "C\u00e2u tr\u1ea3 l\u1eddi cho ph\u1ea7n {{name}}", "part.correct answer title": "C\u00e2u tr\u1ea3 l\u1eddi d\u1ef1 ki\u1ebfn \u200b\u200bcho ph\u1ea7n {{name}}", "part.jme.must-match.failed": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n ch\u01b0a \u0111\u00fang \u0111\u1ecbnh d\u1ea1ng.", "question.score feedback.none": "question.score feedback.none", "control.submit part.confirm remove next parts": "<p> M\u1ed9t ho\u1eb7c nhi\u1ec1u ph\u1ea7n ti\u1ebfp theo ph\u1ee5 thu\u1ed9c v\u00e0o c\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n cho ph\u1ea7n n\u00e0y. Vi\u1ec7c g\u1eedi l\u1ea1i ph\u1ea7n n\u00e0y s\u1ebd l\u00e0m m\u1ea5t hi\u1ec7u l\u1ef1c c\u1ee7a c\u00e1c ph\u1ea7n \u0111\u00f3 v\u00e0 x\u00f3a ch\u00fang kh\u1ecfi c\u00e2u h\u1ecfi. Thao t\u00e1c n\u00e0y kh\u00f4ng th\u1ec3 ho\u00e0n t\u00e1c. </p>\n<p> B\u1ea1n c\u00f3 mu\u1ed1n g\u1eedi ph\u1ea7n n\u00e0y m\u1ed9t l\u1ea7n n\u1eefa kh\u00f4ng? </p>", "control.back to menu": "Quay tr\u1edf l\u1ea1i menu", "display.error making html": "L\u1ed7i khi t\u1ea1o HTML trong {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} trong <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Kh\u00f4ng bi\u1ebft l\u00e0m th\u1ebf n\u00e0o \u0111\u1ec3 ph\u00e2n bi\u1ec7t <code> {{tree}} </code>", "math.order complex numbers": "Kh\u00f4ng th\u1ec3 s\u1eafp x\u1ebfp th\u1ee9 t\u1ef1 s\u1ed1 ph\u1ee9c", "menu.choose a question": "Ch\u1ecdn m\u1ed9t c\u00e2u h\u1ecfi.", "part.choose next part.answered": "B\u1ea1n mu\u1ed1n l\u00e0m g\u00ec ti\u1ebfp theo?", "part.choose next part.unanswered": "Ho\u1eb7c, b\u1ea1n c\u00f3 th\u1ec3:", "part.choose next part.will be locked": "(Ph\u1ea7n n\u00e0y s\u1ebd b\u1ecb kh\u00f3a)", "part.reached dead end": "Kh\u00f4ng c\u00f2n g\u00ec \u0111\u1ec3 l\u00e0m n\u1eefa.", "part.next part.penalty amount": "(thua cu\u1ed9c {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n kh\u00f4ng h\u1ee3p l\u1ec7. B\u1ea1n ph\u1ea3i nh\u1eadp m\u1ed9t s\u1ed1 nguy\u00ean, m\u1ed9t s\u1ed1 th\u1eadp ph\u00e2n ho\u1eb7c m\u1ed9t ph\u00e2n s\u1ed1.", "question": "C\u00e2u h\u1ecfi", "question.progress": "Ti\u1ebfn \u0111\u1ed9 c\u00e2u h\u1ecfi:", "question.score feedback.unattempted": "Ch\u01b0a n\u1ed7 l\u1ef1c", "question.score feedback.attempted": "C\u00f3 n\u1ed7 l\u1ef1c", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Quay l\u1ea1i ph\u1ea7n tr\u01b0\u1edbc", "end.print": "In b\u1ea3ng \u0111i\u1ec3m b\u00e0i ki\u1ec3m tra c\u1ee7a b\u1ea1n", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Ch\u1eef k\u00fd h\u00e0m kh\u00f4ng h\u1ee3p l\u1ec7: {{str}}", "part.custom.expected answer has wrong type": "C\u00e2u tr\u1ea3 l\u1eddi mong \u0111\u1ee3i c\u1ee7a ph\u1ea7n n\u00e0y b\u1ecb sai th\u1ec3 lo\u1ea1i. N\u00f3 n\u00ean l\u00e0 <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "C\u00e0i \u0111\u1eb7t ph\u1ea7n nh\u1eadp c\u1ee7a c\u00e2u tr\u1ea3 l\u1eddi <code> {{option}} </code> kh\u00f4ng \u0111\u00fang lo\u1ea1i. N\u00f3 ph\u1ea3i l\u00e0 <code> {{shouldbe}}</code>.", "matrix input.size control legend": "K\u00edch th\u01b0\u1edbc", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "M\u00e0u n\u1ec1n", "modal.style.text colour": "M\u00e0u v\u0103n b\u1ea3n", "modal.style.text size": "C\u1ee1 v\u0103n b\u1ea3n", "modal.style.explanation": "S\u1eed d\u1ee5ng c\u00e1c c\u1ea5u h\u00ecnh n\u00e0y \u0111\u1ec3 thay \u0111\u1ed5i giao di\u1ec7n c\u1ee7a b\u00e0i ki\u1ec3m tra.", "modal.style.reset to defaults": "\u0110\u1eb7t tr\u1edf v\u1ec1 m\u1eb7c \u0111\u1ecbnh", "modal.style.text size preview": "H\u1ea7u h\u1ebft v\u0103n b\u1ea3n s\u1ebd l\u1edbn nh\u01b0 th\u1ebf n\u00e0y.", "control.style options": "T\u00f9y ch\u1ecdn hi\u1ec3n th\u1ecb", "part.marking.partially correct": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111\u00fang m\u1ed9t ph\u1ea7n.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Chuy\u1ec3n \u0111\u1ebfn ph\u1ea7n n\u1ed9i dung", "result.learning objective": "M\u1ee5c ti\u00eau h\u1ecdc t\u1eadp", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "B\u1ea1n c\u00f3 th\u1ec3 thi\u1ebfu d\u1ea5u hai ch\u1ea5m sau t\u00ean v\u00e0 m\u00f4 t\u1ea3", "jme.script.note.invalid definition.description missing closing bracket": "B\u1ea1n c\u00f3 th\u1ec3 thi\u1ebfu d\u1ea5u ngo\u1eb7c \u0111\u00f3ng", "jme.script.note.empty expression": "Ghi ch\u00fa <code> {{name}} </code> tr\u1ed1ng.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "H\u00e0ng {{row}}, c\u1ed9t {{column}}", "control.move to next question": "Chuy\u1ec3n sang c\u00e2u h\u1ecfi ti\u1ebfp theo", "diagnostic.use retry": "S\u1eed d\u1ee5ng m\u1ed9t l\u1ea7n th\u1eed l\u1ea1i v\u00e0 th\u1eed l\u1ea1i ch\u1ee7 \u0111\u1ec1 n\u00e0y.", "diagnostic.move to next topic": "Chuy\u1ec3n sang ch\u1ee7 \u0111\u1ec1 ti\u1ebfp theo.", "diagnostic.next step question": "B\u1ea1n mu\u1ed1n l\u00e0m g\u00ec ti\u1ebfp theo?", "diagnostic.now assessing topic": "Hi\u1ec7n \u0111ang \u0111\u00e1nh gi\u00e1 {{current_topic}}", "diagnostic.one retry left": "B\u1ea1n c\u00f2n 1 l\u1ea7n th\u1eed l\u1ea1i", "diagnostic.retries left": "B\u1ea1n c\u00f2n {{retries}} l\u1ea7n th\u1eed l\u1ea1i.", "diagnostic.percentage completed": "B\u1ea1n \u0111\u00e3 ho\u00e0n th\u00e0nh <strong>{{percentage}}%</strong> b\u00e0i ki\u1ec3m tra.", "diagnostic.test is over": "B\u00e0i ki\u1ec3m tra k\u1ebft th\u00fac.", "diagnostic.passed all lo": "B\u1ea1n \u0111\u00e3 ho\u00e0n th\u00e0nh t\u1ea5t c\u1ea3 c\u00e1c m\u1ee5c ti\u00eau h\u1ecdc t\u1eadp.", "diagnostic.more work on lo": "B\u1ea1n c\u1ea7n n\u1ed7 l\u1ef1c h\u01a1n \u0111\u1ed1i v\u1edbi c\u00e1c m\u1ee5c ti\u00eau h\u1ecdc t\u1eadp sau: {{los}}.", "diagnostic.move to next question in topic": "Chuy\u1ec3n sang c\u00e2u h\u1ecfi ti\u1ebfp theo trong ch\u1ee7 \u0111\u1ec1.", "diagnostic.complete": "Ho\u00e0n th\u00e0nh!", "diagnostic.studying topic": "\u0110ang nghi\u00ean c\u1ee9u {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Mong mu\u1ed1n c\u00f3 m\u1ed9t c\u00e1i g\u00ec \u0111\u00f3 gi\u1eefa d\u1ea5u ngo\u1eb7c m\u1edf v\u00e0 d\u1ea5u ph\u1ea9y", "part.waiting for pre submit": "C\u00e2u tr\u1ea3 l\u1eddi c\u1ee7a b\u1ea1n \u0111ang \u0111\u01b0\u1ee3c ch\u1ea5m \u0111i\u1ec3m. Vui l\u00f2ng ch\u1edd.", "diagnostic.end test": "K\u1ebft th\u00fac b\u00e0i ki\u1ec3m tra.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "pt-br": {"translation": {"page.loading": "Carregando...", "page.saving": "<p>Salvando.</p>\n<p>Isso pode demorar alguns segundos.</p>", "mathjax.math processing error": "\"{{-message}}\" quando \"texificando\" <code>{{expression}}</ code>", "die.numbas failed": "Numbas falhou", "die.sorry": "Desculpe, Numbas encontrou um erro; portanto, n\u00e3o pode continuar. Abaixo est\u00e1 uma descri\u00e7\u00e3o do erro.", "die.error": "Erro", "modal.ok": "OK", "modal.cancel": "Cancelar", "exam.exam name": "Nome do Exame:", "exam.random seed": "ID de sess\u00e3o:", "exam.student name": "Nome do Estudante:", "exam.number of questions": "N\u00famero de perguntas:", "exam.marks available": "Notas dispon\u00edveis:", "exam.pass percentage": "Percentual de aprova\u00e7\u00e3o:", "exam.time allowed": "Tempo Permitido:", "exam.passed": "Aprovado", "exam.failed": "Reprovado", "exam.review header": "Avalia\u00e7\u00e3o:", "frontpage.start": "Iniciar", "suspend.paused header": "Pausado", "suspend.exam suspended": "O exame foi interrompido. Pressione <em>Continuar<em> para continuar.", "suspend.you can resume": "Voc\u00ea poder\u00e1 continuar essa sess\u00e3o na pr\u00f3xima vez que iniciar esta atividade.", "suspend.resume": "Continuar", "result.exit": "Sair", "result.print": "Imprimir relat\u00f3rio de resultados", "result.exam summary": "Relat\u00f3rio do exame", "result.performance summary": "Relat\u00f3rio de performance", "result.exam start": "Iniciar exame", "result.exam stop": "Parar exame", "result.time spent": "Tempo gasto", "result.questions attempted": "Quest\u00f5es tentadas:", "result.score": "Pontua\u00e7\u00e3o", "result.result": "Resultado", "result.question number": "N\u00famero da quest\u00e3o", "result.question score": "Pontua\u00e7\u00e3o", "result.question review title": "Revisar esta quest\u00e3o", "result.click a question to review": "Clique no n\u00famero de uma quest\u00e3o para ver como as suas respostas foram avaliadas e, quando dispon\u00edvel, as solu\u00e7\u00f5es completas.", "end.exam has finished": "O exame terminou. Voc\u00ea pode fechar esta janela.", "control.confirm leave": "Voc\u00ea n\u00e3o concluiu o exame.", "control.not all questions answered": "Voc\u00ea n\u00e3o completou todas as quest\u00f5es neste exame.", "control.not all questions submitted": "Voc\u00ea modificou uma ou mais respostas mas n\u00e3o as enviou. Por favor, verifique se cada uma das quest\u00f5es foi enviada.", "control.confirm end": "Tem certeza que quer finalizar o exame? Ap\u00f3s finalizar o exame n\u00e3o ser\u00e1 poss\u00edvel modificar as suas respostas.", "control.confirm regen": "Gostaria de re-aleatorizar esta quest\u00e3o? Se clicar OK, todas as suas respostas e pontua\u00e7\u00f5es para a quest\u00e3o atual ser\u00e3o perdidos.", "control.confirm reveal": "Gostaria de revelar a resposta para esta quest\u00e3o? Pontos recebidos at\u00e9 o momento ser\u00e3o bloqueados e voc\u00ea n\u00e3o poder\u00e1 responder essa quest\u00e3o mais tarde.", "control.proceed anyway": "Prosseguir mesmo assim?", "control.regen": "Tentar outra quest\u00e3o similar a esta", "control.submit answer": "Enviar resposta", "control.submit all parts": "Enviar todas as partes", "control.submit again": "Enviar novamente", "control.submit": "Enviar", "control.previous": "Anterior", "control.next": "Pr\u00f3ximo", "control.advice": "Dica", "control.reveal": "Revelar respostas", "control.total": "Total", "control.pause": "Pausar", "control.end exam": "Finalizar exame", "control.back to results": "Retornar aos resultados", "display.part.jme.error making maths": "Erro ao criar o display matem\u00e1tico", "exam.xml.bad root": "Elemento raiz do XML do exame deve ser 'exam'", "exam.changeQuestion.no questions": "Este exame n\u00e3o cont\u00e9m quest\u00f5es! Verifique se h\u00e1 erros no arquivo .exam.", "feedback.you were awarded": "Voc\u00ea recebeu <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) tirados.", "jme.tokenise.invalid": "Express\u00e3o inv\u00e1lida: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Poucos argumentos para a opera\u00e7\u00e3o {{op}}", "jme.shunt.no left bracket in function": "Sem abertura de colchete correspondete na aplica\u00e7\u00e3o da fun\u00e7\u00e3o ou tupla", "jme.shunt.no left square bracket": "Sem abertura de colchete correspondente", "jme.shunt.no left bracket": "Sem abertura de colchete correspondente", "jme.shunt.no right bracket": "Sem fechamento de colchete correspondente", "jme.shunt.no right square bracket": "Sem fechamento de colchete para finalizar a lista", "jme.shunt.missing operator": "Express\u00e3o n\u00e3o pode ser avaliada -- fata um operador.", "jme.typecheck.function maybe implicit multiplication": "Opera\u00e7\u00e3o {{name}} n\u00e3o definida. Voc\u00ea quis dizer <code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Opera\u00e7\u00e3o <code>{{op}}</code> n\u00e3o definida. <code>{{op}}</code> \u00e9 uma vari\u00e1vel e voc\u00ea quis dizer <code>{{sugestion}}*(...)</code>?", "jme.typecheck.op not defined": "Opera\u00e7\u00e3o '{{op}}' n\u00e3o definida.", "jme.typecheck.no right type definition": "Nenhuma defini\u00e7\u00e3o de '{{op}}' do tipo correto foi encontrada.", "jme.typecheck.no right type unbound name": "Vari\u00e1vel <code>{{name}}</code> n\u00e3o definida.", "jme.typecheck.map not on enumerable": "Opera\u00e7\u00e3o <code>map</code> funciona em uma listas ou intervalo, n\u00e3o {{type}}", "jme.evaluate.undefined variable": "Vari\u00e1vel {{name}} n\u00e3o definida.", "jme.thtml.not html": "Passou um valor n\u00e3o-HTML para o contrutor THTML.", "jme.func.switch.no default case": "Sem caso default no Switch", "jme.func.listval.invalid index": "\u00cdndice {{index}} inv\u00e1lido em lista de tamanho {{size}}", "jme.func.listval.not a list": "Objeto n\u00e3o \u00e9 subscriptable", "jme.func.matrix.invalid row type": "N\u00e3o \u00e9 poss\u00edvel construir uma matriz a partir de linhas do tipo {{type}}", "jme.func.except.continuous range": "N\u00e3o \u00e9 poss\u00edvel usar o operador 'except' em intervalos cont\u00ednuos.", "jme.matrix.reports bad size": "Matriz informa tamanho incorretamente - deve ser um erro na fun\u00e7\u00e3o construtor", "jme.texsubvars.no right bracket": "Sem <code>]</code> correspondente nos argumentos de {{op}}.", "jme.texsubvars.missing parameter": "Faltando par\u00e2metro em {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Sem <code>}</code> correspondente em {{op}}", "jme.user javascript.error": "Erro na fun\u00e7\u00e3o definida pelo usu\u00e1rio <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Erro ao criar fun\u00e7\u00e3o <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Erro de sintaxe ao definir fun\u00e7\u00e3o", "jme.variables.variable not defined": "Vari\u00e1vel <code>{{name}}</code> n\u00e3o definida.", "jme.variables.empty definition": "Defini\u00e7\u00e3o da vari\u00e1vel <code>{{name}}</code> est\u00e1 vazia.", "jme.variables.circular reference": "Refer\u00eancia circular na defini\u00e7\u00e3o da vari\u00e1vel <code>{{name}}</code>", "jme.variables.error computing dependency": "Erro ao computar vari\u00e1vel referenciada <code>{{name}}</code>", "jme.variables.error evaluating variable": "Erro ao avaliar a vari\u00e1vel {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Um conjunto v\u00e1lido de vari\u00e1veis de quest\u00e3o n\u00e3o foi gerado a tempo.", "jme.display.unknown token type": "N\u00e3o \u00e9 poss\u00edvel \"texificar\" tipo {{type}}", "jme.display.collectRuleset.no sets": "Nenhum conjunto passado para collectRuleset!", "jme.display.collectRuleset.set not defined": "Conjunto de regras {{name}} n\u00e3o foi definido", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree deve receber um Scope", "math.precround.complex": "N\u00e3o \u00e9 poss\u00edvel arredondar para um n\u00famero complexo de casas decimais", "math.siground.complex": "N\u00e3o \u00e9 poss\u00edvel arredondar para um n\u00famero complexo de algarismos significantes.", "math.combinations.complex": "N\u00e3o \u00e9 poss\u00edvel calcular combina\u00e7\u00f5es de n\u00fameros complexos", "math.permutations.complex": "N\u00e3o \u00e9 poss\u00edvel calcular permuta\u00e7\u00f5es de n\u00fameros complexos", "math.gcf.complex": "N\u00e3o \u00e9 poss\u00edvel calcular o MDC de n\u00fameros complexos", "math.lcm.complex": "N\u00e3o \u00e9 poss\u00edvel calcular o MMC de n\u00fameros complexos", "math.lt.order complex numbers": "N\u00e3o \u00e9 poss\u00edvel ordenar n\u00fameros complexos", "math.choose.empty selection": "Sele\u00e7\u00e3o vazia passada a um fun\u00e7\u00e3o rand\u00f4mica", "matrixmath.abs.non-square": "N\u00e3o \u00e9 poss\u00edvel calcular a determinante de uma matriz n\u00e3o quadrada", "matrixmath.abs.too big": "Desculpe, ainda n\u00e3o \u00e9 poss\u00edvel calcular a determinante de matrizes maiores que 3x3", "matrixmath.mul.different sizes": "N\u00e3o \u00e9 poss\u00edvel multiplicar matrizes de diferentes tamanhos", "vectormath.cross.not 3d": "Poss\u00edvel calcular produto vetorial apenas para vetores tridimensionais.", "vectormath.dot.matrix too big": "N\u00e3o \u00e9 poss\u00edvel calcular o produto escalar de uma matriz que n\u00e3o seja $1 \\times N$ ou $N \\times 1$.", "vectormath.cross.matrix too big": "N\u00e3o \u00e9 poss\u00edvel calcular o produto vetorial de uma matriz que n\u00e3o seja  $1 \\times N$ ou $N \\times 1$.", "part.with steps answer prompt": "Resposta:", "part.script.error": "Erro na part {{path}}, no script {{script}}: {{-message}}", "part.marking.steps no matter": "Como voc\u00ea recebeu nota m\u00e1xima para esta parte, suas respostas para os passos n\u00e3o foram contabilizadas.", "part.marking.revealed steps no penalty": "Voc\u00ea revelou os passos.", "part.marking.used variable replacements": "Esta parte foi pontuada usando suas respostas para as partes anteriores.", "part.marking.variable replacement part not answered": "Voc\u00ea deve responder {{part}} primeiro", "part.marking.resubmit because of variable replacement": "A avalia\u00e7\u00e3o desta parte depende das duas respostas em outras partes, as quais voc\u00ea modificou. Envie esta parte novamente para atualizar sua pontua\u00e7\u00e3o.", "part.marking.not submitted": "Sem resposta enviada", "part.marking.did not answer": "Voc\u00ea n\u00e3o respondeu esta quest\u00e3o", "part.marking.nothing entered": "Voc\u00ea n\u00e3o inseriu uma resposta.", "part.marking.incorrect": "Sua resposta est\u00e1 incorreta.", "part.marking.correct": "Sua resposta est\u00e1 correta.", "part.marking.uncaught error": "Erro ao avaliar: {{-message}}", "part.marking.no result": "Esta part n\u00e3o p\u00f4de ser avaliada.", "part.correct answer": "Resposta esperada:", "part.missing type attribute": "{{part}}: Faltando atributo de tipo de parte", "part.unknown type": "{{part}}: Tipo de parte n\u00e3o reconhecido {{type}}", "part.setting not present": "Propriedade '{{property}}' n\u00e3o atribuida", "part.jme.answer missing": "Resposta correta n\u00e3o encontrada", "part.jme.answer too long": "Sua resposta \u00e9 muito longa.", "part.jme.answer too short": "Sua resposta \u00e9 muito curta.", "part.jme.answer invalid": "Sua resposta n\u00e3o \u00e9 uma express\u00e3o matem\u00e1tica v\u00e1lida.\n<br/>{{-message}}", "part.jme.marking.correct": "Sua resposta est\u00e1 numericamente correta.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Sua resposta deve conter: {{strings}}", "part.jme.must-have several": "Sua resposta deve conter todos de: {{strings}}", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Sua resposta n\u00e3o deve conter: {{strings}}", "part.jme.not-allowed several": "Sua resposta n\u00e3o deve conter nenhum de: {{strings}}", "part.jme.unexpected variable name": "Sua resposta usa uma vari\u00e1vel de nome inesperado <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Sua resposta usa uma vari\u00e1vel de nome inesperado <code>{{name}}</code>. Voc\u00ea quis dizer <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Resposta para exibi\u00e7\u00e3o n\u00e3o encontrada", "part.patternmatch.correct except case": "Sua resposta est\u00e1 correta, exceto a formata\u00e7\u00e3o.", "part.numberentry.correct except decimal": "Sua resposta est\u00e1 na faixa permitida, por\u00e9m n\u00fameros decimais n\u00e3o s\u00e3o permitidos.", "part.numberentry.correct except fraction": "Sua resposta est\u00e1 na faixa permitida, mas fra\u00e7\u00f5es n\u00e3o s\u00e3o permitidas.", "part.numberentry.answer invalid": "Voc\u00ea n\u00e3o inseriu um n\u00famero valido.", "part.numberentry.answer not integer": "Sua resposta \u00e9 inv\u00e1lida. Voc\u00ea deve inserir um n\u00famero inteiro, n\u00e3o um decimal.", "part.numberentry.answer not integer or decimal": "Sua resposta \u00e9 inv\u00e1lida. Voc\u00ea deve inserir um inteiro ou um decimal.", "part.numberentry.zero sig fig": "Esta parte est\u00e1 configurada para arredondar a resposta do estudante para zero algarismos significantes, o que n\u00e3o faz sentido.", "part.mcq.options def not a list": "A express\u00e3o que define {{properties}} n\u00e3o \u00e9 uma lista.", "part.mcq.marking matrix string empty": "A express\u00e3o customizada da matriz de pontua\u00e7\u00e3o est\u00e1 vazia.", "part.mcq.choices missing": "Defini\u00e7\u00e3o das alternativas n\u00e3o encontrada", "part.mcq.matrix not a number": "A c\u00e9lula da matriz de pontua\u00e7\u00e3o ({{row}},{{column}}) da parte {{part}} n\u00e3o cont\u00e9m um n\u00famero.", "part.mcq.wrong number of choices": "Voc\u00ea selecionou o n\u00famero errado de op\u00e7\u00f5es.", "part.mcq.no choices selected": "Nenhuma op\u00e7\u00e3o selecionada.", "part.mcq.matrix not a list": "Matriz de pontua\u00e7\u00e3o, definida por uma express\u00e3o JME, n\u00e3o \u00e9 uma lista mas deveria ser.", "part.mcq.matrix wrong type": "Elemento de tipo inv\u00e1lido '{{type}}' usado na matriz de pontua\u00e7\u00e3o.", "part.mcq.matrix mix of numbers and lists": "Mix de n\u00fameros e listas usados na matriz de pontua\u00e7\u00e3o.", "part.mcq.matrix wrong size": "Matriz de pontua\u00e7\u00e3o tem ", "part.mcq.correct choice": "Voc\u00ea escolheu a resposta correta.", "part.matrix.answer invalid": "Sua resposta n\u00e3o \u00e9 v\u00e1lida.", "part.matrix.invalid cell": "Uma ou mais c\u00e9lulas da sua resposta est\u00e3o vazias ou s\u00e3o inv\u00e1lidas.", "part.matrix.some incorrect": "Uma ou mais c\u00e9lulas da sua resposta est\u00e3o incorretas, mas voc\u00ea recebeu pontua\u00e7\u00e3o pelo resto.", "part.matrix.empty": "Voc\u00ea n\u00e3o inseriu uma resposta.", "part.matrix.empty cell": "Uma ou mais c\u00e9lulas da sua resposta est\u00e3o vazias.", "part.matrix.size mismatch": "O autor da quest\u00e3o n\u00e3o permitiu que o estudante decida as dimens\u00f5es da resposta, mas a resposta correta \u00e9 {{correct_dimension}} enquanto a resposta inserida \u00e9 {{input_dimensions}}", "part.gapfill.feedback header": "<strong> Lacuna {{index}}</strong>", "part.extension.not implemented": "Parte n\u00e3o implementou o m\u00e9todo <code>{{name}}</code>.", "question.loaded name mismatch": "N\u00e3o \u00e9 poss\u00edvel retomar esta tentativa - o pacote modificou desde a \u00faltima sess\u00e3o.", "question.error": "Quest\u00e3o {{number}}: {{-message}}", "question.preamble.error": "Erro no pre\u00e2mbulo: {{-message}}", "question.preamble.syntax error": "Erro de sintaxe no pre\u00e2mbulo", "question.unsupported part type": "Tipo de parte n\u00e3o suportado.", "question.header": "Quest\u00e3o {{number}}", "question.submit part": "Enviar parte", "question.show steps": "Mostrar passos", "question.show steps penalty": "Voc\u00ea perder\u00e1 <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Sua pontua\u00e7\u00e3o n\u00e3o ser\u00e1 afetada.", "question.show steps already penalised": "Voc\u00ea j\u00e1 mostrou passos. Voc\u00ea pode mostr\u00e1-los novamente sem mais penalidades.", "question.hide steps": "Ocultar passos", "question.hide steps no penalty": "Sua pontua\u00e7\u00e3o n\u00e3o ser\u00e1 afetada.", "question.advice": "Dica", "question.no such part": "N\u00e3o foi poss\u00edvel encontrar parte {{path}}", "question.can not submit": "N\u00e3o foi poss\u00edvel enviar a resposta - verifique se h\u00e1 erros", "question.answer submitted": "Resposta enviada", "question.score feedback.show": "Mostrar feedback", "question.score feedback.hide": "Ocultar feedback", "question.score feedback.answered total actual": "Pontua\u00e7\u00e3o: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Respondida.", "question.score feedback.answered actual": "Pontua\u00e7\u00e3o: {{scoreString}}", "question.score feedback.answered": "Respondida.", "question.score feedback.unanswered": "N\u00e3o respondida.", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Sua resposta est\u00e1 correta.", "question.score feedback.partial": "Sua resposta est\u00e1 parcialmente correta.", "question.score feedback.wrong": "Sua resposta est\u00e1 incorreta", "question.selector.unsubmitted changes": "Modifica\u00e7\u00f5es n\u00e3o enviadas", "timing.no accumulator": "sem acumulador de tempo {{name}}", "timing.time remaining": "Tempo restante:", "xml.could not load": "N\u00e3o foi poss\u00edvel carregar um documento XML: {{-message}}", "xml.property not number": "Propriedade {{name}} deve ser um n\u00famero, mas n\u00e3o \u00e9 ({{value}}), no n\u00f3 {{element}}", "xml.property not boolean": "Propriedade {{name}} deve ser um booleano, mas n\u00e3o \u00e9 ({{value}}), no n\u00f3 {{element}}", "xml.error in variable definition": "Erro na defini\u00e7\u00e3o da vari\u00e1vel <code>{{name}}</code>", "scorm.error initialising": "Erro ao inicializar protocolo SCORM: {{-message}}", "scorm.failed save": "<p> A requisi\u00e7\u00e3o para salvar dados falhou. Pressione <b>OK</b> para tentar novamente.</p>\n<p>Se receber essa mensagem repetidas vezes, verifique sua conex\u00e3o a internet ou use outro computador. Suas respostas previamente enviadas foram salvas com sucesso e ser\u00e3o restauradas caso continue esta sess\u00e3o em outro computador.</p>\n<p>Se esta mensagem aparecer persistentemente e voc\u00ea n\u00e3o conseguir salvar <em>nenhuma</em> resposta, por favor, contate o professor.</p>", "scorm.no exam suspend data": "Falha ao retomar: sem dados suspensos do exame.", "scorm.error loading suspend data": "Erro ao carregar dados suspensos: {{-message}}", "scorm.error loading question": "Erro ao carregar quest\u00e4o {{number}}: {{-message}}", "scorm.no question suspend data": "Sem dados suspensos de quest\u00e3o", "scorm.error loading part": "Erro ao carregar parte {{part}}: {{-message}}", "scorm.no part suspend data": "Sem dados suspensos de parte", "util.product.non list": "Passou uma n\u00e3o-lista para <code>Numbas.util.product</code>", "mark": "ponto", "was": "era", "part": "parte", "gap": "lacuna", "step": "passo", "jme.substituteTree.undefined variable": "Vari\u00e1vel indifinida <code>{{name}}</code>", "jme.user javascript.returned undefined": "Fun\u00e7\u00e3o javascript definida pelo usu\u00e1rio <code>{{name}}</code> retornou <code>undefined</code>.", "part.marking.steps change": "Voc\u00ea recebeu <strong>{{count,niceNumber}}</strong> $t(mark) por suas respostas aos passos.", "part.marking.revealed steps with penalty": "Voc\u00ea revelou passos. A pontua\u00e7\u00e3o m\u00e1xima que pode obter por esta parte \u00e9 <strong>{{count,niceNumber}}</strong> $t(mark). Suas pontua\u00e7\u00f5es ser\u00e3o ajustadas de acordo.", "part.marking.total score": "Voc\u00ea pontuou <strong>{{count,niceNumber}}</strong> $t(mark) por esta parte.", "part.numberentry.precision type.dp": "casa decimal", "part.numberentry.precision type.dp_plural": "casas decimais", "part.numberentry.precision type.sigfig": "algarismo significativo", "part.numberentry.precision type.sigfig_plural": "algarismos significativos", "part.numberentry.give your answer to precision": "Arredonde sua resposta para {{count,niceNumber}} {{precisionType}}", "question.unsubmitted changes": "Voc\u00ea modificou a sua resposta mas n\u00e3o a enviou. Por favor, verifique sua resposta e pressione o bot\u00e3o <strong>Enviar resposta</stron>.", "question.unsubmitted changes_plural": "Voc\u00ea modificou suas respostas mas n\u00e3o as enviou. Por favor, verifique suas respostas e pressione o bot\u00e3o <strong>Enviar todas as partes</stron>.", "util.equality not defined for type": "Igualdade n\u00e3o definida para o tipo {{type}}", "mark_plural": "pontos", "was_plural": "eram", "die.script not loaded": "Numbas n\u00e3o iniciou pois o arquivo <code>{{file}}</code> n\u00e3o foi carregado. Verifique se est\u00e1 incluso em <code>script.js</code>.", "math.combinations.n less than zero": "Imposs\u00edvel calcular combina\u00e7\u00f5es: n \u00e9 menor que zero", "math.combinations.k less than zero": "Imposs\u00edvel calcular combina\u00e7\u00f5es: k \u00e9 menor que zero", "math.combinations.n less than k": "Imposs\u00edvel calcular combina\u00e7\u00f5es: n \u00e9 menor que k", "math.permutations.n less than zero": "Imposs\u00edvel calcular permuta\u00e7\u00f5es: n \u00e9 menor que zero", "math.permutations.k less than zero": "Imposs\u00edvel calcular permuta\u00e7\u00f5es: k \u00e9 menor que zero", "math.permutations.n less than k": "Imposs\u00edvel calcular permuta\u00e7\u00f5es: n \u00e9 menor que k", "part.numberentry.give your answer to precision_0": "Arredonde sua resposta para o inteiro mais pr\u00f3ximo.", "mathjax.error": "Erro de processamento MathJax: {{-message}}", "mathjax.error with context": "Erro de processamento MathJax em {{context}}: {{-message}}", "exam.introduction": "Introdu\u00e7\u00e3o do exame", "exam.feedback": "Mensagem de feedback do exame", "jme.tokenise.keypair key not a string": "Chave do dicion\u00e1rio deve ser uma string, n\u00e3o {{type}}.", "jme.shunt.list mixed argument types": "N\u00e3o \u00e9 poss\u00edvel analisar {{mode}}: mix de dicion\u00e1rio e lista", "jme.func.listval.key not in dict": "Dicion\u00e1rio n\u00e3o cont\u00e9m a chave <code>{{key}}</code>", "part.prompt": "aviso", "part.feedback": "feedback", "part.numberentry.answer not reduced": "Sua respostar n\u00e3o est\u00e1 reduzida para os menores termos.", "part.numberentry.give your answer as a reduced fraction": "Reduza a sua resposta para os menores termos.", "part.numberentry.negative decimal places": "Esta parte est\u00e1 configurada para arredondar a resposta do estudante para um n\u00famero negativo de algarismos significantes, o que n\u00e3o faz sentido.", "part.mcq.choices": "op\u00e7\u00f5es", "part.mcq.answers": "respostas", "part.mcq.matrix cell empty": "A c\u00e9lula da matriz de pontua\u00e7\u00e3o ({{row}},{{column}}) da parte {{part}} est\u00e1 vazia.", "part.mcq.matrix jme error": "A c\u00e9lula da matriz de pontua\u00e7\u00e3o ({{row}},{{column}}) da parte {{part}} possui erro JME: {{error}}.", "question.statement": "Enunciado", "ruleset.circular reference": "Refer\u00eancia circular na defini\u00e7\u00e3o do conjunto de regras <code>{{name}}</code>", "ruleset.set not defined": "Conjunto de regras {{name}} n\u00e3o foi definido", "jme.evaluate.no scope given": "Numbas.jme.evaluate deve receber um Scope", "answer.number.not a number": "A resposta  n\u00e3o \u00e9 um n\u00famero v\u00e1lido.", "answer.number.fractions not allowed": "N\u00e3o pode inserir uma  fra\u00e7\u00e3o.", "answer.jme.invalid expression": "{{-mensagem}}", "answer.matrix.fractions not allowed": "N\u00e3o pode inserir fra\u00e7\u00f5es.", "answer.matrix.some cell not a number": "Uma ou mais partes  da resposta  n\u00e3o \u00e9 um n\u00famero v\u00e1lido.", "exam.enter password": "Palavra-passe:", "exam.password.correct": "A palavra-passe est\u00e1 correcta. Pode come\u00e7ar o exame.", "exam.password.incorrect": "A palavra-passe est\u00e1 incorrecta.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Revisar", "control.confirm regen no marks": "Gostaria de re-aleatorizar esta quest\u00e3o?", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Resposta parcial", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Pontua\u00e7\u00e3o: {{scoreString}}", "question.score feedback.score total actual": "Pontua\u00e7\u00e3o: {{scoreString}}", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Pergunta", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Tamanho", "matrix input.rows": "Linhas", "matrix input.columns": "Colunas", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "sv-se": {"translation": {"page.loading": "Laddar...", "page.saving": "<p>Sparar.</p>\n<p>Detta kan ta n\u00e5gra sekunder</p>", "mathjax.math processing error": "\"{{-message}}\" n\u00e4r Numbas konverterade <code>{{expression}}</code> till LaTeX", "die.numbas failed": "Numbas kunde inte starta", "die.sorry": "Numbas fick ett fel som stannade programmet. Underliggande finns en beskrivning av felet.", "die.error": "Fel", "modal.ok": "OK", "modal.cancel": "Avbryt", "exam.exam name": "Namn p\u00e5 prov:", "exam.random seed": "Sessions ID:", "exam.student name": "Studentens namn", "exam.number of questions": "Antal fr\u00e5gor:", "exam.marks available": "Po\u00e4ng tillg\u00e4ngliga", "exam.pass percentage": "Godk\u00e4nd percentil", "exam.time allowed": "Till\u00e5ten tid", "exam.passed": "Godk\u00e4nd", "exam.failed": "Icke godk\u00e4nt", "exam.review header": "Granska:", "frontpage.start": "Start", "suspend.paused header": "Pausad", "suspend.exam suspended": "Provet har pausats. Tryck p\u00e5 <em>\u00c5teruppta</em> f\u00f6r att forts\u00e4tta.", "suspend.you can resume": "Du kan forts\u00e4tta n\u00e4sta g\u00e5ng du startar denna aktivitet", "suspend.resume": "\u00c5teruppta", "result.exit": "Avsluta prov", "result.print": "Skriv ut resultat-statistik", "result.exam summary": "Summering prov", "result.performance summary": "Summering utfall", "result.exam start": "Prov start:", "result.exam stop": "Prov avslutning:", "result.time spent": "Tid anv\u00e4nt:", "result.questions attempted": "Fr\u00e5gor ", "result.score": "Po\u00e4ng", "result.result": "Resultat:", "result.question number": "Fr\u00e5ga", "result.question score": "Resultat", "result.question review title": "\u00c5terkoppla denna fr\u00e5ga", "result.click a question to review": "Klicka p\u00e5 en fr\u00e5ga f\u00f6r att se hur dina svar \u00e4r bed\u00f6mda samt f\u00f6r att se eventuella l\u00f6sningar.", "end.exam has finished": "Provet har nu avslutats. Du kan st\u00e4nga detta f\u00f6nster.", "control.confirm leave": "Du har inte avslutat ditt prov", "control.not all questions answered": "Du har inte svarat p\u00e5 alla fr\u00e5gor i detta prov.", "control.not all questions submitted": "Du har \u00e4ndrat n\u00e5got eller n\u00e5gra svar men inte sparat dina \u00e4ndringar. Var sn\u00e4ll och se s\u00e5 alla fr\u00e5gor har sparats.", "control.confirm end": "\u00c4r du s\u00e4ker p\u00e5 att du vill avsluta ditt prov? Efter du avslutat kan du inte g\u00e5 tillbaka och \u00e4ndra dina svar.", "control.confirm regen": "Vill du skapa en ny version av fr\u00e5gan? Om du v\u00e4ljer OK f\u00f6rsvinner alla dina eventuella svar och po\u00e4ng samt fr\u00e5gan \u00e4ndras.", "control.confirm reveal": "Vill du visa svaret till denna fr\u00e5ga? Eventuella po\u00e4ng kommer l\u00e5sas samt du kommer inte kunna svara p\u00e5 fr\u00e5gan i efterhand. ", "control.proceed anyway": "Forts\u00e4tt \u00e4nd\u00e5?", "control.regen": "F\u00f6rs\u00f6k med en liknande fr\u00e5ga", "control.submit answer": "Skicka in svar", "control.submit all parts": "Skicka in alla delar", "control.submit again": "Skicka in igen", "control.submit": "Skicka", "control.previous": "F\u00f6reg\u00e5ende", "control.next": "N\u00e4sta", "control.advice": "R\u00e5d", "control.reveal": "Visa svar", "control.total": "Totalpo\u00e4ng", "control.pause": "Paus", "control.end exam": "Avsluta prov", "control.back to results": "G\u00e5 tillbaka till resultat", "display.part.jme.error making maths": "Ett fel uppstod n\u00e4r math display skulle skapas", "exam.xml.bad root": "Rot elementet av prov XML borde vara 'prov'", "exam.changeQuestion.no questions": "Detta prov inneh\u00e5ller inga fr\u00e5gor! Unders\u00f6k .exam filen f\u00f6r att hitta fel.", "feedback.you were awarded": "Du har f\u00e5tt <strong>{{count,niceNumber}}</strong> $t(mark).", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) tagits bort", "jme.tokenise.invalid": "Ogiltigt uttryck: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "inte nog med argument f\u00f6r operationen {{op}}", "jme.shunt.no left bracket in function": "Ingen matchande v\u00e4nster-bracket i funktion eller serie", "jme.shunt.no left square bracket": "Ingen matchande v\u00e4nster-bracket", "jme.shunt.no left bracket": "Ingen matchande v\u00e4nster-bracket", "jme.shunt.no right bracket": "Ingen matchande h\u00f6ger-bracket", "jme.shunt.no right square bracket": "Ingen matchande h\u00f6ger-klammer som avslutar listan", "jme.shunt.missing operator": "Uttrycket kan inte utv\u00e4rderas -- Det saknas en operator.", "jme.typecheck.function maybe implicit multiplication": "Operationen {{name}} \u00e4r inte definierad. Menade du <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Operationen {{op}} \u00e4r inte definierad. Menade du <br/><code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operationen {{op}} \u00e4r inte definierad.", "jme.typecheck.no right type definition": "Ingen definition av '{{op}}' av korrekt typ hittades", "jme.typecheck.no right type unbound name": "Variabeln <code>{{name}}</code> \u00e4r inte definierad.", "jme.typecheck.map not on enumerable": "<code>map</code> operationen m\u00e5ste arbeta med en lista eller ett intervall, inte {{type}}", "jme.evaluate.undefined variable": "Variabeln {{name}} \u00e4r odefinierad", "jme.thtml.not html": "THTML konstrukt\u00f6ren fick ett icke HTML v\u00e4rde.", "jme.func.switch.no default case": "Inget standardstatement fall f\u00f6r switch uttryck", "jme.func.listval.invalid index": "index f\u00f6r lista \u00e4r ogiltig  {{index}} p\u00e5 lista av storlek {{size}}", "jme.func.listval.not a list": "Objektet \u00e4r inte prenumereringsbart", "jme.func.matrix.invalid row type": "Kan inte konstruera ett matrix fr\u00e5n rader av text {{type}}", "jme.func.except.continuous range": "Kan inte anv\u00e4nda 'except' operatorn p\u00e5 kontinuerliga intervall", "jme.matrix.reports bad size": "Matrisen visar fel storlek - det m\u00e5ste vara n\u00e5got fel i funtionen som konstruerade den.", "jme.texsubvars.no right bracket": "Inga matchande <code>]</code> i {{op}} argumenten.", "jme.texsubvars.missing parameter": "Parameter saknas i {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Ingen matchande <code>}</code> in {{op}}", "jme.user javascript.error": "Fel i anv\u00e4ndar-definierad javascript funktion <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Del uppstod n\u00e4r funktionen skapades <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Syntaxfel i funktionens definition", "jme.variables.variable not defined": "Variable <code>{{name}}</code> is not defined.", "jme.variables.empty definition": "Variabelns definition <code>{{name}}</code> \u00e4r tom.", "jme.variables.circular reference": "Circular variable reference in definition of <code>{{name}}</code>", "jme.variables.error computing dependency": "Fel i ber\u00e4kning av referens variabeln <code>{{name}}</code>", "jme.variables.error evaluating variable": "Fel i evaluering av variabel {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Ett set av fr\u00e5gevariabler kunde inte genereras i tid", "jme.display.unknown token type": "Kan inte textifiera token skrift {{type}}", "jme.display.collectRuleset.no sets": "Inga sets \u00e4r givna till Samlade-Regler-setet!", "jme.display.collectRuleset.set not defined": "Regelsamlingen {{name}} har inte definierats", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplify Tr\u00e4det m\u00e5ste ges en omfattning", "math.precround.complex": "Kan inte avrunda till ett komplext tal", "math.siground.complex": "Kan inte avrunda till ett komplext tal av signifikant v\u00e4rde", "math.combinations.complex": "Kan inte ber\u00e4kna kombinationen av komplexa tal", "math.permutations.complex": "Kan inte ber\u00e4kna permutationer av komplexa tal", "math.gcf.complex": "Kan inte ber\u00e4kna SGF av komplexa tal ", "math.lcm.complex": "Kan inte ber\u00e4kna MGM av komplexa tal ", "math.lt.order complex numbers": "Kan inte sortera complexa tal", "math.choose.empty selection": "tom sektion given till random-funktionen ", "matrixmath.abs.non-square": "Kan inte ber\u00e4kna determinanten av en matrix som inte \u00e4r fyrkantig", "matrixmath.abs.too big": "Urs\u00e4kta, systemet kan inte ber\u00e4kna determinanter av matriser st\u00f6rre \u00e4n 3x3.", "matrixmath.mul.different sizes": "Kan inte multiplicera matriser av olika storlekar.", "vectormath.cross.not 3d": "Kan bara ta kryssprodukten av tre dimensions vektorer.", "vectormath.dot.matrix too big": "Kan inte ber\u00e4kna dot produkten av en matrix som inte \u00e4r $1 \\times N$ eller $N \\times 1$.", "vectormath.cross.matrix too big": "Kan inte ber\u00e4kna kors produkten av en matrix som inte \u00e4r $1 \\g\u00e5nger N$ eller $N \\g\u00e5nger 1$.", "part.with steps answer prompt": "Svar: ", "part.script.error": "Fel i del {{path}} script {{script}}: {{-message}}", "part.marking.steps no matter": "Eftersom du redan r\u00e4ttat delen kan du inte f\u00e5 po\u00e4ng f\u00f6r nya svar.", "part.marking.revealed steps no penalty": "Du har \u00f6ppnat steg l\u00f6sningen.", "part.marking.used variable replacements": "Denna del r\u00e4ttades enligt dina tidigare svar", "part.marking.variable replacement part not answered": "Du m\u00e5ste svara p\u00e5 {{part}} f\u00f6rst", "part.marking.resubmit because of variable replacement": "Denna dels po\u00e4ng beror p\u00e5 svar p\u00e5 andra delar, dessa svar \u00e4r \u00e4ndrade s\u00e5 spara igen f\u00f6r att uppdatera din po\u00e4ng.", "part.marking.not submitted": "Inget svar inskickat", "part.marking.did not answer": "Du svarade inte p\u00e5 denna fr\u00e5ga", "part.marking.nothing entered": "Du har inte skrivit in ett svar.", "part.marking.incorrect": "Ditt svar \u00e4r inkorrekt.", "part.marking.correct": "Ditt svar \u00e4r korrekt.", "part.marking.uncaught error": "Fel i skapning av {{part}}: {{-message}}", "part.marking.no result": "Denna del kunde inte r\u00e4ttas", "part.correct answer": "F\u00f6rv\u00e4ntat svar.", "part.missing type attribute": "{{part}}: det saknas en attribut", "part.unknown type": " {{part}}: Icke-l\u00e4sbar del {{type}}", "part.setting not present": "Egenskap '{{property}}' inte satt", "part.jme.answer missing": "Korrekt svar saknas", "part.jme.answer too long": "Ditt svar \u00e4r f\u00f6r l\u00e5ngt", "part.jme.answer too short": "Ditt svar \u00e4r f\u00f6r kort", "part.jme.answer invalid": "Ditt svar \u00e4r inte ett korrekt matematiskt uttryck.<br/>{{-message}}.", "part.jme.marking.correct": "Ditt svar \u00e4r numeriskt korrekt.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "Ditt svar m\u00e5ste inneh\u00e5lla: {{strings}}", "part.jme.must-have several": "Ditt svar m\u00e5ste inneh\u00e5lla alla: {{strings}}", "part.jme.not-allowed bits": " <span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "Ditt svar kan inte inneh\u00e5lla: {{strings}}", "part.jme.not-allowed several": "Ditt svar kan inte inneh\u00e5lla n\u00e5gon av: {{strings}}", "part.jme.unexpected variable name": "Ditt svar verkar inneh\u00e5lla en ogiltigt variabelnamn <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "Ditt svar verkar inneh\u00e5lla en ogiltigt variabelnamn <code>{{name}}</code>. Menade du <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "visat svar saknas", "part.patternmatch.correct except case": "Ditt svar \u00e4r korrekt, f\u00f6rutom enheten.", "part.numberentry.correct except decimal": "Ditt svar \u00e4r i r\u00e4tt intervall, dock till\u00e5ts inte decimaler.", "part.numberentry.correct except fraction": "Ditt svar \u00e4r i r\u00e4tt intervall, dock till\u00e5ts inte br\u00e5k.", "part.numberentry.answer invalid": "Du skrev inte en siffra", "part.numberentry.answer not integer": "Ditt svar \u00e4r felaktigt. Du m\u00e5ste anv\u00e4nda positiva heltal, inte decimaler.", "part.numberentry.answer not integer or decimal": "Ditt svar \u00e4r felaktigt. Du m\u00e5ste anv\u00e4nda heltal eller decimaler.", "part.numberentry.zero sig fig": "Du kan inte avrunda ett svar till noll v\u00e4rdesiffror.", "part.mcq.options def not a list": "Uttrycket som definierar {{properties}} \u00e4r inte en lista.", "part.mcq.marking matrix string empty": "R\u00e4ttnings-matrisen \u00e4r tom", "part.mcq.choices missing": "Definition av valen saknas.", "part.mcq.matrix not a number": "Del {{part}} r\u00e4ttnings-matrisen cell ({{row}},{{column}}) blir inget nummer", "part.mcq.wrong number of choices": "Du valde fel antal svar.", "part.mcq.no choices selected": "Inga svar \u00e4r valda.", "part.mcq.matrix not a list": "R\u00e4ttnings-matrisen, definierad av JME uttrycket, m\u00e5ste vara en lista.", "part.mcq.matrix wrong type": "Element av ogiltig typ '{{type}}' anv\u00e4nds i r\u00e4ttnings-matrisen", "part.mcq.matrix mix of numbers and lists": "En blandning av siffror och listor anv\u00e4nds i r\u00e4ttnings-matrisen.", "part.mcq.matrix wrong size": "Matrisen \u00e4r av fel storlek", "part.mcq.correct choice": "Du valde r\u00e4tt svar.", "part.matrix.answer invalid": "Ditt svar \u00e4r inte giltigt.", "part.matrix.invalid cell": "En eller fler av cellerna i ditt svar \u00e4r tomma eller ogiltiga.", "part.matrix.some incorrect": "En eller flera av cellerna i ditt svar \u00e4r inte korrekta, Du har dock f\u00e5tt po\u00e4ng f\u00f6r resterande svar.", "part.matrix.empty": "Du har inte skrivit n\u00e5got svar.", "part.matrix.empty cell": "En eller fler celler i ditt svar \u00e4r tomma.", "part.matrix.size mismatch": "Dimensionerna p\u00e5 svaret \u00e4r inte \u00f6ppna f\u00f6r \u00e4ndring, det korrekta dimensionen \u00e4r: \n{{correct_dimensions}} medan svaret givet \u00e4r: {{input_dimensions}}", "part.gapfill.feedback header": "<strong>Mellanrum {{index}}</strong>", "part.extension.not implemented": "Delen har inte anv\u00e4nt <code>{{name}}</code> metoden.", "question.loaded name mismatch": "Kan inte \u00e5teruppta detta f\u00f6rs\u00f6k. objektet har \u00e4ndrats sen du arbetade med det sist.", "question.error": "Fr\u00e5ga {{number}}: {{-message}}", "question.preamble.error": "Fel i inledningen: {{-message}}", "question.preamble.syntax error": "Syntax error i inledningen", "question.unsupported part type": "Ogiltig del typ", "question.header": "Fr\u00e5ga {{number}}", "question.submit part": "Spara del", "question.show steps": "Visa steg", "question.show steps penalty": "Du kommer f\u00f6rlora <strong>{{count,niceNumber}}</strong> $t(mark).", "question.show steps no penalty": "Dina po\u00e4ng kommer inte p\u00e5verkas.", "question.show steps already penalised": "Du har redan visat stegen. Du kan nu \u00f6ppna dem igen utan po\u00e4ngavdrag.", "question.hide steps": "G\u00f6m steg", "question.hide steps no penalty": "Dina po\u00e4nk kommer inte p\u00e5verkas.", "question.advice": "Tips", "question.no such part": "Kan inte hitta del {{path}}", "question.can not submit": "Kan inte spara svar - unders\u00f6k eventuella fel", "question.answer submitted": "Svar sparat", "question.score feedback.show": "Visa Feedback", "question.score feedback.hide": "G\u00f6m feedback", "question.score feedback.answered total actual": "Po\u00e4ng: {{score,niceNumber}}/{{marks,niceNumber}}", "question.score feedback.answered total": "{{marksString}}. Besvarade.", "question.score feedback.answered actual": "Po\u00e4ng: {{scoreString}}", "question.score feedback.answered": "Besvarad.", "question.score feedback.unanswered": "Obesvarad.", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Ditt svar \u00e4r korrekt", "question.score feedback.partial": "Ditt svar \u00e4r delvis korrekt", "question.score feedback.wrong": "Ditt svar \u00e4r inkorrekt", "question.selector.unsubmitted changes": "Ickesparade \u00e4ndringar", "timing.no accumulator": "Ingen tids-ackumulator {{name}}", "timing.time remaining": "Tid Kvar:", "xml.could not load": "Kunder inte ladda XML dokumentet: {{-message}}", "xml.property not number": "Egenskap {{name}} borde vara en siffra, men \u00e4r det inte ({{value}}), i node {{element}}", "xml.property not boolean": "Egenskap {{name}} borde vara en boolean, men \u00e4r det inte ({{value}}), i node {{element}}", "xml.error in variable definition": "En variabel kunde inte definieras:  <code>{{name}}</code>", "scorm.error initialising": "Ett fel uppstod i SCORM protokollet: {{-message}}", "scorm.failed save": "<p> Sparnings-f\u00f6rfr\u00e5gan har misslyckats. Tryck <b>OK</b> f\u00f6r att f\u00f6rs\u00f6ka igen.</p> <p>Om du vid upprepade tillf\u00e4llen f\u00e5r detta meddelande kontrollera din internet-uppkoppling eller dator. Dina f\u00f6reg\u00e5ende sparade svar \u00e4r kvar och kan laddas p\u00e5 en annan dator</p>\n<p>Om du har problem med att svara <em>any</em> var sn\u00e4ll och kontakta din l\u00e4rare f\u00f6r hj\u00e4lp.</p>", "scorm.no exam suspend data": "Kunde inte \u00e5teruppta: ingen tidigare data hittades.", "scorm.error loading suspend data": "Fel i laddning av data: {{-message}}", "scorm.error loading question": "Ett fel uppstod vid laddning av fr\u00e5ga {{number}}: {{-message}}", "scorm.no question suspend data": "Ingen fr\u00e5ga, stannar", "scorm.error loading part": "Fel i laddning av del {{part}}: {{-message}}", "scorm.no part suspend data": "Ingen del, stannar", "util.product.non list": "En icke lista gavs till <code>Numbas.util.product</code>", "mark": "po\u00e4ng", "was": "var", "part": "del", "gap": "mellanrum", "step": "steg", "jme.substituteTree.undefined variable": "Odefinierad variabel: <code>{{name}}</code>", "jme.user javascript.returned undefined": "Javascript funktionen gav en felkod: <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "Du fick  <strong>{{count,niceNumber}}</strong> $t(mark) po\u00e4ng f\u00f6r dina svar p\u00e5 denna del.", "part.marking.revealed steps with penalty": "Du visade l\u00f6snings-stegen. Ditt maxpo\u00e4ng f\u00f6r denna delen \u00e4r <strong>{{count,niceNumber}}</strong> $t(mark). Dina po\u00e4ng skalas ned p\u00e5 grund av l\u00f6sningen.", "part.marking.total score": "Du fick <strong>{{count,niceNumber}}</strong> $t(mark) po\u00e4ng f\u00f6r denna del.", "part.numberentry.precision type.dp": "decimal", "part.numberentry.precision type.dp_plural": "decimaler", "part.numberentry.precision type.sigfig": "v\u00e4rdesiffra", "part.numberentry.precision type.sigfig_plural": "v\u00e4rdesiffror", "part.numberentry.give your answer to precision": "Avrunda ditt svar till {{count,niceNumber}} {{precisionType}}.", "question.unsubmitted changes": "Du har \u00e4ndrat svaret men inte sparat. Var sn\u00e4ll och <strong>Spara svar</strong> button. efter att du kontrollerat svaret.", "question.unsubmitted changes_plural": "Du har \u00e4ndrat svaret men inte sparat. Var sn\u00e4ll och <strong>Spara svar</strong> button. efter att du kontrollerat svaret till alla delar.", "util.equality not defined for type": "Ekvivalens \u00e4r inte definierat f\u00f6r text {{type}}", "mark_plural": "po\u00e4ng", "was_plural": "var", "die.script not loaded": "Numbas kunde inte startas f\u00f6r att filen <code>{{file}}</code> inte laddades. Unders\u00f6k om filen finns med i <code>scripts.js</code>.", "math.combinations.n less than zero": "Kan inte ber\u00e4kna kombinationer: n \u00e4r mindre \u00e4n 0", "math.combinations.k less than zero": "Kan inte ber\u00e4kna kombinationer: k \u00e4r mindre \u00e4n 0", "math.combinations.n less than k": "Kan inte ber\u00e4kna kombinationer: n \u00e4r mindre \u00e4n k", "math.permutations.n less than zero": "Kan inte ber\u00e4kna permutationer: n \u00e4r mindre \u00e4n 0", "math.permutations.k less than zero": "Kan inte ber\u00e4kna permutationer: k \u00e4r mindre \u00e4n 0", "math.permutations.n less than k": "Kan inte ber\u00e4kna permutationer: n \u00e4r mindre \u00e4n k", "part.numberentry.give your answer to precision_0": "Avrunda dina svar till n\u00e4rmsta heltal.", "mathjax.error": "Ett fel uppstod i MathJax {{context}}: {{-message}}", "mathjax.error with context": "MathJax felaktig process i {{context}}: {{-message}}", "exam.introduction": "Prov introduktion", "exam.feedback": "Prov, feedback meddelande", "jme.tokenise.keypair key not a string": "Ordboks nyckeln b\u00f6r vara en \"string\" inte {{type}}.", "jme.shunt.list mixed argument types": "Kan inte f\u00f6rst\u00e5 {{mode}}: en mix av olika typer av element (listor och ord).", "jme.func.listval.key not in dict": "Ordboken inneh\u00e5ller inte nyckeln  <code>{{key}}</code>", "part.prompt": "Meddelande", "part.feedback": "Feedback", "part.numberentry.answer not reduced": "Ditt svar \u00e4r inte i enklaste form.", "part.numberentry.give your answer as a reduced fraction": "F\u00f6renkla ditt svar till enklaste form.", "part.numberentry.negative decimal places": "Denna del avrundar studentens svar till ett negativt nummer decimaler. Ogiltigt kommando.", "part.mcq.choices": "Val", "part.mcq.answers": "Svar", "part.mcq.matrix cell empty": "Del {{part}} r\u00e4ttnings-matris cell ({{row}},{{column}}) \u00e4r tom.", "part.mcq.matrix jme error": "Del {{part}} r\u00e4ttnings-matris cell ({{row}},{{column}}) ger ett JME error: {{error}}", "question.statement": "P\u00e5st\u00e5ende", "ruleset.circular reference": "Cirkul\u00e4r referens i definitionen av regelsetet <code>{{name}}</code>", "ruleset.set not defined": "refelsetet {{name}} har inte definierats", "jme.evaluate.no scope given": "Numbas.jme.evaluate m\u00e5ste ges ett intervall.", "answer.number.not a number": "Your answer is not a valid number.", "answer.number.fractions not allowed": "You may not enter a fraction.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "You may not enter fractions.", "answer.matrix.some cell not a number": "One or more of the cells in your answer is not a valid number.", "exam.enter password": "Password:", "exam.password.correct": "This password is correct. You can start the exam.", "exam.password.incorrect": "This password is incorrect.", "frontpage.scorm.lms not connected": "This exam is running in standalone mode. Your answers and marks will not be saved!", "result.question review": "Review", "control.confirm regen no marks": "Would you like to re-randomise this question?", "control.confirm reveal no marks": "Would you like to reveal the answer to this question?", "jme.tokenise.invalid near": "Invalid expression: <code>{{expression}}</code> at position {{position}} near <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Invalid object passed into number constructor.", "jme.subvars.null substitution": "Empty variable substitution: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "The data type {{type}} has already been registered so can't be registered again.", "jme.type.no cast method": "Can't automatically convert from {{from}} to {{to}}.", "jme.display.simplifyTree.empty expression": "Expression is empty", "jme.display.simplifyTree.stuck in a loop": "Simplifier is stuck in a loop: <code>{{expr}}</code>", "math.niceNumber.undefined": "Was expecting a number, but got <code>undefined</code>", "math.rangeToList.zero step size": "Can't convert a range with step size zero to a list.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "You revealed the steps.", "part.marking.maximum scaled down": "The maximum you can score for this part is <strong>{{count,niceNumber}}</strong> $t(mark). Your scores will be scaled down accordingly.", "part.marking.minimum score applied": "The minimum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "The maximum score for this part is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "There was an error in this part's marking algorithm. Please report this. {{-message}}", "part.marking.no result after replacement": "This part could not be marked using your answers to previous parts.", "part.marking.missing required note": "The marking algorithm does not define the note <code>{{note}}</code>", "marking.apply.not a list": "The first argument to <code>apply</code> must be a list, and isn't", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> not found", "marking.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "marking.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "marking.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "marking.note.empty expression": "The note <code>{{name}}</code> is empty.", "marking.script.error parsing notes": "Error parsing marking script: {{- message}}", "part.feedback out of date": "This feedback is based on your last submitted answer. Save your changed answer to get updated feedback.", "part.jme.invalid value generator expression": "Invalid value generator expression for variable <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "You chose an incorrect answer.", "part.matrix.not all cells same precision": "You have not given every cell in your answer to the same precision.", "part.gapfill.error marking gap": "Error marking {{name}}: {{-message}}", "part.custom.empty setting": "No value given.", "part.custom.unrecognised input type": "Unrecognised setting type <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Error evaluating input option <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "Definition of input option <code>{{option}}</code> is missing.", "part.custom.error evaluating setting": "Error evaluating setting <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Error while creating question {{number}}: {{-message}}", "question.score feedback.not marked": "Not marked", "question.score feedback.partially answered": "Partially answered", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Po\u00e4ng: {{scoreString}}", "question.score feedback.score total actual": "Po\u00e4ng: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Error in definition of variable <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Couldn't load the extension <code>{{name}}</code>.", "control.toggle navigation menu": "Toggle the navigation menu", "part.input title": "Answer for part {{name}}", "part.correct answer title": "Expected answer for part {{name}}", "part.jme.must-match.failed": "Your answer is not in the expected form.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>One or more subsequent parts depend on your answer to this part. Submitting this part again will invalidate those parts, and remove them from the question. This cannot be undone.</p>\n<p>Would you like to submit this part again?</p>", "control.back to menu": "Go back to the menu", "display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "A question variable has not been given a name.", "jme.calculus.unknown derivative": "Don't know how to differentiate <code>{{tree}}</code>", "math.order complex numbers": "Can't order complex numbers", "menu.choose a question": "Choose a question.", "part.choose next part.answered": "What do you want to do next?", "part.choose next part.unanswered": "Or, you could:", "part.choose next part.will be locked": "(This part will be locked)", "part.reached dead end": "There's nothing more to do from here.", "part.next part.penalty amount": "(lose {{count}} $t(mark))", "part.marking.counts towards objective": "This part counts towards the objective <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Your answer is invalid. You must enter an integer, a decimal or a fraction.", "question": "Question", "question.progress": "Question progress", "question.score feedback.unattempted": "Not attempted", "question.score feedback.attempted": "Attempted", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Objectives", "question.penalties": "Penalties", "question.back to previous part": "Go back to the previous part", "end.print": "Print your exam transcript", "math.shuffle_together.lists not all the same length": "Not all lists are the same length.", "jme.parse signature.invalid signature string": "Invalid function signature string: {{str}}", "part.custom.expected answer has wrong type": "The expected answer for this part has the wrong type. It should be <code>{{shouldbe}}</code>.", "part.custom.input option has wrong type": "The answer input setting <code>{{option}}</code> has the wrong type. It should be <code>{{shouldbe}}</code>.", "matrix input.size control legend": "Size", "matrix input.rows": "Rows", "matrix input.columns": "Columns", "part.jme.error checking numerically": "There was an error numerically checking your answer: {{-message}}", "part.gapfill.cyclic adaptive marking": "There is a cycle in the adaptive marking for this part: <strong>{{name1}}</strong> relies on <strong>{{name2}}</strong>, which eventually relies on <strong>{{name1}}</strong>.", "modal.style.background colour": "Background colour", "modal.style.text colour": "Text colour", "modal.style.text size": "Text size", "modal.style.explanation": "Use these controls to change the appearance of the exam.", "modal.style.reset to defaults": "Reset to defaults", "modal.style.text size preview": "Most text will be this big.", "control.style options": "Display options", "part.marking.partially correct": "Your answer is partially correct.", "part.marking.error in adaptive marking": "There was an error in the adaptive marking for this part. Please report this. {{-message}}", "page.skip to content": "Skip to content", "result.learning objective": "Learning objective", "jme.interpreted as": "interpreted as", "jme.script.note.compilation error": "Error compiling note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error evaluating note <code>{{name}}</code> - {{-message}}", "jme.script.note.invalid definition": "Invalid note definition: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "You might be missing a colon after the name and description", "jme.script.note.invalid definition.description missing closing bracket": "You might be missing a closing bracket", "jme.script.note.empty expression": "The note <code>{{name}}</code> is empty.", "jme.script.error parsing notes": "Error parsing marking script: {{- message}}", "matrix input.cell label": "Row {{row}}, column {{column}}", "control.move to next question": "Move to the next question", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}, "nl-nl": {"translation": {"page.loading": "Bezig met laden ...", "page.saving": "<p>Gegevens worden opgeslagen.</p>\n<p>Dit kan even duren.</p>", "mathjax.math processing error": "MathJax processing error: \"{{-message}}\" when texifying <code>{{expression}}</code>", "die.numbas failed": "Er is een fout opgetreden", "die.sorry": "Sorry, Numbas is een probleem tegengekomen en kan niet verder. Hieronder staat een beschrijving van het probleem.", "die.error": "Probleem", "modal.ok": "OK", "modal.cancel": "Annuleren", "exam.exam name": "Naam toets:", "exam.random seed": "Sessie ID:", "exam.student name": "Naam:", "exam.number of questions": "Aantal vragen:", "exam.marks available": "Punten beschikbaar:", "exam.pass percentage": "Geslaagd bij:", "exam.time allowed": "Beschikbare tijd:", "exam.passed": "Gehaald", "exam.failed": "Niet gehaald", "exam.review header": "Review: ", "frontpage.start": "Start", "suspend.paused header": "Gepauzeerd", "suspend.exam suspended": "De toets is onderbroken. Klik Ga door om verder te gaan.", "suspend.you can resume": "Volgende keer dat je de toets opstart kun je verder gaan met deze sessie.", "suspend.resume": "Ga door", "result.exit": "Exit", "result.print": "Print dit overzicht van de resultaten", "result.exam summary": "Overzicht toets", "result.performance summary": "Overzicht van de resultaten", "result.exam start": "Start toets:", "result.exam stop": "Einde toets:", "result.time spent": "Tijd:", "result.questions attempted": "Aantal gemaakte vragen:", "result.score": "Score:", "result.result": "Resultaat:", "result.question number": "Vraag nummer", "result.question score": "Score", "result.question review title": "Review deze vraag", "result.click a question to review": "Klik op een vraagnummer om je scores en eventuele uitwerkingen te zien.", "end.exam has finished": "De toets is afgelopen. Je kunt het venster sluiten.", "control.confirm leave": "De toets is nog niet af.", "control.not all questions answered": "Je hebt niet alle vragen in deze toets beantwoord.", "control.not all questions submitted": "Je hebt een of meer antwoorden gewijzigd maar ze nog niet verzonden. Controleer of elk antwoord verzonden is.", "control.confirm end": "Weet je zeker dat je de toets wilt afsluiten? Als je afsluit kun je geen antwoorden meer wijzigen.", "control.confirm regen": "Weet je zeker dat je deze vraag opnieuw wilt maken? Als je opnieuw begint verlies je je huidige score.", "control.confirm reveal": "Wil je het antwoord zien? Je verliest je huidige score en kunt de vraag later niet opnieuw beantwoorden.", "control.proceed anyway": "Doorgaan?", "control.regen": "Probeer soortgelijke vraag opnieuw", "control.submit answer": "Verzend", "control.submit all parts": "Verzend alles", "control.submit again": "Verzend opnieuw", "control.submit": "Verzend", "control.previous": "Vorige", "control.next": "Volgende", "control.advice": "Uitwerking", "control.reveal": "Laat zien", "control.total": "Totaal", "control.pause": "Pauze", "control.end exam": "Klaar", "control.back to results": "Ga terug naar resultaten", "control.move to next question": "Move to the next question", "display.part.jme.error making maths": "Fout tijdens weergeven formule", "exam.xml.bad root": "Top element van toets XML moet 'exam' zijn", "exam.changeQuestion.no questions": "Deze toets bevat geen vragen! Controleer het .exam bestand op fouten.", "feedback.you were awarded": "Je hebt <strong>{{count}}</strong> $t(mark) gekregen.", "feedback.taken away": "<strong>{{count,niceNumber}}</strong> $t(mark) $t(was) afgenomen.", "jme.tokenise.invalid": "Ongeldige formule: <code>{{expression}}</code>", "jme.shunt.not enough arguments": "Te weinig argumenten voor operator <code>{{op}}</code>", "jme.shunt.no left bracket in function": "Linker haakje ontbreekt in functieaanroep of rijtje", "jme.shunt.no left square bracket": "Linker haakje ontbreekt", "jme.shunt.no left bracket": "Linker haakje ontbreekt", "jme.shunt.no right bracket": "Rechter haakje ontbreekt", "jme.shunt.no right square bracket": "Rechter blokhaak ontbreekt aan het einde van de lijst", "jme.shunt.missing operator": "Formule kan niet berekend worden -- operator ontbreekt.", "jme.typecheck.function maybe implicit multiplication": "Operator {{name}} is niet gedefinieerd. Bedoel je <br/><code>{{first}}*{{possibleOp}}(...)</code>?", "jme.typecheck.function not defined": "Operator {{op}} is niet gedefinieerd. Bedoel je <br/><code>{{suggestion}}*(...)</code>?", "jme.typecheck.op not defined": "Operator '{{op}}' is niet gedefinieerd.", "jme.typecheck.no right type definition": "Geen definitie van '{{op}}' bekend van het juiste type.", "jme.typecheck.no right type unbound name": "Variabele <code>{{name}}</code> is niet gedefinieerd.", "jme.typecheck.map not on enumerable": "<code>map</code> operator vereist een list of range, niet {{type}}", "jme.evaluate.undefined variable": "Variabele {{name}} is niet gedefinieerd", "jme.thtml.not html": "Passed een niet-HTML waarde in de THML constructor", "jme.func.switch.no default case": "Default case van Switch statement ontbreekt", "jme.func.listval.invalid index": "Ongeldige list index {{index}} voor een list van grootte {{size}}", "jme.func.listval.not a list": "Object is niet indexeerbaar", "jme.func.matrix.invalid row type": "Een matrix kan niet gemaakt worden uit rijen van type {{type}}", "jme.func.except.continuous range": "De 'except' operator kan niet gebruikt worden op continue intervallen.", "jme.matrix.reports bad size": "Matrix afmetingen incorrect - moet fout in constructor functie zijn", "jme.texsubvars.no right bracket": "Geen passend <code>]</code> in {{op}} argumenten.", "jme.texsubvars.missing parameter": "Ontbrekende parameter in {{op}}: {{parameter}}", "jme.texsubvars.no right brace": "Geen passend <code>}</code> in {{op}}", "jme.user javascript.error": "Fout in javascript functie <code>{{name}}</code>: {{-message}}", "jme.variables.error making function": "Fout tijdens genereren functie <code>{{name}}</code>: {{-message}}", "jme.variables.syntax error in function definition": "Syntax error in functie definitie", "jme.variables.variable not defined": "Variabele <code>{{name}}</code> is niet gedefinieerd.", "jme.variables.empty definition": "Definitie van variabele <code>{{name}}</code> is leeg.", "jme.variables.circular reference": "Circulaire verwijzing naar variabele in <code>{{name}}</code>", "jme.variables.error computing dependency": "Fout tijdens het berekenen van variabele <code>{{name}}</code>", "jme.variables.error evaluating variable": "Fout bij berekenen variabele {{name}}: {{-message}}", "jme.variables.question took too many runs to generate variables": "Een geldige set vraag variabelen werd niet gegenereerd in de tijd.", "jme.display.unknown token type": "Kan token van type {{type}} niet naar TeX vertalen", "jme.display.collectRuleset.no sets": "Geen sets opgegeven aan collectRuleset!", "jme.display.collectRuleset.set not defined": "Ruleset {{name}} is niet gedefinieerd", "jme.display.simplifyTree.no scope given": "Numbas.jme.display.simplifyTree vereist een Scope", "math.precround.complex": "Afronden op een complex aantal decimalen is niet mogelijk", "math.siground.complex": "Afronden op een complex aantal significante cijfers is niet mogelijk", "math.combinations.complex": "Combinaties van complexe getallen kunnen niet berekend worden", "math.permutations.complex": "Permutaties van complexe getallen kunnen niet berekend worden", "math.gcf.complex": "De GGD van complexe getallen kan niet berekend worden", "math.lcm.complex": "Het KGV van complexe getallen kan niet berekend worden", "math.lt.order complex numbers": "Complexe getallen kunnen niet geordend worden", "math.choose.empty selection": "Een lege selectie gebruikt voor de random functie", "matrixmath.abs.non-square": "De determinant van een niet-vierkante matrix kan niet berekend worden.", "matrixmath.abs.too big": "Sorry, de determinant van een matrix groter dan 3x3 kan nog niet berekend worden.", "matrixmath.mul.different sizes": "Matrices van verschillende grootte kunnen niet vermenigvuldigd worden.", "vectormath.cross.not 3d": "Uitproduct is alleen te berekenen voor 3D-vectoren.", "vectormath.dot.matrix too big": "Het inproduct is alleen te berekenen voor een matrix met afmetingen $1 \\times N$ of $N \\times 1$.", "vectormath.cross.matrix too big": "Uitproduct is alleen te berekenen voor een matrix met afmetingen $1 \\times N$ of $N \\times 1$.", "part.with steps answer prompt": "Antwoord: ", "part.script.error": "Fout in deel {{path}} custom script {{script}}: {{-message}}", "part.marking.steps no matter": "Omdat je al alle punten voor dit onderdeel hebt leveren de afzonderlijke stappen geen punten meer op.", "part.marking.revealed steps no penalty": "Je hebt de stappen bekeken.", "part.marking.used variable replacements": "Dit deel is gescoord op basis van eerder gegeven antwoorden.", "part.marking.variable replacement part not answered": "Je moet {{part}} eerst beantwoorden", "part.marking.resubmit because of variable replacement": "De score van dit onderdeel hangt af van eerder gegeven antwoorden die je nu hebt gewijzigd. Verzend dit deel opnieuw om je score te updaten.", "part.marking.not submitted": "Geen antwoord verzonden", "part.marking.did not answer": "Je hebt deze vraag niet beantwoord.", "part.marking.nothing entered": "Je hebt geen antwoord ingevuld.", "part.marking.incorrect": "Je antwoord is fout.", "part.marking.correct": "Je antwoord is juist.", "part.marking.uncaught error": "Fout bij scoren van {{part}}: {{-message}}", "part.marking.no result": "Dit onderdeel kan niet gescoord worden.", "part.correct answer": "Goede antwoord:", "part.missing type attribute": "{{part}}: Onderdeeltype ontbreekt", "part.unknown type": "{{part}}: Onderdeeltype {{type}} is onbekend", "part.setting not present": "Property '{{property}}' onbekend", "part.jme.answer missing": "Goede antwoord voor een JME onderdeel ontbreekt (%s)", "part.jme.answer too long": "Je antwoord is te lang.", "part.jme.answer too short": "Je antwoord is te kort.", "part.jme.answer invalid": "Je antwoord is geen geldige formule.<br/>{{-message}}", "part.jme.marking.correct": "Je antwoord heeft de juiste waarde.", "part.jme.must-have bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.must-have one": "In je antwoord moet {{strings}} voorkomen", "part.jme.must-have several": "In je antwoord moet {{string}} allemaal voorkomen", "part.jme.not-allowed bits": "<span class=\"monospace\">{{string}}</span>", "part.jme.not-allowed one": "In je antwoord mag {{strings}} niet voorkomen", "part.jme.not-allowed several": "In je antwoord mag niets uit {{strings}} voorkomen", "part.jme.unexpected variable name": "In je antwoord gebruik je de onbekende variabelenaam <code>{{name}}</code>.", "part.jme.unexpected variable name suggestion": "In je antwoord gebruik je de onbekende variabelenaam <code>{{name}}</code>. Bedoel je <code>{{suggestion}}</code>?", "part.patternmatch.display answer missing": "Display answer ontbreekt in het Pattern Match onderdeel (%s)", "part.patternmatch.correct except case": "Je antwoord is goed, maar je haalt hoofd- en kleine letters door elkaar.", "part.numberentry.correct except decimal": "Je antwoord is goed maar mag geen decimaal getal zijn.", "part.numberentry.correct except fraction": "Je antwoord klopt maar breuken zijn niet toegestaan.", "part.numberentry.answer invalid": "Je hebt geen geldig getal ingevoerd.", "part.numberentry.answer not integer": "Je antwoord is niet correct. Vul een geheel getal in, geen decimaal getal.", "part.numberentry.answer not integer or decimal": "Je antwoord is niet correct. Vul een geheel getal of decimaal getal in.", "part.numberentry.zero sig fig": "Dit deel is zo gemaakt dat een antwoord op 0 significante cijfers wordt afgerond wat zinloos is.", "part.mcq.options def not a list": "De expressie die {{properties}} definieert is geen list.", "part.mcq.marking matrix string empty": "De score-matrix is leeg.", "part.mcq.choices missing": "Antwoordmogelijkheden voor Multiple Response onderdeel (%s) ontbreken", "part.mcq.matrix not a number": "In onderdeel {{part}} specificeert cel {{row}},{{column}} van de scorematrix geen getal", "part.mcq.wrong number of choices": "Je hebt het verkeerde aantal mogelijkheden geselecteerd.", "part.mcq.no choices selected": "Geen keuze gemaakt.", "part.mcq.matrix not a list": "De scorematrix van een Multiple Response onderdeel, gegeven door een JME expressie, moet een list zijn maar is het niet.", "part.mcq.matrix wrong type": "Element heeft een verkeerd type '{{type}}' in de score matrix.", "part.mcq.matrix mix of numbers and lists": "Mix van getallen en lijsten gebruikt in de score matrix.", "part.mcq.matrix wrong size": "Score matrix heeft de verkeerde afmetingen.", "part.mcq.correct choice": "Je hebt het juiste antwoord gekozen.", "part.matrix.answer invalid": "Je antwoord is ongeldig.", "part.matrix.invalid cell": "E\u00e9n of meer van de cellen in antwoord leeg of ongeldig.", "part.matrix.some incorrect": "Een of meer van de cellen in uw antwoord onjuist is, maar je hebt gekregen cijfers voor de rest.", "part.matrix.empty": "U hebt geen antwoord ingevuld", "part.matrix.empty cell": "E\u00e9n of meer van de cellen in antwoord leeg.", "part.matrix.size mismatch": "De vraagauteur heeft de leerling niet toegestaan de afmetingen van zijn antwoord te bepalen, maar het juiste antwoord is {{correct_dimensions}} terwijl de antwoordinvoer {{input_dimensions}} is", "part.gapfill.feedback header": "<strong>Vak {{index}}</strong>", "part.extension.not implemented": "Deel heeft de methode <code>{{name}}</code> niet ge\u00efmplementeerd.", "question.loaded name mismatch": "Kan deze poging niet hervatten - de toets is veranderd sinds de vorige sessie.", "question.error": "Vraag {{number}}: {{-message}}", "question.preamble.error": "Fout in preamble: {{-message}}", "question.preamble.syntax error": "Syntaxisfout in preamble", "question.unsupported part type": "Onbekend onderdeeltype", "question.header": "Vraag {{number}}", "question.submit part": "Verzend antwoord", "question.show steps": "Laat stappen zien", "question.show steps penalty": "Je verliest <strong>{{count}}</strong> $t(mark).", "question.show steps no penalty": "Je score verandert niet.", "question.show steps already penalised": "Je hebt de stappen al bekeken. Je kunt ze nog eens bekijken zonder opnieuw punten te verliezen.", "question.hide steps": "Verberg stappen", "question.hide steps no penalty": "Je score blijft ongewijzigd.", "question.advice": "Uitwerking", "question.no such part": "Onderdeel {{path}} ontbreekt", "question.can not submit": "Antwoord kan niet verzonden worden - controleer op fouten.", "question.answer submitted": "Antwoord verzonden", "question.score feedback.show": "Geef feedback", "question.score feedback.hide": "Verberg", "question.score feedback.answered total actual": "Score: {{score}}/{{marks}}", "question.score feedback.answered total": "{{marksString}}. Beantwoord.", "question.score feedback.answered actual": "Score: {{scoreString}}", "question.score feedback.answered": "Beantwoord.", "question.score feedback.unanswered": "Niet gemaakt.", "question.score feedback.unanswered total": "{{marksString}}.", "question.score feedback.correct": "Je antwoord is goed", "question.score feedback.partial": "Je antwoord is gedeeltelijk goed", "question.score feedback.wrong": "Je antwoord is fout", "question.selector.unsubmitted changes": "Niet verzonden wijzigingen.", "timing.no accumulator": "no timing accumulator {{name}}", "timing.time remaining": "Resterende tijd: %s", "xml.could not load": "Het XML document kan niet geladen worden: {{-message}}", "xml.property not number": "Property {{name}} moet een getal zijn maar is het niet ({{value}}), in node {{element}}", "xml.property not boolean": "Property {{name}} moet een boolean zijn maar is het niet ({{value}}), in node {{element}}", "xml.error in variable definition": "Fout in definitie van variabele <code>{{name}}</code>", "scorm.error initialising": "Fout initialiseren SCORM protocol: {{-message}}", "scorm.failed save": "De gegevens konden niet opgeslagen worden op de server. Je huidige sessie, inclusief antwoorden en scores, is misschien niet bewaard. Je kunt het nog eens proberen. Mail naar <a href=\"mailto:numbas@ncl.ac.uk\">numbas@ncl.ac.uk</a> als dit vaker voorkomt.", "scorm.no exam suspend data": "Hervatten mislukt: geen toetsgegevens gevonden.", "scorm.error loading suspend data": "Fout bij het laden van toetsgegevens: {{-message}}", "scorm.error loading question": "Fout bij het laden van vraag {{number}}: {{-message}}", "scorm.no question suspend data": "Geen vraaggegevens", "scorm.error loading part": "Fout bij laden van deel {{part}}: {{-message}}", "scorm.no part suspend data": "Geen gegevens van dit deel gevonden", "util.product.non list": "Er werd een niet-lijst doorgegeven aan <code>Numbas.util.product</code>", "mark": "punt", "was": "was", "part": "deel", "gap": "gat", "step": "stap", "jme.substituteTree.undefined variable": "Ongedefinieerde variabele: <code>{{name}}</code>", "jme.user javascript.returned undefined": "User-defined javascript function <code>{{name}}</code> returned <code>undefined</code>.", "part.marking.steps change": "Je krijgt <strong>{{count}}</strong> $t(mark) voor je antwoorden bij de verschillende stappen", "part.marking.revealed steps with penalty": "Je hebt de stappen bekeken. Het maximale aantal $t(mark) voor dit onderdeel is <strong>{{count}}</strong>. Je score wordt verlaagd.", "part.marking.total score": "Je krijgt <strong>{{count}}</strong> $t(mark) voor dit onderdeel.", "part.numberentry.precision type.dp": "decimaal", "part.numberentry.precision type.dp_plural": "decimalen", "part.numberentry.precision type.sigfig": "significant cijfer", "part.numberentry.precision type.sigfig_plural": "significante cijfers", "part.numberentry.give your answer to precision": "Rond af op {{count}} {{precisionType}}. ", "question.unsubmitted changes": "Je hebt je antwoord veranderd maar nog niet verzonden. Controleer je antwoord en klik op <strong>Verzend</strong>.", "question.unsubmitted changes_plural": "Je hebt antwoorden gewijzigd maar niet verzonden. Controleer alle antwoorden en klik op <strong>Verzend alles</strong>.", "util.equality not defined for type": "Gelijkheid niet gedefinieerd voor type {{type}}", "mark_plural": "punten", "was_plural": "waren", "die.script not loaded": "Numbas kon niet starten omdat bestand <code>{{file}}</code> niet geladen is. Controleer of het inbegrepen is in <code>scripts.js</code>.", "math.combinations.n less than zero": "Kan geen combinaties berekenen: n is kleiner dan nul", "math.combinations.k less than zero": "Kan geen combinaties berekenen: k is kleiner dan nul", "math.combinations.n less than k": "Kan geen combinaties berekenen: n is kleiner dan k", "math.permutations.n less than zero": "Kan geen permutaties berekenen: n is kleiner dan nul", "math.permutations.k less than zero": "Kan geen permutaties berekenen: k is kleiner dan nul", "math.permutations.n less than k": "Kan geen permutaties berekenen: n is kleiner dan k", "part.numberentry.give your answer to precision_0": "Rond je antwoord af tot op het dichtbij geheel", "mathjax.error": "MathJax verwerkingsprobleem: {{-message}}", "mathjax.error with context": "MathJax verwerkingsprobleem in {{-context}}: {{-message}}", "exam.introduction": "Examenintroductie", "exam.feedback": "Feedbackbericht voor het examen", "jme.tokenise.keypair key not a string": "De key van een dictionary moet een string zijn, niet {{type}}.", "jme.shunt.list mixed argument types": "{{mode}} kan niet worden geparsed: het is een mix van dictionary- en lijst-elementen", "jme.func.listval.key not in dict": "De dictionary bevat de key <code>{{key}}</code> niet", "part.prompt": "Vraagstelling", "part.feedback": "feedback", "part.numberentry.answer not reduced": "Je antwoord is nog niet onvereenvoudigbaar.", "part.numberentry.give your answer as a reduced fraction": "Vereenvoudig je antwoord. ", "part.numberentry.negative decimal places": "Dit deel is ingesteld zodat de antwoorden van studenten worden afgerond tot op een negatief aantal decimale plaatsen, wat geen betekenis heeft.", "part.mcq.choices": "keuzes", "part.mcq.answers": "antwoorden", "part.mcq.matrix cell empty": "Cell ({{row}},{{column}}) van de scorematrix van deel {{part}} is leeg", "part.mcq.matrix jme error": "Cell ({{row}},{{column}}) van de scorematrix van deel {{part}} geeft een JME error: {{-error}}", "question.statement": "Statement", "ruleset.circular reference": "Circulaire referentie in de definitie van ruleset <code>{{name}}</code>", "ruleset.set not defined": "Ruleset {{name}} is niet gedefinieerd", "jme.evaluate.no scope given": "Numbas.jme.evaluate moet een Scope meekrijgen", "answer.number.not a number": "Jouw antwoord is geen geldig getal.", "answer.number.fractions not allowed": "Je mag geen breuk ingeven.", "answer.jme.invalid expression": "{{-message}}", "answer.matrix.fractions not allowed": "Je mag geen breuken ingeven.", "answer.matrix.some cell not a number": "Een of meer cellen in jouw antwoord is geen geldig getal.", "exam.enter password": "Paswoord:", "exam.password.correct": "Het paswoord is correct. Je kan de toets starten.", "exam.password.incorrect": "Het paswoord is incorrect.", "frontpage.scorm.lms not connected": "Deze toets wordt in de zelfstandige modus uitgevoerd. Jouw antwoorden en cijfers worden niet opgeslagen!", "result.question review": "Review", "control.confirm regen no marks": "Weet je zeker dat je deze vraag opnieuw wilt maken?", "control.confirm reveal no marks": "Wil je het antwoord op deze vraag zien?", "jme.tokenise.invalid near": "Ongeldige uitdrukking: <code>{{expression}}</code> op positie {{position}} bij <code>{{nearby}}</code>", "jme.tokenise.number.object not complex": "Ongeldig object meegegeven aan de number constructor.", "jme.subvars.null substitution": "Substitutie van een lege variabele: <code>$t(left brace){{str}}$t(right brace)", "jme.type.type already registered": "Het datatype {{type}} is reeds geregistreerd en kan niet opnieuw worden geregistreerd.", "jme.type.no cast method": "Kan niet automatisch converteren van {{from}} naar {{to}}.", "jme.display.simplifyTree.empty expression": "Uitdrukking is leeg", "jme.display.simplifyTree.stuck in a loop": "De simplifier zit vast in een lus: <code>{{expr}}</code>", "math.niceNumber.undefined": "Verwachtte een getal, maar kreeg <code>undefined</code>", "math.rangeToList.zero step size": "Een range met stapgrootte 0 kan niet naar een lijst worden omgezet.", "part.error": "{{path}}: {{-message}}", "part.marking.revealed steps": "Je liet de stappen zien.", "part.marking.maximum scaled down": "De maximale score voor dit deel is <strong>{{count,niceNumber}}</strong> $t(mark). Je scores zullen overeenkomstig geschaald worden.", "part.marking.minimum score applied": "De minimale score voor dit deel is <strong>{{score,niceNumber}}</strong>.", "part.marking.maximum score applied": "De maximale score voor dit deel is <strong>{{score,niceNumber}}</strong>.", "part.marking.error in marking script": "Er was een probleem in het score-algoritme van dit deel. Meld dit aub. {{-message}}", "part.marking.no result after replacement": "Het is onmogelijk om punten toe te kennen aan dit deel gebruikmakend van je antwoorden op vorige delen.", "part.marking.missing required note": "Het score-algoritme definieert de note <code>{{note}}</code> niet", "marking.apply.not a list": "Het eerste argument van <code>apply</code> moet een lijst zijn, en dat is niet zo", "marking.apply marking script.script not found": "Marking script <code>{{name}}</code> niet gevonden", "marking.note.compilation error": "Error bij het compileren van note <code>{{name}}</code> - {{-message}}", "marking.note.error evaluating note": "Error bij het evalueren van note <code>{{name}}</code> - {{-message}}", "marking.note.invalid definition": "Ongeldige note-definitie: <code>{{source}}</code>. {{-hint}}", "marking.note.invalid definition.missing colon": "Mogelijk mis je een dubbelpunt naar de naam en beschrijving", "marking.note.invalid definition.description missing closing bracket": "Mogelijk mis je een sluitingshaakje", "marking.note.empty expression": "De note <code>{{name}}</code> is leeg.", "marking.script.error parsing notes": "Probleem bij het parsen van het scorescript: {{- message}}", "part.feedback out of date": "Deze feedback is gebaseerd op jouw laatst ingediende antwoord. Dien jouw gewijzigde antwoord in om bijgewerkte feedback te krijgen.", "part.jme.invalid value generator expression": "Ongeldige waardegeneratoruitdrukking voor variabele <code>{{name}}</code>: {{-message}}", "part.mcq.incorrect choice": "Je koos een incorrect antwoord.", "part.matrix.not all cells same precision": "Niet alle cellen in je antwoord heb je met dezelfde precisie ingegeven.", "part.gapfill.error marking gap": "Probleem bij het scoren van {{name}}: {{-message}}", "part.custom.empty setting": "Geen waarde gegeven.", "part.custom.unrecognised input type": "Onbekend instellingtype <code>{{input_type}}</code>", "part.custom.error evaluating input option": "Probleem bij het evalueren van inputoptie <code>{{option}}</code>: {{-error}}", "part.custom.input option missing": "De definitie van inputoptie <code>{{option}}</code> mist.", "part.custom.error evaluating setting": "Probleem bij het evalueren van instelling <code>{{setting}}</code>: {{-error}}", "question.error creating question": "Probleem bij het maken van vraag {{number}}: {{-message}}", "question.score feedback.not marked": "Nog niet gescoord", "question.score feedback.partially answered": "Gedeeltelijk beantwoord", "question.score feedback.score total": "{{marksString}}", "question.score feedback.score actual": "Score: {{scoreString}}", "question.score feedback.score total actual": "Score: {{score,niceNumber}}/{{marks,niceNumber}}", "variable.error in variable definition": "Probleem in de definite van variabele <code>{{name}}</code>", "left brace": "{", "right brace": "}", "extension.not found": "Kon de extensie <code>{{name}}</code> niet laden.", "control.toggle navigation menu": "Toggle het navigatiemenu", "part.input title": "Antwoord voor deel {{name}}", "part.correct answer title": "Verwachte antwoord voor deel {{name}}", "part.jme.must-match.failed": "Jouw antwoord is niet in de juiste vorm.", "question.score feedback.none": "", "control.submit part.confirm remove next parts": "<p>Een of meer volgende onderdelen zijn afhankelijk van jouw antwoord op dit onderdeel. Door dit onderdeel opnieuw in te dienen, worden die onderdelen ongeldig en worden ze uit de vraag verwijderd. Dit kan niet ongedaan gemaakt worden.</p>\n<p>Wil je dit onderdeel opnieuw indienen?</p>", "control.back to menu": "Ga terug naar het menu", "display.error making html": "Probleem bij het maken van HTML in {{contextDescription}}: {{-message}}", "jme.subvars.error compiling": "{{-message}} in <code>{{expression}}</code>", "jme.variables.empty name": "Er is een vraagvariabele waaraan geen naam is gegeven.", "jme.calculus.unknown derivative": "Weet niet hoe <code>{{tree}}</code> kan worden afgeleid", "math.order complex numbers": "Complexe getallen kunnen niet worden geordend", "menu.choose a question": "Kies een vraag.", "part.choose next part.answered": "Wat wil je hierna doen?", "part.choose next part.unanswered": "Of, je kan:", "part.choose next part.will be locked": "(Dit deel zal niet toegankelijk zijn)", "part.reached dead end": "Er is niets meer om te doen vanaf hier.", "part.next part.penalty amount": "(verlies {{count}} $t(mark))", "part.marking.counts towards objective": "Dit deel telt mee voor het doel <strong>\u201c{{objective}}\u201d</strong>.", "part.numberentry.answer not integer or decimal or fraction": "Je antwoord is ongeldig. Je moet een geheel getal, kommagetal of breuk ingeven.", "question": "Vraag", "question.progress": "Vraagvooruitgang:", "question.score feedback.unattempted": "Niet geprobeerd", "question.score feedback.attempted": "Geprobeerd", "question.score feedback.score actual.plain": "{{scoreString}}", "question.score feedback.score total actual.plain": "{{score,niceNumber}}/{{marks,niceNumber}}", "question.objectives": "Doelen", "question.penalties": "Bestraffingen", "question.back to previous part": "Ga terug naar het vorige onderdeel", "end.print": "Print het afschrift van je examen", "math.shuffle_together.lists not all the same length": "Niet alle lijsten hebben dezelfde lengte.", "jme.parse signature.invalid signature string": "Ongeldige functiesignatuur: {{str}}", "part.custom.expected answer has wrong type": "Het verwachte antwoord voor dit deel heeft het verkeerde type. Het zou <code>{{shouldbe}}</code> moeten zijn.", "part.custom.input option has wrong type": "De antwoordinvoerinstelling <code>{{option}}</code> heeft het verkeerde type. Het zou <code>{{shouldbe}}</code> moeten zijn.", "matrix input.size control legend": "Grootte", "matrix input.rows": "Rijen", "matrix input.columns": "Kolommen", "part.jme.error checking numerically": "Er was een probleem bij het numeriek controleren van je antwoord: {{message}}", "part.gapfill.cyclic adaptive marking": "Er is een lus in de adaptieve scoring van dit deel: <strong>{{name1}}</strong> berust op <strong>{{name2}}</strong>, die uiteindelijk berust op <strong>{{name1}}</strong>.", "modal.style.background colour": "Achtergrondkleur", "modal.style.text colour": "Tekstkleur", "modal.style.text size": "Tekstgrootte", "modal.style.explanation": "Gebruik dit om het uitzicht van het examen te veranderen.", "modal.style.reset to defaults": "Herstel standaardwaarden", "modal.style.text size preview": "De meeste tekst zal deze grootte hebben.", "control.style options": "Weergave-opties", "part.marking.partially correct": "Je antwoord is gedeeltelijk juist.", "part.marking.error in adaptive marking": "Er was een probleem in de adaptieve scoring voor dit deel. Meld dit aub. {{-message}}", "page.skip to content": "Ga door naar inhoud", "result.learning objective": "Leerdoel", "jme.interpreted as": "Ge\u00efnterpreteerd als", "jme.script.note.compilation error": "Error bij het compileren van note <code>{{name}}</code>: {{-message}}", "jme.script.note.error evaluating note": "Error bij het evalueren van note <code>{{name}}</code>: {{-message}}", "jme.script.note.invalid definition": "Ongeldige note definitie: <code>{{source}}</code>. {{-hint}}", "jme.script.note.invalid definition.missing colon": "Mogelijk mis je een dubbelpunt na de naam en beschrijving", "jme.script.note.invalid definition.description missing closing bracket": "Mogelijk mis je een sluitingshaakje", "jme.script.note.empty expression": "De note <code>{{name}}</code> is leeg.", "jme.script.error parsing notes": "Error bij het parsen van het scorescript: {{- message}}", "matrix input.cell label": "Rij {{row}}, kolom {{column}}", "diagnostic.use retry": "Use one retry and try this topic again.", "diagnostic.move to next topic": "Move on to the next topic.", "diagnostic.next step question": "What would you like to do next?", "diagnostic.now assessing topic": "Now assessing {{current_topic}}", "diagnostic.one retry left": "You have 1 retry left", "diagnostic.retries left": "You have {{retries}} retries left.", "diagnostic.percentage completed": "You've completed <strong>{{percentage}}%</strong> of the test.", "diagnostic.test is over": "The test is over.", "diagnostic.passed all lo": "You have passed all learning objectives.", "diagnostic.more work on lo": "You need to do some more work on the following learning objectives: {{los}}.", "diagnostic.move to next question in topic": "Move on to the next question in topic.", "diagnostic.complete": "Complete!", "diagnostic.studying topic": "Studying {{topic}}", "display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.", "jme.shunt.expected argument before comma": "Expected to see something between the opening bracket and the comma", "part.waiting for pre submit": "Your answer is being marked. Please wait.", "diagnostic.end test": "End the test.", "page.no stylesheet loaded": "The page's stylesheet file has not loaded.", "modal.confirm": "Confirm", "modal.alert": "Alert", "suspend.resumed header": "Attempt resumed", "jme.vector.value not an array of numbers": "Tried to construct a vector using a value that is not an array of numbers.", "jme.matrix.value not the right type": "Tried to construct a vector using a value of the wrong type.", "jme.subvars.html inserted twice": "An HTML value has been embedded twice. Consider defining a function to generate a new value each time it is used.", "jme.variables.invalid function language": "The language <code>{{language}}</code> is not valid.", "jme.variables.duplicate definition": "There is more than one definition of the variable <code>{{name}}</code>.", "math.random_integer_partition.invalid k": "The size of the partition must be between 1 and {{n}}.", "part.marking.parameter already in scope": "There is a variable named <code>{{name}}</code>, which is also the name of a marking parameter. Please rename the variable.", "part.marking.adaptive variable replacement refers to self": "This part refers to itself in a variable replacement for adaptive marking.", "part.marking.adaptive variable replacement refers to nothing": "This part contains an invalid variable replacement for adaptive marking.", "part.numberentry.display answer wrong type": "The display answer for this part is a value of type <code>{{got_type}}</code>, but should be a <code>{{want_type}}</code>.", "part.matrix.invalid type in prefilled": "There is an invalid value of type <code>{{n}}</code> in the array of pre-filled cells.", "diagnostic.make a choice": "Make a choice", "matrixmath.not square": "This operation only works on a square matrix.", "matrixmath.not invertible": "This operation only works on an invertible matrix.", "matrixmath.gauss-jordan elimination.not enough columns": "There must be at least as many columns as rows.", "question.required extension not available": "This question requires the extension <code>{{-extension}}</code> but it is not available.", "util.formatNumberNotation.unrecognised syntax": "The number formatting syntax <code>{{syntax}}</code> is not recognised.", "worksheet.number of exams to generate": "Number of sheets to generate", "worksheet.starting with id": "Starting with ID", "worksheet.show exam id": "Show sheet ID?", "worksheet.page break between questions": "Page breaks between questions?", "worksheet.page margins": "Page margins (mm)", "worksheet.text size": "Text size (pt)", "worksheet.generate": "Generate", "worksheet.generating exams": "Generating sheets", "worksheet.sheet id": "Sheet ID:", "worksheet.print single": "Print this sheet", "worksheet.print several": "Print these sheets", "worksheet.answer sheets": "Answer sheets", "worksheet.question sheets": "Question sheets", "worksheet.reconfigure": "Generate different sheets", "worksheet.show sheet": "Preview the sheet with ID:", "accessibility statement": "Accessibility statement and guide to adapting Numbas to your needs.", "exam.enter your name": "Your name:", "exam.attempt download security warning": "This exam is configured to allow you to download your data, but it is not running in a secure browser context. You will not be able to download your exam data. Contact your lecturer or teacher for help.", "result.download exam object": "Download your exam data", "control.return to question": "Return to the question", "control.show introduction": "Introduction", "analysis.header": "Analyse attempt data", "analysis.help.upload files": "Upload attempt data files that your students have given you.", "analysis.help.file input label": "Choose student attempt data files, or drag files onto this window.", "analysis.table.total": "Exam totals", "analysis.table.question": "Exam and question totals", "analysis.table.all": "All details", "analysis.student name.anonymous": "No name given", "analysis.expected": "Expected results", "analysis.start time": "Start time", "analysis.maximum": "Maximum Marks", "analysis.file": "File", "analysis.download": "Download", "analysis.delete": "Delete", "analysis.view results": "View results", "analysis.upload files": "Upload files", "analysis.upload more": "Upload more files", "analysis.attempt data": "Attempt data", "analysis.select format": "Select data to show", "analysis.download this table": "Download this table", "analysis.student": "Student Results", "analysis.question key": "Question Key", "analysis.question name": "Question Name", "analysis.group": "Group", "analysis.question": "Question", "analysis.part": "Part", "analysis.gap": "Gap", "analysis.record type": "Record Type", "analysis.score": "Score", "analysis.marks available": "Marks Available", "analysis.percentage": "Percentage", "analysis.answer": "Answer", "analysis.student name": "Student Name", "analysis.summary.no files": "You have not uploaded any files yet.", "analysis.summary.no decrypted files": "You have not uploaded any valid files yet.", "analysis.summary.one file": "One attempt.", "analysis.summary.several files": "{{num_files,niceNumber}} attempts.", "analysis.not secure context": "This page must be opened in a secure browser context. A secure context is either a page served over HTTPS, or a file loaded from your device.", "jme.shunt.pipe right hand takes no arguments": "The expression on the right-hand side of the pipe operator must be a function application.", "question.explore.no parts defined": "There are no parts defined in this question.", "answer": "answer", "worksheet.answersheet show question content": "Show question content in answer sheets?", "modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:", "modal.end exam button": "End exam", "lightbox.zoom in on image": "Zoom in on this image", "exam.progress": "Progress", "exam.questions answered": "{{numAnsweredQuestions}} of {{numQuestions}} questions answered.", "result.question marks available": "Marks Available", "result.question answered": "Answered?", "control.confirm end.correct": "You may now end the exam.", "control.confirm end.incorrect": "This is not the expected text.", "control.confirm end.password": "end", "jme.typecheck.for in name wrong type": "The name in a <code>for</code> statement must be a name or list of names, not {{type}}.", "jme.makeFast.no fast definition of function": "The function <code>{{name}}</code> here isn't defined in a way that can be made fast.", "part.show feedback": "Show feedback", "part.hide feedback": "Hide feedback", "part.feedback title": "Feedback for {{name}}.", "part.jme.must-match.warning": "Your answer is not in the expected form: {{-message}}", "part.numberentry.write your answer as a fraction": "Write your answer as a fraction.", "question.nav.label": "Question controls", "question.answer saved": "Answer saved", "question.all answers saved": "All answers saved", "analysis.back to results": "Back to results", "analysis.review": "Review", "analysis.review this": "Review this attempt", "analysis.reviewing attempt": "Reviewing attempt by {{student_name}}.", "part.there is new feedback": "The feedback has changed.", "modal.style.colour scheme": "Colour scheme", "modal.style.automatic colour scheme": "Automatic", "modal.style.light colour scheme": "Light", "modal.style.dark colour scheme": "Dark", "modal.style.custom colour scheme": "Custom", "modal.style.main font": "Main font", "modal.style.font.sans serif": "Sans serif", "modal.style.font.serif": "Serif", "modal.style.font.monospace": "Monospace", "modal.style.spacing": "Spacing", "modal.style.font weight": "Font weight", "modal.style.forced colours warning": "Your browser has overridden the colours used in this page because of a setting such as high contrast mode. Changes to the colour scheme settings here might not have any effect.", "modal.style.text preview": "Most text will look like this.", "modal.style.more options": "More options", "modal.style.main colour": "Main (brand) colour", "modal.style.primary colour": "Primary button colour", "modal.style.link colour": "Link colour", "modal.style.success colour": "Success/correct colour", "modal.style.info colour": "Info colour", "modal.style.warning colour": "Warning colour", "modal.style.danger colour": "Danger/incorrect colour", "modal.style.muted colour": "Muted text colour", "modal.style.highlight colour": "Highlight colour", "exam.error loading exam definition": "There was an error while loading the exam definition: {{text}}", "exam.no exam definition": "No exam definition was given.", "jme.typecheck.wrong arguments for anonymous function": "Wrong number of arguments for this anonymous function.", "worksheet.top": "Top", "worksheet.left": "Left", "worksheet.bottom": "Bottom", "worksheet.right": "Right"}}}
        }
        });
        ;
Numbas.queueScript('marking_scripts', ['marking'], function() {
            Numbas.raw_marking_scripts = {"jme": "expand_juxtapositions_settings (Settings for the \"expand juxtapositions\" step):\n    [\n        \"singleLetterVariables\": settings[\"singleLetterVariables\"],\n        \"noUnknownFunctions\": not settings[\"allowUnknownFunctions\"],\n        \"implicitFunctionComposition\": settings[\"implicitFunctionComposition\"],\n        \"normaliseSubscripts\": true\n    ]\n\nstudentExpr_empty (If the student's answer is empty, don't try to mark it):\n    assert(trim(studentAnswer)<>\"\" and parse(studentAnswer)<>parse(\"\"),\n        warn(translate(\"part.marking.nothing entered\"));\n        fail(translate(\"part.marking.nothing entered\"))\n    )\n\nstudentExpr (The student's answer, parsed):\n    apply(studentExpr_empty);\n    try(\n        simplify(\n            expand_juxtapositions(parse(studentAnswer), expand_juxtapositions_settings),\n            'basic'\n        )\n    , message,\n        warn(translate(\"part.jme.answer invalid\",[\"message\":message]));\n        fail(translate(\"part.jme.answer invalid\",[\"message\":message]));\n        nothing\n    )\n\ncleanedStudentString (The student's answer as a string, cleaned up): string(studentExpr)\n\nscope_vars (Variables already defined in the scope):\n    definedvariables()\n\ncorrectExpr (The correct answer, parsed):\n    expand_juxtapositions(parse(settings[\"correctAnswer\"]), expand_juxtapositions_settings)\n\nstudentMatch (The result of matching the student's expression against the pattern):\n    scope_case_sensitive(match(studentExpr,settings[\"mustMatchPattern\"]),settings[\"caseSensitive\"])\n\ncorrectMatch (The result of matching the correct answer against the pattern):\n    scope_case_sensitive(match(correctExpr,settings[\"mustMatchPattern\"]),settings[\"caseSensitive\"])\n\ncompareName (The name of the matched group from each expression to compare): \n    settings[\"nameToCompare\"]\n\nformula_replacement_pattern: \"$v;lhs = ?;rhs\"\n\nformula_replacement:\n    if(is_formula,\n        string(\n            substitute(settings, expression(\"resultsequal(lhs, rhs, checkingType, checkingAccuracy)\"))\n        )\n    ,\n        \"lhs = rhs\"\n    )\n\nstudentCompare (The part of the student's expression to compare):\n    if(settings[\"mustMatchPattern\"]=\"\" or compareName=\"\",\n        replace(formula_replacement_pattern, formula_replacement, studentExpr)\n    ,\n        studentMatch[\"groups\"][compareName]\n    )\n\ncorrectCompare (The part of the correct expression to compare):\n    if(settings[\"mustMatchPattern\"]=\"\" or compareName=\"\",\n        replace(formula_replacement_pattern, formula_replacement, correctExpr)\n    ,\n        correctMatch[\"groups\"][compareName]\n    )\n\nfailNameToCompare (If comparing just a subexpression, stop marking if the student's expression doesn't have that subexpression):\n    assert(settings[\"mustMatchPattern\"]=\"\" or compareName=\"\" or (studentMatch[\"match\"] and compareName in studentMatch[\"groups\"]),\n        incorrect(settings[\"mustMatchMessage\"]);\n        end()\n    )\n\nstudentVariables (Variables used in the student's answer): \n    scope_case_sensitive(set(findvars(studentCompare)),settings[\"caseSensitive\"])\n\ncorrectVariables (Variables used in the correct answer):\n    scope_case_sensitive(set(findvars(correctCompare)),settings[\"caseSensitive\"])\n\nunexpectedVariables (Unexpected variables used in the student's answer):\n    let(uvars, filter(not (x in correctVariables),x,list(studentVariables)),\n        assert(not settings[\"checkVariableNames\"] or len(uvars)=0,\n            warn(translate(\"part.jme.unexpected variable name\",[\"name\":uvars[0]]));\n            feedback(translate(\"part.jme.unexpected variable name\",[\"name\":uvars[0]]))\n        );\n        uvars\n    )\n\nfailMinLength (Is the student's answer too short?):\n    assert(settings[\"minLength\"]=0 or len(cleanedStudentString)>=settings[\"minLength\"],\n        multiply_credit(settings[\"minLengthPC\"],settings[\"minLengthMessage\"]);\n        true\n    )\n\nfailMaxLength:\n    assert(settings[\"maxLength\"]=0 or len(cleanedStudentString)<=settings[\"maxLength\"],\n        multiply_credit(settings[\"maxLengthPC\"],settings[\"maxLengthMessage\"]);\n        true\n    )\n\nforbiddenStrings:\n    filter(x in cleanedStudentString, x, settings[\"notAllowed\"])\n\nforbiddenStringsPenalty:\n    assert(len(forbiddenStrings)=0,\n        translate(\n          if(len(settings[\"notAllowed\"])=1, 'part.jme.not-allowed one', 'part.jme.not-allowed several'),\n          [\"strings\":map(translate('part.jme.not-allowed bits',[\"string\":str]),str,forbiddenStrings)]\n        );\n        multiply_credit(settings[\"notAllowedPC\"],settings[\"notAllowedMessage\"]);\n        warn(settings[\"notAllowedMessage\"])\n    )\n\nrequiredStrings:\n    filter(not (x in cleanedStudentString), x, settings[\"mustHave\"])\n\nrequiredStringsPenalty:\n    assert(len(requiredStrings)=0,\n        translate(\n          if(len(settings[\"mustHave\"])=1, 'part.jme.must-have one', 'part.jme.must-have several'),\n          [\"strings\":map(translate('part.jme.must-have bits',[\"string\":str]),str,forbiddenStrings)]\n        );\n        multiply_credit(settings[\"mustHavePC\"],settings[\"mustHaveMessage\"]);\n        warn(settings[\"mustHaveMessage\"])\n    )\n\nvRange (The range to pick variable values from): \n    settings[\"vsetRangeStart\"]..settings[\"vsetRangeEnd\"] # 0\n\nanswerVariables (Variables used in either the correct answer or the student's answer):\n    correctVariables or studentVariables\n\nformula_match:\n  scope_case_sensitive(match(correctExpr,\"$v;lhs = ?;rhs\"),settings[\"caseSensitive\"])\n\nis_formula (Is the correct answer a formula of the form name = expression?):\n  formula_match[\"match\"]\n\nformula_variable (The variable on the left-hand side of the formula, if the correct answer is a formula):\n  try(string(formula_match[\"groups\"][\"lhs\"]),err,\"\")\n\nformula_expression (The right-hand side of the formula, if the correct answer is a formula):\n  formula_match[\"groups\"][\"rhs\"]\n\nformula_type (The type of value the formula produces, if the correct answer is a formula):\n  let(t,scope_case_sensitive(infer_type(formula_expression),settings[\"caseSensitive\"]),\n    if(t=\"name\",\"number\",t)\n  )\n\nvalue_generator_definitions:\n    dict([normalise_subscripts(k), v] for: [k,v] of: items(settings[\"valueGenerators\"]))\n\nvalue_generators (Expressions which generate values for each variable in the answer):\n    dict(map(\n        [\n          name,\n          get(\n            value_generator_definitions,\n            name,\n            if(is_formula and name=formula_variable,\n              exec(function(\"random\"),[formula_expression,default_value_generator[formula_type]])\n            ,\n              default_value_generator[get(variable_types,name,\"number\")]\n            )\n          )\n        ],\n        name,\n        answerVariables\n    ))\n\nvariable_types (Inferred types for each of the variables):\n    scope_case_sensitive(infer_variable_types(correctExpr),settings[\"caseSensitive\"])\n\ndefault_value_generator:\n    [\n        \"number\": expression(\"random(vRange)\"),\n        \"decimal\": expression(\"dec(random(vRange))\"),\n        \"integer\": expression(\"int(random(vRange))\"),\n        \"rational\": expression(\"rational(random(vRange))\"),\n        \"matrix\": expression(\"matrix(repeat(repeat(random(vRange),3),3))\"),\n        \"vector\": expression(\"vector(repeat(random(vRange),3))\"),\n        \"boolean\": expression(\"random(true,false)\"),\n        \"set\": expression(\"set(repeat(random(vRange),5))\")\n    ]\n\nvset (The sets of variable values to test against):\n    try(\n        repeat(\n            scope_case_sensitive(make_variables(value_generators,vRange),settings[\"caseSensitive\"]),\n            settings[\"vsetRangePoints\"]\n        ),\n        message,\n        warn(translate(\"part.jme.error checking numerically\",[\"message\":message]));\n        fail(translate(\"part.jme.error checking numerically\",[\"message\":message]));\n        []\n    )\n\nagree (Do the student's answer and the expected answer agree on each of the sets of variable values?):\n    apply(vset);\n    map(\n        try(\n            scope_case_sensitive(resultsequal(unset(question_definitions,eval(studentCompare,vars)),unset(question_definitions,eval(correctCompare,vars)),settings[\"checkingType\"],settings[\"checkingAccuracy\"]),settings[\"caseSensitive\"]),\n            message,\n            warn(translate(\"part.jme.answer invalid\",[\"message\":message]));\n            fail(translate(\"part.jme.answer invalid\",[\"message\":message]));\n            false\n        ),\n        vars,\n        vset\n    )\n\nnumFails (The number of times the student's answer and the expected answer disagree):\n    apply(agree);\n    len(filter(not x,x,agree))\n\nnumericallyCorrect (Is the student's answer numerically correct?):\n    apply(numFails);\n    if(numFails<settings[\"failureRate\"],\n        correct(translate(\"part.jme.marking.correct\"))\n    ,\n        incorrect()\n    )\n\nsameVars (Does the student use the same variables as the correct answer?):\n    // Removed, but still defined so that older questions with custom marking algorithms don't break\n    nothing\n\nstudentMatches (Does the student's answer match the required pattern?):\n    matches(studentExpr,settings[\"mustMatchPattern\"])\n\nmustMatchMessage:\n    if(settings[\"mustMatchMessage\"]=\"\",\n        translate(\"part.jme.must-match.failed\")\n    ,\n        translate(\"part.jme.must-match.warning\", [\"message\": settings[\"mustMatchMessage\"]])\n    )\n\nfailMatchPatternPrevent (Prevent submission if the student's answer doesn't match the required pattern):\n    assert(settings[\"mustMatchWarningTime\"]<>\"prevent\" or studentMatches,\n        warn(mustMatchMessage);\n        fail(mustMatchMessage)\n    )\n\nfailMatchPattern (Give feedback if the student's answer doesn't match the required pattern):\n    assert(settings[\"mustMatchPattern\"]=\"\" or studentMatches,\n        assert(settings[\"mustMatchWarningTime\"]<>\"input\",\n            warn(mustMatchMessage)\n        );\n        if(compareName=\"\",\n            multiply_credit(settings[\"mustMatchPC\"], mustMatchMessage)\n        ,\n            set_credit(0,mustMatchMessage)\n        );\n        true\n    )\n\nmark:\n    apply(studentExpr);\n    apply(failNameToCompare);\n    apply(unexpectedVariables);\n    apply(sameVars);\n    apply(failMatchPatternPrevent);\n    apply(numericallyCorrect);\n    apply(failMinLength);\n    apply(failMaxLength);\n    apply(forbiddenStringsPenalty);\n    apply(requiredStringsPenalty);\n    apply(failMatchPattern)\n\ninterpreted_answer (The student's answer, to be reused by other parts):\n    apply(studentExpr);\n    studentExpr\n\n", "gapfill": "marked_original_order (Mark the gaps in the original order, mainly to establish if every gap has a valid answer):\n    map(\n        mark_part(gap[\"path\"],studentAnswer),\n        [gap,studentAnswer],\n        zip(gaps,studentAnswer)\n    )\n\ninterpreted_answers (The interpreted answers for each gap, in the original order):\n    map(\n        res[\"values\"][\"interpreted_answer\"],\n        res,\n        marked_original_order\n    )\n\nanswers (The student's answers to each gap):\n    if(settings[\"sortAnswers\"],\n        sort(interpreted_answers)\n    ,\n        interpreted_answers\n    )\n\ngap_order:\n    if(settings[\"sortAnswers\"],\n        sort_destinations(interpreted_answers)\n    ,\n        gap_adaptive_order\n    )\n\nanswer_order:\n    if(settings[\"sortAnswers\"],\n        0..(len(studentAnswer)-1)\n    ,\n        gap_adaptive_order\n    )\n\ngap_feedback (Feedback on each of the gaps):\n    map(\n        try(\n            let(\n                answer, studentAnswer[answer_number],\n                result, submit_part(gaps[gap_number][\"path\"],answer),\n                gap, gaps[gap_number],\n                name, gap[\"name\"],\n                noFeedbackIcon, not gap[\"settings\"][\"showFeedbackIcon\"],\n                non_warning_feedback, filter(x[\"op\"]<>\"warning\",x,result[\"feedback\"]),\n                    assert(noFeedbackIcon,\n                        assert(name=\"\" or len(gaps)=1 or len(non_warning_feedback)=0,feedback(translate('part.gapfill.feedback header',[\"name\": name])))\n                    );\n                    concat_feedback(non_warning_feedback, if(marks>0,result[\"marks\"]/marks,1/len(gaps)), noFeedbackIcon);\n                    result\n            ),\n            err,\n            fail(translate(\"part.gapfill.error marking gap\",[\"name\": gaps[gap_number][\"name\"], \"message\": err]))\n        ),\n        [gap_number,answer_number],\n        zip(gap_order,answer_order)\n    )\n\nall_valid (Are the answers to all of the gaps valid?):\n  all(map(res[\"valid\"],res,marked_original_order))\n\nmark:\n  assert(all_valid or not settings[\"sortAnswers\"], fail(translate(\"question.can not submit\")));\n  apply(answers);\n  apply(gap_feedback)\n\ninterpreted_answer:\n  answers\n\npre_submit:\n    map(\n        let(\n            answer, studentAnswer[answer_number],\n            result, submit_part(gaps[gap_number][\"path\"],answer),\n            check_pre_submit(gaps[gap_number][\"path\"], answer, exec_path)\n        ),\n        [gap_number,answer_number],\n        zip(gap_order,answer_order)\n    )\n", "patternmatch": "regex_match (Match the student's answer with the correct answer, interpreted as a regular expression):\n  match_regex(settings[\"correctAnswer\"],studentAnswer,\"u\")\n\nregex_match_case_insensitive (Match the student's answer with the correct answer, interpreted as a case-insensitive regular expression):\n  match_regex(settings[\"correctAnswer\"],studentAnswer,\"iu\")\n\nexact_match (Is the student's answer exactly the correct answer?):\n  studentAnswer=settings[\"correctAnswer\"]\n\nexact_match_case_insensitive (Is the student's answer exactly the correct answer?):\n  lower(studentAnswer)=lower(settings[\"correctAnswer\"])\n\nmatches (Does the student's answer match the correct answer?):\n  switch(\n    settings[\"matchMode\"]=\"regex\", len(regex_match)>0,\n    settings[\"matchMode\"]=\"exact\", exact_match,\n    false\n  )\n      \nmatches_case_insensitive (Does the student's answer match the correct answer, ignoring case?):\n  switch(\n    settings[\"matchMode\"]=\"regex\", len(regex_match_case_insensitive)>0,\n    settings[\"matchMode\"]=\"exact\", exact_match_case_insensitive,\n    false\n  )\n     \n\nmark:\n  assert(settings[\"allowEmpty\"] or len(studentAnswer)>0,\n    warn(translate(\"part.marking.nothing entered\"));\n    fail(translate(\"part.marking.nothing entered\"))\n  );\n  if(settings[\"caseSensitive\"],\n    if(matches,\n      correct(),\n      if(matches_case_insensitive,\n        set_credit(settings[\"partialCredit\"],translate(\"part.patternmatch.correct except case\")),\n        incorrect()\n      )\n    )\n  ,\n    if(matches_case_insensitive,\n      correct()\n    ,\n      incorrect()\n    )\n  )\n\ninterpreted_answer (The student's answer, to be reused by other parts):\n  studentAnswer\n", "matrixentry": "rows (The number of rows in the student's answer): len(studentAnswer)\n\ncols (The number of columns in the student's answer): if(rows>0,len(studentAnswer[0]),0)\n\ncorrect_rows (The number of rows in the correct answer): len(settings['correctAnswer'])\n\ncorrect_cols (The number of columns in the correct answer): len(settings['correctAnswer'][0])\n\nnum_cells (The number of cells in the student's answer): rows*cols\n\ncell_indexes (The index of each cell in the student's answer): \n    product(list(0..rows-1), list(0..cols-1))\n\nstudent_cell_precisions (Precision the student used in each cell):\n    switch(\n        settings[\"precisionType\"]=\"dp\",\n            map(map(countdp(cell),cell,row), row, studentAnswer)\n        ,\n        settings[\"precisionType\"]=\"sigfig\",\n            map(map(countsigfigs(cell),cell,row), row, studentAnswer)\n        ,\n        map(map(0,cell,row),row,studentAnswer)\n    )\n\nall_same_precision:\n    if(len(student_cell_precisions)=0 or all(map(all(map(togivenprecision(c,settings[\"precisionType\"],student_cell_precisions[0][0],settings[\"strictPrecision\"]),c,row)),row,studentAnswer)),\n        true\n    ,\n        feedback(translate(\"part.matrix.not all cells same precision\"));\n        false\n    )\n\nstudentPrecision (Maximum precision student used, or the required precision, whichever's higher):\n    max(settings[\"precision\"], max(map(max(row),row,student_cell_precisions)))\n\nallowFractions: settings[\"allowFractions\"]\n\nallowedNotationStyles: [\"plain\",\"en\",\"si-en\"]\n\nstudentNumbers:\n    if(settings[\"allowFractions\"],\n        map(map(parsenumber_or_fraction(c, allowedNotationStyles), c, row), row, studentAnswer)\n    ,\n        map(map(parsenumber(c, allowedNotationStyles), c, row), row, studentAnswer)\n    )\n\nstudentMatrix (The student's answer, with each cell parsed to numbers): \n    matrix(studentNumbers)\n\nempty_cells (The positions of the cells in the student's answer which have been left empty):\n  filter(trim(studentAnswer[p[0]][p[1]])=\"\", p, cell_indexes)\n\nany_empty (Are any of the cells empty?):\n  assert(len(empty_cells)=0,\n    warn(translate(\"part.matrix.empty cell\"));\n    fail(translate(\"part.matrix.empty cell\"));\n    true\n  )\n\ninvalid_cells (The positions of the cells in the student's answer which can't be interpreted as numbers):\n    filter(isnan(studentNumbers[p[0]][p[1]]), p, cell_indexes)\n\nany_invalid (Are any of the cells invalid?):\n    assert(len(invalid_cells)=0,\n        warn(translate(\"part.matrix.invalid cell\"));\n        fail(translate(\"part.matrix.invalid cell\"));\n        true\n    )\n\nwrong_precision_cells (The indexes of the cells which are given to the wrong precision):\n    filter(not togivenprecision(studentAnswer[p[0]][p[1]], settings[\"precisionType\"], settings[\"precision\"], settings[\"strictPrecision\"]), p, cell_indexes)\n\nwrong_precision (Has every cell been given to the correct precision?):\n    assert(len(wrong_precision_cells)=0,\n        multiply_credit(settings['precisionPC'], settings[\"precisionMessage\"])\n    )\n\nwrong_size (Does the student's answer have the wrong dimensions?):\n    assert(rows=correct_rows and cols=correct_cols,\n        incorrect();\n        end()\n    )\n\nrounded_student_matrix (The student's answer, with each cell rounded to studentPrecision): \n    map(\n        switch(\n            settings[\"precisionType\"]=\"dp\",\n            precround(c, studentPrecision),\n            settings[\"precisionType\"]=\"sigfig\",\n            siground(c, studentPrecision),\n            c\n        ),\n        c,\n        studentMatrix\n    )\n\nrounded_correct_matrix:\n    map(\n        map(\n            switch(\n                settings[\"precisionType\"]=\"dp\",\n                precround(c, studentPrecision),\n                settings[\"precisionType\"]=\"sigfig\",\n                siground(c, studentPrecision),\n                c\n            ),\n            c,\n            row\n        ),\n        row,\n        settings[\"correctAnswer\"]\n    )\n\ncorrect_cells (The indexes of the cells which are correct):\n    filter(\n        if(p[0]<correct_rows and p[1]<correct_cols,\n            withintolerance(rounded_student_matrix[p[0]][p[1]], rounded_correct_matrix[p[0]][p[1]], settings['tolerance']),\n            false\n        ),\n        p, \n        cell_indexes\n    )\n\nmark:\n    apply(any_empty);\n    apply(any_invalid);\n    assert(settings[\"precisionType\"]=\"none\" and not settings[\"allowFractions\"], apply(all_same_precision));\n    apply(wrong_size);\n    if(len(correct_cells)=len(cell_indexes),\n        correct()\n    ,\n        if(settings['markPerCell'],\n            set_credit(len(correct_cells)/num_cells,translate('part.matrix.some incorrect',[\"count\":num_cells-len(correct_cells)]))\n        ,\n            incorrect();\n            end()\n        )\n    );\n    apply(wrong_precision)\n\ninterpreted_answer (The student's answer, to be reused by other parts):\n    apply(any_empty);\n    apply(any_invalid);\n    studentMatrix\n", "numberentry": "studentNumber (The student's answer, parsed as a number):\n    if(settings[\"allowFractions\"],\n        parsedecimal_or_fraction(studentAnswer,settings[\"notationStyles\"])\n    ,\n        parsedecimal(studentAnswer,settings[\"notationStyles\"])\n    )\n\nisInteger (Is the student's answer an integer?):\n    countdp(studentAnswer)=0\n\nisFraction (Is the student's answer a fraction?):\n    \"/\" in studentAnswer\n\nnumerator (The numerator of the student's answer, or 0 if not a fraction):\n    if(isFraction,\n        parsenumber(split(studentAnswer,\"/\")[0],settings[\"notationStyles\"])\n    ,\n        0\n    )\n\ndenominator (The numerator of the student's answer, or 0 if not a fraction):\n    if(isFraction,\n        parsenumber(split(studentAnswer,\"/\")[1],settings[\"notationStyles\"])\n    ,\n        0\n    )\n\ncancelled (Is the student's answer a cancelled fraction?):\n    assert(isFraction and gcd(numerator,denominator)=1,\n        assert(not settings[\"mustBeReduced\"],\n            multiply_credit(settings[\"mustBeReducedPC\"],translate(\"part.numberentry.answer not reduced\"))\n        );\n        false\n    )\n\ncleanedStudentAnswer:\n    cleannumber(studentAnswer, settings[\"notationStyles\"])\n\nstudent_is_scientific (Is the student's answer written in scientific notation?):\n    not isnan(matchnumber(studentAnswer, [\"scientific\"])[1])\n\nscientific_precision_offset (A number in scientific notation has 1 more significant digit than decimal places):\n    award(1,settings[\"precisionType\"]=\"dp\")\n\nstudentPrecision:\n    max(settings[\"precision\"],\n        switch(\n            student_is_scientific, countsigfigs(cleanedStudentAnswer)-scientific_precision_offset,\n            settings[\"precisionType\"]=\"dp\", max(settings[\"precision\"],countdp(cleanedStudentAnswer)),\n            settings[\"precisionType\"]=\"sigfig\", max(settings[\"precision\"],countsigfigs(cleanedStudentAnswer)),\n            0\n        )\n    )\n\nraw_minvalue:\n    switch(\n        student_is_scientific, siground(settings[\"minvalue\"],studentPrecision+scientific_precision_offset),\n        settings[\"precisionType\"]=\"dp\", precround(settings[\"minvalue\"],studentPrecision),\n        settings[\"precisionType\"]=\"sigfig\", siground(settings[\"minvalue\"],studentPrecision),\n        settings[\"minvalue\"]\n    )\n\nraw_maxvalue:\n    switch(\n        student_is_scientific, siground(settings[\"maxvalue\"],studentPrecision+scientific_precision_offset),\n        settings[\"precisionType\"]=\"dp\", precround(settings[\"maxvalue\"],studentPrecision),\n        settings[\"precisionType\"]=\"sigfig\", siground(settings[\"maxvalue\"],studentPrecision),\n        settings[\"maxvalue\"]\n    )\n\nminvalue: min(raw_minvalue,raw_maxvalue)\n\nmaxvalue: max(raw_minvalue,raw_maxvalue)\n\nvalidNumber (Is the student's answer a valid number?):\n    if(isnan(studentNumber),\n        warn(translate(\"part.numberentry.answer invalid\"));\n        fail(translate(\"part.numberentry.answer invalid\"))\n    ,\n        true\n    )\n\nnumberInRange (Is the student's number in the allowed range?):\n    if(studentNumber>=minvalue and studentNumber<=maxvalue,\n        correct()\n    ,\n        incorrect();\n        end()\n    )\n\ncorrectPrecision (Has the student's answer been given to the desired precision?):     \n    if(\n        if(student_is_scientific,\n            togivenprecision_scientific(studentAnswer,settings['precisionType'],settings['precision']),\n            togivenprecision(cleanedStudentAnswer,settings['precisionType'],settings['precision'],settings[\"strictPrecision\"])\n        )\n    ,\n        true\n    ,\n        multiply_credit(settings[\"precisionPC\"],settings[\"precisionMessage\"]);\n        false\n    )\n\nmark (Mark the student's answer):\n    apply(validNumber);\n    apply(numberInRange);\n    assert(numberInRange,end());\n    if(isFraction,\n        apply(cancelled)\n    ,\n        apply(correctPrecision)\n    )\n \ninterpreted_answer (The student's answer, to be reused by other parts):\n    apply(validNumber);\n    studentNumber\n\n", "multipleresponse": "numAnswers: len(settings[\"matrix\"])\n\nnumChoices: if(numAnswers=1,1,len(settings[\"matrix\"][0]))\n\nnumTicks (How many options did the student tick?):\n  sum(map(sum(map(if(x,1,0),x,row)),row,studentAnswer))\n\nwrongNumber:\n  assert(numTicks >= settings[\"minAnswers\"] and (settings[\"maxAnswers\"]=0 or numTicks<=settings[\"maxAnswers\"]),\n    if(settings[\"warningType\"]=\"prevent\",\n        fail(translate(\"part.mcq.wrong number of choices\"))\n    ,\n        incorrect(translate(\"part.mcq.wrong number of choices\"));\n        end()\n    )\n  )\n\ntick_indexes (Indexes of choice/answer pairs):\n    flatten(map(\n        map([x,y], x, shuffleAnswers),\n        y,\n        shuffleChoices\n    ))\n\nonly_ticked_score_ticks (The score for each choice/answer pair):\n  map(\n    if(studentAnswer[x][y],\n      let(distractor,settings[\"distractors\"][x][y], credit, if(marks=0,0,settings[\"matrix\"][x][y]/marks),\n        switch(\n          credit<>0,\n            if(not isnonemptyhtml(distractor),\n              add_credit(credit,translate(if(credit>0,'part.mcq.correct choice','part.mcq.incorrect choice')))\n            ,\n              add_credit(credit,distractor)\n            )\n          ,\n            if(isnonemptyhtml(distractor),negative_feedback(distractor),if(marks<>0,negative_feedback(translate('part.mcq.incorrect choice')),false))\n        );credit\n      )\n    ,\n      0\n    ),\n    [x,y],\n    tick_indexes\n  )\n\n\nlayout_tick_indexes (Indexes of choice/answer pairs shown in the layout):\n    filter(layout[tick[0]][tick[1]],tick,tick_indexes)\n\nbinary_score_ticks (Scores and feedback for each choice/answer pair, in the \"binary\" marking method):\n    let(\n        per_tick, 1/len(layout_tick_indexes),\n        scores,map(\n            let(distractor,settings[\"distractors\"][x][y],\n                should_tick, settings[\"matrix\"][x][y]>0,\n                if(studentAnswer[x][y]=should_tick,\n                    per_tick\n                ,\n                    assert(not isnonemptyhtml(distractor),negative_feedback(distractor));\n                    0\n                )\n            ),\n            [x,y],\n            layout_tick_indexes\n        ),\n        total, sum(scores),\n        switch(\n            total=1,correct(),\n            total=0 or settings[\"markingMethod\"]=\"all-or-nothing\",incorrect(),\n            set_credit(total,translate('part.marking.partially correct'))\n        )\n    )\n\nscore_ticks:\n    switch(\n        settings[\"markingMethod\"] in [\"score per matched cell\",\"all-or-nothing\"], apply(binary_score_ticks);binary_score_ticks,\n        apply(only_ticked_score_ticks);only_ticked_score_ticks\n    )\n\ntotal_score: \n    sum(score_ticks)\n\nmark:\n  assert(marks>0,correct());  // any answer is correct when 0 marks are available\n  assert(settings[\"markingMethod\"]<>\"sum ticked cells\" or numTicks>0,\n    warn(translate(\"part.marking.nothing entered\"));\n    fail(translate(\"part.marking.nothing entered\"))\n  );\n  apply(wrongNumber);\n  apply(score_ticks)\n\ninterpreted_answer (The student's answer, to be reused by other parts):\n  studentAnswer\n"};
        });
        ;
Numbas.queueScript('SCORM_API_wrapper', [], function(module) {
/* ===========================================================
pipwerks SCORM Wrapper for JavaScript
v1.1.20121005
Created by Philip Hutchison, January 2008
https://github.com/pipwerks/scorm-api-wrapper
Copyright (c) Philip Hutchison
MIT-style license: http://pipwerks.mit-license.org/
This wrapper works with both SCORM 1.2 and SCORM 2004.
Inspired by APIWrapper.js, created by the ADL and
Concurrent Technologies Corporation, distributed by
the ADL (http://www.adlnet.gov/scorm).
SCORM.API.find() and SCORM.API.get() functions based
on ADL code, modified by Mike Rustici
(http://www.scorm.com/resources/apifinder/SCORMAPIFinder.htm),
further modified by Philip Hutchison
=============================================================== */
var pipwerks = {};                                  //pipwerks 'namespace' helps ensure no conflicts with possible other "SCORM" variables
pipwerks.UTILS = {};                                //For holding UTILS functions
pipwerks.debug = { isActive: false };                //Enable (true) or disable (false) for debug mode
pipwerks.SCORM = {                                  //Define the SCORM object
    version:    null,                               //Store SCORM version.
    handleCompletionStatus: true,                   //Whether or not the wrapper should automatically handle the initial completion status
    handleExitMode: true,                           //Whether or not the wrapper should automatically handle the exit mode
    API:        { handle: null,
                  isFound: false },                 //Create API child object
    connection: { isActive: false },                //Create connection child object
    data:       { completionStatus: null,
                  exitStatus: null },               //Create data child object
    debug:      {}                                  //Create debug child object
};
/* --------------------------------------------------------------------------------
   pipwerks.SCORM.isAvailable
   A simple function to allow Flash ExternalInterface to confirm
   presence of JS wrapper before attempting any LMS communication.
   Parameters: none
   Returns:    Boolean (true)
----------------------------------------------------------------------------------- */
pipwerks.SCORM.isAvailable = function() {
    return true;
};
// ------------------------------------------------------------------------- //
// --- SCORM.API functions ------------------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
   pipwerks.SCORM.API.find(window)
   Looks for an object named API in parent and opener windows
   Parameters: window (the browser window object).
   Returns:    Object if API is found, null if no API found
---------------------------------------------------------------------------- */
pipwerks.SCORM.API.find = function(win) {
    var API = null,
        findAttempts = 0,
        findAttemptLimit = 500,
        errorGettingAPI = false,
        traceMsgPrefix = "SCORM.API.find",
        trace = pipwerks.UTILS.trace,
        scorm = pipwerks.SCORM;
    try {
        while (!errorGettingAPI &&
               (!win.API && !win.API_1484_11) &&
               (win.parent) &&
               (win.parent != win) &&
               (findAttempts <= findAttemptLimit)) {
                    findAttempts++;
                    win = win.parent;
        }
    } catch(e) {
        errorGettingAPI = e;
    }
    try {
        if(scorm.version) {                                            //If SCORM version is specified by user, look for specific API
            switch(scorm.version) {
                case "2004" :
                    if(win.API_1484_11) {
                        API = win.API_1484_11;
                    } else {
                        trace(traceMsgPrefix + ": SCORM version 2004 was specified by user, but API_1484_11 cannot be found.");
                    }
                    break;
                case "1.2" :
                    if(win.API) {
                        API = win.API;
                    } else {
                        trace(traceMsgPrefix + ": SCORM version 1.2 was specified by user, but API cannot be found.");
                    }
                    break;
            }
        } else {                                                    //If SCORM version not specified by user, look for APIs
            if(win.API_1484_11) {                                    //SCORM 2004-specific API.
                scorm.version = "2004";                                //Set version
                API = win.API_1484_11;
            } else if(win.API) {                                        //SCORM 1.2-specific API
                scorm.version = "1.2";                                //Set version
                API = win.API;
            }
        }
    } catch(e) {
        errorGettingAPI = e;
    }
    if(API) {
        trace(traceMsgPrefix + ": API found. Version: " + scorm.version);
        trace("API: " + API);
    } else {
        trace(traceMsgPrefix + ": Error finding API. \nFind attempts: " + findAttempts + ". \nFind attempt limit: " + findAttemptLimit + ". \nError getting window parent: " + errorGettingAPI);
    }
    return API;
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.API.get()
   Looks for an object named API, first in the current window's frame
   hierarchy and then, if necessary, in the current window's opener window
   hierarchy (if there is an opener window).
   Parameters:  None.
   Returns:     Object if API found, null if no API found
---------------------------------------------------------------------------- */
pipwerks.SCORM.API.get = function() {
    var API = null,
        win = window,
        scorm = pipwerks.SCORM,
        find = scorm.API.find,
        trace = pipwerks.UTILS.trace;
    try {
        API = find(win);

        if(!API && win.parent && win.parent != win) {
            API = find(win.parent);
        }
        if(!API && win.top.opener) {
            API = find(win.top.opener);
        }
        //Special handling for Plateau
        //Thanks to Joseph Venditti for the patch
        if(!API && win.top.opener && win.top.opener.document) {
            API = find(win.top.opener.document);
        }
    } catch {}
    if(API) {
        scorm.API.isFound = true;
    } else {
        trace("API.get failed: Can't find the API!");
    }
    return API;
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.API.getHandle()
   Returns the handle to API object if it was previously set
   Parameters:  None.
   Returns:     Object (the pipwerks.SCORM.API.handle variable).
---------------------------------------------------------------------------- */
pipwerks.SCORM.API.getHandle = function() {
    var API = pipwerks.SCORM.API;
    if(!API.handle && !API.isFound) {
        API.handle = API.get();
    }
    return API.handle;
};
// ------------------------------------------------------------------------- //
// --- pipwerks.SCORM.connection functions --------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
   pipwerks.SCORM.connection.initialize()
   Tells the LMS to initiate the communication session.
   Parameters:  None
   Returns:     Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.connection.initialize = function() {
    var success = false,
        scorm = pipwerks.SCORM,
        completionStatus = scorm.data.completionStatus,
        trace = pipwerks.UTILS.trace,
        makeBoolean = pipwerks.UTILS.StringToBoolean,
        debug = scorm.debug,
        traceMsgPrefix = "SCORM.connection.initialize ";
    trace("connection.initialize called.");
    scorm.connection.isActive = false;
    if(!scorm.connection.isActive) {
        scorm.API.handle = null;
        scorm.API.isFound = false;
        var API = scorm.API.getHandle(),
            errorCode = 0;
        if(API) {
            switch(scorm.version) {
                case "1.2" : success = makeBoolean(API.LMSInitialize("")); break;
                case "2004": success = makeBoolean(API.Initialize("")); break;
            }
            if(success) {
                //Double-check that connection is active and working before returning 'true' boolean
                errorCode = debug.getCode();
                if(errorCode !== null && errorCode === 0) {
                    scorm.connection.isActive = true;
                    if(scorm.handleCompletionStatus) {
                        //Automatically set new launches to incomplete
                        completionStatus = scorm.status("get");
                        if(completionStatus) {
                            switch(completionStatus) {
                                //Both SCORM 1.2 and 2004
                                case "not attempted": scorm.status("set", "incomplete"); break;
                                //SCORM 2004 only
                                case "unknown" : scorm.status("set", "incomplete"); break;
                                //Additional options, presented here in case you'd like to use them
                                //case "completed"  : break;
                                //case "incomplete" : break;
                                //case "passed"     : break;    //SCORM 1.2 only
                                //case "failed"     : break;    //SCORM 1.2 only
                                //case "browsed"    : break;    //SCORM 1.2 only
                            }
                        }
                    }
                } else {
                    success = false;
                    trace(traceMsgPrefix + "failed. \nError code: " + errorCode + " \nError info: " + debug.getInfo(errorCode));
                }
            } else {
                errorCode = debug.getCode();
                if(errorCode !== null && errorCode !== 0) {
                    trace(traceMsgPrefix + "failed. \nError code: " + errorCode + " \nError info: " + debug.getInfo(errorCode));
                } else {
                    trace(traceMsgPrefix + "failed: No response from server.");
                }
            }
        } else {
            trace(traceMsgPrefix + "failed: API is null.");
        }
    } else {
          trace(traceMsgPrefix + "aborted: Connection already active.");
     }
     return success;
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.connection.terminate()
   Tells the LMS to terminate the communication session
   Parameters:  None
   Returns:     Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.connection.terminate = function() {
    var success = false,
        scorm = pipwerks.SCORM,
        exitStatus = scorm.data.exitStatus,
        completionStatus = scorm.data.completionStatus,
        trace = pipwerks.UTILS.trace,
        makeBoolean = pipwerks.UTILS.StringToBoolean,
        debug = scorm.debug,
        traceMsgPrefix = "SCORM.connection.terminate ";
    if(scorm.connection.isActive) {
        var API = scorm.API.getHandle(),
            errorCode = 0;
        if(API) {
             if(scorm.handleExitMode && !exitStatus) {
                if(completionStatus !== "completed" && completionStatus !== "passed") {
                    switch(scorm.version) {
                        case "1.2" : success = scorm.set("cmi.core.exit", "suspend"); break;
                        case "2004": success = scorm.set("cmi.exit", "suspend"); break;
                    }
                } else {
                    switch(scorm.version) {
                        case "1.2" : success = scorm.set("cmi.core.exit", "logout"); break;
                        case "2004": success = scorm.set("cmi.exit", "normal"); break;
                    }
                }
            }
            switch(scorm.version) {
                case "1.2" : success = makeBoolean(API.LMSFinish("")); break;
                case "2004": success = makeBoolean(API.Terminate("")); break;
            }
            if(success) {
                scorm.connection.isActive = false;
            } else {
                errorCode = debug.getCode();
                trace(traceMsgPrefix + "failed. \nError code: " + errorCode + " \nError info: " + debug.getInfo(errorCode));
            }
        } else {
            trace(traceMsgPrefix + "failed: API is null.");
        }
    } else {
        trace(traceMsgPrefix + "aborted: Connection already terminated.");
    }
    return success;
};
// ------------------------------------------------------------------------- //
// --- pipwerks.SCORM.data functions --------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
   pipwerks.SCORM.data.get(parameter)
   Requests information from the LMS.
   Parameter: parameter (string, name of the SCORM data model element)
   Returns:   string (the value of the specified data model element)
---------------------------------------------------------------------------- */
pipwerks.SCORM.data.get = function(parameter) {
    var value = null,
        scorm = pipwerks.SCORM,
        trace = pipwerks.UTILS.trace,
        debug = scorm.debug,
        traceMsgPrefix = "SCORM.data.get(" + parameter + ") ";
    if(scorm.connection.isActive) {
        var API = scorm.API.getHandle(),
            errorCode = 0;
          if(API) {
            switch(scorm.version) {
                case "1.2" : value = API.LMSGetValue(parameter); break;
                case "2004": value = API.GetValue(parameter); break;
            }
            errorCode = debug.getCode();
            //GetValue returns an empty string on errors
            //If value is an empty string, check errorCode to make sure there are no errors
            if(value !== "" || errorCode === 0) {
                //GetValue is successful.
                //If parameter is lesson_status/completion_status or exit status, let's
                //grab the value and cache it so we can check it during connection.terminate()
                switch(parameter) {
                    case "cmi.core.lesson_status":
                    case "cmi.completion_status" : scorm.data.completionStatus = value; break;
                    case "cmi.core.exit":
                    case "cmi.exit" : scorm.data.exitStatus = value; break;
                }
            } else {
                trace(traceMsgPrefix + "failed. \nError code: " + errorCode + "\nError info: " + debug.getInfo(errorCode));
            }
        } else {
            trace(traceMsgPrefix + "failed: API is null.");
        }
    } else {
        trace(traceMsgPrefix + "failed: API connection is inactive.");
    }
    trace(traceMsgPrefix + " value: " + value);
    return String(value);
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.data.set()
   Tells the LMS to assign the value to the named data model element.
   Also stores the SCO's completion status in a variable named
   pipwerks.SCORM.data.completionStatus. This variable is checked whenever
   pipwerks.SCORM.connection.terminate() is invoked.
   Parameters: parameter (string). The data model element
               value (string). The value for the data model element
   Returns:    Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.data.set = function(parameter, value) {
    var success = false,
        scorm = pipwerks.SCORM,
        trace = pipwerks.UTILS.trace,
        makeBoolean = pipwerks.UTILS.StringToBoolean,
        debug = scorm.debug,
        traceMsgPrefix = "SCORM.data.set(" + parameter + ") ";
    if(scorm.connection.isActive) {
        var API = scorm.API.getHandle(),
            errorCode = 0;
        if(API) {
            switch(scorm.version) {
                case "1.2" : success = makeBoolean(API.LMSSetValue(parameter, value)); break;
                case "2004": success = makeBoolean(API.SetValue(parameter, value)); break;
            }
            if(success) {
                if(parameter === "cmi.core.lesson_status" || parameter === "cmi.completion_status") {
                    scorm.data.completionStatus = value;
                }
            } else {
                trace(traceMsgPrefix + "failed. \nError code: " + errorCode + ". \nError info: " + debug.getInfo(errorCode));
            }
        } else {
            trace(traceMsgPrefix + "failed: API is null.");
        }
    } else {
        trace(traceMsgPrefix + "failed: API connection is inactive.");
    }
    return success;
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.data.save()
   Instructs the LMS to persist all data to this point in the session
   Parameters: None
   Returns:    Boolean
---------------------------------------------------------------------------- */
pipwerks.SCORM.data.save = function() {
    var success = false,
        scorm = pipwerks.SCORM,
        trace = pipwerks.UTILS.trace,
        makeBoolean = pipwerks.UTILS.StringToBoolean,
        traceMsgPrefix = "SCORM.data.save failed";
    if(scorm.connection.isActive) {
        var API = scorm.API.getHandle();
        if(API) {
            switch(scorm.version) {
                case "1.2" : success = makeBoolean(API.LMSCommit("")); break;
                case "2004": success = makeBoolean(API.Commit("")); break;
            }
        } else {
            trace(traceMsgPrefix + ": API is null.");
        }
    } else {
        trace(traceMsgPrefix + ": API connection is inactive.");
    }
    return success;
};
pipwerks.SCORM.status = function(action, status) {
    var success = false,
        scorm = pipwerks.SCORM,
        trace = pipwerks.UTILS.trace,
        traceMsgPrefix = "SCORM.getStatus failed",
        cmi = "";
    if(action !== null) {
        switch(scorm.version) {
            case "1.2" : cmi = "cmi.core.lesson_status"; break;
            case "2004": cmi = "cmi.completion_status"; break;
        }
        switch(action) {
            case "get": success = scorm.data.get(cmi); break;
            case "set": if(status !== null) {
                            success = scorm.data.set(cmi, status);
                        } else {
                            success = false;
                            trace(traceMsgPrefix + ": status was not specified.");
                        }
                        break;
            default : success = false;
                        trace(traceMsgPrefix + ": no valid action was specified.");
        }
    } else {
        trace(traceMsgPrefix + ": action was not specified.");
    }
    return success;
};
// ------------------------------------------------------------------------- //
// --- pipwerks.SCORM.debug functions -------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
   pipwerks.SCORM.debug.getCode
   Requests the error code for the current error state from the LMS
   Parameters: None
   Returns:    Integer (the last error code).
---------------------------------------------------------------------------- */
pipwerks.SCORM.debug.getCode = function() {
    var scorm = pipwerks.SCORM,
        API = scorm.API.getHandle(),
        trace = pipwerks.UTILS.trace,
        code = 0;
    if(API) {
        switch(scorm.version) {
            case "1.2" : code = parseInt(API.LMSGetLastError(), 10); break;
            case "2004": code = parseInt(API.GetLastError(), 10); break;
        }
    } else {
        trace("SCORM.debug.getCode failed: API is null.");
    }
    return code;
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.debug.getInfo()
   "Used by a SCO to request the textual description for the error code
   specified by the value of [errorCode]."
   Parameters: errorCode (integer).
   Returns:    String.
----------------------------------------------------------------------------- */
pipwerks.SCORM.debug.getInfo = function(errorCode) {
    var scorm = pipwerks.SCORM,
        API = scorm.API.getHandle(),
        trace = pipwerks.UTILS.trace,
        result = "";
    if(API) {
        switch(scorm.version) {
            case "1.2" : result = API.LMSGetErrorString(errorCode.toString()); break;
            case "2004": result = API.GetErrorString(errorCode.toString()); break;
        }
    } else {
        trace("SCORM.debug.getInfo failed: API is null.");
    }
    return String(result);
};
/* -------------------------------------------------------------------------
   pipwerks.SCORM.debug.getDiagnosticInfo
   "Exists for LMS specific use. It allows the LMS to define additional
   diagnostic information through the API Instance."
   Parameters: errorCode (integer).
   Returns:    String (Additional diagnostic information about the given error code).
---------------------------------------------------------------------------- */
pipwerks.SCORM.debug.getDiagnosticInfo = function(errorCode) {
    var scorm = pipwerks.SCORM,
        API = scorm.API.getHandle(),
        trace = pipwerks.UTILS.trace,
        result = "";
    if(API) {
        switch(scorm.version) {
            case "1.2" : result = API.LMSGetDiagnostic(errorCode); break;
            case "2004": result = API.GetDiagnostic(errorCode); break;
        }
    } else {
        trace("SCORM.debug.getDiagnosticInfo failed: API is null.");
    }
    return String(result);
};
// ------------------------------------------------------------------------- //
// --- Shortcuts! ---------------------------------------------------------- //
// ------------------------------------------------------------------------- //
// Because nobody likes typing verbose code.
pipwerks.SCORM.init = pipwerks.SCORM.connection.initialize;
pipwerks.SCORM.get = pipwerks.SCORM.data.get;
pipwerks.SCORM.set = pipwerks.SCORM.data.set;
pipwerks.SCORM.save = pipwerks.SCORM.data.save;
pipwerks.SCORM.quit = pipwerks.SCORM.connection.terminate;
// ------------------------------------------------------------------------- //
// --- pipwerks.UTILS functions -------------------------------------------- //
// ------------------------------------------------------------------------- //
/* -------------------------------------------------------------------------
   pipwerks.UTILS.StringToBoolean()
   Converts 'boolean strings' into actual valid booleans.
   (Most values returned from the API are the strings "true" and "false".)
   Parameters: String
   Returns:    Boolean
---------------------------------------------------------------------------- */
pipwerks.UTILS.StringToBoolean = function(value) {
    var t = typeof value;
    switch(t) {
       //typeof new String("true") === "object", so handle objects as string via fall-through.
       //See https://github.com/pipwerks/scorm-api-wrapper/issues/3
       case "object":
       case "string": return (/(true|1)/i).test(value);
       case "number": return !!value;
       case "boolean": return value;
       case "undefined": return null;
       default: return false;
    }
};
/* -------------------------------------------------------------------------
   pipwerks.UTILS.trace()
   Displays error messages when in debug mode.
   Parameters: msg (string)
   Return:     None
---------------------------------------------------------------------------- */
pipwerks.UTILS.trace = function(msg) {
     if(pipwerks.debug.isActive) {
        if(window.console && window.console.log) {
            console.log(msg);
        } else {
            //alert(msg);
        }
     }
};
module.exports.pipwerks = pipwerks
});
;
Numbas.queueScript('analysis-display', ['base', 'download', 'util', 'csv', 'display-base'], function() {
    Numbas.analysis = {};

    /**
     * Given max scores for an item for each attempt, describe it as either `undefined`, `"varies"` or a number.
     * The max score is `undefined` if no attempt used this item.
     * It's `"varies"` if there are two or more attempts with different values.
     * It's a number if every attempt that uses this item has the same value.
     *
     * @param {Array.<number>} scores
     * @returns {string}
     */
    const describe_max_score = Numbas.analysis.describe_max_score = function(scores) {
        const unique_scores = new Set(scores.filter((s) => s !== undefined));
        if(unique_scores.size == 0) {
            return undefined;
        } else if(unique_scores.size > 1) {
            return 'varies';
        } else {
            return Array.from(unique_scores)[0];
        }
    }

    /**
     * A file uploaded by the user. It should be an attempt data file produced by the Numbas exam runtime.
     * This file is decoded, decrypted and parsed as JSON. The file is considered succesfully processed if all of these steps succeed.
     */
    class AttemptFile {
        constructor(file, vm) {
            this.file = file;
            this.vm = vm;
            this.content = Knockout.observable(null);
            this.error = Knockout.observable(null);
            this.raw_text = Knockout.observable(null);

            this.status = Knockout.computed(() => {
                if(this.error()) {
                    return 'error';
                } else if(this.content() && this.attempt_grouped_questions()) {
                    return 'processed';
                } else {
                    return 'processing';
                }
            });
            this.status_class = Knockout.computed(() => {
                return {
                    'error': 'danger',
                    'processed': '',
                    'processing': 'info'
                }[this.status()] || '';
            });

            this.student_name = Knockout.computed(() => this.content()?.student_name || R('analysis.student name.anonymous'));

            this.start_time = Knockout.computed(() => new Date(this.content()?.start));

            this.score = Knockout.computed(() => this.content()?.score || 0);
            this.max_score = Knockout.computed(() => this.content()?.max_score || 0);

            this.score_percentage = Knockout.computed(() => this.score() / this.max_score());

            this.download_url = window.URL.createObjectURL(this.file);

            /** For each question defined in the exam, an object relating it to the group and question index it appeared at in this attempt.
             */
            this.attempt_grouped_questions = Knockout.computed(() => {
                const exam_object = this.vm.exam_object;
                const content = this.content();
                if(!(content && exam_object)) {
                    return;
                }

                const attempt_grouped_questions = exam_object.question_groups.map(() => []);

                const question_order = content.questionGroupOrder.flatMap((i, s) => content.questionSubsets[s].map((n) => {
                    return {group_number:i, question_number:n}
                })).map((d, i) => {
                    d.data = content.questions[i]; return d
                });
                question_order.forEach(({group_number, question_number, data}) => {
                    attempt_grouped_questions[group_number][question_number] = data;
                });

                return attempt_grouped_questions;
            }, this);

            /** Data for the row corresponding to this attempt in the question totals-only data table.
             */
            this.question_table_row = Knockout.computed(() => {
                const exam_object = this.vm.exam_object;
                if(!(this.content() && exam_object)) {
                    return [];
                }
                const attempt_grouped_questions = this.attempt_grouped_questions();

                const row = [this.student_name(), this.score()];

                exam_object.question_groups.forEach((g, gi) => {
                    g.questions.forEach((q, qi) => {
                        const attempt_question = attempt_grouped_questions[gi][qi];
                        row.push(attempt_question?.score);
                    });
                });

                return row;
            });

            /** Data for the row corresponding to this attempt in the full data table.
             */
            this.full_table_row = Knockout.computed(() => {
                const exam_object = this.vm.exam_object;
                if(!(this.content() && exam_object)) {
                    return [];
                }
                const attempt_grouped_questions = this.attempt_grouped_questions();

                const row = [this.student_name(), this.score()];

                exam_object.question_groups.forEach((g, gi) => {
                    g.questions.forEach((q, qi) => {
                        const attempt_question = attempt_grouped_questions[gi][qi];

                        row.push(attempt_question?.score);

                        if(q.partsMode == 'explore') {
                            return;
                        }

                        q.parts.forEach((p, pi) => {
                            const attempt_part = attempt_question?.parts[pi];
                            row.push(attempt_part?.score);
                            if(p.type != 'gapfill') {
                                row.push(attempt_part?.student_answer || '');
                            }
                            p.gaps?.forEach((g, ggi) => {
                                const attempt_gap = attempt_part?.gaps[ggi];
                                row.push(attempt_gap?.score);
                                row.push(attempt_gap?.student_answer || '');
                            });
                            p.steps?.forEach((s, si) => {
                                const attempt_step = attempt_part?.steps[si];
                                row.push(attempt_step?.score);
                                row.push(attempt_step?.student_answer || '');
                            });
                        });
                    });
                });

                return row;
            });

            this.decrypt();
        }

        /** Replace the underlying `file` property with a new File object.
         *
         * @param {File} file
         */
        replace_file(file) {
            this.file = file;
            this.decrypt();
        }

        /** Serialise this file to JSON to store in `history.state`.
         *
         * @returns {object}
         */
        as_json() {
            return {
                filename: this.file.name,
                raw_text: this.raw_text()
            }
        }

        /** Forget about this file - remove it from the list of uploaded files.
         */
        remove() {
            this.vm.uploaded_files.remove(this);
        }

        /** Decrypt and parse this file's contents.
         */
        async decrypt() {
            try {
                const raw_text = await this.file.text();
                this.raw_text(raw_text);

                const encoded_text = raw_text.slice(raw_text.match(/^----$/m).index + 5);
                const encrypted_text = Numbas.util.b64decode(encoded_text);

                const passcode = this.vm.passcode();
                this.decrypted_text = await Numbas.download.decrypt(encrypted_text, passcode);

                this.content(JSON.parse(this.decrypted_text));

                this.error(null);
            } catch(e) {
                this.error(e);
                return;
            }
        }

        /** The SCORM data model for this attempt.
         *
         * @returns {object}
         */
        scorm_cmi() {
            const suspend_data = this.content();
            const cmi = {
                'cmi.entry': 'resume',
                'cmi.mode': 'review',
                'cmi.suspend_data': JSON.stringify(suspend_data),
                'cmi.learner_name': this.student_name(),
                'cmi.score.raw': this.score(),
                'numbas.user_role': 'instructor'
            }

            var partAcc = 0;

            /** Initialise SCORM elements for the given part.
             *
             * @param {object} p - Suspend data for the part.
             * @param {string} path
             */
            function visit_part(p, path) {
                const prepath = `cmi.interactions.${partAcc}.`;
                cmi[prepath + 'id'] = path;
                cmi[prepath + 'learner_response'] = p.student_answer;
                partAcc += 1;
                if(p.gaps) {
                    p.gaps.forEach((g, i) => visit_part(g, `${path}g${i}`));
                }
                if(p.steps) {
                    p.steps.forEach((s, i) => visit_part(s, `${path}s${i}`));
                }
            }
            suspend_data.questions.forEach((q, i) => {
                const qid = `q${i}`;
                cmi[`cmi.objectives.${i}.id`] = qid;
                cmi[`cmi.objectives.${i}.score.raw`] = q.score;
                q.parts.forEach((p, j) => visit_part(p, `${qid}p${j}`));
            });
            cmi['cmi.objectives._count'] = suspend_data.questions.length;
            cmi['cmi.interactions._count'] = partAcc;
            return cmi;
        }
    }

    class ViewModel {
        constructor(exam_source) {
            /** The definition of the exam, as a string.
             */
            this.exam_source = exam_source;

            const exam_json = exam_source.slice(exam_source.indexOf("\n") + 1);

            /** The exam definition, loaded from `source.exam` in this package, as an object.
             */
            this.exam_object = JSON.parse(exam_json);

            /** List of the uploaded files which have successfully uploaded, before they are decrypted. */
            this.uploaded_files = Knockout.observableArray();

            /** Definitions of all of the questions in the exam.
             */
            this.all_questions = Knockout.observableArray([]);

            /** A different decryption key to use, entered by the user.
             */
            this.overridden_passcode = Knockout.observable('');

            /** The decryption key for attempt data files, loaded from the exam definition.
             */
            this.default_passcode = Knockout.observable(this.exam_object?.navigation?.downloadEncryptionKey);

            /** Should the `overridden_passcode` be used instead of `default_passcode`?
             */
            this.override_passcode = Knockout.observable(false);

            /** The current tab the user is on. Toggles between 'upload', 'list_files', and 'table'.*/
            this.current_tab = Knockout.observable('list_files');

            this.current_tab.subscribe((v) => {
                document.body.dataset.currentTab = v;
            });

            /**
             * Descriptive names for each column in the full table.
             * This is an array of four rows, containing cells that span several rows or columns.
             */
            this.table_header_readable = Knockout.observableArray([[]]);

            /** Machine-readable names for each column in the full table, giving the path to the corresponding question/part.
             */
            this.table_header_computer = Knockout.observableArray([]);

            this.table_format_options = Knockout.observableArray([
                { label: 'total', name: R('analysis.table.total')},
                { label: 'question', name: R('analysis.table.question')},
                { label: 'all', name: R('analysis.table.all')}
            ]);

            /** The currently selected table format, from the above labels.
             */
            this.table_format = Knockout.observable(this.table_format_options()[0]);

            this.reviewing_file = Knockout.observable(null);

            this.review_file = (file) => {
                const changed = file != this.reviewing_file();
                this.reviewing_file(file);
                this.current_tab('review');
                if(changed) {
                    window.API_1484_11 = new SCORM_API({scorm_cmi: file.scorm_cmi()});
                    const existing_container = document.querySelector('section[data-tab="review"] numbas-exam');
                    const exam_container = document.createElement('numbas-exam');
                    const source_element = existing_container.querySelector('script[type="application/numbas-exam"]');
                    if(source_element) {
                        exam_container.append(source_element);
                        exam_container.append(existing_container.querySelector('script[slot="extension-data"]'));
                    } else {
                        const {source_url, extension_data} = Numbas.get_exam_init_data();
                        exam_container.setAttribute('source_url', source_url);
                        const extension_data_script = document.createElement('script');
                        extension_data_script.setAttribute('type', 'application/json');
                        extension_data_script.setAttribute('slot','extension-data');
                        extension_data_script.textContent = extension_data;
                        exam_container.append(extension_data_script);
                    }
                    exam_container.setAttribute('storage','scorm');
                    existing_container.replaceWith(exam_container);
                }
            };

            this.reviewing_attempt_text = Knockout.computed(function() {
                const file = this.reviewing_file();
                if(!file) {
                    return '';
                }
                return R('analysis.reviewing attempt', {student_name: file.student_name()});
            }, this);

            /** The uploaded files, sorted by status and then by student name.
             */
            this.sorted_files = Knockout.computed(function() {
                return this.uploaded_files().slice().sort((a, b) => {
                    const a_status = a.status();
                    const b_status = b.status();
                    if(a_status != b_status) {
                        return a_status < b_status ? -1 : a_status > b_status ? 1 : 0;
                    }
                    const canonical_name = (f) => f.student_name().toLowerCase().trim();
                    const a_name = canonical_name(a);
                    const b_name = canonical_name(b);

                    return a_name < b_name ? -1 : a_name > b_name ? 1 : 0;
                })
            }, this);

            /** Just uploaded files which have been succesfully decrypted. Only these are shown in the results table.
             */
            this.decrypted_files = Knockout.computed(function() {
                return this.sorted_files().filter((f) => f.status() == 'processed');
            }, this);

            /** The maximum possible score for the exam.
             */
            this.max_score = Knockout.computed(function() {
                return describe_max_score(this.decrypted_files().map((a) => a.content()?.max_score));
            }, this);


            /** The "expected results" row for the "all details" table: available marks for the exam and each question and part.
             */
            this.full_expected_results_row = Knockout.computed(() => {
                const attempts = this.decrypted_files();
                const exam_object = this.exam_object;

                if(!(exam_object && attempts.length)) {
                    return [];
                }

                const row = [
                    R("analysis.expected"),
                    describe_max_score(attempts.map((a) => a.content().max_score))
                ];
                const attempt_grouped_questions = attempts.map((attempt) => attempt.attempt_grouped_questions());
                exam_object.question_groups.forEach((g, gi) => {
                    g.questions.forEach((q, qi) => {
                        const attempt_question = attempt_grouped_questions.map((a) => a[gi][qi]);

                        row.push(describe_max_score(attempt_question.map((q) => q?.max_score)));

                        if(q.partsMode == 'explore') {
                            return;
                        }

                        q.parts.forEach((p, pi) => {
                            const attempt_part = attempt_question.map((q) => q?.parts[pi]);
                            row.push(describe_max_score(attempt_part.map((p) => p?.max_score)));
                            if(p.type != 'gapfill') {
                                row.push('');
                            }
                            p.gaps?.forEach((g, ggi) => {
                                const attempt_gap = attempt_part.map((p) => p?.gaps[ggi]);
                                row.push(describe_max_score(attempt_gap.map((g) => g?.max_score)));
                                row.push('');
                            });
                            p.steps?.forEach((s, si) => {
                                const attempt_step = attempt_part.map((p) => p?.steps[si]);
                                row.push(describe_max_score(attempt_step.map((s) => s?.max_score)));
                                row.push('');
                            });
                        });
                    });
                });

                return row;
            }, this);

            /** A text summary of the files and their status.
             */
            this.file_summary = Knockout.computed(() => {
                const uploaded_files = this.uploaded_files();
                const decrypted_files = this.decrypted_files();
                if(uploaded_files.length == 0) {
                    return R('analysis.summary.no files');
                } else if(decrypted_files.length == 0) {
                    return R('analysis.summary.no decrypted files');
                } else if(decrypted_files.length == 1) {
                    return R('analysis.summary.one file');
                } else {
                    return R('analysis.summary.several files', {num_files: decrypted_files.length});
                }
            }, this);

            /** The passcode to use for decrypting files - defaults to that stored in the exam definition,
             * but can be overridden if eg there are multiple versions with different passcodes
             * In such cases, the exam used to generate the analysis page must not miss questions that the students could have seen.
             */
            this.passcode = Knockout.computed(function() {
                if (this.override_passcode()) {
                    return this.overridden_passcode();
                }
                return this.default_passcode();
            }, this);

            /** A File object representing the currently-shown table in CSV format.
             */
            this.download_table = Knockout.computed(() => {
                let table_body;
                const attempts = this.decrypted_files();
                const formatters = {
                    total: () => {
                        table_body = [
                            [R('exam.student name'), R('control.total'), R('analysis.marks available'), R('analysis.percentage')],
                            ...attempts.map((file) => {
                                const content = file.content();
                                return [content.student_name, content.score, content.max_score, (100 * content.score / content.max_score) + '%'];
                            })
                        ];
                    },

                    question: () => {
                        table_body = [
                            [R('exam.student name'), R('control.total'), ...this.all_questions().map((q) => q.name)],
                            [R('analysis.expected'), this.max_score(), ...this.all_questions().map((q) => q.question.max_score)],
                            ...attempts.map((f) => f.question_table_row())
                        ];
                    },

                    all: () => {
                        const header_webpage = this.table_header_readable();
                        const readable_header = header_webpage.map(() => []);
                        const col = header_webpage.map(() => 0);

                        /**
                         * Visit a cell in the `header_webpage` object, and fill in the corresponding parts of `readable_header`, then visit the next cell down.
                         *
                         * @param {number} row - The index of the row to visit.
                         */
                        function visit_cell(row) {
                            const cell = header_webpage[row][col[row]];
                            if(!cell) {
                                return;
                            }
                            col[row] += 1;
                            readable_header[row].push(cell.text);
                            for(let c = 1; c < cell.cols; c++) {
                                readable_header[row].push('');
                            }
                            for(let r = 1;r < cell.rows;r++) {
                                for(let c = 1; c < cell.cols; c++) {
                                    readable_header[row + r].push('');
                                }
                                readable_header[row + r].push('');
                            }
                            // move to the next row down
                            row += cell.rows;
                            if(row < col.length) {
                                // visit all the cells in this row
                                while(readable_header[row].length < readable_header[row - cell.rows].length) {
                                    visit_cell(row);
                                }
                            }
                        }

                        while(col[0] < header_webpage[0].length) {
                            visit_cell(0);
                        }
                        table_body = [this.table_header_computer()];
                        table_body = table_body.concat(readable_header);
                        table_body.push(this.full_expected_results_row());
                        table_body = table_body.concat(attempts.map((f) => f.full_table_row().map((x) => x === undefined ? '' : x)));
                    }
                };

                formatters[this.table_format()?.label]();
                const exam_slug = Numbas.util.slugify(this.exam_object.name);
                const format_slug = Numbas.util.slugify(this.table_format().name);
                const filename = `${exam_slug}-results-${format_slug}.csv`;
                const content = Numbas.csv.from_array(table_body);
                return new File([content], filename);
            });
        }

        /** Load the state from `window.history`.
         */
        async load_state() {
            this.set_exam_details_from_json();

            const state = window.history.state || {};
            if(state.current_tab !== undefined) {
                this.current_tab(state.current_tab);
            }
            if(state.table_format !== undefined) {
                this.table_format(this.table_format_options().find((o) => o.label == state.table_format));
            }
            if(state.files !== undefined) {
                this.uploaded_files(state.files.map((fd) => {
                    const af = new AttemptFile(new File([fd.raw_text], fd.filename), this);
                    af.decryptPromise = af.decrypt();
                    return af;
                }));
            }
            if(state.reviewing_file !== undefined && state.reviewing_file !== null && state.reviewing_file >= 0) {
                const af = this.uploaded_files()[state.reviewing_file];
                af.decryptPromise.then(() => {
                    this.review_file(af);
                });
            }

            Knockout.computed(() => {
                const state = {
                    current_tab: this.current_tab(),
                    table_format: this.table_format().label,
                    files: this.uploaded_files().map((f) => f.as_json()),
                    reviewing_file: this.current_tab() == 'review' ? this.uploaded_files().indexOf(this.reviewing_file()) : undefined
                };

                if(window.history.state?.current_tab != state.current_tab) {
                    window.history.pushState(state, null);
                } else {
                    window.history.replaceState(state, null);
                }
            }, this);

        }

        /** Make the structure of the data tables from the exam definition.
         */
        set_exam_details_from_json() {
            const exam_object = this.exam_object;
            /** Do some processing on the data to produce columns for the results table.
             */
            const originalOrder = [R('exam.student name'), R('control.total')];
            const humanReadableOrder = [[{text:R('exam.student name'), cols:1, rows: 4}, {text: R('control.total'), cols: 1, rows: 4}], [], [], []];
            const all_questions = [];

            exam_object.question_groups.forEach((group_object, group_index) => {
                const group_label = 'group' + group_index;
                group_object.questions.forEach((question_object, question_index) => {
                    const customName = group_object.questionNames[question_index] || question_object.name;
                    const questionName = customName;
                    const question_label = group_label + 'q' + question_index;
                    originalOrder.push(question_label);
                    const question_header = {text: questionName, cols: 1, rows: 1};
                    humanReadableOrder[0].push(question_header);
                    humanReadableOrder[1].push({text: R('analysis.score'), cols: 1, rows: 3});

                    all_questions.push({
                        group: group_object,
                        question: question_object,
                        name: customName,
                        max_score: Knockout.computed(() => {
                            return describe_max_score(this.decrypted_files().map((a) => a.attempt_grouped_questions()[group_index][question_index]?.max_score));
                        }, this)
                    });

                    if(question_object.partsMode != 'explore') {
                        question_object.parts.forEach((part_object, part_index) => {
                            const partKey = part_index;
                            const partName = part_object.name || (Numbas.util.capitalise(R('part')) + " " + partKey);
                            const partType = part_object.type;
                            const part_label = question_label + 'p' + part_index;
                            originalOrder.push(part_label + " score");
                            const part_header = {text: partName, cols: 1, rows: 1}
                            humanReadableOrder[1].push(part_header);
                            humanReadableOrder[2].push({text: R('analysis.score'), cols: 1, rows: 2})
                            if (partType != 'gapfill') {
                                originalOrder.push(part_label + " answer");
                                humanReadableOrder[2].push({text: R('analysis.answer'), cols: 1, rows: 2});
                                part_header.cols += 1;
                            }
                            part_object.gaps?.forEach((gap_object, gap_index) => { //if optional chaining is not supported, update to full if.
                                const gapKey = gap_index;
                                const gapName = gap_object.name || (R('gap') + " " + gapKey);
                                const gap_label = part_label + 'g' + gap_index;
                                originalOrder.push(gap_label + " score");
                                originalOrder.push(gap_label + " answer");
                                humanReadableOrder[2].push({text: gapName, cols: 2, rows: 1});
                                humanReadableOrder[3].push({text: R('analysis.score'), cols: 1, rows: 1});
                                humanReadableOrder[3].push({text: R('analysis.answer'), cols: 1, rows: 1});
                                part_header.cols += 2;
                            });
                            part_object.steps?.forEach((step_object, step_index) => {
                                const stepKey = step_index;
                                const stepName = step_object.name || (R('step') + " " + stepKey);
                                const step_label = part_label + 's' + step_index;
                                originalOrder.push(step_label + " score");
                                originalOrder.push(step_label + "answer");
                                humanReadableOrder[2].push({text: stepName, cols: 2, rows: 1});
                                humanReadableOrder[3].push({text: R('analysis.score'), cols: 1, rows: 1});
                                humanReadableOrder[3].push({text: R('analysis.answer'), cols: 1, rows: 1});
                                part_header.cols += 2;
                            });
                            question_header.cols += part_header.cols;
                        });
                    }
                });
            });

            /** Fill in the view model with the processed data.
             */
            this.all_questions(all_questions);
            this.table_header_computer(originalOrder);
            this.table_header_readable(humanReadableOrder);
        }

        /** Handler for the 'change' event on the upload files input.
         *
         * @param {object} vm
         * @param {Event} evt
         */
        input_files(vm, evt) {
            this.add_files(Array.from(evt.target.files));
        }

        /**
         * Given a list of `File` objects, wrap them in `AttemptFile` and add to the uploaded_files objects.
         *
         * If a file with the same name has already been uploaded, replace it with the new version.
         *
         * @param {Array.<File>} files
         */
        async add_files(files) {
            if (this.current_tab() != 'list_files') {
                this.move_tab('list_files')()
            };
            files.forEach((file) => {
                const existing = this.uploaded_files().find((f) => f.file.name == file.name);
                if(existing) {
                    existing.replace_file(file);
                } else {
                    const af = new AttemptFile(file, this);
                    this.uploaded_files.push(af);
                }
            });
        }

        /**
         * Make a function which will set the current tab to the given one.
         *
         * @param {string} tab - The ID of the tab to move to.
         * @returns {Function}
         */
        move_tab(tab) {
            return () => {
                this.current_tab(tab);
            }
        }
    }




/** A SCORM API.
 * It provides the `window.API_1484_11` object, which SCORM packages use to interact with the data model.
 *
 * @param {object} options
 */
function SCORM_API(options) {
    var data = options.scorm_cmi;

    this.callbacks = new CallbackHandler();

    this.initialise_data(data);

    this.initialise_api();
}
SCORM_API.prototype = {
    /** Has the API been initialised?
     */
    initialized: false,

    /** Has the API been terminated?
     */
    terminated: false,

    /** The code of the last error that was raised
     */
    last_error: 0,

    /**
     * Setup the SCORM data model.
     * Merge in elements loaded from the page with elements saved to localStorage, taking the most recent value when there's a clash.
     *
     * @param {Object<string>} data
     */
    initialise_data: function(data) {
        // create the data model
        this.data = {};
        for(var key in data) {
            this.data[key] = data[key];
        }

        /** SCORM display mode - 'normal' or 'review'
         */
        this.mode = this.data['cmi.mode'];

        /**
         * Is the client allowed to change data model elements?
         * Not allowed in review mode.
         */
        this.allow_set = this.mode == 'normal';

        this.callbacks.trigger('initialise_data');
    },

    /** Initialise the SCORM API and expose it to the SCORM activity.
     */
    initialise_api: function() {
        var sc = this;

        /** The API object to expose to the SCORM activity
         */
        this.API_1484_11 = {};
        ['Initialize', 'Terminate', 'GetLastError', 'GetErrorString', 'GetDiagnostic', 'GetValue', 'SetValue', 'Commit'].forEach(function(fn) {
            sc.API_1484_11[fn] = function() {
                return sc[fn].apply(sc, arguments);
            };
        });

        /** Counts for the various lists in the data model
         */
        this.counts = {
            'comments_from_learner': 0,
            'comments_from_lms': 0,
            'interactions': 0,
            'objectives': 0,
        }
        this.interaction_counts = [];

        /** Set the counts based on the existing data model
         */
        for(var key in this.data) {
            this.check_key_counts_something(key);
        }

        this.callbacks.trigger('initialise_api');
    },

    /** For a given data model key, if it belongs to a list, update the counter for that list.
     *
     * @param {string} key
     */
    check_key_counts_something: function(key) {
        var m;

        m = key.match(/^cmi.(\w+).(\d+)/);
        if(m) {
            var ckey = m[1];
            var n = parseInt(m[2]);
            this.counts[ckey] = Math.max(n + 1, this.counts[ckey]);
            this.data['cmi.' + ckey + '._count'] = this.counts[ckey];
            if(ckey == 'interactions' && this.interaction_counts[n] === undefined) {
                this.interaction_counts[n] = {
                    'objectives': 0,
                    'correct_responses': 0
                }
            }
        }

        m = key.match(/^cmi.interactions.(\d+).(objectives|correct_responses).(\d+)/);
        if(m) {
            var n1 = parseInt(m[1]);
            var skey = m[2];
            var n2 = parseInt(m[3]);
            this.interaction_counts[n1][skey] = Math.max(n2 + 1, this.interaction_counts[n1][skey]);
            this.data['cmi.interactions.' + n1 + '.' + skey + '._count'] = this.interaction_counts[n1][skey];
        }
    },

    Initialize: function(b) {
        this.callbacks.trigger('Initialize', b);
        if(b != '' || this.initialized || this.terminated) {
            return false;
        }
        this.initialized = true;
        return true;
    },

    Terminate: function(b) {
        this.callbacks.trigger('Terminate', b);
        if(b != '' || !this.initialized || this.terminated) {
            return false;
        }
        this.terminated = true;

        return true;
    },

    GetLastError: function() {
        return this.last_error;
    },

    GetErrorString: function(code) {
        return "I haven't written any error strings yet.";
    },

    GetDiagnostic: function(code) {
        return "I haven't written any error handling yet.";
    },

    GetValue: function(key) {
        var v = this.data[key];
        if(v === undefined) {
            return '';
        } else {
            return v;
        }
    },

    SetValue: function(key, value) {
        if(!this.allow_set) {
            return;
        }
        value = (value + '');
        var changed = value != this.data[key];
        if(changed) {
            this.data[key] = value;
            this.check_key_counts_something(key);
        }
        this.callbacks.trigger('SetValue', key, value, changed);
    },

    Commit: function(s) {
        this.callbacks.trigger('Commit');
        return true;
    }
}

class CallbackHandler {
    constructor() {
        this.callbacks = {};
    }

    on(key, fn) {
        if(this.callbacks[key] === undefined) {
            this.callbacks[key] = [];
        }
        this.callbacks[key].push(fn);
    }

    trigger(key) {
        if(!this.callbacks[key]) {
            return;
        }
        var args = Array.prototype.slice.call(arguments, 1);
        this.callbacks[key].forEach(function(fn) {
            fn.apply(this, args);
        });
    }
}






    Numbas.analysis.init = async function() {
        await Numbas.init_promise;

        Numbas.display.localisePage(document.body);

        /*
        if(!window.isSecureContext) {
            document.body.classList.add('not-secure-context');
            return;
        }
        */
        const params = new URLSearchParams(location.search);

        const locale = params.get('locale') || 'en-GB';

        Numbas.locale.set_preferred_locale(locale);

        const source_url = params.get('source_url');

        let exam_source;
        if(source_url) {
            /** Load and parse the .exam file. */
            const r = new XMLHttpRequest();
            r.open('GET', source_url);
            r.send();
            await (new Promise((resolve) => {
                r.addEventListener('load', () => {
                    setTimeout(resolve, 1);
                });
            }));
            exam_source = r.responseText;
        } else {
            exam_source = document.querySelector('numbas-exam script[type="application/numbas-exam"]').textContent;
        }

        const viewModel = new ViewModel(exam_source);
        viewModel.load_state();

        document.body.addEventListener('dragover', (evt) => evt.preventDefault());
        document.body.addEventListener('drop', (evt) => {
            evt.preventDefault();
            const files = Array.from(evt.dataTransfer.items)
                .filter((f) => f.kind == 'file')
                .map((i) => i.getAsFile())
            ;
            viewModel.add_files(files);
        });

        Knockout.applyBindings(viewModel, document.querySelector('main#analysis'));
        window.viewModel = viewModel;

        /** Respond to browser history navigation.
         */
        window.addEventListener('popstate', function(event) {
            if (event.state) {
                viewModel.current_tab(event.state.current_tab);
            }
        });

    };
});
;
Numbas.queueScript('answer-widgets', ['knockout', 'util', 'jme', 'jme-display', 'localisation'], function() {
    var util = Numbas.util;
    if(typeof Knockout === 'undefined') {
        return;
    }

    /** @namespace Numbas.answer_widgets */
    var answer_widgets = Numbas.answer_widgets = {
        /**
         * @enum {Numbas.answer_widgets.custom_answer_widget_params}
         */
        custom_widgets: {}
    };

    var custom_widgets = answer_widgets.custom_widgets;

    /** @typedef Numbas.answer_widgets.custom_answer_widget
     * @function setAnswerJSON
     * @function disable
     * @function enable
     */

    /** @callback Numbas.answer_widgets.custom_answer_widget_constructor
     * @param {Element} element - The parent element of the widget.
     * @param {Numbas.parts.Part} part - The part whose answer the widget represents.
     * @param {string} title - The `title` attribute for the widget: a text description of what the widget represents.
     * @param {Object<Function>} events - Callback functions for events triggered by the widget.
     * @param {Numbas.answer_widgets.answer_changed} answer_changed - A function to call when the entered answer changes.
     * @param {object} options - Any options for the widget.
     * @constructs Numbas.answer_widgets.custom_answer_widget
     */

    /** A function to call when the content of an answer input widget changes.
     *
     * @callback Numbas.answer_widgets.answer_changed
     * @param {Numbas.custom_part_answer} answer
     */

    /** Parameters for registering a custom answer widget.
     *
     * @memberof Numbas.answer_widgets
     * @typedef Numbas.answer_widgets.custom_answer_widget_params
     * @property {string} name - The name of the widget. Used by custom part type definitions to refer to this widget.
     * @property {string} niceName - A readable name to be displayed in the editor.
     * @property {string} signature - The signature of the type of JME value that the input produces.
     * @property {Function} answer_to_jme - Convert a raw answer to a JME token.
     * @property {Array} options_definition - A definition of options that the widget accepts.
     * @property {Numbas.answer_widgets.custom_answer_widget_constructor} widget - A constructor for the widget.
     * @property {Numbas.storage.scorm.inputWidgetStorage} scorm_storage - Methods to save and resume answers using this widget.
     */

    /** Register a custom answer widget.
     *
     * @function
     * @name register_custom_widget
     * @param {Numbas.answer_widgets.custom_answer_widget_params} params
     * @memberof Numbas.answer_widgets
     */
    answer_widgets.register_custom_widget = function(params) {
        var name = params.name;
        custom_widgets[name] = params;
        Numbas.parts.register_custom_part_input_type(name, params.signature);
        Numbas.parts.CustomPart.prototype.student_answer_jme_types[name] = params.answer_to_jme;
        var input_option_types = Numbas.parts.CustomPart.prototype.input_option_types[name] = {};
        if(Numbas.storage) {
            Numbas.storage.inputWidgetStorage[name] = params.scorm_storage;
        }
        params.options_definition.forEach(function(def) {
            var types = {
                'choose_several': 'list of boolean',
                'list_of_strings': 'list of string',
                'choice_maker': 'list of string',
                'number_notation_styles': 'list of string',
                'string': 'string',
                'mathematical_expression': 'string',
                'checkbox': 'boolean',
                'dropdown': 'string',
                'code': 'string',
                'percent': 'number',
                'html': 'string'
            };
            input_option_types[def.name] = types[def.input_type];
        });

        Knockout.components.register('answer-widget-' + name, {
            viewModel: function(params) {
                this.name = name;
                this.params = params;
            },
            template: '<div data-bind="custom_answer_widget: {params: params, name: name}"></div>'
        });
    }


    /** Ensure `v` is an observable, and if it's not given return the default value.
     *
     * @param {object|Observable|undefined} v
     * @param {object} d - The default value.
     * @returns {Observable}
     */
    function defaultObservable(v, d) {
        return v !== undefined ? Knockout.isObservable(v) ? v : Knockout.observable(v) : Knockout.observable(d);
    }

Numbas.signals.on('localisation initialised', () => {
    Knockout.components.register('answer-widget', {
        viewModel: function(params) {
            this.answerJSON = params.answer;
            this.part = params.part;
            this.id = params.id;
            this.disable = params.disable;
            this.widget = params.widget || Knockout.computed(function() {
                var part = Knockout.unwrap(this.part);
                return part && part.input_widget();
            }, this);
            this.widget_options = params.widget_options || Knockout.computed(function() {
                var part = Knockout.unwrap(this.part);
                return part && part.input_options()
            }, this);
            this.classes = {'answer-widget':true};
            this.classes['answer-widget-' + this.widget] = true;
            this.events = params.events;
            this.title = params.title || '';
        },
        template: `
            <span data-bind="if: widget">
                <span data-bind="css: classes, component: {name: 'answer-widget-'+Knockout.unwrap(widget), params: {answerJSON: answerJSON, part: part, id: id, disable: disable, options: widget_options, events: events, title: title}}"></span>
            </span>
        `
    });
    Knockout.components.register('answer-widget-string', {
        viewModel: function(params) {
            this.answerJSON = params.answerJSON;
            var init = Knockout.unwrap(this.answerJSON);
            this.input = Knockout.observable(init.valid ? init.value || '' : '');
            this.id = params.id;
            this.part = params.part;
            this.disable = params.disable;
            this.options = Knockout.unwrap(params.options);
            this.events = params.events;
            this.title = params.title || '';
            this.allowEmpty = this.options.allowEmpty;
            var lastValue = this.input();
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    if(v && v.value != this.input()) {
                        this.input(v.value);
                    }
                }, this),
                this.input.subscribe(function(value) {
                    var empty = value == '';
                    var valid = !empty || this.allowEmpty;
                    if(value != lastValue) {
                        this.answerJSON({valid: valid, value: value, empty: empty});
                    }
                    lastValue = value;
                }, this)
            ];
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
            }
        },
        template: `
            <input type="text" autocapitalize="off" inputmode="text" spellcheck="false" data-bind="textInput: input, autosize: true, disable: Knockout.unwrap(disable) || Knockout.unwrap(part.revealed) || Knockout.unwrap(part.locked), event: events, attr: {title: title, id: id+'-input'}, part_aria_validity: part.display.hasWarnings, part: part.display"/>
        `
    });
    Knockout.components.register('answer-widget-number', {
        viewModel: function(params) {
            var vm = this;
            this.answerJSON = params.answerJSON;
            this.part = params.part;
            this.id = params.id;
            this.options = Knockout.unwrap(params.options);
            this.allowFractions = this.options.allowFractions || false;
            this.allowedNotationStyles = this.options.allowedNotationStyles || ['plain', 'en', 'si-en'];
            this.disable = params.disable;
            this.events = params.events;
            this.title = params.title || '';
            var init = Knockout.unwrap(this.answerJSON);
            /** Clean up a number, to be set as the value for the input widget.
             * It's run through {@link Numbas.math.niceNumber} with the first allowed notation style.
             * `undefined` produces an empty string.
             *
             * @param {number} n
             * @returns {string}
             */
            function cleanNumber(n) {
                if(n === undefined) {
                    return '';
                }
                if(util.isNumber(n, vm.allowFractions, vm.allowedNotationStyles)) {
                    return n + '';
                }
                return Numbas.math.niceNumber(n, {style: vm.allowedNotationStyles[0]}) || '';
            }
            this.input = Knockout.observable(init.valid ? cleanNumber(init.value) : '');
            this.result = Knockout.computed(function() {
                var input = this.input().trim();
                if(input == '') {
                    return {valid:false, empty: true};
                }
                if(!util.isNumber(input, this.allowFractions, this.allowedNotationStyles)) {
                    if(util.isNumber(input, true, this.allowedNotationStyles)) {
                        return {valid: false, warnings: [R('answer.number.fractions not allowed')]};
                    } else {
                        return {valid:false, warnings: [R('answer.number.not a number')]};
                    }
                } else {
                    var n = Numbas.util.parseNumber(input, this.allowFractions, this.allowedNotationStyles);
                    return {valid:true, value: n};
                }
            }, this);
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    if(!v || v.value == this.result().value) {
                        return;
                    }
                    var s = cleanNumber(v.value);
                    if(s != this.input() && v.valid) {
                        this.input(s);
                    }
                }, this)
            ];

            let lastValue = this.input();

            this.setAnswerJSON = Knockout.computed(function() {
                if(Knockout.unwrap(this.disable)) {
                    return;
                }
                if(this.input() != lastValue) {
                    this.answerJSON(this.result());
                    lastValue = this.input();
                }
            }, this);
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.result.dispose();
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <input type="text" autocapitalize="off" inputmode="text" spellcheck="false" data-bind="textInput: input, autosize: true, disable: Knockout.unwrap(disable) || Knockout.unwrap(part.revealed) || Knockout.unwrap(part.locked), event: events, attr: {title: title, id: id+'-input'}, part_aria_validity: part.display.hasWarnings, part: part.display"/>
        `
    });
    Knockout.components.register('answer-widget-jme', {
        viewModel: function(params) {
            this.answerJSON = params.answerJSON;
            var p = this.part = params.part;
            var scope = Knockout.unwrap(p).getScope();
            this.id = params.id;
            this.options = Knockout.unwrap(params.options);
            this.showPreview = this.options.showPreview || false;
            this.returnString = this.options.returnString || false;
            this.disable = params.disable;
            this.events = params.events;
            this.title = params.title || '';
            var init = Knockout.unwrap(this.answerJSON);
            /** Clean a supplied expression, to be used as the value for the input widget.
             * If it's a string, leave it alone.
             * If it's a {@link Numbas.jme.tree}, run it through {@link Numbas.jme.display.treeToJME}.
             *
             * @param {string|Numbas.jme.tree} expr
             * @returns {string}
             */
            function cleanExpression(expr) {
                if(typeof(expr) == 'string') {
                    return expr;
                }
                return Numbas.jme.display.treeToJME(expr, {}, scope) || '';
            }
            this.input = Knockout.observable(init.valid ? cleanExpression(init.value) : '');
            this.latex = Knockout.computed(function() {
                var input = this.input();
                if(input === '') {
                    return '';
                }
                try {
                    var tex = Numbas.jme.display.exprToLaTeX(input, '', scope);
                    if(tex === undefined) {
                        throw(new Numbas.Error('display.part.jme.error making maths'));
                    }
                } catch {
                    return '';
                }
                return tex;
            }, this).extend({throttle:100});
            this.result = Knockout.computed(function() {
                var input = this.input().trim();
                if(input == '') {
                    return {valid:false, empty:true};
                }
                if(this.options.returnString) {
                    return {valid: true, value: input};
                } else {
                    try {
                        var expr = Numbas.jme.compile(input);
                        if(!expr) {
                            return {valid: false, empty: true};
                        }
                        var scope = Knockout.unwrap(p).getScope();
                        var ruleset = new Numbas.jme.rules.Ruleset([], {});
                        expr = Numbas.jme.display.simplifyTree(expr, ruleset, scope);
                        return {valid: true, value: expr}
                    } catch(e) {
                        return {valid: false, warnings: [R('answer.jme.invalid expression', {message:e.message})]};
                    }
                }
            }, this);
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    if(!v || v.value == this.result().value) {
                        return;
                    }
                    var s = cleanExpression(v.value);
                    if(s != this.input() && v.valid) {
                        this.input(s);
                    }
                }, this)
            ];
            var lastValue = this.input();
            this.setAnswerJSON = Knockout.computed(function() {
                if(this.input() != lastValue) {
                    this.answerJSON(this.result());
                    lastValue = this.input();
                }
            }, this);
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.latex.dispose();
                this.result.dispose();
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <input 
                type="text"
                autocapitalize="off"
                inputmode="text"
                spellcheck="false"
                data-bind="event: events, textInput: input, autosize: true, disable: Knockout.unwrap(disable) || Knockout.unwrap(part.revealed) || Knockout.unwrap(part.locked), attr: {id: id+'-input', title: title}, part_aria_validity: part.display.hasWarnings, part: part.display"
            />
            <output class="jme-preview" aria-live="polite" data-bind="visible: showPreview && latex(), attr: {for: id+'-input'}, maths: '\\\\displaystyle{{'+latex()+'}}'"></output>
        `
    });
    Knockout.components.register('answer-widget-gapfill', {
        viewModel: function(params) {
            this.answerJSON = params.answerJSON;
            var part = params.part;
            this.disable = params.disable;
            this.gaps = Knockout.computed(function() {
                return Knockout.unwrap(part.gaps).map(function(gap) {
                    return {answerJSON: Knockout.observable(), part: gap};
                });
            }, this)
            this.setAnswerJSON = Knockout.computed(function() {
                this.answerJSON(this.gaps().map(function(g) {
                    return g.answerJSON()
                }));
            }, this);
            this.dispose = function() {
                this.gaps.dispose();
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <table class="table">
                <tbody data-bind="foreach: gaps">
                    <tr>
                        <th><span data-bind="text: part.header"></span></th>
                        <td><div data-bind="component: {name: 'answer-widget', params: {answer: answerJSON, widget: Knockout.unwrap(part.type).widget, part: part, disable: disable}}"></div></td>
                    </tr>
                </tbody>
            </table>
        `
    });
    Knockout.components.register('answer-widget-matrix', {
        viewModel: function(params) {
            var vm = this;
            this.answerJSON = params.answerJSON;
            this.part = params.part;
            this.id = params.id;
            this.options = Knockout.unwrap(params.options);
            this.disable = params.disable;
            this.title = params.title || '';
            this.events = params.events;
            this.allowFractions = this.options.allowFractions || false;
            this.allowedNotationStyles = this.options.allowedNotationStyles || ['plain', 'en', 'si-en'];
            this.allowResize = this.options.allowResize === undefined ? true : this.options.allowResize;
            this.numRows = this.options.numRows || 1;
            this.numColumns = this.options.numColumns || 1;
            this.minColumns = this.options.minColumns || 0;
            this.maxColumns = this.options.maxColumns || 0;
            this.minRows = this.options.minRows || 0;
            this.maxRows = this.options.maxRows || 0;
            this.prefilledCells = this.options.prefilledCells || [];
            this.showBrackets = this.options.showBrackets === undefined ? true : this.options.showBrackets;
            this.rowHeaders = this.options.rowHeaders || [];
            this.columnHeaders = this.options.columnHeaders || [];
            this.parseCells = this.options.parseCells === undefined ? true : this.options.parseCells;
            var init = Knockout.unwrap(this.answerJSON);
            var value = init.value;
            if(value !== undefined) {
                value = value.map(function(r) {
                    return r.map(function(c) {
                        return vm.parseCells ? Numbas.math.niceNumber(c, {style: vm.allowedNotationStyles[0]}) || '' : c
                    })
                });
            }
            if(!value) {
                value = [];
                for(let i = 0;i < this.numRows;i++) {
                    var row = [];
                    for(let j = 0;j < this.numColumns;j++) {
                        row.push('');
                    }
                    value.push(row);
                }
            }
            this.input = Knockout.observable(value);
            this.result = Knockout.computed(function() {
                var value = this.input().slice().map(function(r) {
                    return r.map(function(cell) {
                        return cell + '';
                    })
                });
                var cells = Array.prototype.concat.apply([], value);
                var empty = cells.every(function(cell) {
                    return !cell.trim()
                });
                if(empty) {
                    return {valid: false, empty: true};
                }
                if(this.parseCells) {
                    var valid = cells.every(function(cell) {
                        return cell.trim() && util.isNumber(cell, vm.allowFractions, vm.allowedNotationStyles)
                    });
                    if(!valid) {
                        var validFractions = cells.every(function(cell) {
                            return util.isNumber(cell, true, vm.allowedNotationStyles)
                        });
                        if(validFractions) {
                            return {valid: false, warnings: [R('answer.matrix.fractions not allowed')]};
                        } else {
                            return {valid:false, warnings: [R('answer.matrix.some cell not a number')]};
                        }
                    } else {
                        var matrix = value.map((row) => row.map((cell) => Numbas.util.parseNumber(cell, this.allowFractions, this.allowedNotationStyles)));
                        matrix.rows = value.length;
                        matrix.columns = matrix.rows > 0 ? value[0].length : 0;
                        return {valid:true, value: matrix};
                    }
                } else {
                    return {valid: true, value: value};
                }
            }, this);
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    if(!v || util.objects_equal(v.value, this.result().value)) {
                        return;
                    }
                    if(v.valid) {
                        this.input(v.value);
                    }
                }, this)
            ];
            var lastValue = this.result();
            this.setAnswerJSON = Knockout.computed(function() {
                var result = this.result();
                var valuesSame =
                    (!result.valid && !lastValue.valid) ||
                    (
                        (result.value !== undefined && lastValue.value !== undefined) &&
                        result.value.length == lastValue.value.length &&
                        result.value.every(function(row, i) {
                            return row.length == lastValue.value[i].length && row.every(function(cell, j) {
                                return cell == lastValue.value[i][j];
                            })
                        })
                    );
                if(!valuesSame || result.valid != lastValue.valid) {
                    this.answerJSON(result);
                }
                lastValue = result;
            }, this);
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.result.dispose();
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <fieldset data-bind="part_aria_validity: part.display.hasWarnings, part: part.display">
                <matrix-input 
                data-bind="attr: {id: id+'-input'}"
                params="value: input, 
                    allowResize: true,
                    disable: disable,
                    allowResize: allowResize,
                    rows: numRows,
                    columns: numColumns,
                    minColumns: minColumns,
                    maxColumns: maxColumns,
                    minRows: minRows,
                    maxRows: maxRows,
                    prefilledCells: prefilledCells,
                    showBrackets: showBrackets,
                    rowHeaders: rowHeaders,
                    columnHeaders: columnHeaders,
                    events: events,
                    title: title
                "></matrix-input>
            </fieldset>
        `
    });


    /**
     * A generic component for entering a matrix.
     * Shows a grid of text inputs, optionally surrounded by brackets and/or with a control box on top to change the number of rows and columns.
     */
    Knockout.components.register('matrix-input', {
        viewModel: function(params) {
            var vm = this;
            this.allowResize = defaultObservable(params.allowResize, false);
            this.minColumns = defaultObservable(params.minColumns, 0);
            this.maxColumns = defaultObservable(params.maxColumns, 0);
            this.minRows = defaultObservable(params.minRows, 0);
            this.maxRows = defaultObservable(params.maxRows, 0);
            this.showBrackets = defaultObservable(params.showBrackets, true);
            this.rowHeaders = defaultObservable(params.rowHeaders, []);
            this.columnHeaders = defaultObservable(params.columnHeaders, []);
            this.prefilledCells = defaultObservable(params.prefilledCells, []);
            this.hasRowHeaders = Knockout.computed(function() {
                return Knockout.unwrap(this.rowHeaders).length > 0;
            }, this);
            this.hasColumnHeaders = Knockout.computed(function() {
                return Knockout.unwrap(this.columnHeaders).length > 0;
            }, this);
            this.title = params.title || '';
            var _numRows = typeof params.rows == 'function' ? params.rows : Knockout.observable(Knockout.unwrap(params.rows) || 2);
            this.numRows = Knockout.computed({
                read: _numRows,
                write: function(v) {
                    v = parseInt(v);
                    var minRows = Knockout.unwrap(this.minRows);
                    var maxRows = Knockout.unwrap(this.maxRows);
                    v = minRows == 0 ? v : Math.max(minRows, v);
                    v = maxRows == 0 ? v : Math.min(maxRows, v);
                    if(v !== _numRows() && !Knockout.unwrap(params.disable)) {
                        return _numRows(v);
                    }
                }
            }, this);
            if(typeof params.rows == 'function') {
                params.rows.subscribe(function(v) {
                    vm.numRows(v);
                });
            }
            var _numColumns = typeof params.columns == 'function' ? params.columns : Knockout.observable(Knockout.unwrap(params.columns) || 2);
            this.numColumns = Knockout.computed({
                read: _numColumns,
                write: function(v) {
                    var minColumns = Knockout.unwrap(this.minColumns);
                    var maxColumns = Knockout.unwrap(this.maxColumns);
                    v = minColumns == 0 ? v : Math.max(minColumns, v);
                    v = maxColumns == 0 ? v : Math.min(maxColumns, v);
                    if(v !== _numColumns() && !Knockout.unwrap(params.disable)) {
                        return _numColumns(v);
                    }
                }
            }, this);
            if(typeof params.columns == 'function') {
                params.columns.subscribe(function(v) {
                    vm.numColumns(v);
                });
            }
            this.value = Knockout.observableArray([]);
            /** Produce the output value for the widget.
             */
            function make_result() {
                var v = vm.value().map(function(row, i) {
                    return row().map(function(cell, j) {
                        return cell.cell()
                    })
                })
                vm.result(v);
            };
            /**
             * Make a new cell.
             *
             * @param {number|string} c - The value of the cell.
             * @param {number} row
             * @param {number} column
             * @returns {object} - `cell` is an observable holding the cell's value.
             */
            function make_cell(c, row, column) {
                var prefilled = ((Knockout.unwrap(vm.prefilledCells) || [])[row] || [])[column];
                var use_prefilled = prefilled != '' && prefilled !== undefined;
                c = use_prefilled ? prefilled : c;
                var cell = {cell: Knockout.observable(c), prefilled: use_prefilled, label: R('matrix input.cell label', {row:row + 1, column:column + 1})};
                cell.cell.subscribe(make_result);
                return cell;
            }
            /** Overwrite the value of the widget with the given matrix.
             *
             * @param {matrix} v
             */
            function setMatrix(v) {
                vm.numRows(v.rows || v.length || 1);
                vm.numColumns(v.columns || (v.length ? v[0].length : 1));
                vm.value(v.map(function(r, row) {
                    return Knockout.observableArray(r.map(function(c, column) {
                        return make_cell(c, row, column)
                    }))
                }));
            }
            setMatrix(Knockout.unwrap(params.value));
            this.disable = params.disable || false;
            this.keydown = function(obj, e) {
                this.oldPos = e.target.selectionStart;
                return true;
            }
            this.moveArrow = function(obj, e) {
                var cell = e.target;
                while(cell && cell.tagName.toUpperCase() != 'TD') {
                    cell = cell.parentElement;
                }
                if(!cell) {
                    return;
                }
                var selectionStart = e.target.selectionStart;
                var column = Array.from(cell.parentElement.children).indexOf(cell);
                let el;

                switch(e.key) {
                case 'ArrowRight':
                    if(selectionStart == this.oldPos && selectionStart == e.target.selectionEnd && e.target.selectionEnd == e.target.value.length) {
                        cell.nextElementSibling?.querySelector('input')?.focus();
                    }
                    break;
                case 'ArrowLeft':
                    if(selectionStart == this.oldPos && selectionStart == e.target.selectionEnd && e.target.selectionEnd == 0) {
                        cell.previousElementSibling?.querySelector('input')?.focus();
                    }
                    break;
                case 'ArrowUp':
                    el = cell.parentElement.previousElementSibling?.children[column]?.querySelector('input');
                    if(el) {
                        el.focus();
                        el.setSelectionRange(this.oldPos, this.oldPos);
                    }
                    break;
                case 'ArrowDown':
                    el = cell.parentElement.nextElementSibling?.children[column]?.querySelector('input');
                    if(el) {
                        el.focus();
                        el.setSelectionRange(this.oldPos, this.oldPos);
                    }
                    break;
                }
                return false;
            }

            this.events = params.events || {};
            var okeydown = params.events && params.events.keydown;
            this.events.keydown = function(obj, e) {
                vm.keydown(obj, e);
                if(okeydown) {
                    return okeydown(obj, e);
                }
                return true;
            };
            var okeyup = params.events && params.events.keyup;
            this.events.keyup = function(obj, e) {
                vm.moveArrow(obj, e);
                if(okeyup) {
                    return okeyup(obj, e);
                }
                return true;
            };

            this.result = Knockout.observableArray([]);
            make_result();
            this.update = function() {
                // update value when number of rows or columns changes
                var numRows = parseInt(this.numRows());
                var numColumns = parseInt(this.numColumns());
                var value = this.value();
                if(numRows == value.length && (numRows == 0 || numColumns == value[0]().length)) {
                    return;
                }
                value.splice(numRows, value.length - numRows);
                for(let i = 0;i < numRows;i++) {
                    var row;
                    if(value.length <= i) {
                        row = [];
                        value.push(Knockout.observableArray(row));
                    } else {
                        row = value[i]();
                    }
                    row.splice(numColumns, row.length - numColumns);
                    for(let j = 0;j < numColumns;j++) {
                        if(row.length <= j) {
                            row.push(make_cell('', i, j));
                        }
                    }
                    value[i](row);
                }
                this.value(value.slice());
                make_result();
            }
            this.updateComputed = Knockout.computed(this.update, this);
            this.subscriptions = [
                params.value.subscribe(function(v) {
                    if(v == this.result()) {
                        return;
                    }
                    setMatrix(v);
                }, this)
            ];
            var firstGo = true;
            //update value with model
            this.setValue = Knockout.computed(function() {
                var v = this.result();
                if(firstGo) {
                    firstGo = false;
                    return;
                }
                params.value(v);
            }, this)
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.updateComputed.dispose();
                this.setValue.dispose();
            }
        },
        template: `
            <div class="matrix-input" data-bind="attr: {title: title}">
                <!-- ko if: allowResize -->
                <div class="matrix-size">
                    <fieldset><legend class="sr-only">${R('matrix input.size control legend')}</legend>
                    <label class="num-rows">${R('matrix input.rows')}: <input type="number" data-bind="event: events, value: numRows, autosize: true, disable: disable, attr: {'min': minRows()==0 ? 1 : minRows(), 'max': maxRows()==0 ? '' : maxRows()}"/></label>
                    <label class="num-columns">${R('matrix input.columns')}: <input type="number" min="1" data-bind="event: events, value: numColumns, autosize: true, disable: disable, attr: {'min': minColumns()==0 ? 1 : minColumns(), 'max': maxColumns()==0 ? '' : maxColumns()}"/></label>
                    </fieldset>
                </div>
                <!-- /ko -->
                <fieldset class="matrix-wrapper">
                    <legend class="sr-only" data-bind="text: title"></legend>
                    <span class="left bracket" data-bind="visible: showBrackets"></span>
                    <table class="matrix">
                        <thead data-bind="if: hasColumnHeaders">
                            <tr>
                                <th data-bind="visible: hasRowHeaders"><span data-bind="latex: rowHeaders()[0]"></span></th>
                                <!-- ko foreach: columnHeaders --><th data-bind="latex: $data"></th><!-- /ko -->
                            </tr>
                        </thead>
                        <tbody data-bind="foreach: value">
                            <tr>
                                <th data-bind="visible: $parent.hasRowHeaders"><span data-bind="latex: $parent.rowHeaders()[$index()+1] || ''"></span></th>
                                <!-- ko foreach: $data -->
                                <td class="cell"><input type="text" autocapitalize="off" inputmode="text" spellcheck="false" data-bind="attr: {'aria-label': label}, textInput: cell, autosize: true, disable: prefilled || $parents[1].disable, event: $parents[1].events"/></td>
                                <!-- /ko -->
                            </tr>
                        </tbody>
                    </table>
                    <span class="right bracket" data-bind="visible: showBrackets"></span>
                </fieldset>
            </div>
        `
    });

    Knockout.components.register('answer-widget-radios', {
        viewModel: function(params) {
            this.part = params.part;
            this.id = params.id;
            this.disable = params.disable;
            this.options = Knockout.unwrap(params.options);
            this.events = params.events;
            this.choices = Knockout.observableArray(this.options.choices);
            this.answerAsArray = this.options.answerAsArray;
            this.choice = Knockout.observable(null);
            this.answerJSON = params.answerJSON;
            var init = Knockout.unwrap(this.answerJSON);
            if(init.valid) {
                if(this.answerAsArray) {
                    var choice = init.value.findIndex(function(c) {
                        return c[0];
                    });
                    if(choice >= 0) {
                        this.choice(choice);
                    }
                } else {
                    this.choice(init.value);
                }
            }
            this.choiceArray = Knockout.pureComputed(function() {
                var choice = this.choice();
                if(choice === null || choice === undefined) {
                    return null;
                }
                return this.choices().map(function(c, i) {
                    return [i == choice];
                })
            }, this);
            this.result = Knockout.computed(function() {
                var value = this.answerAsArray ? this.choiceArray() : this.choice();
                var valid = value !== null;
                var empty = value === null;
                return {value: value, valid: valid, empty: empty};
            }, this);
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    if(!v || !v.valid) {
                        this.choice(null);
                        return;
                    }
                    var choice = this.answerAsArray ? v.value.findIndex(function(c) {
                        return c[0];
                    }) : v.value;
                    if(choice != this.choice()) {
                        this.choice(choice);
                    }
                }, this)
            ];
            var lastValue = this.result();
            this.setAnswerJSON = Knockout.computed(function() {
                var result = this.result();
                var valuesSame =
                    (!result.valid && !lastValue.valid) ||
                    !lastValue.valid ||
                    (this.answerAsArray ?
                        result.value.every(function(c, i) {
                            return c[0] == lastValue.value[i][0];
                        })
                        : result.value == lastValue.value
                    )
                ;
                if(!valuesSame || result.valid != lastValue.valid) {
                    this.answerJSON(result);
                }
                lastValue = result;
            }, this);
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <form>
                <fieldset data-bind="part_aria_validity: part.display.hasWarnings, part: part.display, attr: {id: id+'-input'}">
                    <menu class="list-unstyled" data-bind="foreach: choices">
                        <li>
                            <label>
                                <input type="radio" name="choice" data-bind="checkedValue: $index, checked: $parent.choice, disable: $parent.disable, event: $parent.events"/> 
                                <span data-bind="html: $data"></span>
                            </label>
                        </li>
                    </menu>
                </fieldset>
            </form>
        `
    });
    Knockout.components.register('answer-widget-dropdown', {
        viewModel: function(params) {
            this.part = params.part;
            this.id = params.id;
            this.disable = params.disable;
            this.options = Knockout.unwrap(params.options);
            this.title = params.title || '';
            this.events = params.events;
            this.nonempty_choices = this.options.choices.map(function(c, i) {
                return {label: c, index: i}
            });
            this.choices = this.nonempty_choices.slice();
            this.choices.splice(0, 0, {label: '', index: null});
            this.answerAsArray = this.options.answerAsArray;
            this.choice = Knockout.observable(null);
            this.answerJSON = params.answerJSON;
            var init = Knockout.unwrap(this.answerJSON);
            if(init.valid) {
                if(this.answerAsArray) {
                    var choice = init.value.findIndex(function(c) {
                        return c[0];
                    });
                    if(choice >= 0) {
                        this.choice(this.choices[choice + 1]);
                    }
                } else {
                    this.choice(this.choices[init.value + 1]);
                }
            }
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    if(!v || !v.valid) {
                        this.choice(null);
                        return;
                    }
                    var current = this.choice()
                    var choice = this.answerAsArray ? v.value.findIndex(function(c) {
                        return c[0];
                    }) : v.value;
                    if(!current || choice != current.index) {
                        this.choice(this.choices[choice + 1]);
                    }
                }, this)
            ];
            this.setAnswerJSON = Knockout.computed(function() {
                var choice = this.choice();
                if(choice && choice.index !== null) {
                    var value;
                    if(this.answerAsArray) {
                        value = this.choices.slice(1).map(function(c, i) {
                            return [i == choice.index];
                        });
                    } else {
                        value = choice.index;
                    }
                    this.answerJSON({valid: true, value: value});
                } else {
                    if(this.answerJSON().valid) {
                        this.answerJSON({valid: false, empty: true});
                    }
                }
            }, this);
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <select class="multiplechoice dropdownlist screen-only" data-bind="options: choices, optionsText: 'label', value: choice, disable: disable, event: events, attr: {title: title, id: id+'-input'}, part_aria_validity: part.display.hasWarnings, part: part.display"></select>
            <span class="multiplechoice dropdownlist print-only" data-bind="foreach: nonempty_choices">
                <span class="dropdownlist-option" data-bind="css: {'checked': $parent.choice() == $data}, text: label">
            </span>
        `
    });
    Knockout.components.register('answer-widget-checkboxes', {
        viewModel: function(params) {
            var vm = this;
            this.part = params.part;
            this.id = params.id;
            this.disable = params.disable;
            this.options = Knockout.unwrap(params.options);
            this.events = params.events;
            this.answerJSON = params.answerJSON;
            var init = Knockout.unwrap(this.answerJSON);
            this.answerAsArray = this.options.answerAsArray;
            this.choices = Knockout.computed(function() {
                return Knockout.unwrap(this.options.choices).map(function(choice, i) {
                    return {
                        content: choice,
                        ticked: Knockout.observable(init.valid ? vm.answerAsArray ? init.value[i][0] : init.value[i] : false)
                    }
                });
            }, this);
            this.subscriptions = [
                this.answerJSON.subscribe(function(v) {
                    var current = this.choices().map(function(c) {
                        return c.ticked();
                    });
                    if(!v || v.value === undefined) {
                        return;
                    }
                    var value = v.value;
                    if(this.answerAsArray) {
                        value = value.map(function(row) {
                            return row[0];
                        });
                    }
                    if(current.length == value.length && current.every(function(t, i) {
                        return t == value[i];
                    })) {
                        return;
                    }
                    this.choices().map(function(c, i) {
                        c.ticked(v.value[i]);
                    });
                }, this)
            ];
            this.make_result = function() {
                var v = this.choices().map(function(c) {
                    return c.ticked()
                });
                if(this.answerAsArray) {
                    return v.map(function(c) {
                        return [c];
                    });
                } else {
                    return v;
                }
            }
            var lastValue = this.make_result();
            this.setAnswerJSON = Knockout.computed(function() {
                var value = this.make_result();
                var same = util.objects_equal(value, lastValue);
                if(!same) {
                    this.answerJSON({valid: true, value: value});
                }
                lastValue = value;
            }, this);
            this.dispose = function() {
                this.subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <form>
                <fieldset data-bind="part_aria_validity: part.display.hasWarnings, part: part.display, attr: {id: id+'-input'}">
                    <menu class="list-unstyled" data-bind="foreach: choices">
                        <li>
                            <label>
                                <input type="checkbox" name="choice" data-bind="checked: ticked, disable: $parent.disable, event: $parent.events"/>
                                <span data-bind="html: content"></span>
                            </label>
                        </li>
                    </menu>
                </fieldset>
            </form>
        `
    });
    Knockout.components.register('answer-widget-m_n_x', {
        viewModel: function(params) {
            var vm = this;
            this.part = params.part;
            this.id = params.id;
            this.answerJSON = params.answerJSON;
            this.disable = params.disable;
            this.options = Knockout.unwrap(params.options);
            this.events = params.events;
            this.choices = Knockout.observableArray(this.options.choices);
            this.answers = Knockout.observableArray(this.options.answers);
            this.layout = this.options.layout;
            for(let i = 0;i < this.answers().length;i++) {
                this.layout[i] = this.layout[i] || [];
                for(let j = 0;j < this.choices().length;j++) {
                    this.layout[i][j] = this.layout[i][j] === undefined || this.layout[i][j];
                }
            }
            switch(this.options.displayType) {
                case 'radiogroup':
                    this.input_type = 'radio';
                    break;
                default:
                    this.input_type = 'checkbox';
            }
            this.ticks = Knockout.computed(function() {
                var choices = this.choices();
                var answers = this.answers();
                var ticks = [];
                for(let i = 0;i < choices.length;i++) {
                    var row = [];
                    row.name = 'row-' + i;
                    if(this.input_type == 'checkbox') {
                        for(let j = 0;j < answers.length;j++) {
                            row.push({ticked: Knockout.observable(false), display: this.layout[j][i]});
                        }
                    } else {
                        var ticked = row.ticked = Knockout.observable(null);
                        for(let j = 0;j < answers.length;j++) {
                            row.push({ticked: ticked, display: this.layout[j][i], name: row.name});
                        }
                    }
                    ticks.push(row);
                }
                return ticks;
            }, this);
            var init = Knockout.unwrap(this.answerJSON);
            if(init.valid) {
                var ticks = this.ticks();
                for(let i = 0;i < ticks.length;i++) {
                    if(this.input_type == 'checkbox') {
                        for(let j = 0;j < ticks[i].length;j++) {
                            ticks[i][j].ticked(init.value[j] && init.value[j][i]);
                        }
                    } else {
                        if(typeof init.value[i] == "number") {
                            ticks[i].ticked(init.value[i]);
                        } else {
                            for(let j = 0;j < ticks[i].length;j++) {
                                if(init.value[j][i]) {
                                    ticks[i].ticked(j);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
            this.result = Knockout.computed(function() {
                var ticks;
                if(this.input_type == 'checkbox') {
                    ticks = this.ticks().map(function(r) {
                        return r.map(function(d) {
                            return d.ticked()
                        })
                    });
                } else {
                    ticks = this.ticks().map(function(r) {
                        var ticked = r.ticked();
                        return vm.answers().map(function(a, i) {
                            return i == ticked;
                        });
                    });
                }
                // because of the never-ending madness to do with the order of matrices in multiple choice parts,
                // this matrix needs to be transposed
                // It makes more sense for the array to go [choice][answer], because that's how they're displayed, but
                // changing that would mean breaking old questions.
                var numAnswers = this.answers().length;
                var numChoices = this.choices().length;
                var oticks = [];
                for(let i = 0;i < numAnswers;i++) {
                    var row = [];
                    oticks.push(row);
                    for(let j = 0;j < numChoices;j++) {
                        row.push(ticks[j][i]);
                    }
                }
                return {valid: true, value: oticks};
            }, this);
            var lastValue = this.result();
            this.setAnswerJSON = Knockout.computed(function() {
                var result = this.result();
                var same = util.objects_equal(result.value, lastValue.value);
                if(!same) {
                    this.answerJSON(result);
                }
                lastValue = result;
            }, this);
            this.dispose = function() {
                this.ticks.dispose();
                this.setAnswerJSON.dispose();
            }
        },
        template: `
            <form>
                <fieldset data-bind="part_aria_validity: part.display.hasWarnings, part: part.display, attr: {id: id+'-input'}">
                    <table>
                        <thead>
                            <tr>
                                <td></td>
                                <!-- ko foreach: answers -->
                                <th><span data-bind="html: $data"></span></th>
                                <!-- /ko -->
                            </tr>
                        </thead>
                        <tbody data-bind="foreach: choices">
                            <tr>
                                <th><span data-bind="html: $data"></span></th>
                                <!-- ko foreach: $parent.ticks()[$index()] -->
                                    <td>
                                    <!-- ko if: $parents[1].input_type=="checkbox" -->
                                        <input type="checkbox" data-bind="visible: display, checked: ticked, disable: $parents[1].disable, event: $parents[1].events"/>
                                    <!-- /ko -->
                                    <!-- ko if: $parents[1].input_type=="radio" -->
                                        <input type="radio" data-bind="visible: display, attr: {name: name, value: $index()}, checked: ticked, disable: $parents[1].disable, event: $parents[1].events, checkedValue: $index()"/>
                                    <!-- /ko -->
                                    </td>
                                <!-- /ko -->
                            </tr>
                        </tbody>
                    </table>
                </fieldset>
            </form>
        `
    });

    Knockout.bindingHandlers.custom_answer_widget = {
        init: function(element, valueAccessor, allBindings) {
            var value = valueAccessor();
            var params = value.params;
            var widget_name = value.name;
            if(!custom_widgets[widget_name]) {
                throw(new Numbas.Error('display.answer widget.unknown widget type', {name: widget_name}));
            }
            var answerJSON = params.answerJSON;
            var init_answerJSON = Knockout.unwrap(answerJSON);
            var part = Knockout.unwrap(params.part);
            var disable = params.disable;
            var options = Knockout.unwrap(params.options);
            var events = params.events || {};
            var title = Knockout.unwrap(params.title) || '';

            var lastValue = init_answerJSON;

            /**
             * Set the answerJSON observable with an answer from the widget.
             *
             * @param {Numbas.custom_part_answer} value
             */
            function answer_changed(value) {
                if(lastValue.value != value.value) {
                    if(!Knockout.unwrap(disable)) {
                        answerJSON(value);
                    }
                    lastValue = value;
                }
            }

            var widget = new custom_widgets[widget_name].widget(element, part, title, events, answer_changed, options);
            widget.setAnswerJSON(init_answerJSON);

            var subscriptions = [
                answerJSON.subscribe(function(v) {
                    if(v && v.value != lastValue.value) {
                        widget.setAnswerJSON(v);
                        lastValue = v;
                    }
                })
            ];
            if(Knockout.isObservable(disable)) {
                subscriptions.push(
                    disable.subscribe(function(v) {
                        if(v) {
                            widget.disable();
                        } else {
                            widget.enable();
                        }
                    }, this)
                );
            } else {
                if(disable) {
                    widget.disable();
                }
            }
            Knockout.utils.domNodeDisposal.addDisposeCallback(element, function() {
                subscriptions.forEach(function(sub) {
                    sub.dispose();
                });
            });
        },
        update: function() {
        }
    };
});
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Wrappers for the various navigation actions the user can do.
 *
 * The assumption is that these should only be called in response to some event the user triggers, by clicking or whatever.
 *
 * Provides {@link Numbas.controls}
 */
Numbas.queueScript('controls', ['base', 'schedule'], function() {
/** @namespace Numbas.controls */
Numbas.controls = /** @lends Numbas.controls */ {
    /** Start the exam - triggered when user clicks "Start" button on frontpage.
     *
     * @see Numbas.Exam#begin
     */
    beginExam: function() {
        Numbas.exam.begin();
    },
    /** Pause the exam.
     *
     * @see Numbas.Exam#pause
     */
    pauseExam: function() {
        Numbas.exam.pause();
    },
    /** Resume the paused exam.
     *
     * @see Numbas.Exam#resume
     */
    resumeExam: function() {
        Numbas.exam.resume();
    },

    /** Show the introduction text, while the exam is in progress.
     */
    showIntroduction: function() {
        Numbas.exam.display.showInfoPage('introduction');
    },

    /** Try to end the exam.
     *
     * @see Numbas.Exam#tryEnd
     */
    endExam: function() {
        Numbas.exam.tryEnd();
    },
    /** In an ended exam, go back from reviewing a question the results page. */
    backToResults: function() {
        Numbas.exam.display.showInfoPage('result');
    },
    /** Go back to the question menu.
     */
    backToMenu: function() {
        Numbas.exam.showMenu();
    },
    /** Try to move to the next question.
     *
     * @param {Numbas.Exam} exam
     * @see Numbas.Exam#tryChangeQuestion
     */
    nextQuestion: function(exam) {
        exam = arguments.length == 1 ? exam : Numbas.exam;
        exam.tryChangeQuestion(exam.currentQuestion.number + 1);
    },
    /** Try to move to the previous question.
     *
     * @param {Numbas.Exam} exam
     * @see Numbas.Exam#tryChangeQuestion
     */
    previousQuestion: function(exam) {
        exam = arguments.length == 1 ? exam : Numbas.exam;
        exam.tryChangeQuestion(exam.currentQuestion.number - 1);
    },
    /** Make a function which tries to jump to question N.
     *
     * @param {number} n - Number of the question to jump to.
     * @param {Numbas.Exam} exam
     * @returns {Function}
     * @see Numbas.controls.jumpQuestion
     */
    makeQuestionJumper: function(n, exam) {
        exam = exam || Numbas.exam;
        return function() {
            Numbas.controls.jumpQuestion(n, exam);
        }
    },
    /** Try to move directly to a particular question.
     *
     * @param {number} jumpTo - Number of the question to jump to.
     * @param {Numbas.Exam} exam
     * @see Numbas.Exam#tryChangeQuestion
     */
    jumpQuestion: function(jumpTo, exam) {
        exam = exam || Numbas.exam;
        if(exam.currentQuestion && jumpTo == exam.currentQuestion.number) {
            exam.display.showQuestion();
            return;
        }
        exam.tryChangeQuestion(jumpTo);
    },
    /** Regenerate the current question.
     *
     * @param {Numbas.Exam} exam
     * @see Numbas.Exam#regenQuestion
     */
    regenQuestion: function(exam) {
        exam = arguments.length == 1 ? exam : Numbas.exam;
        exam.display.root_element.showConfirm(
            R('control.confirm regen' + (exam.mark == 0 ? ' no marks' : '')),
            function() {
                exam.regenQuestion();
            }
        );
    },
    /** Show the advice for the current question.
     *
     * @param {Numbas.Exam} exam
     * @see Numbas.Question#getAdvice
     */
    getAdvice: function(exam) {
        exam = arguments.length == 1 ? exam : Numbas.exam;
        Numbas.exam.currentQuestion.getAdvice();
    },
    /** Reveal the answers to the current question.
     *
     * @param {Numbas.Exam} exam
     * @see Numbas.Question#revealAnswer
     */
    revealAnswer: function(exam) {
        exam = arguments.length == 1 ? exam : Numbas.exam;
        exam.display.root_element.showConfirm(R('control.confirm reveal' + (exam.mark == 0 ? ' no marks' : '')),
            function() {
                exam.currentQuestion.revealAnswer();
            }
        );
    },

    /** Submit a part.
     *
     * @param {Numbas.parts.Part} part
     */
    submitPart: function(part) {
        /** Actually submit the part.
         */
        function go() {
            if(part.locked) {
                return;
            }
            part.submit();
            part.question?.exam?.store?.save();
        }
        if(part.question.partsMode == 'explore') {
            var uses_answer = part.nextParts.some(function(np) {
                return np.instance !== null && np.usesStudentAnswer();
            })
            if(uses_answer) {
                part.question.exam.display.root_element.showConfirm(R('control.submit part.confirm remove next parts'), go);
                return;
            }
        }
        go();
    },

    /** Submit student's answers to all parts in the current question.
     *
     * @param {Numbas.Exam} exam
     * @see Numbas.Question#submit
     */
    submitQuestion: function(exam) {
        exam = arguments.length == 1 ? exam : Numbas.exam;
        exam.currentQuestion.submit();
    }
};
});
;
/*
Copyright 2022-2023 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Functions related to the download and interpretation of student results, and interaction with the results page, usually for outside-LTI contexts. */
Numbas.queueScript('csv', ['jme'], function() {


    /** Functions related to the download and interpretation of student results, and interaction with the results page, usually for outside-LTI contexts.
     *
     * @namespace Numbas.csv
     */
    var csv = Numbas.csv = /** @lends Numbas.csv */ {
        // items should be accessible through Numbas.csv.function, so either write them inside this as key:function pairs, or if necessary as:
        //var ensure_decimal = math.ensure_decimal = function(n) { ? We need them to be like this to ensure they're accessible from elsewhere, maybe.

        /** Escape the contents of a CSV cell.
         *
         * @param {string} cell
         * @returns {string}
         */
        escape_cell: function(cell) {
            cell = cell + '';
            if (cell.match(/[,"'\n\r]/)) {
                cell = '"' + cell.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; //note: this does \\" from an escaped \", not \\\", so there's no way to tell the difference between a string which had \" and one which had "
            }
            return cell;
        },

        /** Break a constructed csv into cells.
         *
         * @param {string} csv
         * @returns {Array.<Array.<string>>}
         */
        split_csv_into_cells: function(csv) {
            //crude, please update or replace - vanilla_csv is good if used universally.
            let current_char;
            let escaped = false;
            let quoted = false;
            const rows = [];
            let current_row = [];
            let current_cell = '';
            for (let i = 0; i < csv.length;i++) {
                current_char = csv.charAt(i);
                if (escaped) {
                    current_cell += current_char;
                    escaped = false;
                    continue;
                } else if (current_char.match(/[\\]/)) {
                    escaped = true;
                } else if (current_char.match(/["]/)) {
                    quoted = !quoted
                } else if (current_char.match(/[,]/)) {
                    if (quoted) {
                        current_cell += current_char;
                    } else {
                        current_row.push(current_cell);
                        current_cell = '';
                    }
                } else if (current_char.match(/[\n\r]/)) {
                    if (quoted) {
                        current_cell += current_char;
                    } else {
                        current_row.push(current_cell);
                        current_cell = '';
                        rows.push(current_row);
                        current_row = [];
                    }
                } else {
                    current_cell += current_char;
                }
            }
            //same as if match new line, because end of file!
            current_row.push(current_cell);
            rows.push(current_row);

            return rows;
        },

        /** Escape each cell of a list of strings such that each will not cause issues within a csv.
         *
         * @param {Array.<string>} cells
         * @returns {string}
         */
        make_row: function(cells) {
            return cells.map(csv.escape_cell).join(',');
        },

        /** Escape each cell of a two-dimensional array of strings such that each will not cause issues within a csv.
         *
         * @param {Array.<Array.<string>>} rows
         * @returns {string}
         */
        from_array: function(rows) {
            return rows.map(csv.make_row).join('\n');
        },


        /** Download a file with the given contents, by creating a link and then clicking it.
         *
         * @param {string} file - The contents of the file
         */
        create_and_download_file: function(file) {
            //pulled from https://stackoverflow.com/questions/8310657/how-to-create-a-dynamic-file-link-for-download-in-javascript
            const mime_type = 'text/plain';
            var blob = new Blob([file], { type: mime_type });
            var dlink = document.createElement('a');
            document.body.appendChild(dlink); //may be necessary for firefox/some browsers
            dlink.download = "results.csv";
            dlink.href = window.URL.createObjectURL(blob);
            dlink.onclick = function(e) {
                var that = this;
                setTimeout(function() {
                    window.URL.revokeObjectURL(that.href);
                }, 1500);
            };

            dlink.click()
            dlink.remove()

        }


    }
});
;
Numbas.queueScript('decimal',[],function(module) {
/* decimal.js v10.1.1 https://github.com/MikeMcl/decimal.js/LICENCE */
!function(n){"use strict";var h,R,e,o,u=9e15,g=1e9,m="0123456789abcdef",t="2.3025850929940456840179914546843642076011014886287729760333279009675726096773524802359972050895982983419677840422862486334095254650828067566662873690987816894829072083255546808437998948262331985283935053089653777326288461633662222876982198867465436674744042432743651550489343149393914796194044002221051017141748003688084012647080685567743216228355220114804663715659121373450747856947683463616792101806445070648000277502684916746550586856935673420670581136429224554405758925724208241314695689016758940256776311356919292033376587141660230105703089634572075440370847469940168269282808481184289314848524948644871927809676271275775397027668605952496716674183485704422507197965004714951050492214776567636938662976979522110718264549734772662425709429322582798502585509785265383207606726317164309505995087807523710333101197857547331541421808427543863591778117054309827482385045648019095610299291824318237525357709750539565187697510374970888692180205189339507238539205144634197265287286965110862571492198849978748873771345686209167058",r="3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632789",c={precision:20,rounding:4,modulo:1,toExpNeg:-7,toExpPos:21,minE:-u,maxE:u,crypto:!1},N=!0,f="[DecimalError] ",w=f+"Invalid argument: ",s=f+"Precision limit exceeded",a=f+"crypto unavailable",L=Math.floor,v=Math.pow,l=/^0b([01]+(\.[01]*)?|\.[01]+)(p[+-]?\d+)?$/i,d=/^0x([0-9a-f]+(\.[0-9a-f]*)?|\.[0-9a-f]+)(p[+-]?\d+)?$/i,p=/^0o([0-7]+(\.[0-7]*)?|\.[0-7]+)(p[+-]?\d+)?$/i,b=/^(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i,T=1e7,U=7,E=t.length-1,x=r.length-1,y={name:"[object Decimal]"};function M(n){var e,i,t,r=n.length-1,s="",o=n[0];if(0<r){for(s+=o,e=1;e<r;e++)t=n[e]+"",(i=U-t.length)&&(s+=C(i)),s+=t;o=n[e],(i=U-(t=o+"").length)&&(s+=C(i))}else if(0===o)return"0";for(;o%10==0;)o/=10;return s+o}function q(n,e,i){if(n!==~~n||n<e||i<n)throw Error(w+n)}function O(n,e,i,t){var r,s,o;for(s=n[0];10<=s;s/=10)--e;return--e<0?(e+=U,r=0):(r=Math.ceil((e+1)/U),e%=U),s=v(10,U-e),o=n[r]%s|0,null==t?e<3?(0==e?o=o/100|0:1==e&&(o=o/10|0),i<4&&99999==o||3<i&&49999==o||5e4==o||0==o):(i<4&&o+1==s||3<i&&o+1==s/2)&&(n[r+1]/s/100|0)==v(10,e-2)-1||(o==s/2||0==o)&&0==(n[r+1]/s/100|0):e<4?(0==e?o=o/1e3|0:1==e?o=o/100|0:2==e&&(o=o/10|0),(t||i<4)&&9999==o||!t&&3<i&&4999==o):((t||i<4)&&o+1==s||!t&&3<i&&o+1==s/2)&&(n[r+1]/s/1e3|0)==v(10,e-3)-1}function D(n,e,i){for(var t,r,s=[0],o=0,u=n.length;o<u;){for(r=s.length;r--;)s[r]*=e;for(s[0]+=m.indexOf(n.charAt(o++)),t=0;t<s.length;t++)s[t]>i-1&&(void 0===s[t+1]&&(s[t+1]=0),s[t+1]+=s[t]/i|0,s[t]%=i)}return s.reverse()}y.absoluteValue=y.abs=function(){var n=new this.constructor(this);return n.s<0&&(n.s=1),_(n)},y.ceil=function(){return _(new this.constructor(this),this.e+1,2)},y.comparedTo=y.cmp=function(n){var e,i,t,r,s=this,o=s.d,u=(n=new s.constructor(n)).d,c=s.s,f=n.s;if(!o||!u)return c&&f?c!==f?c:o===u?0:!o^c<0?1:-1:NaN;if(!o[0]||!u[0])return o[0]?c:u[0]?-f:0;if(c!==f)return c;if(s.e!==n.e)return s.e>n.e^c<0?1:-1;for(e=0,i=(t=o.length)<(r=u.length)?t:r;e<i;++e)if(o[e]!==u[e])return o[e]>u[e]^c<0?1:-1;return t===r?0:r<t^c<0?1:-1},y.cosine=y.cos=function(){var n,e,i=this,t=i.constructor;return i.d?i.d[0]?(n=t.precision,e=t.rounding,t.precision=n+Math.max(i.e,i.sd())+U,t.rounding=1,i=function(n,e){var i,t,r=e.d.length;t=r<32?(i=Math.ceil(r/3),Math.pow(4,-i).toString()):(i=16,"2.3283064365386962890625e-10");n.precision+=i,e=W(n,1,e.times(t),new n(1));for(var s=i;s--;){var o=e.times(e);e=o.times(o).minus(o).times(8).plus(1)}return n.precision-=i,e}(t,J(t,i)),t.precision=n,t.rounding=e,_(2==o||3==o?i.neg():i,n,e,!0)):new t(1):new t(NaN)},y.cubeRoot=y.cbrt=function(){var n,e,i,t,r,s,o,u,c,f,a=this,h=a.constructor;if(!a.isFinite()||a.isZero())return new h(a);for(N=!1,(s=a.s*Math.pow(a.s*a,1/3))&&Math.abs(s)!=1/0?t=new h(s.toString()):(i=M(a.d),(s=((n=a.e)-i.length+1)%3)&&(i+=1==s||-2==s?"0":"00"),s=Math.pow(i,1/3),n=L((n+1)/3)-(n%3==(n<0?-1:2)),(t=new h(i=s==1/0?"5e"+n:(i=s.toExponential()).slice(0,i.indexOf("e")+1)+n)).s=a.s),o=(n=h.precision)+3;;)if(f=(c=(u=t).times(u).times(u)).plus(a),t=F(f.plus(a).times(u),f.plus(c),o+2,1),M(u.d).slice(0,o)===(i=M(t.d)).slice(0,o)){if("9999"!=(i=i.slice(o-3,o+1))&&(r||"4999"!=i)){+i&&(+i.slice(1)||"5"!=i.charAt(0))||(_(t,n+1,1),e=!t.times(t).times(t).eq(a));break}if(!r&&(_(u,n+1,0),u.times(u).times(u).eq(a))){t=u;break}o+=4,r=1}return N=!0,_(t,n,h.rounding,e)},y.decimalPlaces=y.dp=function(){var n,e=this.d,i=NaN;if(e){if(i=((n=e.length-1)-L(this.e/U))*U,n=e[n])for(;n%10==0;n/=10)i--;i<0&&(i=0)}return i},y.dividedBy=y.div=function(n){return F(this,new this.constructor(n))},y.dividedToIntegerBy=y.divToInt=function(n){var e=this.constructor;return _(F(this,new e(n),0,1,1),e.precision,e.rounding)},y.equals=y.eq=function(n){return 0===this.cmp(n)},y.floor=function(){return _(new this.constructor(this),this.e+1,3)},y.greaterThan=y.gt=function(n){return 0<this.cmp(n)},y.greaterThanOrEqualTo=y.gte=function(n){var e=this.cmp(n);return 1==e||0===e},y.hyperbolicCosine=y.cosh=function(){var n,e,i,t,r,s=this,o=s.constructor,u=new o(1);if(!s.isFinite())return new o(s.s?1/0:NaN);if(s.isZero())return u;i=o.precision,t=o.rounding,o.precision=i+Math.max(s.e,s.sd())+4,o.rounding=1,e=(r=s.d.length)<32?(n=Math.ceil(r/3),Math.pow(4,-n).toString()):(n=16,"2.3283064365386962890625e-10"),s=W(o,1,s.times(e),new o(1),!0);for(var c,f=n,a=new o(8);f--;)c=s.times(s),s=u.minus(c.times(a.minus(c.times(a))));return _(s,o.precision=i,o.rounding=t,!0)},y.hyperbolicSine=y.sinh=function(){var n,e,i,t,r=this,s=r.constructor;if(!r.isFinite()||r.isZero())return new s(r);if(e=s.precision,i=s.rounding,s.precision=e+Math.max(r.e,r.sd())+4,s.rounding=1,(t=r.d.length)<3)r=W(s,2,r,r,!0);else{n=16<(n=1.4*Math.sqrt(t))?16:0|n,r=W(s,2,r=r.times(Math.pow(5,-n)),r,!0);for(var o,u=new s(5),c=new s(16),f=new s(20);n--;)o=r.times(r),r=r.times(u.plus(o.times(c.times(o).plus(f))))}return _(r,s.precision=e,s.rounding=i,!0)},y.hyperbolicTangent=y.tanh=function(){var n,e,i=this,t=i.constructor;return i.isFinite()?i.isZero()?new t(i):(n=t.precision,e=t.rounding,t.precision=n+7,t.rounding=1,F(i.sinh(),i.cosh(),t.precision=n,t.rounding=e)):new t(i.s)},y.inverseCosine=y.acos=function(){var n,e=this,i=e.constructor,t=e.abs().cmp(1),r=i.precision,s=i.rounding;return-1!==t?0===t?e.isNeg()?P(i,r,s):new i(0):new i(NaN):e.isZero()?P(i,r+4,s).times(.5):(i.precision=r+6,i.rounding=1,e=e.asin(),n=P(i,r+4,s).times(.5),i.precision=r,i.rounding=s,n.minus(e))},y.inverseHyperbolicCosine=y.acosh=function(){var n,e,i=this,t=i.constructor;return i.lte(1)?new t(i.eq(1)?0:NaN):i.isFinite()?(n=t.precision,e=t.rounding,t.precision=n+Math.max(Math.abs(i.e),i.sd())+4,t.rounding=1,N=!1,i=i.times(i).minus(1).sqrt().plus(i),N=!0,t.precision=n,t.rounding=e,i.ln()):new t(i)},y.inverseHyperbolicSine=y.asinh=function(){var n,e,i=this,t=i.constructor;return!i.isFinite()||i.isZero()?new t(i):(n=t.precision,e=t.rounding,t.precision=n+2*Math.max(Math.abs(i.e),i.sd())+6,t.rounding=1,N=!1,i=i.times(i).plus(1).sqrt().plus(i),N=!0,t.precision=n,t.rounding=e,i.ln())},y.inverseHyperbolicTangent=y.atanh=function(){var n,e,i,t,r=this,s=r.constructor;return r.isFinite()?0<=r.e?new s(r.abs().eq(1)?r.s/0:r.isZero()?r:NaN):(n=s.precision,e=s.rounding,t=r.sd(),Math.max(t,n)<2*-r.e-1?_(new s(r),n,e,!0):(s.precision=i=t-r.e,r=F(r.plus(1),new s(1).minus(r),i+n,1),s.precision=n+4,s.rounding=1,r=r.ln(),s.precision=n,s.rounding=e,r.times(.5))):new s(NaN)},y.inverseSine=y.asin=function(){var n,e,i,t,r=this,s=r.constructor;return r.isZero()?new s(r):(e=r.abs().cmp(1),i=s.precision,t=s.rounding,-1!==e?0===e?((n=P(s,i+4,t).times(.5)).s=r.s,n):new s(NaN):(s.precision=i+6,s.rounding=1,r=r.div(new s(1).minus(r.times(r)).sqrt().plus(1)).atan(),s.precision=i,s.rounding=t,r.times(2)))},y.inverseTangent=y.atan=function(){var n,e,i,t,r,s,o,u,c,f=this,a=f.constructor,h=a.precision,l=a.rounding;if(f.isFinite()){if(f.isZero())return new a(f);if(f.abs().eq(1)&&h+4<=x)return(o=P(a,h+4,l).times(.25)).s=f.s,o}else{if(!f.s)return new a(NaN);if(h+4<=x)return(o=P(a,h+4,l).times(.5)).s=f.s,o}for(a.precision=u=h+10,a.rounding=1,n=i=Math.min(28,u/U+2|0);n;--n)f=f.div(f.times(f).plus(1).sqrt().plus(1));for(N=!1,e=Math.ceil(u/U),t=1,c=f.times(f),o=new a(f),r=f;-1!==n;)if(r=r.times(c),s=o.minus(r.div(t+=2)),r=r.times(c),void 0!==(o=s.plus(r.div(t+=2))).d[e])for(n=e;o.d[n]===s.d[n]&&n--;);return i&&(o=o.times(2<<i-1)),N=!0,_(o,a.precision=h,a.rounding=l,!0)},y.isFinite=function(){return!!this.d},y.isInteger=y.isInt=function(){return!!this.d&&L(this.e/U)>this.d.length-2},y.isNaN=function(){return!this.s},y.isNegative=y.isNeg=function(){return this.s<0},y.isPositive=y.isPos=function(){return 0<this.s},y.isZero=function(){return!!this.d&&0===this.d[0]},y.lessThan=y.lt=function(n){return this.cmp(n)<0},y.lessThanOrEqualTo=y.lte=function(n){return this.cmp(n)<1},y.logarithm=y.log=function(n){var e,i,t,r,s,o,u,c,f=this,a=f.constructor,h=a.precision,l=a.rounding;if(null==n)n=new a(10),e=!0;else{if(i=(n=new a(n)).d,n.s<0||!i||!i[0]||n.eq(1))return new a(NaN);e=n.eq(10)}if(i=f.d,f.s<0||!i||!i[0]||f.eq(1))return new a(i&&!i[0]?-1/0:1!=f.s?NaN:i?0:1/0);if(e)if(1<i.length)s=!0;else{for(r=i[0];r%10==0;)r/=10;s=1!==r}if(N=!1,o=V(f,u=h+5),t=e?Z(a,u+10):V(n,u),O((c=F(o,t,u,1)).d,r=h,l))do{if(o=V(f,u+=10),t=e?Z(a,u+10):V(n,u),c=F(o,t,u,1),!s){+M(c.d).slice(r+1,r+15)+1==1e14&&(c=_(c,h+1,0));break}}while(O(c.d,r+=10,l));return N=!0,_(c,h,l)},y.minus=y.sub=function(n){var e,i,t,r,s,o,u,c,f,a,h,l,d=this,p=d.constructor;if(n=new p(n),!d.d||!n.d)return d.s&&n.s?d.d?n.s=-n.s:n=new p(n.d||d.s!==n.s?d:NaN):n=new p(NaN),n;if(d.s!=n.s)return n.s=-n.s,d.plus(n);if(f=d.d,l=n.d,u=p.precision,c=p.rounding,!f[0]||!l[0]){if(l[0])n.s=-n.s;else{if(!f[0])return new p(3===c?-0:0);n=new p(d)}return N?_(n,u,c):n}if(i=L(n.e/U),a=L(d.e/U),f=f.slice(),s=a-i){for(o=(h=s<0)?(e=f,s=-s,l.length):(e=l,i=a,f.length),(t=Math.max(Math.ceil(u/U),o)+2)<s&&(s=t,e.length=1),e.reverse(),t=s;t--;)e.push(0);e.reverse()}else{for((h=(t=f.length)<(o=l.length))&&(o=t),t=0;t<o;t++)if(f[t]!=l[t]){h=f[t]<l[t];break}s=0}for(h&&(e=f,f=l,l=e,n.s=-n.s),o=f.length,t=l.length-o;0<t;--t)f[o++]=0;for(t=l.length;s<t;){if(f[--t]<l[t]){for(r=t;r&&0===f[--r];)f[r]=T-1;--f[r],f[t]+=T}f[t]-=l[t]}for(;0===f[--o];)f.pop();for(;0===f[0];f.shift())--i;return f[0]?(n.d=f,n.e=S(f,i),N?_(n,u,c):n):new p(3===c?-0:0)},y.modulo=y.mod=function(n){var e,i=this,t=i.constructor;return n=new t(n),!i.d||!n.s||n.d&&!n.d[0]?new t(NaN):!n.d||i.d&&!i.d[0]?_(new t(i),t.precision,t.rounding):(N=!1,9==t.modulo?(e=F(i,n.abs(),0,3,1)).s*=n.s:e=F(i,n,0,t.modulo,1),e=e.times(n),N=!0,i.minus(e))},y.naturalExponential=y.exp=function(){return B(this)},y.naturalLogarithm=y.ln=function(){return V(this)},y.negated=y.neg=function(){var n=new this.constructor(this);return n.s=-n.s,_(n)},y.plus=y.add=function(n){var e,i,t,r,s,o,u,c,f,a,h=this,l=h.constructor;if(n=new l(n),!h.d||!n.d)return h.s&&n.s?h.d||(n=new l(n.d||h.s===n.s?h:NaN)):n=new l(NaN),n;if(h.s!=n.s)return n.s=-n.s,h.minus(n);if(f=h.d,a=n.d,u=l.precision,c=l.rounding,!f[0]||!a[0])return a[0]||(n=new l(h)),N?_(n,u,c):n;if(s=L(h.e/U),t=L(n.e/U),f=f.slice(),r=s-t){for((o=(o=r<0?(i=f,r=-r,a.length):(i=a,t=s,f.length))<(s=Math.ceil(u/U))?s+1:o+1)<r&&(r=o,i.length=1),i.reverse();r--;)i.push(0);i.reverse()}for((o=f.length)-(r=a.length)<0&&(r=o,i=a,a=f,f=i),e=0;r;)e=(f[--r]=f[r]+a[r]+e)/T|0,f[r]%=T;for(e&&(f.unshift(e),++t),o=f.length;0==f[--o];)f.pop();return n.d=f,n.e=S(f,t),N?_(n,u,c):n},y.precision=y.sd=function(n){var e;if(void 0!==n&&n!==!!n&&1!==n&&0!==n)throw Error(w+n);return this.d?(e=k(this.d),n&&this.e+1>e&&(e=this.e+1)):e=NaN,e},y.round=function(){var n=this.constructor;return _(new n(this),this.e+1,n.rounding)},y.sine=y.sin=function(){var n,e,i=this,t=i.constructor;return i.isFinite()?i.isZero()?new t(i):(n=t.precision,e=t.rounding,t.precision=n+Math.max(i.e,i.sd())+U,t.rounding=1,i=function(n,e){var i,t=e.d.length;if(t<3)return W(n,2,e,e);i=16<(i=1.4*Math.sqrt(t))?16:0|i,e=e.times(Math.pow(5,-i)),e=W(n,2,e,e);for(var r,s=new n(5),o=new n(16),u=new n(20);i--;)r=e.times(e),e=e.times(s.plus(r.times(o.times(r).minus(u))));return e}(t,J(t,i)),t.precision=n,t.rounding=e,_(2<o?i.neg():i,n,e,!0)):new t(NaN)},y.squareRoot=y.sqrt=function(){var n,e,i,t,r,s,o=this,u=o.d,c=o.e,f=o.s,a=o.constructor;if(1!==f||!u||!u[0])return new a(!f||f<0&&(!u||u[0])?NaN:u?o:1/0);for(N=!1,t=0==(f=Math.sqrt(+o))||f==1/0?(((e=M(u)).length+c)%2==0&&(e+="0"),f=Math.sqrt(e),c=L((c+1)/2)-(c<0||c%2),new a(e=f==1/0?"1e"+c:(e=f.toExponential()).slice(0,e.indexOf("e")+1)+c)):new a(f.toString()),i=(c=a.precision)+3;;)if(t=(s=t).plus(F(o,s,i+2,1)).times(.5),M(s.d).slice(0,i)===(e=M(t.d)).slice(0,i)){if("9999"!=(e=e.slice(i-3,i+1))&&(r||"4999"!=e)){+e&&(+e.slice(1)||"5"!=e.charAt(0))||(_(t,c+1,1),n=!t.times(t).eq(o));break}if(!r&&(_(s,c+1,0),s.times(s).eq(o))){t=s;break}i+=4,r=1}return N=!0,_(t,c,a.rounding,n)},y.tangent=y.tan=function(){var n,e,i=this,t=i.constructor;return i.isFinite()?i.isZero()?new t(i):(n=t.precision,e=t.rounding,t.precision=n+10,t.rounding=1,(i=i.sin()).s=1,i=F(i,new t(1).minus(i.times(i)).sqrt(),n+10,0),t.precision=n,t.rounding=e,_(2==o||4==o?i.neg():i,n,e,!0)):new t(NaN)},y.times=y.mul=function(n){var e,i,t,r,s,o,u,c,f,a=this.constructor,h=this.d,l=(n=new a(n)).d;if(n.s*=this.s,!(h&&h[0]&&l&&l[0]))return new a(!n.s||h&&!h[0]&&!l||l&&!l[0]&&!h?NaN:h&&l?0*n.s:n.s/0);for(i=L(this.e/U)+L(n.e/U),(c=h.length)<(f=l.length)&&(s=h,h=l,l=s,o=c,c=f,f=o),s=[],t=o=c+f;t--;)s.push(0);for(t=f;0<=--t;){for(e=0,r=c+t;t<r;)u=s[r]+l[t]*h[r-t-1]+e,s[r--]=u%T|0,e=u/T|0;s[r]=(s[r]+e)%T|0}for(;!s[--o];)s.pop();return e?++i:s.shift(),n.d=s,n.e=S(s,i),N?_(n,a.precision,a.rounding):n},y.toBinary=function(n,e){return z(this,2,n,e)},y.toDecimalPlaces=y.toDP=function(n,e){var i=this,t=i.constructor;return i=new t(i),void 0===n?i:(q(n,0,g),void 0===e?e=t.rounding:q(e,0,8),_(i,n+i.e+1,e))},y.toExponential=function(n,e){var i,t=this,r=t.constructor;return i=void 0===n?A(t,!0):(q(n,0,g),void 0===e?e=r.rounding:q(e,0,8),A(t=_(new r(t),n+1,e),!0,n+1)),t.isNeg()&&!t.isZero()?"-"+i:i},y.toFixed=function(n,e){var i,t,r=this,s=r.constructor;return i=void 0===n?A(r):(q(n,0,g),void 0===e?e=s.rounding:q(e,0,8),A(t=_(new s(r),n+r.e+1,e),!1,n+t.e+1)),r.isNeg()&&!r.isZero()?"-"+i:i},y.toFraction=function(n){var e,i,t,r,s,o,u,c,f,a,h,l,d=this,p=d.d,g=d.constructor;if(!p)return new g(d);if(f=i=new g(1),o=(s=(e=new g(t=c=new g(0))).e=k(p)-d.e-1)%U,e.d[0]=v(10,o<0?U+o:o),null==n)n=0<s?e:f;else{if(!(u=new g(n)).isInt()||u.lt(f))throw Error(w+u);n=u.gt(e)?0<s?e:f:u}for(N=!1,u=new g(M(p)),a=g.precision,g.precision=s=p.length*U*2;h=F(u,e,0,1,1),1!=(r=i.plus(h.times(t))).cmp(n);)i=t,t=r,r=f,f=c.plus(h.times(r)),c=r,r=e,e=u.minus(h.times(r)),u=r;return r=F(n.minus(i),t,0,1,1),c=c.plus(r.times(f)),i=i.plus(r.times(t)),c.s=f.s=d.s,l=F(f,t,s,1).minus(d).abs().cmp(F(c,i,s,1).minus(d).abs())<1?[f,t]:[c,i],g.precision=a,N=!0,l},y.toHexadecimal=y.toHex=function(n,e){return z(this,16,n,e)},y.toNearest=function(n,e){var i=this,t=i.constructor;if(i=new t(i),null==n){if(!i.d)return i;n=new t(1),e=t.rounding}else{if(n=new t(n),void 0===e?e=t.rounding:q(e,0,8),!i.d)return n.s?i:n;if(!n.d)return n.s&&(n.s=i.s),n}return n.d[0]?(N=!1,i=F(i,n,0,e,1).times(n),N=!0,_(i)):(n.s=i.s,i=n),i},y.toNumber=function(){return+this},y.toOctal=function(n,e){return z(this,8,n,e)},y.toPower=y.pow=function(n){var e,i,t,r,s,o,u=this,c=u.constructor,f=+(n=new c(n));if(!(u.d&&n.d&&u.d[0]&&n.d[0]))return new c(v(+u,f));if((u=new c(u)).eq(1))return u;if(t=c.precision,s=c.rounding,n.eq(1))return _(u,t,s);if((e=L(n.e/U))>=n.d.length-1&&(i=f<0?-f:f)<=9007199254740991)return r=I(c,u,i,t),n.s<0?new c(1).div(r):_(r,t,s);if((o=u.s)<0){if(e<n.d.length-1)return new c(NaN);if(0==(1&n.d[e])&&(o=1),0==u.e&&1==u.d[0]&&1==u.d.length)return u.s=o,u}return(e=0!=(i=v(+u,f))&&isFinite(i)?new c(i+"").e:L(f*(Math.log("0."+M(u.d))/Math.LN10+u.e+1)))>c.maxE+1||e<c.minE-1?new c(0<e?o/0:0):(N=!1,c.rounding=u.s=1,i=Math.min(12,(e+"").length),(r=B(n.times(V(u,t+i)),t)).d&&O((r=_(r,t+5,1)).d,t,s)&&(e=t+10,+M((r=_(B(n.times(V(u,e+i)),e),e+5,1)).d).slice(t+1,t+15)+1==1e14&&(r=_(r,t+1,0))),r.s=o,N=!0,_(r,t,c.rounding=s))},y.toPrecision=function(n,e){var i,t=this,r=t.constructor;return i=void 0===n?A(t,t.e<=r.toExpNeg||t.e>=r.toExpPos):(q(n,1,g),void 0===e?e=r.rounding:q(e,0,8),A(t=_(new r(t),n,e),n<=t.e||t.e<=r.toExpNeg,n)),t.isNeg()&&!t.isZero()?"-"+i:i},y.toSignificantDigits=y.toSD=function(n,e){var i=this.constructor;return void 0===n?(n=i.precision,e=i.rounding):(q(n,1,g),void 0===e?e=i.rounding:q(e,0,8)),_(new i(this),n,e)},y.toString=function(){var n=this,e=n.constructor,i=A(n,n.e<=e.toExpNeg||n.e>=e.toExpPos);return n.isNeg()&&!n.isZero()?"-"+i:i},y.truncated=y.trunc=function(){return _(new this.constructor(this),this.e+1,1)},y.valueOf=y.toJSON=function(){var n=this,e=n.constructor,i=A(n,n.e<=e.toExpNeg||n.e>=e.toExpPos);return n.isNeg()?"-"+i:i};var F=function(){function S(n,e,i){var t,r=0,s=n.length;for(n=n.slice();s--;)t=n[s]*e+r,n[s]=t%i|0,r=t/i|0;return r&&n.unshift(r),n}function Z(n,e,i,t){var r,s;if(i!=t)s=t<i?1:-1;else for(r=s=0;r<i;r++)if(n[r]!=e[r]){s=n[r]>e[r]?1:-1;break}return s}function P(n,e,i,t){for(var r=0;i--;)n[i]-=r,r=n[i]<e[i]?1:0,n[i]=r*t+n[i]-e[i];for(;!n[0]&&1<n.length;)n.shift()}return function(n,e,i,t,r,s){var o,u,c,f,a,h,l,d,p,g,m,w,v,N,b,E,x,y,M,q,O=n.constructor,D=n.s==e.s?1:-1,F=n.d,A=e.d;if(!(F&&F[0]&&A&&A[0]))return new O(n.s&&e.s&&(F?!A||F[0]!=A[0]:A)?F&&0==F[0]||!A?0*D:D/0:NaN);for(u=s?(a=1,n.e-e.e):(s=T,a=U,L(n.e/a)-L(e.e/a)),M=A.length,x=F.length,g=(p=new O(D)).d=[],c=0;A[c]==(F[c]||0);c++);if(A[c]>(F[c]||0)&&u--,null==i?(N=i=O.precision,t=O.rounding):N=r?i+(n.e-e.e)+1:i,N<0)g.push(1),h=!0;else{if(N=N/a+2|0,c=0,1==M){for(A=A[f=0],N++;(c<x||f)&&N--;c++)b=f*s+(F[c]||0),g[c]=b/A|0,f=b%A|0;h=f||c<x}else{for(1<(f=s/(A[0]+1)|0)&&(A=S(A,f,s),F=S(F,f,s),M=A.length,x=F.length),E=M,w=(m=F.slice(0,M)).length;w<M;)m[w++]=0;for((q=A.slice()).unshift(0),y=A[0],A[1]>=s/2&&++y;f=0,(o=Z(A,m,M,w))<0?(v=m[0],M!=w&&(v=v*s+(m[1]||0)),1<(f=v/y|0)?(s<=f&&(f=s-1),1==(o=Z(l=S(A,f,s),m,d=l.length,w=m.length))&&(f--,P(l,M<d?q:A,d,s))):(0==f&&(o=f=1),l=A.slice()),(d=l.length)<w&&l.unshift(0),P(m,l,w,s),-1==o&&(o=Z(A,m,M,w=m.length))<1&&(f++,P(m,M<w?q:A,w,s)),w=m.length):0===o&&(f++,m=[0]),g[c++]=f,o&&m[0]?m[w++]=F[E]||0:(m=[F[E]],w=1),(E++<x||void 0!==m[0])&&N--;);h=void 0!==m[0]}g[0]||g.shift()}if(1==a)p.e=u,R=h;else{for(c=1,f=g[0];10<=f;f/=10)c++;p.e=c+u*a-1,_(p,r?i+p.e+1:i,t,h)}return p}}();function _(n,e,i,t){var r,s,o,u,c,f,a,h,l,d=n.constructor;n:if(null!=e){if(!(h=n.d))return n;for(r=1,u=h[0];10<=u;u/=10)r++;if((s=e-r)<0)s+=U,o=e,c=(a=h[l=0])/v(10,r-o-1)%10|0;else if(l=Math.ceil((s+1)/U),(u=h.length)<=l){if(!t)break n;for(;u++<=l;)h.push(0);a=c=0,o=(s%=U)-U+(r=1)}else{for(a=u=h[l],r=1;10<=u;u/=10)r++;c=(o=(s%=U)-U+r)<0?0:a/v(10,r-o-1)%10|0}if(t=t||e<0||void 0!==h[l+1]||(o<0?a:a%v(10,r-o-1)),f=i<4?(c||t)&&(0==i||i==(n.s<0?3:2)):5<c||5==c&&(4==i||t||6==i&&(0<s?0<o?a/v(10,r-o):0:h[l-1])%10&1||i==(n.s<0?8:7)),e<1||!h[0])return h.length=0,f?(e-=n.e+1,h[0]=v(10,(U-e%U)%U),n.e=-e||0):h[0]=n.e=0,n;if(0==s?(h.length=l,u=1,l--):(h.length=l+1,u=v(10,U-s),h[l]=0<o?(a/v(10,r-o)%v(10,o)|0)*u:0),f)for(;;){if(0==l){for(s=1,o=h[0];10<=o;o/=10)s++;for(o=h[0]+=u,u=1;10<=o;o/=10)u++;s!=u&&(n.e++,h[0]==T&&(h[0]=1));break}if(h[l]+=u,h[l]!=T)break;h[l--]=0,u=1}for(s=h.length;0===h[--s];)h.pop()}return N&&(n.e>d.maxE?(n.d=null,n.e=NaN):n.e<d.minE&&(n.e=0,n.d=[0])),n}function A(n,e,i){if(!n.isFinite())return j(n);var t,r=n.e,s=M(n.d),o=s.length;return e?(i&&0<(t=i-o)?s=s.charAt(0)+"."+s.slice(1)+C(t):1<o&&(s=s.charAt(0)+"."+s.slice(1)),s=s+(n.e<0?"e":"e+")+n.e):r<0?(s="0."+C(-r-1)+s,i&&0<(t=i-o)&&(s+=C(t))):o<=r?(s+=C(r+1-o),i&&0<(t=i-r-1)&&(s=s+"."+C(t))):((t=r+1)<o&&(s=s.slice(0,t)+"."+s.slice(t)),i&&0<(t=i-o)&&(r+1===o&&(s+="."),s+=C(t))),s}function S(n,e){var i=n[0];for(e*=U;10<=i;i/=10)e++;return e}function Z(n,e,i){if(E<e)throw N=!0,i&&(n.precision=i),Error(s);return _(new n(t),e,1,!0)}function P(n,e,i){if(x<e)throw Error(s);return _(new n(r),e,i,!0)}function k(n){var e=n.length-1,i=e*U+1;if(e=n[e]){for(;e%10==0;e/=10)i--;for(e=n[0];10<=e;e/=10)i++}return i}function C(n){for(var e="";n--;)e+="0";return e}function I(n,e,i,t){var r,s=new n(1),o=Math.ceil(t/U+4);for(N=!1;;){if(i%2&&G((s=s.times(e)).d,o)&&(r=!0),0===(i=L(i/2))){i=s.d.length-1,r&&0===s.d[i]&&++s.d[i];break}G((e=e.times(e)).d,o)}return N=!0,s}function H(n){return 1&n.d[n.d.length-1]}function i(n,e,i){for(var t,r=new n(e[0]),s=0;++s<e.length;){if(!(t=new n(e[s])).s){r=t;break}r[i](t)&&(r=t)}return r}function B(n,e){var i,t,r,s,o,u,c,f=0,a=0,h=0,l=n.constructor,d=l.rounding,p=l.precision;if(!n.d||!n.d[0]||17<n.e)return new l(n.d?n.d[0]?n.s<0?0:1/0:1:n.s?n.s<0?0:n:NaN);for(c=null==e?(N=!1,p):e,u=new l(.03125);-2<n.e;)n=n.times(u),h+=5;for(c+=t=Math.log(v(2,h))/Math.LN10*2+5|0,i=s=o=new l(1),l.precision=c;;){if(s=_(s.times(n),c,1),i=i.times(++a),M((u=o.plus(F(s,i,c,1))).d).slice(0,c)===M(o.d).slice(0,c)){for(r=h;r--;)o=_(o.times(o),c,1);if(null!=e)return l.precision=p,o;if(!(f<3&&O(o.d,c-t,d,f)))return _(o,l.precision=p,d,N=!0);l.precision=c+=10,i=s=u=new l(1),a=0,f++}o=u}}function V(n,e){var i,t,r,s,o,u,c,f,a,h,l,d=1,p=n,g=p.d,m=p.constructor,w=m.rounding,v=m.precision;if(p.s<0||!g||!g[0]||!p.e&&1==g[0]&&1==g.length)return new m(g&&!g[0]?-1/0:1!=p.s?NaN:g?0:p);if(a=null==e?(N=!1,v):e,m.precision=a+=10,t=(i=M(g)).charAt(0),!(Math.abs(s=p.e)<15e14))return f=Z(m,a+2,v).times(s+""),p=V(new m(t+"."+i.slice(1)),a-10).plus(f),m.precision=v,null==e?_(p,v,w,N=!0):p;for(;t<7&&1!=t||1==t&&3<i.charAt(1);)t=(i=M((p=p.times(n)).d)).charAt(0),d++;for(s=p.e,1<t?(p=new m("0."+i),s++):p=new m(t+"."+i.slice(1)),c=o=p=F((h=p).minus(1),p.plus(1),a,1),l=_(p.times(p),a,1),r=3;;){if(o=_(o.times(l),a,1),M((f=c.plus(F(o,new m(r),a,1))).d).slice(0,a)===M(c.d).slice(0,a)){if(c=c.times(2),0!==s&&(c=c.plus(Z(m,a+2,v).times(s+""))),c=F(c,new m(d),a,1),null!=e)return m.precision=v,c;if(!O(c.d,a-10,w,u))return _(c,m.precision=v,w,N=!0);m.precision=a+=10,f=o=p=F(h.minus(1),h.plus(1),a,1),l=_(p.times(p),a,1),r=u=1}c=f,r+=2}}function j(n){return String(n.s*n.s/0)}function $(n,e){var i,t,r;for(-1<(i=e.indexOf("."))&&(e=e.replace(".","")),0<(t=e.search(/e/i))?(i<0&&(i=t),i+=+e.slice(t+1),e=e.substring(0,t)):i<0&&(i=e.length),t=0;48===e.charCodeAt(t);t++);for(r=e.length;48===e.charCodeAt(r-1);--r);if(e=e.slice(t,r)){if(r-=t,n.e=i=i-t-1,n.d=[],t=(i+1)%U,i<0&&(t+=U),t<r){for(t&&n.d.push(+e.slice(0,t)),r-=U;t<r;)n.d.push(+e.slice(t,t+=U));e=e.slice(t),t=U-e.length}else t-=r;for(;t--;)e+="0";n.d.push(+e),N&&(n.e>n.constructor.maxE?(n.d=null,n.e=NaN):n.e<n.constructor.minE&&(n.e=0,n.d=[0]))}else n.e=0,n.d=[0];return n}function W(n,e,i,t,r){var s,o,u,c,f=n.precision,a=Math.ceil(f/U);for(N=!1,c=i.times(i),u=new n(t);;){if(o=F(u.times(c),new n(e++*e++),f,1),u=r?t.plus(o):t.minus(o),t=F(o.times(c),new n(e++*e++),f,1),void 0!==(o=u.plus(t)).d[a]){for(s=a;o.d[s]===u.d[s]&&s--;);if(-1==s)break}s=u,u=t,t=o,o=s,0}return N=!0,o.d.length=a+1,o}function J(n,e){var i,t=e.s<0,r=P(n,n.precision,1),s=r.times(.5);if((e=e.abs()).lte(s))return o=t?4:1,e;if((i=e.divToInt(r)).isZero())o=t?3:2;else{if((e=e.minus(i.times(r))).lte(s))return o=H(i)?t?2:3:t?4:1,e;o=H(i)?t?1:4:t?3:2}return e.minus(r).abs()}function z(n,e,i,t){var r,s,o,u,c,f,a,h,l,d=n.constructor,p=void 0!==i;if(p?(q(i,1,g),void 0===t?t=d.rounding:q(t,0,8)):(i=d.precision,t=d.rounding),n.isFinite()){for(p?(r=2,16==e?i=4*i-3:8==e&&(i=3*i-2)):r=e,0<=(o=(a=A(n)).indexOf("."))&&(a=a.replace(".",""),(l=new d(1)).e=a.length-o,l.d=D(A(l),10,r),l.e=l.d.length),s=c=(h=D(a,10,r)).length;0==h[--c];)h.pop();if(h[0]){if(o<0?s--:((n=new d(n)).d=h,n.e=s,h=(n=F(n,l,i,t,0,r)).d,s=n.e,f=R),o=h[i],u=r/2,f=f||void 0!==h[i+1],f=t<4?(void 0!==o||f)&&(0===t||t===(n.s<0?3:2)):u<o||o===u&&(4===t||f||6===t&&1&h[i-1]||t===(n.s<0?8:7)),h.length=i,f)for(;++h[--i]>r-1;)h[i]=0,i||(++s,h.unshift(1));for(c=h.length;!h[c-1];--c);for(o=0,a="";o<c;o++)a+=m.charAt(h[o]);if(p){if(1<c)if(16==e||8==e){for(o=16==e?4:3,--c;c%o;c++)a+="0";for(c=(h=D(a,r,e)).length;!h[c-1];--c);for(o=1,a="1.";o<c;o++)a+=m.charAt(h[o])}else a=a.charAt(0)+"."+a.slice(1);a=a+(s<0?"p":"p+")+s}else if(s<0){for(;++s;)a="0"+a;a="0."+a}else if(++s>c)for(s-=c;s--;)a+="0";else s<c&&(a=a.slice(0,s)+"."+a.slice(s))}else a=p?"0p+0":"0";a=(16==e?"0x":2==e?"0b":8==e?"0o":"")+a}else a=j(n);return n.s<0?"-"+a:a}function G(n,e){if(n.length>e)return n.length=e,!0}function K(n){return new this(n).abs()}function Q(n){return new this(n).acos()}function X(n){return new this(n).acosh()}function Y(n,e){return new this(n).plus(e)}function nn(n){return new this(n).asin()}function en(n){return new this(n).asinh()}function tn(n){return new this(n).atan()}function rn(n){return new this(n).atanh()}function sn(n,e){n=new this(n),e=new this(e);var i,t=this.precision,r=this.rounding,s=t+4;return n.s&&e.s?n.d||e.d?!e.d||n.isZero()?(i=e.s<0?P(this,t,r):new this(0)).s=n.s:!n.d||e.isZero()?(i=P(this,s,1).times(.5)).s=n.s:i=e.s<0?(this.precision=s,this.rounding=1,i=this.atan(F(n,e,s,1)),e=P(this,s,1),this.precision=t,this.rounding=r,n.s<0?i.minus(e):i.plus(e)):this.atan(F(n,e,s,1)):(i=P(this,s,1).times(0<e.s?.25:.75)).s=n.s:i=new this(NaN),i}function on(n){return new this(n).cbrt()}function un(n){return _(n=new this(n),n.e+1,2)}function cn(n){if(!n||"object"!=typeof n)throw Error(f+"Object expected");var e,i,t,r=!0===n.defaults,s=["precision",1,g,"rounding",0,8,"toExpNeg",-u,0,"toExpPos",0,u,"maxE",0,u,"minE",-u,0,"modulo",0,9];for(e=0;e<s.length;e+=3)if(i=s[e],r&&(this[i]=c[i]),void 0!==(t=n[i])){if(!(L(t)===t&&s[e+1]<=t&&t<=s[e+2]))throw Error(w+i+": "+t);this[i]=t}if(i="crypto",r&&(this[i]=c[i]),void 0!==(t=n[i])){if(!0!==t&&!1!==t&&0!==t&&1!==t)throw Error(w+i+": "+t);if(t){if("undefined"==typeof crypto||!crypto||!crypto.getRandomValues&&!crypto.randomBytes)throw Error(a);this[i]=!0}else this[i]=!1}return this}function fn(n){return new this(n).cos()}function an(n){return new this(n).cosh()}function hn(n,e){return new this(n).div(e)}function ln(n){return new this(n).exp()}function dn(n){return _(n=new this(n),n.e+1,3)}function pn(){var n,e,i=new this(0);for(N=!1,n=0;n<arguments.length;)if((e=new this(arguments[n++])).d)i.d&&(i=i.plus(e.times(e)));else{if(e.s)return N=!0,new this(1/0);i=e}return N=!0,i.sqrt()}function gn(n){return n instanceof h||n&&"[object Decimal]"===n.name||!1}function mn(n){return new this(n).ln()}function wn(n,e){return new this(n).log(e)}function vn(n){return new this(n).log(2)}function Nn(n){return new this(n).log(10)}function bn(){return i(this,arguments,"lt")}function En(){return i(this,arguments,"gt")}function xn(n,e){return new this(n).mod(e)}function yn(n,e){return new this(n).mul(e)}function Mn(n,e){return new this(n).pow(e)}function qn(n){var e,i,t,r,s=0,o=new this(1),u=[];if(void 0===n?n=this.precision:q(n,1,g),t=Math.ceil(n/U),this.crypto)if(crypto.getRandomValues)for(e=crypto.getRandomValues(new Uint32Array(t));s<t;)429e7<=(r=e[s])?e[s]=crypto.getRandomValues(new Uint32Array(1))[0]:u[s++]=r%1e7;else{if(!crypto.randomBytes)throw Error(a);for(e=crypto.randomBytes(t*=4);s<t;)214e7<=(r=e[s]+(e[s+1]<<8)+(e[s+2]<<16)+((127&e[s+3])<<24))?crypto.randomBytes(4).copy(e,s):(u.push(r%1e7),s+=4);s=t/4}else for(;s<t;)u[s++]=1e7*Math.random()|0;for(t=u[--s],n%=U,t&&n&&(r=v(10,U-n),u[s]=(t/r|0)*r);0===u[s];s--)u.pop();if(s<0)u=[i=0];else{for(i=-1;0===u[0];i-=U)u.shift();for(t=1,r=u[0];10<=r;r/=10)t++;t<U&&(i-=U-t)}return o.e=i,o.d=u,o}function On(n){return _(n=new this(n),n.e+1,this.rounding)}function Dn(n){return(n=new this(n)).d?n.d[0]?n.s:0*n.s:n.s||NaN}function Fn(n){return new this(n).sin()}function An(n){return new this(n).sinh()}function Sn(n){return new this(n).sqrt()}function Zn(n,e){return new this(n).sub(e)}function Pn(n){return new this(n).tan()}function Rn(n){return new this(n).tanh()}function Ln(n){return _(n=new this(n),n.e+1,1)}(h=function n(e){var i,t,r;function s(n){var e,i,t,r=this;if(!(r instanceof s))return new s(n);if(n instanceof(r.constructor=s))return r.s=n.s,void(N?!n.d||n.e>s.maxE?(r.e=NaN,r.d=null):n.e<s.minE?(r.e=0,r.d=[0]):(r.e=n.e,r.d=n.d.slice()):(r.e=n.e,r.d=n.d?n.d.slice():n.d));if("number"==(t=typeof n)){if(0===n)return r.s=1/n<0?-1:1,r.e=0,void(r.d=[0]);if(r.s=n<0?(n=-n,-1):1,n===~~n&&n<1e7){for(e=0,i=n;10<=i;i/=10)e++;return void(r.d=N?s.maxE<e?(r.e=NaN,null):e<s.minE?[r.e=0]:(r.e=e,[n]):(r.e=e,[n]))}return 0*n!=0?(n||(r.s=NaN),r.e=NaN,void(r.d=null)):$(r,n.toString())}if("string"!==t)throw Error(w+n);return 45===n.charCodeAt(0)?(n=n.slice(1),r.s=-1):r.s=1,b.test(n)?$(r,n):function(n,e){var i,t,r,s,o,u,c,f,a;if("Infinity"===e||"NaN"===e)return+e||(n.s=NaN),n.e=NaN,n.d=null,n;if(d.test(e))i=16,e=e.toLowerCase();else if(l.test(e))i=2;else{if(!p.test(e))throw Error(w+e);i=8}for(o=0<=(s=(e=0<(s=e.search(/p/i))?(c=+e.slice(s+1),e.substring(2,s)):e.slice(2)).indexOf(".")),t=n.constructor,o&&(s=(u=(e=e.replace(".","")).length)-s,r=I(t,new t(i),s,2*s)),s=a=(f=D(e,i,T)).length-1;0===f[s];--s)f.pop();return s<0?new t(0*n.s):(n.e=S(f,a),n.d=f,N=!1,o&&(n=F(n,r,4*u)),c&&(n=n.times(Math.abs(c)<54?Math.pow(2,c):h.pow(2,c))),N=!0,n)}(r,n)}if(s.prototype=y,s.ROUND_UP=0,s.ROUND_DOWN=1,s.ROUND_CEIL=2,s.ROUND_FLOOR=3,s.ROUND_HALF_UP=4,s.ROUND_HALF_DOWN=5,s.ROUND_HALF_EVEN=6,s.ROUND_HALF_CEIL=7,s.ROUND_HALF_FLOOR=8,s.EUCLID=9,s.config=s.set=cn,s.clone=n,s.isDecimal=gn,s.abs=K,s.acos=Q,s.acosh=X,s.add=Y,s.asin=nn,s.asinh=en,s.atan=tn,s.atanh=rn,s.atan2=sn,s.cbrt=on,s.ceil=un,s.cos=fn,s.cosh=an,s.div=hn,s.exp=ln,s.floor=dn,s.hypot=pn,s.ln=mn,s.log=wn,s.log10=Nn,s.log2=vn,s.max=bn,s.min=En,s.mod=xn,s.mul=yn,s.pow=Mn,s.random=qn,s.round=On,s.sign=Dn,s.sin=Fn,s.sinh=An,s.sqrt=Sn,s.sub=Zn,s.tan=Pn,s.tanh=Rn,s.trunc=Ln,void 0===e&&(e={}),e&&!0!==e.defaults)for(r=["precision","rounding","toExpNeg","toExpPos","maxE","minE","modulo","crypto"],i=0;i<r.length;)e.hasOwnProperty(t=r[i++])||(e[t]=this[t]);return s.config(e),s}(c)).default=h.Decimal=h,t=new h(t),r=new h(r),"function"==typeof define&&define.amd?define(function(){return h}):"undefined"!=typeof module&&module.exports?("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator&&(y[Symbol.for("nodejs.util.inspect.custom")]=y.toString,y[Symbol.toStringTag]="Decimal"),module.exports=h):(n||(n="undefined"!=typeof self&&self&&self.self==self?self:window),e=n.Decimal,h.noConflict=function(){return n.Decimal=e,h},n.Decimal=h)}(this);
module.exports = {Decimal: module.exports.Decimal};
});
;
Numbas.queueScript('diagnostic', ['util', 'jme', 'localisation', 'jme-variables'], function() {
    var jme = Numbas.jme;

    var diagnostic = Numbas.diagnostic = {
        scripts: {},
        load_scripts: function() {
            for(var x in Numbas.raw_diagnostic_scripts) {
                diagnostic.scripts[x] = new diagnostic.DiagnosticScript(Numbas.raw_diagnostic_scripts[x], null, Numbas.jme.builtinScope);
            }
        }
    };

    diagnostic.DiagnosticScript = Numbas.jme.variables.note_script_constructor();

    /** Definition of a knowledge graph.
     *
     * @typedef Numbas.diagnostic.knowledge_graph_definition
     * @property {Array.<{title: string, name: string}>} learning_objectives - Learning objectives
     * @property {Array.<{depends_on: Array.<string>, title: string, learning_objectives: Array.<string>, name: string}>} topics - Topics
     */

    /**
     * A representation of a knowledge graph, with a node for each topic and a directed edge when one topic leads to another.
     *
     * @param {Numbas.diagnostic.knowledge_graph_definition} data
     */
    diagnostic.KnowledgeGraph = function(data) {
        this.data = data;
        var topicdict = this.topicdict = {};
        this.topics = (data.topics || []).map(function(t) {
            var topic = {
                name: t.name,
                learning_objectives: (t.learning_objectives || []).slice(),
                depends_on: (t.depends_on || []).slice(),
                leads_to: []
            };
            topicdict[topic.name] = topic;
            return t;
        });

        this.topics.forEach(function(t) {
            (t.depends_on || []).forEach(function(name) {
                topicdict[name].leads_to.push(t.name);
            });
        });

        this.learning_objectives = (data.learning_objectives || []).slice();
    }

    /** A controller for a diagnostic algorithm.
     *
     * @param {Numbas.diagnostic.KnowledgeGraph} knowledge_graph
     * @param {Numbas.Exam} exam
     * @param {Numbas.diagnostic.DiagnosticScript} script
     */
    var DiagnosticController = diagnostic.DiagnosticController = function(knowledge_graph, exam, script) {
        this.knowledge_graph = knowledge_graph;
        this.exam = exam;
        this.script = script;
        this.scope = new jme.Scope([exam.scope, {variables: this.make_init_variables()}]);
        this.state = script.evaluate_note('state', this.scope).value;
    }
    DiagnosticController.prototype = {
        /**
         * Produce summary data about a question for a diagnostic script to use.
         *
         * @param {Numbas.Question} question
         * @returns {Numbas.jme.token} - A dictionary with keys `name`, `number` and `credit`.
         */
        question_data: function(question) {
            if(!question) {
                return new jme.types.TNothing();
            }
            return jme.wrapValue({
                name: question.name,
                number: question.number,
                credit: question.marks > 0 ? question.score / question.marks : 0,
                marks: question.marks
            });
        },

        /**
         * Make the initial variables for the diagnostic script.
         *
         * @returns {object}
         */
        make_init_variables: function() {
            var dc = this;

            var topicdict = {};
            Object.entries(this.knowledge_graph.topicdict).forEach(function(d) {
                var topic_name = d[0];
                var topic = {};
                Object.entries(d[1]).forEach(function(x) {
                    topic[x[0]] = x[1];
                });
                var group = dc.exam.question_groups.find(function(g) {
                    return g.settings.name == topic_name;
                });
                if(!group) {
                    return;
                }
                topic.questions = [];
                for(var i = 0;i < group.numQuestions;i++) {
                    topic.questions.push({
                        topic: topic_name,
                        number: i
                    });
                }
                topicdict[topic_name] = topic;
            });

            return {
                topics: jme.wrapValue(topicdict),
                learning_objectives: jme.wrapValue(this.knowledge_graph.learning_objectives)
            }
        },

        /** Get the name of the topic the current question belongs to.
         *
         * @returns {string}
         */
        current_topic: function() {
            return this.exam.currentQuestion ? this.exam.currentQuestion.group.settings.name : null;
        },

        /**
         * Evaluate a note in the diagnostic script, adding in the `state` and `current_question` variables.
         *
         * @param {string} note - The name of the note to evaluate.
         * @returns {Numbas.jme.token}
         */
        evaluate_note: function(note) {
            var parameters = {
                state: this.state,
                current_topic: jme.wrapValue(this.current_topic()),
                current_question: this.question_data(this.exam.currentQuestion)
            }
            return this.script.evaluate_note(note, this.scope, parameters).value;
        },

        /** Unwrap a description of a question produced by the script, to either `null` or a dictionary with keys `topic` and `number`.
         *
         * @param {Numbas.jme.token} v
         * @returns {object|null}
         */
        unwrap_question: function(v) {
            if(jme.isType(v, 'nothing')) {
                return null;
            } else {
                return jme.unwrapValue(jme.castToType(v,
                    {
                        type: 'dict',
                        items: {
                            'topic': 'string',
                            'number': 'number'
                        }
                    }
                ));
            }
        },

        /** Get the new state after ending the exam.
         */
        after_exam_ended: function() {
            this.state = this.evaluate_note('after_exam_ended');
        },

        /**
         * Get the list of actions to offer to the student when they ask to move on.
         *
         * @returns {object}
         */
        next_actions: function() {
            var dc = this;
            var res = this.evaluate_note('next_actions');
            res = jme.castToType(res, 'dict');
            var feedback = jme.unwrapValue(jme.castToType(res.value.feedback, 'string'));
            var actions = jme.castToType(res.value.actions, 'list').value.map(function(action) {
                action = jme.castToType(action, 'dict');
                return {
                    label: jme.unwrapValue(action.value.label),
                    state: action.value.state,
                    next_topic: dc.unwrap_question(action.value.next_question)
                };
            });
            return {
                feedback: feedback,
                actions: actions
            };
        },

        /** Get the first topic to pick a question on.
         *
         * @returns {string}
         */
        first_question: function() {
            var res = this.evaluate_note('first_question');
            return this.unwrap_question(res);
        },

        /**
         * Produce a summary of the student's progress through the test.
         *
         * @returns {string}
         */
        progress: function() {
            var res = jme.castToType(
                this.evaluate_note('progress'),
                {
                    type: 'list',
                    all_items: {
                        type: 'dict',
                        items: {
                            'name': 'string',
                            'progress': 'number',
                            'credit': 'number'
                        }
                    }
                }
            );
            return jme.unwrapValue(res);
        },

        /**
         * Get a block of feedback text to show to the student.
         *
         * @returns {string}
         */
        feedback: function() {
            var res = this.evaluate_note('feedback');
            return jme.unwrapValue(jme.castToType(res, 'string'));
        }
    }
})
;
Numbas.queueScript('display-base', ['display-util', 'display-color', 'controls', 'math', 'xml', 'util', 'timing', 'jme', 'jme-display'], function() {
var jme = Numbas.jme;
var display_util = Numbas.display_util;
var display_color = Numbas.display_color;

var mj_promise = globalThis.MathJax?.startup?.promise || Promise.resolve();

/** A . controller for user style customisations.
 */
class StyleCustomiser {
    static style_variables = ['--text-size', '--spacing-scale', '--font-weight', '--main-font'];

    static color_groups = [
        'background',
        'text',
        'main',
        'primary',
        'link',
        'success',
        'info',
        'warning',
        'danger',
        'muted',
        'highlight',
    ];

    constructor(root_element) {
        this.root_element = root_element;
        this.initial_style = getComputedStyle(root_element);

        this.style = this.make_style_object();
        this.staged_style = this.make_style_object();

        this.constructor.color_groups.forEach((name) => {
            this.style[`--custom-${name}-color`] = this.styleObservable(`--light-${name}-color`);
        });

        this.color_scheme = Knockout.observable('automatic');
        this.color_scheme.initial_value = this.color_scheme();

        this.using_custom_color_scheme = Knockout.pureComputed(() => this.color_scheme() == 'custom');

        /** Make an observable which watches the given media query. Its value is `true` if the media query is matches.
         *
         * @param {string} query
         * @returns {observable.<boolean>}
         */
        function media_observable(query) {
            const match = window.matchMedia(query);
            const obs = Knockout.observable(match.matches);
            match.addEventListener('change', () => {
                obs(match.matches);
            });
            return obs;
        }

        this.forced_colors = media_observable('(forced-colors: active)');
        this.prefers_dark = media_observable('(prefers-color-scheme: dark)');

        this.invert_images = Knockout.observable(false);

        this.font_options = Numbas.display.font_options.map(({name, label}) => {
            return {name, label: R(label)};
        });

        this.reset();

        this.load();

        Knockout.computed(() => {
            const root = this.root_element;

            var css_vars = this.css_vars();

            let color_scheme = this.color_scheme();
            if(color_scheme == 'automatic') {
                root.dataset.prefersColorScheme = this.prefers_dark() ? 'dark' : 'light';
            } else {
                root.dataset.prefersColorScheme = color_scheme;
            }

            const invert_images = this.invert_images();
            root.dataset.invertImages = invert_images;

            for(const [k, v] of Object.entries(css_vars)) {
                root.style.setProperty(k, v);
            }

            const custom_bg = this.style['--custom-background-color']();
            const custom_text = this.style['--custom-text-color']();
            const target_contrast = display_color.dpsContrast(display_color.parseRGB(custom_bg), display_color.parseRGB(custom_text));

            this.constructor.color_groups.forEach((name) => {
                const property_name = `--custom-${name}-color`;
                const col = this.style[property_name]();
                const rgb = display_color.parseRGB(col);
                root.style.setProperty(property_name, col);
                const text = display_color.text_for(rgb, target_contrast);
                root.style.setProperty(`--custom-${name}-text-color`, text);
                if(name == 'background') {
                    root.classList.toggle('dark-background', color_scheme == 'custom' && display_color.is_dark(rgb));
                }
            });

            var options = {
                color_scheme,
                invert_images
            };

            for(const [k, v] of Object.entries(this.style)) {
                options[k] = v();
            }

            try {
                localStorage.setItem(Numbas.display.style_options_localstorage_key, JSON.stringify(options));
            } catch {
            }
        });

    }

    css_vars() {
        return {
            '--text-size': parseFloat(this.style['--text-size']()),
            '--spacing-scale': parseFloat(this.style['--spacing-scale']()),
            '--font-weight': parseFloat(this.style['--font-weight']()),
            '--main-font': this.style['--main-font'](),
            '--staged-text-size': parseFloat(this.staged_style['--text-size']()),
            '--staged-spacing-scale': parseFloat(this.staged_style['--spacing-scale']()),
            '--staged-font-weight': parseFloat(this.staged_style['--font-weight']()),
            '--staged-main-font': this.staged_style['--main-font'](),
        };
    }

    /** Make a Knockout observable whose initial value is taken from the CSS custom property with the given name.
     *
     * @param {string} property_name
     * @returns {observable.<string>}
     */
    styleObservable(property_name) {
        const value = this.initial_style.getPropertyValue(property_name);
        const obs = Knockout.observable();
        obs.initial_value = value;
        return obs;
    }

    /** Make an object with an observable for each name in `this.style_variables`.
     *
     * @returns {Object<observable<string>>}
     */
    make_style_object() {
        return Object.fromEntries(this.constructor.style_variables.map((name) => [name, name.startsWith('--') ? this.styleObservable(name) : Knockout.observable('')]));
    }

    /** Save the changes to the style options.
     */
    save() {
        Object.entries(this.staged_style).forEach(([k, obs]) => {
            this.style[k](obs());
        });
    }

    /** Load the style options from localStorage.
     */
    load() {
        try {
            const saved_style_options = JSON.parse(localStorage.getItem(Numbas.display.style_options_localstorage_key)) || {};
            if(saved_style_options.color_scheme) {
                this.color_scheme(saved_style_options.color_scheme);
            }
            if(saved_style_options.invert_images !== undefined) {
                this.invert_images(saved_style_options.invert_images);
            }
            for(const [k, v] of Object.entries(this.style)) {
                if(k in saved_style_options) {
                    v(saved_style_options[k]);
                }
            }
            for(const [k, v] of Object.entries(this.staged_style)) {
                v(this.style[k]());
            }
        } catch(e) {
            console.error(e);
        }
    }

    /** Reset the style variables to their initial values.
     */
    reset() {
        Object.values(this.style).forEach((obs) => {
            if(obs.initial_value !== undefined) {
                obs(obs.initial_value);
            }
        });
        Object.values(this.staged_style).forEach((obs) => {
            if(obs.initial_value !== undefined) {
                obs(obs.initial_value);
            }
        });
        this.color_scheme(this.color_scheme.initial_value);
    }
}

/** A container element for a self-contained Numbas exam.
 */
class NumbasExamElement extends HTMLElement {
    //alert / confirm boxes
    //
    /** Callback functions for the modals.
     *
     * @type {Object<Function>}
     */
    modal = {
        ok: Knockout.observable(function() {}),
        cancel: Knockout.observable(function() {}),
    }

    lightbox_pressing_state = 'none';

    constructor() {
        super();

        this.attachShadow({mode:'open'});
    }

    connectedCallback() {
        if(this.getAttribute('noload') !== null) {
            return;
        }
        if(document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => this.load_exam());
        } else {
            this.load_exam();
        }
    }

    /** Has this exam been loaded?
     */
    has_loaded = false;

    async load_exam() {
        if(this.has_loaded) {
            return;
        }

        this.has_loaded = true;

        await Numbas.awaitScripts(['start-exam', 'display']);

        const template_id = this.getAttribute('template') || 'numbas-exam-template';

        const template = document.getElementById(template_id);
        this.shadowRoot.append(template.content.cloneNode(true));

        this.load();
    }

    showLoadProgress(scheduler) {
        var p = 100 * scheduler.completed_jobs / scheduler.num_jobs;
        this.shadowRoot.querySelector('#loading progress').value = p;
    }

    async load() {
        try {
            const options = {
                exam_url: this.getAttribute('source_url'),
                storage:  this.getAttribute('storage')?.toLowerCase(),
                element: this
            };
            const exam_source_element = this.querySelector('script[type="application/numbas-exam"]');
            if(exam_source_element) {
                options.exam_source = exam_source_element.textContent;
            }

            if(!(options.exam_url || options.exam_source)) {
                return;
            }

            const {exam_data} = await Numbas.load_exam(options);

            let extension_data_json = this.getAttribute('extensions');
            const extension_data_element = this.querySelector('script[slot="extension-data"]');
            if(extension_data_element) {
                extension_data_json = extension_data_element.textContent.trim();
            }
            const extension_data = JSON.parse(extension_data_json);

            for(const extension of exam_data.extensions) {
                const data = extension_data[extension];
                Numbas.extension_url_root[extension] = data.root;
                for(const js of data.javascripts) {
                    const src = `${data.root}/${js}`;
                    if(!document.head.querySelector(`script[data-numbas-extension="${extension}"][src="${src}"]`)) {
                        const script = document.createElement('script');
                        script.src = src;
                        script.dataset.numbasExtension = extension;
                        document.head.appendChild(script);
                    }
                }
                for(const css of data.stylesheets) {
                    const link = document.createElement('link');
                    link.rel = 'stylesheet';
                    const src = `${data.root}/${css}`;
                    link.href = src;
                    this.shadowRoot.appendChild(link);
                }
            }
        } catch(err) {
            Numbas.display.die(err);
        }
    }

    init(exam) {
        this.shadowRoot.append(MathJax.svgStylesheet().cloneNode(true));

        const lightbox = this.lightbox = this.shadowRoot.getElementById('lightbox');
        lightbox.addEventListener('click', () => this.hide_lightbox());
        document.addEventListener('keyup', () => {
            if(lightbox.classList.contains('shown')) {
                this.hide_lightbox();
            }
        });

        display_util.localisePage(this.shadowRoot);

        // bind buttons in the modals

        for(const b of this.shadowRoot.querySelectorAll('button[aria-controls="navMenu"]')) {
            b.addEventListener('click', () => {
                this.shadowRoot.querySelector('exam-container').classList.toggle('show-sidebar');
            });
        }

        display_util.set_jme_scope(this.shadowRoot.querySelector('#infoDisplay'), exam.scope);
        display_util.set_jme_scope(this.shadowRoot.querySelector('#diagnostic-feedback'), exam.scope);


        const style_customiser = new StyleCustomiser(this.shadowRoot.querySelector('exam-container'));
        this.style_customiser = style_customiser;

        var vm = this.viewModel = {
            exam: Knockout.observable(exam.display),
            style_customiser,
            saveStyle: () => {
                style_customiser.save();
                this.shadowRoot.getElementById('style-modal').close();
            },
            modal: this.modal,
            loaded: Knockout.observable(false),

            showStyleModal: () => this.showStyleModal(),

            closeModal: function(_, e) {
                let el = e.target;
                while(el && el.tagName != 'DIALOG') {
                    el = el.parentElement;
                }
                if(el) {
                    el.close();
                }
            }
        };


        vm.css = Knockout.pureComputed(function() {
            var exam = vm.exam();
            var classes = {
                'show-nav': exam.viewType() == 'question' || (exam.viewType() == 'infopage' && exam.infoPage() == 'introduction'),
                'show-sidebar': false,
                'no-printing': !exam.allowPrinting(),
                'info-page': exam.viewType() == 'infopage',
                'no-printing-questions': !exam.exam.settings.resultsprintquestions,
                'no-printing-advice': !exam.exam.settings.resultsprintadvice,
                'loaded': vm.loaded(),
            }
            return classes;
        });

        exam.signals.on('ready', () => {
            vm.loaded(true);
            this.dispatchEvent(new CustomEvent('numbas:loaded', {detail: {exam: this.exam}}));
        })


        vm.attr = Knockout.pureComputed(() => {
            var exam = vm.exam();
            return {
                'data-navigatemode': exam.exam.settings.navigateMode,
            }
        });

        this.setExam(exam);

        Knockout.applyBindings(this.viewModel, this.shadowRoot.querySelector('exam-container'));
    }

    setExam(exam) {
        this.viewModel.exam(exam.display);
        for(var i = 0;i < exam.questionList.length;i++) {
            exam.display.applyQuestionBindings(exam.questionList[i]);
        }
        exam.display.questions().map(function(q) {
            q.question.signals.on('HTMLAttached', function() {
                q.init();
            });
        });
        exam.signals.on_any((name) => {
            this.dispatchEvent(new CustomEvent('numbas:signal:' + name));
        });
        exam.events.on('', (name, ...args) => {
            this.dispatchEvent(new CustomEvent('numbas:event:' + name, {detail: args}));
        });
        Numbas.signals.trigger('display ready');
        this.dispatchEvent(new CustomEvent('numbas:setExam', {detail: {exam}}));
    }

    /** Show an alert dialog.
     *
     * @param {string} msg - message to show the user
     * @param {Function} fnOK - callback when OK is clicked
     */
    showAlert(msg, fnOK) {
        this.modal.ok(fnOK);
        this.shadowRoot.getElementById('alert-modal-body').innerHTML = msg;
        this.shadowRoot.getElementById('alert-modal').showModal();
    }

    /** Show the modal with styling options.
     */
    showStyleModal() {
        this.shadowRoot.getElementById('style-modal').showModal();
    }

    /** Show a confirmation dialog box.
     *
     * @param {string} msg - message to show the user
     * @param {Function} fnOK - callback if OK is clicked
     * @param {Function} fnCancel - callback if cancelled
     */
    showConfirm(msg, fnOK, fnCancel) {
        this.modal.ok(fnOK);
        this.modal.cancel(fnCancel);
        this.shadowRoot.getElementById('confirm-modal-body').innerHTML = msg;
        this.shadowRoot.getElementById('confirm-modal').showModal();
    }

    /** Show the end exam confirmation dialog box.
     *
     * @param {string} msg - message to show the user
     * @param {Function} fnEnd - callback to end the exam
     * @param {Function} fnCancel - callback if cancelled
     */
    showConfirmEndExam(msg, fnEnd, fnCancel) {
        this.modal.ok(fnEnd);
        this.modal.cancel(fnCancel);
        const confirmationInputMsg = R('modal.confirm end exam', {endConfirmation : R('control.confirm end.password')});
        this.shadowRoot.getElementById('confirm-end-exam-modal-message').innerHTML = msg;
        this.shadowRoot.getElementById('confirm-end-exam-modal-input-message').innerHTML = confirmationInputMsg;
        this.shadowRoot.getElementById('confirm-end-exam-modal').showModal();
    }

    /** Register event listeners to show the lightbox when images in this element are clicked.
     *
     * @param {Element} element
     */
    register_lightbox(element) {
        const register_image = (img) => {

            var wrapper = document.createElement('span');
            wrapper.setAttribute('class', 'lightbox-image-wrapper');
            var align = img.getAttribute('align');
            switch(align) {
                case 'bottom':
                case 'middle':
                case 'top':
                    wrapper.style['vertical-align'] = align;
                    break;
                case 'left':
                case 'right':
                    wrapper.style['float'] = align;
                    break;
            }

            img.replaceWith(wrapper);

            wrapper.appendChild(img);

            var button = document.createElement('button');
            button.type = 'button';
            button.textContent = '🔍';
            button.title = button.ariaLabel = R('lightbox.zoom in on image');

            const activate = () => {
                const {lightbox} = this;
                lightbox.innerHTML = '';
                var elem = img.cloneNode();
                const scope = display_util.find_jme_scope(img);
                if(scope) {
                    jme.variables.DOMcontentsubvars(elem, scope);
                }
                lightbox.appendChild(elem);
                this.show_lightbox(wrapper);
            }

            button.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                if(this.lightbox_pressing_state != 'key') {
                    activate();
                } else {
                    this.lightbox_pressing_state = 'click';
                }
            });
            button.addEventListener('keydown', (e) => {
                this.lightbox_pressing_state = 'key';
            })
            button.addEventListener('keyup', (e) => {
                if(this.lightbox_pressing_state == 'click') {
                    e.preventDefault();
                    e.stopPropagation();
                    activate();
                }
                this.lightbox_pressing_state = 'none';
            })
            button.addEventListener('blur', (e) => {
                this.lightbox_pressing_state = 'none';
            });

            button.addEventListener

            wrapper.appendChild(button);
        }
        Array.from(element.querySelectorAll('img,object')).forEach(function(img) {
            if(img.complete) {
                register_image(img);
            } else {
                img.addEventListener('load', function() {
                    register_image(img);
                }, {once: true});
            }
        });
    }

    /** Show the lightbox.
     *
     * @param {Element} original - The original image element which is going to be copied into the lightbox.
     */
    show_lightbox(original) {
        this.lightbox.showModal();
        this.lightbox_original_element = original;
    }

    /** Hide the lightbox.
     *
     */
    hide_lightbox() {
        this.lightbox.close();
        this.lightbox.innerHTML = '';
        this.lightbox_pressing_state = 'none';
        if(this.lightbox_original_element) {
            this.lightbox_original_element.querySelector('button').focus();
        }
    }

}
customElements.define('numbas-exam', NumbasExamElement);

/** @namespace Numbas.display */
var display = Numbas.display = /** @lends Numbas.display */ {
    /**
     * Should Numbas control the browser's scroll and focus?
     * Set to true if the page only contains this Numbas exam.
     * Disable this if the exam is embedded in a page.
     */
    control_focus: true,

    /** Initialise the display.
     */
    init: function() {
        document.body.classList.add('loaded');

        display_util.localisePage(document.body);
    },

    /** Update the progress bar when loading.
     */
    showLoadProgress: function() {
        const progress_element = document.querySelector('#loading progress');
        if(!progress_element) {
            return;
        }

        var p = 100 * Numbas.schedule.completed / Numbas.schedule.total;
        progress_element.value = p;
    },

    style_options_localstorage_key: 'numbas-style-options',

    /** List of options for the display font.
     */
    font_options: [
        {name: 'sans-serif', label: 'modal.style.font.sans serif'},
        {name: 'serif', label: 'modal.style.font.serif'},
        {name: 'monospace', label: 'modal.style.font.monospace'},
    ],

    /** Make MathJax typeset any maths in the selector.
     *
     * @param {Element} [element] - Element to typeset. If not given, the whole page is typeset.
     * @param {Function} callback - Function to call when typesetting is finished.
     */
    typeset: function(element, callback) {
        var delay = 10;

        /**
         * Try to typeset the given elements.
         * An element is typeset if it is attached to the main document, and has a parent which specifies a JME scope to use.
         *
         * After each attempt, if there are any elements still waiting to be typeset, there's an exponentially growing delay before trying again.
         *
         * Once all elements have been typeset, the callback is called.
         */
        function try_to_typeset() {
            var root = display.find_root_ancestor(element);
            var scope = display_util.find_jme_scope(element);

            if((root.nodeType == root.DOCUMENT_FRAGMENT_NODE || root.nodeType == root.DOCUMENT_NODE) && scope) {
                mj_promise = mj_promise.then(() => {
                    MathJax.typesetPromise([element]).then(() => {
                        if(callback) {
                            callback();
                        }
                    });
                }).catch((e) => {
                    if(MathJax === undefined && !display.failedMathJax) {
                        display.failedMathJax = true;
                    } else {
                        Numbas.schedule.halt(e);
                    }
                });
                return;
            }

            delay *= 1.1;
            setTimeout(try_to_typeset, delay);
        }

        setTimeout(try_to_typeset, 1);
    },

    /** Make HTML from an XML node and bind it to the given scope and display object.
     * Variables are substituted from the given scope using {@link Numbas.jme.variables.DOMcontentsubvars}.
     *
     * @param {Element} xml
     * @param {XMLDocument} template
     * @param {Numbas.jme.Scope} scope
     * @param {string} contextDescription - Description of the JME context, for error messages.
     * @param {Element} root_element - The exam's root `<numbas-exam>` element - used to register lightbox handlers.
     * @returns {Promise} - Resolves to the produced HTML element after variables have been substituted.
     */
    makeHTMLFromXML: function(xml, template, scope, contextDescription, root_element) {
        var htmlString = Numbas.xml.transform(template, xml);
        var d = document.createElement('div');
        d.innerHTML = htmlString;
        Numbas.xml.localise(d);
        var html = d.firstElementChild;
        display_util.set_jme_scope(html, scope);
        if(!html.getAttribute('data-jme-context-description')) {
            html.setAttribute('data-jme-context-description', contextDescription);
        }
        var promise = new Promise(
            function(resolve, reject) {
                html = jme.variables.DOMcontentsubvars(html, scope);

                root_element.register_lightbox(html);
                Numbas.display.typeset(html);
                resolve(html);
            })
            .catch(function(error) {
                var errorContextDescriptionBits = [];
                var errorContextDescription;
                if(error.element) {
                    var elem = error.element;
                    while(elem) {
                        if(elem.nodeType == 1) {
                            var desc = Numbas.display.getLocalisedAttribute(elem, 'data-jme-context-description');
                            if(desc) {
                                errorContextDescriptionBits.splice(0, 0, desc);
                            }
                        }
                        elem = elem.parentElement;
                    }
                    errorContextDescription = errorContextDescriptionBits.join(' ');
                } else {
                    errorContextDescription = contextDescription;
                }
                Numbas.schedule.halt(new Numbas.Error('display.error making html', {contextDescription: errorContextDescription, message: error.message}, error));
            })
        ;

        return promise;
    },


    /** The Numbas exam has failed so much it can't continue - show an error message and the error.
     *
     * @param {Error} e
     */
    die: function(e) {
        var message = (e || e.message) + '';
        var stack = e.stack.replace(/\n/g, '<br>\n');
        Numbas.debug(message, false, e);
        //hide all the non-error stuff
        for(const element of document.querySelectorAll('numbas-exam')) {
            element.hidden = true;
        }
        //show the error stuff
        const die_element = document.getElementById('die');
        if(!die_element) {
            return;
        }
        die_element.hidden = false;
        document.body.querySelector('#die .error .message').innerHTML = message;
        document.body.querySelector('#die .error .stack').innerHTML = stack;
    },

    // References to functions in Numbas.display_util, for backwards compatibility.
    measureText: display_util.measureText,

    showScoreFeedback: display_util.showScoreFeedback,

    passwordHandler: display_util.passwordHandler,

    localisePage: display_util.localisePage,

    getLocalisedAttribute: display_util.getLocalisedAttribute,

    setJMEScope: display_util.set_jme_scope,

    find_root_ancestor: display_util.find_root_ancestor,

};

display.StyleCustomiser = StyleCustomiser;
display.NumbasExamElement = NumbasExamElement;

});
;
Numbas.queueScript('display-color', [], function() {
    /** Based on culori.js
     *
     * MIT License
     * 
     * Copyright (c) 2018 Dan Burzo
     * 
     * Permission is hereby granted, free of charge, to any person obtaining a copy
     * of this software and associated documentation files (the "Software"), to deal
     * in the Software without restriction, including without limitation the rights
     * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     * copies of the Software, and to permit persons to whom the Software is
     * furnished to do so, subject to the following conditions:
     * 
     * The above copyright notice and this permission notice shall be included in all
     * copies or substantial portions of the Software.
     * 
     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     * SOFTWARE.
     */

    var k = Math.pow(29, 3) / Math.pow(3, 3);
    var e = Math.pow(6, 3) / Math.pow(29, 3);

    var D65 = {
        X: 0.3127 / 0.329,
        Y: 1,
        Z: (1 - 0.3127 - 0.329) / 0.329
    };

    var f = (value) => value > e ? Math.cbrt(value) : (k * value + 16) / 116;

    /** Convert a colour in XYZ65 format to LAB65.
     */
    var convertXyz65ToLab65 = ({ x, y, z, alpha }) => {
        let f0 = f(x / D65.X);
        let f1 = f(y / D65.Y);
        let f22 = f(z / D65.Z);
        let res = {
            mode: "lab65",
            l: 116 * f1 - 16,
            a: 500 * (f0 - f1),
            b: 200 * (f1 - f22)
        };
        if (alpha !== void 0) {
            res.alpha = alpha;
        }
        return res;
    };
    var convertXyz65ToLab65_default = convertXyz65ToLab65;

    /** Convert a colour in RGB format to XYZ65.
     */
    var convertRgbToXyz65 = (rgb4) => {
        let { r: r2, g, b, alpha } = convertRgbToLrgb_default(rgb4);
        let res = {
            mode: "xyz65",
            x: 0.4123907992659593 * r2 + 0.357584339383878 * g + 0.1804807884018343 * b,
            y: 0.2126390058715102 * r2 + 0.715168678767756 * g + 0.0721923153607337 * b,
            z: 0.0193308187155918 * r2 + 0.119194779794626 * g + 0.9505321522496607 * b
        };
        if (alpha !== void 0) {
            res.alpha = alpha;
        }
        return res;
    };
    var convertRgbToXyz65_default = convertRgbToXyz65;

    /** Convert a colour in RGB format to LAB65.
     */
    var convertRgbToLab65 = (rgb4) => {
        let res = convertXyz65ToLab65_default(convertRgbToXyz65_default(rgb4));
        if (rgb4.r === rgb4.b && rgb4.b === rgb4.g) {
            res.a = res.b = 0;
        }
        return res;
    };
    var convertRgbToLab65_default = convertRgbToLab65;


    /** Convert a colour in lRGB format to Oklab.
     */
    var convertLrgbToOklab = ({ r: r2, g, b, alpha }) => {
        let L = Math.cbrt(
            0.41222147079999993 * r2 + 0.5363325363 * g + 0.0514459929 * b
        );
        let M2 = Math.cbrt(
            0.2119034981999999 * r2 + 0.6806995450999999 * g + 0.1073969566 * b
        );
        let S = Math.cbrt(
            0.08830246189999998 * r2 + 0.2817188376 * g + 0.6299787005000002 * b
        );
        let res = {
            mode: "oklab",
            l: 0.2104542553 * L + 0.793617785 * M2 - 0.0040720468 * S,
            a: 1.9779984951 * L - 2.428592205 * M2 + 0.4505937099 * S,
            b: 0.0259040371 * L + 0.7827717662 * M2 - 0.808675766 * S
        };
        if (alpha !== void 0) {
            res.alpha = alpha;
        }
        return res;
    };
    var convertLrgbToOklab_default = convertLrgbToOklab;

    var rgb_lrgb_fn = (c4) => {
        const abs3 = Math.abs(c4);
        if (abs3 <= 0.04045) {
            return c4 / 12.92;
        }
        return (Math.sign(c4) || 1) * Math.pow((abs3 + 0.055) / 1.055, 2.4);
    };

    /** Convert a colour in RGB format to lRGB.
     */
    var convertRgbToLrgb = ({ r: r2, g, b, alpha }) => {
        let res = {
            mode: "lrgb",
            r: rgb_lrgb_fn(r2),
            g: rgb_lrgb_fn(g),
            b: rgb_lrgb_fn(b)
        };
        if (alpha !== void 0)
            res.alpha = alpha;
        return res;
    };
    var convertRgbToLrgb_default = convertRgbToLrgb;

    /** Convert a colour in RGB format to Oklab.
     */
    var convertRgbToOklab = (rgb4) => {
        let res = convertLrgbToOklab_default(convertRgbToLrgb_default(rgb4));
        if (rgb4.r === rgb4.b && rgb4.b === rgb4.g) {
            res.a = res.b = 0;
        }
        return res;
    };
    var convertRgbToOklab_default = convertRgbToOklab;

    /** Convert a colour in Oklab format to Lrgb.
     */
    var convertOklabToLrgb = ({ l, a, b, alpha }) => {
        let L = Math.pow(
            l * 0.9999999984505198 + 0.39633779217376786 * a + 0.2158037580607588 * b,
            3
        );
        let M2 = Math.pow(
            l * 1.0000000088817609 - 0.10556134232365635 * a - 0.06385417477170591 * b,
            3
        );
        let S = Math.pow(
            l * 1.0000000546724108 - 0.08948418209496575 * a - 1.2914855378640917 * b,
            3
        );
        let res = {
            mode: "lrgb",
            r: 4.076741661347994 * L - 3.307711590408193 * M2 + 0.230969928729428 * S,
            g: -1.2684380040921763 * L + 2.6097574006633715 * M2 - 0.3413193963102197 * S,
            b: -0.004196086541837188 * L - 0.7034186144594493 * M2 + 1.7076147009309444 * S
        };
        if (alpha !== void 0) {
            res.alpha = alpha;
        }
        return res;
    };

    var convertLrgbToRgb = ({ r, g, b, alpha }, mode = 'rgb') => {
        const fn = (c = 0) => {
            const abs = Math.abs(c);
            if (abs > 0.0031308) {
                return (Math.sign(c) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
            }
            return c * 12.92;
        };

        let res = {
            mode,
            r: fn(r),
            g: fn(g),
            b: fn(b)
        };
        if (alpha !== undefined) res.alpha = alpha;
        return res;
    };

    /** Convert a colour in Oklab format to Oklch.
     */
    var convertLabToLch = ({ l, a, b, alpha }, mode = "lch") => {
        let c4 = Math.sqrt(a * a + b * b);
        let res = { mode, l, c: c4 };
        if (c4)
            res.h = normalizeHue_default(Math.atan2(b, a) * 180 / Math.PI);
        if (alpha !== void 0)
            res.alpha = alpha;
        return res;
    };

    var normalizeHue = (hue3) => (hue3 = hue3 % 360) < 0 ? hue3 + 360 : hue3;
    var normalizeHue_default = normalizeHue;

    /** Convert a colour in Oklch format to Oklab.
     */
    function convertLchToLab({ l, c: c4, h, alpha }, mode = "lab") {
        let res = {
            mode,
            l,
            a: c4 ? c4 * Math.cos(h / 180 * Math.PI) : 0,
            b: c4 ? c4 * Math.sin(h / 180 * Math.PI) : 0
        };
        if (alpha !== void 0)
            res.alpha = alpha;
        return res;
    };

    /** Convert a colour in RGB format to Oklch.
     */
    function convertRgbToOklch(col) {
        return convertLabToLch(convertRgbToOklab(col));
    }

    /** Convert a colour in Oklch format to RGB.
     */
    function convertOklchToRgb(col) {
        return convertLabToRgb(convertLchToLab(col));
    }

    /** Linear interpolation between `a` and `b`.
     *
     * @param {number} a
     * @param {number} b
     * @param {number} t
     * @returns {number}
     */
    function lerp(a, b, t) {
        if(a===undefined) {
            return b;
        }
        if(b===undefined) {
            return a;
        }
        return (1-t)*a + t*b;
    }

    /** By Christian Lawson-Perfect
    */

    /** Parse a hex string to an RGB colour object.
     *
     * @param {string} hex
     * @returns {Color.rgb}
     */
    function parseRGB(hex) {
        var r = parseInt(hex.slice(1,3),16);
        var g = parseInt(hex.slice(3,5),16);
        var b = parseInt(hex.slice(5,7),16);
        return {r: r/255, g: g/255, b: b/255, mode: 'rgb'} ;
    }

    /** Mix two colours in Oklch colour space.
     *
     * @param {Color.oklab} oklab1
     * @param {Color.oklab} oklab2
     * @param {number} t
     *
     * @returns {Color}
     */
    function mix(oklab1, oklab2, t) {
        const {l: l1, c: c1, h: h1} = oklab1;
        const {l: l2, c: c2, h: h2} = oklab2;

        return {l: lerp(l1,l2,t), c: lerp(c1,c2,t), h: c2==0 ? h1 : lerp(h1,h2,t), mode: 'oklch'};
    }

    /*
      Delta Phi Star perceptual lightness contrast by Andrew Somers:
      https://github.com/Myndex/deltaphistar 
    */
    const PHI = (1 + Math.sqrt(5))/2;

    /** Perceptual contrast between two colours.
     *
     * @param {Color.rgb} a
     * @param {Color.rgb} b
     * @returns {number}
     */
    function dpsContrast(a, b) {
      const dps = Math.abs(Math.pow(convertRgbToLab65(a).l, PHI) - Math.pow(convertRgbToLab65(b).l, PHI));
      const contrast = Math.pow(dps, 1/PHI) * Math.SQRT2 - 40;
      return contrast < 7.5 ? 0 : contrast;
    }

    /** Is the given colour "dark"? True if the contrast against white is more than the contrast against black.
     *
     * @param {Color.rgb} col
     * @returns {boolean}
     */
    function is_dark(col) {
        const black_contrast = dpsContrast(col, {r:0, g:0, b:0, mode: 'rgb'});
        const white_contrast = dpsContrast(col, {r:1, g:1, b:1, mode: 'rgb'});
        return black_contrast < white_contrast;
    }

    /** Format an RGB color as a hex code.
     *
     * @param {Color.rgb} rgb
     * @returns {string}
     */
    function rgbToHex({r, g, b}) {
        function f(n) {
            return Math.floor(Math.max(0,Math.min(1,n))*255).toString(16).padStart(2,'0');
        }
        return `#${f(r)}${f(g)}${f(b)}`;
    }

    /** Which text colour should be used for a given background?
     *
     * @param {Color.rgb} col
     * @returns {string} `"black"` or `"white"`.
     */
    function text_for(col, target_contrast) {
        const col_oklab = convertRgbToOklab(col);

        target_contrast = target_contrast === undefined ? 1 : target_contrast;

        function clamp(n) {
            return Math.max(0,Math.min(1,n));
        }

        let best;
        let mindist = Infinity;

        for(let l=0; l<=1; l += 0.01) {
            const oklab = {mode:'lab65', l, a: col_oklab.a, b: col_oklab.b};
            const {r,g,b} = convertLrgbToRgb(convertOklabToLrgb(oklab));
            const rgb = {r: clamp(r), g: clamp(g), b: clamp(b)};
            const contrast = dpsContrast(col, rgb);
            const d = Math.abs(contrast - target_contrast);
            if(d < mindist) {
                mindist = d;
                best = rgb;
            }
        }

        if(!best) {
            return 'black';
        }

        return rgbToHex(best);
    }

    Numbas.display_color = {
        dpsContrast,
        is_dark,
        text_for,
        parseRGB,
        convertRgbToLab65,
        convertOklabToLrgb,
        convertRgbToOklab,
        convertLrgbToRgb,
        rgbToHex
    }
});
;
Numbas.queueScript('display-util', ['math'], function() {
    var measurer;
    var measureText_cache = {};

    /**
     * Measure the size of the text in the given input element.
     * Measurements are cached by text value and relevant styles.
     *
     * @param {Element} element
     * @returns {DOMRect}
     */
    function measureText(element) {
        var styles = window.getComputedStyle(element);

        if(!measurer) {
            measurer = document.createElement('div');
            measurer.style['position'] = 'absolute';
            measurer.style['left'] = '-10000';
            measurer.style['top'] = '-10000';
            measurer.style['visibility'] = 'hidden';
        }

        var keys = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing'];
        var id = element.value + ';' + keys.map(function(key) {
            return styles[key];
        }).join(';');
        if(measureText_cache[id]) {
            return measureText_cache[id];
        }
        keys.forEach(function(key) {
            measurer.style[key] = styles[key];
        });
        measurer.textContent = element.value;
        document.body.appendChild(measurer);
        var box = measurer.getBoundingClientRect();
        measureText_cache[id] = box;
        document.body.removeChild(measurer);
        return box;
    }

    /** Resolve a feedback setting, returning a boolean representing whether the feedback should currently be shown.
     *
     * @param {Numbas.display_util.feedbackable} obj
     * @param {string} setting - One of `["always", "oncompletion", "inreview", "never"]`.
     * @returns {boolean}
     */
    function resolve_feedback_setting(obj, setting) {
        return Knockout.pureComputed(function() {
            if(Numbas.is_instructor) {
                return true;
            }

            switch(setting) {
                case 'always':
                    return true;
                case 'oncompletion':
                    return obj.ended();
                case 'inreview':
                    return obj.revealed();
                case 'never':
                    return false;
            }
        });
    }

    /** An object which can produce feedback: {@link Numbas.Question} or {@link Numbas.parts.Part}.
     *
     * @typedef {object} Numbas.display_util.feedbackable
     * @property {observable.<boolean>} answered - Has the object been answered?
     * @property {observable.<boolean>} isDirty - Has the student's answer changed?
     * @property {observable.<number>} score - Number of marks awarded
     * @property {observable.<number>} marks - Number of marks available
     * @property {observable.<number>} credit - Proportion of available marks awarded
     * @property {observable.<boolean>} doesMarking - Does the object do any marking?
     * @property {observable.<boolean>} revealed - Have the correct answers been revealed?
     * @property {observable.<boolean>} ended - Has the exam ended?
     */
    /** Settings for {@link Numbas.display_util.showScoreFeedback}
     *
     * @typedef {object} Numbas.display_util.showScoreFeedback_settings
     * @property {string} showTotalMark - When to show the total marks available.
     * @property {string} showActualMark - When to show the student's current score.
     * @property {string} showAnswerState - When to show the correct/incorrect state after marking.
     * @property {boolean} [reveal_answers_for_instructor=true] - When `Numbas.is_instructor` is true, always act as if the object has been revealed?
     */
    /** Feedback states for a question or part: "wrong", "correct", "partial" or "none".
     *
     * @typedef {string} Numbas.display_util.feedback_state
     */
    /** A model representing feedback on an item which is marked - a question or a part.
     *
     * @typedef {object} Numbas.display_util.scoreFeedback
     * @property {observable.<boolean>} update - Call `update(true)` when the score changes. Used to trigger animations.
     * @property {observable.<boolean>} revealed - Have the correct answers been revealed?
     * @property {observable.<Numbas.display_util.feedback_state>} state - The current state of the item, to be shown to the student.
     * @property {observable.<boolean>} showActualMark - Should the current score be shown?
     * @property {observable.<boolean>} showTotalMark - Should the total available marks be shown?
     * @property {observable.<boolean>} answered - Has the item been answered? False if the student has changed their answer since submitting.
     * @property {observable.<string>} answeredString - Translated text describing how much of the item has been answered: 'unanswered', 'partially answered' or 'answered'
     * @property {observable.<string>} attemptedString - Translated text describing whether the item has been answered.
     * @property {observable.<string>} message - Text summarising the state of the item.
     * @property {observable.<string>} plainMessage - Plain text summarising the state of the item.
     * @property {observable.<string>} iconClass - CSS class for the feedback icon.
     * @property {observable.<object>} iconAttr - A dictionary of attributes for the feedback icon.
     */

    /** Update a score feedback box.
     *
     * @param {Numbas.display_util.feedbackable} obj - Object to show feedback about.
     * @param {Numbas.display_util.showScoreFeedback_settings} settings
     * @memberof Numbas.display
     * @returns {Numbas.display_util.scoreFeedback}
     */
    function showScoreFeedback(obj, settings) {
        var niceNumber = Numbas.math.niceNumber;
        var newScore = Knockout.observable(false);
        var answered = Knockout.computed(function() {
            return obj.answered && obj.answered();
        });
        var attempted = Knockout.computed(function() {
            return obj.visited !== undefined && obj.visited();
        });

        var showActualMark = resolve_feedback_setting(obj, settings.showActualMark);
        var showTotalMark = resolve_feedback_setting(obj, settings.showTotalMark);
        var showAnswerState = resolve_feedback_setting(obj, settings.showAnswerState);

        var revealed = Knockout.computed(function() {
            return (obj.revealed() && showActualMark()) || (Numbas.is_instructor && settings.reveal_answers_for_instructor !== false);
        });

        var showFeedbackIcon = settings.showFeedbackIcon === undefined ? showAnswerState() : settings.showFeedbackIcon;

        var anyAnswered = Knockout.computed(function() {
            if(obj.anyAnswered === undefined) {
                return answered();
            } else {
                return obj.anyAnswered();
            }
        });
        var partiallyAnswered = Knockout.computed(function() {
            return anyAnswered() && !answered();
        }, this);
        var state = Knockout.computed(function() {
            var credit = obj.credit();
            if(obj.doesMarking() && showFeedbackIcon && (revealed() || (showAnswerState() && anyAnswered()))) {
                if(credit <= 0) {
                    return 'wrong';
                } else if(Numbas.math.precround(credit, 10) >= 1) {
                    return 'correct';
                } else {
                    return 'partial';
                }
            } else {
                return 'none';
            }
        });
        var messageIngredients = Knockout.computed(function() {
            var score = obj.score();
            var marks = obj.marks();
            var scoreobj = {
                marks: marks,
                score: score,
                marksString: niceNumber(marks) + ' ' + R('mark', {count:parseFloat(marks)}),
                scoreString: niceNumber(score) + ' ' + R('mark', {count:parseFloat(score)}),
            };
            var messageKey;

            if(marks == 0) {
                messageKey = 'question.score feedback.not marked';
            } else {
                if(showActualMark()) {
                    if(showTotalMark()) {
                        messageKey = 'question.score feedback.score total actual';
                    } else {
                        messageKey = 'question.score feedback.score actual';
                    }
                } else if(showTotalMark()) {
                    messageKey = 'question.score feedback.score total';
                } else {
                    var key = answered() ? 'answered' : anyAnswered() ? 'partially answered' : 'unanswered';
                    messageKey = 'question.score feedback.' + key;
                }
            }
            return {key: messageKey, scoreobj: scoreobj};
        });
        return {
            update: Knockout.computed({
                read: function() {
                    return newScore();
                },
                write: function() {
                    newScore(true);
                    newScore(false);
                }
            }),
            revealed: revealed,
            state: state,
            showActualMark: showActualMark,
            showTotalMark: showTotalMark,
            showAnswerState: showAnswerState,
            answered: answered,
            answeredString: Knockout.computed(function() {
                if(obj.marks() == 0 && !obj.doesMarking()) {
                    return '';
                }
                var key = answered() ? 'answered' : partiallyAnswered() ? 'partially answered' : 'unanswered';
                return R('question.score feedback.' + key);
            }, this),
            attemptedString: Knockout.computed(function() {
                var key = attempted() ? 'attempted' : 'unattempted';
                return R('question.score feedback.' + key);
            }, this),
            message: Knockout.computed(function() {
                var ingredients = messageIngredients();
                return R(ingredients.key, ingredients.scoreobj);
            }),
            plainMessage: Knockout.computed(function() {
                var ingredients = messageIngredients();
                var key = ingredients.key;
                if(key == 'question.score feedback.score total actual' || key == 'question.score feedback.score actual') {
                    key += '.plain';
                }
                return R(key, ingredients.scoreobj);
            }),
            iconClass: Knockout.computed(function() {
                if (!showFeedbackIcon) {
                    return 'invisible';
                }
                switch(state()) {
                case 'wrong':
                    return 'icon-remove';
                case 'correct':
                    return 'icon-ok';
                case 'partial':
                    return 'icon-ok partial';
                default:
                    return 'invisible';
                }
            }),
            iconAttr: Knockout.computed(function() {
                return {title:state() == 'none' ? '' : R('question.score feedback.' + state())};
            })
        }
    };

    /** An object that handles validating a password.
     *
     * @param {{settings: Function, correct_message: string, incorrect_message: string}} settings
     * @returns {{value: observable.<string>, valid: observable.<boolean>, feedback: observable.<object>}}
     */
    function passwordHandler(settings) {
        var value = Knockout.observable('');

        var valid = Knockout.computed(function() {
            return settings.accept(value());
        });

        return {
            value: value,
            valid: valid,
            feedback: Knockout.computed(() => {
                if(valid()) {
                    return {iconClass: 'icon-ok', title: settings.correct_message, buttonClass: 'success'};
                } else if(value() == '') {
                    return {iconClass: '', title: '', buttonClass: 'primary'}
                } else {
                    return {iconClass: 'icon-remove', title: settings.incorrect_message, buttonClass: 'danger'};
                }
            })
        };
    }

    /** Localise strings in page HTML - for tags with an attribute `data-localise`, run that attribute through R.js to localise it, and replace the tag's HTML with the result.
     *
     * @param {Element} root
     */
    function localisePage(root) {
        for(const e of root.querySelectorAll('[data-localise]')) {
            const localString = R(e.getAttribute('data-localise'));
            e.innerHTML = localString;
        }
        for(const e of root.querySelectorAll('[localise-aria-label]')) {
            const localString = R(e.getAttribute('localise-aria-label'));
            e.setAttribute('aria-label', localString);
        }
    }

    /** Get the attribute with the given name or, if it doesn't exist, look for localise-<name>.
     * If that exists, localise its value and set the desired attribute, then return it.
     *
     * @param {Element} elem
     * @param {string} name
     * @returns {string}
     */
    function getLocalisedAttribute(elem, name) {
        var attr_localise;
        var attr = elem.getAttribute(name);
        if(!attr && (attr_localise = elem.getAttribute('localise-' + name))) {
            attr = R(attr_localise);
            elem.setAttribute(name, attr);
        }
        return attr;
    }

    /**
     * Move focus to this element.
     * An element which isn't in the tabbing order can't receive focus, but it can be helpful to move the caret position to the top of a container element.
     * This function temporarily sets tabIndex on the given element, focuses it, then restores the original tabIndex and blurs the element.
     *
     * @param {Element} element
     */
    function force_focus(element) {
        if(!Numbas.display.control_focus) {
            return;
        }

        const ot = element.tabIndex;
        element.tabIndex = 0;
        element.focus();
        element.blur();
        element.tabIndex = ot;
    }

    /**
     * A Knockout observable representing a duration of time.
     * The main observable is a number representing the duration in seconds.
     * Computed attributes `display` and `machine` are string renderings of the duration for display or machine-reading.
     *
     * @param {number} initial
     * @returns {observable.<number>}
     */
    function duration_observable(initial) {
        const obs = Knockout.observable(initial);
        obs.display = Knockout.computed(function() {
            return Numbas.timing.secsToDisplayTime(Math.max(0, obs()));
        });
        obs.machine = Knockout.computed(function() {
            return Numbas.timing.secsToMachineDuration(Math.max(0, obs()));
        });
        return obs;
    }

    /**
     * Find the JME scope that applies to this element.
     * Looks for an element with a `jme_scope` attribute.
     *
     * @param {Element} element
     * @returns {Numbas.jme.Scope}
     */
    function find_jme_scope(element) {
        while(element) {
            if(element.jme_scope !== undefined) {
                return element.jme_scope;
            }
            element = element.parentElement;
        }
    }

    /** Associate a JME scope with the given element.
     *
     * @param {Element} element
     * @param {Numbas.jme.Scope} scope
     */
    function set_jme_scope(element, scope) {
        element.jme_scope = scope;
    }


    /**
     * Find the element's top ancestor node. For elements in the document, this will be the document object itself.
     *
     * @param {Element} element
     * @returns {Node}
     */
    function find_root_ancestor(element) {
        while(element.parentNode) {
            element = element.parentNode;
        }
        return element;
    }

    Numbas.display_util = {
        measureText,
        showScoreFeedback,
        passwordHandler,
        localisePage,
        getLocalisedAttribute,
        resolve_feedback_setting,
        force_focus,
        duration_observable,
        find_jme_scope,
        set_jme_scope,
        find_root_ancestor,
    };
});
;
/*
Copyright 2011-16 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Display code. Provides {@link Numbas.display} */
Numbas.queueScript('display', ['display-base', 'knockout-handlers', 'exam-display', 'question-display', 'part-display', 'display/parts/extension', 'display/parts/gapfill', 'display/parts/information', 'display/parts/jme', 'display/parts/matrix', 'display/parts/multipleresponse', 'display/parts/numberentry', 'display/parts/patternmatch'], function() {
});
;
/*
Copyright 2022-2023 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Functions related to creating files to download and encrypting them. */
Numbas.queueScript('download', ['jme'], function() {

/** @namespace Numbas.download */
var download = Numbas.download = /** @lends Numbas.download */ {

    /**
     * Dynamically creates and enacts a download link for a provided file.
     * This is necessary if the contents of the file can change after the button is loaded but before it is clicked.
     *
     * @param {string} contents
     * @param {string} filename - The name of the downloaded file.
     * @param {string} mime_type - The MIME type of the file.
     */
    download_file: function(contents, filename, mime_type) {
        //pulled from https://stackoverflow.com/questions/8310657/how-to-create-a-dynamic-file-link-for-download-in-javascript
        mime_type = mime_type || 'text/plain';
        var blob = new Blob([contents], { type: mime_type });
        var dlink = document.createElement('a');
        document.body.appendChild(dlink); //may be necessary for firefox/some browsers
        dlink.download = filename;
        dlink.href = window.URL.createObjectURL(blob);
        dlink.onclick = function(e) {
            var that = this;
            setTimeout(function() {
                window.URL.revokeObjectURL(that.href);
            }, 1500);
        };

        dlink.click()
        dlink.remove()
    },

    /*
    Given some key material and some random salt
    derive an AES-GCM key using PBKDF2.
    */
    getEncryptionKey: async function(password, salt) {
        const enc = new TextEncoder();
        const keyMaterial = await window.crypto.subtle.importKey(
            "raw",
            enc.encode(password),
            { name: "PBKDF2" },
            false,
            ["deriveBits", "deriveKey"]
        );
        return await window.crypto.subtle.deriveKey(
            {
                "name": "PBKDF2",
                salt: salt,
                "iterations": 100000,
                "hash": "SHA-256"
            },
            keyMaterial,
            { "name": "AES-GCM", "length": 256 },
            true,
            ["encrypt", "decrypt"]
        );
    },

    /**
     * Derive a key from a password supplied by the user, and use the key to encrypt the message.
     * Update the "ciphertextValue" box with a representation of part of the ciphertext.
     *
     * @param {string} message
     * @param {string} password
     * @returns {string}
     */
    encrypt: async function(message, password) {
        const salt = new Uint8Array(16);
        const key = await download.getEncryptionKey(password, salt);
        const iv = new Uint8Array(12);
        const enc = new TextEncoder();
        const encoded = enc.encode(message);

        const ciphertext = await window.crypto.subtle.encrypt(
            {
                name: "AES-GCM",
                iv: iv
            },
            key,
            encoded
        );
        return ciphertext;
    },

    /*
    Derive a key from a password supplied by the user, and use the key
    to decrypt the ciphertext.
    If the ciphertext was decrypted successfully,
    update the "decryptedValue" box with the decrypted value.
    If there was an error decrypting,
    update the "decryptedValue" box with an error message.
    */
    decrypt: async function(ciphertext, password) {
        const salt = new Uint8Array(16);
        const iv = new Uint8Array(12);
        const key = await download.getEncryptionKey(password, salt);

        const decrypted = await window.crypto.subtle.decrypt(
            {
                name: "AES-GCM",
                iv: iv
            },
            key,
            ciphertext
        );

        const dec = new TextDecoder();
        return dec.decode(decrypted);

    },

}
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.CustomPart} constructor. */
Numbas.queueScript('evaluate-settings', ['base', 'jme', 'jme-variables', 'util'], function() {
    var jme = Numbas.jme;

    Numbas.evaluate_settings = {};

    var setting_evaluators = Numbas.evaluate_settings.setting_evaluators = {
        'string': function(def, value, scope) {
            if(def.subvars) {
                value = jme.subvars(value, scope, true);
            }
            return new jme.types.TString(value);
        },
        'mathematical_expression': function(def, value, scope) {
            if(!value.trim()) {
                throw(new Numbas.Error("part.custom.empty setting"));
            }
            if(def.subvars) {
                value = jme.subvars(value, scope);
            }
            var result = new jme.types.TExpression(value);
            return result;
        },
        'checkbox': function(def, value) {
            return new jme.types.TBool(value);
        },
        'dropdown': function(def, value) {
            return new jme.types.TString(value);
        },
        'code': function(def, value, scope) {
            if(def.evaluate) {
                if(!value.trim()) {
                    throw(new Numbas.Error('part.custom.empty setting'));
                }
                return scope.evaluate(value);
            } else {
                return new jme.types.TString(value);
            }
        },
        'percent': function(def, value) {
            return new jme.types.TNum(value / 100);
        },
        'html': function(def, value, scope) {
            if(def.subvars) {
                value = jme.contentsubvars(value, scope);
            }
            return new jme.types.TString(value);
        },
        'list_of_strings': function(def, value, scope) {
            return new jme.types.TList(value.map(function(s) {
                if(def.subvars) {
                    s = jme.subvars(s, scope);
                }
                return new jme.types.TString(s)
            }));
        },
        'choose_several': function(def, value) {
            return new jme.wrapValue(value);
        }
    };


    Numbas.evaluate_settings.evaluate_settings = function(definition, raw_settings, scope) {
        var settings = {};
        definition.settings.forEach(function(s) {
            var name = s.name;
            var value = raw_settings[name];
            if(value === undefined) {
                value = s.default_value;
            }
            if(!setting_evaluators[s.input_type]) {
                throw(new Numbas.Error('part.custom.unrecognised input type', {input_type:s.input_type}));
            }
            try {
                settings[name] = setting_evaluators[s.input_type](s, value, scope);
            } catch(e) {
                throw(new Numbas.Error('part.custom.error evaluating setting', {setting: name, error: e.message}, e));
            }
        });
        return settings;
    }
});
;
Numbas.queueScript('exam-display', ['display-util', 'display-base', 'math', 'util', 'timing'], function() {
    var display = Numbas.display;
    var util = Numbas.util;
    /** Display properties of the {@link Numbas.Exam} object.
     *
     * @name ExamDisplay
     * @memberof Numbas.display
     * @class
     * @param {Numbas.Exam} e - associated exam
     * @param {Element} root_element
     *
     */
    display.ExamDisplay = function(e, root_element) {
        this.exam = e;

        this.root_element = root_element;
        root_element.exam = this.exam;

        /** The exam's mode.
         *
         * @see Numbas.Exam#mode
         * @member {observable|string} mode
         * @memberof Numbas.display.ExamDisplay
         */
        this.mode = Knockout.observable(e.mode);

        /** Has the exam ended?
         *
         * @member {observable|boolean} ended
         * @memberof Numbas.display.ExamDisplay
         */
        this.ended = Knockout.observable(false);

        /** Have the correct answers been revealed?
         *
         * @see Numbas.Exam#revealed
         * @member {observable|boolean} revealed
         * @memberof Numbas.display.ExamDisplay
         */
        this.revealed = Knockout.observable(e.revealed);

        /** Should expected answers to parts be shown?
         */
        this.expectedAnswersRevealed = Knockout.pureComputed(function() {
            if(!this.revealed()) {
                return false;
            }
            return this.exam.settings.revealExpectedAnswers == 'inreview';
        }, this);

        /** Is {@link Numbas.store} currently saving?
         *
         * @member {observable|boolean} saving
         * @memberof Numbas.display.ExamDisplay
         */
        this.saving = Knockout.observable(false);
        /** The name of the currently displayed info page.
         *
         * @member {observable|string} infoPage
         * @memberof Numbas.display.ExamDisplay
         */
        this.infoPage = Knockout.observable(null);
        /** The current question.
         *
         * @see Numbas.Exam#currentQuestion
         * @member {observable|Numbas.display.QuestionDisplay} currentQuestion
         * @memberof Numbas.display.ExamDisplay
         */
        this.currentQuestion = Knockout.observable(null);
        /** What kind of view are we in at the moment? 'infopage' or 'question'.
         *
         * @member {observable|string} viewType
         * @memberof Numbas.display.ExamDisplay
         */
        this.viewType = Knockout.computed(function() {
            if(this.infoPage()) {
                return 'infopage';
            } else if(this.currentQuestion()) {
                return 'question';
            }
        }, this);

        /** The ID of the header to use for the label of the <main> tag.
         *
         * @member {observable|string} main_labelledby
         * @memberof Numbas.display.ExamDisplay
         */
        this.main_labelledby = Knockout.computed(function() {
            if(this.infoPage()) {
                return `infopage-${this.infoPage()}-header`;
            } else if(this.currentQuestion()) {
                return `${this.currentQuestion().question.path}-header`;
            }
        }, this);

        /** The number of the current question.
         *
         * @member {observable|number} currentQuestionNumber
         * @memberof Numbas.display.ExamDisplay
         */
        this.currentQuestionNumber = Knockout.computed(function() {
            var q = this.currentQuestion();
            if(q) {
                return q.question.number;
            } else {
                return null;
            }
        }, this);
        /** All the exam's question display objects.
         *
         * @member {observable|Numbas.display.QuestionDisplay[]} questions
         * @memberof Numbas.display.ExamDisplay
         */
        this.questions = Knockout.observableArray([]);

        /** The number of questions in the exam.
         *
         * @member {observable|number} numQuestions
         * @memberof Numbas.display.ExamDisplay
         */
        this.numQuestions = Knockout.pureComputed(function() {
            return this.questions().length;
        }, this);

        /** How many questions do some assessment, i.e. have one or more parts that aren't information-only, or are explore mode?
         */
        this.numAssessedQuestions = Knockout.computed(function() {
            return this.questions().filter(function(qd) {
                return qd.question.partsMode == 'explore' || qd.question.parts.some(function(p) {
                    return p.type != 'information';
                });
            }).length;
        }, this);
        /** Can the student go back to the previous question, or to the introduction? False if the current question is the first one and there's no introduction.
         *
         * @member {observable|boolean} canReverse
         * @memberof Numbas.display.ExamDisplay
         */
        this.canReverse = Knockout.computed(function() {
            return (this.mode() == 'review' || this.exam.settings.navigateReverse) && this.currentQuestionNumber() > 0 || (this.viewType() == 'question' && this.exam.hasIntro);
        }, this);
        /** Can the student go forward to the next question? False if the current question is the last one.
         *
         * @member {observable|boolean} canAdvance
         * @memberof Numbas.display.ExamDisplay
         */
        this.canAdvance = Knockout.computed(function() {
            switch(this.exam.settings.navigateMode) {
                case 'diagnostic':
                    return true;
                default:
                    return this.currentQuestionNumber() < this.exam.settings.numQuestions - 1 || (this.viewType() == 'infopage' && this.infoPage() == 'introduction');
            }
        }, this);

        /** Move backwards, to the previous question or to the introduction if already on the first question.
         *
         * @member {observable|boolean} reverse
         * @memberof Numbas.display.ExamDisplay
         */
        this.reverse = () => {
            if(this.viewType() == 'question') {
                if(this.currentQuestionNumber() == 0) {
                    this.showInfoPage('introduction');
                } else {
                    Numbas.controls.previousQuestion(this.exam);
                }
            }
        };

        /** Move forwards, to the next question or just switch back to viewing questions if viewing an info page.
         *
         * @member {observable|boolean} advance
         * @memberof Numbas.display.ExamDisplay
         */
        this.advance = () => {
            if(this.viewType() == 'question') {
                Numbas.controls.nextQuestion(this.exam);
            } else {
                this.exam.resumeExam();
            }
        };

        /** The student's total score.
         *
         * @see Numbas.Exam#score
         * @member {observable|number} score
         * @memberof Numbas.display.ExamDisplay
         */
        this.score = Knockout.observable(e.score);

        /** The total marks available for the exam.
         *
         * @see Numbas.Exam#mark
         * @member {observable|number} marks
         * @memberof Numbas.display.ExamDisplay
         */
        this.marks = Knockout.observable(e.mark);

        /** Score feedback for the whole exam.
         *
         * @member {Numbas.display_util.scoreFeedback} scoreFeedback
         * @memberof Numbas.display.ExamDisplay
         */
        this.scoreFeedback = Numbas.display_util.showScoreFeedback({
            answered: function() {
                return false;
            },
            isDirty: function() {
                return false;
            },
            score: this.score,
            marks: this.marks,
            credit: Knockout.computed(function() {
                    var score = this.score();
                    var marks = this.marks();
                    return marks == 0 ? 0 : score / marks;
                }, this),
            doesMarking: function() {
                return true;
            },
            revealed: this.revealed,
            ended: this.ended
        }, this.exam.settings);

        /** Show the student their total score?
         *
         * @member {observable|boolean} showActualMark
         * @memberof Numbas.display.ExamDisplay
         */
        this.showActualMark = this.scoreFeedback.showActualMark;

        /** Allow the student to print an exam transcript?
         *
         * @see Numbas.Exam#settings#percentPass
         * @member {observable|boolean} allowPrinting
         * @memberof Numbas.display.ExamDisplay
         */
        this.allowPrinting = Knockout.observable(e.settings.allowPrinting);

        /** Key to use for encrypting student data
         *
         * @member {observable|string} downloadEncryptionKey
         * @memberof Numbas.display.ExamDisplay
         */
        this.downloadEncryptionKey = Knockout.observable(e.settings.downloadEncryptionKey);
        /** Label to use for the "print your transcript" button on the results page.
         *
         * @member {observable|string} printLabel
         * @memberof Numbas.display.ExamDisplay
         */
        this.printLabel = Knockout.computed(function() {
            return R(this.showActualMark() || !this.allowPrinting() ? "result.print" : "end.print");
        }, this);
        /** The percentage score the student needs to achieve to pass, formatted as a string.
         *
         * @see Numbas.Exam#settings#percentPass
         * @member {observable|string} percentPass
         * @memberof Numbas.display.ExamDisplay
         */
        this.percentPass = Knockout.observable(Numbas.math.niceNumber(e.settings.percentPass * 100) + '%');
        /** String displaying the student's current score, and the total marks available, if allowed.
         *
         * @member {observable|string} examScoreDisplay
         * @memberof Numbas.display.ExamDisplay
         */
        this.examScoreDisplay = Knockout.computed(function() {
            var niceNumber = Numbas.math.niceNumber;
            var score = this.score();
            var marks = this.marks();
            var totalExamScoreDisplay = '';
            if(this.scoreFeedback.showTotalMark()) {
                totalExamScoreDisplay = niceNumber(score) + '/' + niceNumber(marks);
            } else {
                totalExamScoreDisplay = niceNumber(score);
            }
            return totalExamScoreDisplay;
        }, this);
        /** The student's total score as a percentage of the total marks available.
         *
         * @member {observable|number} percentScore
         * @memberof Numbas.display.ExamDisplay
         */
        this.percentScore = Knockout.observable(0);

        /** The header for the column on the results page that might show the score, the marks available, or just the answered state.
         *
         * @member {observable|string} resultsScoreColumnHeader
         * @memberof Numbas.display.ExamDisplay
         */
        this.resultsScoreColumnHeader = Knockout.computed(function() {
            if(this.scoreFeedback.showActualMark()) {
                return R('result.question score');
            } else if(this.scoreFeedback.showTotalMark()) {
                return R('result.question marks available');
            } else {
                return R('result.question answered');
            }
        }, this);


        /** The time left in the exam, in seconds.
         *
         * @member {observable|number} timeRemaining
         * @memberof Numbas.display.ExamDisplay
         */
        this.timeRemaining = Numbas.display_util.duration_observable(0);

        /** Show the time remaining?
         *
         * @member {observable|boolean} showTime
         * @memberof Numbas.display.ExamDisplay
         */
        this.showTime = Knockout.observable(true);

        /** Show the names of question groups in the menu?
         *
         * @member {observable|string} showQuestionGroupNames
         * @memberof Numbas.display.ExamDisplay
         */
        this.showQuestionGroupNames = Knockout.observable(e.settings.showQuestionGroupNames);

        /** Time the exam started, formatted for display.
         *
         * @member {observable|Date} startTime
         * @memberof Numbas.display.ExamDisplay
         */
        this.startTime = Knockout.observable();

        /** Time the exam ended, formatted for display.
         *
         * @member {observable|Date} endTime
         * @memberof Numbas.display.ExamDisplay
         */
        this.endTime = Knockout.observable();

        /** The time allowed for the exam, in seconds.
         *
         * @member {observable|number} duration
         * @memberof Numbas.display.ExamDisplay
         */
        this.duration = Numbas.display_util.duration_observable(e.settings.duration);

        /** The total time the student has spent in the exam.
         *
         * @member {observable|string} timeSpent
         * @memberof Numbas.display.ExamDisplay
         */
        this.timeSpent = Numbas.display_util.duration_observable(0);

        /** Is the student allowed to pause the exam?
         *
         * @member {boolean} allowPause
         * @memberof Numbas.display.ExamDisplay
         */
        this.allowPause = e.settings.allowPause;
        /** Total number of questions the student attempted.
         *
         * @member {observable|number} questionsAttempted
         * @memberof Numbas.display.ExamDisplay
         */
        this.questionsAttempted = Knockout.computed(function() {
            return this.questions().filter((q) => q.answered() ? 1 : 0).length;
        }, this);
        /** Total number of questions the student attempted, formatted as a fraction of the total number of questions.
         *
         * @member {observable|string} questionsAttemptedDisplay
         * @memberof Numbas.display.ExamDisplay
         */
        this.questionsAttemptedDisplay = Knockout.computed(function() {
            return R('exam.questions answered', {numAnsweredQuestions: this.questionsAttempted(), numQuestions: this.numAssessedQuestions()});
        }, this);
        /** The result of the exam - passed or failed?
         *
         * @member {observable|string} result
         * @memberof Numbas.display.ExamDisplay
         */
        this.result = Knockout.observable('');
        /** Did the student pass the exam?
         *
         * @member {observable|boolean} passed
         * @memberof Numbas.display.ExamDisplay
         */
        this.passed = Knockout.observable(false);
        /** Message shown to the student based on their total score.
         *
         * @member {observable|string} feedbackMessage
         * @memberof Numbas.display.ExamDisplay
         */
        this.feedbackMessage = Knockout.observable(null);

        /** Does this exam need a password to begin?
         *
         * @member {boolean} needsPassword
         * @memberof Numbas.display.ExamDisplay
         */
        this.needsPassword = e.settings.startPassword != '';

        /** Does this exam allow the student to download their attempt data?
         *
         * @member {boolean} allowAttemptDownload
         * @memberof Numbas.display.ExamDisplay
         */
        this.allowAttemptDownload = e.settings.allowAttemptDownload;

        /** Must the student write their name before the exam can begin?
         *
         * @member {boolean} needsStudentName
         * @memberof Numbas.display.ExamDisplay
         */
        this.needsStudentName = this.allowAttemptDownload;

        /**
         * Show a warning that downloading attempt data won't work?
         * True if the window is not in a secure context.
         *
         * @member {boolean} showAttemptDownloadSecurityWarning
         * @memberof Numbas.display.ExamDisplay
         */
        this.showAttemptDownloadSecurityWarning = this.allowAttemptDownload && !window.isSecureContext;

        /** Key for encrypting student data.
         *
         * @member {string} downloadEncryptionKey
         * @memberof Numbas.display.ExamDisplay
         */
        this.downloadEncryptionKey = e.settings.downloadEncryptionKey;

        /** The student's name, as entered by the student.
         *
         * @member {observable|string} student_name
         * @memberof Numbas.display.ExamDisplay
         */
        this.student_name = Knockout.observable(this.exam.student_name || '');

        /**
         * Handler for the password on the front page.
         */
        this.passwordHandler = Numbas.display_util.passwordHandler({
            accept: (password) => this.exam.acceptPassword(password),
            correct_message: R('exam.password.correct'),
            incorrect_message: R('exam.password.incorrect')
        });

        /** Can the exam begin? True if no password is required, or if the student has entered the right password, and no name is required or the student has entered a name.
         *
         * @member {observable|boolean} canBegin
         * @memberof Numbas.display.ExamDisplay
         */
        this.canBegin = Knockout.computed(function() {
            return this.passwordHandler.valid() && !(this.needsStudentName && this.student_name().trim() == '');
        }, this);

        /** The student's progress through a diagnostic test.
         */
        this.diagnostic_progress = Knockout.observableArray([]);
        this.diagnostic_feedback = Knockout.observable('');
        this.diagnostic_next_actions = Knockout.observable({feedback: '', actions:[]});

        this.current_topic = Knockout.observable(null);

        /** Confirmation text entered by the student to end the exam.
         *
         * @member {observable|string} confirmEnd
         * @memberof Numbas.display.ExamDisplay
         */
        this.confirmEndHandler = Numbas.display_util.passwordHandler({
            accept: (value) => util.caselessCompare(value, R('control.confirm end.password')),
            correct_message: R('control.confirm end.correct'),
            incorrect_message: R('control.confirm end.incorrect')
        });
    }
    display.ExamDisplay.prototype = /** @lends Numbas.display.ExamDisplay.prototype */
    {
        /** Reference to the associated exam object.
         *
         * @type {Numbas.Exam}
         * @memberof Numbas.display.ExamDisplay
         */
        exam: undefined,

        /** Try to begin the exam.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        beginExam: function() {
            if(!this.canBegin()) {
                return;
            }
            if(this.needsStudentName) {
                this.exam.student_name = this.exam.student_name || this.student_name();
            }
            this.exam.begin();
        },

        /** Update the timer.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        showTiming: function() {
            this.showTime(true);
            this.duration(this.exam.settings.duration);
            this.timeRemaining(this.exam.timeRemaining);
            this.timeSpent(this.exam.timeSpent);
        },

        /**
         * Show the question menu.
         *
         * @see Numbas.Exam.showMenu
         */
        showMenu: function() {
            this.exam.showMenu();
        },

        /** Initialise the question list display.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        initQuestionList: function() {
            var exam = this.exam;
            var ended = this.ended;
            this.question_groups = this.exam.question_groups.map(function(g) {
                var questions = Knockout.observable(g.questionList.map(function(q) {
                    return q.display
                }));
                var show_name = Knockout.computed(function() {
                    return questions().some(function(q) {
                        return q.visible();
                    });
                });
                var qg = {
                    name: g.settings.name,
                    group: g,
                    questions: questions,
                    show_name: show_name,
                    doesMarking: Knockout.observable(true)
                }
                qg.marks = Knockout.computed(function() {
                    var total = 0;
                    questions().forEach(function(qd) {
                        total += qd.marks();
                    });
                    return total;
                });
                qg.score = Knockout.computed(function() {
                    var score = 0;
                    questions().forEach(function(qd) {
                        score += qd.score();
                    });
                    return score;
                });
                qg.credit = Knockout.computed(function() {
                    var score = qg.score();
                    var marks = qg.marks();
                    return marks == 0 ? 0 : score / marks;
                });
                qg.revealed = Knockout.computed(function() {
                    return questions().every(function(qd) {
                        return qd.revealed();
                    });
                });
                qg.ended = ended;
                qg.anyAnswered = Knockout.computed(function() {
                    return questions().some(function(qd) {
                        return qd.anyAnswered();
                    });
                })
                qg.answered = Knockout.computed(function() {
                    return questions().every(function(qd) {
                        return qd.answered();
                    });
                });
                qg.feedback = Numbas.display_util.showScoreFeedback(qg, exam.settings);
                return qg;
            });
            for(var i = 0; i < this.exam.questionList.length; i++) {
                this.questions.push(this.exam.questionList[i].display);
            }
        },

        /** Called when a question is added to the exam.
         *
         * @param {Numbas.Question} question
         */
        addQuestion: function(question) {
            this.updateQuestionList();
        },

        updateQuestionList: function() {
            if(!this.question_groups) {
                return;
            }
            this.question_groups.forEach(function(qg) {
                qg.questions(qg.group.questionList.map(function(q) {
                    return q.display;
                }));
            });
            this.questions(this.exam.questionList.map(function(q) {
                return q.display;
            }));
        },

        pause: function() {
            this.exam.pause();
        },

        resumeExam: function() {
            this.exam.resume();
        },

        endExam: function() {
            this.exam.tryEnd();
        },

        /** Hide the timer.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        hideTiming: function() {
            this.showTime(false);
        },

        /** Set the exam start time.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        setStartTime: function() {
            this.startTime(this.exam.start);
        },

        /** Set the exam end time.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        setEndTime: function() {
            this.endTime(this.exam.stop);
        },

        /** Show/update the student's total score.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        showScore: function() {
            var exam = this.exam;
            this.marks(Numbas.math.niceNumber(exam.mark));
            this.score(Numbas.math.niceNumber(exam.score));
            this.percentScore(exam.percentScore);

            if(exam.settings.navigateMode == 'diagnostic' && exam.diagnostic_progress) {
                this.diagnostic_feedback(exam.diagnostic_feedback);
                this.diagnostic_progress(exam.diagnostic_progress.map(function(a) {
                    return {
                        name: a.name,
                        progress: a.progress,
                        credit: a.credit
                    };
                }));
            }
        },
        /** Update the question list display - typically, scroll so the current question is visible.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        updateQuestionMenu: function() {
            var exam = this.exam;
            //scroll question list to centre on current question
            if(display.carouselGo) {
                display.carouselGo(exam.currentQuestion.number - 1, 300);
            }
        },
        /** Show an info page (one of the front page, pause, or results).
         *
         * @param {string} page - Name of the page to show.
         * @memberof Numbas.display.ExamDisplay
         */
        showInfoPage: function(page) {
            window.onbeforeunload = null;

            var exam = this.exam;

            //scroll back to top of screen
            Numbas.display.control_focus && scroll(0, 0);

            switch(page) {
                case "frontpage":
                    this.marks(exam.mark);
                    break;
                case "result":
                    this.ended(true);
                    this.result(exam.result);
                    this.passed(exam.passed);
                    this.feedbackMessage(Numbas.util.isNonemptyHTML(exam.feedbackMessage) ? exam.feedbackMessage : null);
                    break;
                case "paused":
                case "resumed":
                    this.showScore();
                    break;
            }

            this.infoPage(page);
            this.currentQuestion(null);

            this.hideNavMenu();
        },

        /** Show the modal dialog with actions the student can take to move on from the current question.
         */
        showDiagnosticActions: function() {
            var ed = this;
            var res = this.exam.diagnostic_actions();
            var actions = {
                feedback: res.feedback,
                actions: res.actions.map(function(action) {
                    var out = {
                        label: action.label,
                        go: function() {
                            ed.do_diagnostic_action(action);
                        }
                    }
                    return out;
                })
            };
            this.diagnostic_next_actions(actions);
            this.root_element.shadowRoot.getElementById('next-actions-modal').showModal();
        },

        /** Show the current question.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        showQuestion: function() {
            var exam = this.exam;
            this.infoPage(null);
            this.currentQuestion(exam.currentQuestion.display);
            if(exam.settings.preventLeave && this.mode() != 'review') {
                window.onbeforeunload = function() {
                    return R('control.confirm leave')
                };
            } else {
                window.onbeforeunload = null;
            }
            exam.currentQuestion.display.show();
            if(exam.settings.navigateMode == 'diagnostic') {
                this.current_topic(exam.diagnostic_controller.current_topic());
            }
            this.hideNavMenu();
        },
        /** Hide the sliding side menu.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        hideNavMenu: function() {
            this.root_element.classList.remove('show-sidebar');
        },
        /** Called just before the current question is regenerated.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        startRegen: function() {
            this.root_element.shadowRoot.getElementById('questionDisplay').hidden = true;
            var html = this.exam.currentQuestion.display.html;
            html.parentElement.removeChild(html);
            this.oldQuestion = this.exam.currentQuestion.display;
        },
        /** Called after the current question has been regenerated.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        endRegen: function() {
            var currentQuestion = this.exam.currentQuestion;
            this.questions.splice(currentQuestion.number, 1, currentQuestion.display);
            var group = this.question_groups.filter(function(g) {
                return g.group == currentQuestion.group
            })[0];
            var n_in_group = currentQuestion.group.questionList.indexOf(currentQuestion);
            var group_questions = group.questions();
            group_questions.splice(n_in_group, 1, currentQuestion.display);
            group.questions(group_questions);
            this.applyQuestionBindings(currentQuestion);
            this.root_element.shadowRoot.getElementById('questionDisplay').hidden = false;
        },

        do_diagnostic_action: function(action) {
            this.root_element.shadowRoot.getElementById('next-actions-modal').close();
            this.exam.do_diagnostic_action(action);
        },
        /**
         * Apply knockout bindings to the given question.
         *
         * @param {Numbas.Question} question
         * @memberof Numbas.display.ExamDisplay
         */
        applyQuestionBindings: function(question) {
        },
        /** Reveal the answers to every question in the exam.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        revealAnswers: function() {
            this.revealed(this.exam.revealed);
        },

        /** Called when the exam ends.
         *
         * @memberof Numbas.display.ExamDisplay
         */
        end: function() {
            this.ended(true);
            this.timeSpent(this.exam.timeSpent);
            this.mode(this.exam.mode);
            this.questions().map(function(q) {
                q.end();
            });
        },

        /** Download the attempt data.
         */
        download_attempt_data: async function() {
            /** Remove newlines from a string used in the preamble.
             * @param {string} s
             * @returns {string}
             */
            function sanitise_preamble(s) {
                return (s || '').replace(/\n/g, '');
            }
            const preamble = `Numbas attempt data
Exam: ${sanitise_preamble(this.exam.settings.name)}
Student name: ${sanitise_preamble(this.exam.student_name)}
Start time: ${sanitise_preamble(this.exam.start.toISOString())}
----\n`;

            const exam_object = this.exam.store.examSuspendData();
            const contents = JSON.stringify(exam_object); //this will need to be a json of the exam object, which seems like it should be created somewhere already as we have ways to access it?
            let encryptedContents;
            try {
                encryptedContents = await Numbas.download.encrypt(contents, this.exam.settings.downloadEncryptionKey);
            } catch {
                this.root_element.showAlert(R('exam.attempt download security warning'));
                return;
            }
            encryptedContents = util.b64encode(encryptedContents);
            const exam_slug = util.slugify(this.exam.settings.name) ;
            const student_name_slug = util.slugify(this.exam.student_name);
            const start_time = this.exam.start.toISOString().replace(':', '-');

            const filename = `${exam_slug}-${student_name_slug}-${start_time}.txt`;

            Numbas.download.download_file(preamble + encryptedContents, filename);
        }
    };
});
;
Numbas.queueScript('exam-to-xml', [], function() {

class ExamError extends Error {
    constructor(message, hint = '') {
        super();
        this.message = message;
        this.hint = hint;
    }
}

/**
 * Return a template literal formatter which copies attributes from the given object to a new object.
 * Put each attribute name on a separate line.
 * You can give an interpolation to specify a different value for a target attribute.
 *
 * @param {object} arg
 * @returns {Function}
 */
function copy_attrs(arg) {
        return function(strs, ...vars) {
                const o = {};
                strs.forEach((str, i) => {
                        str = str.trim();
                        if(!str) {
                                return;
                        }
                        const lines = str.split('\n');
                        lines.slice(0, -1).forEach((line) => {
                                line = line.trim();
                                o[line] = arg[line];
                        });
                        const last = lines.at(-1).trim();
                        o[last] = i < vars.length ? vars[i] : arg[last];
                })
                return o;
        }
}

/**
 * Convert all the keys in the object to lowercase.
 *
 * @param {object} data
 * @returns {object}
 */
function lowercase_keys(data) {
    return Object.fromEntries(Object.entries(data).map(([k, v]) => [k.toLowerCase(), v]));
}

class ExamEvent {
    constructor(builder, name, action, message) {
        this.builder = builder;
        this.name = name;
        this.action = action;
        this.message = message;
    }

    toXML() {
        return this.builder.element(
            'event',
            {
                type: this.name,
                action: this.action,
            },
            [this.builder.makeContentNode(this.message)]
        );
    }
}

class FeedbackMessage {
    message = '';

    threshold = 0;

    constructor(builder, data) {
        this.builder = builder;

        builder.tryLoad(data, ['message', 'threshold'], this);
    }

    toXML() {
        return this.builder.element(
            'feedbackmessage',
            {
                threshold: this.threshold
            },
            [this.builder.makeContentNode(this.message)]
        );
    }
}

class QuestionGroup {
    name = '';

    pickingStrategy = 'all-ordered'; // 'all-ordered', 'all-shuffled', 'random-subset'

    pickQuestions = 0;

    constructor(builder, data) {
        this.builder = builder;
        this.questions = [];

        builder.tryLoad(data, ['name', 'pickingStrategy', 'pickQuestions'], this);

        const {questions, questionnames, variable_overrides} = lowercase_keys(data);

        if(questions) {
            this.questions = questions.map((q) => builder.question(q));
        }

        if(questionnames) {
            questionnames.forEach((name, i) => {
                this.questions[i].customName = name
            });
        }

        if(variable_overrides) {
            variable_overrides.forEach((vos, i) => {
                const q = this.questions[i];
                for(const {name, definition} of vos) {
                    const v = q.get_variable(name);
                    if(v) {
                        v.definition = definition;
                    }
                }
            })
        }
    }

    toXML() {
        return this.builder.element(
            'question_group',
            {
                name: this.name,
                pickingStrategy: this.pickingStrategy,
                pickQuestions: this.pickQuestions,
            },
            [
                this.builder.element(
                    'questions',
                    {},
                    this.questions.map((q) => q.toXML())
                )
            ]
        );
    }
}

class Question {
    name = 'Untitled Question';

    customName = '';

    statement = '';

    advice = '';

    parts_mode = 'all';

    maxMarks = 0;

    objectiveVisibility = 'always';

    penaltyVisibility = 'always';

    constructor(builder, data) {
        this.builder = builder;

        this.name = data.name;
        this.parts = [];
        this.builtin_constants = {};
        this.constants = [];
        this.variables = [];
        this.variablesTest = {
            condition: '',
            maxruns: 10
        };

        this.functions = [];
        this.rulesets = {};

        this.tags = [];
        this.objectives = [];
        this.penalties = [];

        this.extensions = [];

        this.preamble = {
            js: '',
            css: ''
        }

        builder.tryLoad(data, ['name', 'statement', 'advice', 'maxMarks', 'objectiveVisibility', 'penaltyVisibility', 'extensions'], this);

        builder.tryLoad(data, ['partsMode'], this, ['parts_mode']);

        const {tags, parts, builtin_constants, constants, variables, variablesTest, functions, preamble, rulesets, objectives, penalties} = data;

        if(tags) {
            this.tags = tags.slice();
        }

        if(parts) {
            this.parts = parts.map((p) => builder.part(p));
        }

        if(builtin_constants) {
            this.builtin_constants = builtin_constants;
        }

        if(constants) {
            this.constants = constants.map((c) => builder.custom_constant(c));
        }

        if(variables) {
            this.variables = Object.values(variables).map((v) => builder.variable(v));
        }

        if(variablesTest) {
            builder.tryLoad(variablesTest, ['condition', 'maxRuns'], this.variablesTest);
        }

        if(functions) {
            this.functions = Object.entries(functions).map(([name, def]) => builder.function(name, def));
        }

        if(preamble) {
            builder.tryLoad(preamble, ['js', 'css'], this.preamble);
        }

        if(rulesets) {
            this.rulesets = builder.rulesets(rulesets);
        }

        if(objectives) {
            this.objectives = objectives.map((o) => builder.scorebin(o));
        }

        if(penalties) {
            this.penalties = penalties.map((p) => builder.scorebin(p));
        }
    }

    get_variable(name) {
        return this.variables.find((v) => v.name == name);
    }

    toXML() {
        const {builder} = this;
        const element = builder.element.bind(builder);

        return element(
            'question',
            copy_attrs(this)`
                name
                customName
                partsmode ${this.parts_mode}
                maxMarks
                objectiveVisibility
                penaltyVisibility
            `,
            [
                element('statement', {}, [builder.makeContentNode(this.statement)]),
                element(
                    'parts',
                    {},
                    this.parts.map((p) => p.toXML())
                ),
                element('advice', {}, [builder.makeContentNode(this.advice)]),
                element(
                    'constants',
                    {},
                    [
                        element(
                            'builtin',
                            {},
                            Object.entries(this.builtin_constants).map(([name, enable]) => element('constant', {name, enable}))
                        ),
                        element(
                            'custom',
                            {},
                            this.constants.map((c) => c.toXML())
                        )
                    ]
                ),
                element(
                    'variables',
                    {
                        condition: this.variablesTest.condition,
                        maxRuns: this.variablesTest.maxRuns
                    },
                    this.variables.map((v) => v.toXML())
                ),
                element(
                    'functions',
                    {},
                    this.functions.map((f) => f.toXML())
                ),
                element(
                    'preambles',
                    {
                        nosubvars: true
                    },
                    Object.entries(this.preamble).map(([language, text]) => element('preamble', {language}, [builder.text_node(text)]))
                ),

                element(
                    'rulesets',
                    {},
                    Object.entries(this.rulesets).map(([name, rules]) =>
                        element(
                            'set',
                            {name},
                            rules.map((rule) => typeof rule == 'string' ? element('include', {name:rule}) : rule.toXML())
                        )
                    )
                ),

                element('objectives', {}, this.objectives.map((o) => o.toXML())),
                element('penalties', {}, this.penalties.map((p) => p.toXML())),

                element('tags', {}, this.tags.map((tag) => element('tag', {}, [builder.text_node(tag)]))),

                element('extensions', {}, this.extensions.map((extension) => element('extension', {}, [builder.text_node(extension)])))
            ]
        )
    }
}

class CustomConstant {
    name = '';

    value = '';

    tex = '';

    constructor(builder, data) {
        this.builder = builder;

        builder.tryLoad(data, ['name', 'value', 'tex'], this);
    }

    toXML() {
        return this.builder.element(
            'constant',
            {
                name: this.name,
                value: this.value,
                tex: this.tex
            }
        )
    }
}

class CustomFunction {
    name = '';

    type = '';

    definition = '';

    language = 'jme';

    constructor(builder, name, data) {
        this.builder = builder;

        this.parameters = {};

        this.name = name;

        builder.tryLoad(data, ['parameters', 'type', 'definition', 'language'], this);
    }

    toXML() {
        const {builder} = this;

        return builder.element(
            'function',
            {
                name: this.name,
                outtype: this.type,
                definition: this.definition,
                language: this.language,
            },
            [
                builder.element(
                    'parameters',
                    {},
                    this.parameters.map(([name, type]) => builder.element('parameter', {name, type}))
                )
            ]
        )
    }
}

class VariableReplacement {
    variable = '';

    part = '';

    must_go_first = false;

    constructor(builder, data) {
        this.builder = builder;

        builder.tryLoad(data, ['variable', 'part', 'must_go_first'], this);
    }

    toXML() {
        return this.builder.element(
            'replace',
            {
                variable: this.variable,
                part: this.part,
                must_go_first: this.must_go_first
            }
        )
    }
}

class NextPart {
    otherPart = '';

    label = '';

    availabilityCondition = '';

    penalty = '';

    penaltyAmount = '';

    showPenaltyHint = true;

    lockAfterLeaving = false;

    constructor(builder, data) {
        this.builder = builder;
        this.variable_replacements = [];

        builder.tryLoad(data, ['otherPart', 'label', 'availabilityCondition', 'penalty', 'showPenaltyHint', 'lockAfterLeaving', 'penaltyAmount'], this);

        const {variablereplacements} = lowercase_keys(data);
        if(variablereplacements) {
            this.variable_replacements = variablereplacements.map((vrd) => builder.tryLoad(vrd, ['variable', 'definition'], {}));
        }
    }

    toXML() {
        const {builder} = this;
        const element = builder.element.bind(builder);

        return element(
            'nextpart',
            copy_attrs(this)`
                index ${this.otherPart}
                label
                availabilityCondition
                penalty
                penaltyAmount
                showPenaltyHint
                lockAfterLeaving
            `,
            [
                element(
                    'variablereplacements',
                    {},
                    this.variable_replacements.map(({variable, definition}) => element('replacement', {variable, definition}))
                )
            ]
        )
    }
}

class Variable {
    name = '';

    definition = '';

    constructor(builder, data) {
        this.builder = builder;

        builder.tryLoad(data, ['name', 'definition'], this);
    }

    toXML() {
        const {builder} = this;

        return builder.element(
            'variable',
            {
                name: this.name
            },
            [
                builder.element(
                    'value',
                    {},
                    [builder.text_node(this.definition)]
                )
            ]
        );
    }
}

class ScoreBin {
    name = '';

    limit = 0;

    constructor(builder, data) {
        this.builder = builder;

        builder.tryLoad(data, ['name', 'limit'], this);
    }

    toXML() {
        return this.builder.element(
            'scorebin',
            {
                name: this.name,
                limit: this.limit
            }
        )
    }
}

class Part {
    useCustomName = false;

    customName = '';

    prompt = '';

    alternativeFeedbackMessage = '';

    useAlternativeFeedback = false;

    type = '';

    stepsPenalty = 0;

    enableMinimumMarks = true;

    minimumMarks = 0;

    showCorrectAnswer = true;

    showFeedbackIcon = true;

    variableReplacementStrategy = 'originalfirst';

    adaptiveMarkingPenalty = 0;

    customMarkingAlgorithm = '';

    extendBaseMarkingAlgorithm = true;

    exploreObjective = null;

    suggestGoingBack = false;

    constructor(builder, data) {
        this.builder = builder;

        this.steps = [];
        this.alternatives = [];
        this.scripts = {};
        this.variable_replacements = [];
        this.next_parts = [];

        builder.tryLoad(
            data,
            [
                    'useCustomName',
                    'customName',
                    'stepsPenalty',
                    'minimumMarks',
                    'enableMinimumMarks',
                    'showCorrectAnswer',
                    'showFeedbackIcon',
                    'variableReplacementStrategy',
                    'adaptiveMarkingPenalty',
                    'customMarkingAlgorithm',
                    'extendBaseMarkingAlgorithm',
                    'exploreObjective',
                    'suggestGoingBack',
                    'useAlternativeFeedback',
            ],
            this
        );

        const {marks, prompt, alternativefeedbackmessage, steps, alternatives, scripts, variablereplacements, nextparts} = lowercase_keys(data);

        if(marks !== undefined) {
            this.marks = marks;
        }

        if(prompt !== undefined) {
            this.prompt = prompt;
        }

        if(alternativefeedbackmessage) {
            this.alternativeFeedbackMessage = alternativefeedbackmessage;
        }

        if(steps) {
            this.steps = steps.map((step) => builder.part(step));
        }

        if(alternatives) {
            this.alternatives = alternatives.map((alternative) => builder.part(alternative));
        }

        if(scripts) {
            Object.assign(this.scripts, scripts);
        }

        if(variablereplacements) {
            this.variable_replacements = variablereplacements.map((vr) => builder.variable_replacement(vr));
        }

        if(nextparts) {
            this.next_parts = nextparts.map((np) => builder.next_part(np));
        }
    }

    toXML() {
        const {builder} = this;
        const element = builder.element.bind(builder);
        return element(
            'part',
            copy_attrs(this)`
            useCustomName
            type
            marks
            stepsPenalty
            enableMinimumMarks
            minimumMarks
            showCorrectAnswer
            showFeedbackIcon
            exploreObjective
            suggestGoingBack
            useAlternativeFeedback
            customName
            `,
            [
                element('prompt', {}, [builder.makeContentNode(this.prompt)]),
                element('alternativefeedbackmessage', {}, this.alternativeFeedbackMessage ? [builder.makeContentNode(this.alternativeFeedbackMessage)] : []),
                element('steps', {}, this.steps.map((step) => step.toXML())),
                element('alternatives', {}, this.alternatives.map((alternative) => alternative.toXML())),
                element('scripts', {}, Object.entries(this.scripts).map(([name, {order, script}]) => element('script', {name, order: order || 'instead'}, [builder.text_node(script)]))),
                element('markingalgorithm', {extend: this.extendBaseMarkingAlgorithm}, [builder.text_node(this.customMarkingAlgorithm)]),
                element(
                    'adaptivemarking',
                    {
                        penalty: this.adaptiveMarkingPenalty,
                        strategy: this.variableReplacementStrategy,
                    },
                    [
                        element(
                            'variablereplacements',
                            {},
                            this.variable_replacements.map((vr) => vr.toXML())
                        )
                    ]
                ),
                element('nextparts', {}, this.next_parts.map((np) => np.toXML()))
            ]
        );
    }
}

class JMEPart extends Part {
    type = 'jme'

    answer = '';

    answerSimplification = '';

    showPreview = true;

    checkingType = 'reldiff';

    checkingAccuracy = 0;

    failureRate = 1;

    vsetRangeStart = 0;

    vsetRangeEnd = 1;

    vsetRangePoints = 5;

    checkVariableNames = false;

    singleLetterVariables = false;

    allowUnknownFunctions = true;

    implicitFunctionComposition = false;

    caseSensitive = false;

    constructor(builder, data) {
        super(builder, data);
        this.valueGenerators = [];

        builder.tryLoad(data, ['answer', 'answerSimplification', 'showPreview', 'checkingType', 'failureRate', 'vsetRangePoints', 'checkVariableNames', 'singleLetterVariables', 'allowUnknownFunctions', 'implicitFunctionComposition', 'caseSensitive'], this);

        if(this.checkingType.toLowerCase() == 'reldiff' || this.checkingType.toLowerCase() == 'absdiff') {
            this.checkingAccuracy = 0.0001;
        } else { // dp or sigfig
            this.checkingAccuracy = 5;
        }

        builder.tryLoad(data, 'checkingAccuracy', this);

        const {maxlength, minlength, musthave, notallowed, mustmatchpattern, vsetrange, valuegenerators} = lowercase_keys(data);

        this.maxLength = builder.length_restriction('maxlength', maxlength, 'Your answer is too long.');
        this.minLength = builder.length_restriction('minlength', minlength, 'Your answer is too short.');
        this.mustHave = builder.string_restriction('musthave', musthave, 'Your answer does not contain all required elements.');
        this.notAllowed = builder.string_restriction('notallowed', notallowed, 'Your answer contains elements which are not allowed.');
        this.mustMatchPattern = builder.pattern_restriction('mustmatchpattern', mustmatchpattern);

        if(vsetrange) {
            const [start, end] = vsetrange;
            this.vsetRangeStart = start;
            this.vsetRangeEnd = end;
        }

        if(valuegenerators) {
            this.valueGenerators = valuegenerators.slice();
        }
    }

    toXML() {
        const part = super.toXML();

        const {builder} = this;
        const element = builder.element.bind(builder);
        const text_node = builder.text_node.bind(builder);

        part.append(element(
            'answer',
            copy_attrs(this)`
                checkVariableNames
                singleLetterVariables
                allowUnknownFunctions
                implicitFunctionComposition
                caseSensitive
                showPreview
            `,
            [
                element(
                    'correctanswer',
                    {
                        simplification: this.answerSimplification
                    },
                    [element('math', {}, [text_node(this.answer)])]
                ),
                element(
                    'checking',
                    {
                        type: this.checkingType,
                        accuracy: this.checkingAccuracy,
                        failureRate: this.failureRate
                    },
                    [
                        element(
                            'range',
                            {
                                start: this.vsetRangeStart,
                                end: this.vsetRangeEnd,
                                points: this.vsetRangePoints
                            }
                        ),
                        element('valuegenerators', {}, this.valueGenerators.map(({name, value}) => element('generator', {name, value}))),
                    ]
                ),

                this.maxLength.toXML(),
                this.minLength.toXML(),
                this.mustHave.toXML(),
                this.notAllowed.toXML(),
                this.mustMatchPattern.toXML()
            ]
        ));

        return part;
    }
}

class Restriction {
    message = '';

    partialCredit = 0;

    constructor(builder, name, data, default_message) {
        this.builder = builder;
        this.message = default_message;

        this.name = name;

        builder.tryLoad(data, ['partialCredit', 'message'], this);
    }

    toXML() {
        return this.builder.element(
            this.name,
            {
                partialcredit: `${this.partialCredit}%`
            },
            [this.builder.element('message', {}, [this.builder.makeContentNode(this.message)])]
        );
    }
}

class LengthRestriction extends Restriction {
    length = 0;

    constructor(builder, name, data, ...args) {
        super(builder, name, data, ...args);

        builder.tryLoad(data, ['length'], this);
    }

    toXML() {
        const restriction = super.toXML();

        if(this.length >= 0) {
            restriction.setAttribute('length', this.length);
        }

        return restriction;
    }
}

class StringRestriction extends Restriction {
    showStrings = false;

    constructor(builder, name, data, ...args) {
        super(builder, name, data, ...args);

        this.strings = [];

        builder.tryLoad(data, ['showStrings'], this);

        const {strings} = data;
        if(strings) {
            this.strings = strings.slice();
        }
    }

    toXML() {
        const restriction = super.toXML();

        restriction.setAttribute('showstrings', this.showStrings);

        for(const string of this.strings) {
            restriction.append(this.builder.element('string', {}, [this.builder.text_node(string)]));
        }

        return restriction;
    }
}

class PatternRestriction extends Restriction {
    pattern = '';

    nameToCompare = '';

    warningTime = 'input';

    constructor(builder, name, data, ...args) {
        super(builder, name, data, ...args);

        builder.tryLoad(data, ['pattern', 'nameToCompare', 'warningTime'], this);
    }

    toXML() {
        const restriction = super.toXML();

        restriction.setAttribute('pattern', this.pattern);
        restriction.setAttribute('nametocompare', this.nameToCompare);
        restriction.setAttribute('warningtime', this.warningTime);

        return restriction;
    }
}

class PatternMatchPart extends Part {
    type = 'patternmatch';

    caseSensitive = false;

    partialCredit = 0;

    answer = '';

    displayAnswer = '';

    matchMode = 'regex';

    allowEmpty = false;

    constructor(builder, data) {
        super(builder, data);

        builder.tryLoad(data, ['caseSensitive', 'partialCredit', 'answer', 'displayAnswer', 'matchMode', 'allowEmpty'], this);
    }

    toXML() {
        const part = super.toXML();

        const {builder} = this;
        const element = builder.element.bind(builder);

        part.append(element('displayanswer', {}, [builder.makeContentNode(this.displayAnswer)]));

        part.append(element('correctanswer', {mode:this.matchMode, allowempty: this.allowEmpty}, [builder.text_node(this.answer)]));

        part.append(element(
            'case',
            {
                sensitive: this.caseSensitive,
                partialcredit: `${this.partialCredit}%`
            }
        ));

        return part;
    }
}

class NumberEntryPart extends Part {
    type = 'numberentry';

    allowFractions = false;

    notationStyles = ['en', 'si-en', 'plain'];

    checkingType = 'range';

    answer = 0;

    checkingAccuracy = 0;

    minvalue = 0;

    maxvalue = 0;

    correctAnswerFraction = false;

    correctAnswerStyle = 'plain';

    inputStep = 1;

    mustBeReduced = false;

    mustBeReducedPC = 0;

    precisionType = 'none';

    precision = 0;

    precisionPartialCredit = 0;

    precisionMessage = '';

    showPrecisionHint = true;

    showFractionHint = true;

    strictPrecision = true;

    displayAnswer = '';

    constructor(builder, data) {
        super(builder, data);

        builder.tryLoad(data, ['correctAnswerFraction', 'correctAnswerStyle', 'allowFractions', 'notationStyles', 'checkingType', 'inputstep', 'mustBeReduced', 'mustBeReducedPC', 'precisionType', 'precision', 'precisionPartialCredit', 'precisionMessage', 'strictPrecision', 'showPrecisionHint', 'showFractionHint', 'displayAnswer'], this);

        const {answer} = lowercase_keys(data);
        if(this.checkingType == 'range') {
            if(answer !== undefined) {
                this.maxvalue = this.minvalue = answer;
            } else {
                builder.tryLoad(data, ['minvalue', 'maxvalue'], this);
            }
        } else {
            builder.tryLoad(data, ['answer', 'checkingAccuracy'], this);
        }
    }

    toXML() {
        const part = super.toXML();

        const {builder} = this;
        const element = builder.element.bind(builder);

        part.append(element(
            'answer',
            Object.assign(
                copy_attrs(this)`
                    checkingType
                    inputStep
                    allowFractions
                    showFractionHint
                    notationStyles ${this.notationStyles.join(',')}
                    correctAnswerFraction
                    correctAnswerStyle
                    mustBeReduced
                    mustBeReducedPC ${this.mustBeReducedPC + '%'}
                    displayAnswer
                `,
                this.checkingType == 'range' ?
                    {
                        minvalue: this.minvalue,
                        maxvalue: this.maxvalue
                    }
                :
                    {
                        answer: this.answer,
                        checkingAccuracy: this.checkingAccuracy
                    }
            ),
            [
                element(
                    'precision',
                    {
                        type: this.precisionType,
                        precision: this.precision,
                        partialcredit: `${this.precisionPartialCredit}%`,
                        strict: this.strictPrecision,
                        showprecisionhint: this.showPrecisionHint
                    },
                    [element('message', {}, [builder.makeContentNode(this.precisionMessage)])]
                )
            ]
        ));

        return part;
    }
}

class MatrixEntryPart extends Part {
    type = 'matrix';

    correctAnswer = '';

    correctAnswerFractions = false;

    numRows = 3;

    numColumns = 3;

    allowResize = true;

    minColumns = 0;

    maxColumns = 0;

    minRows = 0;

    maxRows = 0;

    prefilledCells = '';

    tolerance = 0;

    markPerCell = false;

    allowFractions = false;

    precisionType = 'none';

    precision = 0;

    precisionPartialCredit = 0;

    precisionMessage = '';

    strictPrecision = true;

    constructor(builder, data) {
        super(builder, data);

        builder.tryLoad(
            data,
            [
                'correctAnswer',
                'correctAnswerFractions',
                'numRows',
                'numColumns',
                'allowResize',
                'minColumns',
                'maxColumns',
                'minRows',
                'maxRows',
                'prefilledCells',
                'tolerance',
                'markPerCell',
                'allowFractions',
                'precisionType',
                'precision',
                'precisionPartialCredit',
                'precisionMessage',
                'strictPrecision'
            ],
            this
        );
    }

    toXML() {
        const part = super.toXML();

        const {builder} = this;
        const element = builder.element.bind(builder);

        part.append(element(
            'answer',
            copy_attrs(this)`
                correctAnswer
                correctAnswerFractions
                rows ${this.numRows}
                columns ${this.numColumns}
                allowResize
                minColumns
                maxColumns
                minRows
                maxRows
                tolerance
                markPerCell
                allowFractions
                prefilledCells
            `,
            [
                element(
                    'precision',
                    {
                        type: this.precisionType,
                        precision: this.precision,
                        partialCredit: `${this.precisionPartialCredit}%`,
                        strict: this.strictPrecision
                    },
                    [element('message', {}, [builder.makeContentNode(this.precisionMessage)])]
                )
            ]
        ));

        return part;
    }
}

class MultipleChoicePart extends Part {
    minMarksEnabled = false;

    minMarks = 0;

    maxMarksEnabled = false;

    maxMarks = 0;

    minAnswers = 0;

    maxAnswers = 0;

    shuffleChoices = false;

    shuffleAnswers = false;

    displayType = 'radiogroup';

    displayColumns = 1;

    showBlankOption = true;

    warningType = 'none';

    layoutType = 'all';

    layoutExpression = '';

    showCellAnswerState = true;

    markingMethod = 'positive';

    choicesHeader = '';

    answersHeader = '';

    default_displayType() {
        return 'radiogroup';
    }

    constructor(builder, data) {
        super(builder, data);

        this.choices = [];
        this.answers = [];
        this.matrix = [];
        this.distractors = [];

        this.displayType = this.default_displayType();

        builder.tryLoad(data, ['minMarks', 'maxMarks', 'minAnswers', 'maxAnswers', 'shuffleChoices', 'shuffleAnswers', 'displayType', 'displayColumns', 'warningType', 'showCellAnswerState', 'markingMethod', 'choicesHeader', 'answersHeader', 'showBlankOption'], this);

        const {minmarks, maxmarks, choices, answers, layout, matrix, distractors} = lowercase_keys(data);

        if(minmarks !== undefined) {
            this.minMarksEnabled = true;
        }

        if(maxmarks !== undefined) {
            this.maxMarksEnabled = true;
        }

        if(choices) {
            this.choices = Array.isArray(choices) ? choices.slice() : choices;
        }

        if(answers) {
            this.answers = Array.isArray(answers) ? answers.slice() : answers;
        }

        if(layout !== undefined) {
            builder.tryLoad(layout, ['type', 'expression'], this, ['layoutType', 'layoutExpression']);
        }

        if(matrix !== undefined) {
            this.matrix = matrix;
            if(Array.isArray(matrix) && matrix.length > 0 && !Array.isArray(matrix[0])) {
                this.matrix = matrix.map((x) => [x]);
            }
        }

        if(distractors) {
            this.distractors = distractors;
            if(Array.isArray(distractors) && distractors.length > 0 && !Array.isArray(distractors[0])) {
                this.distractors = distractors.map((x) => [x]);
            }
        }
    }

    toXML() {
        const part = super.toXML();

        const {builder} = this;
        const element = builder.element.bind(builder);

        part.setAttribute('showcellanswerstate', this.showCellAnswerState);

        const choices = element(
            'choices',
            {
                minimumexpected: this.minAnswers,
                maximumexpected: this.maxAnswers,
                displaycolumns: this.displayColumns,
                shuffle: this.shuffleChoices,
                displayType: this.displayType,
                showBlankOption: this.showBlankOption,
            }
        );
        choices.append(element('header', {}, [builder.makeContentNode(this.choicesHeader)]));
        if(typeof this.choices == 'string') {
            choices.setAttribute('def', this.choices);
        } else {
            for(const choice of this.choices) {
                choices.append(element('choice', {}, [builder.makeContentNode(choice)]));
            }
        }
        part.append(choices);

        const answers = element(
            'answers',
            {
                shuffle: this.shuffleAnswers,
            }
        );
        answers.append(element('header', {}, [builder.makeContentNode(this.answersHeader)]));
        if(typeof this.answers == 'string') {
            answers.setAttribute('def', this.answers);
        } else {
            for(const answer of this.answers) {
                answers.append(element('answer', {}, [builder.makeContentNode(answer)]));
            }
        }
        part.append(answers);

        part.append(element(
            'layout',
            {
                type: this.layoutType,
                expression: this.layoutExpression
            }
        ));

        part.append(element(
            'marking',
            {
                method: this.markingMethod
            },
            [
                element('maxmarks', {enabled: this.maxMarksEnabled, value: this.maxMarks}),
                element('minmarks', {enabled: this.minMarksEnabled, value: this.minMarks}),
                typeof this.matrix == 'string' ?
                    element(
                        'matrix',
                        {
                            def: this.matrix
                        }
                    )
                :
                    element(
                        'matrix',
                        {},
                        this.matrix.flatMap((row, i) => row.map((v, j) => element('mark', {answerindex: j, choiceindex: i, value: v})))
                    ),
                element(
                    'distractors',
                    {},
                    this.distractors.flatMap((row, i) => row.map((v, j) => element('distractor', {choiceindex: i, answerindex: j}, [builder.makeContentNode(v)])))
                ),
                element('warning', {type: this.warningType})
            ]
        ))

        return part;
    }
}

class ChooseOnePart extends MultipleChoicePart {
    type = '1_n_2';
}

class ChooseSeveralPart extends MultipleChoicePart {
    type = 'm_n_2';

    default_displayType() {
        return 'checkbox';
    }
}

class MatchChoicesWithAnswersPart extends MultipleChoicePart {
    type = 'm_n_x';
}

class InformationPart extends Part {
    type = 'information';
}

/** Create a class for a custom part type with the given definition.
 *
 * @param {object} definition
 * @returns {Function}
 */
function custom_part_constructor(definition) {
    class CustomPart extends Part {
        type = definition.short_name;

        constructor(builder, data) {
            super(builder, data);

            this.type = definition.short_name;
            this.settings = {};

            const {settings: settings_def} = lowercase_keys(definition);
            const {settings: settings_data} = lowercase_keys(data);
            if(settings_def) {
                for(const {name} of settings_def) {
                    builder.tryLoad(settings_data, name, this.settings);
                }
            }
        }

        toXML() {
            const part = super.toXML();

            const {builder} = this;
            const element = builder.element.bind(builder);

            part.setAttribute('custom', true);

            const settings = element(
                'settings',
                {},
                Object.entries(this.settings).map(([name, value]) => element(
                    'setting',
                    {
                        name,
                        value: JSON.stringify(value)
                    }
                ))
            )
            part.append(settings);

            return part;
        }
    }

    return CustomPart;
}

class ExtensionPart extends Part {
    type = 'extension';
}

class GapFillPart extends Part {
    type = 'gapfill';

    sortAnswers = false;

    constructor(builder, data) {
        super(builder, data);
        this.gaps = [];

        const {gaps} = lowercase_keys(data);
        if(gaps) {
            this.gaps = gaps.map((g) => builder.part(g));
        }

        builder.tryLoad(data, ['sortAnswers'], this);

        this.prompt = this.prompt.replace(/\[\[(\d+?)\]\]/g, (_, d) => {
            d = parseInt(d);
            if(d >= this.gaps.length) {
                throw(new ExamError(`Reference to an undefined gap in a gapfill part (${d})`));
            }
            return `<gapfill reference="${d}"></gapfill>`;
        })
    }

    toXML() {
        const part = super.toXML();

        const {builder} = this;
        const element = builder.element.bind(builder);

        part.append(element(
            'gaps',
            {},
            this.gaps.map((g) => g.toXML())
        ));

        part.append(element(
            'marking',
            {
                sortanswers: this.sortAnswers
            }
        ));

        return part;
    }
}

class SimplificationRule {
    pattern = '';

    result = '';

    constructor(builder, data) {
        this.builder = builder;
        this.conditions = [];

        builder.tryLoad(data, ['pattern', 'conditions', 'result'], this);
    }

    toXML() {
        const {builder} = this;
        return builder.element(
            'ruledef',
            {
                pattern: this.pattern,
                result: this.result,
            },
            [builder.element('conditions', {}, this.conditions.map((c) => builder.element('condition', {}, [builder.text_node(c)])))]
        );
    }
}

class Exam {
    name = ''                                                     // title of exam

    duration = 0                                                // allowed time for exam, in seconds

    percentPass = 0                                         // percentage classified as a pass

    allowPrinting = true                                // allow student to print an exam transcript?

    showactualmarkwhen = 'always'                     // When to show student's score to student.

    showtotalmarkwhen = 'always'                        // When to show total marks available to student.

    showanswerstatewhen = 'always'                    // When to show right/wrong on questions.

    showpartfeedbackmessageswhen = 'always' // When to show part feedback messages.

    enterreviewmodeimmediately = true     // Enter review mode immediately after ending the exam?

    allowrevealanswer = true                        // allow student to reveal answer to question?

    intro = ''                                                    // text shown on the front page

    end_message = ''                                        // text shown on the results page

    showexpectedanswerswhen = 'inreview'    // When to show expected answers.

    showadvicewhen = true                                 // When to show question advice.

    resultsprintquestions = true                // show questions on printed results page?

    resultsprintadvice = true                     // show advice on printed results page?

    feedbackMessages = []                             // text shown on the results page when the student achieves a certain score

    showQuestionGroupNames = false            // show the names of question groups?

    showstudentname = true                            // show the student's name?

    shuffleQuestionGroups = false             // randomize the order of question groups?

    knowledge_graph = null

    diagnostic_script = 'diagnosys'

    custom_diagnostic_script = ''


    constructor(builder, data) {
        this.builder = builder;

        this.navigation = {
            'allowregen': false,
            'navigatemode': 'sequence',
            'reverse': true,
            'browse': true,
            'allowsteps': true,
            'showfrontpage': true,
            'onleave': builder.examevent('onleave', 'none', 'You have not finished the current question.'),
            'preventleave': true,
            'typeendtoleave': false,
            'startpassword': '',
            'allowAttemptDownload': false,
            'downloadEncryptionKey': '',
            'autoSubmit': true,
        }

        this.timing = {
            'timeout': builder.examevent('timeout', 'none', ''),
            'timedwarning': builder.examevent('timedwarning', 'none', ''),
            'allowPause': true,
        }

        this.rulesets = {};

        this.functions = [];

        this.variables = [];

        this.question_groups = [];

        this.resources = [];

        this.extensions = [];

        this.custom_part_types = [];

        data = lowercase_keys(data);

        builder.tryLoad(data, ['name', 'duration', 'percentPass', 'allowPrinting', 'resources', 'extensions', 'custom_part_types', 'showQuestionGroupNames', 'showstudentname', 'shuffleQuestionGroups'], this);

        const {navigation, timing, feedback, rulesets, functions, variables, question_groups, diagnostic} = data;

        if(navigation) {
            builder.tryLoad(navigation, ['allowregen', 'navigatemode', 'reverse', 'browse', 'allowsteps', 'showfrontpage', 'showresultspage', 'preventleave', 'typeendtoleave', 'startpassword', 'allowAttemptDownload', 'downloadEncryptionKey', 'autoSubmit'], this.navigation);
            const {onleave} = navigation;
            if(onleave) {
                builder.tryLoad(onleave, ['action', 'message'], this.navigation.onleave);
            }
        }

        if(timing) {
                builder.tryLoad(timing, ['allowPause'], this.timing);
                for(const event of ['timeout', 'timedwarning']) {
                        if(event in timing) {
                                builder.tryLoad(timing[event], ['action', 'message'], this.timing[event]);
                        }
                }
        }

        if(feedback) {
            builder.tryLoad(feedback, ['showactualmarkwhen', 'showtotalmarkwhen', 'showanswerstatewhen', 'showpartfeedbackmessageswhen', 'enterreviewmodeimmediately', 'allowrevealanswer', 'showexpectedanswerswhen', 'showadvicewhen'], this);
            builder.tryLoad(feedback, ['intro', 'end_message'], this);
            const {results_options, feedbackmessages} = lowercase_keys(feedback);
            if(results_options) {
                builder.tryLoad(results_options, ['printquestions', 'printadvice'], this, ['resultsprintquestions', 'resultsprintadvice']);
            }
            if(feedbackmessages) {
                this.feedbackMessages = feedbackmessages.map((f) => builder.feedback_message(f));
            }
        }

        if(rulesets) {
            this.rulesets = builder.rulesets(rulesets);
        }

        if(functions) {
            Object.entries(functions).forEach(([name, def]) => {
                this.functions.push(builder.function(name, def));
            });
        }

        if(variables) {
            Object.entries(variables).forEach(([name, def]) => {
                this.variables.push(builder.variable(name, def));
            });
        }

        if(question_groups) {
            for(const qg of question_groups) {
                this.question_groups.push(builder.question_group(qg));
            }
        }

        if(diagnostic) {
            this.knowledge_graph = diagnostic.knowledge_graph;
            this.diagnostic_script = diagnostic.script;
            this.custom_diagnostic_script = diagnostic.customScript;
        }
    }

    toXML() {
        const {builder} = this;
        const root = builder.doc.documentElement;
        root.setAttribute('name', this.name);
        root.setAttribute('percentpass', `${this.percentPass}%`);
        root.setAttribute('allowprinting', this.allowPrinting);

        const element = builder.element.bind(builder);

        const {navigation, timing} = this;
        const settings = element(
            'settings',
            {},
            [element(
                    'navigation',
                    copy_attrs(navigation)`
                        allowregen
                        navigatemode
                        reverse
                        browse
                        allowsteps
                        showfrontpage
                        preventleave
                        typeendtoleave
                        startpassword
                        allowAttemptDownload
                        downloadEncryptionKey
                        autoSubmit
                    `,
                    [navigation.onleave.toXML()]
                ),

                element(
                    'timing',
                    {
                        duration: this.duration,
                        allowPause: timing.allowPause
                    },
                    [timing.timeout.toXML(), timing.timedwarning.toXML()]
                ),

                element(
                    'feedback',
                    copy_attrs(this)`
                        enterreviewmodeimmediately
                        showactualmarkwhen
                        showtotalmarkwhen
                        showanswerstatewhen
                        showpartfeedbackmessageswhen
                        allowrevealanswer
                        showstudentname
                        showexpectedanswerswhen
                        showadvicewhen
                    `,
                    [
                        element('intro', {}, [builder.makeContentNode(this.intro)]),
                        element('end_message', {}, [builder.makeContentNode(this.end_message)]),
                        element(
                            'results_options',
                            {
                                printquestions: this.resultsprintquestions,
                                printadvice: this.resultsprintadvice
                            }
                        ),
                        element('feedbackmessages', {}, this.feedbackMessages.map((fm) => fm.toXML()))
                    ]
                ),

                element(
                    'rulesets',
                    {},
                    Object.entries(this.rulesets).map(([name, rules]) => {
                        return element(
                            'set',
                            {name},
                            rules.map((rule) => typeof rule == 'string' ? element('include', {name:rule}) : rule.toXML())
                        )
                    })
                ),

                element(
                    'diagnostic',
                    {},
                    [element(
                        'algorithm',
                        {
                            script: this.diagnostic_script,
                        },
                        [builder.text_node(this.custom_diagnostic_script)]
                    )]
                )
            ]
        );
        root.append(settings);

        root.append(element('functions', {}, this.functions.map((f) => f.toXML())));

        root.append(element('variables', {}, this.variables.map((v) => v.toXML())));

        root.append(element(
            'question_groups',
            {
                showQuestionGroupNames: this.showQuestionGroupNames,
                shuffleQuestionGroups: this.shuffleQuestionGroups
            },
            this.question_groups.map((qg) => qg.toXML())
        ));

        if(this.knowledge_graph) {
            root.append(element('knowledge_graph', {}, [builder.text_node(JSON.stringify(this.knowledge_graph))]));
        }

        return root;
    }
}

class ExamBuilder {

    part_constructors = {
        'jme': JMEPart,
        'numberentry': NumberEntryPart,
        'matrix': MatrixEntryPart,
        'patternmatch': PatternMatchPart,
        '1_n_2': ChooseOnePart,
        'm_n_2': ChooseSeveralPart,
        'm_n_x': MatchChoicesWithAnswersPart,
        'gapfill': GapFillPart,
        'information': InformationPart,
        'extension': ExtensionPart,
    }

    constructor() {
        this.doc = document.implementation.createDocument(null, "exam");
    }

    /**
     * Try to load the given attributes from `data` into `obj`.
     *
     * @param {object} data
     * @param {string|string[]} attrs - Names of attributes to load.
     * @param {object} obj
     * @param {string|string[]} altnames - Names to map names in `attr` to.
     * @returns {object} - The `obj` argument.
     */
    tryLoad(data, attrs, obj, altnames = []) {
        if(typeof attrs == 'string') {
            attrs = [attrs];
        }
        if(typeof altnames == 'string') {
            altnames = [altnames];
        }
        data = lowercase_keys(data);
        attrs.forEach((attr, i) => {
            const altname = altnames[i] || attr;
            attr = attr.toLowerCase();
            if(attr in data) {
                obj[altname] = data[attr];
            }
        });
        return obj;
    }

    /**
     * Convert a block of content into HTML, wrapped in a `<content>` tag.
     *
     * @param {string} s
     * @returns {Element}
     */
    makeContentNode(s) {
        if(s === undefined) {
            s = '';
        }
        const content = this.doc.createElement('content');

        const span = document.createElement('span');
        span.innerHTML = s;

        const serializer = new XMLSerializer();

        content.innerHTML = serializer.serializeToString(span).replace('span xmlns="http://www.w3.org/1999/xhtml"', 'span');

        for(const a of content.querySelectorAll('a:not([target])')) {
            a.setAttribute('target', '_blank');
        }

        return content;
    }

    /**
     * Make an XML element.
     *
     * @param {string} name
     * @param {object} [attributes] - Attributes to set on the element.
     * @param {Array.<Element>} [children]
     * @returns {Element}
     */
    element(name, attributes, children) {
        const elem = this.doc.createElement(name);
        if(attributes) {
            Object.entries(attributes).forEach(([k, v]) => elem.setAttribute(k.toLowerCase(), (v === null || v === undefined) ? '' : v));
        }
        if(children) {
            for(const child of children) {
                elem.appendChild(child);
            }
        }
        return elem;
    }

    /**
     * Create a text node with the given text.
     *
     * @param {string} text
     * @returns {Node}
     */
    text_node(text) {
        return this.doc.createTextNode(text);
    }

    /**
     * Make a tree of XML elements.
     *
     * @param {Array} struct
     * @returns {Element}
     */
    makeTree(struct) {
        if(Array.isArray(struct)) {
            const [name, children] = struct;
            const elem = this.doc.createElement(name);
            if(children) {
                for(const c of children) {
                    elem.append(this.makeTree(c));
                }
            }
            return elem;
        } else if(typeof struct == 'string') {
            return this.doc.createElement(struct);
        } else {
            return struct;
        }
    }

    /**
     * Append a list of elements or tree structures {@see ExamBuilder.makeTree} to an XML element.
     *
     * @param {Element} element
     * @param {Array} things
     */
    appendMany(element, things) {
        for(const thing of things) {
            if(thing instanceof Element) {
                element.append(thing);
            } else {
                element.append(this.makeTree(thing));
            }
        }
    }

    exam(data) {
        this.custom_part_types = data.custom_part_types;

        return new Exam(this, data);
    }

    examevent(name, action, message) {
        return new ExamEvent(this, name, action, message);
    }

    simplification_rule(data) {
        return new SimplificationRule(this, data);
    }

    feedback_message(data) {
        return new FeedbackMessage(this, data);
    }

    question_group(data) {
        return new QuestionGroup(this, data);
    }

    question(data) {
        return new Question(this, data);
    }

    function(name, def) {
        return new CustomFunction(this, name, def);
    }

    string_restriction(name, data, default_message) {
        return new StringRestriction(this, name, data === undefined ? {} : data, default_message);
    }

    length_restriction(name, data, default_message) {
        return new LengthRestriction(this, name, data === undefined ? {} : data, default_message);
    }

    pattern_restriction(name, data) {
        return new PatternRestriction(this, name, data === undefined ? {} : data);
    }

    variable_replacement(data) {
        return new VariableReplacement(this, data);
    }

    next_part(data) {
        return new NextPart(this, data);
    }

    custom_constant(data) {
        return new CustomConstant(this, data);
    }

    variable(data) {
        return new Variable(this, data);
    }

    part(data) {
        const kind = data.type.toLowerCase();

        const constructors = Object.assign(
            {},
            this.part_constructors,
            Object.fromEntries(this.custom_part_types.map((cpt) => [cpt.short_name, custom_part_constructor(cpt)]))
        );

        const part_constructor = constructors[kind];

        if(!part_constructor) {
            throw(new ExamError(
                `Invalid part type ${kind}`,
                `Valid part types are ${Object.keys(constructors).join(', ')}.`
            ))
        }

        return new part_constructor(this, data);
    }

    scorebin(data) {
        return new ScoreBin(this, data);
    }

    rulesets(data) {
        return Object.fromEntries(Object.entries(data).map(([name, rules]) => {
            const l = [];
            for(const rule of rules) {
                if(typeof rule == 'string') {
                    l.push(rule);
                } else {
                    l.push(this.simplification_rule(rule));
                }
            }
            return [name, l];
        }));
    }
}

Numbas.exam_to_xml = function(data) {
    const builder = new ExamBuilder();

    const exam = builder.exam(data);

    const xml = exam.toXML();

    return xml;
}

});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Defines the {@link Numbas.Exam} object. */
Numbas.queueScript('exam', ['base', 'timing', 'util', 'xml', 'schedule', 'storage', 'scorm-storage', 'math', 'question', 'jme-variables', 'jme-display', 'jme-rules', 'jme', 'diagnostic', 'diagnostic_scripts'], function() {
    var util = Numbas.util;

/** Create a {@link Numbas.Exam} object from an XML definition.
 *
 * @memberof Numbas
 * @param {Element} xml
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {Element} [display_root=undefined] - Should this exam make a {@link Numbas.display.ExamDisplay} object?
 * @param {Numbas.Scheduler} scheduler
 * @returns {Numbas.Exam}
 */
Numbas.createExamFromXML = function(xml, store, display_root, scheduler) {
    var exam = new Exam(store, scheduler);

    exam.loadFromXML(xml);

    exam.finaliseLoad(display_root)

    return exam;
}

/** Create a {@link Numbas.Exam} object from a JSON definition.
 *
 * @memberof Numbas
 * @param {object} data
 * @param {Numbas.storage.BlankStorage} [store] - the storage engine to use
 * @param {Element} display_root - The root element of the exam's display.
 * @param {Numbas.Scheduler} scheduler
 * @returns {Numbas.Exam}
 */
Numbas.createExamFromJSON = function(data, store, display_root, scheduler) {
    var exam = new Exam(store, scheduler);

    exam.loadFromJSON(data);

    exam.finaliseLoad(display_root)

    return exam;
}

/** Keeps track of all info we need to know while exam is running.
 *
 *
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {Numbas.schedule.Scheduler} scheduler - The task scheduler to use.
 * @class
 * @memberof Numbas
 */
function Exam(store, scheduler) {
    scheduler = scheduler || new Numbas.Scheduler();

    this.store = store;
    this.scheduler = scheduler;
    this.signals = new Numbas.schedule.SignalBox();
    this.events = new Numbas.schedule.EventBox();
    var scope = new Numbas.jme.Scope(Numbas.jme.builtinScope);
    this.scope = scope;

    var settings = this.settings = util.copyobj(Exam.prototype.settings);
    settings.navigationEvents = {};
    settings.timerEvents = {};
    this.feedbackMessages = [];
    this.question_groups = [];

}
Numbas.Exam = Exam;

/** The exam is ready for the student to start interacting with it.
 *
 * @event Numbas.Exam#ready
 */

/** The question list has been initialised - every question is loaded and ready to use.
 *
 * @event Numbas.Exam#question_list_initialised
 */

Exam.prototype = /** @lends Numbas.Exam.prototype */ {

    /** Load the exam's settings from an XML <exam> node.
     *
     * @param {Element} xml
     */
    loadFromXML: function(xml) {
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        if(!xml) {
            throw(new Numbas.Error('exam.xml.bad root'));
        }
        var settings = this.settings;

        this.xml = xml;
        tryGetAttribute(settings, xml, '.', ['name', 'percentPass', 'allowPrinting']);
        tryGetAttribute(settings, xml, 'questions', ['shuffle', 'all', 'pick'], ['shuffleQuestions', 'allQuestions', 'pickQuestions']);
        tryGetAttribute(settings,
            xml,
            'settings/navigation',
            [
                'allowregen',
                'navigatemode',
                'reverse',
                'browse',
                'allowsteps',
                'showfrontpage',
                'showresultspage',
                'preventleave',
                'typeendtoleave',
                'startpassword',
                'allowAttemptDownload',
                'downloadEncryptionKey',
                'autoSubmit'
            ],

            [
                'allowRegen',
                'navigateMode',
                'navigateReverse',
                'navigateBrowse',
                'allowSteps',
                'showFrontPage',
                'showResultsPage',
                'preventLeave',
                'typeendtoleave',
                'startPassword',
                'allowAttemptDownload',
                'downloadEncryptionKey',
                'autoSubmit'
            ]
        );
        //get navigation events and actions
        var navigationEventNodes = xml.selectNodes('settings/navigation/event');
        var e;
        for(let i = 0; i < navigationEventNodes.length; i++) {
            e = ExamEvent.createFromXML(navigationEventNodes[i]);
            settings.navigationEvents[e.type] = e;
        }
        tryGetAttribute(settings, xml, 'settings/timing', ['duration', 'allowPause']);
        var timerEventNodes = this.xml.selectNodes('settings/timing/event');
        for(let i = 0; i < timerEventNodes.length; i++) {
            e = ExamEvent.createFromXML(timerEventNodes[i]);
            settings.timerEvents[e.type] = e;
        }
        var feedbackPath = 'settings/feedback';
        tryGetAttribute(settings, xml, feedbackPath,
            [
                'showactualmarkwhen',
                'showtotalmarkwhen',
                'showanswerstatewhen',
                'showpartfeedbackmessageswhen',
                'enterreviewmodeimmediately',
                'allowrevealanswer',
                'showstudentname',
                'showexpectedanswerswhen',
                'showadvicewhen'
            ],
            [
                'showActualMark',
                'showTotalMark',
                'showAnswerState',
                'showPartFeedbackMessages',
                'enterReviewModeImmediately',
                'allowRevealAnswer',
                'showStudentName',
                'revealExpectedAnswers',
                'revealAdvice'
            ]
        );
        tryGetAttribute(settings, xml, 'settings/feedback/results_options', ['printquestions', 'printadvice'], ['resultsprintquestions', 'resultsprintadvice']);
        var serializer = new XMLSerializer();
        var isEmpty = Numbas.xml.isEmpty;
        var introNode = this.xml.selectSingleNode(feedbackPath + '/intro/content/span');
        this.hasIntro = !isEmpty(introNode);
        this.introMessage = this.hasIntro ? serializer.serializeToString(introNode) : '';

        var end_message_node = this.xml.selectSingleNode(feedbackPath + '/end_message/content/span');
        this.has_end_message = !isEmpty(end_message_node);
        this.end_message = this.has_end_message ? serializer.serializeToString(end_message_node) : '';

        var feedbackMessageNodes = this.xml.selectNodes(feedbackPath + '/feedbackmessages/feedbackmessage');
        for(let i = 0;i < feedbackMessageNodes.length;i++) {
            var feedbackMessageNode = feedbackMessageNodes[i];
            var feedbackMessage = {threshold: 0, message: ''};
            feedbackMessage.message = serializer.serializeToString(feedbackMessageNode.selectSingleNode('content/span'));
            tryGetAttribute(feedbackMessage, null, feedbackMessageNode, ['threshold']);
            this.feedbackMessages.push(feedbackMessage);
        }
        var rulesetNodes = xml.selectNodes('settings/rulesets/set');
        var sets = {};
        for(let i = 0; i < rulesetNodes.length; i++) {
            var name = rulesetNodes[i].getAttribute('name');
            var set = [];
            //get new rule definitions
            var defNodes = rulesetNodes[i].selectNodes('ruledef');
            for(var j = 0; j < defNodes.length; j++) {
                var pattern = defNodes[j].getAttribute('pattern');
                var result = defNodes[j].getAttribute('result');
                var conditions = [];
                var conditionNodes = defNodes[j].selectNodes('conditions/condition');
                for(let k = 0; k < conditionNodes.length; k++) {
                    conditions.push(Numbas.xml.getTextContent(conditionNodes[k]));
                }
                var rule = new Numbas.jme.display.Rule(pattern, conditions, result);
                set.push(rule);
            }
            //get included sets
            var includeNodes = rulesetNodes[i].selectNodes('include');
            for(let j = 0; j < includeNodes.length; j++) {
                set.push(includeNodes[j].getAttribute('name'));
            }
            sets[name] = this.scope.rulesets[name] = set;
        }
        for(const [name, set] of Object.entries(sets)) {
            this.scope.rulesets[name] = Numbas.jme.collectRuleset(set, this.scope.allRulesets());
        }
        // question groups
        tryGetAttribute(settings, xml, 'question_groups', ['showQuestionGroupNames', 'shuffleQuestionGroups']);
        var groupNodes = this.xml.selectNodes('question_groups/question_group');
        for(let i = 0;i < groupNodes.length;i++) {
            var qg = new QuestionGroup(this, i);
            qg.loadFromXML(groupNodes[i]);
            this.question_groups.push(qg);
        }

        // knowledge graph
        var knowledgeGraphNode = this.xml.selectSingleNode('knowledge_graph');
        if(knowledgeGraphNode) {
            var kgdata = Numbas.xml.getTextContent(knowledgeGraphNode);
            if(kgdata) {
                this.knowledge_graph = new Numbas.diagnostic.KnowledgeGraph(JSON.parse(kgdata));
            }
        }

        var diagnosticAlgorithmNode = this.xml.selectSingleNode('settings/diagnostic/algorithm');
        tryGetAttribute(settings, null, diagnosticAlgorithmNode, ['script'], ['diagnosticScript']);
        settings.customDiagnosticScript = Numbas.xml.getTextContent(diagnosticAlgorithmNode);
    },

    loadFromJSON: function(data) {
        this.json = data;
        var exam = this;
        var settings = exam.settings;
        var tryLoad = Numbas.json.tryLoad;
        var tryGet = Numbas.json.tryGet;
        tryLoad(data, ['name', 'duration', 'percentPass', 'allowPrinting', 'showQuestionGroupNames', 'showStudentName', 'shuffleQuestions', 'shuffleQuestionGroups'], settings);
        var question_groups = tryGet(data, 'question_groups');
        if(question_groups) {
            question_groups.forEach(function(qgdata) {
                var qg = new QuestionGroup(exam);
                qg.loadFromJSON(qgdata);
                exam.question_groups.push(qg);
            });
        }
        var navigation = tryGet(data, 'navigation');
        if(navigation) {
            tryLoad(navigation, ['allowRegen', 'allowSteps', 'showFrontPage', 'showResultsPage', 'preventLeave', 'typeendtoleave', 'startPassword', 'allowAttemptDownload', 'downloadEncryptionKey', 'autoSubmit', 'navigateMode'], settings);
            tryLoad(navigation, ['reverse', 'browse'], settings, ['navigateReverse', 'navigateBrowse']);
            var onleave = tryGet(navigation, 'onleave');
            settings.navigationEvents.onleave = ExamEvent.createFromJSON('onleave', onleave);
        }
        var timing = tryGet(data, 'timing');
        if(timing) {
            tryLoad(timing, ['allowPause'], settings);
            var timeout = tryGet(timing, 'timeout');
            if(timeout) {
                settings.timerEvents.timeout = ExamEvent.createFromJSON('timeout', timeout);
            }
            var timedwarning = tryGet(timing, 'timedwarning');
            if(timedwarning) {
                settings.timerEvents.timedwarning = ExamEvent.createFromJSON('timedwarning', timedwarning);
            }
        }
        var feedback = tryGet(data, 'feedback');
        if(feedback) {
            tryLoad(
                feedback,
                [
                    'showactualmarkwhen',
                    'showtotalmarkwhen',
                    'showanswerstatewhen',
                    'showpartfeedbackmessageswhen',
                    'enterreviewmodeimmediately',
                    'showexpectedanswerswhen',
                    'showadvicewhen',
                    'allowrevealanswer',
                    'advicethreshold',
                ],
                settings,
                [
                    'showActualMark',
                    'showTotalMark',
                    'showAnswerState',
                    'showPartFeedbackMessages',
                    'enterReviewModeImmediately',
                    'revealExpectedAnswers',
                    'revealAdvice',
                    'allowRevealAnswer',
                    'adviceThreshold'
                ]
            );
            tryLoad(feedback, ['intro'], exam, ['introMessage']);
            var results_options = tryGet(feedback, 'results_options')
            if(results_options) {
                tryLoad(results_options, ['resultsprintquestions', 'resultsprintadvice'], settings);
            }
            var feedbackmessages = tryGet(feedback, 'feedbackmessages');
            if(feedbackmessages) {
                feedbackmessages.forEach(function(d) {
                    var fm = {threshold: 0, message: ''};
                    tryLoad(d, ['mesage', 'threshold'], fm);
                    exam.feedbackMessages.push(fm);
                });
            }
        }

        var diagnostic = tryGet(data, 'diagnostic');
        if(diagnostic) {
            var knowledge_graph = tryGet(diagnostic, 'knowledge_graph');
            if(knowledge_graph) {
                this.knowledge_graph = new Numbas.diagnostic.KnowledgeGraph(knowledge_graph);
            }
            tryLoad(diagnostic, ['script', 'customScript'], settings, ['diagnosticScript', 'customDiagnosticScript']);
        }
    },

    /** Perform any tidying up or processing that needs to happen once the exam's definition has been loaded.
     *
     * @param {Element} [display_root] - The root element of the exam display.
     * @fires Numbas.Exam#diagnostic_controller_initialised
     */
    finaliseLoad: function(display_root) {
        var exam = this;
        const makeDisplay = display_root !== undefined;
        var settings = this.settings;
        this.settings.initial_duration = this.settings.duration;

        this.updateDurationExtension();

        this.updateDisplayDuration();
        this.feedbackMessages.sort(function(a, b) {
            var ta = a.threshold;
            var tb = b.threshold; return ta > tb ? 1 : ta < tb ? -1 : 0
        });

        if(this.settings.navigateMode == 'diagnostic') {
            exam.signals.on('question list initialised', function() {
                exam.questionList.forEach(function(q) {
                    var topics = [];
                    q.tags.forEach(function(t) {
                        var m = t.match(/skill: (.*)/);
                        if(m) {
                            topics.push(m[1]);
                        }
                    });
                    q.topics = topics;
                });

                var script;
                switch(exam.settings.diagnosticScript) {
                    case 'custom':
                        script = new Numbas.diagnostic.DiagnosticScript(exam.settings.customDiagnosticScript);
                        break;
                    default:
                        script = Numbas.diagnostic.scripts[exam.settings.diagnosticScript];
                        if(exam.settings.customDiagnosticScript) {
                            script = new Numbas.diagnostic.DiagnosticScript(exam.settings.customDiagnosticScript, script);
                        }
                }
                exam.diagnostic_controller = new Numbas.diagnostic.DiagnosticController(exam.knowledge_graph, exam, script);
                exam.signals.trigger('diagnostic controller initialised');
            }).catch(function(e) {
                Numbas.schedule.halt(e);
            });
        }

        if(Numbas.is_instructor) {
            settings.allowPrinting = true;
        }

        //initialise display
        if(Numbas.display && makeDisplay) {
            this.display = new Numbas.display.ExamDisplay(this, display_root);
        }
    },

    /** Signals produced while loading this exam.
     *
     * @type {Numbas.schedule.SignalBox}
     */
    signals: undefined,

    /** Storage engine
     *
     * @type {Numbas.storage.BlankStorage}
     */
    store: undefined,

    /** How was the exam started?
     *
     * One of: `ab-initio`, `resume`, or `review`
     *
     * @type {string}
     */
    entry: 'ab-initio',

    /** Settings for the exam object.
     *
     * @property {string} name - Title of exam
     * @property {number} percentPass - Percentage of max. score student must achieve to pass
     * @property {boolean} allowPrinting - Allow the student to print an exam transcript? If not, the theme should hide everything in print media and not show any buttons to print.
     * @property {boolean} shuffleQuestions - should the questions be shuffled?
     * @property {boolean} shuffleQuestionGroups - randomize question group order?
     * @property {number} numQuestions - number of questions in this sitting
     * @property {boolean} preventLeave - prevent the browser from leaving the page while the exam is running?
     * @property {boolean} typeendtoleave - require written confirmation before leaving the exam?
     * @property {string} startPassword - password the student must enter before beginning the exam
     * @property {boolean} allowRegen - can student re-randomise a question?
     * @property {boolean} allowAttemptDownload - Can the student download their results as a CSV?
     * @property {string} downloadEncryptionKey - key for encryption student data?
     * @property {boolean} autoSubmit - Automatically submit parts after entering an answer? If false, then the student must click the "Save answer" button.
     * @property {string} navigateMode - how is the exam navigated? Either `"sequence"`, `"menu"` or `"diagnostic"`
     * @property {boolean} navigateReverse - can student navigate to previous question?
     * @property {boolean} navigateBrowse - can student jump to any question they like?
     * @property {boolean} allowSteps - are steps enabled?
     * @property {boolean} showFrontPage - show the frontpage before starting the exam?
     * @property {boolean} enterReviewModeImmediately - Should the exam go into review mode immediately after ending, or only when re-entering in review mode?
     * @property {Array.<Object<Numbas.ExamEvent>>} navigationEvents - checks to perform when doing certain navigation action
     * @property {Array.<Object<Numbas.ExamEvent>>} timerEvents - Events based on timing.
     * @property {number} duration - The time allowed for the exam, in seconds.
     * @property {number} duration_extension - A number of seconds to add to the duration.
     * @property {number} initial_duration - The duration without any extension applied.
     * @property {boolean} allowPause - Can the student suspend the timer with the pause button or by leaving?
     * @property {string} showActualMark - When should the current score be shown?
     * @property {string} showTotalMark - When should total marks in the exam be shown?
     * @property {string} showAnswerState - When to tell the student if answer is correct/wrong/partial?
     * @property {string} showPartFeedbackMessages - When to show part feedback messages?
     * @property {boolean} allowRevealAnswer - Allow 'reveal answer' button?
     * @property {boolean} showQuestionGroupNames - Show the names of question groups?
     * @property {string} revealAdvice - When should question advice be shown?
     * @property {string} revealExpectedAnswers - When should expected answers be shown?
     * @property {boolean} resultsprintquestions - Show questions in printed results?
     * @property {boolean} resultsprintadvice - Show advice in printed results?
     * @memberof Numbas.Exam
     * @instance
     */
    settings: {
        name: '',
        percentPass: 0,
        allowPrinting: true,
        shuffleQuestions: false,
        numQuestions: 0,
        preventLeave: true,
        startPassword: '',
        allowRegen: false,
        allowAttemptDownload: false,
        downloadEncryptionKey: '',
        autoSubmit: true,
        navigateMode: 'menu',
        navigateReverse: false,
        navigateBrowse: false,
        allowSteps: true,
        showFrontPage: true,
        enterReviewModeImmediately: true,
        navigationEvents: {},
        timerEvents: {},
        duration: 0,
        initial_duration: 0,
        allowPause: false,
        showActualMark: 'inreview',
        showTotalMark: 'inreview',
        showAnswerState: 'inreview',
        showPartFeedbackMessages: 'inreview',
        allowRevealAnswer: false,
        showQuestionGroupNames: false,
        shuffleQuestionGroups: false,
        showStudentName: true,
        revealAdvice: 'inreview',
        revealExpectedAnswers: 'inreview',
        resultsprintquestions: true,
        resultsprintadvice: true,
        diagnosticScript: 'diagnosys',
        customDiagnosticScript: ''
    },
    /** Base node of exam XML
     *
     * @type {Element}
     */
    xml: undefined,
    /** Definition of the exam
     *
     * @type {object}
     */
    json: undefined,
    /**
     * Can be:
     *
     * - `"normal"` - Student is currently sitting the exam.
     * - `"review"` - Student is reviewing a completed exam.
     *
     * @type {string}
     */
    mode: 'normal',
    /** Total marks available in the exam.
     *
     * @type {number}
     */
    mark: 0,
    /** Student's current score.
     *
     * @type {number}
     */
    score: 0,                    //student's current score
    /** Student's score as a percentage.
     *
     * @type {number}
     */
    percentScore: 0,
    /** Have the correct answers been revealed?
     *
     * @type {boolean}
     */
    revealed: false,
    /** Did the student pass the exam?
     *
     * @type {boolean}
     */
    passed: false,                //did student pass the exam?
    /** Student's name.
     *
     * @type {string}
     */
    student_name: undefined,
    /** Student's ID.
     *
     * @type {string}
     */
    student_id: undefined,
    /** JME evaluation environment.
     *
     * Contains variables, rulesets and functions defined by the exam and by extensions.
     *
     * Inherited by each {@link Numbas.Question}'s scope.
     *
     * @type {Numbas.jme.Scope}
     */
    scope: undefined,
    /** Number of the current question.
     *
     * @type {number}
     */
    currentQuestionNumber: 0,
    /** Object representing the current question.
     *
     * @type {Numbas.Question}
     */
    currentQuestion: undefined,
    /**
     * The order in which the question groups are displayed
     *
     * @type {Array.<number>}
     */
    questionGroupOrder: [],
    /** Groups of questions in the exam.
     *
     * @type {Array.<Numbas.QuestionGroup>}
     */
    question_groups: [],
    /** Which questions are used?
     *
     * @type {Array.<number>}
     */
    questionSubset: [],
    /** Question objects, in the order the student will see them.
     *
     * @type {Array.<Numbas.Question>}
     */
    questionList: [],
    /** Stopwatch object - updates the timer every second.
     *
     * @property {Date} start - The time that the stopwatch started.
     * @property {Date} end - The time that the stopwatch ended.
     * @property {number} oldTimeSpent - The value of `timeSpent` when the stopwatch was last updated.
     * @property {number} id - The id of the `Interval` which calls {@link Numbas.Exam#countDown}.
     */
    stopwatch: undefined,
    /** Time that the exam should stop.
     *
     * @type {Date}
     */
    endTime: undefined,
    /** Seconds until the end of the exam.
     *
     * @type {number}
     */
    timeRemaining: 0,
    /** Seconds the exam has been in progress.
     *
     * @type {number}
     */
    timeSpent: 0,
    /** Is the exam in progress?
     *
     * `false` before starting, when paused, and after ending.
     *
     * @type {boolean}
     */
    inProgress: false,
    /** Time the exam started.
     *
     * @type {Date}
     */
    start: Date(),
    /** Time the exam finished.
     *
     * @type {null|Date}
     */
    stop: null,
    /* Display object for this exam.
     *
     * @type {Numbas.display.ExamDisplay}
     */
    display: undefined,
    /** Stuff to do when starting exam afresh, before showing the front page.
     *
     * @fires Numbas.Exam#ready
     * @fires Numbas.Exam#display_ready
     */
    init: function() {
        var exam = this;
        if(exam.store) {
            exam.store.init(exam);        //initialise storage
            exam.set_exam_variables();
        }

        exam.scheduler.job(() => exam.chooseQuestionSubset());            //choose questions to use
        exam.scheduler.job(() => exam.makeQuestionList());                //create question objects

        exam.signals.on('question list initialised', function() {
            if(exam.store) {
                exam.store.init_questions();  //initialise question storage
                exam.store.save();            //make sure data get saved to LMS
            }
        });

        var ready_signals = ['question list initialised'];
        if(exam.settings.navigateMode == 'diagnostic') {
            ready_signals.push('diagnostic controller initialised');
        }
        exam.signals.on(ready_signals, function() {
            exam.scheduler.job(function() {
                exam.calculateScore();
                exam.signals.trigger('ready');
            });
        });

        exam.signals.on(['ready', 'display question list initialised'], function() {
            exam.signals.trigger('display ready');
        });
    },
    /** Restore previously started exam from storage.
     *
     * @fires Numbas.Exam#ready
     * @listens Numbas.Exam#question_list_initialised
     */
    load: function() {
        var exam = this;
        if(!this.store) {
            return;
        }
        this.loading = true;
        var suspendData = this.store.load(this);    //get saved info from storage
        exam.seed = suspendData.randomSeed || exam.seed;
        exam.scheduler.job(() => exam.set_exam_variables());
        exam.scheduler.job(() => {
            var numQuestions = 0;
            if(suspendData.questionGroupOrder) {
                exam.questionGroupOrder = suspendData.questionGroupOrder.slice();
            } else {
                exam.questionGroupOrder = Numbas.math.range(exam.question_groups.length);
            }
            exam.questionGroupOrder.forEach(function(defined, displayed) {
                var subset = suspendData.questionSubsets[displayed];
                exam.question_groups[defined].questionSubset = subset;
                numQuestions += subset.length;
            });
            exam.settings.numQuestions = numQuestions;
            exam.setStartTime(new Date(suspendData.start));
            if(suspendData.stop) {
                exam.setEndTime(new Date(suspendData.stop));
            }
            if(exam.settings.allowPause) {
                exam.timeSpent = suspendData.timeSpent;
                exam.timeRemaining = exam.settings.duration - (suspendData.duration - suspendData.timeRemaining);
            } else {
                exam.endTime = new Date(exam.start.getTime() + exam.settings.duration * 1000);
                exam.timeRemaining = (exam.endTime - new Date()) / 1000;
            }
            exam.score = suspendData.score;
            if(exam.settings.navigateMode == 'diagnostic') {
                exam.signals.on('diagnostic controller initialised', function() {
                    exam.diagnostic_controller.state = exam.scope.evaluate(suspendData.diagnostic.state);
                });
            }
        });
        exam.scheduler.job(() => this.makeQuestionList(true));
        exam.signals.on('question list initialised', function() {
            if(suspendData.currentQuestion !== undefined) {
                exam.changeQuestion(suspendData.currentQuestion);
            }
            exam.loading = false;
            exam.calculateScore();
            exam.signals.trigger('ready');
        });
    },

    /** Set exam-level variables.
     *
     */
    set_exam_variables: function() {
        this.scope.setVariable('initial_seed', Numbas.jme.wrapValue(this.seed));
        this.scope.setVariable('student_id', Numbas.jme.wrapValue(this.student_id));
    },

    /** Decide which questions to use and in what order.
     *
     * @fires Numbas.Exam#chooseQuestionSubset
     * @see Numbas.QuestionGroup#chooseQuestionSubset
     */
    chooseQuestionSubset: function() {
        var numQuestions = 0;
        var numGroups = this.question_groups.length;
        if (this.settings.shuffleQuestionGroups) {
            this.questionGroupOrder = Numbas.math.deal(numGroups);
        } else {
            this.questionGroupOrder = Numbas.math.range(numGroups);
        }
        for (var i = 0; i < numGroups; i++) {
            var groupIndex = this.questionGroupOrder[i];
            this.question_groups[groupIndex].chooseQuestionSubset();
            numQuestions += this.question_groups[groupIndex].questionSubset.length;
        }
        this.settings.numQuestions = numQuestions;
        if(numQuestions == 0) {
            throw(new Numbas.Error('exam.changeQuestion.no questions'));
        }
        this.signals.trigger('chooseQuestionSubset');
    },
    /**
     * Having chosen which questions to use, make question list and create question objects.
     *
     * If loading, need to restore randomised variables instead of generating anew.
     *
     * @param {boolean} loading
     * @fires Numbas.Exam#question_list_initialised
     * @fires Numbas.Exam#display_question_list_initialised
     * @listens Numbas.Question#ready
     * @listens Numbas.Question#mainHTMLAttached
     */
    makeQuestionList: function(loading) {
        var exam = this;
        this.questionList = [];
        this.questionAcc = 0;
        switch(this.settings.navigateMode) {
            case 'diagnostic':
                this.makeDiagnosticQuestions(loading);
                break;
            default:
                this.makeAllQuestions(loading);
        }
        exam.scheduler.job(() => {
            Promise.all(exam.questionList.map(function(q) {
                return q.signals.on(['ready'])
            })).then(function() {
                exam.settings.numQuestions = exam.questionList.length;
                if(exam.settings.navigateMode == 'diagnostic') {
                    exam.mark = 1;
                } else {
                    exam.mark = 0;
                    for(var i = 0; i < exam.settings.numQuestions; i++) {
                        exam.mark += exam.questionList[i].marks;
                    }
                }
                exam.signals.trigger('question list initialised');
            }).catch(function(e) {
                Numbas.schedule.halt(e);
            });
            exam.display && Promise.all(exam.questionList.map(function(q) {
                return q.signals.on(['ready', 'mainHTMLAttached'])
            })).then(function() {
                //register questions with exam display
                exam.display.initQuestionList();
                exam.signals.trigger('display question list initialised');
            }).catch(function(e) {
                Numbas.schedule.halt(e);
            });
        });
        if(loading) {
            exam.scheduler.job(() => this.updateScore());
        }
    },

    makeAllQuestions: function(loading) {
        var exam = this;
        var ogroups = this.question_groups.slice();
        this.question_groups = [];
        this.questionGroupOrder.forEach(function(groupIndex, i) {
            var group = ogroups[groupIndex];
            exam.question_groups[i] = group;
            group.questionList = [];
            group.questionSubset.forEach(function(n) {
                exam.scheduler.job(() => group.createQuestion(n, loading));
            });
        });
    },

    makeDiagnosticQuestions: function(loading) {
        var exam = this;
        this.question_groups.forEach(function(g) {
            g.questionList = [];
        });
        if(loading) {
            var eobj = this.store.load(this);
            eobj.questions.forEach(function(qobj, n) {
                var group = exam.question_groups[qobj.group];
                group.createQuestion(qobj.number_in_group, true)
            });
        }
    },

    /** Show the given info page.
     *
     * @param {string} page - The name of the page to show.
     */
    showInfoPage: function(page) {
        this.display && this.display.showInfoPage(page);
        this.events.trigger('showInfoPage', page);
    },

    /**
     * Show the question menu.
     *
     * @fires Numbas.Exam#event:showInfoPage
     */
    showMenu: function() {
        if(this.currentQuestion && this.currentQuestion.leavingDirtyQuestion()) {
            return;
        }
        this.currentQuestion = undefined;
        this.showInfoPage('menu');
    },

    /** Accept the given password to begin the exam?
     *
     * @param {string} password
     * @returns {boolean}
     */
    acceptPassword: function(password) {
        password = password.trim().toLowerCase();
        var startPassword = this.settings.startPassword.trim().toLowerCase();
        return this.settings.password == '' || password == startPassword;
    },


    /** Record the exam start time.
     *
     * @param {Date} start
     */
    setStartTime: function(start) {
        this.start = start;
        this.display && this.display.setStartTime(this.start);
    },

    /** Record the exam end time.
     *
     * @param {Date} stop
     */
    setEndTime: function(stop) {
        this.stop = stop;
        this.display && this.display.setEndTime(this.stop);
    },

    /**
     * Begin the exam - start timing, go to the first question.
     *
     * @fires Numbas.Exam#begin
     */
    begin: function() {
        this.setStartTime(new Date());
        this.endTime = new Date(this.start.getTime() + this.settings.duration * 1000);    //work out when the exam should end
        this.timeRemaining = this.settings.duration;
        this.updateScore();                //initialise score
        //set countdown going
        if(this.mode != 'review') {
            this.startTiming();
        }

        switch(this.settings.navigateMode) {
            case 'sequence':
                this.changeQuestion(0);            //start at the first question!
                this.events.trigger('showQuestion');
                this.display && this.display.showQuestion();    //display the current question
                break;
            case 'menu':
                this.showInfoPage('menu');
                break;
            case 'diagnostic':
                var question = this.diagnostic_controller.first_question();
                this.next_diagnostic_question(question);
                break;
        }
        this.signals.trigger('begin');
    },
    /**
     * Pause the exam, and show the `suspend` page.
     *
     * @fires Numbas.Exam#event:pause
     * @fires Numbas.Exam#event:showInfoPage
     */
    pause: function() {
        this.endTiming();
        this.showInfoPage('paused');
        this.store && this.store.pause();
        this.events.trigger('pause');
    },
    /**
     * Resume the exam.
     *
     * @fires Numbas.Exam#event:resume
     * @fires Numbas.Exam#event:showInfoPage
     */
    resume: function() {
        this.startTiming();
        if(this.display) {
            if(this.currentQuestion) {
                this.display.showQuestion();
                this.events.trigger('showQuestion');
            } else if(this.settings.navigateMode == 'menu') {
                this.showInfoPage('menu');
            }
        }
        this.events.trigger('resume');
    },
    /**
     * Set the stopwatch going.
     *
     * @fires Numbas.Exam#event:startTiming
     * @fires Numbas.Exam#event:hideTiming
     * @fires Numbas.Exam#event:showTiming
     */
    startTiming: function() {
        this.inProgress = true;
        this.stopwatch = {
            start: new Date(),
            end: new Date((new Date()).getTime() + this.timeRemaining * 1000),
            oldTimeSpent: this.timeSpent,
            id: setInterval(function() {
                exam.countDown();
            }, 1000)
        };
        if(this.settings.duration > 0) {
            this.display && this.display.showTiming();
            this.events.trigger('showTiming');
        } else {
            this.display && this.display.hideTiming();
            this.events.trigger('hideTiming');
        }
        var exam = this;
        this.events.trigger('startTiming');
        this.countDown();
    },
    /**
     * Calculate time remaining and end the exam when timer reaches zero.
     *
     * @fires Numbas.Exam#event:countDown
     * @fires Numbas.Exam#event:alert
     */
    countDown: function() {
        var t = new Date();
        this.timeSpent = this.stopwatch.oldTimeSpent + (t - this.stopwatch.start) / 1000;
        if(this.settings.duration > 0) {
            this.timeRemaining = Math.ceil((this.stopwatch.end - t) / 1000);
            this.display && this.display.showTiming();
            this.events.trigger('showTiming');
            let e;
            if(this.settings.duration > 300 && this.timeRemaining < 300 && !this.showedTimeWarning) {
                this.showedTimeWarning = true;
                e = this.settings.timerEvents['timedwarning'];
                if(e && e.action == 'warn') {
                    this.display && this.display.root_element.showAlert(e.message);
                    this.events.trigger('alert', e.message);
                }
            } else if(this.timeRemaining <= 0) {
                e = this.settings.timerEvents['timeout'];
                if(e && e.action == 'warn') {
                    this.display && this.display.root_element.showAlert(e.message);
                    this.events.trigger('alert', e.message);
                }
                this.end(true);
            }
        }
        this.events.trigger('countDown', this.timeRemaining);
    },
    /**
     * Stop the stopwatch.
     *
     * @fires Numbas.Exam#event:endTiming
     */
    endTiming: function() {
        this.inProgress = false;
        clearInterval(this.stopwatch.id);
        this.events.trigger('endTiming');
    },

    /**
     * Get any duration extension from the storage.
     */
    updateDurationExtension: function() {
        if(!this.store) {
            return;
        }
        var data = this.store.getDurationExtension();
        if(data) {
            if(data.disabled) {
                this.changeDuration(0);
                return;
            }
            var extension = 0;
            switch(data.units) {
                case 'minutes':
                    extension = parseFloat(data.amount) * 60;
                    break;
                case 'percent':
                    extension = parseFloat(data.amount) / 100 * this.settings.initial_duration;
                    break;
            }
            if(!isNaN(extension)) {
                this.changeDuration(this.settings.initial_duration + extension);
            }
        }
    },

    /**
     * Set the duration of the exam.
     *
     * @param {number} duration
     */
    changeDuration: function(duration) {
        var diff = duration - this.settings.duration;
        this.settings.duration = duration;

        if(diff != 0) {
            if(this.settings.duration > 0) {
                this.events.trigger('showTiming');
            } else {
                this.events.trigger('hideTiming');
            }
        }

        this.timeRemaining += diff;
        if(this.stopwatch) {
            this.stopwatch.end = new Date(this.stopwatch.end.getTime() + diff * 1000);
        }
        this.updateDisplayDuration();
    },

    /**
     * Update the timing display.
     *
     * @fires Numbas.Exam#event:updateDisplayDuration
     */
    updateDisplayDuration: function() {
        var duration = this.settings.duration;
        this.events.trigger('updateDisplayDuration', duration);
        this.display && (duration > 0 ? this.display.showTiming() : this.display.hideTiming());
        this.events.trigger('showTiming');
    },


    /** Recalculate and display the student's total score.
     *
     * @fires Numbas.Exam#event:updateScore
     * @see Numbas.Exam#calculateScore
     */
    updateScore: function() {
        this.calculateScore();
        this.display && this.display.showScore();
        this.store && this.store.saveExam(this);
        this.events.trigger('updateScore');
    },

    /**
     * Calculate the student's score.
     *
     * @fires Numbas.Exam#event:calculateScore
     */
    calculateScore: function() {
        this.score = 0;
        switch(this.settings.navigateMode) {
            case 'sequence':
            case 'menu':
                for(let i = 0; i < this.questionList.length; i++) {
                    this.score += this.questionList[i].score;
                }
                this.percentScore = this.mark > 0 ? Math.floor(100 * this.score / this.mark) : 0;
                break;

            case 'diagnostic':
                if(this.diagnostic_controller) {
                    this.diagnostic_progress = this.diagnostic_controller.progress();
                    this.diagnostic_feedback = this.diagnostic_controller.feedback();
                    var credit = this.diagnostic_progress.at(-1)?.credit || 0;
                    this.score = credit * this.mark;
                    this.percentScore = Math.floor(100 * credit);
                }
                break;
        }
        this.events.trigger('calculateScore');
    },
    /**
     * Call this when student wants to move between questions.
     *
     * Will check move is allowed and if so change question and update display.
     *
     * @param {number} i - Number of the question to move to
     * @fires Numbas.Exam#event:tryChangeQuestion
     * @fires Numbas.Exam#event:showDiagnosticActions
     * @see Numbas.Exam#changeQuestion
     */
    tryChangeQuestion: function(i) {
        this.events.trigger('tryChangeQuestion', i);
        switch(this.settings.navigateMode) {
            case 'sequence':
                if(! (
                       this.mode == 'review'
                    || this.settings.navigateBrowse     // is browse navigation enabled?
                    || (this.questionList[i].visited && this.settings.navigateReverse)    // if not, we can still move backwards to questions already seen if reverse navigation is enabled
                    || (i > this.currentQuestion.number && this.questionList[i - 1].visited)    // or you can always move to the next question
                )) {
                    return;
                }
                break;
        }

        var exam = this;
        /** Change the question.
         */
        function go() {
            switch(exam.settings.navigateMode) {
                case 'diagnostic':
                    var res = exam.diagnostic_actions();
                    if(res.actions.length == 1) {
                        exam.do_diagnostic_action(res.actions[0]);
                    } else if(res.actions.length == 0) {
                        exam.end(true);
                    } else {
                        exam.display && exam.display.showDiagnosticActions();
                        exam.events.trigger('showDiagnosticActions');
                    }
                    break;
                default:
                    if(i < 0 || i >= exam.settings.numQuestions) {
                        return;
                    }
                    exam.changeQuestion(i);
                    exam.display && exam.display.showQuestion();
                    exam.events.trigger('showQuestion');
            }
        }
        var currentQuestion = this.currentQuestion;
        if(!currentQuestion) {
            go();
            return;
        }
        if(i == currentQuestion.number) {
            return;
        }
        if(currentQuestion.leavingDirtyQuestion()) {
        } else if(this.mode == 'review' || currentQuestion.answered || currentQuestion.revealed || currentQuestion.marks == 0) {
            go();
        } else {
            var eventObj = this.settings.navigationEvents.onleave;
            switch(eventObj.action) {
                case 'none':
                    go();
                    break;
                case 'warnifunattempted':
                    if(this.display) {
                        this.display.root_element.showConfirm(eventObj.message + '<p>' + R('control.proceed anyway') + '</p>', go);
                    } else {
                        go();
                    }
                    break;
                case 'preventifunattempted':
                    this.display && this.display.root_element.showAlert(eventObj.message);
                    this.events.trigger('alert', eventObj.message);
                    break;
            }
        }
    },
    /**
     * Change the current question. Student's can't trigger this without going through {@link Numbas.Exam#tryChangeQuestion}.
     *
     * @param {number} i - Number of the question to move to
     * @fires Numbas.Exam#event:changeQuestion
     */
    changeQuestion: function(i) {
        if(this.currentQuestion) {
            this.currentQuestion.leave();
        }
        this.currentQuestion = this.questionList[i];
        if(!this.currentQuestion) {
            throw(new Numbas.Error('exam.changeQuestion.no questions'));
        }
        this.currentQuestion.visited = true;
        this.events.trigger('changeQuestion', i);
        this.store && this.store.changeQuestion(this.currentQuestion);
    },
    /**
     * Show a question in review mode.
     *
     * @param {number} i - Number of the question to show
     * @fires Numbas.Exam#event:reviewQuestion
     */
    reviewQuestion: function(i) {
        this.changeQuestion(i);
        this.display && this.display.showQuestion();
        this.events.trigger('reviewQuestion', i);
    },
    /**
     * Regenerate the current question.
     *
     * @fires Numbas.Exam#event:startRegen
     * @fires Numbas.Exam#event:endRegen
     * @listens Numbas.Question#ready
     * @listens Numbas.Question#mainHTMLAttached
     * @returns {Promise} - Resolves when the new question is ready.
     */
    regenQuestion: function() {
        var e = this;
        var oq = e.currentQuestion;
        var n = oq.number;
        var group = oq.group
        var n_in_group = group.questionList.indexOf(oq);
        e.events.trigger('startRegen');
        e.display && e.display.startRegen();
        var q;
        if(this.xml) {
            q = Numbas.createQuestionFromXML(oq.originalXML, oq.number, e, oq.group, e.scope, e.store);
        } else if(this.json) {
            q = Numbas.createQuestionFromJSON(oq.json, oq.number, e, oq.group, e.scope, e.store);
        }
        q.generateVariables();
        q.signals.on(['ready', 'mainHTMLAttached'], function() {
            e.currentQuestion.display.init();
            if(e.display) {
                e.display.showQuestion();
                e.events.trigger('showQuestion');
                e.display.endRegen();
            }
        });
        return q.signals.on('ready', function() {
            e.questionList[n] = group.questionList[n_in_group] = q;
            e.changeQuestion(n);
            e.updateScore();
            e.events.trigger('endRegen', oq, q);
        });
    },
    /**
     * Try to end the exam - shows confirmation dialog, and checks that all answers have been submitted.
     *
     * @fires Numbas.Exam#event:tryEnd
     * @see Numbas.Exam#end
     */
    tryEnd: function() {
        this.events.trigger('tryEnd');
        var exam = this;
        var message = R('control.confirm end');
        var answeredAll = true;
        var submittedAll = true;
        for(let i = 0;i < this.questionList.length;i++) {
            if(!this.questionList[i].answered) {
                answeredAll = false;
                break;
            }
            if(this.questionList[i].isDirty()) {
                submittedAll = false;
            }
        }
        if(this.currentQuestion && this.currentQuestion.leavingDirtyQuestion()) {
            return;
        }
        if(!answeredAll) {
            message = R('control.not all questions answered') + '<br/>' + message;
        } else if(!submittedAll) {
            message = R('control.not all questions submitted') + '<br/>' + message;
        }
        if(this.display) {
            if (exam.settings.typeendtoleave) {
                this.display.root_element.showConfirmEndExam(
                    message,
                    function() {
                        exam.end(true);
                    }
                );
            } else {
                this.display.root_element.showConfirm(
                    message,
                    function() {
                        exam.end(true);
                    }
                );
            }
        } else {
            exam.end(true);
        }
    },
    /**
     * End the exam. The student can't directly trigger this without going through {@link Numbas.Exam#tryEnd}.
     *
     * @param {boolean} save - should the end time be saved? See {@link Numbas.storage.BlankStorage#end}
     * @fires Numbas.Exam#event:end
     * @fires Numbas.Exam#event:showInfoPage
     */
    end: function(save) {
        this.mode = 'review';
        switch(this.settings.navigateMode) {
            case 'diagnostic':
                if(save) {
                    this.diagnostic_controller.after_exam_ended();
                }
                this.feedbackMessage = this.diagnostic_controller.feedback();
                break;
            default:
                //work out summary info
                this.passed = (this.percentScore >= this.settings.percentPass * 100);
                this.result = R(this.passed ? 'exam.passed' : 'exam.failed')
                var percentScore = this.mark > 0 ? 100 * this.score / this.mark : 0;
                this.feedbackMessage = null;
                for(let i = 0;i < this.feedbackMessages.length;i++) {
                    if(percentScore >= this.feedbackMessages[i].threshold) {
                        this.feedbackMessage = this.feedbackMessages[i].message;
                    } else {
                        break;
                    }
                }
        }
        if(save) {
            //get time of finish
            this.setEndTime(new Date());
            //stop the stopwatch
            this.endTiming();
            //send result to LMS, and tell it we're finished
            this.store && this.store.end();
        }
        this.display && this.display.end();

        //display the results

        var revealAnswers = this.settings.enterReviewModeImmediately || (this.entry == 'review' && this.store.reviewModeAllowed()) || Numbas.is_instructor;

        for(let i = 0;i < this.questionList.length;i++) {
            this.questionList[i].lock();
        }

        if(revealAnswers) {
            this.revealAnswers();
        }

        this.events.trigger('end', save);
        this.showInfoPage('result');
    },
    /** Reveal the answers to every question in the exam.
     *
     * @fires Numbas.Exam#event:revealAnswers
     */
    revealAnswers: function() {
        this.revealed = true;
        for(let i = 0;i < this.questionList.length;i++) {
            this.questionList[i].revealAnswer(true);
        }
        this.events.trigger('revealAnswers');
        this.display && this.display.revealAnswers();
    },

    /** Get the prompt text and list of action options when the student asks to move on.
     *
     * @returns {object}
     */
    diagnostic_actions: function() {
        return this.diagnostic_controller.next_actions();
    },

    do_diagnostic_action: function(action) {
        this.diagnostic_controller.state = action.state;
        this.next_diagnostic_question(action.next_topic);
    },

    /** Show the next question, drawn from the given topic.
     *
     * @param {object} data
     * @fires Numbas.Exam#event:initQuestion
     * @fires Numbas.Exam#event:showQuestion
     */
    next_diagnostic_question: function(data) {
        if(data === null) {
            this.end(true);
            return;
        }
        var topic_name = data.topic;
        var question_number = data.number;
        var exam = this;
        if(topic_name === null) {
            this.end(true);
        } else {
            var group = this.question_groups.find(function(g) {
                return g.settings.name == topic_name;
            });
            var question = group.createQuestion(question_number);
            question.signals.on(['ready']).then(function() {
                if(exam.store) {
                    exam.store.initQuestion(question);
                }
                exam.changeQuestion(question.number);
                exam.updateScore();
                exam.events.trigger('initQuestion', question);
            }).catch(function(e) {
                Numbas.schedule.halt(e);
            });
            question.signals.on(['ready', 'mainHTMLAttached']).then(function() {
                exam.display && exam.display.showQuestion();
                exam.events.trigger('showQuestion');
            }).catch(function(e) {
                Numbas.schedule.halt(e);
            });
        }
    },
};
/** Represents what should happen when a particular timing or navigation event happens.
 *
 * @class
 * @memberof Numbas
 */
function ExamEvent() {}
ExamEvent.prototype = /** @lends Numbas.ExamEvent.prototype */ {
    /** Name of the event this corresponds to.
     *
     * Navigation events:
     * - `onleave` - The student tries to move to another question without answering the current one.
     *
     * (There used to be more, but now they're all the same one)
     *
     * Timer events:
     * - `timedwarning` - Five minutes until the exam ends.
     * - `timeout` - There's no time left; the exam is over.
     *
     * @memberof Numbas.ExamEvent
     * @instance
     * @type {string}
     */
    type: '',
    /** Action to take when the event happens.
     *
     * Choices for timer events:
     * - `none` - Don't do anything.
     * - `warn` - Show a message.
     *
     * Choices for navigation events:
     * - `none` - just allow the navigation
     * - `warnifunattempted` - Show a warning but allow the student to continue.
     * - `preventifunattempted` - Show a warning but allow the student to continue.
     *
     * @memberof Numbas.ExamEvent
     * @instance
     * @type {string}
     */
    action: 'none',
    /** Message to show the student when the event happens.
     *
     * @memberof Numbas.ExamEvent
     * @instance
     * @type {string}
     */
    message: ''
};
ExamEvent.createFromXML = function(eventNode) {
    var e = new ExamEvent();
    var tryGetAttribute = Numbas.xml.tryGetAttribute;
    tryGetAttribute(e, null, eventNode, ['type', 'action']);
    e.message = Numbas.xml.serializeMessage(eventNode);
    return e;
}
ExamEvent.createFromJSON = function(type, data) {
    var e = new ExamEvent();
    e.type = type;
    if(data) {
        e.action = data.action;
        e.message = data.message;
    }
    return e;
}


/** Represents a group of questions.
 *
 * @class
 * @param {Numbas.Exam} exam - The exam this group belongs to.
 * @param {number} number - The index of this group in the list of groups.
 * @property {Numbas.Exam} exam - The exam this group belongs to.
 * @property {Element} xml - The XML defining the group.
 * @property {object} json - The JSON object defining the group.
 * @property {Array.<number>} questionSubset - The indices of the picked questions, in the order they should appear to the student.
 * @property {Array.<Numbas.Question>} questionList - The questions in this group.
 * @memberof Numbas
 */
function QuestionGroup(exam, number) {
    this.exam = exam;
    this.number = number;
    this.settings = util.copyobj(this.settings);
}
QuestionGroup.prototype = {
    /** Load this question group's settings from the given XML <question_group> node.
     *
     * @param {Element} xml
     */
    loadFromXML: function(xml) {
        this.xml = xml;
        Numbas.xml.tryGetAttribute(this.settings, this.xml, '.', ['name', 'pickingStrategy', 'pickQuestions']);
        this.questionNodes = this.xml.selectNodes('questions/question');
        this.numQuestions = this.questionNodes.length;
    },
    /** Load this question group's settings from the given JSON dictionary.
     *
     * @param {object} data
     */
    loadFromJSON: function(data) {
        this.json = data;
        Numbas.json.tryLoad(data, ['name', 'pickingStrategy', 'pickQuestions'], this.settings);
        if('variable_overrides' in data) {
            for(let i = 0;i < data.variable_overrides.length;i++) {
                var vos = data.variable_overrides[i];
                var qd = data.questions[i];
                if('variables' in qd) {
                    vos.forEach(function(vo) {
                        var v = Object.values(qd.variables).find(function(v) {
                            return v.name == vo.name;
                        });
                        if(v) {
                            v.definition = vo.definition;
                        }
                    });
                }
            }
        }
        this.numQuestions = data.questions.length;
    },
    /** Settings for this group.
     *
     * @property {string} name - The group's name.
     * @property {string} pickingStrategy - How to pick the list of questions: 'all-ordered', 'all-shuffled' or 'random-subset'.
     * @property {number} pickQuestions - If `pickingStrategy` is 'random-subset', how many questions to pick.
     */
    settings: {
        name: '',
        pickingStrategy: 'all-ordered',
        pickQuestions: 1
    },
    /** Decide which questions to use and in what order. */
    chooseQuestionSubset: function() {
        switch(this.settings.pickingStrategy) {
            case 'all-ordered':
                this.questionSubset = Numbas.math.range(this.numQuestions);
                break;
            case 'all-shuffled':
                this.questionSubset = Numbas.math.deal(this.numQuestions);
                break;
            case 'random-subset':
                this.questionSubset = Numbas.math.deal(this.numQuestions).slice(0, this.settings.pickQuestions);
                break;
        }
    },
    /**
     * Create a question in this group.
     *
     * @param {number} n - The index of the question in the definitions.
     * @param {boolean} loading - Is the question being resumed?
     * @fires Numbas.Exam#event:createQuestion
     * @returns {Numbas.Question} question
     */
    createQuestion: function(n, loading) {
        var exam = this.exam;
        var question;
        if(this.xml) {
            question = Numbas.createQuestionFromXML(this.questionNodes[n], exam.questionAcc++, exam, this, exam.scope, exam.store, loading);
        } else if(this.json) {
            question = Numbas.createQuestionFromJSON(this.json.questions[n], exam.questionAcc++, exam, this, exam.scope, exam.store, loading);
        }
        question.number_in_group = n;
        if(loading) {
            question.resume();
        } else {
            question.generateVariables();
            question.signals.on('finalisedLoad', function() {
                question.signals.trigger('ready');
            });
        }
        exam.questionList.push(question);
        this.questionList.push(question);
        exam.display && exam.display.addQuestion(question);
        exam.events.trigger('createQuestion', question);
        return question;
    }
}
});
;
Numbas.queueScript('i18next',[],function(module) {
    var exports = module.exports;
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).i18next=t()}(this,function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function n(e){for(var n=1;n<arguments.length;n++){var o=null!=arguments[n]?Object(arguments[n]):{},i=Object.keys(o);"function"==typeof Object.getOwnPropertySymbols&&(i=i.concat(Object.getOwnPropertySymbols(o).filter(function(e){return Object.getOwnPropertyDescriptor(o,e).enumerable}))),i.forEach(function(n){t(e,n,o[n])})}return e}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}function r(e,t,n){return t&&i(e.prototype,t),n&&i(e,n),e}function a(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function s(t,n){return!n||"object"!==e(n)&&"function"!=typeof n?a(t):n}function u(e){return(u=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}function l(e,t){return(l=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function c(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&l(e,t)}var p={type:"logger",log:function(e){this.output("log",e)},warn:function(e){this.output("warn",e)},error:function(e){this.output("error",e)},output:function(e,t){console&&console[e]&&console[e].apply(console,t)}},g=new(function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};o(this,e),this.init(t,n)}return r(e,[{key:"init",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.prefix=t.prefix||"i18next:",this.logger=e||p,this.options=t,this.debug=t.debug}},{key:"setDebug",value:function(e){this.debug=e}},{key:"log",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return this.forward(t,"log","",!0)}},{key:"warn",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return this.forward(t,"warn","",!0)}},{key:"error",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return this.forward(t,"error","")}},{key:"deprecate",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return this.forward(t,"warn","WARNING DEPRECATED: ",!0)}},{key:"forward",value:function(e,t,n,o){return o&&!this.debug?null:("string"==typeof e[0]&&(e[0]="".concat(n).concat(this.prefix," ").concat(e[0])),this.logger[t](e))}},{key:"create",value:function(t){return new e(this.logger,n({},{prefix:"".concat(this.prefix,":").concat(t,":")},this.options))}}]),e}()),f=function(){function e(){o(this,e),this.observers={}}return r(e,[{key:"on",value:function(e,t){var n=this;return e.split(" ").forEach(function(e){n.observers[e]=n.observers[e]||[],n.observers[e].push(t)}),this}},{key:"off",value:function(e,t){this.observers[e]&&(t?this.observers[e]=this.observers[e].filter(function(e){return e!==t}):delete this.observers[e])}},{key:"emit",value:function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];this.observers[e]&&[].concat(this.observers[e]).forEach(function(e){e.apply(void 0,n)});this.observers["*"]&&[].concat(this.observers["*"]).forEach(function(t){t.apply(t,[e].concat(n))})}}]),e}();function h(){var e,t,n=new Promise(function(n,o){e=n,t=o});return n.resolve=e,n.reject=t,n}function d(e){return null==e?"":""+e}function v(e,t,n){function o(e){return e&&e.indexOf("###")>-1?e.replace(/###/g,"."):e}function i(){return!e||"string"==typeof e}for(var r="string"!=typeof t?[].concat(t):t.split(".");r.length>1;){if(i())return{};var a=o(r.shift());!e[a]&&n&&(e[a]=new n),e=e[a]}return i()?{}:{obj:e,k:o(r.shift())}}function y(e,t,n){var o=v(e,t,Object);o.obj[o.k]=n}function m(e,t){var n=v(e,t),o=n.obj,i=n.k;if(o)return o[i]}function b(e,t,n){var o=m(e,n);return void 0!==o?o:m(t,n)}function k(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}var x={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;"};function w(e){return"string"==typeof e?e.replace(/[&<>"'\/]/g,function(e){return x[e]}):e}var S="undefined"!=typeof window&&window.navigator&&window.navigator.userAgent&&window.navigator.userAgent.indexOf("MSIE")>-1,L=function(e){function t(e){var n,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{ns:["translation"],defaultNS:"translation"};return o(this,t),n=s(this,u(t).call(this)),S&&f.call(a(n)),n.data=e||{},n.options=i,void 0===n.options.keySeparator&&(n.options.keySeparator="."),n}return c(t,f),r(t,[{key:"addNamespaces",value:function(e){this.options.ns.indexOf(e)<0&&this.options.ns.push(e)}},{key:"removeNamespaces",value:function(e){var t=this.options.ns.indexOf(e);t>-1&&this.options.ns.splice(t,1)}},{key:"getResource",value:function(e,t,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},i=void 0!==o.keySeparator?o.keySeparator:this.options.keySeparator,r=[e,t];return n&&"string"!=typeof n&&(r=r.concat(n)),n&&"string"==typeof n&&(r=r.concat(i?n.split(i):n)),e.indexOf(".")>-1&&(r=e.split(".")),m(this.data,r)}},{key:"addResource",value:function(e,t,n,o){var i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{silent:!1},r=this.options.keySeparator;void 0===r&&(r=".");var a=[e,t];n&&(a=a.concat(r?n.split(r):n)),e.indexOf(".")>-1&&(o=t,t=(a=e.split("."))[1]),this.addNamespaces(t),y(this.data,a,o),i.silent||this.emit("added",e,t,n,o)}},{key:"addResources",value:function(e,t,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{silent:!1};for(var i in n)"string"!=typeof n[i]&&"[object Array]"!==Object.prototype.toString.apply(n[i])||this.addResource(e,t,i,n[i],{silent:!0});o.silent||this.emit("added",e,t,n)}},{key:"addResourceBundle",value:function(e,t,o,i,r){var a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{silent:!1},s=[e,t];e.indexOf(".")>-1&&(i=o,o=t,t=(s=e.split("."))[1]),this.addNamespaces(t);var u=m(this.data,s)||{};i?function e(t,n,o){for(var i in n)"__proto__"!==i&&(i in t?"string"==typeof t[i]||t[i]instanceof String||"string"==typeof n[i]||n[i]instanceof String?o&&(t[i]=n[i]):e(t[i],n[i],o):t[i]=n[i]);return t}(u,o,r):u=n({},u,o),y(this.data,s,u),a.silent||this.emit("added",e,t,o)}},{key:"removeResourceBundle",value:function(e,t){this.hasResourceBundle(e,t)&&delete this.data[e][t],this.removeNamespaces(t),this.emit("removed",e,t)}},{key:"hasResourceBundle",value:function(e,t){return void 0!==this.getResource(e,t)}},{key:"getResourceBundle",value:function(e,t){return t||(t=this.options.defaultNS),"v1"===this.options.compatibilityAPI?n({},{},this.getResource(e,t)):this.getResource(e,t)}},{key:"getDataByLanguage",value:function(e){return this.data[e]}},{key:"toJSON",value:function(){return this.data}}]),t}(),O={processors:{},addPostProcessor:function(e){this.processors[e.name]=e},handle:function(e,t,n,o,i){var r=this;return e.forEach(function(e){r.processors[e]&&(t=r.processors[e].process(t,n,o,i))}),t}},R={},C=function(t){function i(e){var t,n,r,l,c=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return o(this,i),t=s(this,u(i).call(this)),S&&f.call(a(t)),n=["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],r=e,l=a(t),n.forEach(function(e){r[e]&&(l[e]=r[e])}),t.options=c,void 0===t.options.keySeparator&&(t.options.keySeparator="."),t.logger=g.create("translator"),t}return c(i,f),r(i,[{key:"changeLanguage",value:function(e){e&&(this.language=e)}},{key:"exists",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}},n=this.resolve(e,t);return n&&void 0!==n.res}},{key:"extractFromKey",value:function(e,t){var n=void 0!==t.nsSeparator?t.nsSeparator:this.options.nsSeparator;void 0===n&&(n=":");var o=void 0!==t.keySeparator?t.keySeparator:this.options.keySeparator,i=t.ns||this.options.defaultNS;if(n&&e.indexOf(n)>-1){var r=e.match(this.interpolator.nestingRegexp);if(r&&r.length>0)return{key:e,namespaces:i};var a=e.split(n);(n!==o||n===o&&this.options.ns.indexOf(a[0])>-1)&&(i=a.shift()),e=a.join(o)}return"string"==typeof i&&(i=[i]),{key:e,namespaces:i}}},{key:"translate",value:function(t,o,i){var r=this;if("object"!==e(o)&&this.options.overloadTranslationOptionHandler&&(o=this.options.overloadTranslationOptionHandler(arguments)),o||(o={}),null==t)return"";Array.isArray(t)||(t=[String(t)]);var a=void 0!==o.keySeparator?o.keySeparator:this.options.keySeparator,s=this.extractFromKey(t[t.length-1],o),u=s.key,l=s.namespaces,c=l[l.length-1],p=o.lng||this.language,g=o.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if(p&&"cimode"===p.toLowerCase()){if(g){var f=o.nsSeparator||this.options.nsSeparator;return c+f+u}return u}var h=this.resolve(t,o),d=h&&h.res,v=h&&h.usedKey||u,y=h&&h.exactUsedKey||u,m=Object.prototype.toString.apply(d),b=void 0!==o.joinArrays?o.joinArrays:this.options.joinArrays,k=!this.i18nFormat||this.i18nFormat.handleAsObject;if(k&&d&&("string"!=typeof d&&"boolean"!=typeof d&&"number"!=typeof d)&&["[object Number]","[object Function]","[object RegExp]"].indexOf(m)<0&&("string"!=typeof b||"[object Array]"!==m)){if(!o.returnObjects&&!this.options.returnObjects)return this.logger.warn("accessing an object - but returnObjects options is not enabled!"),this.options.returnedObjectHandler?this.options.returnedObjectHandler(v,d,o):"key '".concat(u," (").concat(this.language,")' returned an object instead of string.");if(a){var x="[object Array]"===m,w=x?[]:{},S=x?y:v;for(var L in d)if(Object.prototype.hasOwnProperty.call(d,L)){var O="".concat(S).concat(a).concat(L);w[L]=this.translate(O,n({},o,{joinArrays:!1,ns:l})),w[L]===O&&(w[L]=d[L])}d=w}}else if(k&&"string"==typeof b&&"[object Array]"===m)(d=d.join(b))&&(d=this.extendTranslation(d,t,o,i));else{var R=!1,C=!1;if(!this.isValidLookup(d)&&void 0!==o.defaultValue){if(R=!0,void 0!==o.count){var N=this.pluralResolver.getSuffix(p,o.count);d=o["defaultValue".concat(N)]}d||(d=o.defaultValue)}this.isValidLookup(d)||(C=!0,d=u);var j=o.defaultValue&&o.defaultValue!==d&&this.options.updateMissing;if(C||R||j){if(this.logger.log(j?"updateKey":"missingKey",p,c,u,j?o.defaultValue:d),a){var E=this.resolve(u,n({},o,{keySeparator:!1}));E&&E.res&&this.logger.warn("Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.")}var P=[],F=this.languageUtils.getFallbackCodes(this.options.fallbackLng,o.lng||this.language);if("fallback"===this.options.saveMissingTo&&F&&F[0])for(var V=0;V<F.length;V++)P.push(F[V]);else"all"===this.options.saveMissingTo?P=this.languageUtils.toResolveHierarchy(o.lng||this.language):P.push(o.lng||this.language);var T=function(e,t){r.options.missingKeyHandler?r.options.missingKeyHandler(e,c,t,j?o.defaultValue:d,j,o):r.backendConnector&&r.backendConnector.saveMissing&&r.backendConnector.saveMissing(e,c,t,j?o.defaultValue:d,j,o),r.emit("missingKey",e,c,t,d)};if(this.options.saveMissing){var A=void 0!==o.count&&"string"!=typeof o.count;this.options.saveMissingPlurals&&A?P.forEach(function(e){r.pluralResolver.getPluralFormsOfKey(e,u).forEach(function(t){return T([e],t)})}):T(P,u)}}d=this.extendTranslation(d,t,o,h,i),C&&d===u&&this.options.appendNamespaceToMissingKey&&(d="".concat(c,":").concat(u)),C&&this.options.parseMissingKeyHandler&&(d=this.options.parseMissingKeyHandler(d))}return d}},{key:"extendTranslation",value:function(e,t,o,i,r){var a=this;if(this.i18nFormat&&this.i18nFormat.parse)e=this.i18nFormat.parse(e,o,i.usedLng,i.usedNS,i.usedKey,{resolved:i});else if(!o.skipInterpolation){o.interpolation&&this.interpolator.init(n({},o,{interpolation:n({},this.options.interpolation,o.interpolation)}));var s,u=o.interpolation&&o.interpolation.skipOnVariables||this.options.interpolation.skipOnVariables;if(u){var l=e.match(this.interpolator.nestingRegexp);s=l&&l.length}var c=o.replace&&"string"!=typeof o.replace?o.replace:o;if(this.options.interpolation.defaultVariables&&(c=n({},this.options.interpolation.defaultVariables,c)),e=this.interpolator.interpolate(e,c,o.lng||this.language,o),u){var p=e.match(this.interpolator.nestingRegexp);s<(p&&p.length)&&(o.nest=!1)}!1!==o.nest&&(e=this.interpolator.nest(e,function(){for(var e=arguments.length,n=new Array(e),o=0;o<e;o++)n[o]=arguments[o];return r&&r[0]===n[0]?(a.logger.warn("It seems you are nesting recursively key: ".concat(n[0]," in key: ").concat(t[0])),null):a.translate.apply(a,n.concat([t]))},o)),o.interpolation&&this.interpolator.reset()}var g=o.postProcess||this.options.postProcess,f="string"==typeof g?[g]:g;return null!=e&&f&&f.length&&!1!==o.applyPostProcessor&&(e=O.handle(f,e,t,this.options&&this.options.postProcessPassResolved?n({i18nResolved:i},o):o,this)),e}},{key:"resolve",value:function(e){var t,n,o,i,r,a=this,s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return"string"==typeof e&&(e=[e]),e.forEach(function(e){if(!a.isValidLookup(t)){var u=a.extractFromKey(e,s),l=u.key;n=l;var c=u.namespaces;a.options.fallbackNS&&(c=c.concat(a.options.fallbackNS));var p=void 0!==s.count&&"string"!=typeof s.count,g=void 0!==s.context&&"string"==typeof s.context&&""!==s.context,f=s.lngs?s.lngs:a.languageUtils.toResolveHierarchy(s.lng||a.language,s.fallbackLng);c.forEach(function(e){a.isValidLookup(t)||(r=e,!R["".concat(f[0],"-").concat(e)]&&a.utils&&a.utils.hasLoadedNamespace&&!a.utils.hasLoadedNamespace(r)&&(R["".concat(f[0],"-").concat(e)]=!0,a.logger.warn('key "'.concat(n,'" for languages "').concat(f.join(", "),'" won\'t get resolved as namespace "').concat(r,'" was not yet loaded'),"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),f.forEach(function(n){if(!a.isValidLookup(t)){i=n;var r,u,c=l,f=[c];if(a.i18nFormat&&a.i18nFormat.addLookupKeys)a.i18nFormat.addLookupKeys(f,l,n,e,s);else p&&(r=a.pluralResolver.getSuffix(n,s.count)),p&&g&&f.push(c+r),g&&f.push(c+="".concat(a.options.contextSeparator).concat(s.context)),p&&f.push(c+=r);for(;u=f.pop();)a.isValidLookup(t)||(o=u,t=a.getResource(n,e,u,s))}}))})}}),{res:t,usedKey:n,exactUsedKey:o,usedLng:i,usedNS:r}}},{key:"isValidLookup",value:function(e){return!(void 0===e||!this.options.returnNull&&null===e||!this.options.returnEmptyString&&""===e)}},{key:"getResource",value:function(e,t,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return this.i18nFormat&&this.i18nFormat.getResource?this.i18nFormat.getResource(e,t,n,o):this.resourceStore.getResource(e,t,n,o)}}]),i}();function N(e){return e.charAt(0).toUpperCase()+e.slice(1)}var j=function(){function e(t){o(this,e),this.options=t,this.whitelist=this.options.supportedLngs||!1,this.supportedLngs=this.options.supportedLngs||!1,this.logger=g.create("languageUtils")}return r(e,[{key:"getScriptPartFromCode",value:function(e){if(!e||e.indexOf("-")<0)return null;var t=e.split("-");return 2===t.length?null:(t.pop(),"x"===t[t.length-1].toLowerCase()?null:this.formatLanguageCode(t.join("-")))}},{key:"getLanguagePartFromCode",value:function(e){if(!e||e.indexOf("-")<0)return e;var t=e.split("-");return this.formatLanguageCode(t[0])}},{key:"formatLanguageCode",value:function(e){if("string"==typeof e&&e.indexOf("-")>-1){var t=["hans","hant","latn","cyrl","cans","mong","arab"],n=e.split("-");return this.options.lowerCaseLng?n=n.map(function(e){return e.toLowerCase()}):2===n.length?(n[0]=n[0].toLowerCase(),n[1]=n[1].toUpperCase(),t.indexOf(n[1].toLowerCase())>-1&&(n[1]=N(n[1].toLowerCase()))):3===n.length&&(n[0]=n[0].toLowerCase(),2===n[1].length&&(n[1]=n[1].toUpperCase()),"sgn"!==n[0]&&2===n[2].length&&(n[2]=n[2].toUpperCase()),t.indexOf(n[1].toLowerCase())>-1&&(n[1]=N(n[1].toLowerCase())),t.indexOf(n[2].toLowerCase())>-1&&(n[2]=N(n[2].toLowerCase()))),n.join("-")}return this.options.cleanCode||this.options.lowerCaseLng?e.toLowerCase():e}},{key:"isWhitelisted",value:function(e){return this.logger.deprecate("languageUtils.isWhitelisted",'function "isWhitelisted" will be renamed to "isSupportedCode" in the next major - please make sure to rename it\'s usage asap.'),this.isSupportedCode(e)}},{key:"isSupportedCode",value:function(e){return("languageOnly"===this.options.load||this.options.nonExplicitSupportedLngs)&&(e=this.getLanguagePartFromCode(e)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(e)>-1}},{key:"getBestMatchFromCodes",value:function(e){var t,n=this;return e?(e.forEach(function(e){if(!t){var o=n.formatLanguageCode(e);n.options.supportedLngs&&!n.isSupportedCode(o)||(t=o)}}),!t&&this.options.supportedLngs&&e.forEach(function(e){if(!t){var o=n.getLanguagePartFromCode(e);if(n.isSupportedCode(o))return t=o;t=n.options.supportedLngs.find(function(e){if(0===e.indexOf(o))return e})}}),t||(t=this.getFallbackCodes(this.options.fallbackLng)[0]),t):null}},{key:"getFallbackCodes",value:function(e,t){if(!e)return[];if("string"==typeof e&&(e=[e]),"[object Array]"===Object.prototype.toString.apply(e))return e;if(!t)return e.default||[];var n=e[t];return n||(n=e[this.getScriptPartFromCode(t)]),n||(n=e[this.formatLanguageCode(t)]),n||(n=e[this.getLanguagePartFromCode(t)]),n||(n=e.default),n||[]}},{key:"toResolveHierarchy",value:function(e,t){var n=this,o=this.getFallbackCodes(t||this.options.fallbackLng||[],e),i=[],r=function(e){e&&(n.isSupportedCode(e)?i.push(e):n.logger.warn("rejecting language code not found in supportedLngs: ".concat(e)))};return"string"==typeof e&&e.indexOf("-")>-1?("languageOnly"!==this.options.load&&r(this.formatLanguageCode(e)),"languageOnly"!==this.options.load&&"currentOnly"!==this.options.load&&r(this.getScriptPartFromCode(e)),"currentOnly"!==this.options.load&&r(this.getLanguagePartFromCode(e))):"string"==typeof e&&r(this.formatLanguageCode(e)),o.forEach(function(e){i.indexOf(e)<0&&r(n.formatLanguageCode(e))}),i}}]),e}(),E=[{lngs:["ach","ak","am","arn","br","fil","gun","ln","mfe","mg","mi","oc","pt","pt-BR","tg","ti","tr","uz","wa"],nr:[1,2],fc:1},{lngs:["af","an","ast","az","bg","bn","ca","da","de","dev","el","en","eo","es","et","eu","fi","fo","fur","fy","gl","gu","ha","hi","hu","hy","ia","it","kn","ku","lb","mai","ml","mn","mr","nah","nap","nb","ne","nl","nn","no","nso","pa","pap","pms","ps","pt-PT","rm","sco","se","si","so","son","sq","sv","sw","ta","te","tk","ur","yo"],nr:[1,2],fc:2},{lngs:["ay","bo","cgg","fa","id","ja","jbo","ka","kk","km","ko","ky","lo","ms","sah","su","th","tt","ug","vi","wo","zh"],nr:[1],fc:3},{lngs:["be","bs","cnr","dz","hr","ru","sr","uk"],nr:[1,2,5],fc:4},{lngs:["ar"],nr:[0,1,2,3,11,100],fc:5},{lngs:["cs","sk"],nr:[1,2,5],fc:6},{lngs:["csb","pl"],nr:[1,2,5],fc:7},{lngs:["cy"],nr:[1,2,3,8],fc:8},{lngs:["fr"],nr:[1,2],fc:9},{lngs:["ga"],nr:[1,2,3,7,11],fc:10},{lngs:["gd"],nr:[1,2,3,20],fc:11},{lngs:["is"],nr:[1,2],fc:12},{lngs:["jv"],nr:[0,1],fc:13},{lngs:["kw"],nr:[1,2,3,4],fc:14},{lngs:["lt"],nr:[1,2,10],fc:15},{lngs:["lv"],nr:[1,2,0],fc:16},{lngs:["mk"],nr:[1,2],fc:17},{lngs:["mnk"],nr:[0,1,2],fc:18},{lngs:["mt"],nr:[1,2,11,20],fc:19},{lngs:["or"],nr:[2,1],fc:2},{lngs:["ro"],nr:[1,2,20],fc:20},{lngs:["sl"],nr:[5,1,2,3],fc:21},{lngs:["he"],nr:[1,2,20,21],fc:22}],P={1:function(e){return Number(e>1)},2:function(e){return Number(1!=e)},3:function(e){return 0},4:function(e){return Number(e%10==1&&e%100!=11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2)},5:function(e){return Number(0==e?0:1==e?1:2==e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5)},6:function(e){return Number(1==e?0:e>=2&&e<=4?1:2)},7:function(e){return Number(1==e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2)},8:function(e){return Number(1==e?0:2==e?1:8!=e&&11!=e?2:3)},9:function(e){return Number(e>=2)},10:function(e){return Number(1==e?0:2==e?1:e<7?2:e<11?3:4)},11:function(e){return Number(1==e||11==e?0:2==e||12==e?1:e>2&&e<20?2:3)},12:function(e){return Number(e%10!=1||e%100==11)},13:function(e){return Number(0!==e)},14:function(e){return Number(1==e?0:2==e?1:3==e?2:3)},15:function(e){return Number(e%10==1&&e%100!=11?0:e%10>=2&&(e%100<10||e%100>=20)?1:2)},16:function(e){return Number(e%10==1&&e%100!=11?0:0!==e?1:2)},17:function(e){return Number(1==e||e%10==1&&e%100!=11?0:1)},18:function(e){return Number(0==e?0:1==e?1:2)},19:function(e){return Number(1==e?0:0==e||e%100>1&&e%100<11?1:e%100>10&&e%100<20?2:3)},20:function(e){return Number(1==e?0:0==e||e%100>0&&e%100<20?1:2)},21:function(e){return Number(e%100==1?1:e%100==2?2:e%100==3||e%100==4?3:0)},22:function(e){return Number(1==e?0:2==e?1:(e<0||e>10)&&e%10==0?2:3)}};var F=function(){function e(t){var n,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};o(this,e),this.languageUtils=t,this.options=i,this.logger=g.create("pluralResolver"),this.rules=(n={},E.forEach(function(e){e.lngs.forEach(function(t){n[t]={numbers:e.nr,plurals:P[e.fc]}})}),n)}return r(e,[{key:"addRule",value:function(e,t){this.rules[e]=t}},{key:"getRule",value:function(e){return this.rules[e]||this.rules[this.languageUtils.getLanguagePartFromCode(e)]}},{key:"needsPlural",value:function(e){var t=this.getRule(e);return t&&t.numbers.length>1}},{key:"getPluralFormsOfKey",value:function(e,t){var n=this,o=[],i=this.getRule(e);return i?(i.numbers.forEach(function(i){var r=n.getSuffix(e,i);o.push("".concat(t).concat(r))}),o):o}},{key:"getSuffix",value:function(e,t){var n=this,o=this.getRule(e);if(o){var i=o.noAbs?o.plurals(t):o.plurals(Math.abs(t)),r=o.numbers[i];this.options.simplifyPluralSuffix&&2===o.numbers.length&&1===o.numbers[0]&&(2===r?r="plural":1===r&&(r=""));var a=function(){return n.options.prepend&&r.toString()?n.options.prepend+r.toString():r.toString()};return"v1"===this.options.compatibilityJSON?1===r?"":"number"==typeof r?"_plural_".concat(r.toString()):a():"v2"===this.options.compatibilityJSON?a():this.options.simplifyPluralSuffix&&2===o.numbers.length&&1===o.numbers[0]?a():this.options.prepend&&i.toString()?this.options.prepend+i.toString():i.toString()}return this.logger.warn("no plural rule found for: ".concat(e)),""}}]),e}(),V=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};o(this,e),this.logger=g.create("interpolator"),this.options=t,this.format=t.interpolation&&t.interpolation.format||function(e){return e},this.init(t)}return r(e,[{key:"init",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};e.interpolation||(e.interpolation={escapeValue:!0});var t=e.interpolation;this.escape=void 0!==t.escape?t.escape:w,this.escapeValue=void 0===t.escapeValue||t.escapeValue,this.useRawValueToEscape=void 0!==t.useRawValueToEscape&&t.useRawValueToEscape,this.prefix=t.prefix?k(t.prefix):t.prefixEscaped||"{{",this.suffix=t.suffix?k(t.suffix):t.suffixEscaped||"}}",this.formatSeparator=t.formatSeparator?t.formatSeparator:t.formatSeparator||",",this.unescapePrefix=t.unescapeSuffix?"":t.unescapePrefix||"-",this.unescapeSuffix=this.unescapePrefix?"":t.unescapeSuffix||"",this.nestingPrefix=t.nestingPrefix?k(t.nestingPrefix):t.nestingPrefixEscaped||k("$t("),this.nestingSuffix=t.nestingSuffix?k(t.nestingSuffix):t.nestingSuffixEscaped||k(")"),this.nestingOptionsSeparator=t.nestingOptionsSeparator?t.nestingOptionsSeparator:t.nestingOptionsSeparator||",",this.maxReplaces=t.maxReplaces?t.maxReplaces:1e3,this.alwaysFormat=void 0!==t.alwaysFormat&&t.alwaysFormat,this.resetRegExp()}},{key:"reset",value:function(){this.options&&this.init(this.options)}},{key:"resetRegExp",value:function(){var e="".concat(this.prefix,"(.+?)").concat(this.suffix);this.regexp=new RegExp(e,"g");var t="".concat(this.prefix).concat(this.unescapePrefix,"(.+?)").concat(this.unescapeSuffix).concat(this.suffix);this.regexpUnescape=new RegExp(t,"g");var n="".concat(this.nestingPrefix,"(.+?)").concat(this.nestingSuffix);this.nestingRegexp=new RegExp(n,"g")}},{key:"interpolate",value:function(e,t,n,o){var i,r,a,s=this,u=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{};function l(e){return e.replace(/\$/g,"$$$$")}var c=function(e){if(e.indexOf(s.formatSeparator)<0){var i=b(t,u,e);return s.alwaysFormat?s.format(i,void 0,n):i}var r=e.split(s.formatSeparator),a=r.shift().trim(),l=r.join(s.formatSeparator).trim();return s.format(b(t,u,a),l,n,o)};this.resetRegExp();var p=o&&o.missingInterpolationHandler||this.options.missingInterpolationHandler,g=o&&o.interpolation&&o.interpolation.skipOnVariables||this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:function(e){return l(e)}},{regex:this.regexp,safeValue:function(e){return s.escapeValue?l(s.escape(e)):l(e)}}].forEach(function(t){for(a=0;i=t.regex.exec(e);){if(void 0===(r=c(i[1].trim())))if("function"==typeof p){var n=p(e,i,o);r="string"==typeof n?n:""}else{if(g){r=i[0];continue}s.logger.warn("missed to pass in variable ".concat(i[1]," for interpolating ").concat(e)),r=""}else"string"==typeof r||s.useRawValueToEscape||(r=d(r));if(e=e.replace(i[0],t.safeValue(r)),t.regex.lastIndex=0,++a>=s.maxReplaces)break}}),e}},{key:"nest",value:function(e,t){var o,i,r=this,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},s=n({},a);function u(e,t){var o=this.nestingOptionsSeparator;if(e.indexOf(o)<0)return e;var i=e.split(new RegExp("".concat(o,"[ ]*{"))),r="{".concat(i[1]);e=i[0],r=(r=this.interpolate(r,s)).replace(/'/g,'"');try{s=JSON.parse(r),t&&(s=n({},t,s))}catch(t){return this.logger.warn("failed parsing options string in nesting for key ".concat(e),t),"".concat(e).concat(o).concat(r)}return delete s.defaultValue,e}for(s.applyPostProcessor=!1,delete s.defaultValue;o=this.nestingRegexp.exec(e);){var l=[],c=!1;if(o[0].includes(this.formatSeparator)&&!/{.*}/.test(o[1])){var p=o[1].split(this.formatSeparator).map(function(e){return e.trim()});o[1]=p.shift(),l=p,c=!0}if((i=t(u.call(this,o[1].trim(),s),s))&&o[0]===e&&"string"!=typeof i)return i;"string"!=typeof i&&(i=d(i)),i||(this.logger.warn("missed to resolve ".concat(o[1]," for nesting ").concat(e)),i=""),c&&(i=l.reduce(function(e,t){return r.format(e,t,a.lng,a)},i.trim())),e=e.replace(o[0],i),this.regexp.lastIndex=0}return e}}]),e}();var T=function(e){function t(e,n,i){var r,l=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return o(this,t),r=s(this,u(t).call(this)),S&&f.call(a(r)),r.backend=e,r.store=n,r.services=i,r.languageUtils=i.languageUtils,r.options=l,r.logger=g.create("backendConnector"),r.state={},r.queue=[],r.backend&&r.backend.init&&r.backend.init(i,l.backend,l),r}return c(t,f),r(t,[{key:"queueLoad",value:function(e,t,n,o){var i=this,r=[],a=[],s=[],u=[];return e.forEach(function(e){var o=!0;t.forEach(function(t){var s="".concat(e,"|").concat(t);!n.reload&&i.store.hasResourceBundle(e,t)?i.state[s]=2:i.state[s]<0||(1===i.state[s]?a.indexOf(s)<0&&a.push(s):(i.state[s]=1,o=!1,a.indexOf(s)<0&&a.push(s),r.indexOf(s)<0&&r.push(s),u.indexOf(t)<0&&u.push(t)))}),o||s.push(e)}),(r.length||a.length)&&this.queue.push({pending:a,loaded:{},errors:[],callback:o}),{toLoad:r,pending:a,toLoadLanguages:s,toLoadNamespaces:u}}},{key:"loaded",value:function(e,t,n){var o=e.split("|"),i=o[0],r=o[1];t&&this.emit("failedLoading",i,r,t),n&&this.store.addResourceBundle(i,r,n),this.state[e]=t?-1:2;var a={};this.queue.forEach(function(n){var o,s,u,l,c,p;o=n.loaded,s=r,l=v(o,[i],Object),c=l.obj,p=l.k,c[p]=c[p]||[],u&&(c[p]=c[p].concat(s)),u||c[p].push(s),function(e,t){for(var n=e.indexOf(t);-1!==n;)e.splice(n,1),n=e.indexOf(t)}(n.pending,e),t&&n.errors.push(t),0!==n.pending.length||n.done||(Object.keys(n.loaded).forEach(function(e){a[e]||(a[e]=[]),n.loaded[e].length&&n.loaded[e].forEach(function(t){a[e].indexOf(t)<0&&a[e].push(t)})}),n.done=!0,n.errors.length?n.callback(n.errors):n.callback())}),this.emit("loaded",a),this.queue=this.queue.filter(function(e){return!e.done})}},{key:"read",value:function(e,t,n){var o=this,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:350,a=arguments.length>5?arguments[5]:void 0;return e.length?this.backend[n](e,t,function(s,u){s&&u&&i<5?setTimeout(function(){o.read.call(o,e,t,n,i+1,2*r,a)},r):a(s,u)}):a(null,{})}},{key:"prepareLoading",value:function(e,t){var n=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=arguments.length>3?arguments[3]:void 0;if(!this.backend)return this.logger.warn("No backend was added via i18next.use. Will not load resources."),i&&i();"string"==typeof e&&(e=this.languageUtils.toResolveHierarchy(e)),"string"==typeof t&&(t=[t]);var r=this.queueLoad(e,t,o,i);if(!r.toLoad.length)return r.pending.length||i(),null;r.toLoad.forEach(function(e){n.loadOne(e)})}},{key:"load",value:function(e,t,n){this.prepareLoading(e,t,{},n)}},{key:"reload",value:function(e,t,n){this.prepareLoading(e,t,{reload:!0},n)}},{key:"loadOne",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=e.split("|"),i=o[0],r=o[1];this.read(i,r,"read",void 0,void 0,function(o,a){o&&t.logger.warn("".concat(n,"loading namespace ").concat(r," for language ").concat(i," failed"),o),!o&&a&&t.logger.log("".concat(n,"loaded namespace ").concat(r," for language ").concat(i),a),t.loaded(e,o,a)})}},{key:"saveMissing",value:function(e,t,o,i,r){var a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{};this.services.utils&&this.services.utils.hasLoadedNamespace&&!this.services.utils.hasLoadedNamespace(t)?this.logger.warn('did not save key "'.concat(o,'" as the namespace "').concat(t,'" was not yet loaded'),"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!"):null!=o&&""!==o&&(this.backend&&this.backend.create&&this.backend.create(e,t,o,i,null,n({},a,{isUpdate:r})),e&&e[0]&&this.store.addResource(e[0],t,o,i))}}]),t}();function A(e){return"string"==typeof e.ns&&(e.ns=[e.ns]),"string"==typeof e.fallbackLng&&(e.fallbackLng=[e.fallbackLng]),"string"==typeof e.fallbackNS&&(e.fallbackNS=[e.fallbackNS]),e.whitelist&&(e.whitelist&&e.whitelist.indexOf("cimode")<0&&(e.whitelist=e.whitelist.concat(["cimode"])),e.supportedLngs=e.whitelist),e.nonExplicitWhitelist&&(e.nonExplicitSupportedLngs=e.nonExplicitWhitelist),e.supportedLngs&&e.supportedLngs.indexOf("cimode")<0&&(e.supportedLngs=e.supportedLngs.concat(["cimode"])),e}function U(){}return new(function(t){function i(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1?arguments[1]:void 0;if(o(this,i),e=s(this,u(i).call(this)),S&&f.call(a(e)),e.options=A(t),e.services={},e.logger=g,e.modules={external:[]},n&&!e.isInitialized&&!t.isClone){if(!e.options.initImmediate)return e.init(t,n),s(e,a(e));setTimeout(function(){e.init(t,n)},0)}return e}return c(i,f),r(i,[{key:"init",value:function(){var t=this,o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=arguments.length>1?arguments[1]:void 0;function r(e){return e?"function"==typeof e?new e:e:null}if("function"==typeof o&&(i=o,o={}),o.whitelist&&!o.supportedLngs&&this.logger.deprecate("whitelist",'option "whitelist" will be renamed to "supportedLngs" in the next major - please make sure to rename this option asap.'),o.nonExplicitWhitelist&&!o.nonExplicitSupportedLngs&&this.logger.deprecate("whitelist",'options "nonExplicitWhitelist" will be renamed to "nonExplicitSupportedLngs" in the next major - please make sure to rename this option asap.'),this.options=n({},{debug:!1,initImmediate:!0,ns:["translation"],defaultNS:["translation"],fallbackLng:["dev"],fallbackNS:!1,whitelist:!1,nonExplicitWhitelist:!1,supportedLngs:!1,nonExplicitSupportedLngs:!1,load:"all",preload:!1,simplifyPluralSuffix:!0,keySeparator:".",nsSeparator:":",pluralSeparator:"_",contextSeparator:"_",partialBundledLanguages:!1,saveMissing:!1,updateMissing:!1,saveMissingTo:"fallback",saveMissingPlurals:!0,missingKeyHandler:!1,missingInterpolationHandler:!1,postProcess:!1,postProcessPassResolved:!1,returnNull:!0,returnEmptyString:!0,returnObjects:!1,joinArrays:!1,returnedObjectHandler:!1,parseMissingKeyHandler:!1,appendNamespaceToMissingKey:!1,appendNamespaceToCIMode:!1,overloadTranslationOptionHandler:function(t){var n={};if("object"===e(t[1])&&(n=t[1]),"string"==typeof t[1]&&(n.defaultValue=t[1]),"string"==typeof t[2]&&(n.tDescription=t[2]),"object"===e(t[2])||"object"===e(t[3])){var o=t[3]||t[2];Object.keys(o).forEach(function(e){n[e]=o[e]})}return n},interpolation:{escapeValue:!0,format:function(e,t,n,o){return e},prefix:"{{",suffix:"}}",formatSeparator:",",unescapePrefix:"-",nestingPrefix:"$t(",nestingSuffix:")",nestingOptionsSeparator:",",maxReplaces:1e3,skipOnVariables:!1}},this.options,A(o)),this.format=this.options.interpolation.format,i||(i=U),!this.options.isClone){this.modules.logger?g.init(r(this.modules.logger),this.options):g.init(null,this.options);var a=new j(this.options);this.store=new L(this.options.resources,this.options);var s=this.services;s.logger=g,s.resourceStore=this.store,s.languageUtils=a,s.pluralResolver=new F(a,{prepend:this.options.pluralSeparator,compatibilityJSON:this.options.compatibilityJSON,simplifyPluralSuffix:this.options.simplifyPluralSuffix}),s.interpolator=new V(this.options),s.utils={hasLoadedNamespace:this.hasLoadedNamespace.bind(this)},s.backendConnector=new T(r(this.modules.backend),s.resourceStore,s,this.options),s.backendConnector.on("*",function(e){for(var n=arguments.length,o=new Array(n>1?n-1:0),i=1;i<n;i++)o[i-1]=arguments[i];t.emit.apply(t,[e].concat(o))}),this.modules.languageDetector&&(s.languageDetector=r(this.modules.languageDetector),s.languageDetector.init(s,this.options.detection,this.options)),this.modules.i18nFormat&&(s.i18nFormat=r(this.modules.i18nFormat),s.i18nFormat.init&&s.i18nFormat.init(this)),this.translator=new C(this.services,this.options),this.translator.on("*",function(e){for(var n=arguments.length,o=new Array(n>1?n-1:0),i=1;i<n;i++)o[i-1]=arguments[i];t.emit.apply(t,[e].concat(o))}),this.modules.external.forEach(function(e){e.init&&e.init(t)})}this.modules.languageDetector||this.options.lng||this.logger.warn("init: no languageDetector is used and no lng is defined");["getResource","addResource","addResources","addResourceBundle","removeResourceBundle","hasResourceBundle","getResourceBundle","getDataByLanguage"].forEach(function(e){t[e]=function(){var n;return(n=t.store)[e].apply(n,arguments)}});var u=h(),l=function(){t.changeLanguage(t.options.lng,function(e,n){t.isInitialized=!0,t.logger.log("initialized",t.options),t.emit("initialized",t.options),u.resolve(n),i(e,n)})};return this.options.resources||!this.options.initImmediate?l():setTimeout(l,0),u}},{key:"loadResources",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:U,o="string"==typeof e?e:this.language;if("function"==typeof e&&(n=e),!this.options.resources||this.options.partialBundledLanguages){if(o&&"cimode"===o.toLowerCase())return n();var i=[],r=function(e){e&&t.services.languageUtils.toResolveHierarchy(e).forEach(function(e){i.indexOf(e)<0&&i.push(e)})};if(o)r(o);else this.services.languageUtils.getFallbackCodes(this.options.fallbackLng).forEach(function(e){return r(e)});this.options.preload&&this.options.preload.forEach(function(e){return r(e)}),this.services.backendConnector.load(i,this.options.ns,n)}else n(null)}},{key:"reloadResources",value:function(e,t,n){var o=h();return e||(e=this.languages),t||(t=this.options.ns),n||(n=U),this.services.backendConnector.reload(e,t,function(e){o.resolve(),n(e)}),o}},{key:"use",value:function(e){if(!e)throw new Error("You are passing an undefined module! Please check the object you are passing to i18next.use()");if(!e.type)throw new Error("You are passing a wrong module! Please check the object you are passing to i18next.use()");return"backend"===e.type&&(this.modules.backend=e),("logger"===e.type||e.log&&e.warn&&e.error)&&(this.modules.logger=e),"languageDetector"===e.type&&(this.modules.languageDetector=e),"i18nFormat"===e.type&&(this.modules.i18nFormat=e),"postProcessor"===e.type&&O.addPostProcessor(e),"3rdParty"===e.type&&this.modules.external.push(e),this}},{key:"changeLanguage",value:function(e,t){var n=this;this.isLanguageChangingTo=e;var o=h();this.emit("languageChanging",e);var i=function(e){var i="string"==typeof e?e:n.services.languageUtils.getBestMatchFromCodes(e);i&&(n.language||(n.language=i,n.languages=n.services.languageUtils.toResolveHierarchy(i)),n.translator.language||n.translator.changeLanguage(i),n.services.languageDetector&&n.services.languageDetector.cacheUserLanguage(i)),n.loadResources(i,function(e){!function(e,i){i?(n.language=i,n.languages=n.services.languageUtils.toResolveHierarchy(i),n.translator.changeLanguage(i),n.isLanguageChangingTo=void 0,n.emit("languageChanged",i),n.logger.log("languageChanged",i)):n.isLanguageChangingTo=void 0,o.resolve(function(){return n.t.apply(n,arguments)}),t&&t(e,function(){return n.t.apply(n,arguments)})}(e,i)})};return e||!this.services.languageDetector||this.services.languageDetector.async?!e&&this.services.languageDetector&&this.services.languageDetector.async?this.services.languageDetector.detect(i):i(e):i(this.services.languageDetector.detect()),o}},{key:"getFixedT",value:function(t,o){var i=this,r=function t(o,r){var a;if("object"!==e(r)){for(var s=arguments.length,u=new Array(s>2?s-2:0),l=2;l<s;l++)u[l-2]=arguments[l];a=i.options.overloadTranslationOptionHandler([o,r].concat(u))}else a=n({},r);return a.lng=a.lng||t.lng,a.lngs=a.lngs||t.lngs,a.ns=a.ns||t.ns,i.t(o,a)};return"string"==typeof t?r.lng=t:r.lngs=t,r.ns=o,r}},{key:"t",value:function(){var e;return this.translator&&(e=this.translator).translate.apply(e,arguments)}},{key:"exists",value:function(){var e;return this.translator&&(e=this.translator).exists.apply(e,arguments)}},{key:"setDefaultNamespace",value:function(e){this.options.defaultNS=e}},{key:"hasLoadedNamespace",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(!this.isInitialized)return this.logger.warn("hasLoadedNamespace: i18next was not initialized",this.languages),!1;if(!this.languages||!this.languages.length)return this.logger.warn("hasLoadedNamespace: i18n.languages were undefined or empty",this.languages),!1;var o=this.languages[0],i=!!this.options&&this.options.fallbackLng,r=this.languages[this.languages.length-1];if("cimode"===o.toLowerCase())return!0;var a=function(e,n){var o=t.services.backendConnector.state["".concat(e,"|").concat(n)];return-1===o||2===o};if(n.precheck){var s=n.precheck(this,a);if(void 0!==s)return s}return!!this.hasResourceBundle(o,e)||(!this.services.backendConnector.backend||!(!a(o,e)||i&&!a(r,e)))}},{key:"loadNamespaces",value:function(e,t){var n=this,o=h();return this.options.ns?("string"==typeof e&&(e=[e]),e.forEach(function(e){n.options.ns.indexOf(e)<0&&n.options.ns.push(e)}),this.loadResources(function(e){o.resolve(),t&&t(e)}),o):(t&&t(),Promise.resolve())}},{key:"loadLanguages",value:function(e,t){var n=h();"string"==typeof e&&(e=[e]);var o=this.options.preload||[],i=e.filter(function(e){return o.indexOf(e)<0});return i.length?(this.options.preload=o.concat(i),this.loadResources(function(e){n.resolve(),t&&t(e)}),n):(t&&t(),Promise.resolve())}},{key:"dir",value:function(e){if(e||(e=this.languages&&this.languages.length>0?this.languages[0]:this.language),!e)return"rtl";return["ar","shu","sqr","ssh","xaa","yhd","yud","aao","abh","abv","acm","acq","acw","acx","acy","adf","ads","aeb","aec","afb","ajp","apc","apd","arb","arq","ars","ary","arz","auz","avl","ayh","ayl","ayn","ayp","bbz","pga","he","iw","ps","pbt","pbu","pst","prp","prd","ug","ur","ydd","yds","yih","ji","yi","hbo","men","xmn","fa","jpr","peo","pes","prs","dv","sam"].indexOf(this.services.languageUtils.getLanguagePartFromCode(e))>=0?"rtl":"ltr"}},{key:"createInstance",value:function(){return new i(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},arguments.length>1?arguments[1]:void 0)}},{key:"cloneInstance",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:U,r=n({},this.options,t,{isClone:!0}),a=new i(r);return["store","services","language"].forEach(function(t){a[t]=e[t]}),a.services=n({},this.services),a.services.utils={hasLoadedNamespace:a.hasLoadedNamespace.bind(a)},a.translator=new C(a.services,a.options),a.translator.on("*",function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];a.emit.apply(a,[e].concat(n))}),a.init(r,o),a.translator.options=a.options,a.translator.backendConnector.services.utils={hasLoadedNamespace:a.hasLoadedNamespace.bind(a)},a}}]),i}())});

    module.exports = {'i18next': module.exports};
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Sets up JME built-in functions.
 *
 * Provides {@link Numbas.jme}
 */
Numbas.queueScript('jme-builtins', ['jme-base', 'jme-rules', 'jme-calculus', 'jme-variables', 'seedrandom'], function() {
var util = Numbas.util;
var math = Numbas.math;
var vectormath = Numbas.vectormath;
var matrixmath = Numbas.matrixmath;
var setmath = Numbas.setmath;
var jme = Numbas.jme;

var Scope = jme.Scope;
var funcObj = jme.funcObj;

var types = Numbas.jme.types;
var TNum = types.TNum;
var TInt = types.TInt;
var TRational = types.TRational;
var TDecimal = types.TDecimal;
var TString = types.TString;
var TBool = types.TBool;
var THTML = types.THTML;
var TList = types.TList;
var TDict = types.TDict;
var TMatrix = types.TMatrix;
var TName = types.TName;
var TRange = types.TRange;
var TSet = types.TSet;
var TVector = types.TVector;
var TExpression = types.TExpression;
var TOp = types.TOp;
var TFunc = types.TFunc;
var TLambda = types.TLambda;

var sig = jme.signature;

/** The built-in JME evaluation scope.
 *
 * @type {Numbas.jme.Scope}
 * @memberof Numbas.jme
 */
var builtinScope = jme.builtinScope = new Scope({rulesets:jme.rules.simplificationRules});
builtinScope.setConstant('nothing', {value: new types.TNothing, tex: '\\text{nothing}'});
/** Definitions of constants to include in `Numbas.jme.builtinScope`.
 *
 * @type {Array.<Numbas.jme.constant_definition>}
 * @memberof Numbas.jme
 */
Numbas.jme.builtin_constants = [
    {name: 'e', value: new TNum(Math.E), tex: 'e'},
    {name: 'pi', value: new TNum(Math.PI), tex: '\\pi'},
    {name: 'i', value: new TNum(math.complex(0, 1)), tex: 'i'},
    {name: 'infinity,infty', value: new TNum(Infinity), tex: '\\infty'},
    {name: 'NaN', value: new TNum(NaN), tex: '\\texttt{NaN}'},
    {name: 'j', value: new TNum(math.complex(0, 1)), tex: 'j', enabled: false},
];
Numbas.jme.variables.makeConstants(Numbas.jme.builtin_constants, builtinScope);

/** Add a function to the built-in scope.
 *
 * @see Numbas.jme.builtinScope
 * @param {string} name
 * @param {Array.<Function|string>} intype - A list of data type constructors for the function's paramters' types. Use the string '?' to match any type. Or, give the type's name with a '*' in front to match any number of that type. If `null`, then `options.typecheck` is used.
 * @param {Function} outcons - The constructor for the output value of the function
 * @param {Numbas.jme.evaluate_fn} fn - JavaScript code which evaluates the function.
 * @param {Numbas.jme.funcObj_options} options
 * @returns {Numbas.jme.funcObj}
 */
function newBuiltin(name, intype, outcons, fn, options) {
    options = options || {};
    options.random = 'random' in options ? options.random : false;
    return builtinScope.addFunction(new funcObj(name, intype, outcons, fn, options));
}

newBuiltin('+u', [TNum], TNum, function(a) {
    return a;
});
newBuiltin('+u', [TVector], TVector, function(a) {
    return a;
});
newBuiltin('+u', [TMatrix], TMatrix, function(a) {
    return a;
});
newBuiltin('-u', [TNum], TNum, math.negate);
newBuiltin('-u', [TVector], TVector, vectormath.negate);
newBuiltin('-u', [TMatrix], TMatrix, matrixmath.negate);
newBuiltin('+', [TNum, TNum], TNum, math.add);
newBuiltin('+', [TList, TList], TList, null, {
    evaluate: function(args, scope) {
        var value = args[0].value.concat(args[1].value);
        return new TList(value);
    }
});
newBuiltin('+', [TList, '?'], TList, null, {
    evaluate: function(args, scope) {
        var value = args[0].value.slice();
        value.push(args[1]);
        return new TList(value);
    }
});

const fn_dict_update = {
    evaluate: function(args, scope) {
        var nvalue = {};

        if(args.length == 1 && args[0].type == 'list') {
            args = args[0].value;
        }

        args.forEach((arg) => {
            Object.keys(arg.value).forEach(function(x) {
                nvalue[x] = arg.value[x];
            });
        });

        return new TDict(nvalue);
    }
}
newBuiltin('+', [TDict, TDict], TDict, null, fn_dict_update);

newBuiltin('merge', ['*dict'], TDict, null, fn_dict_update);
newBuiltin('merge', ['list of dict'], TDict, null, fn_dict_update);

var fconc = function(a, b) {
    return a + b;
}
newBuiltin('+', [TString, '?'], TString, fconc);
newBuiltin('+', ['?', TString], TString, fconc);
newBuiltin('+', [TVector, TVector], TVector, vectormath.add);
newBuiltin('+', [TMatrix, TMatrix], TMatrix, matrixmath.add);
newBuiltin('-', [TNum, TNum], TNum, math.sub);
newBuiltin('-', [TVector, TVector], TVector, vectormath.sub);
newBuiltin('-', [TMatrix, TMatrix], TMatrix, matrixmath.sub);
newBuiltin('*', [TNum, TNum], TNum, math.mul);
newBuiltin('*', [TNum, TVector], TVector, vectormath.mul);
newBuiltin('*', [TVector, TNum], TVector, function(a, b) {
    return vectormath.mul(b, a)
});
newBuiltin('*', [TMatrix, TVector], TVector, vectormath.matrixmul);
newBuiltin('*', [TNum, TMatrix], TMatrix, matrixmath.scalarmul);
newBuiltin('*', [TMatrix, TNum], TMatrix, function(a, b) {
    return matrixmath.scalarmul(b, a);
});
newBuiltin('*', [TMatrix, TMatrix], TMatrix, matrixmath.mul);
newBuiltin('*', [TVector, TMatrix], TVector, vectormath.vectormatrixmul);
newBuiltin('/', [TNum, TNum], TNum, math.div);
newBuiltin('/', [TMatrix, TNum], TMatrix, function(a, b) {
    return matrixmath.scalardiv(a, b);
});
newBuiltin('/', [TVector, TNum], TVector, function(a, b) {
    return vectormath.div(a, b)
});
newBuiltin('^', [TNum, TNum], TNum, math.pow);
newBuiltin('dot', [TVector, TVector], TNum, vectormath.dot);
newBuiltin('dot', [TMatrix, TVector], TNum, vectormath.dot);
newBuiltin('dot', [TVector, TMatrix], TNum, vectormath.dot);
newBuiltin('dot', [TMatrix, TMatrix], TNum, vectormath.dot);
newBuiltin('cross', [TVector, TVector], TVector, vectormath.cross);
newBuiltin('cross', [TMatrix, TVector], TVector, vectormath.cross);
newBuiltin('cross', [TVector, TMatrix], TVector, vectormath.cross);
newBuiltin('cross', [TMatrix, TMatrix], TVector, vectormath.cross);
newBuiltin('det', [TMatrix], TNum, matrixmath.abs);
newBuiltin('numrows', [TMatrix], TNum, function(m) {
    return m.rows
});
newBuiltin('numcolumns', [TMatrix], TNum, function(m) {
    return m.columns
});
newBuiltin('angle', [TVector, TVector], TNum, vectormath.angle);
newBuiltin('transpose', [TVector], TMatrix, vectormath.transpose);
newBuiltin('transpose', [TMatrix], TMatrix, matrixmath.transpose);
newBuiltin('transpose', ['list of list'], TList, null, {
    evaluate: function(args, scope) {
        var lists = args[0].value;
        var l = Math.min(...lists.map((l) => l.value.length));
        var o = [];
        for(let i = 0;i < l;i++) {
            o.push(new TList(lists.map((l) => l.value[i])));
        }
        return new TList(o);
    }
});
newBuiltin('is_zero', [TVector], TBool, vectormath.is_zero);
newBuiltin('id', [TNum], TMatrix, matrixmath.id);
newBuiltin('sum_cells', [TMatrix], TNum, matrixmath.sum_cells);
newBuiltin('numrows', [TMatrix], TNum, function(m) {
    return matrixmath.numrows(m)
});
newBuiltin('numcolumns', [TMatrix], TNum, function(m) {
    return matrixmath.numcolumns(m)
});
newBuiltin('combine_vertically', [TMatrix, TMatrix], TMatrix, function(m1, m2) {
    return matrixmath.combine_vertically(m1, m2)
});
newBuiltin('stack', [TMatrix, TMatrix], TMatrix, function(m1, m2) {
    return matrixmath.combine_vertically(m1, m2)
});
newBuiltin('combine_horizontally', [TMatrix, TMatrix], TMatrix, function(m1, m2) {
    return matrixmath.combine_horizontally(m1, m2)
});
newBuiltin('augment', [TMatrix, TMatrix], TMatrix, function(m1, m2) {
    return matrixmath.combine_horizontally(m1, m2)
});
newBuiltin('combine_diagonally', [TMatrix, TMatrix], TMatrix, function(m1, m2) {
    return matrixmath.combine_diagonally(m1, m2)
});
newBuiltin('lu_decomposition', [TMatrix], TList, null, {
    evaluate: function(args, scope) {
        var m = args[0].value;
        const [L, U] = matrixmath.lu_decomposition(m);
        return new TList([new TMatrix(L), new TMatrix(U)]);
    }
});

newBuiltin('gauss_jordan_elimination', [TMatrix], TMatrix, matrixmath.gauss_jordan_elimination);

newBuiltin('inverse', [TMatrix], TMatrix, matrixmath.inverse);

newBuiltin('..', [TNum, TNum], TRange, math.defineRange);
newBuiltin('#', [TRange, TNum], TRange, math.rangeSteps);
newBuiltin('in', [TNum, TRange], TBool, function(x, r) {
    var start = r[0];
    var end = r[1];
    var step_size = r[2];
    if(x > end || x < start) {
        return false;
    }
    if(step_size === 0) {
        return true;
    } else {
        var max_steps = Math.floor(end - start) / step_size;
        var steps = Math.floor((x - start) / step_size);
        return step_size * steps + start == x && steps <= max_steps;
    }
});
newBuiltin('list', [TRange], TList, function(range) {
    return math.rangeToList(range).map(function(n) {
        return new TNum(n)
    });
});
newBuiltin('dict', ['*keypair'], TDict, null, {
    evaluate: function(args, scope) {
        if(args.length == 0) {
            return new TDict({});
        }
        var value = {};
        if(args[0].tok.type == 'keypair') {
            args.forEach(function(kp) {
                value[kp.tok.key] = jme.evaluate(kp.args[0], scope);
            });
        } else if(args.length == 1) {
            var list = scope.evaluate(args[0]);
            var items = list.value;
            if(list.type != 'list' || !items.every(function(item) {
                return item.type == 'list' && item.value.length == 2 && item.value[0].type == 'string';
            })) {
                throw(new Numbas.Error('jme.typecheck.no right type definition', {op:'dict'}));
            }
            items.forEach(function(item) {
                value[item.value[0].value] = item.value[1];
            });
        } else {
            throw(new Numbas.Error('jme.typecheck.no right type definition', {op:'dict'}));
        }
        return new TDict(value);
    }
});
Numbas.jme.lazyOps.push('dict');
newBuiltin('keys', [TDict], TList, function(d) {
    var o = [];
    Object.keys(d).forEach(function(key) {
        o.push(new TString(key));
    })
    return o;
});
newBuiltin('values', [TDict], TList, function(d) {
    var o = [];
    Object.values(d).forEach(function(v) {
        o.push(v);
    })
    return o;
});
newBuiltin('values', [TDict, sig.listof(sig.type('string'))], TList, function(d, keys) {
    return keys.map(function(key) {
        if(!Object.hasOwn(d, key.value)) {
            throw(new Numbas.Error('jme.func.listval.key not in dict', {key:key}));
        } else {
            return d[key.value];
        }
    });
})
newBuiltin('items', [TDict], TList, null, {
    evaluate: function(args, scope) {
        var o = [];
        Object.entries(args[0].value).forEach(function(x) {
            o.push(new TList([new TString(x[0]), x[1]]))
        });
        return new TList(o);
    }
});
newBuiltin('listval', [TDict, TString], '?', null, {
    evaluate: function(args, scope) {
        var d = args[0].value;
        var key = args[1].value;
        if(!Object.hasOwn(d, key)) {
            throw(new Numbas.Error('jme.func.listval.key not in dict', {key:key}));
        }
        return d[key];
    }
});
newBuiltin('get', [TDict, TString, '?'], '?', null, {
    evaluate: function(args, scope) {
        var d = args[0].value;
        var key = args[1].value;
        if(!Object.hasOwn(d, key)) {
            return args[2]
        }
        return d[key];
    }
});
newBuiltin('in', [TString, TDict], TBool, function(s, d) {
    return Object.hasOwn(d, s);
});
newBuiltin('json_decode', [TString], '?', null, {
    evaluate: function(args, scope) {
        var data = JSON.parse(args[0].value);
        return jme.wrapValue(data);
    }
});
newBuiltin('json_encode', ['?'], TString, null, {
    evaluate: function(args, scope) {
        var s = new TString(JSON.stringify(jme.unwrapValue(args[0])));
        s.safe = true;
        return s;
    }
});
newBuiltin('formatstring', [TString, TList], TString, null, {
    evaluate: function(args, scope) {
        var str = args[0].value;
        var extra = args[1].value;
        return new TString(util.formatString.apply(util, [str].concat(extra.map(function(x) {
            return jme.tokenToDisplayString(x, scope);
        }))));
    }
});
newBuiltin('jme_string', ['?'], TString, null, {evaluate: function(args, scope) {
    return new TString(jme.display.treeToJME({tok:args[0]}, {}, scope))
}});
newBuiltin('unpercent', [TString], TNum, util.unPercent);
newBuiltin('letterordinal', [TNum], TString, util.letterOrdinal);
newBuiltin('html', [TString], THTML, null, {
    evaluate: function(args, scope) {
        var container = document.createElement('div');
        container.innerHTML = args[0].value;
        var subber = new jme.variables.DOMcontentsubber(scope);
        subber.subvars(container);
        var nodes = Array.from(container.childNodes);
        nodes.forEach((node) => {
            if(node.nodeType == node.ELEMENT_NODE) {
                node.setAttribute('data-interactive', 'false');
            }
        });
        return new THTML(nodes);
    }
});
newBuiltin('isnonemptyhtml', [TString], TBool, function(html) {
    return util.isNonemptyHTML(html);
});
newBuiltin('image', [TString, '[number]', '[number]'], THTML, null, {
    evaluate: function(args, scope) {
        var url = args[0].value;
        var width = args[1];
        var height = args[2];
        var img = document.createElement('img');
        img.setAttribute('src', url);
        if(width.type != 'nothing') {
            img.style.width = width.value + 'em';
        }
        if(height.type != 'nothing') {
            img.style.height = height.value + 'em';
        }
        var subber = new jme.variables.DOMcontentsubber(scope);
        var element = subber.subvars(img);

        // The subber replaces SVG images with <object> tags which have an event listener for when the content loads, so they must be considered interactive.
        element.setAttribute('data-interactive', element.tagName.toLowerCase() == 'object');

        return new THTML(element);
    }
});
newBuiltin('latex', [TString], TString, null, {
    evaluate: function(args, scope) {
        var s = new TString(args[0].value);
        s.latex = true;
        s.display_latex = true;
        s.safe = args[0].safe;
        return s;
    }
});
newBuiltin('safe', [TString], TString, null, {
    evaluate: function(args, scope) {
        var s = args[0];
        while(jme.isFunction(s.tok, 'safe')) {
            s = s.args[0];
        }
        var t;
        if(s.args) {
            var r = scope.evaluate(s);
            t = new TString(r.value);
            t.latex = r.latex;
            t.display_latex = r.display_latex;
        } else {
            t = new TString(s.tok.value);
        }
        t.safe = true;
        return t;
    }
});
Numbas.jme.lazyOps.push('safe');
jme.findvarsOps.safe = function(tree, boundvars, scope) {
    return [];
}

newBuiltin('escape_html', [TString], TString, function(str) {
    const p = document.createElement('p');
    p.appendChild(document.createTextNode(str));
    return p.innerHTML;
});

newBuiltin('render', [TString, sig.optional(sig.type('dict'))], TString, null, {
    evaluate: function(args, scope) {
        var str = args[0].value;
        var variables = args.length > 1 ? args[1].value : {};
        scope = new Scope([scope, {variables: variables}]);
        return new TString(jme.contentsubvars(str, scope, true));
    }
});
jme.findvarsOps.render = function(tree, boundvars, scope) {
    var vars = [];
    if(tree.args[0].tok.type != 'string') {
        vars = jme.findvars(tree.args[0], [], scope);
    }
    if(tree.args.length > 1) {
        vars = vars.merge(jme.findvars(tree.args[1], boundvars, scope));
    }
    return vars;
}
newBuiltin('capitalise', [TString], TString, function(s) {
    return util.capitalise(s);
});
newBuiltin('upper', [TString], TString, function(s) {
    return s.toUpperCase();
});
newBuiltin('lower', [TString], TString, function(s) {
    return s.toLowerCase();
});
newBuiltin('pluralise', [TNum, TString, TString], TString, function(n, singular, plural) {
    return util.pluralise(n, singular, plural);
});
newBuiltin('join', [TList, TString], TString, null, {
    evaluate: function(args, scope) {
        var list = args[0].value;
        var delimiter = args[1].value;
        return new TString(list.map(function(x) {
            return jme.tokenToDisplayString(x, scope);
        }).join(delimiter));
    }
});
newBuiltin('split', [TString, TString], TList, function(str, delimiter) {
    return str.split(delimiter).map(function(s) {
        return new TString(s)
    });
});
newBuiltin('trim', [TString], TString, function(str) {
    return str.trim();
});
newBuiltin('currency', [TNum, TString, TString], TString, util.currency, {latex: true});
newBuiltin('separateThousands', [TNum, TString], TString, util.separateThousands);
newBuiltin('listval', [TString, TNum], TString, function(s, i) {
    return s[i]
});
newBuiltin('listval', [TString, TRange], TString, function(s, range) {
    return s.slice(range[0], range[1])
});
newBuiltin('in', [TString, TString], TBool, function(sub, str) {
    return str.indexOf(sub) >= 0
});
newBuiltin('lpad', [TString, TNum, TString], TString, util.lpad);
newBuiltin('rpad', [TString, TNum, TString], TString, util.rpad);
newBuiltin('match_regex', [TString, TString], TList, function(pattern, str) {
    var re = new RegExp(pattern, 'u');
    var m = re.exec(str);
    return m || [];
}, {unwrapValues: true});
newBuiltin('match_regex', [TString, TString, TString], TList, function(pattern, str, flags) {
    var re = new RegExp(pattern, flags);
    var m = re.exec(str);
    return m || [];
}, {unwrapValues: true});

newBuiltin('split_regex', [TString, TString], TList, function(str, delimiter) {
    return str.split(new RegExp(delimiter, 'u')).map(function(s) {
        return new TString(s)
    });
});
newBuiltin('split_regex', [TString, TString, TString], TList, function(str, delimiter, flags) {
    return str.split(new RegExp(delimiter, flags)).map(function(s) {
        return new TString(s)
    });
});

newBuiltin('replace_regex', [TString, TString, TString], TString, function(pattern, replacement, str) {
    return str.replace(new RegExp(pattern, 'u'), replacement);
});

newBuiltin('replace_regex', [TString, TString, TString, TString], TString, function(pattern, replacement, str, flags) {
    return str.replace(new RegExp(pattern, flags), replacement);
});

//the next three versions of the `except` operator
//exclude numbers from a range, given either as a range, a list or a single value
newBuiltin('except', [TRange, TRange], TList,
    function(range, except) {
        if(range[2] == 0) {
            throw(new Numbas.Error("jme.func.except.continuous range"));
        }
        var cons = best_number_type_for_range(range);
        range = math.rangeToList(range);
        if(except[2] == 0) {
            return range.filter(function(i) {
                return i < except[0] || i > except[1]
            }).map(function(i) {
                return new cons(i)
            });
        } else {
            except = math.rangeToList(except);
            return math.except(range, except).map(function(i) {
                return new cons(i)
            });
        }
    }
);
newBuiltin('except', [TRange, 'list of number'], TList,
    function(range, except) {
        if(range[2] == 0) {
            throw(new Numbas.Error("jme.func.except.continuous range"));
        }
        var cons = best_number_type_for_range(range);
        range = math.rangeToList(range)
        except = except.map(function(i) {
            return i.value;
        });
        return math.except(range, except).map(function(i) {
            return new cons(i)
        });
    }
);
newBuiltin('except', [TRange, TNum], TList,
    function(range, except) {
        if(range[2] == 0) {
            throw(new Numbas.Error("jme.func.except.continuous range"));
        }
        var cons = best_number_type_for_range(range);
        range = math.rangeToList(range);
        return math.except(range, [except]).map(function(i) {
            return new cons(i)
        });
    }
);
//exclude numbers from a list, so use the math.except function
newBuiltin('except', [TList, TRange], TList,
    function(range, except) {
        except = math.rangeToList(except);
        return range.filter(function(r) {
            return !except.some(function(e) {
                return math.eq(r.value, e)
            });
        });
    }
);
//exclude values of any type from a list containing values of any type, so use the util.except function
newBuiltin('except', [TList, TList], TList, null, {
    evaluate: function(args, scope) {
        return new TList(util.except(args[0].value, args[1].value, scope));
    }
});
newBuiltin('except', [TList, '?'], TList, null, {
    evaluate: function(args, scope) {
        return new TList(util.except(args[0].value, [args[1]], scope));
    }
});
newBuiltin('distinct', [TList], TList, null, {
    evaluate: function(args, scope) {
        return new TList(util.distinct(args[0].value, scope));
    }
}, {unwrapValues: false});
newBuiltin('in', ['?', TList], TBool, null, {
    evaluate: function(args, scope) {
        return new TBool(util.contains(args[1].value, args[0], scope));
    }
});
newBuiltin('<', [TNum, TNum], TBool, math.lt);
newBuiltin('>', [TNum, TNum], TBool, math.gt);
newBuiltin('<=', [TNum, TNum], TBool, math.leq);
newBuiltin('>=', [TNum, TNum], TBool, math.geq);
newBuiltin('<>', ['?', '?'], TBool, null, {
    evaluate: function(args, scope) {
        return new TBool(util.neq(args[0], args[1], scope));
    }
});
newBuiltin('=', ['?', '?'], TBool, null, {
    evaluate: function(args, scope) {
        return new TBool(util.eq(args[0], args[1], scope));
    }
});
newBuiltin('isclose', [TNum, TNum, sig.optional(sig.type('number')), sig.optional(sig.type('number'))], TBool, math.isclose);
newBuiltin('is_scalar_multiple', [TVector, TVector, sig.optional(sig.type('number')), sig.optional(sig.type('number'))], TBool, math.is_scalar_multiple);

newBuiltin('and', [TBool, TBool], TBool, null, {
    evaluate: function(args, scope) {
        let a = scope.evaluate(args[0]);

        if(jme.isType(a, 'set')) {
            const b = scope.evaluate(args[1]);
            return new TSet(setmath.intersection(jme.castToType(a, 'set').value, jme.castToType(b, 'set').value, scope));
        }

        a = jme.castToType(a,'boolean')

        if(!a.value) {
            return new TBool(false);
        }
        const b = jme.castToType(scope.evaluate(args[1]), 'boolean');
        return b;
    }
});
newBuiltin('not', [TBool], TBool, function(a) {
    return !a;
});
newBuiltin('or', [TBool, TBool], TBool, null, {
    evaluate: function(args, scope) {
        let a = scope.evaluate(args[0]);
        if(a.type == 'set') {
            const b = scope.evaluate(args[1]);
            return new TSet(setmath.union(jme.castToType(a, 'set').value, jme.castToType(b, 'set').value, scope));
        }

        a = jme.castToType(a,'boolean')

        if(a.value) {
            return new TBool(true);
        }
        const b = jme.castToType(scope.evaluate(args[1]), 'boolean');
        return b;
    }
});

newBuiltin('xor', [TBool, TBool], TBool, function(a, b) {
    return (a || b) && !(a && b);
});

newBuiltin('implies', [TBool, TBool], TBool, null, {
    evaluate: function(args, scope) {
        const a = scope.evaluate(args[0]);

        if(!a.value) {
            return new TBool(true);
        }
        const b = scope.evaluate(args[1]);
        return b;
    }
});

newBuiltin('nand', [TBool, TBool], TBool, null, {
    evaluate: function(args, scope) {
        const a = scope.evaluate(args[0]);

        if(!a.value) {
            return new TBool(true);
        }
        const b = scope.evaluate(args[1]);
        return new TBool(!b.value);
    }
});

newBuiltin('nor', [TBool, TBool], TBool, null, {
    evaluate: function(args, scope) {
        const a = scope.evaluate(args[0]);

        if(a.value) {
            return new TBool(false);
        }
        const b = scope.evaluate(args[1]);
        return new TBool(!b.value);
    }
});

jme.lazyOps.push('and');
jme.lazyOps.push('or');
jme.lazyOps.push('implies');
jme.lazyOps.push('nand');
jme.lazyOps.push('nor');

newBuiltin('abs', [TNum], TNum, math.abs);
newBuiltin('abs', [TString], TNum, function(s) {
    return s.length
});
newBuiltin('abs', [TList], TNum, function(l) {
    return l.length;
});
newBuiltin('abs', [TRange], TNum, function(r) {
    return r[2] == 0 ? Math.abs(r[0] - r[1]) : math.rangeSize(r);
});
newBuiltin('abs', [TVector], TNum, vectormath.abs);
newBuiltin('abs', [TDict], TNum, function(d) {
    return Object.keys(d).length;
});
newBuiltin('arg', [TNum], TNum, math.arg);
newBuiltin('re', [TNum], TNum, math.re);
newBuiltin('im', [TNum], TNum, math.im);
newBuiltin('conj', [TNum], TNum, math.conjugate);
newBuiltin('isint', [TNum], TBool, function(a) {
    return util.isInt(a);
});
newBuiltin('sqrt', [TNum], TNum, math.sqrt);
newBuiltin('ln', [TNum], TNum, math.log);
newBuiltin('log', [TNum], TNum, math.log10);
newBuiltin('log', [TNum, TNum], TNum, math.log_base);
newBuiltin('exp', [TNum], TNum, math.exp);
newBuiltin('fact', [TNum], TNum, math.factorial);
newBuiltin('gamma', [TNum], TNum, math.gamma);
newBuiltin('sin', [TNum], TNum, math.sin);
newBuiltin('cos', [TNum], TNum, math.cos);
newBuiltin('tan', [TNum], TNum, math.tan);
newBuiltin('cosec', [TNum], TNum, math.cosec);
newBuiltin('sec', [TNum], TNum, math.sec);
newBuiltin('cot', [TNum], TNum, math.cot);
newBuiltin('arcsin', [TNum], TNum, math.arcsin);
newBuiltin('arccos', [TNum], TNum, math.arccos);
newBuiltin('arctan', [TNum], TNum, math.arctan);
newBuiltin('sinh', [TNum], TNum, math.sinh);
newBuiltin('cosh', [TNum], TNum, math.cosh);
newBuiltin('tanh', [TNum], TNum, math.tanh);
newBuiltin('cosech', [TNum], TNum, math.cosech);
newBuiltin('sech', [TNum], TNum, math.sech);
newBuiltin('coth', [TNum], TNum, math.coth);
newBuiltin('arcsinh', [TNum], TNum, math.arcsinh);
newBuiltin('arccosh', [TNum], TNum, math.arccosh);
newBuiltin('arctanh', [TNum], TNum, math.arctanh);
newBuiltin('atan2', [TNum, TNum], TNum, math.atan2);
newBuiltin('ceil', [TNum], TNum, null, {
    evaluate: function(args, scope) {
        var n = math.ceil(jme.castToType(args[0], 'number').value);
        if(n.complex) {
            return new TNum(n);
        } else {
            return new TInt(n);
        }
    }
});
newBuiltin('floor', [TNum], TNum, null, {
    evaluate: function(args, scope) {
        var n = math.floor(jme.castToType(args[0], 'number').value);
        if(n.complex) {
            return new TNum(n);
        } else {
            return new TInt(n);
        }
    }
});
newBuiltin('round', [TNum], TNum, null, {
    evaluate: function(args, scope) {
        var n = math.round(jme.castToType(args[0], 'number').value);
        if(n.complex) {
            return new TNum(n);
        } else {
            return new TInt(n);
        }
    }
});
newBuiltin('tonearest', [TNum, TNum], TNum, math.toNearest);
newBuiltin('trunc', [TNum], TNum, math.trunc);
newBuiltin('trunc', [TNum, TNum], TNum, math.trunc);
newBuiltin('fract', [TNum], TNum, math.fract);
newBuiltin('degrees', [TNum], TNum, math.degrees);
newBuiltin('radians', [TNum], TNum, math.radians);
newBuiltin('sign', [TNum], TNum, math.sign);
newBuiltin('rational_approximation', [TNum], TList, function(n) {
    return math.rationalApproximation(n).map(function(x) {
        return new TInt(x);
    });
});
newBuiltin('rational_approximation', [TNum, TNum], TList, function(n, accuracy) {
    return math.rationalApproximation(n, accuracy).map(function(x) {
        return new TInt(x);
    });
});
newBuiltin('factorise', [TNum], TList, function(n) {
    return math.factorise(n).map(function(n) {
        return new TNum(n)
    });
}
);
newBuiltin('largest_square_factor', [TNum], TInt, math.largest_square_factor);
newBuiltin('divisors', [TNum], TList, function(n) {
    return math.divisors(n).map(function(n) {
        return new TNum(n)
    });
}
);
newBuiltin('proper_divisors', [TNum], TList, function(n) {
    return math.proper_divisors(n).map(function(n) {
        return new TNum(n)
    });
    }
);

/** Work out which number type best represents a range: if all values are integers, return `TInt`, otherwise `TNum`.
 *
 * @param {Numbas.math.range} range
 * @returns {Function} - a token constructor
 */
function best_number_type_for_range(range) {
    if(util.isInt(range[0]) && util.isInt(range[2]) && range[2] != 0) {
        return TInt;
    } else {
        return TNum;
    }
}
newBuiltin('random', [TRange], TNum, null, {
    evaluate: function(args, scope) {
        var range = args[0];
        var n = math.random(range.value);
        var cons = best_number_type_for_range(range.value);
        return new cons(n);
    },
    random:true
});
newBuiltin('random', [TList], '?', null, {
    random:true,
    evaluate: function(args, scope) {
        return math.choose(args[0].value);
    }
});
newBuiltin('random', ['*?'], '?', null, {
    random:true,
    evaluate: function(args, scope) {
        return math.choose(args);
    }
});
newBuiltin('weighted_random', [sig.listof(sig.list(sig.anything(), sig.type('number')))], '?', null, {
    evaluate: function(args, scope) {
        var items = args[0].value.map(function(item) {
            return [item.value[0], Numbas.jme.unwrapValue(item.value[1])];
        });
        return math.weighted_random(items);
    },
    random: true
});

newBuiltin('seedrandom', ['?', '?'], '?', null, {
    evaluate: function(args, scope) {
        const seed = Numbas.jme.unwrapValue(scope.evaluate(args[0]));
        const orandom = Math.random;
        Math.seedrandom(seed);
        let result;
        try {
            result = scope.evaluate(args[1]);
        } finally {
            Math.random = orandom;
        }
        return result;
    }
});
jme.lazyOps.push('seedrandom');
jme.isDeterministicOps['seedrandom'] = function(expr, scope) {
    // The second argument is always deterministic.
    return jme.isDeterministic(expr.args[0], scope);
}

newBuiltin('mod', [TNum, TNum], TNum, math.mod);
newBuiltin('max', [TNum, TNum], TNum, math.max);
newBuiltin('min', [TNum, TNum], TNum, math.min);
newBuiltin('clamp', [TNum, TNum, TNum], TNum, function(x, min, max) {
    return math.max(math.min(x, max), min);
});
newBuiltin('max', [TRange], TNum, function(range) {
    return range[1];
});
newBuiltin('min', [TRange], TNum, function(range) {
    return range[0];
});
newBuiltin('max', [sig.listof(sig.type('number'))], TNum, math.listmax, {unwrapValues: true});
newBuiltin('min', [sig.listof(sig.type('number'))], TNum, math.listmin, {unwrapValues: true});
/**
 * Define a builtin function with input signature `type, number` which returns a number-like type with the `precisionType` attribute specified.
 *
 * @param {string} name - The name of the functoin.
 * @param {Function} fn - The function.
 * @param {Function} type - The constructor for the type of the first argument, which must be the same as the output.
 * @param {string} precisionType - The precision type of the returned number.
 */
function function_with_precision_info(name, fn, type, precisionType) {
    newBuiltin(name, [type, TNum], type, function(a, precision) {
        var r = fn(a, precision);
        var t = new type(r);
        t.precisionType = precisionType;
        t.precision = precision;
        return t;
    }, {unwrapValues: true});
}

function_with_precision_info('precround', math.precround, TNum, 'dp');
function_with_precision_info('precround', matrixmath.precround, TMatrix, 'dp');
function_with_precision_info('precround', vectormath.precround, TVector, 'dp');
function_with_precision_info('siground', math.siground, TNum, 'sigfig');
function_with_precision_info('siground', matrixmath.siground, TMatrix, 'sigfig');
function_with_precision_info('siground', vectormath.siground, TVector, 'sigfig');
newBuiltin('dpformat', [TNum, TNum], TString, function(n, p) {
    return math.niceNumber(n, {precisionType: 'dp', precision:p});
}, {latex: true});
newBuiltin('dpformat', [TNum, TNum, TString], TString, function(n, p, style) {
    return math.niceNumber(n, {precisionType: 'dp', precision:p, style: style});
}, {latex: true});
newBuiltin('sigformat', [TNum, TNum], TString, function(n, p) {
    return math.niceNumber(n, {precisionType: 'sigfig', precision:p});
}, {latex: true});
newBuiltin('sigformat', [TNum, TNum, TString], TString, function(n, p, style) {
    return math.niceNumber(n, {precisionType: 'sigfig', precision:p, style:style});
}, {latex: true});
newBuiltin('formatnumber', [TNum, TString], TString, function(n, style) {
    return math.niceNumber(n, {style:style});
});
newBuiltin('string', [TNum], TString, math.niceNumber);
newBuiltin('parsenumber', [TString, TString], TNum, function(s, style) {
    return util.parseNumber(s, false, style, true);
});
newBuiltin('parsenumber', [TString, sig.listof(sig.type('string'))], TNum, function(s, styles) {
    return util.parseNumber(s, false, styles, true);
}, {unwrapValues: true});
newBuiltin('parsenumber_or_fraction', [TString], TNum, function(s) {
    return util.parseNumber(s, true, "plain-en", true);
});
newBuiltin('parsenumber_or_fraction', [TString, TString], TNum, function(s, style) {
    return util.parseNumber(s, true, style, true);
});
newBuiltin('parsenumber_or_fraction', [TString, sig.listof(sig.type('string'))], TNum, function(s, styles) {
    return util.parseNumber(s, true, styles, true);
}, {unwrapValues: true});

newBuiltin('with_precision', [TNum, 'nothing or number', 'nothing or string'], TNum, null, {
    evaluate: function(args, scope) {
        var n = args[0];
        var precision = args[1];
        var precisionType = args[2];

        if(jme.isType(precision, 'nothing')) {
            delete n.precision;
        } else {
            n.precision = precision.value;
        }

        if(jme.isType(precisionType, 'nothing')) {
            delete n.precisionType;
        } else {
            n.precisionType = precisionType.value;
        }

        return n;
    }
});

newBuiltin('imprecise', [TNum], TNum, null, {
    evaluate: function(args, scope) {
        var n = args[0];

        delete n.precision;
        delete n.precisionType;

        return n;
    }
});

newBuiltin('parsedecimal', [TString, TString], TDecimal, function(s, style) {
    return util.parseDecimal(s, false, style, true);
});
newBuiltin('parsedecimal', [TString, sig.listof(sig.type('string'))], TDecimal, function(s, styles) {
    return util.parseDecimal(s, false, styles, true);
}, {unwrapValues: true});
newBuiltin('parsedecimal_or_fraction', [TString], TDecimal, function(s, style) {
    return util.parseDecimal(s, true, "plain-en", true);
});
newBuiltin('parsedecimal_or_fraction', [TString, TString], TDecimal, function(s, style) {
    return util.parseDecimal(s, true, style, true);
});
newBuiltin('parsedecimal_or_fraction', [TString, sig.listof(sig.type('string'))], TDecimal, function(s, styles) {
    return util.parseDecimal(s, true, styles, true);
}, {unwrapValues: true});

newBuiltin('tobinary', [TInt], TString, function(n) {
    return n.toString(2);
}, {latex: true});
newBuiltin('tooctal', [TInt], TString, function(n) {
    return n.toString(8);
}, {latex: true});
newBuiltin('tohexadecimal', [TInt], TString, function(n) {
    return n.toString(16);
}, {latex: true});
newBuiltin('tobase', [TInt, TInt], TString, function(n, b) {
    return n.toString(b);
}, {latex: true});
newBuiltin('frombinary', [TString], TInt, function(s) {
    return util.parseInt(s, 2);
});
newBuiltin('fromoctal', [TString], TInt, function(s) {
    return util.parseInt(s, 8);
});
newBuiltin('fromhexadecimal', [TString], TInt, function(s) {
    return util.parseInt(s, 16);
});
newBuiltin('frombase', [TString, TInt], TInt, function(s, b) {
    return util.parseInt(s, b);
});

newBuiltin('scientificnumberlatex', [TNum], TString, null, {
    evaluate: function(args, scope) {
        var n = args[0].value;
        if(n.complex) {
            n = n.re;
        }
        var bits = math.parseScientific(math.niceRealNumber(n, {style:'scientific', scientificStyle: 'plain'}));
        var s = new TString(math.niceRealNumber(bits.significand, {syntax:'latex'}) + ' \\times 10^{' + bits.exponent + '}');
        s.latex = true;
        s.safe = true;
        s.display_latex = true;
        return s;
    }
});
newBuiltin('scientificnumberlatex', [TDecimal], TString, null, {
    evaluate: function(args, scope) {
        var n = args[0].value;
        var bits = math.parseScientific(n.re.toExponential());
        var s = new TString(math.niceRealNumber(bits.significand) + ' \\times 10^{' + bits.exponent + '}');
        s.latex = true;
        s.safe = true;
        s.display_latex = true;
        return s;
    }
});
newBuiltin('scientificnumberhtml', [TDecimal], THTML, function(n) {
    var bits = math.parseScientific(n.re.toExponential());
    var s = document.createElement('span');
    s.innerHTML = math.niceRealNumber(bits.significand) + ' × 10<sup>' + bits.exponent + '</sup>';
    s.setAttribute('data-interactive', 'false');
    return s;
});
newBuiltin('scientificnumberhtml', [TNum], THTML, function(n) {
    if(n.complex) {
        n = n.re;
    }
    var bits = math.parseScientific(math.niceRealNumber(n, {style:'scientific', scientificStyle:'plain'}));
    var s = document.createElement('span');
    s.innerHTML = math.niceRealNumber(bits.significand) + ' × 10<sup>' + bits.exponent + '</sup>';
    s.setAttribute('data-interactive', 'false');
    return s;
});

newBuiltin('togivenprecision', [TString, TString, TNum, TBool], TBool, math.toGivenPrecision);
newBuiltin('togivenprecision_scientific', [TString, TString, TNum], TBool, math.toGivenPrecisionScientific);
newBuiltin('withintolerance', [TNum, TNum, TNum], TBool, math.withinTolerance);
newBuiltin('countdp', [TString], TNum, function(s) {
    return math.countDP(util.cleanNumber(s));
});
newBuiltin('countsigfigs', [TString], TNum, function(s) {
    return math.countSigFigs(util.cleanNumber(s));
});
newBuiltin('isnan', [TNum], TBool, function(n) {
    if(n.complex) {
        return isNaN(n.re) || isNaN(n.im);
    }
    return isNaN(n);
});
newBuiltin('matchnumber', [TString, sig.listof(sig.type('string'))], TList, function(s, styles) {
    var result = util.matchNotationStyle(s, styles, true);
    return [new TString(result.matched), new TNum(util.parseNumber(result.cleaned, false, ['plain'], true))];
}, {unwrapValues:true});
newBuiltin('cleannumber', [TString, sig.optional(sig.listof(sig.type('string')))], TString, util.cleanNumber, {unwrapValues:true});
newBuiltin('isbool', [TString], TBool, util.isBool);
newBuiltin('perm', [TNum, TNum], TNum, math.permutations);
newBuiltin('comb', [TNum, TNum], TNum, math.combinations);
newBuiltin('root', [TNum, TNum], TNum, math.root);
newBuiltin('award', [TNum, TBool], TNum, function(a, b) {
    return (b ? a : 0);
});
newBuiltin('gcd', [TNum, TNum], TNum, math.gcd);
newBuiltin('gcd', [TInt, TInt], TInt, function(a,b) { return new TInt(math.gcd(a,b)); },{unwrapValues: true});
newBuiltin('gcd_without_pi_or_i', [TNum, TNum], TNum, function(a, b) {    // take out factors of pi or i before working out gcd. Used by the fraction simplification rules
        if(a.complex && a.re == 0) {
            a = a.im;
        }
        if(b.complex && b.re == 0) {
            b = b.im;
        }
        a = a / math.pow(Math.PI, math.piDegree(a));
        b = b / math.pow(Math.PI, math.piDegree(b));
        return math.gcf(a, b);
});
newBuiltin('coprime', [TNum, TNum], TBool, math.coprime);
newBuiltin('lcm', [sig.multiple(sig.type('number'))], TNum, math.lcm);
newBuiltin('lcm', [sig.multiple(sig.type('integer'))], TInt, function() {
    return new TInt(math.lcm.apply(math, arguments));
},{unwrapValues: true});
newBuiltin('lcm', [sig.listof(sig.type('integer'))], TInt, function(l) {
        if(l.length == 0) {
            return new TInt(1);
        } else if(l.length == 1) {
            return new TInt(l[0]);
        } else {
            return new TInt(math.lcm.apply(math, l));
        }
    },
    {unwrapValues: true}
);
newBuiltin('lcm', [sig.listof(sig.type('number'))], TNum, function(l) {
        if(l.length == 0) {
            return 1;
        } else if(l.length == 1) {
            return l[0];
        } else {
            return math.lcm.apply(math, l);
        }
    },
    {unwrapValues: true}
);
newBuiltin('|', [TNum, TNum], TBool, math.divides);


var Fraction = math.Fraction;

// Integer arithmetic
newBuiltin('int', [TNum], TInt, function(n) {
    return n;
});
newBuiltin('+u', [TInt], TInt, function(a) {
    return a;
});
newBuiltin('-u', [TInt], TInt, math.negate);
newBuiltin('+', [TInt, TInt], TInt, math.add);
newBuiltin('-', [TInt, TInt], TInt, math.sub);
newBuiltin('*', [TInt, TInt], TInt, math.mul);
newBuiltin('/', [TInt, TInt], TRational, function(a, b) {
    return new Fraction(a, b);
});
newBuiltin('^', [TInt, TInt], TNum, function(a, b) {
    return math.pow(a, b);
});
newBuiltin('mod', [TInt, TInt], TInt, math.mod);
newBuiltin('string', [TInt], TString, math.niceNumber);
newBuiltin('max', [TInt, TInt], TInt, math.max);
newBuiltin('min', [TInt, TInt], TInt, math.min);
newBuiltin('max', [sig.listof(sig.type('integer'))], TInt, math.listmax, {unwrapValues: true});
newBuiltin('min', [sig.listof(sig.type('integer'))], TInt, math.listmin, {unwrapValues: true});

// Rational arithmetic
newBuiltin('+u', [TRational], TRational, function(a) {
    return a;
});
newBuiltin('-u', [TRational], TRational, function(r) {
    return r.negate();
});
newBuiltin('+', [TRational, TRational], TRational, function(a, b) {
    return a.add(b);
});
newBuiltin('-', [TRational, TRational], TRational, function(a, b) {
    return a.subtract(b);
});
newBuiltin('*', [TRational, TRational], TRational, function(a, b) {
    return a.multiply(b);
});
newBuiltin('*', [TRational, TNum], TNum, function(a, b) {
    return math.mul(a.toFloat(), b);
});
newBuiltin('/', [TRational, TRational], TRational, function(a, b) {
    return a.divide(b);
});
newBuiltin('^', [TRational, TInt], TRational, function(a, b) {
    return a.pow(b);
});
newBuiltin('max', [TRational, TRational], TRational, Fraction.max);
newBuiltin('min', [TRational, TRational], TRational, Fraction.min);
newBuiltin('max', [sig.listof(sig.type('rational'))], TRational, function(l) {
    return Fraction.max.apply(Fraction, l);
}, {unwrapValues: true});
newBuiltin('min', [sig.listof(sig.type('rational'))], TRational, function(l) {
    return Fraction.min.apply(Fraction, l);
}, {unwrapValues: true});
newBuiltin('trunc', [TRational], TInt, function(a) {
    return a.trunc();
});
newBuiltin('floor', [TRational], TInt, function(a) {
    return a.floor();
});
newBuiltin('ceil', [TRational], TInt, function(a) {
    return a.ceil();
});
newBuiltin('fract', [TRational], TRational, function(a) {
    return a.fract();
});

newBuiltin('string', [TRational], TString, function(a) {
    return a.toString();
});
newBuiltin('rational', [TNum], TRational, function(n) {
    var r = math.rationalApproximation(n);
    return new Fraction(r[0], r[1]);
});

//Decimal arithmetic
newBuiltin('string', [TDecimal], TString, math.niceComplexDecimal);

newBuiltin('decimal', [TNum], TDecimal, null, {
    evaluate: function(args, scope) {
        if(args.length !== 1) {
            throw(new Numbas.Error("jme.typecheck.no right type definition", {op:'decimal'}));
        }
        /**
         * Replace all occurrences of the `number` type in an expression with the equivalent `decimal` value.
         *
         * @param {Numbas.jme.tree} tree
         * @returns {Numbas.jme.tree}
         */
        function replace_number(tree) {
            var ntree = {};
            if(tree.args) {
                ntree.args = tree.args.map(replace_number);
            }
            var tok;
            switch(tree.tok.type) {
                case 'number':
                    var n = tree.tok;
                    var d = (typeof n.originalValue == 'string') ? new math.ComplexDecimal(new Decimal(n.originalValue)) : math.numberToDecimal(n.value);
                    tok = new TDecimal(d);
                    tok.precisionType = n.precisionType;
                    tok.precision = n.precision;
                    break;
                default:
                    tok = tree.tok;
            }
            tree.tok = tok;
            return tree;
        }
        var tree = replace_number(args[0]);
        var arg = scope.evaluate(tree);
        if(jme.isType(arg, 'decimal')) {
            return jme.castToType(arg, 'decimal');
        } else if(jme.isType(arg, 'number')) {
            const n = jme.castToType(arg, 'number');
            const d = math.numberToDecimal(n.value);
            const t = new TDecimal(d);
            t.precisionType = n.precisionType;
            t.precision = n.precision;
            return t;
        } else if(jme.isType(arg, 'string')) {
            const s = jme.castToType(arg, 'string').value;
            const d = new Decimal(s);
            const t = new TDecimal(d);
            t.precisionType = 'dp';
            t.precision = math.countDP(s);
            return t;
        } else {
        }
    }
});
Numbas.jme.lazyOps.push('decimal');
newBuiltin('decimal', [TRational], TDecimal, null, {
    evaluate: function(args, scope) {
        var n = args[0];
        return new TDecimal((new Decimal(n.value.numerator)).dividedBy(new Decimal(n.value.denominator)));
    }
});
newBuiltin('decimal', [TString], TDecimal, function(x) {
    var d = new Decimal(x);
    var t = new TDecimal(d);
    t.precisionType = 'dp';
    t.precision = math.countDP(x);
    return t;
}, {unwrapValues:true});
newBuiltin('+u', [TDecimal], TDecimal, function(a) {
    return a;
});
newBuiltin('-u', [TDecimal], TDecimal, function(a) {
    return a.negated();
});
newBuiltin('+', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return a.plus(b);
});
newBuiltin('+', [TNum, TDecimal], TDecimal, function(a, b) {
    return math.ensure_decimal(a).plus(b);
});
newBuiltin('-', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return a.minus(b);
});
newBuiltin('-', [TNum, TDecimal], TDecimal, function(a, b) {
    return math.ensure_decimal(a).minus(b);
});
newBuiltin('*', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return a.times(b);
});
newBuiltin('/', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return a.dividedBy(b);
});
newBuiltin('/', [TNum, TDecimal], TDecimal, function(a, b) {
    return math.ensure_decimal(a).dividedBy(b);
});
newBuiltin('abs', [TDecimal], TDecimal, function(a) {
    return a.absoluteValue();
});
newBuiltin('ceil', [TDecimal], TDecimal, function(a) {
    return a.re.ceil();
});
newBuiltin('cos', [TDecimal], TDecimal, function(a) {
    return a.re.cos();
});
newBuiltin('countdp', [TDecimal], TInt, function(a) {
    return a.decimalPlaces();
});
newBuiltin('floor', [TDecimal], TDecimal, function(a) {
    return a.re.floor();
});
newBuiltin('>', [TDecimal, TDecimal], TBool, function(a, b) {
    return a.greaterThan(b);
});
newBuiltin('>=', [TDecimal, TDecimal], TBool, function(a, b) {
    return a.greaterThanOrEqualTo(b);
});
newBuiltin('>=', [TDecimal, TNum], TBool, function(a, b) {
    return math.geq(a.re.toNumber(), b);
});
newBuiltin('cosh', [TDecimal], TDecimal, function(a) {
    return a.re.cosh();
});
newBuiltin('sinh', [TDecimal], TDecimal, function(a) {
    return a.re.sinh();
});
newBuiltin('tanh', [TDecimal], TDecimal, function(a) {
    return a.re.tanh();
});
newBuiltin('arccos', [TDecimal], TDecimal, function(a) {
    return a.re.acos();
});
newBuiltin('arccosh', [TDecimal], TDecimal, function(a) {
    return a.re.acosh();
});
newBuiltin('arcsinh', [TDecimal], TDecimal, function(a) {
    return a.re.asinh();
});
newBuiltin('arctanh', [TDecimal], TDecimal, function(a) {
    return a.re.atanh();
});
newBuiltin('arcsin', [TDecimal], TDecimal, function(a) {
    return a.re.asin();
});
newBuiltin('arctan', [TDecimal], TDecimal, function(a) {
    return a.re.atan();
});
newBuiltin('atan2', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return Decimal.atan2(a.re, b.re);
});
newBuiltin('isint', [TDecimal], TBool, function(a) {
    return a.isInt();
})
newBuiltin('isnan', [TDecimal], TBool, function(a) {
    return a.isNaN();
})
newBuiltin('iszero', [TDecimal], TBool, function(a) {
    return a.isZero();
})
newBuiltin('<', [TDecimal, TDecimal], TBool, function(a, b) {
    return a.lessThan(b);
});
newBuiltin('<=', [TDecimal, TDecimal], TBool, function(a, b) {
    return a.lessThanOrEqualTo(b);
});
newBuiltin('<=', [TDecimal, TNum], TBool, function(a, b) {
    return math.leq(a.re.toNumber(), b);
});
newBuiltin('log', [TDecimal], TDecimal, function(a) {
    return a.re.log();
})
newBuiltin('log', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return a.re.log().div(b.re.log());
})
newBuiltin('mod', [TDecimal, TDecimal], TDecimal, function(a, b) {
    var m = a.re.mod(b.re);
    if(m.isNegative()) {
        m = m.plus(b.re);
    }
    return m;
});
newBuiltin('exp', [TDecimal], TDecimal, function(a) {
    return a.exp();
});
newBuiltin('ln', [TDecimal], TDecimal, function(a) {
    return a.ln();
});
newBuiltin('arg', [TDecimal], TDecimal, function(a) {
    return a.argument();
});
newBuiltin('countsigfigs', [TDecimal], TInt, function(a) {
    return a.re.countSigFigs();
});
newBuiltin('round', [TDecimal], TDecimal, function(a) {
    return a.round();
});
newBuiltin('sin', [TDecimal], TDecimal, function(a) {
    return a.re.sin();
});
newBuiltin('sqrt', [TDecimal], TDecimal, function(a) {
    return a.squareRoot();
});
newBuiltin('tan', [TDecimal], TDecimal, function(a) {
    return a.re.tan();
});
function_with_precision_info('precround', function(a, dp) {
    return a.toDecimalPlaces(dp);
}, TDecimal, 'dp');
newBuiltin('min', [TDecimal, TDecimal], TDecimal, math.ComplexDecimal.min);
newBuiltin('max', [TDecimal, TDecimal], TDecimal, math.ComplexDecimal.max);
newBuiltin('max', [sig.listof(sig.type('decimal'))], TDecimal, function(l) {
    return math.listmax(l, math.ComplexDecimal.max);
}, {unwrapValues: true});
newBuiltin('min', [sig.listof(sig.type('decimal'))], TDecimal, function(l) {
    return math.listmin(l, math.ComplexDecimal.min);
}, {unwrapValues: true});
newBuiltin('dpformat', [TDecimal, TNum], TString, function(a, dp) {
    return a.toFixed(dp);
});
newBuiltin('tonearest', [TDecimal, TDecimal], TDecimal, function(a, x) {
    return a.toNearest(x.re);
});
newBuiltin('^', [TDecimal, TDecimal], TDecimal, function(a, b) {
    return a.pow(b);
});
newBuiltin('^', [TInt, TDecimal], TDecimal, function(a, b) {
    return math.ensure_decimal(a).pow(b);
});
newBuiltin('sigformat', [TDecimal, TNum], TString, function(a, sf) {
    return a.toPrecision(sf);
});
function_with_precision_info('siground', function(a, dp) {
    return a.toSignificantDigits(dp);
}, TDecimal, 'sigfig');
newBuiltin('formatnumber', [TDecimal, TString], TString, function(n, style) {
    return math.niceComplexDecimal(n, {style:style});
});
newBuiltin('trunc', [TDecimal], TDecimal, function(a) {
    return a.re.trunc();
});
newBuiltin('fract', [TDecimal], TDecimal, function(a) {
    return a.re.minus(a.re.trunc());
});



newBuiltin('sum', [sig.listof(sig.type('number'))], TNum, math.sum, {unwrapValues: true});
newBuiltin('sum', [sig.listof(sig.type('integer'))], TInt, function(list) {
    return new TInt(math.sum(list));
}, {unwrapValues: true});
newBuiltin('sum', [sig.listof(sig.type('decimal'))], TDecimal, function(list) {
    let total = math.ensure_decimal(0);
    for(let x of list) {
        total = total.plus(x);
    }
    return total;
}, {unwrapValues: true});
newBuiltin('sum', [sig.listof(sig.type('rational'))], TRational, function(list) {
    let total = new Fraction(0,1);
    for(let x of list) {
        total = total.add(x);
    }
    return total;
}, {unwrapValues: true});
newBuiltin('sum', [TVector], TNum, math.sum);

newBuiltin('prod', [sig.listof(sig.type('number'))], TNum, math.prod, {unwrapValues: true});
newBuiltin('prod', [sig.listof(sig.type('integer'))], TInt, function(list) {
    return new TInt(math.prod(list));
}, {unwrapValues: true});
newBuiltin('prod', [sig.listof(sig.type('decimal'))], TDecimal, function(list) {
    let total = math.ensure_decimal(1);
    for(let x of list) {
        total = total.times(x);
    }
    return total;
}, {unwrapValues: true});
newBuiltin('prod', [sig.listof(sig.type('rational'))], TRational, function(list) {
    let total = new Fraction(1,1);
    for(let x of list) {
        total = total.multiply(x);
    }
    return total;
}, {unwrapValues: true});
newBuiltin('prod', [TVector], TNum, math.prod);
newBuiltin('deal', [TNum], TList,
    function(n) {
        return math.deal(n).map(function(i) {
            return new TNum(i);
        });
    },
    {
        random:true
    }
);
newBuiltin('shuffle', [TList], TList,
    function(list) {
        return math.shuffle(list);
    },
    {
        random:true
    }
);
newBuiltin('reorder', [TList, sig.listof(sig.type('number'))], TList, function(list, order) {
    order = order.map(function(n) {
        return n.value;
    });
    return math.reorder(list, order);
});
newBuiltin('shuffle_together', [sig.listof(sig.type('list'))], TList, function(lists) {
    lists = lists.map(function(l) {
        return l.value;
    });
    lists = math.shuffle_together(lists);
    return lists.map(function(l) {
        return new TList(l);
    });
}, {random: true});

newBuiltin('random_integer_partition', [TNum, TNum], TList, function(n, k) {
    return math.random_integer_partition(n, k).map(function(x) {
        return new TInt(x);
    })
}, {random: true});

//if needs to be a bit different because it can return any type
newBuiltin('if', [TBool, '?', '?'], '?', null, {
    evaluate: function(args, scope) {
        if(args.length !== 3) {
            throw(new Numbas.Error("jme.typecheck.no right type definition", {op:'if'}));
        }
        var test = jme.evaluate(args[0], scope);
        if(jme.isType(test, 'boolean')) {
            test = jme.castToType(test, 'boolean').value;
        } else {
            // If the test can't be cast to a boolean, use JS's truthiness test on the value attribute.
            // Ideally this should throw an error, but I don't know if anything depends on this undocumented behaviour.
            test = test.value;
        }
        if(test) {
            return jme.evaluate(args[1], scope);
        } else {
            return jme.evaluate(args[2], scope);
        }
    }
});
Numbas.jme.lazyOps.push('if');
newBuiltin('switch', [sig.multiple(sig.sequence(sig.type('boolean'), sig.anything())), '?'], '?', null, {
    evaluate: function(args, scope) {
        for(let i = 0; i < args.length - 1; i += 2) {
            var result = jme.evaluate(args[i], scope).value;
            if(result) {
                return jme.evaluate(args[i + 1], scope);
            }
        }
        if(args.length % 2 == 1) {
            return jme.evaluate(args.at(-1), scope);
        } else {
            throw(new Numbas.Error('jme.func.switch.no default case'));
        }
    }
});
Numbas.jme.lazyOps.push('switch');
newBuiltin('isa', ['?', TString], TBool, null, {
    evaluate: function(args, scope) {
        var tok = args[0].tok;
        var kind = jme.evaluate(args[1], scope).value;
        if(tok.type == 'name') {
            var c = scope.getConstant(tok.name);
            if(c) {
                tok = c.value;
            }
        }
        if(tok.type == 'name' && scope.getVariable(tok.name) == undefined) {
            return new TBool(kind == 'name');
        }
        tok = scope.evaluate(args[0]);
        var match = false;
        if(kind == 'complex') {
            match = jme.isType(tok, 'number') && tok.value.complex || false;
        } else {
            match = jme.isType(tok, kind);
        }
        return new TBool(match);
    }
});
Numbas.jme.lazyOps.push('isa');
// repeat(expr,n) evaluates expr n times and returns a list of the results
newBuiltin('repeat', ['?', TNum], TList, null, {
    evaluate: function(args, scope) {
        var size = jme.evaluate(args[1], scope).value;
        var value = [];
        for(let i = 0;i < size;i++) {
            value[i] = jme.evaluate(args[0], scope);
        }
        return new TList(value);
    }
});
Numbas.jme.lazyOps.push('repeat');

/** Evaluate the given expressions until the list of conditions is satisfied.
 *
 * @param {Array.<string>} names - Names for each expression.
 * @param {Array.<Numbas.jme.tree>} definitions - Definition of each expression.
 * @param {Array.<Numbas.jme.tree>} conditions - Expressions in terms of the assigned names, which should evaluate to `true` if the values are acceptable.
 * @param {Numbas.jme.Scope} scope - The scope in which to evaluate everything.
 * @param {number} [maxRuns=100] - The maximum number of times to try to generate a set of values.
 * @returns {Object<Numbas.jme.token>} - A dictionary mapping names to their generated values.
 */
function satisfy(names, definitions, conditions, scope, maxRuns) {
        maxRuns = maxRuns === undefined ? 100 : maxRuns;
        if(definitions.length != names.length) {
            throw(new Numbas.Error('jme.func.satisfy.wrong number of definitions'));
        }
        var satisfied = false;
        var runs = 0;
        while(runs < maxRuns && !satisfied) {
            runs += 1;
            var variables = {};
            for(let i = 0; i < names.length; i++) {
                variables[names[i]] = scope.evaluate(definitions[i]);
            }
            var nscope = new jme.Scope([scope, {variables:variables}]);
            satisfied = true;
            for(let i = 0; i < conditions.length; i++) {
                var ok = nscope.evaluate(conditions[i]);
                if(ok.type != 'boolean') {
                    throw(new Numbas.Error('jme.func.satisfy.condition not a boolean'));
                }
                if(!ok.value) {
                    satisfied = false;
                    break;
                }
            }
        }
        if(!satisfied) {
            throw(new Numbas.Error('jme.func.satisfy.took too many runs'));
        }
        return variables;
}
newBuiltin('satisfy', [TList, TList, TList, TNum], TList, null, {
    evaluate: function(args, scope) {
        var names = args[0].args.map(function(t) {
            return t.tok.name;
        });
        var definitions = args[1].args;
        var conditions = args[2].args;
        var maxRuns = args.length > 3 ? scope.evaluate(args[3]).value : 100;
        var variables = satisfy(names, definitions, conditions, scope, maxRuns);
        return new TList(names.map(function(name) {
            return variables[name];
        }));
    }
});
Numbas.jme.lazyOps.push('satisfy');
jme.findvarsOps.satisfy = function(tree, boundvars, scope) {
    var names = tree.args[0].args.map(function(t) {
        return t.tok.name
    });
    boundvars = boundvars.concat(0, 0, names);
    var vars = [];
    for(let i = 1;i < tree.args.length;i++) {
        vars = vars.merge(jme.findvars(tree.args[i], boundvars, scope));
    }
    return vars;
}
newBuiltin('listval', [TList, TNum], '?', null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var index = util.wrapListIndex(args[1].value, list.vars);
        if(list.type != 'list') {
            if(list.type == 'name') {
                throw(new Numbas.Error('jme.variables.variable not defined', {name:list.name}));
            } else {
                throw(new Numbas.Error('jme.func.listval.not a list'));
            }
        }
        if(index in list.value) {
            return list.value[index];
        } else {
            throw(new Numbas.Error('jme.func.listval.invalid index', {index:index, size:list.value.length}));
        }
    }
});
newBuiltin('listval', [TList, TRange], TList, null, {
    evaluate: function(args, scope) {
        var range = args[1].value;
        var list = args[0];
        var size = list.vars;
        var start = util.wrapListIndex(range[0], size);
        var end = util.wrapListIndex(range[1], size);
        var step = range[2];
        var value;
        if(step != 1) {
            value = [];
            for(let i = start;i < end;i += step) {
                if(i % 1 == 0) {
                    value.push(list.value[i]);
                }
            }
        } else {
            value = list.value.slice(start, end);
        }
        return new TList(value);
    }
});
newBuiltin('listval', [TVector, TNum], TNum, null, {
    evaluate: function(args, scope) {
        var vector = args[0].value;
        var index = util.wrapListIndex(args[1].value, vector.length);
        return new TNum(vector[index] || 0);
    }
});
newBuiltin('listval', [TVector, TRange], TVector, null, {
    evaluate: function(args, scope) {
        var range = args[1].value;
        var vector = args[0].value;
        var start = util.wrapListIndex(range[0], vector.length);
        var end = util.wrapListIndex(range[1], vector.length);
        var v = [];
        for(let i = start;i < end;i++) {
            v.push(vector[i] || 0);
        }
        return new TVector(v);
    }
});
newBuiltin('listval', [TMatrix, TNum], TVector, null, {
    evaluate: function(args, scope) {
        var matrix = args[0].value;
        var index = util.wrapListIndex(args[1].value, matrix.length);
        return new TVector(matrix[index] || []);
    }
});
newBuiltin('listval', [TMatrix, TRange], TMatrix, null, {
    evaluate: function(args, scope) {
        var range = args[1].value;
        var matrix = args[0].value;
        var start = util.wrapListIndex(range[0], matrix.length);
        var end = util.wrapListIndex(range[1], matrix.length);
        var sliced_matrix = matrix.slice(start, end);
        sliced_matrix.columns = matrix.columns;
        sliced_matrix.rows = end - start;
        return new TMatrix(sliced_matrix);
    }
});
newBuiltin('flatten', ['list of list'], TList, null, {
    evaluate: function(args, scope) {
        var o = [];
        args[0].value.forEach(function(l) {
            o = o.concat(l.value);
        });
        return new TList(o);
    }
});
newBuiltin('isset', [TName], TBool, null, {
    evaluate: function(args, scope) {
        var name = args[0].tok.name;
        return new TBool(name in scope.variables);
    }
});
Numbas.jme.lazyOps.push('isset');
jme.findvarsOps.isset = function(tree, boundvars, scope) {
    return boundvars;
}
jme.substituteTreeOps.isset = function(tree, scope, allowUnbound) {
    return tree;
}
/** Map the given expression, considered as a lambda, over the given list.
 *
 * @param {Numbas.jme.types.TLambda} lambda
 * @param {Numbas.jme.types.TList} list - The list to map over.
 * @param {Numbas.jme.Scope} scope - The scope in which to evaluate.
 * @returns {Numbas.jme.types.TList}
 */
function mapOverList(lambda, list, scope) {
    var olist = list.map(function(v) {
        return lambda.evaluate([v], scope);
    });
    return new TList(olist);
}
/** Functions for 'map', by the type of the thing being mapped over.
 * Functions take a JME expression lambda, a name or list of names to map, a value to map over, and a scope to evaluate against.
 *
 * @memberof Numbas.jme
 * @name mapFunctions
 * @enum {Function}
 */
jme.mapFunctions = {
    'list': mapOverList,
    'set': mapOverList,
    'range': function(lambda, range, scope) {
        var list = math.rangeToList(range).map(function(n) {
            return new TNum(n)
        });
        return mapOverList(lambda, list, scope);
    },
    'matrix': function(lambda, matrix, scope) {
        return new TMatrix(matrixmath.map(matrix, function(n) {
            var o = lambda.evaluate([new TNum(n)], scope);
            if(!jme.isType(o, 'number')) {
                throw(new Numbas.Error("jme.map.matrix map returned non number"))
            }
            return jme.castToType(o, 'number').value;
        }));
    },
    'vector': function(lambda, vector, scope) {
        return new TVector(vectormath.map(vector, function(n) {
            var o = lambda.evaluate([new TNum(n)], scope);
            if(!jme.isType(o, 'number')) {
                throw(new Numbas.Error("jme.map.vector map returned non number"))
            }
            return jme.castToType(o, 'number').value;
        }));
    }
}
var fn_map = newBuiltin('map', ['?', TName, '?'], TList, null, {
    make_lambda: function(args, scope) {
        if(args[0].tok.type == 'lambda') {
            return args;
        }
        return [{tok: new TLambda([args[1]], args[0])}, args[2]];
    },
    evaluate: function(args, scope) {
        args = this.options.make_lambda(args, scope);

        var lambda = args[0].tok;

        var value = scope.evaluate(args[1]);

        if(!(value.type in jme.mapFunctions)) {
            throw(new Numbas.Error('jme.typecheck.map not on enumerable', {type:value.type}));
        }

        return jme.mapFunctions[value.type](lambda, value.value, scope);
    }
});
Numbas.jme.lazyOps.push('map');
jme.findvarsOps.map = function(tree, boundvars, scope) {
    return jme.findvars_args(fn_map.options.make_lambda(tree.args, scope), boundvars, scope);
}
jme.substituteTreeOps.map = function(tree, scope, allowUnbound) {
    var list_index = tree.args[0].tok.type == 'lambda' ? 1 : 2;
    tree.args[list_index] = jme.substituteTree(tree.args[list_index], scope, allowUnbound);
    return tree;
}
newBuiltin('for:', ['?', TName, '?'], TList, null, {
    evaluate: function(args, scope) {
        var lambda = args[0];

        var fors = [];

        /** Unfold chained applications of the `for:`, `of:` and `where:` operators.
         *
         * @param {Numbas.jme.tree} arg
         * @returns {object}
         */
        function unfold_for(arg) {
            if(jme.isOp(arg.tok, 'for:')) {
                unfold_for(arg.args[0]);
                unfold_for(arg.args[1]);
                return null;
            } else if(jme.isOp(arg.tok, 'where:')) {
                const f = unfold_for(arg.args[0]);
                f.where = arg.args[1];
                return null;
            } else if(jme.isOp(arg.tok, 'of:')) {
                var value_tree = arg.args[1];
                var namearg = arg.args[0];
                if(jme.isType(namearg.tok, 'name')) {
                    const f = {name: namearg.tok.name, value_tree};
                    fors.push(f);
                    return f;
                } else if(jme.isType(namearg.tok, 'list')) {
                    var names = namearg.args.map(function(subnamearg) {
                        if(!jme.isType(subnamearg.tok, 'name')) {
                            throw(new Numbas.Error('jme.typecheck.for in name wrong type', {type: subnamearg.tok.type}));
                        }
                        return subnamearg.tok.name;
                    });
                    const f = {names, value_tree};
                    fors.push(f);
                    return f;
                } else {
                    throw(new Numbas.Error('jme.typecheck.for in name wrong type', {type: namearg.tok.type}));
                }
            } else {
                throw(new Numbas.Error('jme.typecheck.no right type definition', {op:'for:'}));
            }
        }

        unfold_for(args[1]);

        scope = new Scope(scope);

        var indexes = fors.map(function() {
            return 0;
        });
        var values = fors.map(function() {
            return [];
        });

        var end = fors.length - 1;
        var out = [];
        var j = 0;

        /** After reaching the end of the mapping chain, go back a step and move to the next item in the last collection.
         */
        function retreat() {
            values[j] = [];
            if(fors[j].names !== undefined) {
                fors[j].names.forEach(function(name) {
                    scope.deleteVariable(name);
                });
            } else {
                scope.deleteVariable(fors[j].name);
            }
            indexes[j] = 0;
            j -= 1;
            if(j >= 0) {
                indexes[j] += 1;
            }
        }

        while(j >= 0) {
            if(indexes[j] == 0) {
                values[j] = jme.castToType(scope.evaluate(fors[j].value_tree), 'list').value;
                if(fors[j].names !== undefined) {
                    values[j] = values[j].map(function(v) {
                        return jme.castToType(v, 'list').value;
                    });
                }
            }
            var f = fors[j];
            while(indexes[j] < values[j].length) {
                var value = values[j][indexes[j]];
                if(f.name !== undefined) {
                    scope.setVariable(f.name, value);
                } else {
                    f.names.forEach(function(name, j) {
                        scope.setVariable(name, value[j]);
                    });
                }
                if(f.where === undefined) {
                    break;
                }
                var res = jme.castToType(scope.evaluate(f.where), 'boolean').value;
                if(res) {
                    break;
                }
                indexes[j] += 1;
            }
            if(indexes[j] >= values[j].length) {
                retreat();
                continue;
            }

            if(j == end) {
                out.push(scope.evaluate(lambda));
                indexes[j] += 1;
                while(j >= 0 && indexes[j] >= values[j].length) {
                    retreat();
                }
            } else {
                j += 1;
                if(j <= end) {
                    indexes[j] = 0;
                }
            }
        }

        return new TList(out);
    }
});
Numbas.jme.lazyOps.push('for:');
jme.findvarsOps['for:'] = function(tree, boundvars, scope) {
    var mapped_boundvars = boundvars.slice();
    var vars = [];

    /** Find variables used in part of a `.. for: .. of: ..` expression.
     *
     * @param {Numbas.jme.tree} arg
     */
    function visit_for(arg) {
        if(jme.isOp(arg.tok, 'for:')) {
            visit_for(arg.args[0]);
            visit_for(arg.args[1]);
        } else if(jme.isOp(arg.tok, 'where:')) {
            visit_for(arg.args[0]);
            vars = vars.merge(jme.findvars(arg.args[1], mapped_boundvars, scope));
        } else if(jme.isOp(arg.tok, 'of:')) {
            var namearg = arg.args[0];
            if(namearg.tok.type == 'list') {
                var names = namearg.args;
                for(let i = 0;i < names.length;i++) {
                    mapped_boundvars.push(jme.normaliseName(names[i].tok.name, scope));
                }
            } else {
                mapped_boundvars.push(jme.normaliseName(namearg.tok.name, scope));
            }
            vars = vars.merge(jme.findvars(arg.args[1], mapped_boundvars, scope));
        }
    }
    visit_for(tree.args[1]);
    vars = vars.merge(jme.findvars(tree.args[0], mapped_boundvars, scope));
    return vars;
}
jme.substituteTreeOps['for:'] = function(tree, scope, allowUnbound) {
    var nscope = new Scope([scope]);

    /** Substitute variables into part of a `.. for: .. of: ..` expression.
     *
     * @param {Numbas.jme.tree} arg
     * @returns {Numbas.jme.tree}
     */
    function visit_for(arg) {
        arg = {tok: arg.tok, args: arg.args.slice()};
        if(jme.isOp(arg.tok, 'for:')) {
            arg.args[0] = visit_for(arg.args[0]);
            arg.args[1] = visit_for(arg.args[1]);
        } else if(jme.isOp(arg.tok, 'when:')) {
            arg.args[0] = visit_for(arg.args[0]);
            arg.args[1] = visit_for(arg.args[1]);
        } else if(jme.isOp(arg.tok, 'of:')) {
            var namearg = arg.args[0];
            if(namearg.tok.type == 'list') {
                namearg.args.forEach(function(name) {
                    nscope.deleteVariable(name.tok.name);
                });
            } else {
                nscope.deleteVariable(namearg.tok.name);
            }
            arg.args[1] = jme.substituteTree(arg.args[1], nscope, true);
        } else {
            arg = jme.substituteTree(arg, nscope, true);
        }
        return arg;
    }
    tree.args[1] = visit_for(tree.args[1]);
    tree.args[0] = jme.substituteTree(tree.args[0], nscope, true);
    return tree;
}

var fn_filter = newBuiltin('filter', ['?', TName, '?'], TList, null, {
    make_lambda: function(args, scope) {
        if(args[0].tok.type == 'lambda') {
            return args;
        }
        return [{tok: new TLambda([args[1]], args[0])}, args[2]];
    },
    evaluate: function(args, scope) {
        args = this.options.make_lambda(args, scope);

        var lambda = args[0].tok;
        var list = jme.castToType(scope.evaluate(args[1]), 'list').value;

        var ovalue = list.filter(function(v) {
            return jme.castToType(lambda.evaluate([v], scope), 'boolean').value;
        });

        return new TList(ovalue);
    }
});
Numbas.jme.lazyOps.push('filter');
jme.findvarsOps.filter = function(tree, boundvars, scope) {
    return jme.findvars_args(fn_filter.options.make_lambda(tree.args), boundvars, scope);
}
jme.substituteTreeOps.filter = function(tree, scope, allowUnbound) {
    var list_index = tree.args[0].tok.type == 'lambda' ? 1 : 2;
    tree.args[list_index] = jme.substituteTree(tree.args[list_index], scope, allowUnbound);
    return tree;
}

var fn_iterate = newBuiltin('iterate', ['?', TName, '?', TNum], TList, null, {
    make_lambda: function(args, scope) {
        if(args[0].tok.type == 'lambda') {
            return args;
        }
        return [{tok: new TLambda([args[1]], args[0])}, args[2], args[3]];
    },
    evaluate: function(args, scope) {
        args = this.options.make_lambda(args, scope);

        var lambda = args[0].tok;
        var value = scope.evaluate(args[1]);
        var times = Math.round(jme.castToType(scope.evaluate(args[2]), 'number').value);

        var out = [value];
        for(let i = 0;i < times;i++) {
            value = lambda.evaluate([value], scope);
            out.push(value);
        }
        return new TList(out);
    }
});
Numbas.jme.lazyOps.push('iterate');
jme.findvarsOps.iterate = function(tree, boundvars, scope) {
    return jme.findvars_args(fn_iterate.options.make_lambda(tree.args), boundvars, scope);
}
jme.substituteTreeOps.iterate = function(tree, scope, allowUnbound) {
    var i = tree.args[0].tok.type == 'lambda' ? 0 : 1;
    tree.args[i + 1] = jme.substituteTree(tree.args[i + 1], scope, allowUnbound);
    tree.args[i + 2] = jme.substituteTree(tree.args[i + 2], scope, allowUnbound);
    return tree;
}

var fn_iterate_until = newBuiltin('iterate_until', ['?', TName, '?', '?', sig.optional(sig.type('number'))], TList, null, {
    make_lambda: function(args, scope) {
        if(args[0].tok.type == 'lambda') {
            return args;
        }
        return [{tok: new TLambda([args[1]], args[0])}, args[2], {tok: new TLambda([args[1]], args[3])}, args[4]];
    },

    evaluate: function(args, scope) {
        args = this.options.make_lambda(args, scope);

        var lambda = args[0].tok;
        var value = scope.evaluate(args[1]);
        var condition = args[2].tok;
        var max_iterations = args[3] ? jme.castToType(scope.evaluate(args[3]), 'number').value : 100;

        var out = [value];

        for(let n = 0;n < max_iterations;n++) {
            var stop = condition.evaluate([value], scope);
            if(!jme.isType(stop, 'boolean')) {
                throw(new Numbas.Error('jme.iterate_until.condition produced non-boolean', {type: stop.type}));
            } else {
                stop = jme.castToType(stop, 'boolean');
                if(stop.value) {
                    break;
                }
            }
            value = lambda.evaluate([value], scope);
            out.push(value);
        }

        return new TList(out);
    }
});
Numbas.jme.lazyOps.push('iterate_until');
jme.findvarsOps.iterate_until = function(tree, boundvars, scope) {
    return jme.findvars_args(fn_iterate_until.options.make_lambda(tree.args), boundvars, scope);
}
jme.substituteTreeOps.iterate_until = function(tree, scope, allowUnbound) {
    tree = {
        tok: tree.tok,
        args: tree.args
    };

    var i = tree.args[0].tok.type == 'lambda' ? 0 : 1;
    tree.args[i + 1] = jme.substituteTree(tree.args[i + 1], scope, allowUnbound);
    if(tree.args[i + 3]) {
        tree.args[i + 3] = jme.substituteTree(tree.args[i + 3], scope.allowUnbound);
    }
    return tree;
}

var fn_foldl = newBuiltin('foldl', ['?', TName, TName, '?', TList], '?', null, {
    make_lambda: function(args, scope) {
        if(args[0].tok.type == 'lambda') {
            return args;
        }
        return [{tok: new TLambda([args[1], args[2]], args[0])}, args[3], args[4]];
    },
    evaluate: function(args, scope) {
        args = this.options.make_lambda(args);

        var lambda = args[0].tok;
        var first_value = scope.evaluate(args[1]);
        var list = jme.castToType(scope.evaluate(args[2]), 'list').value;

        var result = list.reduce(function(acc, value) {
            return lambda.evaluate([acc, value], scope);
        }, first_value)
        return result;
    }
});
Numbas.jme.lazyOps.push('foldl');
jme.findvarsOps.foldl = function(tree, boundvars, scope) {
    return jme.findvars_args(fn_foldl.options.make_lambda(tree.args), boundvars, scope);
}
jme.substituteTreeOps.foldl = function(tree, scope, allowUnbound) {
    var i = tree.args[0].tok.type == 'lambda' ? 0 : 2;
    tree.args[i + 1] = jme.substituteTree(tree.args[i + 1], scope, allowUnbound);
    tree.args[i + 2] = jme.substituteTree(tree.args[i + 2], scope, allowUnbound);
    return tree;
}


var fn_take = newBuiltin('take', [TNum, '?', TName, '?'], TList, null, {
    make_lambda: function(args, scope) {
        if(args[1].tok.type == 'lambda') {
            return args;
        }
        return [args[0], {tok: new TLambda([args[2]], args[1])}, args[3]];
    },
    evaluate: function(args, scope) {
        args = this.options.make_lambda(args);

        var n = scope.evaluate(args[0]).value;
        var lambda = args[1].tok;
        var list = args[2];

        list = jme.castToType(scope.evaluate(list), 'list').value;

        var value = [];

        for(let i = 0; i < list.length && value.length < n; i++) {
            var v = list[i];
            var ok = jme.castToType(lambda.evaluate([v], scope), 'boolean').value;
            if(ok) {
                value.push(v);
            }
        };
        return new TList(value);
    }
});
Numbas.jme.lazyOps.push('take');
jme.findvarsOps.take = function(tree, boundvars, scope) {
    return jme.findvars_args(fn_take.options.make_lambda(tree.args), boundvars, scope);
}
jme.substituteTreeOps.take = function(tree, scope, allowUnbound) {
    var list_index = tree.args[1].tok.type == 'lambda' ? 2 : 3;
    var args = tree.args.slice();
    args[0] = jme.substituteTree(args[0], scope, allowUnbound);
    args[list_index] = jme.substituteTree(args[list_index], scope, allowUnbound);
    return {tok:tree.tok, args: args};
}

newBuiltin('separate', [TList, TLambda], TList, null, {
    evaluate: function(args, scope) {
        var trues = [];
        var falses = [];

        var list = args[0];
        var lambda = args[1];

        list.value.forEach((x) => {
            const b = jme.castToType(lambda.evaluate([x], scope), 'boolean').value;
            (b ? trues : falses).push(x);
        });

        return new TList([new TList(trues), new TList(falses)]);
    }
});

newBuiltin('groups_of', [TList, TNum], TList, null, {
    evaluate: function(args, scope) {
        var list = args[0].value;
        var n = args[1].value;

        var out = [];
        for(let i = 0; i < list.length; i += n) {
            const row = list.slice(i, i + n);
            if(row.length) {
                out.push(new TList(row));
            }
        }

        return new TList(out);
    }
});

newBuiltin('enumerate', [TList], TList, function(list) {
    return list.map(function(v, i) {
        return new TList([new TInt(i), v]);
    });
});


/** Is the given token the value `true`?
 *
 * @param {Numbas.jme.token} item
 * @returns {boolean}
 */
function tok_is_true(item) {
    return item.type == 'boolean' && item.value
}
newBuiltin('all', [sig.listof(sig.type('boolean'))], TBool, function(list) {
    return list.every(tok_is_true);
});
newBuiltin('some', [sig.listof(sig.type('boolean'))], TBool, function(list) {
    return list.some(tok_is_true);
});

var let_sig_names = sig.multiple(
                    sig.or(
                        sig.sequence(sig.type('name'), sig.anything()),
                        sig.sequence(sig.listof(sig.type('name')), sig.anything())
                    )
                );
newBuiltin('let', [sig.or(sig.type('dict'), let_sig_names), '?'], TList, null, {
    evaluate: function(args, scope) {
        var signature = sig.or(sig.type('dict'), let_sig_names)(args.map(function(a) {
            if(a.tok.type == 'list' && a.args) {
                return new TList(a.args.map(function(aa) {
                    return aa.tok;
                }));
            } else {
                return a.tok
            }
        }));
        if(!signature) {
            throw(new Numbas.Error('jme.typecheck.no right type definition', {op:'let'}));
        }
        let variables, lambda, nscope;
        if(signature[0].type == "dict") {
            var d = scope.evaluate(args[0]);
            variables = d.value;
            lambda = args[1];
            nscope = new Scope([scope, {variables:variables}]);
            return nscope.evaluate(lambda);
        } else {
            lambda = args.at(-1);
            variables = {};
            nscope = new Scope([scope]);
            for(let i = 0;i < args.length - 1;i += 2) {
                var value = nscope.evaluate(args[i + 1]);
                if(args[i].tok.type == 'name') {
                    var name = args[i].tok.name;
                    nscope.setVariable(name, value);
                } else if(args[i].tok.type == 'list') {
                    var names = args[i].args.map(function(t) {
                        return t.tok.name
                    });
                    var values = jme.castToType(value, 'list').value;
                    for(let j = 0;j < names.length;j++) {
                        nscope.setVariable(names[j], values[j]);
                    }
                }
            }
            return nscope.evaluate(lambda);
        }
    }
});
Numbas.jme.lazyOps.push('let');
jme.findvarsOps.let = function(tree, boundvars, scope) {
    var vars = [];
    boundvars = boundvars.slice();
    for(let i = 0;i < tree.args.length - 1;i += 2) {
        switch(tree.args[i].tok.type) {
            case 'name':
                boundvars.push(jme.normaliseName(tree.args[i].tok.name, scope));
                break;
            case 'list':
                boundvars = boundvars.concat(tree.args[i].args.map(function(t) {
                    return t.tok.name
                }));
                break;
            case 'dict':
                tree.args[i].args.forEach(function(kp) {
                    boundvars.push(kp.tok.key);
                    vars = vars.merge(jme.findvars(kp.args[0], boundvars, scope));
                });
                break;
        }
        vars = vars.merge(jme.findvars(tree.args[i + 1], boundvars, scope));
    }
    // find variables used in the lambda expression, excluding the ones assigned by let
    vars = vars.merge(jme.findvars(tree.args.at(-1), boundvars, scope));
    return vars;
}
jme.substituteTreeOps.let = function(tree, scope, allowUnbound) {
    var nscope = new Scope([scope]);
    let names;
    if(tree.args[0].tok.type == 'dict') {
        var d = tree.args[0];
        names = d.args.map(function(da) {
            return da.tok.key;
        });
        for(let i = 0;i < names.length;i++) {
            nscope.deleteVariable(names[i]);
        }
        d.args = d.args.map(function(da) {
            return jme.substituteTree(da, nscope, allowUnbound)
        });
    } else {
        for(let i = 1;i < tree.args.length - 1;i += 2) {
            switch(tree.args[i - 1].tok.type) {
                case 'name':
                    var name = tree.args[i - 1].tok.name;
                    nscope.deleteVariable(name);
                    break;
                case 'list':
                    names = tree.args[i - 1].args;
                    for(let j = 0;j < names.length;j++) {
                        nscope.deleteVariable(names[j].tok.name);
                    }
                    break;
            }
            tree.args[i] = jme.substituteTree(tree.args[i], nscope, allowUnbound);
        }
    }
}

newBuiltin('unset', [TDict, '?'], '?', null, {
    evaluate: function(args, scope) {
        var defs = jme.unwrapValue(scope.evaluate(args[0]));
        var nscope = scope.unset(defs);
        return nscope.evaluate(args[1]);
    }
});
Numbas.jme.lazyOps.push('unset');

newBuiltin('sort', [TList], TList, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var newlist = new TList(list.vars);
        newlist.value = list.value.slice().sort(jme.compareTokens);
        return newlist;
    }
});
newBuiltin('sort_by', [TNum, sig.listof(sig.type('list'))], TList, null, {
    evaluate: function(args, scope) {
        var index = args[0].value;
        var list = args[1];
        var newlist = new TList(list.vars);
        newlist.value = list.value.slice().sort(jme.sortTokensBy(function(x) {
            return x.value[index];
        }));
        return newlist;
    }
});

newBuiltin('sort_by', [TString, sig.listof(sig.type('dict'))], TList, null, {
    evaluate: function(args, scope) {
        var index = args[0].value;
        var list = args[1];
        var newlist = new TList(list.vars);
        newlist.value = list.value.slice().sort(jme.sortTokensBy(function(x) {
            return x.value[index];
        }));
        return newlist;
    }
});

newBuiltin('sort_destinations', [TList], TList, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var newlist = new TList(list.vars);
        var sorted = list.value.map(function(v, i) {
            return {tok:v, i:i}
        }).sort(function(a, b) {
            return jme.compareTokens(a.tok, b.tok);
        });
        var inverse = [];
        for(let i = 0;i < sorted.length;i++) {
            inverse[sorted[i].i] = i;
        }
        newlist.value = inverse.map(function(n) {
            return new TNum(n);
        });
        return newlist;
    }
});

newBuiltin('group_by', [TNum, sig.listof(sig.type('list'))], TList, null, {
    evaluate: function(args, scope) {
        var index = args[0].value;
        var list = args[1];
        var sorted = list.value.slice().sort(jme.sortTokensBy(function(x) {
            return x.value[index];
        }));
        var out = [];
        for(let i = 0;i < sorted.length;) {
            var key = sorted[i].value[index];
            var values = [sorted[i]];
            for(i++;i < sorted.length;i++) {
                if(jme.compareTokens(key, sorted[i].value[index]) == 0) {
                    values.push(sorted[i]);
                } else {
                    break;
                }
            }
            out.push(new TList([key, new TList(values)]));
        }
        return new TList(out);
    }
});

newBuiltin('group_by', [TString, sig.listof(sig.type('dict'))], TList, null, {
    evaluate: function(args, scope) {
        var index = args[0].value;
        var list = args[1];
        var sorted = list.value.slice().sort(jme.sortTokensBy(function(x) {
            return x.value[index];
        }));
        var out = [];
        for(let i = 0;i < sorted.length;) {
            var key = sorted[i].value[index];
            var values = [sorted[i]];
            for(i++;i < sorted.length;i++) {
                if(jme.compareTokens(key, sorted[i].value[index]) == 0) {
                    values.push(sorted[i]);
                } else {
                    break;
                }
            }
            out.push(new TList([key, new TList(values)]));
        }
        return new TList(out);
    }
});

newBuiltin('reverse', [TList], TList, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        return new TList(list.value.slice().reverse());
    }
});
// indices of given value in given list
newBuiltin('indices', [TList, '?'], TList, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var target = args[1];
        var out = [];
        list.value.map(function(v, i) {
            if(util.eq(v, target, scope)) {
                out.push(new TNum(i));
            }
        });
        return new TList(out);
    }
});
newBuiltin('set', [TList], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(util.distinct(args[0].value, scope));
    }
});
newBuiltin('set', [TRange], TSet, null, {
    evaluate: function(args, scope) {
        var l = jme.castToType(args[0], 'list');
        return new TSet(util.distinct(l.value, scope));
    }
});
newBuiltin('set', ['*?'], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(util.distinct(args, scope));
    }
});
newBuiltin('list', [TSet], TList, function(set) {
    var l = [];
    for(let i = 0;i < set.length;i++) {
        l.push(set[i]);
    }
    return l;
});
newBuiltin('union', [TSet, TSet], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(setmath.union(args[0].value, args[1].value, scope));
    }
});
newBuiltin('intersection', [TSet, TSet], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(setmath.intersection(args[0].value, args[1].value, scope));
    }
});
newBuiltin('or', [TSet, TSet], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(setmath.union(args[0].value, args[1].value, scope));
    }
});
newBuiltin('and', [TSet, TSet], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(setmath.intersection(args[0].value, args[1].value, scope));
    }
});
newBuiltin('-', [TSet, TSet], TSet, null, {
    evaluate: function(args, scope) {
        return new TSet(setmath.minus(args[0].value, args[1].value, scope));
    }
});
newBuiltin('abs', [TSet], TNum, setmath.size);
newBuiltin('in', ['?', TSet], TBool, null, {
    evaluate: function(args, scope) {
        return new TBool(util.contains(args[1].value, args[0], scope));
    }
});
newBuiltin('product', [sig.multiple(sig.type('list'))], TList, function() {
    var lists = Array.prototype.slice.call(arguments);
    var prod = util.product(lists);
    return prod.map(function(l) {
        return new TList(l);
    });
});

newBuiltin('product', [TList, TNum], TList, function(l, n) {
    return util.cartesian_power(l, n).map(function(sl) {
        return new TList(sl);
    });
});

newBuiltin('zip', [sig.multiple(sig.type('list'))], TList, function() {
    var lists = Array.prototype.slice.call(arguments);
    var zipped = util.zip(lists);
    return zipped.map(function(l) {
        return new TList(l);
    });
});
newBuiltin('combinations', [TList, TNum], TList, function(list, r) {
    var prod = util.combinations(list, r);
    return prod.map(function(l) {
        return new TList(l);
    });
});
newBuiltin('combinations_with_replacement', [TList, TNum], TList, function(list, r) {
    var prod = util.combinations_with_replacement(list, r);
    return prod.map(function(l) {
        return new TList(l);
    });
});
newBuiltin('permutations', [TList, TNum], TList, function(list, r) {
    var prod = util.permutations(list, r);
    return prod.map(function(l) {
        return new TList(l);
    });
});
newBuiltin('frequencies', [TList], [TList], null, {
    evaluate: function(args, scope) {
        var o = [];
        var l = args[0].value;
        l.forEach(function(x) {
            var p = o.find(function(item) {
                return util.eq(item[0], x);
            });
            if(p) {
                p[1] += 1;
            } else {
                o.push([x, 1]);
            }
        });
        return new TList(o.map(function(p) {
            return new TList([p[0], new TNum(p[1])]);
        }));
    }
});
newBuiltin('vector', [sig.multiple(sig.type('number'))], TVector, null, {
    evaluate: function(args, scope) {
        var value = [];
        for(let i = 0;i < args.length;i++) {
            value.push(args[i].value);
        }
        var t = new TVector(value);
        if(args.length > 0) {
            t.precisionType = args[0].precisionType;
            t.precision = args[0].precision;
        }
        return t;
    }
});
newBuiltin('vector', [sig.listof(sig.type('number'))], TVector, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var value = list.value.map(function(x) {
            return x.value
        });
        var t = new TVector(value);
        if(list.value.length > 0) {
            var tn = list.value[0];
            t.precisionType = tn.precisionType;
            t.precision = tn.precision;
        }
        return t;
    }
});
newBuiltin('matrix', [sig.listof(sig.type('vector'))], TMatrix, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var rows = list.vars;
        var columns = 0;
        var value = [];
        if(!list.value.length) {
            rows = 0;
            columns = 0;
        } else {
            value = list.value.map(function(v) {
                return v.value
            });
            columns = list.value[0].value.length;
        }
        value.rows = rows;
        value.columns = columns;
        var t = new TMatrix(value);
        if(list.value.length > 0) {
            t.precisionType = list.value[0].precisionType;
            t.precision = list.value[0].precision;
        }
        return t;
    }
});
newBuiltin('matrix', [sig.listof(sig.listof(sig.type('number')))], TMatrix, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var rows = list.vars;
        var columns = 0;
        var value = [];
        if(!list.value.length) {
            rows = 0;
            columns = 0;
        } else {
            for(let i = 0;i < rows;i++) {
                var row = list.value[i].value;
                value.push(row.map(function(x) {
                    return x.value
                }));
                columns = Math.max(columns, row.length);
            }
        }
        value.rows = rows;
        value.columns = columns;
        var t = new TMatrix(value);
        if(rows > 0 && columns > 0) {
            var tn = list.value[0].value[0];
            t.precisionType = tn.precisionType;
            t.precision = tn.precision;
        }
        return t;
    }
});
newBuiltin('matrix', [sig.listof(sig.type('number'))], TMatrix, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var rows = list.vars;
        var columns = 0;
        var value = [];
        if(!list.value.length) {
            rows = 0;
            columns = 0;
        } else {
            value = [list.value.map(function(e) {
                return jme.castToType(e, 'number').value
            })];
            rows = 1;
            columns = list.vars;
        }
        value.rows = rows;
        value.columns = columns;
        var t = new TMatrix(value);
        if(rows > 0 && columns > 0) {
            var tn = list.value[0];
            t.precisionType = tn.precisionType;
            t.precision = tn.precision;
        }
        return t;
    }
});
newBuiltin('matrix', [sig.multiple(sig.listof(sig.type('number')))], TMatrix, null, {
    evaluate: function(args, scope) {
        var rows = args.length;
        var columns = 0;
        var value = [];
        for(let i = 0;i < args.length;i++) {
            var row = args[i].value;
            value.push(row.map(function(x) {
                return x.value
            }));
            columns = Math.max(columns, row.length);
        }
        value.rows = rows;
        value.columns = columns;
        var t = new TMatrix(value);
        if(rows > 0 && columns > 0) {
            var tn = args[0].value[0];
            t.precisionType = tn.precisionType;
            t.precision = tn.precision;
        }
        return t;
    }
});
newBuiltin('rowvector', [sig.multiple(sig.type('number'))], TMatrix, null, {
    evaluate: function(args, scope) {
        var row = [];
        for(let i = 0;i < args.length;i++) {
            row.push(args[i].value);
        }
        var matrix = [row];
        matrix.rows = 1;
        matrix.columns = row.length;
        var t = new TMatrix(matrix);
        if(matrix.columns > 0) {
            var tn = args[0];
            t.precisionType = tn.precisionType;
            t.precision = tn.precision;
        }
        return t;
    }
});
newBuiltin('rowvector', [sig.listof(sig.type('number'))], TMatrix, null, {
    evaluate: function(args, scope) {
        var list = args[0];
        var row = list.value.map(function(x) {
            return x.value
        });
        var matrix = [row];
        matrix.rows = 1;
        matrix.columns = row.length;
        var t = new TMatrix(matrix);
        if(matrix.columns > 0) {
            var tn = args[0].value[0];
            t.precisionType = tn.precisionType;
            t.precision = tn.precision;
        }
        return t;
    }
});
//cast vector to list
newBuiltin('list', [TVector], TList, null, {
    evaluate: function(args, scope) {
        var vector = args[0];
        var value = vector.value.map(function(n) {
            return new TNum(n)
        });
        return new TList(value);
    }
});
//cast matrix to list of lists
newBuiltin('list', [TMatrix], TList, null, {
    evaluate: function(args, scope) {
        var matrix = args[0];
        var value = [];
        for(let i = 0;i < matrix.value.rows;i++) {
            var row = new TList(matrix.value[i].map(function(n) {
                return new TNum(n)
            }));
            value.push(row);
        }
        return new TList(value);
    }
});

newBuiltin('diff', [TExpression, String], TExpression, null, {
    evaluate: function(args, scope) {
        var expr = scope.evaluate(args[0]).tree;
        var name = scope.evaluate(args[1]).value;
        var res = jme.calculus.differentiate(expr, name, scope);
        var ruleset = jme.collectRuleset('all', scope.allRulesets());
        var simplified = jme.display.simplifyTree(res, ruleset, scope);
        return new TExpression(simplified);
    }
});
Numbas.jme.lazyOps.push('diff');

/** Set the content of an HTML element to something corresponding to the value of the given token.
 * If the token is not of type HTML, use {@link Numbas.jme.typeToDisplayString}.
 *
 * @param {Element} element
 * @param {Numbas.jme.token} tok
 * @param {Numbas.jme.Scope} scope
 */
function set_html_content(element, tok, scope) {
    if(tok.type != 'html') {
        element.innerHTML = jme.tokenToDisplayString(tok, scope);
    } else {
        element.appendChild(tok.value);
    }
}
newBuiltin('table', ['list of list', 'list', 'list'], THTML, null, {
    evaluate: function(args, scope) {
        var data = args[0].value;
        var col_headers = args[1].value;
        var row_headers = args[2].value;
        var table = document.createElement('table');
        var thead = document.createElement('thead');
        table.appendChild(thead);
        thead.appendChild(document.createElement('th'));
        for(let i = 0;i < col_headers.length;i++) {
            const th = document.createElement('th');
            th.setAttribute('scope', 'col');
            set_html_content(th, col_headers[i], scope);
            thead.appendChild(th);
        }
        var tbody = document.createElement('tbody');
        table.appendChild(tbody);
        for(let i = 0;i < data.length;i++) {
            var row = document.createElement('tr');
            tbody.appendChild(row);

            const th = document.createElement('th');
            th.setAttribute('scope', 'row');
            set_html_content(th, row_headers[i], scope);
            row.appendChild(th);

            for(let j = 0;j < data[i].value.length;j++) {
                var td = document.createElement('td');
                set_html_content(td, data[i].value[j], scope);
                row.appendChild(td);
            }
        }
        table.setAttribute('data-interactive', 'false');
        return new THTML(table);
    }
});
newBuiltin('table', ['list of list', 'list'], THTML, null, {
    evaluate: function(args, scope) {
        var data = args[0].value;
        var headers = args[1].value;
        var table = document.createElement('table');
        var thead = document.createElement('thead');
        table.appendChild(thead);
        for(let i = 0;i < headers.length;i++) {
            var th = document.createElement('th');
            th.setAttribute('scope', 'col');
            set_html_content(th, headers[i], scope);
            thead.appendChild(th);
        }
        var tbody = document.createElement('tbody');
        table.appendChild(tbody);
        for(let i = 0;i < data.length;i++) {
            var row = document.createElement('tr');
            tbody.appendChild(row);
            for(let j = 0;j < data[i].value.length;j++) {
                var td = document.createElement('td');
                set_html_content(td, data[i].value[j], scope);
                row.appendChild(td);
            }
        }
        table.setAttribute('data-interactive', 'false');
        return new THTML(table);
    }
});
newBuiltin('table', ['list of list'], THTML, null, {
    evaluate: function(args, scope) {
        var data = args[0].value;
        var table = document.createElement('table');
        var tbody = document.createElement('tbody');
        table.appendChild(tbody);
        for(let i = 0;i < data.length;i++) {
            var row = document.createElement('tr');
            tbody.appendChild(row);
            for(let j = 0;j < data[i].value.length;j++) {
                var td = document.createElement('td');
                set_html_content(td, data[i].value[j], scope);
                row.appendChild(td);
            }
        }
        table.setAttribute('data-interactive', 'false');
        return new THTML(table);
    }
});

newBuiltin('max_width', [TNum, THTML], THTML, function(w, h) {
    h[0].style['max-width'] = w + 'em';
    return h[0];
});

newBuiltin('max_height', [TNum, THTML], THTML, function(w, h) {
    h[0].style['max-height'] = w + 'em';
    return h[0];
});

newBuiltin('parse', [TString], TExpression, function(str) {
    return jme.compile(str);
});
newBuiltin('expand_juxtapositions', [TExpression, sig.optional(sig.type('dict'))], TExpression, null, {
    evaluate: function(args, scope) {
        var tree = args[0].tree;
        var options = args[1] ? jme.unwrapValue(args[1]) : undefined;
        return new TExpression(scope.expandJuxtapositions(tree, options));
    }
});

newBuiltin('normalise_subscripts', [TString], TString, null, {
    evaluate: function(args, scope) {
        var tok = new TName(args[0].value);
        return new TString(scope.normaliseSubscripts(tok).name);
    }
});

newBuiltin('expression', [TString], TExpression, null, {
    evaluate: function(args, scope) {
        var notation = Numbas.locale.default_number_notation;
        Numbas.locale.default_number_notation = ['plain'];
        /**
         * Replace all strings in the given expression with copies marked with `subjme`.
         *
         * @param {Numbas.jme.tree} tree
         * @returns {Numbas.jme.tree}
         */
        function sub_strings(tree) {
            if(jme.isType(tree.tok, 'string') && !tree.tok.safe) {
                var tok = new TString(tree.tok.value);
                tok.subjme = true;
                return {tok: tok};
            } else if(tree.args) {
                return {
                    tok: tree.tok,
                    args: tree.args.map(sub_strings)
                };
            } else {
                return tree;
            }
        }
        var arg = sub_strings(args[0]);
        try {
            var str = scope.evaluate(arg);
        } finally {
            Numbas.locale.default_number_notation = notation;
        }
        if(!jme.isType(str, 'string')) {
                throw(new Numbas.Error('jme.typecheck.no right type definition', {op:'expression'}));
        }
        str = jme.castToType(str, 'string');
        return new TExpression(jme.compile(str.value));
    }
});
Numbas.jme.lazyOps.push('expression');
newBuiltin('args', [TExpression], TList, null, {
    evaluate: function(args, scope) {
        if(!args[0].tree.args) {
            return new TList([]);
        }
        return new TList(args[0].tree.args.map(function(tree) {
            return new TExpression(tree);
        }));
    }
});
newBuiltin('as', ['?', TString], '?', null, {
    evaluate: function(args, scope) {
        var target = args[1].value;
        return jme.castToType(args[0], target);
    }
});
newBuiltin('type', [TExpression], TString, null, {
    evaluate: function(args, scope) {
        return new TString(args[0].tree.tok.type);
    }
});
newBuiltin('type', ['?'], TString, null, {
    evaluate: function(args, scope) {
        return new TString(args[0].type);
    }
});
newBuiltin('name', [TString], TName, function(name) {
    return name
});
newBuiltin('string', [TName], TString, function(name) {
    return name
});
newBuiltin('op', [TString], TOp, function(name) {
    return name
});
newBuiltin('function', [TString], TFunc, function(name) {
    return name
});
newBuiltin('assert', [TBool, '?'], '?', null, {
    evaluate: function(args, scope) {
        var result = scope.evaluate(args[0]).value;
        if(!result) {
            return scope.evaluate(args[1]);
        } else {
            return new TBool(false);
        }
    }
});
Numbas.jme.lazyOps.push('assert');
newBuiltin('try', ['?', TName, '?'], '?', null, {
    evaluate: function(args, scope) {
        try {
            var res = scope.evaluate(args[0]);
            return res;
        } catch(e) {
            var variables = {};
            variables[args[1].tok.name] = e.message;
            return scope.evaluate(args[2], variables);
        }
    }
});
Numbas.jme.lazyOps.push('try');
jme.findvarsOps.try = function(tree, boundvars, scope) {
    var try_boundvars = boundvars.slice();
    try_boundvars.push(jme.normaliseName(tree.args[1].tok.name, scope));
    var vars = jme.findvars(tree.args[0], boundvars, scope);
    vars = vars.merge(jme.findvars(tree.args[2], try_boundvars, scope));
    return vars;
}
newBuiltin('exec', [sig.or(sig.type('function'), sig.type('op')), TList], TExpression, null, {
    evaluate: function(args, scope) {
        var tok;
        if(args[0].args) {
            tok = scope.evaluate(args[0]);
        } else {
            tok = args[0].tok;
        }
        var list = scope.evaluate(args[1]);
        var eargs = list.value.map(function(a) {
            if(a.type != 'expression') {
                return {tok:a};
            } else {
                return a.tree;
            }
        });
        tok.vars = eargs.length;
        return new TExpression({tok: tok, args: eargs});
    }
});
Numbas.jme.lazyOps.push('exec');

newBuiltin('simplify', [TExpression, TString], TExpression, null, {
    evaluate: function(args, scope) {
        var tree = args[0].tree;
        var ruleset = jme.rules.collectRuleset(args[1].value, scope.allRulesets());
        return new TExpression(jme.display.simplifyTree(tree, ruleset, scope));
    }
});
newBuiltin('simplify', [TExpression, TList], TExpression, null, {
    evaluate: function(args, scope) {
        var tree = args[0].tree;
        var ruleset = jme.rules.collectRuleset(args[1].value.map(function(x) {
            return x.value
        }), scope.allRulesets());
        return new TExpression(jme.display.simplifyTree(tree, ruleset, scope));
    }
});
newBuiltin('simplify', [TString, TString], TExpression, null, {
    evaluate: function(args, scope) {
        return new TExpression(jme.display.simplify(args[0].value, args[1].value, scope));
    }
});
newBuiltin('string', [TExpression, '[string or list of string]'], TString, null, {
    evaluate: function(args, scope) {
        var flags = {};
        if(args[1]) {
            var rules = args[1].value;
            var ruleset = jme.collectRuleset(rules, scope.allRulesets());
            flags = ruleset.flags;
        }
        return new TString(jme.display.treeToJME(args[0].tree, flags, scope));
    }
});
newBuiltin('latex', [TExpression, '[string or list of string]'], TString, null, {
    evaluate: function(args, scope) {
        var expr = args[0];
        var flags = {};
        if(args[1]) {
            var rules = args[1].value;
            var ruleset = jme.collectRuleset(rules, scope.allRulesets());
            flags = ruleset.flags;
        }
        var tex = jme.display.texify(expr.tree, flags, scope);
        var s = new TString(tex);
        s.latex = true;
        s.display_latex = true;
        return s;
    }
});

newBuiltin('eval', [TExpression], '?', null, {
    evaluate: function(args, scope) {
        return scope.evaluate(args[0].tree);
    },
    random: undefined
});
newBuiltin('eval', [TExpression, TDict], '?', null, {
    evaluate: function(args, scope) {
        return (new Numbas.jme.Scope([scope, {variables:args[1].value}])).evaluate(args[0].tree);
    },
    random: undefined
});
newBuiltin('findvars', [TExpression], TList, null, {
    evaluate: function(args, scope) {
        var vars = jme.findvars(args[0].tree, [], scope);
        return new TList(vars.map(function(v) {
            return new TString(v)
        }));
    }
});
newBuiltin('definedvariables', [], TList, null, {
    evaluate: function(args, scope) {
        var vars = Object.keys(scope.allVariables());
        return new TList(vars.map(function(x) {
            return new TString(x)
        }));
    }
});
newBuiltin('resultsequal', ['?', '?', TString, TNum], TBool, null, {
    evaluate: function(args, scope) {
        var a = args[0];
        var b = args[1];
        var accuracy = args[3].value;
        var checkingFunction = jme.checkingFunctions[args[2].value.toLowerCase()];
        return new TBool(jme.resultsEqual(a, b, checkingFunction, accuracy, scope));
    }
});

newBuiltin('infer_variable_types', [TExpression], TDict, null, {
    evaluate: function(args, scope) {
        var expr = args[0];
        var assignments = jme.inferVariableTypes(expr.tree, scope);
        if(!assignments) {
            assignments = {};
        }
        return jme.wrapValue(assignments);
    }
});

newBuiltin('infer_type', [TExpression], TString, null, {
    evaluate: function(args, scope) {
        var expr = args[0];
        return jme.wrapValue(jme.inferExpressionType(expr.tree, scope));
    }
});

newBuiltin('make_variables', [sig.dict(sig.type('expression')), sig.optional(sig.type('range'))], TDict, null, {
    evaluate: function(args, scope) {
        var todo = {};
        scope = new jme.Scope([scope]);
        if(args.length > 1 && args[1].type != 'nothing') {
            scope.setVariable('vrange', args[1]);
        }
        for(const [k, v] of Object.entries(args[0].value)) {
            scope.deleteVariable(k);
            const tree = v.tree;
            var vars = jme.findvars(tree, [], scope);
            todo[k] = {tree, vars};
        }
        var result = jme.variables.makeVariables(todo, scope);
        var out = {};
        for(const [k, v] of Object.entries(result.variables)) {
            out[k] = v;
        }
        return new TDict(out);
    },
    random: undefined
});

/** Helper function for the JME `match` function.
 *
 * @param {Numbas.jme.tree} expr
 * @param {string} pattern
 * @param {string} options
 * @param {Numbas.jme.Scope} scope
 * @returns {Numbas.jme.token}
 * @see Numbas.jme.rules.Rule#match
 */
function match_subexpression(expr, pattern, options, scope) {
    var rule = new jme.rules.Rule(pattern, null, options);
    var match = rule.match(expr, scope);
    if(!match) {
        return jme.wrapValue({match: false, groups: {}});
    } else {
        var groups = {}
        for(const [k, v] of Object.entries(match)) {
            if(k.slice(0, 2) != '__') {
                groups[k] = new TExpression(v);
            }
        }
        return jme.wrapValue({
            match: true,
            groups: groups
        });
    }
}

newBuiltin('match', [TExpression, TString], TDict, null, {
    evaluate: function(args, scope) {
        var expr = args[0].tree;
        var pattern = args[1].value;
        var options = 'ac';
        return match_subexpression(expr, pattern, options, scope);
    }
});
newBuiltin('match', [TExpression, TString, TString], TDict, null, {
    evaluate: function(args, scope) {
        var expr = args[0].tree;
        var pattern = args[1].value;
        var options = args[2].value;
        return match_subexpression(expr, pattern, options, scope);
    }
});

/** Helper function for the JME `matches` function.
 *
 * @param {Numbas.jme.tree} expr
 * @param {string} pattern
 * @param {string} options
 * @param {Numbas.jme.Scope} scope
 * @returns {Numbas.jme.token}
 * @see Numbas.jme.rules.Rule#match
 */
function matches_subexpression(expr, pattern, options, scope) {
    var rule = new jme.rules.Rule(pattern, null, options);
    var match = rule.match(expr, scope);
    return new TBool(match && true);
}

newBuiltin('matches', [TExpression, TString], TBool, null, {
    evaluate: function(args, scope) {
        var expr = args[0].tree;
        var pattern = args[1].value;
        var options = 'ac';
        return matches_subexpression(expr, pattern, options, scope);
    }
});
newBuiltin('matches', [TExpression, TString, TString], TBool, null, {
    evaluate: function(args, scope) {
        var expr = args[0].tree;
        var pattern = args[1].value;
        var options = args[2].value;
        return matches_subexpression(expr, pattern, options, scope);
    }
});

/** Helper function for the JME `replace` function.
 *
 * @param {string} pattern
 * @param {string} repl
 * @param {Numbas.jme.tree} expr
 * @param {string} options
 * @param {Numbas.jme.Scope} scope
 * @returns {Numbas.jme.token}
 * @see Numbas.jme.rules.Rule#replaceAll
 */
function replace_expression(pattern, repl, expr, options, scope) {
        var rule = new jme.rules.Rule(pattern, repl, options);
        var out = rule.replaceAll(expr, scope).expression;
        return new TExpression(out);
}
newBuiltin('replace', [TString, TString, TExpression], TExpression, null, {
    evaluate: function(args, scope) {
        var pattern = args[0].value;
        var repl = args[1].value;
        var expr = args[2].tree;
        var options = 'acg';
        return replace_expression(pattern, repl, expr, options, scope);
    }
});
newBuiltin('replace', [TString, TString, TExpression, TString], TExpression, null, {
    evaluate: function(args, scope) {
        var pattern = args[0].value;
        var repl = args[1].value;
        var expr = args[2].tree;
        var options = args[3].value;
        return replace_expression(pattern, repl, expr, options, scope);
    }
});
newBuiltin('substitute', [TDict, TExpression], TExpression, null, {
    evaluate: function(args, scope) {
        var substitutions = args[0].value;
        for(const [k, v] of Object.entries(substitutions)) {
            if(v.type == 'expression') {
                substitutions[k] = v.tree;
            }
        }
        var expr = args[1].tree;
        scope = new Scope({variables: substitutions});
        var nexpr = jme.substituteTree(expr, scope, true, true);
        return new TExpression(nexpr);
    }
});

newBuiltin('canonical_compare', ['?', '?'], TNum, null, {
    evaluate: function(args, scope) {
        var cmp = jme.compareTrees(args[0], args[1]);
        return new TNum(cmp);
    }
});
jme.lazyOps.push('canonical_compare');

newBuiltin('numerical_compare', [TExpression, TExpression], TBool, null, {
    evaluate: function(args, scope) {
        var a = args[0].tree;
        var b = args[1].tree;
        return new TBool(jme.compare(a, b, {}, scope));
    }
});

newBuiltin('debug_log', ['?', '?'], '?', null, {
    evaluate: function(args, scope) {
        console.log('DEBUG ' + args[1].value + ':', Numbas.jme.unwrapValue(args[0]));
        return args[0];
    }
}, {unwrapValues: false});

newBuiltin('scope_case_sensitive', ['?', TBool], '?', null, {
    evaluate: function(args, scope) {
        var caseSensitive = args.length > 1 ? scope.evaluate(args[1]).value : true;
        var scope2 = new jme.Scope([scope, {caseSensitive: caseSensitive}]);
        return scope2.evaluate(args[0]);
    }
});
jme.lazyOps.push('scope_case_sensitive');


/**
 * Rewrite an application of the pipe operator `a |> b(...)` to `b(a, ...)`.
 *
 * Note that the `|>` operator won't normally appear in compiled expressions, because the tree is rewritten as part of the compilation process.
 * This definition is added only so that manually-constructed expressions containing `|>` still work.
 *
 * @param {Array.<Numbas.jme.tree>} args
 * @returns {Numbas.jme.tree}
 */
function pipe_rewrite(args) {
    var bargs = args[1].args.slice();
    bargs.splice(0, 0, args[0]);
    var tree = {
        tok: args[1].tok,
        args: bargs
    };

    return tree;
}

newBuiltin('|>', ['?', '?'], '?', null, {
    evaluate: function(args, scope) {
        return scope.evaluate(pipe_rewrite(args));
    }
});
jme.lazyOps.push('|>');
jme.findvarsOps['|>'] = function(tree, boundvars, scope) {
    tree = pipe_rewrite(tree.args);
    return jme.findvars(tree, boundvars, scope);
}

newBuiltin('translate', [TString], TString, function(s) {
    return R(s);
});
newBuiltin('translate', [TString, TDict], TString, function(s, params) {
    return R(s, params);
}, {unwrapValues:true});
///end of builtins


});
;
Numbas.queueScript('jme-calculus', ['jme-base', 'jme-rules'], function() {
/** @file Code to do with differentiation and integration
 *
 * Provides {@link Numbas.jme.calculus}
 */

var jme = Numbas.jme;
var TNum = Numbas.jme.types.TNum;

/** @namespace Numbas.jme.calculus */
var calculus = jme.calculus = {};

var differentiation_rules = [
    ['rational:$n', '0'],
    ['?;a + ?`+;b', '$diff(a) + $diff(b)'],
    ['?;a - ?`+;b', '$diff(a) - $diff(b)'],
    ['+?;a', '$diff(a)'],
    ['-?;a', '-$diff(a)'],
    ['?;u / ?;v', '(v*$diff(u) - u*$diff(v))/v^2'],
    ['?;u * ?;v`+', 'u*$diff(v) + v*$diff(u)'],
    ['e^?;p', '$diff(p)*e^p'],
    ['exp(?;p)', '$diff(p)*exp(p)'],
    ['(`+-rational:$n);a ^ ?;b', 'ln(a) * $diff(b) * a^b'],
    ['?;a^(`+-rational:$n);p', 'p*$diff(a)*a^(p-1)'],
];
/** Rules for differentiating parts of expressions.
 *
 * Occurrences of the function `$diff` in the result expression have differentiation applied with respect to the same variable.
 *
 * @type {Object<Numbas.jme.rules.Rule>}
 */
calculus.differentiation_rules = differentiation_rules.map(function(r) {
    return new Numbas.jme.rules.Rule(r[0], r[1], 'acgs');
});

/** Standard derivatives of functions of one variable.
 *
 * {@link Numbas.jme.calculus.differentiate} replaces `x` in these expressions with the argument of the function, and applies the chain rule.
 *
 * @type {Object<Numbas.jme.tree>}
 */
calculus.derivatives = {
    'cos': '-sin(x)',
    'sin': 'cos(x)',
    'e': 'e^x',
    'ln': '1/x',
    'log': '1/(ln(10)*x)',
    'tan': 'sec(x)^2',
    'cosec': '-cosec(x)*cot(x)',
    'sec': 'sec(x)*tan(x)',
    'cot': '-cosec(x)^2',
    'arcsin': '1/sqrt(1-x^2)',
    'arccos': '-1/sqrt(1-x^2)',
    'arctan': '1/(1+x^2)',
    'cosh': 'sinh(x)',
    'sinh': 'cosh(x)',
    'tanh': 'sech(x)^2',
    'sech': '-sech(x)*tanh(x)',
    'cosech': '-cosech(x)*coth(x)',
    'coth': '-cosech(x)^2',
    'arccosh': '1/sqrt(x^2-1)',
    'arcsinh': '1/sqrt(x^2+1)',
    'arctanh': '1/(1-x^2)',
    'sqrt': '1/(2*sqrt(x))'
};

for(var x in calculus.derivatives) {
    calculus.derivatives[x] = jme.compile(calculus.derivatives[x]);
}

/** Functions that differentiation distributes over.
 *
 * i.e. d/dx f(a, b, ...) = f(da/dx, db/dx, ...)
 *
 * @type {Object<boolean>}
 */
calculus.distributing_derivatives = {
    'vector': true,
    'matrix': true,
    'rowvector': true,

}

var function_derivative_rule = new jme.rules.Rule('m_func(?;f,?;a)', '$diff(m_listval(a,0))*standard_derivative(f,m_listval(a,0))');

/** Differentiate the given expression with respect to the given variable name.
 *
 * @param {Numbas.jme.tree} tree
 * @param {string} x
 * @param {Numbas.jme.Scope} scope
 * @returns {Numbas.jme.tree}
 */
calculus.differentiate = function(tree, x, scope) {
    /** Apply differentiation to the given tree.
     *
     * @param {Numbas.jme.tree} tree
     * @returns {Numbas.jme.tree}
     */
    function apply_diff(tree) {
        if(jme.isFunction(tree.tok, '$diff')) {
            var res = base_differentiate(tree.args[0]);
            return res;
        } else if(jme.isFunction(tree.tok, 'standard_derivative')) {
            var name = tree.args[0].tok.value;
            var derivative = calculus.derivatives[name];
            var arg = apply_diff(tree.args[1]);
            var scope = new jme.Scope({variables: {x: arg}});
            return jme.substituteTree(derivative, scope);
        }
        if(tree.args) {
            var args = tree.args.map(apply_diff);
            return {tok: tree.tok, args: args};
        }
        return tree;
    }

    /** Apply base_differentiation over all the tree's arguments, but don't look at the root token.
     *
     * @param {Numbas.jme.tree} tree
     * @returns {Numbas.jme.tree}
     */
    function distribute_differentiation(tree) {
        var nargs = tree.args.map(base_differentiate);
        return {tok: tree.tok, args: nargs};
    }

    /** Apply differentiation to the given tree.
     * First look at the type of the root token, then see if the tree matches any of the differentiation rules.
     *
     * @see Numbas.jme.calculus.differentiation_rules
     * @param {Numbas.jme.tree} tree
     * @returns {Numbas.jme.tree}
     */
    function base_differentiate(tree) {
        var tok = tree.tok;

        if(jme.isType(tok, 'number')) {
            return {tok: new TNum(0)};
        } else if(jme.isType(tok, 'name')) {
            var nameTok = jme.castToType(tok, 'name');
            return {tok: new TNum(nameTok.name == x ? 1 : 0)};
        } else if(jme.isType(tok, 'list')) {
            var listTok = jme.castToType(tok, 'list');
            if(tree.args) {
                return distribute_differentiation(tree);
            } else {
                return {tok: new jme.types.TList(listTok.value.map(function(v) {
                    return new TNum(0);
                }))};
            }
        } else if(jme.isType(tok, 'expression')) {
            var exprTok = jme.castToType(tok, 'expression');
            return base_differentiate(exprTok.tree);
        } else if(jme.isType(tok, 'op') || jme.isType(tok, 'function')) {
            if(tree.args.length == 1 && tok.name in calculus.derivatives) {
                const res = function_derivative_rule.replace(tree, scope);
                return apply_diff(res.expression);
            }
            if(calculus.distributing_derivatives[tok.name]) {
                return distribute_differentiation(tree);
            }
        }


        for(var i = 0;i < calculus.differentiation_rules.length;i++) {
            var result = calculus.differentiation_rules[i].replace(tree, scope);
            if(result.changed) {
                return apply_diff(result.expression);
            }
        }

        throw(new Numbas.Error("jme.calculus.unknown derivative", {tree: jme.display.treeToJME(tree)}));
    }

    tree = jme.rules.simplificationRules.basic.simplify(tree, scope);

    return base_differentiate(tree);
}

});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Stuff to do with displaying JME expressions - convert to TeX, simplify, or convert syntax trees back to JME
 *
 * Provides {@link Numbas.jme.display}
 */
Numbas.queueScript('jme-display', ['base', 'math', 'jme', 'util', 'jme-rules'], function() {
var math = Numbas.math;
var jme = Numbas.jme;
var util = Numbas.util;

var D1 = new Decimal(1);
var Dm1 = new Decimal(-1)
var DPI = Decimal.acos(-1);

/** @namespace Numbas.jme.display */
jme.display = /** @lends Numbas.jme.display */ {
    /** Convert a JME expression to LaTeX.
     *
     * @param {JME} expr
     * @param {Array.<string>|Numbas.jme.rules.Ruleset} ruleset - Can be anything accepted by {@link Numbas.jme.display.collectRuleset}.
     * @param {Numbas.jme.Scope} scope
     * @param {Numbas.jme.Parser} [parser=Numbas.jme.standardParser]
     * @returns {TeX}
     */
    exprToLaTeX: function(expr, ruleset, scope, parser) {
        if(!ruleset) {
            ruleset = jme.rules.simplificationRules.basic;
        }
        ruleset = jme.collectRuleset(ruleset, scope.allRulesets());
        expr += '';    //make sure expr is a string
        if(!expr.trim().length) {    //if expr is the empty string, don't bother going through the whole compilation proces
            return '';
        }
        var tree = jme.display.simplify(expr, ruleset, scope, parser); //compile the expression to a tree and simplify it

        var settings = util.extend_object({scope: scope}, ruleset.flags);
        var tex = texify(tree, settings, scope); //render the tree as TeX
        return tex;
    },

    /** Convert a compiled JME expression to LaTeX.
     *
     * @param {Numbas.jme.tree} tree
     * @param {Array.<string>|Numbas.jme.rules.Ruleset} ruleset - Can be anything accepted by {@link Numbas.jme.display.collectRuleset}.
     * @param {Numbas.jme.Scope} scope
     * @returns {TeX}
     */
    treeToLaTeX: function(tree, ruleset, scope) {
        if(!ruleset) {
            ruleset = jme.rules.simplificationRules.basic;
        }
        ruleset = jme.collectRuleset(ruleset, scope.allRulesets());

        var simplified_tree = jme.display.simplifyTree(tree, ruleset, scope);

        var settings = util.extend_object({scope: scope}, ruleset.flags);

        var tex = texify(simplified_tree, settings, scope);

        return tex;
    },

    /** Simplify a JME expression string according to the given ruleset and return it as a JME string.
     *
     * @param {JME} expr
     * @param {Array.<string>|Numbas.jme.rules.Ruleset} ruleset - Can be anything accepted by {@link Numbas.jme.display.collectRuleset}.
     * @param {Numbas.jme.Scope} scope
     * @returns {JME}
     *
     * @see Numbas.jme.display.simplify
     */
    simplifyExpression: function(expr, ruleset, scope) {
        if(expr.trim() == '') {
            return '';
        }
        var simplifiedTree = jme.display.simplify(expr, ruleset, scope);
        var settings = util.extend_object({nicenumber: false, noscientificnumbers: true}, ruleset.flags);
        return treeToJME(simplifiedTree, settings, scope);
    },
    /** Simplify a JME expression string according to given ruleset and return it as a syntax tree.
     *
     * @param {JME} expr
     * @param {Array.<string>|Numbas.jme.rules.Ruleset} ruleset
     * @param {Numbas.jme.Scope} scope
     * @param {Numbas.jme.Parser} [parser=Numbas.jme.standardParser]
     * @returns {Numbas.jme.tree}
     *
     * @see Numbas.jme.display.simplifyExpression
     * @see Numbas.jme.display.simplifyTree
     */
    simplify: function(expr, ruleset, scope, parser) {
        if(expr.trim() == '') {
            return '';
        }
        if(!ruleset) {
            ruleset = jme.rules.simplificationRules.basic;
        }
        ruleset = jme.collectRuleset(ruleset, scope.allRulesets());        //collect the ruleset - replace set names with the appropriate Rule objects
        parser = parser || Numbas.jme.standardParser;
        var exprTree = parser.compile(expr, {}, true);    //compile the expression to a tree. notypecheck is true, so undefined function names can be used.
        if(!exprTree) {
            return '';
        }
        return jme.display.simplifyTree(exprTree, ruleset, scope);    // simplify the tree
    },
    /** Simplify a syntax tree according to the given ruleset.
     *
     * @see Numbas.jme.rules.Ruleset#simplify
     * @param {Numbas.jme.tree} exprTree
     * @param {Numbas.jme.rules.Ruleset} ruleset
     * @param {Numbas.jme.Scope} scope
     * @param {boolean} allowUnbound
     * @returns {Numbas.jme.tree}
     *
     * @see Numbas.jme.display.simplify
     */
    simplifyTree: function(exprTree, ruleset, scope, allowUnbound) {
        return ruleset.simplify(exprTree, scope);
    },


    /** Substitute values into a JME string, and return an expression tree.
     *
     * @param {JME} expr
     * @param {Numbas.jme.Scope} scope
     * @returns {Numbas.jme.tree}
     */
    subvars: function(expr, scope) {
        var sbits = Numbas.util.splitbrackets(expr, '{', '}');
        var wrapped_expr = '';
        var subs = [];
        for(let j = 0; j < sbits.length; j += 1) {
            if(j % 2 == 0) {
                wrapped_expr += sbits[j];
            } else {
                var v = scope.evaluate(sbits[j]);
                if(Numbas.jme.display.treeToJME({tok:v}, {}, scope) == '') {
                    continue;
                }
                subs.push(jme.unwrapSubexpression({tok:v}));
                wrapped_expr += ' texify_simplify_subvar(' + (subs.length - 1) + ')';
            }
        }

        var tree = Numbas.jme.compile(wrapped_expr);
        if(!tree) {
            return tree;
        }

        /** Replace instances of `texify_simplify_subvar(x)` anywhere in the tree with the result of evaluating `x`.
         *
         * @param {Numbas.jme.tree} tree
         * @returns {Numbas.jme.tree}{
         */
        function replace_subvars(tree) {
            if(tree.tok.type == 'function' && tree.tok.name == 'texify_simplify_subvar') {
                return subs[tree.args[0].tok.value];
            }
            if(tree.args) {
                var args = tree.args.map(replace_subvars);
                return {tok: tree.tok, args: args, bracketed: tree.bracketed};
            }
            return tree;
        }

        var subbed_tree = replace_subvars(tree);

        return subbed_tree;
    }
};

/** The niceNumber options for a given token.
 *
 * @param {Numbas.jme.token} tok
 * @returns {Numbas.math.niceNumber_settings}
 */
var number_options = jme.display.number_options = function(tok) {
    const options = {
        precisionType: tok.precisionType,
        precision: tok.precision
    };
    if(tok.type == 'integer' || tok.type == 'rational') {
        options.store_precision = false;
    }
    return options;
}

/** Options for rendering a string token.
 *
 * @typedef Numbas.jme.display.string_options
 * @see Numbas.jme.types.TString
 * @property {boolean} latex - This string is LaTeX code.
 * @property {boolean} safe - If true, the string will be considered fixed and won't have special characters escaped.
 */

/** Get options for rendering a string token.
 *
 * @param {Numbas.jme.token} tok
 * @returns {Numbas.jme.display.string_options}
 */
var string_options = jme.display.string_options = function(tok) {
    return {
        latex: tok.latex,
        safe: tok.safe
    };
}

/** Is the given token a complex number?
 *
 * @param {Numbas.jme.token} tok
 * @returns {boolean}
 */
function isComplex(tok) {
    return (tok.type == 'number' && tok.value.complex && tok.value.im != 0) || (tok.type == 'decimal' && !tok.value.isReal());
}

/** Is the given token a negative number?
 *
 * @param {Numbas.jme.token} tok
 * @returns {boolean}
 */
function isNegative(tok) {
    if(!jme.isType(tok, 'number')) {
        return false;
    }
    if(isComplex(tok)) {
        return false;
    }
    if(tok.type == 'decimal') {
        return tok.value.re.isNegative();
    }
    tok = jme.castToType(tok, 'number');
    return tok.value < 0;
}

/** Is the given token a number with non-zero real part?
 *
 * @param {Numbas.jme.token} tok
 * @returns {boolean}
 */
function hasRealPart(tok) {
    switch(tok.type) {
        case 'number':
            return !tok.value.complex || tok.value.re != 0;
        case 'decimal':
            return !tok.value.re.isZero();
        default:
            return hasRealPart(jme.castToType(tok, 'number'));
    }
}

/** Get the complex conjugate of a token, assuming it's a number.
 *
 * @param {Numbas.jme.token} tok
 * @returns {Numbas.jme.token}
 */
function conjugate(tok) {
    switch(tok.type) {
        case 'number':
            return math.conjugate(tok.value);
        case 'decimal':
            return tok.value.conjugate().toComplexNumber();
        default:
            return conjugate(jme.castToType(tok, 'number'));
    }
}

/** Get the negation of a token, assuming it's a number.
 *
 * @param {Numbas.jme.token} tok
 * @returns {Numbas.jme.token}
 */
function negated(tok) {
    var v = tok.value;
    switch(tok.type) {
        case 'number':
            return math.negate(v);
        case 'decimal':
            return v.negated().toComplexNumber();
        default:
            return negated(jme.castToType(tok, 'number'));
    }
}

/** Helper function for texing infix operators.
 *
 * @memberof Numbas.jme.display
 * @private
 *
 * @param {TeX} code - The TeX command for the operator.
 * @returns {Function} - A function which will convert a syntax tree with the operator at the top to TeX, by putting `code` in between the TeX of the two arguments.
 */
function infixTex(code) {
    return function(tree, texArgs) {
        var arity = tree.args.length;
        if(arity == 1) {    //if operation is unary, prepend argument with code
            var arg = this.texifyOpArg(tree, texArgs, 0);
            return tree.tok.postfix ? arg + code : code + arg;
        } else if (arity == 2) {    //if operation is binary, put code in between arguments
            return this.texifyOpArg(tree, texArgs, 0) + ' ' + code + ' ' + this.texifyOpArg(tree, texArgs, 1);
        }
    }
}
/** Helper for texing nullary functions.
 *
 * @memberof Numbas.jme.display
 * @private
 *
 * @param {TeX} code - The TeX command for the function.
 * @returns {Function} - A function which returns the appropriate (constant) TeX code.
 */
function nullaryTex(code) {
    return function(tree, texArgs) {
        return '\\textrm{' + code + '}';
    };
}
/** Helper function for texing functions.
 *
 * @memberof Numbas.jme.display
 * @private
 *
 * @param {TeX} code - The TeX command for the function.
 * @returns {Function} - A function which converts a syntax tree to the appropriate TeX.
 */
function funcTex(code) {
    var f = function(tree, texArgs) {
        return code + ' \\left ( ' + texArgs.join(Numbas.locale.default_list_separator + ' ') + ' \\right )';
    }
    f.code = code;
    return f;
}

/** TeX the name of a pattern-matching operator.
 *
 * @param {TeX} code
 * @returns {TeX}
 */
function patternName(code) {
    return '\\operatorname{\\color{grey}{' + code + '}}';
}

/** TeX a unary positive or minus operation.
 *
 * @param {string} symbol - The symbol for the operation, either `+` or `-`.
 * @returns {Function} - A function which converts a syntax tree to the appropriate TeX.
 */
function texUnaryAdditionOrMinus(symbol) {
    return function(tree, texArgs) {
        var tex = texArgs[0];
        if(tree.args[0].tok.type == 'op') {
            var op = tree.args[0].tok.name;
            if(
                op == '-u' || op == '+u' ||
                (!(op == '/' || op == '*') && jme.precedence[op] > jme.precedence[symbol + 'u'])    //brackets are needed if argument is an operation which would be evaluated after the unary op
            ) {
                tex = '\\left ( ' + tex + ' \\right )';
            }
        } else if(isComplex(tree.args[0].tok)) {
            var tok = tree.args[0].tok;
            switch(tok.type) {
                case 'number':
                    var value = tok.value;
                    return this.number({complex:true, re:-value.re, im:-value.im}, number_options(tok));
                case 'decimal':
                    return this.number(tok.value.negated().toComplexNumber(), number_options(tok));
            }
        }
        return symbol + tex;
    }
}

/** Define how to texify each operation and function.
 *
 * @enum {Function}
 * @memberof Numbas.jme.display
 */
var texOps = jme.display.texOps = {
    '#': (function(tree, texArgs) {
        return texArgs[0] + ' \\, \\# \\, ' + texArgs[1];
    }),
    'not': infixTex('\\neg '),
    '+u': texUnaryAdditionOrMinus('+'),
    '-u': texUnaryAdditionOrMinus('-'),
    '^': (function(tree, texArgs) {
        var tex0 = texArgs[0];
        //if left operand is an operation, it needs brackets round it. Exponentiation is right-associative, so 2^3^4 won't get any brackets, but (2^3)^4 will.
        if(tree.args[0].tok.type == 'op' || (tree.args[0].tok.type == 'function' && tree.args[0].tok.name == 'exp') || this.texifyWouldBracketOpArg(tree, 0)) {
            tex0 = '\\left ( ' + tex0 + ' \\right )';
        }
        var trigFunctions = ['cos', 'sin', 'tan', 'sec', 'cosec', 'cot', 'arcsin', 'arccos', 'arctan', 'cosh', 'sinh', 'tanh', 'cosech', 'sech', 'coth', 'arccosh', 'arcsinh', 'arctanh'];
        if(tree.args[0].tok.type == 'function' && trigFunctions.contains(tree.args[0].tok.name) && jme.isType(tree.args[1].tok, 'number') && util.isInt(tree.args[1].tok.value) && tree.args[1].tok.value > 0) {
            return texOps[tree.args[0].tok.name].code + '^{' + texArgs[1] + '}' + '\\left( ' + this.render(tree.args[0].args[0]) + ' \\right)';
        }
        return (tex0 + '^{ ' + texArgs[1] + ' }');
    }),
    '*': (function(tree, texArgs) {
        var s = this.texifyOpArg(tree, texArgs, 0);
        for(let i = 1; i < tree.args.length; i++) {
            var left = tree.args[i - 1];
            var right = tree.args[i];
            var use_symbol = false;
            if(this.settings.alwaystimes) {
                use_symbol = true;
            } else {
                if(this.texifyWouldBracketOpArg(tree, i - 1) && this.texifyWouldBracketOpArg(tree, i)) {
                    use_symbol = false;
                // if we'd end up with two digits next to each other, but from different arguments, we need a times symbol
                } else if(util.isInt(texArgs[i - 1].charAt(texArgs[i - 1].length - 1)) && util.isInt(texArgs[i].charAt(0)) && !this.texifyWouldBracketOpArg(tree, i)) {
                    use_symbol = true;
                //real number times something that doesn't start with a digit or minus sign
                } else if (jme.isType(left.tok, 'number') && !isComplex(left.tok) && texArgs[i].match(/^[^\-+0-9]/)) {
                    use_symbol = false
                //number times a power of i
                } else if (jme.isOp(right.tok, '^') && jme.isType(right.args[0].tok, 'number') && math.eq(right.args[0].tok.value, math.complex(0, 1)) && jme.isType(left.tok, 'number')) {
                    use_symbol = false;
                // times sign when LHS or RHS is a factorial
                } else if((left.tok.type == 'function' && left.tok.name == 'fact') || (right.tok.type == 'function' && right.tok.name == 'fact')) {
                    use_symbol = true;
                //(anything except i) times i
                } else if (!(jme.isType(left.tok, 'number') && math.eq(jme.castToType(left.tok, 'number').value, math.complex(0, 1))) && jme.isType(right.tok, 'number') && math.eq(jme.castToType(right.tok, 'number').value, math.complex(0, 1))) {
                    use_symbol = false;
                // multiplication of two names, at least one of which has more than one letter
                } else if(right.tok.type == 'name' && left.tok.type == 'name' && Math.max(left.tok.nameInfo.letterLength, right.tok.nameInfo.letterLength) > 1) {
                    use_symbol = true;
                // multiplication of a name by something in brackets
                } else if(jme.isType(left.tok, 'name') && this.texifyWouldBracketOpArg(tree, i)) {
                    use_symbol = true;
                // anything times number, or (-anything), or an op with lower precedence than times, with leftmost arg a number
                } else if (jme.isType(right.tok, 'number') || (right.tok.type == 'op' && jme.precedence[right.tok.name] <= jme.precedence['*'] && texArgs[i].match(/^\d/))) {
                    use_symbol = true;
                }
            }
            s += use_symbol ? ' ' + this.texTimesSymbol() + ' ' : ' ';
            s += this.texifyOpArg(tree, texArgs, i);
        }
        return s;
    }),
    '/': (function(tree, texArgs) {
        if (this.settings.flatfractions) {
            return '\\left. ' + this.texifyOpArg(tree, texArgs, 0) + ' \\middle/ ' + this.texifyOpArg(tree, texArgs, 1) + ' \\right.'
        } else {
            return ('\\frac{ ' + texArgs[0] + ' }{ ' + texArgs[1] + ' }');
        }
    }),
    '+': (function(tree, texArgs) {
        var b = tree.args[1];
        if(jme.isOp(b.tok, '+u') || jme.isOp(b.tok, '-u')) {
            return texArgs[0] + ' + \\left ( ' + texArgs[1] + ' \\right )';
        } else {
            return texArgs[0] + ' + ' + texArgs[1];
        }
    }),
    '-': (function(tree, texArgs) {
        var b = tree.args[1];
        if(isComplex(b.tok) && hasRealPart(b.tok)) {
            var texb = this.number(conjugate(b.tok), number_options(b.tok));
            return texArgs[0] + ' - ' + texb;
        } else{
            if(jme.isOp(b.tok, '+') || jme.isOp(b.tok, '-') || jme.isOp(b.tok, '+u') || jme.isOp(b.tok, '-u')) {
                return texArgs[0] + ' - \\left ( ' + texArgs[1] + ' \\right )';
            } else {
                return texArgs[0] + ' - ' + texArgs[1];
            }
        }
    }),
    'dot': infixTex('\\cdot'),
    'cross': infixTex('\\times'),
    'transpose': (function(tree, texArgs) {
        var tex = texArgs[0];
        if(tree.args[0].tok.type == 'op') {
            tex = '\\left ( ' + tex + ' \\right )';
        }
        return (tex + '^{\\mathrm{T}}');
    }),
    '..': infixTex('\\dots'),
    'except': infixTex('\\operatorname{except}'),
    '<': infixTex('\\lt'),
    '>': infixTex('\\gt'),
    '<=': infixTex('\\leq'),
    '>=': infixTex('\\geq'),
    '<>': infixTex('\\neq'),
    '=': infixTex('='),
    'and': infixTex('\\wedge'),
    'or': infixTex('\\vee'),
    'nand': infixTex('\\operatorname{NAND}'),
    'nor': infixTex('\\operatorname{NOR}'),
    'xor': infixTex('\\operatorname{XOR}'),
    'implies': infixTex('\\implies'),
    'in': infixTex('\\in'),
    '|': infixTex('|'),
    'decimal': function(tree, texArgs) {
        if(jme.isType(tree.args[0].tok, 'string')) {
            var s = jme.castToType(tree.args[0].tok, 'string').value;
            var t = new jme.types.TDecimal(new Decimal(s));
            t.precisionType = 'dp';
            t.precision = math.countDP(s);
            return this.typeToTeX['decimal'].call(this, {tok:t}, t);
        }
        return texArgs[0];
    },
    'abs': (function(tree, texArgs) {
        var arg;
        if(tree.args[0].tok.type == 'vector') {
            arg = this.texVector(tree.args[0].tok.value, number_options(tree.args[0].tok));
        } else if(tree.args[0].tok.type == 'function' && tree.args[0].tok.name == 'vector') {
            arg = this.texVector(tree.args[0]);
        } else if(tree.args[0].tok.type == 'matrix') {
            arg = this.texMatrix(tree.args[0].tok.value, false, number_options(tree.args[0].tok));
        } else if(tree.args[0].tok.type == 'function' && tree.args[0].tok.name == 'matrix') {
            arg = this.texMatrix(tree.args[0], false);
        } else {
            arg = texArgs[0];
        }
        return ('\\left | ' + arg + ' \\right |');
    }),
    'sqrt': (function(tree, texArgs) {
        return ('\\sqrt{ ' + texArgs[0] + ' }');
    }),
    'exp': (function(tree, texArgs) {
        if(this.common_constants.e) {
            return (this.common_constants.e.tex + '^{ ' + texArgs[0] + ' }');
        } else {
            return funcTex('\\exp')(tree, texArgs);
        }
    }),
    'fact': (function(tree, texArgs) {
        if(jme.isType(tree.args[0].tok, 'number') || tree.args[0].tok.type == 'name') {
            return texArgs[0] + '!';
        } else {
            return '\\left (' + texArgs[0] + ' \\right )!';
        }
    }),
    'ceil': (function(tree, texArgs) {
        return '\\left \\lceil ' + texArgs[0] + ' \\right \\rceil';
    }),
    'floor': (function(tree, texArgs) {
        return '\\left \\lfloor ' + texArgs[0] + ' \\right \\rfloor';
    }),
    'int': (function(tree, texArgs) {
        return ('\\int \\! ' + texArgs[0] + ' \\, \\mathrm{d}' + texArgs[1]);
    }),
    'defint': (function(tree, texArgs) {
        return ('\\int_{' + texArgs[2] + '}^{' + texArgs[3] + '} \\! ' + texArgs[0] + ' \\, \\mathrm{d}' + texArgs[1]);
    }),
    'diff': (function(tree, texArgs) {
        var degree = tree.args.length >= 2 ? (jme.isType(tree.args[2].tok, 'number') && jme.castToType(tree.args[2].tok, 'number').value == 1) ? '' : '^{' + texArgs[2] + '}' : '';
        if(tree.args[0].tok.type == 'name') {
            if (this.settings.flatfractions) {
                return ('\\left. \\mathrm{d}' + degree + this.texifyOpArg(tree, texArgs, 0) + ' \\middle/ \\mathrm{d}' + this.texifyOpArg(tree, texArgs, 1) + '\\right.')
            } else {
                return ('\\frac{\\mathrm{d}' + degree + texArgs[0] + '}{\\mathrm{d}' + texArgs[1] + degree + '}');
            }
        } else {
            if (this.settings.flatfractions) {
                return ('\\left. \\mathrm{d}' + degree + '(' + texArgs[0] + ') \\middle/ \\mathrm{d}' + this.texifyOpArg(tree, texArgs, 1) + '\\right.')
            } else {
                return ('\\frac{\\mathrm{d}' + degree + '}{\\mathrm{d}' + texArgs[1] + degree + '} \\left (' + texArgs[0] + ' \\right )');
            }
        }
    }),
    'partialdiff': (function(tree, texArgs) {
        var degree = tree.args.length >= 2 ? (jme.isType(tree.args[2].tok, 'number') && jme.castToType(tree.args[2].tok, 'number').value == 1) ? '' : '^{' + texArgs[2] + '}' : '';
        if(tree.args[0].tok.type == 'name') {
            if (this.settings.flatfractions) {
                return ('\\left. \\partial ' + degree + this.texifyOpArg(tree, texArgs, 0) + ' \\middle/ \\partial ' + this.texifyOpArg(tree, texArgs, 1) + '\\right.')
            } else {
                return ('\\frac{\\partial ' + degree + texArgs[0] + '}{\\partial ' + texArgs[1] + degree + '}');
            }
        } else {
            if (this.settings.flatfractions) {
                return ('\\left. \\partial ' + degree + '(' + texArgs[0] + ') \\middle/ \\partial ' + this.texifyOpArg(tree, texArgs, 1) + '\\right.')
            } else {
                return ('\\frac{\\partial ' + degree + '}{\\partial ' + texArgs[1] + degree + '} \\left (' + texArgs[0] + ' \\right )');
            }
        }
    }),
    'sub': (function(tree, texArgs) {
        return texArgs[0] + '_{ ' + texArgs[1] + ' }';
    }),
    'sup': (function(tree, texArgs) {
        return texArgs[0] + '^{ ' + texArgs[1] + ' }';
    }),
    'limit': (function(tree, texArgs) {
        return ('\\lim_{' + texArgs[1] + ' \\to ' + texArgs[2] + '}{' + texArgs[0] + '}');
    }),
    'mod': (function(tree, texArgs) {
        return texArgs[0] + ' \\pmod{' + texArgs[1] + '}';
    }),
    'perm': (function(tree, texArgs) {
        return '^{' + texArgs[0] + '}\\kern-2pt P_{' + texArgs[1] + '}';
    }),
    'comb': (function(tree, texArgs) {
        return '^{' + texArgs[0] + '}\\kern-1pt C_{' + texArgs[1] + '}';
    }),
    'root': (function(tree, texArgs) {
        if(jme.isType(tree.args[1].tok, 'number')) {
            var n = jme.castToType(tree.args[1].tok, 'number').value;
            if(n == 2) {
                return '\\sqrt{ ' + texArgs[0] + ' }';
            }
        }
        return '\\sqrt[' + texArgs[1] + ']{ ' + texArgs[0] + ' }';
    }),
    'if': (function(tree, texArgs) {
                for(let i = 0;i < 3;i++) {
                    if(tree.args[i].args !== undefined) {
                        texArgs[i] = '\\left ( ' + texArgs[i] + ' \\right )';
                    }
                }
                return '\\textbf{If} \\; ' + texArgs[0] + ' \\; \\textbf{then} \\; ' + texArgs[1] + ' \\; \\textbf{else} \\; ' + texArgs[2];
            }),
    'switch': funcTex('\\operatorname{switch}'),
    'gcd': funcTex('\\operatorname{gcd}'),
    'lcm': funcTex('\\operatorname{lcm}'),
    'trunc': funcTex('\\operatorname{trunc}'),
    'fract': funcTex('\\operatorname{fract}'),
    'degrees': funcTex('\\operatorname{degrees}'),
    'radians': funcTex('\\operatorname{radians}'),
    'round': funcTex('\\operatorname{round}'),
    'sign': funcTex('\\operatorname{sign}'),
    'random': funcTex('\\operatorname{random}'),
    'max': funcTex('\\operatorname{max}'),
    'min': funcTex('\\operatorname{min}'),
    'precround': funcTex('\\operatorname{precround}'),
    'siground': funcTex('\\operatorname{siground}'),
    'award': funcTex('\\operatorname{award}'),
    'hour24': nullaryTex('hour24'),
    'hour': nullaryTex('hour'),
    'ampm': nullaryTex('ampm'),
    'minute': nullaryTex('minute'),
    'second': nullaryTex('second'),
    'msecond': nullaryTex('msecond'),
    'dayofweek': nullaryTex('dayofweek'),
    'sin': funcTex('\\sin'),
    'cos': funcTex('\\cos'),
    'tan': funcTex('\\tan'),
    'sec': funcTex('\\sec'),
    'cot': funcTex('\\cot'),
    'cosec': funcTex('\\csc'),
    'arccos': funcTex('\\arccos'),
    'arcsin': funcTex('\\arcsin'),
    'arctan': funcTex('\\arctan'),
    'cosh': funcTex('\\cosh'),
    'sinh': funcTex('\\sinh'),
    'tanh': funcTex('\\tanh'),
    'coth': funcTex('\\coth'),
    'cosech': funcTex('\\operatorname{cosech}'),
    'sech': funcTex('\\operatorname{sech}'),
    'arcsinh': funcTex('\\operatorname{arcsinh}'),
    'arccosh': funcTex('\\operatorname{arccosh}'),
    'arctanh': funcTex('\\operatorname{arctanh}'),
    'ln': function(tree, texArgs) {
        if(tree.args[0].tok.type == 'function' && tree.args[0].tok.name == 'abs') {
            return '\\ln ' + texArgs[0];
        } else {
            return '\\ln \\left ( ' + texArgs[0] + ' \\right )';
        }
    },
    'log': function(tree, texArgs) {
        var base = tree.args.length == 1 ? '10' : texArgs[1];
        return '\\log_{' + base + '} \\left ( ' + texArgs[0] + ' \\right )';
    },
    'vector': (function(tree, texArgs) {
        return '\\left ( ' + this.texVector(tree) + ' \\right )';
    }),
    'rowvector': (function(tree, texArgs) {
        if(tree.args[0].tok.type != 'list') {
            return this.texMatrix({args:[{args:tree.args}]}, true, number_options(tree.tok));
        } else {
            return this.texMatrix(tree, true, number_options(tree.tok));
        }
    }),
    'matrix': (function(tree, texArgs) {
        return this.texMatrix(tree, !this.settings.barematrices, number_options(tree.tok));
    }),
    'listval': (function(tree, texArgs) {
        return texArgs[0] + ' \\left[' + texArgs[1] + '\\right]';
    }),
    'set': function(tree, texArgs) {
        if(tree.args.length == 1 && tree.args[0].tok.type == 'list') {
            var list = tree.args[0];
            var items;
            items = list.tok ? list.args : list.value.map(tok => { return {tok} });
            return '\\left\\{ ' + items.map(item => this.render(item)).join(Numbas.locale.default_list_separator + ' ') + ' \\right\\}';
        } else {
            return '\\left\\{ ' + texArgs.join(Numbas.locale.default_list_separator + ' ') + ' \\right\\}';
        }
    },
    '`+-': infixTex(patternName('\\pm')),
    '`*/': infixTex(patternName('\\times \\atop \\div')),
    '`|': infixTex(patternName('|')),
    '`&': infixTex(patternName('\\wedge')),
    '`!': infixTex(patternName('\\neg')),
    '`where': infixTex(patternName('where')),
    '`@': infixTex(patternName('@')),
    '`?': unaryPatternTex(patternName('?')),
    '`*': unaryPatternTex(patternName('\\ast')),
    '`+': unaryPatternTex(patternName('+')),
    '`:': infixTex(patternName(':')),
    ';': function(tree, texArgs) {
        return '\\underset{\\color{grey}{' + texArgs[1] + '}}{' + texArgs[0] + '}';
    },
    ';=': function(tree, texArgs) {
        return '\\underset{\\color{grey}{=' + texArgs[1] + '}}{' + texArgs[0] + '}';
    },
    'm_uses': funcTex(patternName('uses')),
    'm_type': funcTex(patternName('type')),
    'm_exactly': overbraceTex('exactly'),
    'm_commutative': overbraceTex('commutative'),
    'm_noncommutative': overbraceTex('non-commutative'),
    'm_associative': overbraceTex('associative'),
    'm_nonassociative': overbraceTex('non-associative'),
    'm_strictplus': overbraceTex('strict-plus'),
    'm_gather': overbraceTex('gather'),
    'm_nogather': overbraceTex('no-gather'),
    'm_func': funcTex(patternName('func')),
    'm_op': funcTex(patternName('op')),
    'm_numeric': overbraceTex('numeric ='),
}

/** Returns a function which puts the given label over the first arg of the op.
 *
 * @param {string} label
 * @returns {Function}
 */
function overbraceTex(label) {
    return function(tree, texArgs) {
        return '\\overbrace{' + texArgs[0] + '}^{\\text{' + label + '}}';
    }
}

/** Produce LaTeX for a unary pattern-matching operator.
 *
 * @param {string} code - TeX for the operator's name.
 * @returns {Function}
 */
function unaryPatternTex(code) {
    return function(tree, texArgs) {
        return '{' + texArgs[0] + '}^{' + code + '}';
    }
}

/** Dictionary of functions to convert specific name annotations to TeX.
 *
 * @enum
 * @memberof Numbas.jme.display
 */
var texNameAnnotations = jme.display.texNameAnnotations = {
    verbatim: function(name) {    //verbatim - use to get round things like i and e being interpreted as constants
        return name;
    },
    op: function(name) {
        return '\\operatorname{' + name + '}';
    },
    vector: function(name) {
        return '\\boldsymbol{' + name + '}';
    },
    unit: function(name) {    //unit vector
        return '\\hat{' + name + '}';
    },
    dot: function(name) {        //dot on top
        return '\\dot{' + name + '}';
    },
    matrix: function(name) {
        return '\\mathrm{' + name + '}';
    },
    diff: function(name) {
        return '{\\mathrm{d}' + name + '}';
    },
    degrees: function(name) {
        return name + '^{\\circ}';
    },
    bb: function(name) {
        return '\\mathbb{' + name + '}';
    },
    complex: propertyAnnotation('complex'),
    imaginary: propertyAnnotation('imaginary'),
    real: propertyAnnotation('real'),
    positive: propertyAnnotation('positive'),
    nonnegative: propertyAnnotation('non-negative'),
    negative: propertyAnnotation('negative'),
    integer: propertyAnnotation('integer'),
    decimal: propertyAnnotation('decimal'),
    rational: propertyAnnotation('rational'),
    nonone: propertyAnnotation('nonone'),
    nonzero: propertyAnnotation('nonzero'),
}

/** Return a function which TeXs an annotation which marks a property for pattern-matching.
 *
 * @param {string} text
 * @returns {Function}
 */
function propertyAnnotation(text) {
    return function(name) {
        return '\\text{' + text + ' } ' + name;
    }
}
texNameAnnotations.verb = texNameAnnotations.verbatim;
texNameAnnotations.v = texNameAnnotations.vector;
texNameAnnotations.m = texNameAnnotations.matrix;

/** TeX a special name used in pattern-matching.
 *
 * @param {TeX} display
 * @returns {TeX}
 */
function texPatternName(display) {
    return '\\text{' + display + '}';
}

/** Names with special renderings.
 *
 * @memberof Numbas.jme.display
 * @type {Object<string>}
 */
var specialNames = jme.display.specialNames = {
    '$z': texPatternName('nothing'),
    '$n': texPatternName('number'),
    '$v': texPatternName('name')
}

/** Definition of a number with a special name.
 *
 * @typedef Numbas.jme.display.special_number_definition
 * @property {number} value - The number itself.
 * @property {TeX} tex - The TeX code for this number.
 * @property {JME} jme - The JME code for this number.
 */

/** Dictionary of functions to turn {@link Numbas.jme.types} objects into TeX strings.
 *
 * @enum
 * @memberof Numbas.jme.display
 */
var typeToTeX = jme.display.typeToTeX = {
    'nothing': function(tree, tok, texArgs) {
        return '\\text{nothing}';
    },
    'integer': function(tree, tok, texArgs) {
        return this.number(tok.value, number_options(tok));
    },
    'rational': function(tree, tok, texArgs) {
        return this.number(tok.value.toFloat(), number_options(tok));
    },
    'decimal': function(tree, tok, texArgs) {
        return this.decimal(tok.value, number_options(tok));
    },
    'number': function(tree, tok, texArgs) {
        return this.number(tok.value, number_options(tok));
    },
    'string': function(tree, tok, texArgs) {
        if(tok.latex) {
            if(tok.safe) {
                return tok.value;
            } else {
                return tok.value.replace(/\\([{}])/g, '$1').replace(/\$/g, '\\$');
            }
        } else {
            return '\\textrm{' + tok.value + '}';
        }
    },
    'boolean': function(tree, tok, texArgs) {
        return tok.value ? 'true' : 'false';
    },
    range: function(tree, tok, texArgs) {
        return tok.value[0] + ' \\dots ' + tok.value[1];
    },
    list: function(tree, tok, texArgs) {
        if(!texArgs) {
            texArgs = [];
            for(let i = 0;i < tok.vars;i++) {
                texArgs[i] = this.render({tok:tok.value[i]});
            }
        }
        return '\\left[ ' + texArgs.join(Numbas.locale.default_list_separator + ' ') + ' \\right]';
    },
    keypair: function(tree, tok, texArgs) {
        var key = '\\textrm{' + tok.key + '}';
        return key + ' \\operatorname{\\colon} ' + texArgs[0];
    },
    dict: function(tree, tok, texArgs) {
        if(!texArgs) {
            texArgs = [];
            if(tok.value) {
                for(var key in tok.value) {
                    texArgs.push(this.render({tok: new jme.types.TKeyPair(key), args:[{tok:tok.value[key]}]}));
                }
            }
        }
        return '\\left[ ' + texArgs.join(Numbas.locale.default_list_separator + ' ') + ' \\right]';
    },
    vector: function(tree, tok, texArgs) {
        return ('\\left ( '
                + this.texVector(tok.value, number_options(tok))
                + ' \\right )');
    },
    matrix: function(tree, tok, texArgs) {
        var m = this.texMatrix(tok.value, false, number_options(tok));
        if(!this.settings.barematrices) {
            m = '\\left ( ' + m + ' \\right )';
        }
        return m;
    },
    name: function(tree, tok, texArgs) {
        var c = this.scope.getConstant(tok.name);
        if(c) {
            return c.tex;
        }
        return this.texName(tok);
    },
    op: function(tree, tok, texArgs) {
        return this.texOp(tree, tok, texArgs);
    },
    'function': function(tree, tok, texArgs) {
        return this.texFunction(tree, tok, texArgs);
    },
    set: function(tree, tok, texArgs) {
        texArgs = [];
        for(let i = 0;i < tok.value.length;i++) {
            texArgs.push(this.render({tok: tok.value[i]}));
        }
        return '\\left\\{ ' + texArgs.join(Numbas.locale.default_list_separator + ' ') + ' \\right\\}';
    },
    expression: function(tree, tok, texArgs) {
        return this.render(tok.tree);
    },
    'lambda': function(tree, tok, texArgs) {
        var names = tok.names.map((name) => this.render(name)).join(', ');
        if(names.length != 1) {
            names = '\\left(' + names + '\\right)';
        }
        var expr = this.render(tok.expr);
        return '\\left(' + names + ' \\to ' + expr + '\\right)';
    },
}
/** Take a nested application of a single op, e.g. `((1*2)*3)*4`, and flatten it so that the tree has one op two or more arguments.
 *
 * @param {Numbas.jme.tree} tree
 * @param {string} op
 * @returns {Array.<Numbas.jme.tree>}
 */
function flatten(tree, op) {
    if(!jme.isOp(tree.tok, op)) {
        return [tree];
    }
    var args = [];
    for(let i = 0;i < tree.args.length;i++) {
        args = args.concat(flatten(tree.args[i], op));
    }
    return args;
}

/** A dictionary of settings for {@link Numbas.jme.display.texify}.
 *
 * @see Numbas.jme.rules.displayFlags
 *
 * @typedef Numbas.jme.display.displayer_settings
 * @property {boolean} fractionnumbers - Show all numbers as fractions?
 * @property {boolean} rowvector - Display vectors as a horizontal list of components?
 * @property {boolean} alwaystimes - Always show the multiplication symbol between multiplicands?
 * @property {boolean} mixedfractions - Show top-heavy fractions as mixed fractions, e.g. 3 3/4?
 * @property {boolean} flatfractions - Display fractions horizontally?
 * @property {boolean} barematrices - Render matrices without wrapping them in parentheses.
 * @property {boolean} nicenumber - Run numbers through {@link Numbas.math.niceNumber}?
 * @property {boolean} noscientificnumbers - If true, don't write numbers in scientific notation.
 * @property {number} accuracy - Accuracy to use when finding rational approximations to numbers. See {@link Numbas.math.rationalApproximation}.
 * @property {boolean} timesdot - Use a dot for the multiplication symbol instead of a cross?
 * @property {boolean} timesspace - Use a space for the multiplication symbol instead of a cross?
 */

/** An object which can convert a JME tree into some display format.
 *
 * @memberof Numbas.jme.display
 * @class
 *
 * @param {Numbas.jme.display.displayer_settings} settings
 * @param {Numbas.jme.Scope} scope
 * @see Numbas.jme.display.Texifier
 * @see Numbas.jme.display.JMEifier
 */
var JMEDisplayer = jme.display.JMEDisplayer = function(settings, scope) {
    this.settings = settings || {};
    this.scope = scope || Numbas.jme.builtinScope;
    this.getConstants();
}
JMEDisplayer.prototype = {

    /** Fill the dictionaries of constants from the scope. Done once, on creation of the Texifier.
     *
     */
    getConstants: function() {
        var scope = this.scope;
        this.constants = Object.values(scope.allConstants()).reverse();
        var common_constants = this.common_constants = {
            pi: null,
            imaginary_unit: null,
            e: null,
            infinity: null
        }
        var cpi = scope.getConstant('pi');
        if(cpi && util.eq(cpi.value, new jme.types.TNum(Math.PI), scope)) {
            common_constants.pi = cpi;
        }

        var imaginary_unit = new jme.types.TNum(math.complex(0, 1));
        this.constants.forEach(function(c) {
            if(jme.isType(c.value, 'number')) {
                var n = jme.castToType(c.value, 'number').value;
                if(util.eq(c.value, imaginary_unit, scope)) {
                    common_constants.imaginary_unit = c;
                } else if(math.piDegree(n) == 1) {
                    common_constants.pi = {
                        scale: n / Math.PI,
                        constant: c
                    }
                } else if(n === Infinity) {
                    common_constants.infinity = c;
                } else if(n == Math.E) {
                    common_constants.e = c;
                }
            }
        });
        this.constants.reverse();
    },

    /** Convert the given JME tree to the output format.
     *
     * @abstract
     * @param {Numbas.jme.tree} tree
     * @returns {*}
     */
    render: function(tree) {
    },

    /** Display a complex number.
     *
     * @abstract
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#number
     */
    complex_number: function(n, options) {
    },

    /** Display a number as a fraction.
     *
     * @abstract
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#number
     */
    rational_number: function(n, options) {
    },

    /** Display a number as a decimal.
     *
     * @abstract
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#number
     */
    real_number: function(n, options) {
    },

    /** Display a number.
     *
     * @param {number|complex} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#complex_number
     * @see Numbas.jme.display.JMEDisplayer#rational_number
     * @see Numbas.jme.display.JMEDisplayer#real_number
     */
    number: function(n, options) {
        if(n.complex) {
            return this.complex_number(n, options);
        } else {
            var fn = this.settings.fractionnumbers ? this.rational_number : this.real_number;
            return fn.call(this, n, options);
        }
    },


    /** Display a complex decimal.
     *
     * @abstract
     * @param {Numbas.math.ComplexDecimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#decimal
     */
    complex_decimal: function(n, options) {
    },

    /** Display a decimal as a fraction.
     *
     * @abstract
     * @param {Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#decimal
     */
    rational_decimal: function(n, options) {
    },

    /** Display a decimal as a decimal.
     *
     * @abstract
     * @param {Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#decimal
     */
    real_decimal: function(n, options) {
    },

    /** Display a decimal.
     *
     * @param {Numbas.math.ComplexDecimal|Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {*}
     * @see Numbas.jme.display.JMEDisplayer#complex_decimal
     * @see Numbas.jme.display.JMEDisplayer#rational_decimal
     * @see Numbas.jme.display.JMEDisplayer#real_decimal
     */
    decimal: function(n, options) {
        var isComplexDecimal = n instanceof Numbas.math.ComplexDecimal;
        if(isComplexDecimal && !n.isReal()) {
            return this.complex_decimal(n, options);
        } else {
            var fn = this.settings.fractionnumbers ? this.rational_decimal : this.real_decimal;
            var re = isComplexDecimal ? n.re : n;
            return fn.call(this, re, options);
        }
    },
};

/** Convert a JME tree to TeX.
 *
 * @augments Numbas.jme.display.JMEDisplayer
 * @memberof Numbas.jme.display
 */
var Texifier = jme.display.Texifier = util.extend(JMEDisplayer, function() {});
Texifier.prototype = {
    __proto__: JMEDisplayer.prototype,

    render: function(tree) {
        var texifier = this;
        if(!tree) {
            return '';
        }
        var texArgs;

        var tok = tree.tok || tree;
        if(jme.isOp(tok, '*')) {
            // flatten nested multiplications, so a string of consecutive multiplications can be considered together
            tree = {tok: tree.tok, args: flatten(tree, '*')};
        }
        if(tree.args) {
            tree = {
                tok: tree.tok,
                args: tree.args.map(function(arg) {
                    return jme.unwrapSubexpression(arg);
                })
            }
            texArgs = tree.args.map(function(arg) {
                return texifier.render(arg);
            });
        } else {
            var constantTex = this.texConstant(tree);
            if(constantTex) {
                return constantTex;
            }
        }
        if(tok.type in this.typeToTeX) {
            return this.typeToTeX[tok.type].call(this, tree, tok, texArgs);
        } else {
            throw(new Numbas.Error(R('jme.display.unknown token type', {type:tok.type})));
        }
    },

    /** Convert a complex number to TeX.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {complex} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    complex_number: function(n, options) {
        var imaginary_unit = '\\sqrt{-1}';
        if(this.common_constants.imaginary_unit) {
            imaginary_unit = this.common_constants.imaginary_unit.tex;
        }
        var re = this.number(n.re, options);
        var im = this.number(n.im, options) + ' ' + imaginary_unit;
        if(n.im == 0) {
            return re;
        } else if(n.re == 0) {
            if(n.im == 1) {
                return imaginary_unit;
            } else if(n.im == -1) {
                return '-' + imaginary_unit;
            } else {
                return im;
            }
        } else if(n.im < 0) {
            if(n.im == -1) {
                return re + ' - ' + imaginary_unit;
            } else {
                return re + ' ' + im;
            }
        } else {
            if(n.im == 1) {
                return re + ' + ' + imaginary_unit;
            } else {
                return re + ' + ' + im;
            }
        }
    },

    /** Convert a complex decimal to TeX.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Numbas.math.ComplexDecimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    complex_decimal: function(n, options) {
        var imaginary_unit = '\\sqrt{-1}';
        if(this.common_constants.imaginary_unit) {
            imaginary_unit = this.common_constants.imaginary_unit.tex;
        }
        var re = this.decimal(n.re, options);
        var im = this.decimal(n.im, options) + ' ' + imaginary_unit;
        if(n.im.isZero()) {
            return re;
        } else if(n.re.isZero()) {
            if(n.im.equals(D1)) {
                return imaginary_unit;
            } else if(n.im.equals(Dm1)) {
                return '-' + imaginary_unit;
            } else {
                return im;
            }
        } else if(n.im.isNegative()) {
            if(n.im.equals(Dm1)) {
                return re + ' - ' + imaginary_unit;
            } else {
                return re + ' ' + im;
            }
        } else {
            if(n.im.equals(D1)) {
                return re + ' + ' + imaginary_unit;
            } else {
                return re + ' + ' + im;
            }
        }
    },

    /** Convert a number to TeX, displaying it as a fraction using {@link Numbas.math.rationalApproximation}.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    rational_number: function(n, options) {
        var piD;
        if(this.common_constants.pi && (piD = math.piDegree(n)) > 0) {
            n /= Math.pow(Math.PI * this.common_constants.pi.scale, piD);
        }
        var out = math.niceNumber(n, Object.assign({}, options, {syntax:'latex'}));
        if(out.length > 20 && !this.settings.noscientificnumbers) {
            var bits = math.parseScientific(n.toExponential(), false);
            return bits.significand + ' ' + this.texTimesSymbol() + ' 10^{' + bits.exponent + '}';
        }
        var f = math.rationalApproximation(Math.abs(n));
        if(f[1] == 1) {
            out = Math.abs(f[0]).toString();
        } else {
            if(this.settings.mixedfractions && f[0] > f[1]) {
                var properNumerator = math.mod(f[0], f[1]);
                var mixedInteger = (f[0] - properNumerator) / f[1];
                if (this.settings.flatfractions) {
                    out = mixedInteger + '\\; \\left. ' + properNumerator + ' \\middle/ ' + f[1] + ' \\right.';
                } else {
                    out = mixedInteger + ' \\frac{' + properNumerator + '}{' + f[1] + '}';
                }
            } else {
                if (this.settings.flatfractions) {
                    out = '\\left. ' + f[0] + ' \\middle/ ' + f[1] + ' \\right.'
                } else {
                    out = '\\frac{' + f[0] + '}{' + f[1] + '}';
                }
            }
        }
        if(n < 0 && out != '0') {
            out = '-' + out;
        }
        var circle_constant_symbol = this.common_constants.pi && this.common_constants.pi.constant.tex;
        switch(piD) {
            case undefined:
            case 0:
                return out;
            case 1:
                if(n == -1) {
                    return '-' + circle_constant_symbol;
                } else {
                    return out + ' ' + circle_constant_symbol;
                }
            default:
                if(n == -1) {
                    return '-' + circle_constant_symbol + '^{' + piD + '}';
                } else {
                    return out + ' ' + circle_constant_symbol + '^{' + piD + '}';
                }
        }
    },

    /** Convert a decimal to TeX, displaying it as a fraction.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    rational_decimal: function(n, options) {
        var piD;
        if(this.common_constants.pi && (piD = math.piDegree(n.toNumber())) > 0) {
            n = n.dividedBy(DPI.times(this.common_constants.pi.scale).pow(piD));
        }
        var out = math.niceDecimal(n, Object.assign({}, options, {syntax:'latex'}));
        if(out.length > 20 && !this.settings.noscientificnumbers) {
            var bits = math.parseScientific(n.toExponential(), false);
            return bits.significand + ' ' + this.texTimesSymbol() + ' 10^{' + bits.exponent + '}';
        }
        var f = n.toFraction();
        if(f[1].equals(D1)) {
            out = f[0].absoluteValue().toString();
        } else {
            if(this.settings.mixedfractions && f[0].greaterThan(f[1])) {
                var properNumerator = f[0].mod(f[1]);
                var mixedInteger = f[0].minus(properNumerator).dividedBy(f[1]);
                if (this.settings.flatfractions) {
                    out = mixedInteger + '\\; \\left. ' + properNumerator + ' \\middle/ ' + f[1] + ' \\right.';
                } else {
                    out = mixedInteger + ' \\frac{' + properNumerator + '}{' + f[1] + '}';
                }
            } else {
                if (this.settings.flatfractions) {
                    out = '\\left. ' + f[0] + ' \\middle/ ' + f[1] + ' \\right.'
                } else {
                    out = '\\frac{' + f[0] + '}{' + f[1] + '}';
                }
            }
        }
        if(n.isNegative() && out != '0') {
            out = '-' + out;
        }
        var circle_constant_symbol = this.common_constants.pi && this.common_constants.pi.constant.tex;
        switch(piD) {
            case undefined:
            case 0:
                return out;
            case 1:
                if(n.isNegative()) {
                    return '-' + circle_constant_symbol;
                } else {
                    return out + ' ' + circle_constant_symbol;
                }
            default:
                if(n == -1) {
                    return '-' + circle_constant_symbol + '^{' + piD + '}';
                } else {
                    return out + ' ' + circle_constant_symbol + '^{' + piD + '}';
                }
        }
    },

    /** Convert a number to TeX, displaying it as a decimal.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    real_number: function(n, options) {
        var piD;
        if(this.common_constants.pi && (piD = math.piDegree(n)) > 0) {
            n /= Math.pow(Math.PI * this.common_constants.pi.scale, piD);
        }
        var out = math.niceNumber(n, Object.assign({}, options, {syntax:'latex'}));
        if(out.length > 20 && !this.settings.noscientificnumbers) {
            var bits = math.parseScientific(n.toExponential(), false);
            return bits.significand + ' ' + this.texTimesSymbol() + ' 10^{' + bits.exponent + '}';
        }
        var circle_constant_symbol = this.common_constants.pi && this.common_constants.pi.constant.tex;
        switch(piD) {
            case undefined:
            case 0:
                return out;
            case 1:
                if(n == 1) {
                    return circle_constant_symbol;
                } else if(n == -1) {
                    return '-' + circle_constant_symbol;
                } else {
                    return out + ' ' + circle_constant_symbol;
                }
            default:
                if(n == 1) {
                    return circle_constant_symbol + '^{' + piD + '}';
                } else if(n == -1) {
                    return '-' + circle_constant_symbol + '^{' + piD + '}';
                } else {
                    return out + ' ' + circle_constant_symbol + '^{' + piD + '}';
                }
        }
    },

    /** Convert a decimal to TeX, displaying it as a decimal.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    real_decimal: function(n, options) {
        var piD;
        if(this.common_constants.pi && (piD = math.piDegree(n.toNumber())) > 0) {
            n = n.dividedBy(DPI.times(this.common_constants.pi.scale).pow(piD));
        }
        var out = math.niceDecimal(n, Object.assign({}, options, {syntax:'latex'}));
        if(out.length > 20 && !this.settings.noscientificnumbers) {
            var bits = math.parseScientific(n.toExponential(), false);
            return bits.significand + ' ' + this.texTimesSymbol() + ' 10^{' + bits.exponent + '}';
        }
        var circle_constant_symbol = this.common_constants.pi && this.common_constants.pi.constant.tex;
        switch(piD) {
            case undefined:
            case 0:
                return out;
            case 1:
                if(n == 1) {
                    return circle_constant_symbol;
                } else if(n == -1) {
                    return '-' + circle_constant_symbol;
                } else {
                    return out + ' ' + circle_constant_symbol;
                }
            default:
                if(n == 1) {
                    return circle_constant_symbol + '^{' + piD + '}';
                } else if(n == -1) {
                    return '-' + circle_constant_symbol + '^{' + piD + '}';
                } else {
                    return out + ' ' + circle_constant_symbol + '^{' + piD + '}';
                }
        }
    },
    /** Convert a vector to TeX. If `settings.rowvector` is true, then it's set horizontally.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Array.<number>|Numbas.jme.tree} v
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    texVector: function(v, options) {
        var texifier = this;
        var out;
        var elements;
        if(v.args) {
            elements = v.args.map(function(x) {
                return texifier.render(x)
            });
        } else {
            elements = v.map(function(x) {
                return texifier.number(x, options)
            });
        }
        if(this.settings.rowvector) {
            out = elements.join(this.settings.matrixcommas === false ? ' \\quad ' : ' ' + Numbas.locale.default_list_separator + ' ');
        } else {
            out = '\\begin{matrix} ' + elements.join(' \\\\ ') + ' \\end{matrix}';
        }
        return out;
    },
    /** Convert a matrix to TeX.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Array.<Array.<number>>|Numbas.jme.tree} m
     * @param {boolean} parens - Enclose the matrix in parentheses?
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {TeX}
     */
    texMatrix: function(m, parens, options) {
        var texifier = this;
        var out;
        let rows;
        if(m.args) {
            var all_lists = true;
            rows = m.args.map(function(x) {
                if(x.tok.type == 'list') {
                    return x.args.map(function(y) {
                        return texifier.render(y);
                    });
                } else {
                    all_lists = false;
                }
            })
            if(!all_lists) {
                return '\\operatorname{matrix}(' + m.args.map(function(x) {
                    return texifier.render(x);
                }).join(Numbas.locale.default_list_separator) + ')';
            }
        } else {
            rows = m.map(function(x) {
                return x.map(function(y) {
                    return texifier.number(y, options)
                });
            });
        }
        var commas = (rows.length == 1 && this.settings.matrixcommas !== false) || this.settings.matrixcommas;
        rows = rows.map(function(x) {
            return x.join((commas ? Numbas.locale.default_list_separator : '') + ' & ');
        });
        out = rows.join(' \\\\ ');
        var macro = parens ? 'pmatrix' : 'matrix';
        return '\\begin{' + macro + '} ' + out + ' \\end{' + macro + '}';
    },

    /** Return the TeX for the multiplication symbol.
     *
     * @returns {TeX}
     */
    texTimesSymbol: function() {
        if(this.settings.timesdot) {
            return '\\cdot';
        } else if(this.settings.timesspace) {
            return '\\,';
        } else {
            return '\\times';
        }
    },

    /** Convert a variable name to TeX.
     *
     * @memberof Numbas.jme.display
     *
     * @param {Numbas.jme.token} tok
     * @param {Function} [longNameMacro=texttt] - Function which returns TeX for a long name.
     * @returns {TeX}
     */
    texName: function(tok, longNameMacro) {
        var name = tok.nameWithoutAnnotation;
        var annotations = tok.annotation;
        longNameMacro = longNameMacro || (function(name) {
            return '\\texttt{' + name.replaceAll('_', '\\_') + '}';
        });
        /** Apply annotations to the given name.
         *
         * @param {TeX} name
         * @returns {TeX}
         */
        function applyAnnotations(name) {
            if(!annotations) {
                return name;
            }
            for(let i = 0;i < annotations.length;i++) {
                var annotation = annotations[i];
                if(annotation in texNameAnnotations) {
                    name = texNameAnnotations[annotation](name);
                } else {
                    name = '\\' + annotation + '{' + name + '}';
                }
            }
            return name;
        }

        if(specialNames[name]) {
            return applyAnnotations(specialNames[name]);
        }

        var nameInfo = tok.nameInfo;
        name = nameInfo.root;
        if(nameInfo.isGreek) {
            name = '\\' + name;
        }
        if(nameInfo.isLong) {
            name = longNameMacro(name);
        }
        name = applyAnnotations(name);
        if(nameInfo.subscript) {
            var subscript = nameInfo.subscript;
            if(nameInfo.subscriptGreek) {
                subscript = '\\' + subscript;
            }
            name += '_{' + subscript + '}';
        }
        if(nameInfo.primes) {
            name += nameInfo.primes;
        }
        return name;
    },

    texConstant: function(tree) {
        var constantTex;
        var scope = this.scope;
        this.constants.find(function(c) {
            if(c.value === null || c.value === undefined) {
                return false;
            }
            if(util.eq(tree.tok, c.value, scope)) {
                constantTex = c.tex;
                return true;
            }
            if(jme.isType(tree.tok, 'number') && jme.isType(c.value, 'number') && util.eq(negated(tree.tok), c.value, scope)) {
                constantTex = '-' + c.tex;
                return true;
            }
        });
        return constantTex;
    },

    texOp: function(tree, tok, texArgs) {
        var name = jme.normaliseName(tok.name, this.scope);
        var fn = name in this.texOps ? this.texOps[name] : infixTex('\\, \\operatorname{' + name + '} \\,');
        return fn.call(this, tree, texArgs);
    },

    texFunction: function(tree, tok, texArgs) {
        var normalisedName = jme.normaliseName(tok.name, this.scope);
        if(this.texOps[normalisedName]) {
            return this.texOps[normalisedName].call(this, tree, texArgs);
        } else {
            /** Long operators get wrapped in `\operatorname`.
             *
             * @param {string} name
             * @returns {TeX}
             */
            function texOperatorName(name) {
                return '\\operatorname{' + name.replace(/_/g, '\\_') + '}';
            }
            return this.texName(tok, texOperatorName) + ' \\left ( ' + texArgs.join(Numbas.locale.default_list_separator + ' ') + ' \\right )';
        }
    },

    /** Would texify put brackets around a given argument of an operator?
     *
     * @param {Numbas.jme.tree} tree
     * @param {number} i - The index of the argument.
     * @returns {boolean}
     */
    texifyWouldBracketOpArg: function(tree, i) {
        var precedence = jme.precedence;

        var arg = tree.args[i];
        if((jme.isOp(arg.tok, '-u') || jme.isOp(arg.tok, '+u')) && isComplex(arg.args[0].tok)) {
            arg = arg.args[0];
        }
        var tok = arg.tok;

        if(tok.type == 'op') {    //if this is an op applied to an op, might need to bracket
            if(tree.args.length == 1) {
                return tree.args[0].tok.type == 'op' && tree.args[0].args.length > 1;
            }
            var op1 = arg.tok.name;    //child op
            var op2 = tree.tok.name;            //parent op
            var p1 = precedence[op1];    //precedence of child op
            var p2 = precedence[op2];    //precedence of parent op
            //if leaving out brackets would cause child op to be evaluated after parent op, or precedences the same and parent op not commutative, or child op is negation and parent is exponentiation
            return (p1 > p2 || (p1 == p2 && i > 0 && !jme.commutative[op2]) || (i > 0 && (op1 == '-u' || op2 == '+u') && precedence[op2] <= precedence['*']))
        } else if(isComplex(tok) && tree.tok.type == 'op' && (tree.tok.name == '*' || tree.tok.name == '-u' || tree.tok.name == '-u' || i == 0 && tree.tok.name == '^')) {
            //complex numbers might need brackets round them when multiplied with something else or unary minusing
            var v = arg.tok.value;
            return !(v.re == 0 || v.im == 0);
        } else if(jme.isOp(tree.tok, '^') && this.settings.fractionnumbers && jme.isType(tok, 'number') && this.texConstant(arg) === undefined && math.rationalApproximation(Math.abs(tok.value))[1] != 1) {
            return true;
        }
        return false;
    },


    /** Apply brackets to an op argument if appropriate.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Numbas.jme.tree} tree
     * @param {Array.<string>} texArgs - The arguments of `thing`, as TeX.
     * @param {number} i - The index of the argument to bracket.
     * @returns {TeX}
     */
    texifyOpArg: function(tree, texArgs, i) {
        var tex = texArgs[i];
        if(this.texifyWouldBracketOpArg(tree, i)) {
            tex = '\\left ( ' + tex + ' \\right )';
        }
        return tex;
    }

}
Texifier.prototype.typeToTeX = jme.display.typeToTeX;
Texifier.prototype.texOps = jme.display.texOps;

/** Turn a syntax tree into a TeX string. Data types can be converted to TeX straightforwardly, but operations and functions need a bit more care.
 *
 * The idea here is that each function and op has a function associated with it which takes a syntax tree with that op at the top and returns the appropriate TeX.
 *
 * @memberof Numbas.jme.display
 * @function
 *
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.display.displayer_settings} settings
 * @param {Numbas.jme.Scope} scope
 *
 * @returns {TeX}
 */
var texify = Numbas.jme.display.texify = function(tree, settings, scope) {
    var texifier = new Texifier(settings, scope);
    return texifier.render(tree);
}

/** Dictionary of functions to turn {@link Numbas.jme.types} objects into JME strings.
 *
 * @enum
 * @memberof Numbas.jme.display
 */
var typeToJME = Numbas.jme.display.typeToJME = {
    'nothing': function(tree, tok, bits) {
        return 'nothing';
    },
    'integer': function(tree, tok, bits) {
        return this.number(tok.value, number_options(tok));
    },
    'rational': function(tree, tok, bits) {
        var value = tok.value.reduced();
        const options = number_options(tok);
        var numerator = this.number(value.numerator, options);
        if(value.denominator == 1) {
            return numerator;
        } else {
            return numerator + '/' + this.number(value.denominator, options);
        }
    },
    'decimal': function(tree, tok, bits) {
        return this.decimal(tok.value, number_options(tok));
    },
    'number': function(tree, tok, bits, settings) {
        return this.number(tok.value, number_options(tok));
    },
    name: function(tree, tok, bits) {
        return tok.name;
    },
    'string': function(tree, tok, bits) {
        return this.string(tok.value, string_options(tok));
    },
    html: function(tree, tok, bits) {
        var html = tok.html.replace(/"/g, '\\"');
        return 'html(safe("' + html + '"))';
    },
    'boolean': function(tree, tok, bits) {
        return (tok.value ? 'true' : 'false');
    },
    range: function(tree, tok, bits) {
        return tok.value[0] + '..' + tok.value[1] + (tok.value[2] == 1 ? '' : '#' + tok.value[2]);
    },
    list: function(tree, tok, bits) {
        var jmeifier = this;
        if(!bits) {
            if(tok.value) {
                bits = tok.value.map(function(b) {
                    return jmeifier.render({tok:b});
                });
            } else {
                bits = [];
            }
        }
        return '[ ' + bits.join(', ') + ' ]';
    },
    keypair: function(tree, tok, bits) {
        var key = this.typeToJME['string'].call(this, null, {value:tok.key}, []);
        var arg = bits[0];
        if(tree.args[0].tok.type == 'op') {
            arg = '( ' + arg + ' )';
        }
        return key + ': ' + arg;
    },
    dict: function(tree, tok, bits) {
        if(!bits) {
            bits = [];
            if(tok.value) {
                for(var key in tok.value) {
                    bits.push(this.render({tok: new jme.types.TKeyPair(key), args:[{tok:tok.value[key]}]}));
                }
            }
        }
        if(bits.length) {
            return '[ ' + bits.join(', ') + ' ]';
        } else {
            return 'dict()';
        }
    },
    vector: function(tree, tok, bits) {
        var jmeifier = this;
        return 'vector(' + tok.value.map(function(n) {
            return jmeifier.number(n, number_options(tok))
        }).join(',') + ')';
    },
    matrix: function(tree, tok, bits) {
        var jmeifier = this;
        return 'matrix(' +
            tok.value.map(function(row) {
                return '[' + row.map(function(n) {
                    return jmeifier.number(n, number_options(tok))
                }).join(',') + ']'
            }).join(',')
            + ')';
    },
    'function': function(tree, tok, bits) {
        if(tok.name in this.jmeFunctions) {
            return this.jmeFunctions[tok.name].call(this, tree, tok, bits);
        }
        if(!bits) {
            return tok.name + '()';
        } else {
            return tok.name + '(' + bits.join(',') + ')';
        }
    },
    op: function(tree, tok, bits) {
        var op = tok.name;
        var args = tree.args;
        var bracketed = [];
        for(let i = 0;i < args.length;i++) {
            var arg = args[i].tok;
            var isNumber = jme.isType(arg, 'number');
            var arg_type = arg.type;
            var arg_value = arg.value;
            var pd;
            var arg_op = null;
            if(arg_type == 'op') {
                arg_op = args[i].tok.name;
            } else if(isNumber) {
                if(isComplex(arg)) {
                    if(arg_value.re != 0) {
                        arg_op = arg_value.im < 0 ? '-' : '+';   // implied addition/subtraction because this number will be written in the form 'a+bi'
                    } else if(i == 0 || arg_value.im != 1) {
                        arg_op = '*';   // implied multiplication because this number will be written in the form 'bi'
                    }
                } else if(isNegative(arg)) {
                    arg_op = '-u';
                } else if(bits[i].indexOf('*') >= 0 || (this.common_constants.pi && (pd = math.piDegree(args[i].tok.value)) > 0 && arg_value / math.pow(Math.PI * this.common_constants.pi.scale, pd) > 1)) {
                    arg_op = '*';   // implied multiplication because this number will be written in the form 'a*pi'
                } else if(bits[i].indexOf('/') >= 0) {
                    arg_op = '/';   // implied division because this number will be written in the form 'a/b'
                }
            }
            var bracketArg = false;
            if(arg_op != null) {
                if((jme.isOp(arg, '-u') || jme.isOp(arg, '+u')) && isComplex(args[i].args[0].tok)) {
                    arg_op = '+';
                }
                var j = i > 0 ? 1 : 0;
                if(op in opBrackets) {
                    bracketArg = opBrackets[op][j][arg_op] == true || (tok.prefix && opBrackets[op][j][arg_op] === undefined);
                } else {
                    bracketArg = tok.prefix == true || tok.postfix == true;
                }
            }
            bracketed[i] = bracketArg;
            if(bracketArg) {
                bits[i] = '(' + bits[i] + ')';
            }
        }
        var symbol = ' ';
        if(this.jmeOpSymbols[op] !== undefined) {
            symbol = this.jmeOpSymbols[op];
        } else if(args.length > 1 && op.length > 1) {
            symbol = ' ' + op + ' ';
        } else {
            symbol = op;
        }
        switch(op) {
        case '-u':
            if(isComplex(args[0].tok)) {
                return this.number(negated(args[0].tok), number_options(args[0].tok));
            }
            break;
        case '-':
            if(isComplex(args[1].tok) && hasRealPart(args[1].tok)) {
                bits[1] = this.number(conjugate(args[1].tok), number_options(args[1].tok));
            }
            break;
        case '*':
            //omit multiplication symbol when not necessary
            var s = bits[0];
            for(let i = 1;i < args.length;i++) {
                //number or brackets followed by name or brackets doesn't need a times symbol
                //except <anything>*(-<something>) does
                var use_symbol = true;
                if(
                    !this.settings.alwaystimes &&
                    ((jme.isType(args[i - 1].tok, 'number') && bits[i - 1].match(/\d$/)) || bracketed[i - 1]) &&
                    (jme.isType(args[i].tok, 'name') || bracketed[i] && !(jme.isOp(tree.args[i].tok, '-u') || jme.isOp(tree.args[i].tok, '+u')))
                ) {
                    use_symbol = false;
                }
                if(use_symbol) {
                    s += symbol;
                }
                s += bits[i];
            }
            return s;
        }
        if(args.length == 1) {
            return tok.postfix ? bits[0] + symbol : symbol + bits[0];
        } else {
            return bits[0] + symbol + bits[1];
        }
    },
    set: function(tree, tok, bits) {
        var jmeifier = this;
        return 'set(' + tok.value.map(function(tree) {
            return jmeifier.render({tok:tree});
        }).join(',') + ')';
    },
    expression: function(tree, tok, bits) {
        var expr = this.render(tok.tree);
        if(this.settings.wrapexpressions) {
            expr = 'expression("' + jme.escape(expr) + '")';
        }
        return expr;
    },
    'lambda': function(tree, tok, bits) {
        var names = tok.names.map((name) => this.render(name)).join(', ');
        if(names.length != 1) {
            names = '(' + names + ')';
        }
        var expr = this.render(tok.expr);
        var fn = '(' + names + ' -> ' + expr + ')';
        if(bits) {
            return fn + '(' + bits.join(',') + ')';
        } else {
            return fn;
        }
    },
}

/** Register a new data type with the displayers.
 *
 * @param {Function} type - The constructor for the type. `type.prototype.type` must be a string giving the type's name.
 * @param {object} renderers - A dictionary of rendering functions, with keys `tex`, `jme` and `displayString`.
 *
 * @see Numbas.jme.display.typeToTeX
 * @see Numbas.jme.display.typeToJME
 * @see Numbas.jme.typeToDisplayString
 */
jme.display.registerType = function(type, renderers) {
    var name = type.prototype.type;
    if(renderers.tex) {
        typeToTeX[name] = renderers.tex;
    }
    if(renderers.jme) {
        typeToJME[name] = renderers.jme;
    }
    if(renderers.displayString) {
        jme.typeToDisplayString[name] = renderers.displayString;
    }
}

/** Define how to render function in JME, for special cases when the normal rendering `f(...)` isn't right.
 *
 * @enum {Function}
 * @memberof Numbas.jme.display
 */
var jmeFunctions = jme.display.jmeFunctions = {
    'dict': typeToJME.dict,
    'fact': function(tree, tok, bits) {
        if(jme.isType(tree.args[0].tok, 'number') || tree.args[0].tok.type == 'name') {
            return bits[0] + '!';
        } else {
            return '( ' + bits[0] + ' )!';
        }
    },
    'listval': function(tree, tok, bits) {
        return bits[0] + '[' + bits[1] + ']';
    }
}

/** A dictionary of settings for {@link Numbas.jme.display.treeToJME}.
 *
 * @typedef Numbas.jme.display.jme_display_settings
 * @property {boolean} fractionnumbers - Show all numbers as fractions?
 * @property {boolean} niceNumber - Run numbers through {@link Numbas.math.niceNumber}?
 * @property {boolean} wrapexpressions - Wrap TExpression tokens in `expression("")`?
 * @property {boolean} store_precision - Render numbers along with their precision metadata, if any?
 * @property {boolean} ignorestringattributes - Don't wrap strings in functions for attributes like latex() and safe().
 * @property {boolean} matrixcommas - Put commas between cells in matrix rows?
 * @property {number} accuracy - Accuracy to use when finding rational approximations to numbers. See {@link Numbas.math.rationalApproximation}.
 */

/** Turn a syntax tree back into a JME expression (used when an expression is simplified).
 *
 * @memberof Numbas.jme.display
 * @function
 *
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.display.jme_display_settings} settings
 * @param {Numbas.jme.Scope} scope
 * @returns {JME}
 */
var treeToJME = jme.display.treeToJME = function(tree, settings, scope) {
    var jmeifier = new JMEifier(settings, scope);
    return jmeifier.render(tree);
}
/** Does each argument (of an operation) need brackets around it?
 *
 * Arrays consisting of one object for each argument of the operation.
 *
 * @enum
 * @memberof Numbas.jme.display
 * @private
 */
var opBrackets = Numbas.jme.display.opBrackets = {
    '+u':[{'+':true, '-':true, '*':false, '/':false}],
    '-u':[{'+':true, '-':true, '*':false, '/':false}],
    '+': [{}, {}],
    '-': [{}, {'+':true, '-':true}],
    '*': [{'+u':true, '+':true, '-':true, '/':true}, {'+u':true, '-u':true, '+':true, '-':true, '/':true}],
    '/': [{'+u':true, '+':true, '-':true, '*':false}, {'+u':true, '-u':true, '+':true, '-':true, '*':true, '/':true}],
    '^': [{'+u':true, '-u':true, '+':true, '-':true, '*':true, '/':true, '^': true}, {'+u':true, '-u':true, '+':true, '-':true, '*':true, '/':true}],
    'and': [{'or':true, 'xor':true, 'nor':true}, {'or':true, 'xor':true, 'nor':true}],
    'not': [{'and':true, 'or':true, 'xor':true, 'nand': true, 'nor': true}],
    'or': [{'xor':true}, {'xor':true}],
    'xor':[{}, {}],
    '=': [{}, {}]
};

/** How to render operator symbols as JME.
 *
 * See `Numbas.jme.display.typeToJME.op`.
 *
 * @enum
 * @memberof Numbas.jme.display
 * @private
 */
var jmeOpSymbols = Numbas.jme.display.jmeOpSymbols = {
    '+u': '+',
    '-u': '-',
    'not': 'not ',
    'fact': '!',
    '+': ' + ',
    '-': ' - '
}

/** An object which can convert a JME tree into a string of JME code.
 *
 * @augments Numbas.jme.display.JMEDisplayer
 * @memberof Numbas.jme.display
 */
var JMEifier = jme.display.JMEifier = util.extend(JMEDisplayer, function() {});
JMEifier.prototype = {
    __proto__: JMEDisplayer.prototype,

    render: function(tree) {
        var jmeifier = this;
        if(!tree) {
            return '';
        }

        if(jme.isOp(tree.tok, '*')) {
            // flatten nested multiplications, so a string of consecutive multiplications can be considered together
            tree = {tok: tree.tok, args: flatten(tree, '*')};
        }

        var bits;
        if(tree.args !== undefined) {
            bits = tree.args.map(function(i) {
                return jmeifier.render(i)
            });
        } else {
            var constant = this.constant(tree);
            if(constant) {
                return constant;
            }
        }
        var tok = tree.tok;
        if(tok.type in this.typeToJME) {
            return this.typeToJME[tok.type].call(this, tree, tok, bits);
        } else {
            throw(new Numbas.Error(R('jme.display.unknown token type', {type:tok.type})));
        }
    },

    constant: function(tree) {
        var constantJME;
        var scope = this.scope;
        this.constants.find(function(c) {
            if(c.value === null) {
                return false;
            }
            if(util.eq(c.value, tree.tok, scope)) {
                constantJME = c.name;
                return true;
            }
            if(jme.isType(tree.tok, 'number') && jme.isType(c.value, 'number') && util.eq(c.value, negated(tree.tok), scope)) {
                constantJME = '-' + c.name;
                return true;
            }
        });
        return constantJME;
    },

    string: function(s, options) {
        options = options || {};

        var str = '"' + jme.escape(s) + '"';

        if(options.latex && !this.settings.ignorestringattributes) {
            return 'latex(' + str + ')';
        } else if(options.safe && !this.settings.ignorestringattributes) {
            return 'safe(' + str + ')';
        } else {
            return str;
        }
    },

    complex_number: function(n, options) {
        var imaginary_unit = 'sqrt(-1)';
        if(this.common_constants.imaginary_unit) {
            imaginary_unit = this.common_constants.imaginary_unit.name;
        }
        options = Object.assign({}, options, {store_precision:false});
        var re = this.number(n.re, options);
        var im = this.number(n.im, options);
        im += (im.match(/\d$/) ? '' : '*') + imaginary_unit;
        if(Math.abs(n.im) < 1e-15) {
            return re;
        } else if(n.re == 0) {
            if(n.im == 1) {
                return imaginary_unit;
            } else if(n.im == -1) {
                return '-' + imaginary_unit;
            } else {
                return im;
            }
        } else if(n.im < 0) {
            if(n.im == -1) {
                return re + ' - ' + imaginary_unit;
            } else {
                return re + ' - ' + im.slice(1);
            }
        } else {
            if(n.im == 1) {
                return re + ' + ' + imaginary_unit;
            } else {
                return re + ' + ' + im;
            }
        }
    },

    /** Call {@link Numbas.math.niceNumber} with the scope's symbols for the imaginary unit and circle constant.
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {string}
     */
    niceNumber: function(n, options) {
        options = options || {};
        if(this.common_constants.imaginary_unit) {
            options.imaginary_unit = this.common_constants.imaginary_unit.name;
        }
        if(this.common_constants.pi) {
            options.circle_constant = {
                scale: this.common_constants.pi.scale,
                symbol: this.common_constants.pi.constant.name
            };
        }
        if(this.common_constants.infinity) {
            options.infinity = this.common_constants.infinity.name;
        }
        return math.niceNumber(n, options);
    },

    /** Call {@link Numbas.math.niceNumber} with the scope's symbols for the imaginary unit and circle constant.
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {string}
     */
    niceDecimal: function(n, options) {
        options = options || {};
        if(this.common_constants.imaginary_unit) {
            options.imaginary_unit = this.common_constants.imaginary_unit.name;
        }
        if(this.common_constants.pi) {
            options.circle_constant = {
                scale: this.common_constants.pi.scale,
                symbol: this.common_constants.pi.constant.name
            };
        }
        if(this.common_constants.infinity) {
            options.infinity = this.common_constants.infinity.name;
        }
        return math.niceComplexDecimal(n, options);
    },

    /** Write a number in JME syntax as a fraction, using {@link Numbas.math.rationalApproximation}.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {JME}
     */
    rational_number: function(n, options) {
        var piD;
        if(isNaN(n)) {
            return 'NaN';
        }
        var circle_constant_symbol = this.common_constants.pi && this.common_constants.pi.constant.name;
        if(this.common_constants.pi && (piD = math.piDegree(n)) > 0) {
            n /= Math.pow(Math.PI * this.common_constants.pi.scale, piD);
        }
        var out;
        if(this.settings.nicenumber === false) {
            out = n + '';
        } else {
            out = this.niceNumber(n, options);
        }
        if(out.length > 20 && !this.settings.noscientificnumbers) {
            var bits = math.parseScientific(n.toExponential(), false);
            return bits.significand + '*10^(' + bits.exponent + ')';
        }
        var f = math.rationalApproximation(Math.abs(n), this.settings.accuracy);
        if(f[1] == 1) {
            out = Math.abs(f[0]).toString();
        } else {
            out = f[0] + '/' + f[1];
        }
        if(n < 0 && out != '0') {
            out = '-' + out;
        }
        switch(piD) {
            case undefined:
            case 0:
                return out;
            case 1:
                return out + ' ' + circle_constant_symbol;
            default:
                return out + ' ' + circle_constant_symbol + '^' + piD;
        }
    },

    /** Write a number in JME syntax as a decimal.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {JME}
     */
    real_number: function(n, options) {
        var piD;
        if(isNaN(n)) {
            return 'NaN';
        }
        options = options || {};
        if(this.common_constants.pi && (piD = math.piDegree(n, false)) > 0) {
            n /= Math.pow(Math.PI * this.common_constants.pi.scale, piD);
        }
        var out;
        if(this.settings.nicenumber === false) {
            out = n + '';
            if(out.match(/e/)) {
                out = math.unscientific(out);
            }
            var precision = options.precision === undefined ? 'nothing' : options.precision;
            var precisionType = options.precisionType === undefined ? 'nothing' : this.string(options.precisionType, {});
            var store_precision = options.store_precision === undefined ? this.settings.store_precision : options.store_precision;
            if(store_precision) {
                if(precision == 'nothing' && precisionType == 'nothing') {
                    out = 'imprecise(' + out + ')';
                } else {
                    out = 'with_precision(' + out + ', ' + precision + ', ' + precisionType + ')';
                }
                return out;
            }
        } else {
            out = this.niceNumber(n, Object.assign({}, options, {style:'plain'}));
        }
        if(Math.abs(n) < 1e-15) {
            if(this.settings.nicenumber === false) {
                return '0';
            } else {
                return this.niceNumber(0, options);
            }
        }
        if(out.length > 20 && !this.settings.noscientificnumbers) {
            var bits = math.parseScientific(n.toExponential(), false);
            return bits.significand + '*10^(' + bits.exponent + ')';
        }
        var circle_constant_symbol = this.common_constants.pi && this.common_constants.pi.constant.name;
        switch(piD) {
            case undefined:
            case 0:
                return out;
            case 1:
                if(n == 1) {
                    return circle_constant_symbol;
                } else if(n == -1) {
                    return '-' + circle_constant_symbol;
                } else {
                    return out + ' ' + circle_constant_symbol;
                }
            default:
                if(n == 1) {
                    return circle_constant_symbol + '^' + piD;
                } else if(n == -1) {
                    return '-' + circle_constant_symbol + '^' + piD;
                } else {
                    return out + ' ' + circle_constant_symbol + '^' + piD;
                }
        }
    },

    /** Write a {@link Numbas.jme.math.ComplexDecimal} in JME syntax.
     *
     * @memberof Numbas.jme.display
     * @private
     *
     * @param {Numbas.math.ComplexDecimal|Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @returns {JME}
     */
    decimal: function(n, options) {
        if(n instanceof Numbas.math.ComplexDecimal) {
            var re = this.decimal(n.re, options);
            if(n.isReal()) {
                return re;
            }
            var imaginary_unit = 'sqrt(-1)';
            if(this.common_constants.imaginary_unit) {
                imaginary_unit = this.common_constants.imaginary_unit.name;
            }
            var im = this.decimal(n.im, options) + '*' + imaginary_unit;
            if(n.re.isZero()) {
                if(n.im.eq(1)) {
                    return imaginary_unit;
                } else if(n.im.eq(-1)) {
                    return '-' + imaginary_unit;
                } else {
                    return im;
                }
            } else if(n.im.lt(0)) {
                if(n.im.eq(-1)) {
                    return re + ' - ' + imaginary_unit;
                } else {
                    return re + ' - ' + im.replace(/^(dec\(")?-/, '$1');
                }
            } else {
                if(n.im.eq(1)) {
                    return re + ' + ' + imaginary_unit;
                } else {
                    return re + ' + ' + im;
                }
            }
        } else if(n instanceof Decimal) {
            var out = math.niceDecimal(n, Object.assign({}, this.settings.plaindecimal ? {} : options, {style: 'plain'}));
            if(this.settings.plaindecimal) {
                return out;
            } else {
                if(out.length > 20) {
                    out = n.toExponential().replace(/e\+0$/, '');
                }
                return 'dec("' + out + '")';
            }
        } else {
            return this.number(n, options);
        }
    }

}
JMEifier.prototype.typeToJME = typeToJME;
JMEifier.prototype.jmeOpSymbols = jmeOpSymbols;
JMEifier.prototype.jmeFunctions = jmeFunctions;


/** Align a series of blocks of text under a header line, connected to the header by ASCII line characters.
 *
 * @param {string} header
 * @param {Array.<string>} items
 * @returns {string}
 */
var align_text_blocks = jme.display.align_text_blocks = function(header, items) {
    /** Pad a block of text so it's in the centre of a line of length `n`.
     *
     * @param {string} text
     * @param {number} n
     * @returns {string}
     */
    function centre(text, n) {
        return text.split('\n').map((line) => {
          if(line.length >= n) {
              return line;
          }
          var npad = (n - line.length) / 2;
          var nlpad = Math.floor(npad);
          var nrpad = Math.ceil(npad);
          for(let i = 0;i < nlpad;i++) {
              line = ' ' + line;
          }
          for(let i = 0;i < nrpad;i++) {
              line = line + ' ';
          }
          return line;

        }).join('\n');
    }

    var item_lines = items.map(function(item) {
        return item.split('\n')
    });
    var item_widths = item_lines.map(function(lines) {
        return lines.reduce(function(m, l) {
            return Math.max(l.length, m)
        }, 0)
    });
    var num_lines = item_lines.reduce(function(t, ls) {
        return Math.max(ls.length, t)
    }, 0);

    // make every item into a block with the same number of lines, and make every line in each block the same width by padding with spaces
    item_lines = item_lines.map(function(lines, i) {
        var w = item_widths[i];
        var o = [];
        for(let j = 0;j < num_lines;j++) {
            var l = lines[j] || '';
            for(let i = l.length;i < w;i++) {
                l += ' ';
            }
            o.push(l);
        }
        return o;
    });

    // join the item blocks together
    var bottom_lines = [];
    for(let i = 0;i < num_lines;i++) {
        bottom_lines.push(item_lines.map(function(lines) {
            return lines[i]
        }).join('  '));
    }

    // all the item blocks joined together
    var bottom_line = bottom_lines.join('\n');

    // calculate the width of the bottom block
    var width = item_widths.reduce(function(t, w) {
        return t + w
    }, 0) + 2 * (items.length - 1);
    var ci = Math.floor(width / 2 - 0.5);
    var top_line = '';
    top_line = centre(header, width);
    var middle_line;
    if(items.length == 1) {
        middle_line = '';
        for(let i = 0;i < width;i++) {
            middle_line += i == ci ? '│' : ' ';
        }
    } else {
        middle_line = items.map(function(rarg, i) {
            var s = '';
            var mid = Math.floor(item_widths[i] / 2 - 0.5);
            for(let j = 0;j < item_widths[i];j++) {
                if(i == 0) {
                    s += j < mid ? ' ' : j == mid ? '┌' : '─';
                } else if(i == items.length - 1) {
                    s += j < mid ? '─' : j == mid ? '┐' : ' ';
                } else {
                    s += j == mid ? '┬' : '─';
                }
            }
            return s;
        }).join('──');
    }
    var top_joins = {
        '│': '│',
        '┌': '├',
        '┐': '┤',
        '─': '┴',
        '┬': '┼'
    }
    var mid = top_joins[middle_line[ci]];
    middle_line = middle_line.slice(0, ci) + mid + middle_line.slice(ci + 1);
    if(top_line.length > bottom_line.length) {
        middle_line = centre(middle_line, header.length);
        bottom_line = centre(bottom_line, header.length);
    }
    return [top_line, middle_line, bottom_line].join('\n');
}

/** Display a tree as a diagram using.
 *
 * @param {Numbas.jme.tree} tree
 * @returns {string}
 */
var tree_diagram = Numbas.jme.display.tree_diagram = function(tree) {
    let args;
    switch(tree.tok.type) {
        case 'op':
        case 'function':
            args = tree.args.map(function(arg) {
                return tree_diagram(arg);
            });
            return align_text_blocks(tree.tok.name, args);
        case 'lambda':
            args = tree.args.map(function(arg) {
                return tree_diagram(arg);
            });
            return align_text_blocks(treeToJME({tok:tree.tok}), args);
        default:
            return treeToJME(tree);
    }
};

/** For backwards compatibility, copy references from some Numbas.jme.rules members to Numbas.jme.display.
 * These used to belong to Numbas.jme.display, but were moved into a separate file.
 */
['Rule', 'getTerms', 'matchTree', 'matchExpression', 'simplificationRules', 'compileRules'].forEach(function(name) {
    jme.display[name] = jme.rules[name];
});
});
;
Numbas.queueScript('jme-rules', ['base', 'math', 'jme-base', 'util'], function() {
/** @file Code to do with JME pattern-matching rules.
 *
 * Provides {@link Numbas.jme.rules}
 */
/** @namespace Numbas.jme.rules */
var math = Numbas.math;
var jme = Numbas.jme;
var util = Numbas.util;
jme.rules = {};

/** Options for {@link Numbas.jme.rules.matchTree}.
 *
 * @typedef Numbas.jme.rules.matchTree_options
 * @type {object}
 * @property {boolean} commutative - Should the commutativity of operations be used? If `false`, terms must appear in the same order as in the pattern.
 * @property {boolean} associative - Should the associativity of operations be used? If `true`, all terms in nested applications of associative ops are gathered together before comparing.
 * @property {boolean} allowOtherTerms - When matching an associative op, if the expression contains terms that don't match any of the pattern, should they be ignored? If `false`, every term in the expression must match a term in the pattern.
 * @property {boolean} strictInverse - If `false`, `a-b` will be interpreted as `a+(-b)` when finding additive terms.
 * @property {Numbas.jme.Scope} scope - A JME scope in which to evaluate conditions.
 */

/** Parse a string specifying options for a Rule.
 *
 * @param {string} str
 * @returns {Numbas.jme.rules.matchTree_options}
 * @see Numbas.jme.rules.Rule
 */
function parse_options(str) {
    return {
        commutative: str.match(/c/) !== null,
        associative: str.match(/a/) !== null,
        allowOtherTerms: str.match(/g/) !== null,
        gatherList: str.match(/l/) !== null,
        strictInverse: str.match(/s/) !== null
    };
}

/** Override or extend a matchTree options object with new values.
 *
 * @memberof Numbas.jme.rules
 * @param {Numbas.jme.rules.matchTree_options} a
 * @param {Numbas.jme.rules.matchTree_options} b
 * @returns {Numbas.jme.rules.matchTree_options}
 */
var extend_options = Numbas.jme.rules.extend_options = function(a, b) {
    a = a || {};
    b = b || {};
    return {
        commutative: b.commutative === undefined ? a.commutative : b.commutative,
        associative: b.associative === undefined ? a.associative : b.associative,
        allowOtherTerms: b.allowOtherTerms === undefined ? a.allowOtherTerms : b.allowOtherTerms,
        gatherList: b.gatherList === undefined ? a.gatherList : b.gatherList,
        strictInverse: b.strictInverse === undefined ? a.strictInverse : b.strictInverse,
        scope: b.scope === undefined ? a.scope : b.scope
    };
}

/** Simplification rule.
 *
 * @memberof Numbas.jme.rules
 * @class
 *
 * @param {JME} pattern - Expression pattern to match. Variables will match any sub-expression.
 * @param {JME} result - Expression pattern to rewrite to.
 * @param {string|Numbas.jme.rules.matchTree_options} options
 * @param {string} [name] - A human-readable name for the rule
 *
 * @property {JME} patternString - The JME string defining the pattern to match.
 * @property {JME} resultString - The JME string defining the result of the rule.
 * @property {Numbas.jme.rules.matchTree_options} options - Default options for the match algorithm.
 * @property {JME} conditionStrings - JME strings defining the conditions.
 * @property {Numbas.jme.tree} patternTree - `patternString` compiled to a syntax tree.
 * @property {Numbas.jme.tree} result - The parameter `result` compiled to a syntax tree.
 * @property {Numbas.jme.tree[]} conditions - The parameter `conditions` compiled to syntax trees.
 */
var Rule = jme.rules.Rule = function(pattern, result, options, name) {
    this.name = name;
    this.patternString = pattern;
    this.pattern = patternParser.compile(pattern);
    if(typeof(options) == 'string') {
        options = parse_options(options);
    }
    this.options = options || {};
    this.resultString = result;
    this.result = jme.compile(result);
}
Rule.prototype = /** @lends Numbas.jme.rules.Rule.prototype */ {
    toString: function() {
        return this.patternString + ' -> ' + this.resultString;
    },

    /** Extend this rule's default options with the given options.
     *
     * @param {Numbas.jme.rules.matchTree_options} options
     * @returns {Numbas.jme.rules.matchTree_options}
     */
    get_options: function(options) {
        if(!options) {
            return this.options;
        } else {
            return extend_options(this.options, options);
        }
    },
    /** Match a rule on given syntax tree.
     *
     * @memberof Numbas.jme.rules.Rule.prototype
     * @param {Numbas.jme.tree} exprTree - The syntax tree to test.
     * @param {Numbas.jme.Scope} scope - Used when checking conditions.
     * @returns {boolean|Numbas.jme.rules.jme_pattern_match} - `false` if no match, or a dictionary of matched subtrees.
     * @see Numbas.jme.rules.matchTree
     */
    match: function(exprTree, scope) {
        return matchTree(this.pattern, exprTree, this.get_options({scope:scope}));
    },

    /** Find all matches for the rule, anywhere within the given expression.
     *
     * @param {Numbas.jme.tree} exprTree - The syntax tree to test.
     * @param {Numbas.jme.Scope} scope - Used when checking conditions.
     * @returns {Array.<Numbas.jme.rules.jme_pattern_match>}
     * @see {Numbas.jme.rules.matchAllTree}
     */
    matchAll: function(exprTree, scope) {
        return matchAllTree(this.pattern, exprTree, this.get_options({scope:scope}));
    },

    /** Transform the given expression if it matches this rule's pattern.
     *
     * @param {Numbas.jme.tree} exprTree - The syntax tree to transform.
     * @param {Numbas.jme.Scope} scope - Used when checking conditions.
     * @returns {Numbas.jme.rules.transform_result}
     * @see Numbas.jme.rules.transform
     */
    replace: function(exprTree, scope) {
        return transform(this.pattern, this.result, exprTree, this.get_options({scope:scope}));
    },

    /** Transform all occurences of this rule's pattern in the given expression.
     *
     * @param {Numbas.jme.tree} exprTree - The syntax tree to transform.
     * @param {Numbas.jme.Scope} scope - Used when checking conditions.
     * @returns {Numbas.jme.rules.transform_result}
     * @see Numbas.jme.rules.transform
     */
    replaceAll: function(exprTree, scope) {
        return transformAll(this.pattern, this.result, exprTree, this.get_options({scope: scope}));
    }
}

/** Options for {@link Numbas.jme.rules.getTerms}.
 *
 * @typedef Numbas.jme.rules.getTerms_options
 * @type {object}
 * @property {boolean} commutative - Should the operator be considered as commutative, for the purposes of matching ops with opposites? If yes, `a>c` will produce terms `c` and `a` when `op='<'`.
 * @property {boolean} associative - Should the operator be considered as associative? If yes, `(a+b)+c` will produce three terms `a`,`b` and `c`. If no, it will produce two terms, `(a+b)` and `c`.
 * @property {boolean} strictInverse - If `false`, `a-b` will be interpreted as `a+(-b)` when finding additive terms.
 */

/** Information to do with a term found in an expression by {@link Numbas.jme.rules.getTerms}.
 *
 * @typedef Numbas.jme.rules.term
 * @type {object}
 * @property {Numbas.jme.tree} term - The term itself.
 * @property {Array.<string>} names - Names captured by this term.
 * @property {Array.<string>} equalnames - Identified names captured by this term.
 * @property {string} quantifier - Code describing how many times the term can appear, if it's a pattern term.
 * @property {number} min - The minimum number of times the term must appear.
 * @property {number} max - The maximum number of times the term can appear.
 * @property {Numbas.jme.tree} defaultValue - A value to use if this term is missing.
 */

var quantifier_combo = {
    '0':  {'`?': '0', '`*': '0', '`+': '0', '`:': '0'},
    '1':  {'`?': '`?', '`*': '`*', '`+': '`+', '`:': '`?'},
    '`?': {'`?': '`?', '`*': '`*', '`+': '`*', '`:': '`?'},
    '`*': {'`?': '`*', '`*': '`*', '`+': '`*', '`:': '`*'},
    '`+': {'`?': '`*', '`*': '`*', '`+': '`+', '`:': '`*'}
};

/** A term in a sequence.
 *
 * @class
 * @param {Numbas.jme.tree} tree
 * @property {Numbas.jme.tree} term - The argument `tree`.
 * @property {Array.<string>} names - Names captured by this term.
 * @property {Array.<string>} inside_equalnames - Identified names captured by this term inside the qualifier.
 * @property {Array.<string>} outside_equalnames - Identified names captured by this term outside the qualifier.
 * @property {string} quantifier - Code describing how many times the term can appear, if it's a pattern term.
 * @property {number} min - The minimum number of times the term must appear.
 * @property {number} max - The maximum number of times the term can appear.
 * @property {Numbas.jme.tree} defaultValue - A value to use if this term is missing.
 */
var Term = Numbas.jme.rules.Term = function(tree) {
    var names = [];
    var inside_equalnames = [];
    var outside_equalnames = [];
    var equalnames = outside_equalnames;
    var quantifier = '1';
    var defaultValue = null;
    if(jme.isName(tree.tok, '$z')) {
        quantifier = '0';
    }
    /** Unwrap quantifiers from the top of the tree.
     */
    while(tree.tok.type == 'op') {
        var op = tree.tok.name;
        if(op == ';') {
            names.push(tree.args[1]);
        } else if(op == ';=') {
            names.push(tree.args[1]);
            equalnames.push(resolveName(tree.args[1]).name);
        } else if(op == '`?' || op == '`*' || op == '`+') {
            quantifier = quantifier_combo[quantifier][tree.tok.name];
            equalnames = inside_equalnames;
        } else if(op == '`:') {
            quantifier = quantifier_combo[quantifier][tree.tok.name];
            if(defaultValue === null) {
                defaultValue = tree.args[1];
            }
        } else if(tree.args.length == 1 && tree.args[0].tok.type == 'op' && ['`?', '`*', '`+', '`:'].indexOf(tree.args[0].tok.name) >= 0) {
            // pull quantifiers through unary operations, so "-(x`?)" is equivalent to "(-x)`?".
            tree = {tok:tree.args[0].tok, args: [{tok:tree.tok, args: tree.args[0].args}]};
            continue;
        } else {
            break;
        }
        tree = tree.args[0];
    }
    /** Find "identified names" - captured subexpressions which must be equal every time the name is captured - inside this tree.
     * These are the right-hand arguments of the `;=` operator.
     * Names found are appended to the list `equalnames`.
     *
     * @param {Numbas.jme.tree} tree
     */
    function find_equal_names(tree) {
        if(tree.tok.type == 'op') {
            switch(tree.tok.name) {
                case ';=':
                    equalnames.push(resolveName(tree.args[1]).name);
                    break;
                case '`+':
                case '`?':
                case '`*':
                    return;
            }
        }
        if(tree.args) {
            tree.args.forEach(find_equal_names);
        }
    }
    find_equal_names(tree);

    this.term = tree;
    this.names = names;
    this.inside_equalnames = inside_equalnames;
    this.outside_equalnames = outside_equalnames;
    this.quantifier = quantifier;
    this.min = quantifier_limits[quantifier][0];
    this.max = quantifier_limits[quantifier][1];
    this.defaultValue = defaultValue;
}

/** Replacements to make when identifying terms in a sequence of applications of a given op.
 * When looking for terms joined by `op`, `nonStrictReplacements[op]` is a list of objects with keys `op` and `replacement`.
 * A tree `A op B` should be replaced with `replacement(tree)`.
 * For example, `x-y` should be rewritten to `x+(-y)`.
 */
var nonStrictReplacements = {
    '+': {
        '-': function(tree) {
            return {tok: new jme.types.TOp('+', false, false, 2, true, true), args: [tree.args[0], insertUnaryMinus(tree.args[1])]};
        }
    },
    '*': {
        '/': function(tree) {
            tree = {tok: new jme.types.TOp('*', false, false, 2, true, true), args: [tree.args[0], {tok:new jme.types.TOp('/u', false, true, 1, false, false), args:[tree.args[1]]}]};
            return tree;
        }
    }
};

/** Dictionary of 'canonical' ops to match in non-strict mode.
 * For example, `a-b` will be matched as `a+(-b)`.
 */
var nonStrictCanonicalOps = {
    '-': '+',
    '/': '*'
}

/** Insert a unary minus in this tree.
 * If it's a product, the minus applies to the leftmost factor.
 *
 * @param {Numbas.jme.tree} tree
 * @returns {Numbas.jme.tree}
 */
function insertUnaryMinus(tree) {
    if(jme.isOp(tree.tok, '*')) {
        return {tok: tree.tok, args: [insertUnaryMinus(tree.args[0]), tree.args[1]]};
    } else if(jme.isOp(tree.tok, '/')) {
        return {tok: tree.tok, args: [insertUnaryMinus(tree.args[0]), tree.args[1]]};
    } else {
        return {tok: new jme.types.TOp('-u', false, true, 1, false, false), args: [tree]};
    }
}

/** Remove capturing operators ; and ;= from the top of a tree.
 *
 * @param {Numbas.jme.tree} tree
 * @returns {object} - `tree`: the unwrapped tree, `names`: a list of names removed, `equalnames`: a list of identified names removed
 */
function unwrapCapture(tree) {
    var names = [];
    var equalnames = [];
    while(jme.isOp(tree.tok, ';')) {
        names.push(tree.args[1]);
        tree = tree.args[0];
    }
    while(jme.isOp(tree.tok, ';=')) {
        names.push(tree.args[1]);
        equalnames.push(resolveName(tree.args[1]).name);
        tree = tree.args[0];
    }

    return {tree:tree, names:names, equalnames: equalnames};
}

/** Given a tree representing a series of terms t1 <op> t2 <op> t3 <op> ..., return the terms as a list.
 *
 * @memberof Numbas.jme.rules
 * @param {Numbas.jme.tree} tree - The tree to find terms in.
 * @param {string} op - The name of the operator whose terms are to be found.
 * @param {Numbas.jme.rules.getTerms_options} options
 * @param {boolean} calculate_minimum - Should the minimum allowed number of occurrences of each term be calculated? This is a pre-process step when getting the terms in a pattern expression.
 * @returns {Array.<Numbas.jme.rules.term>}
 */
var getTerms = Numbas.jme.rules.getTerms = function(tree, op, options, calculate_minimum) {
    /** Add the list of existing names passed in at the start to each term.
     *
     * @param {Array.<Numbas.jme.rules.term>} items
     * @param {Array.<Numbas.jme.tree>} existing_names - Names captured higher up the tree.
     * @param {Array.<Numbas.jme.tree>} existing_equal_names - Identified names captured higher up the tree.
     * @returns {Array.<Numbas.jme.rules.term>}
     */
    function add_existing_names(items, existing_names, existing_equal_names) {
        return existing_names.length == 0 && existing_equal_names.length == 0 ? items : items.map(function(item) {
            return {
                term: item.term,
                names: existing_names.concat(item.names),
                inside_equalnames: item.inside_equalnames,
                outside_equalnames: existing_equal_names.concat(item.outside_equalnames),
                quantifier: item.quantifier,
                min: item.min,
                max: item.max,
                defaultValue: item.defaultValue,
            };
        });
    }

    // we'll cache the results of this call in the tree object, to save time if the same thing is asked for again
    var intree = tree;
    if(intree.terms === undefined) {
        intree.terms = {};
    }
    if(intree.terms[op] === undefined) {
        intree.terms[op] = {};
    }
    var option_signature = options.associative * 2 + (options.strictInverse);

    if(intree.terms[op][option_signature]) {
        return intree.terms[op][option_signature];
    }


    if(jme.isOp(tree.tok, '-u') && op == '*') {
        tree = insertUnaryMinus(tree.args[0]);
    }

    if(!options.strictInverse && op in nonStrictReplacements) {
        for(var subop in nonStrictReplacements[op]) {
            if(jme.isOp(tree.tok, subop)) {
                tree = nonStrictReplacements[op][subop](tree);
            }
        };
    }

    /** Is the given token the op we're looking for?
     * True if it's literally that operator, it's the converse of that operator, or it would be replaced to that op in non-strict mode.
     *
     * @param {Numbas.jme.token} tok
     * @returns {boolean}
     */
    function isThisOp(tok) {
        if(jme.isOp(tok, op)) {
            return true;
        }
        if(options.commutative && jme.converseOps[op] && jme.isOp(tok, jme.converseOps[op])) {
            return true;
        }
        if(!options.strictInverse && op in nonStrictReplacements && tok.type == 'op' && tok.name in nonStrictReplacements[op]) {
            return true;
        }
        return false;
    }

    var args = jme.isOp(tree.tok, op) ? tree.args : [tree];
    if(options.commutative && jme.converseOps[op] && jme.isOp(tree.tok, jme.converseOps[op])) {
        args = tree.args.slice().reverse();
    }

    var terms = [];

    for(var i = 0; i < args.length;i++) {
        var arg = args[i];
        var item = new Term(arg);
        var res = unwrapCapture(arg);
        var argtok = res.tree.tok;
        if(op == '*' && jme.isOp(argtok, '-u')) {
            argtok = unwrapCapture(args[i].args[0]).tree.tok;
        }
        if(options.associative && isThisOp(argtok)) {
            var sub = getTerms(res.tree, op, options, false);
            sub = add_existing_names(sub, item.names, item.outside_equalnames);
            if(item.quantifier != '1') {
                sub = sub.map(function(t) {
                    t.quantifier = quantifier_combo[t.quantifier][item.quantifier];
                });
            }
            terms = terms.concat(sub);
        } else {
            if(item.max > 0) {
                terms.push(item);
            }
        }
    }

    if(calculate_minimum) {
        terms.min_total = 0;
        terms.forEach(function(t) {
            terms.min_total += t.min;
        });
    }

    intree.terms[op][option_signature] = terms;
    return terms;
}

/** The `_match` name in a match object stores the whole tree that matched the pattern.
 * This function makes sure that `_match` is set, setting it to the given tree if it's missing.
 *
 * @param {Numbas.jme.rules.jme_pattern_match} m
 * @param {Numbas.jme.tree} exprTree
 * @returns {Numbas.jme.rules.jme_pattern_match}
 */
function preserve_match(m, exprTree) {
    if(m === false) {
        return false;
    }
    if(m._match === undefined) {
        m._match = exprTree;
    }
    return m;
}

/** A dictionary representing the results of a successful JME pattern match.
 * Maps variable names to trees.
 *
 * @typedef Numbas.jme.rules.jme_pattern_match
 * @type {Object<Numbas.jme.tree>}
 * @see {Numbas.jme.rules#matchTree}
 */

/** Recursively check whether `exprTree` matches `ruleTree`. Variables in `ruleTree` match any subtree.
 *
 * @function
 * @memberof Numbas.jme.rules
 *
 * @param {Numbas.jme.tree} ruleTree
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options - Options specifying the behaviour of the matching algorithm.
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match} - `false` if no match, otherwise a dictionary of subtrees matched to variable names.
 */
var matchTree = jme.rules.matchTree = function(ruleTree, exprTree, options) {
    var m = (function() {
        if(!exprTree) {
            return false;
        }

        if(jme.isType(ruleTree.tok, 'name')) {
            const c = options.scope.getConstant(ruleTree.tok.name);
            if(c) {
                ruleTree = {tok: c.value};
            }
        }

        if(jme.isType(exprTree.tok, 'name')) {
            const c = options.scope.getConstant(exprTree.tok.name);
            if(c) {
                exprTree = {tok: c.value};
            }
        }

        var ruleTok = ruleTree.tok;
        if(jme.isOp(ruleTok, ';') || jme.isOp(ruleTok, ';=')) {
            var m = matchTree(ruleTree.args[0], exprTree, options);
            if(!m) {
                return false;
            }
            var o = resolveName(ruleTree.args[1], m._match);
            m[o.name] = o.value;
            return m;
        }


        switch(ruleTok.type) {
            case 'name':
                return matchName(ruleTree, exprTree, options);
            case 'function':
                return matchFunction(ruleTree, exprTree, options);
            case 'op':
                return matchOp(ruleTree, exprTree, options);
            case 'list':
                return matchList(ruleTree, exprTree, options);
            default:
                return matchToken(ruleTree, exprTree, options);
        }
    })();
    return preserve_match(m, exprTree);
}

/** Conditions for the `$n` rule.
 *
 * @enum {Function}
 * @memberof Numbas.jme.rules
 */
var number_conditions = jme.rules.number_conditions = {
    'complex': function(exprTree) {
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return tok.value.complex;
    },
    'imaginary': function(exprTree) {
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return tok.value.complex && math.re(tok.value) == 0;
    },
    'real': function(exprTree) {
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return math.im(tok.value) == 0;
    },
    'positive': function(exprTree) {
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return math.positive(tok.value);
    },
    'nonnegative': function(exprTree) {
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return math.nonnegative(tok.value);
    },
    'negative': function(exprTree) {
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return math.negative(tok.value);
    },
    'integer': function(exprTree) {
        if(exprTree.tok.type == 'integer') {
            return true;
        }
        try {
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return Numbas.util.isInt(tok.value);
    },
    'decimal': function(exprTree) {
        try {
            jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return math.countDP(exprTree.tok.originalValue) > 0;
    },
    'rational': function(exprTree, options) {
        if(exprTree.tok.type == 'rational') {
            return true;
        }
        return matchTree(patternParser.compile('integer:$n/integer:$n`?'), exprTree, options);
    },
    'nonzero': function(exprTree) {
        try{
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return !math.eq(tok.value, 0);
    },
    'nonone': function(exprTree) {
        try{
            var tok = jme.castToType(exprTree.tok, 'number');
        } catch {
            return false;
        }
        return !math.eq(tok.value, 1);
    }
}

/** Special JME names used in pattern-matching.
 *
 * @enum {Function}
 * @memberof Numbas.jme.rules
 */
var specialMatchNames = jme.rules.specialMatchNames = {
    '?': function(ruleTree, exprTree, options) {
        return {};
    },
    '$n': function(ruleTree, exprTree, options) {
        var ruleTok = ruleTree.tok;
        var exprTok = exprTree.tok;
        if(ruleTok.annotation !== undefined) {
            var satisfies = ruleTok.annotation.every(function(condition) {
                var test = number_conditions[condition];
                return !test || test(exprTree, options);
            });
            if(!satisfies) {
                return false;
            }
        } else {
            if(!jme.isType(exprTok, 'number')) {
                return false;
            }
        }
        return {};
    },
    '$v': function(ruleTree, exprTree, options) {
        var exprTok = exprTree.tok;
        if(exprTok.type != 'name') {
            return false;
        }
        return {};
    },
    '$z': function(ruleTree, exprTree, options) {
        return false;
    }
}

/** Match a name token. `?` matches any name, `$n` matches a number, with constraints specified by annotations, `$z` never matches.
 * Otherwise, the name matches if the expression being considered is exactly the same name, ignoring case.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match. The top token is assumed to be a name.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 * @see Numbas.jme.rules.number_conditions
 */
function matchName(ruleTree, exprTree, options) {
    var ruleTok = ruleTree.tok;
    var exprTok = exprTree.tok;
    if(ruleTok.type != 'name') {
        return false;
    }
    if(ruleTok.nameWithoutAnnotation in specialMatchNames) {
        return specialMatchNames[ruleTok.nameWithoutAnnotation](ruleTree, exprTree, options);
    } else {
        if(exprTok.type != 'name') {
            return false;
        }
        var same = jme.normaliseName(ruleTok.name, options.scope) == jme.normaliseName(exprTok.name, options.scope);
        return same ? {} : false;
    }
}

/** Make a matching function which overrides one or more matching options, then calls {@link Numbas.jme.rules.matchTree}.
 *
 * @param {Numbas.jme.rules.matchTree_options} new_options
 * @returns {Function}
 */
function setMatchOptions(new_options) {
    return function(ruleTree, exprTree, options) {
        return matchTree(ruleTree.args[0], exprTree, extend_options(options, new_options));
    }
}

/** Match if the given pattern occurs as a subexpression anywhere in the given expression.
 *
 * @param {Numbas.jme.tree} ruleTree
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchAnywhere(ruleTree, exprTree, options) {
    var noptions = extend_options(options, {allowOtherTerms: true});
    var m = matchTree(ruleTree, exprTree, noptions);
    if(m !== false) {
        return m;
    }
    if(exprTree.args) {
        for(var i = 0;i < exprTree.args.length;i++) {
            var am = matchAnywhere(ruleTree, exprTree.args[i], options);
            if(am !== false) {
                return am;
            }
        }
    }
    return false;
}

/** Special JME functions used in pattern-matching.
 *
 * @enum {Function}
 * @memberof Numbas.jme.rules
 */
var specialMatchFunctions = jme.rules.specialMatchFunctions = {
    'm_uses': function(ruleTree, exprTree, options) {
        var names = ruleTree.args.map(function(t) {
            return t.tok.name;
        });
        return matchUses(names, exprTree, options);
    },
    'm_exactly': setMatchOptions({allowOtherTerms:false}),
    'm_commutative': setMatchOptions({commutative:true}),
    'm_noncommutative': setMatchOptions({commutative:false}),
    'm_associative': setMatchOptions({associative:true}),
    'm_nonassociative': setMatchOptions({associative:false}),
    'm_strictinverse': setMatchOptions({strictInverse:true}),
    'm_gather': setMatchOptions({gatherList:false}),
    'm_nogather': setMatchOptions({gatherList:true}),
    'm_type': function(ruleTree, exprTree, options) {
        var wantedType = ruleTree.args[0].tok.name || ruleTree.args[0].tok.value;
        return matchType(wantedType, exprTree);
    },
    'm_func': function(ruleTree, exprTree, options) {
        return matchGenericFunction(ruleTree, exprTree, options);
    },
    'm_op': function(ruleTree, exprTree, options) {
        return matchGenericOp(ruleTree, exprTree, options);
    },
    'm_anywhere': function(ruleTree, exprTree, options) {
        return matchAnywhere(ruleTree.args[0], exprTree, options);
    }
}

/** Match the application of a function.
 * Dispatches to one of the special pattern-matching functions, or {@link matchOrdinaryFunction} otherwise.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchFunction(ruleTree, exprTree, options) {
    var ruleTok = ruleTree.tok;
    if(ruleTok.type != 'function') {
        return false;
    }
    if(ruleTok.nameWithoutAnnotation in specialMatchFunctions) {
        return specialMatchFunctions[ruleTok.nameWithoutAnnotation](ruleTree, exprTree, options);
    } else {
        return matchOrdinaryFunction(ruleTree, exprTree, options);
    }
}

/** Match the application of any function. The first argument of `ruleTree` is a pattern that the function's name, considered as a string, must satisfy, and the second argument is a pattern that the function's arguments, considered as a list, must satisfy.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchGenericFunction(ruleTree, exprTree, options) {
    if(exprTree.tok.type != 'function') {
        return false;
    }
    var nameRule = ruleTree.args[0];
    var argsRule = ruleTree.args[1];
    var exprNameTree = {tok: new jme.types.TString(exprTree.tok.name)};
    var argsTree = {tok: new jme.types.TList(), args: exprTree.args};
    var m_name = matchTree(nameRule, exprNameTree, options);
    var m_args = matchTree(argsRule, argsTree, options);
    if(m_name && m_args) {
        return mergeMatches([m_name, m_args]);
    } else {
        return false;
    }
}

/** Match the application of any operator. The first argument of `ruleTree` is a pattern that the operator's name, considered as a string, must satisfy, and the second argument is a pattern that the operator's arguments, considered as a list, must satisfy.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchGenericOp(ruleTree, exprTree, options) {
    if(exprTree.tok.type != 'op') {
        return false;
    }
    var nameRule = ruleTree.args[0];
    var argsRule = ruleTree.args[1];
    var exprNameTree = {tok: new jme.types.TString(exprTree.tok.name)};
    var argsTree = {tok: new jme.types.TList(), args: exprTree.args};
    var m_name = matchTree(nameRule, exprNameTree, options);
    var m_args = matchTree(argsRule, argsTree, options);
    if(m_name && m_args) {
        return mergeMatches([m_name, m_args]);
    } else {
        return false;
    }
}

/** Special JME operators used in pattern-matching.
 *
 * @enum {Function}
 * @memberof Numbas.jme.rules
 */
var specialMatchOps = jme.rules.specialMatchOps = {
    '`?': function(ruleTree, exprTree, options) {
        return matchTree(ruleTree.args[0], exprTree, options);
    },
    '`*': function(ruleTree, exprTree, options) {
        return matchTree(ruleTree.args[0], exprTree, options);
    },
    '`+': function(ruleTree, exprTree, options) {
        return matchTree(ruleTree.args[0], exprTree, options);
    },
    '`|': function(ruleTree, exprTree, options) {
        return matchAny(ruleTree.args, exprTree, options);
    },
    '`:': function(ruleTree, exprTree, options) {
        return matchDefault(ruleTree.args[0], ruleTree.args[1], exprTree, options);
    },
    '`+-': function(ruleTree, exprTree, options) {
        return matchOptionalPrefix(['-u', '+u'], ruleTree.args[0], exprTree, options);
    },
    '`*/': function(ruleTree, exprTree, options) {
        return matchOptionalPrefix(['/u'], ruleTree.args[0], exprTree, options);
    },
    '`!': function(ruleTree, exprTree, options) {
        return matchNot(ruleTree.args[0], exprTree, options);
    },
    '`&': function(ruleTree, exprTree, options) {
        return matchAnd(ruleTree.args, exprTree, options);
    },
    '`where': function(ruleTree, exprTree, options) {
        return matchWhere(ruleTree.args[0], ruleTree.args[1], exprTree, options);
    },
    '`@': function(ruleTree, exprTree, options) {
        return matchMacro(ruleTree.args[0], ruleTree.args[1], exprTree, options);
    }
}

/** Match an application of an operator.
 * Dispatches to one of the special pattern-matching operators, or {@link matchOrdinaryOp} otherwise.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match. It's assumed that the topmost token is an operator.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchOp(ruleTree, exprTree, options) {
    var ruleTok = ruleTree.tok;
    if(ruleTok.type != 'op') {
        return false;
    }
    if(ruleTok.name in specialMatchOps) {
        return specialMatchOps[ruleTok.name](ruleTree, exprTree, options);
    } else {
        return matchOrdinaryOp(ruleTree, exprTree, options);
    }
}

/** Match a `where` condition - the expression must match the given pattern, and the condition specified in terms of the matched names must evaluate to `true`.
 *
 * @param {Numbas.jme.tree} pattern - The pattern to match.
 * @param {Numbas.jme.tree} condition - The condition to evaluate.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchWhere(pattern, condition, exprTree, options) {
    var scope = new Numbas.jme.Scope(options.scope);

    var m = matchTree(pattern, exprTree, options);
    if(!m) {
        return false;
    }

    condition = Numbas.util.copyobj(condition, true);
    condition = jme.substituteTree(condition, new jme.Scope([{variables:m}]), true);
    try {
        var cscope = new jme.Scope([scope, {variables:m}]);
        var result = cscope.evaluate(condition, null, true);
        if(result.type == 'boolean' && result.value == false) {
            return false;
        }
    } catch {
        return false;
    }
    return m;
}

/** Substitute sub-patterns into a bigger pattern before matching.
 *
 * @param {Numbas.jme.tree} subPatterns - A dictionary of patterns.
 * @param {Numbas.jme.tree} pattern - The pattern to substitute into.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchMacro(subPatterns, pattern, exprTree, options) {
    if(subPatterns.tok.type != 'dict') {
        throw(new Numbas.Error('jme.matchTree.match macro first argument not a dictionary'));
    }
    var d = {}
    subPatterns.args.forEach(function(keypair) {
        var name = keypair.tok.key;
        var tree = keypair.args[0];
        d[name] = tree;
    });
    pattern = jme.substituteTree(pattern, new jme.Scope([{variables:d}]), true);
    return matchTree(pattern, exprTree, options)
}

/** Match the application of a function.
 * Matches if the expression is the application of the same function, and all of the arguments match the arguments of the pattern.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchOrdinaryFunction(ruleTree, exprTree, options) {
    var ruleTok = ruleTree.tok;
    var exprTok = exprTree.tok;
    if(exprTok.type != 'function' || (ruleTok.name != '?' && jme.normaliseName(ruleTok.name, options.scope) != jme.normaliseName(exprTok.name, options.scope))) {
        return false;
    }
    var ruleArgs = ruleTree.args.map(function(t) {
        return new Term(t);
    });
    var exprArgs = exprTree.args.map(function(t) {
        return new Term(t);
    });

    var namedTerms = matchTermSequence(ruleArgs, exprArgs, false, false, options);
    if(namedTerms === false) {
        return false;
    }

    /** Is the given name captured by this tree?
     *
     * @param {string} name
     * @param {Numbas.jme.tree} tree
     * @returns {boolean}
     */
    function name_captured(name, tree) {
        if(jme.isOp(tree.tok, ';')) {
            var res = resolveName(tree.args[1]);
            if(res.name == name) {
                return true;
            }
        }
        if(tree.args) {
            return tree.args.some(function(t2) {
                return name_captured(name, t2);
            });
        }
        return false;
    }

    // collate the named groups
    var match = {};
    for(var name in namedTerms) {
        var occurrences = 0;
        for(var i = 0;i < ruleTree.args.length;i++) {
            if(name_captured(name, ruleTree.args[i])) {
                occurrences += 1;
            }
        }
        var terms = namedTerms[name];
        match[name] = occurrences <= 1 ? terms[0] : {tok: new jme.types.TList(terms.length), args: terms};
    }
    return match;
}

/** Match the given expression against the given pattern, which is assumed to be a list.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchList(ruleTree, exprTree, options) {
    if(exprTree.tok.type != 'list') {
        return false;
    }
    /** Get the elements of a list. If it's been evaluated, the elements will be stored as the token's value. Otherwise, they're the arguments of the tree.
     *
     * @param {Numbas.jme.tree} list
     * @returns {Array.<Numbas.jme.tree>}
     */
    function getElements(list) {
        if(list.args) {
            return list.args;
        } else {
            return list.tok.value.map(function(e) {
                return {tok: e};
            });
        }
    }
    var ruleElements = getElements(ruleTree).map(function(t) {
        return new Term(t)
    });
    var exprElements = getElements(exprTree).map(function(t) {
        return new Term(t);
    });

    options = extend_options(options, {allowOtherTerms:false});

    var namedTerms = matchTermSequence(ruleElements, exprElements, false, false, options);
    if(namedTerms === false) {
        return false;
    }

    // collate the named groups
    var match = {};
    for(var name in namedTerms) {
        var terms = namedTerms[name];
        if(terms.length == 1 && !options.gatherList) {
            match[name] = terms[0];
        } else {
            match[name] = {tok: new jme.types.TList(terms.length), args: terms};
        }
    }
    return match;
}

/** Match an exact token - the expression must be the same type, and equal to, the rule token.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.jme_pattern_match}
 */
function matchToken(ruleTree, exprTree, options) {
    var ruleTok = ruleTree.tok;
    var exprTok = exprTree.tok;
    return util.eq(ruleTok, exprTok, options.scope) ? {} : false;
}

/** How many times must a quantifier match? First element is minimum number of occurrences, second element is maximum.
 */
var quantifier_limits = {
    '0': [0, 0],
    '1': [1, 1],
    '`?': [0, 1],
    '`*': [0, Infinity],
    '`+': [1, Infinity]
};

/** Resolve the name and value to store when capturing a subexpression.
 *
 * @param {Numbas.jme.tree} nameTree - The right-hand side of the `;` capturing operator. Either a name, or a keypair giving a name and the value to store.
 * @param {Numbas.jme.tree} value - The value to store, if `nameTree` doesn't override it.
 * @returns {object} - `name` is the name to store under, and `value` is the value.
 */
function resolveName(nameTree, value) {
    var nameTok = nameTree.tok;
    if(!(nameTok.type == 'name' || nameTok.type == 'keypair')) {
        throw(new Numbas.Error('jme.matchTree.group name not a name'));
    }
    var name;
    if(nameTok.type == 'name') {
        name = nameTok.name;
    } else if(nameTok.type == 'keypair') {
        name = nameTok.key;
        value = nameTree.args[0];
    }
    return {name: name, value: value};
}

/** Find names captured by this pattern.
 *
 * @param {Numbas.jme.tree} ruleTree
 * @returns {Array.<string>}
 */
var findCapturedNames = jme.rules.findCapturedNames = function(ruleTree) {
    var tok = ruleTree.tok;
    var names = [];
    if(jme.isOp(tok, ';') || jme.isOp(tok, ';=')) {
        var res = resolveName(ruleTree.args[1]);
        names.push(res.name);
    }
    if(ruleTree.args) {
        for(var i = 0;i < ruleTree.args.length;i++) {
            var argnames = findCapturedNames(ruleTree.args[i]);
            names = names.merge(argnames);
        }
    }
    return names;
}

/** Remove unary divisions from a tree.
 * They're only introduced to make pattern-matching products work more easily, and are a hassle to deal with elsewhere.
 * Expressions of the form `a*(/b)` are replaced with `a/b`.
 *
 * @param {Numbas.jme.tree} tree
 * @returns {Numbas.jme.tree}
 */
function removeUnaryDivision(tree) {
    if(jme.isOp(tree.tok, '*')) {
        if(jme.isOp(tree.args[1].tok, '/u')) {
            return {tok: new Numbas.jme.types.TOp('/', false, false, 2, false, false), args: [removeUnaryDivision(tree.args[0]), removeUnaryDivision(tree.args[1].args[0])]};
        }
        return {tok: tree.tok, args: tree.args.map(removeUnaryDivision)}
    }
    if(jme.isOp(tree.tok, '/u')) {
        return {tok: new Numbas.jme.types.TOp('/', false, false, 2, false, false), args: [{tok:new Numbas.jme.types.TNum(1)}, removeUnaryDivision(tree.args[0])]};
    }
    return tree;
}

/** Match an expression against a pattern which is an application of an operator to one or more terms.
 * Assuming that the pattern and the expression trees are each a sequence of terms joined by the same operator, find the terms of each, and try to match them up, obeying quantifiers in the pattern.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern to match, whose top token must be an operator.
 * @param {Numbas.jme.tree} exprTree - The expression being considered.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {Numbas.jme.jme_pattern_match}
 */
function matchOrdinaryOp(ruleTree, exprTree, options) {
    var ruleTok = ruleTree.tok;
    var exprTok = exprTree.tok;
    var op = ruleTok.name;
    var commuting = options.commutative && ruleTok.commutative;
    var associating = options.associative && ruleTok.associative;
    if(!options.strictInverse && nonStrictCanonicalOps[op]) {
        op = nonStrictCanonicalOps[op];
        commuting = options.commutative && jme.commutative[op];
        associating = options.associative && jme.associative[op];
    }
    var term_options = {commutative: options.commutative, associative: associating, strictInverse: options.strictInverse};
    var ruleTerms = getTerms(ruleTree, op, term_options, true);
    var exprTerms = getTerms(exprTree, op, term_options, false);
    if(exprTerms.length < ruleTerms.min_total) {
        return false;
    }

    if(!associating) {
        if(!jme.isOp(exprTok, op) && ruleTerms.length == 1) {
            return false;
        }
    }

    var namedTerms = matchTermSequence(ruleTerms, exprTerms, commuting, options.allowOtherTerms && associating, options);
    if(namedTerms === false) {
        return false;
    }

    // collate the named groups
    var match = {};
    for(var name in namedTerms) {
        var terms = namedTerms[name];
        if(terms.length == 1) {
            match[name] = removeUnaryDivision(terms[0]);
        } else if(options.gatherList) {
            match[name] = {tok: new jme.types.TList(terms.length), args: terms.map(function(t) {
                return {tok: new jme.types.TExpression(removeUnaryDivision(t))}
            })};
        } else {
            var sub = terms[0];
            for(var i = 1;i < terms.length;i++) {
                sub = {tok: new jme.types.TOp(op), args: [sub, terms[i]]};
            }
            if(op == '*') {
                sub = removeUnaryDivision(sub);
            }
            match[name] = sub;
        }
    }
    match['__op__'] = op;

    return match;
}

/** Match a sequence of terms.
 * Calls {@link Numbas.jme.rules.findSequenceMatch}, and uses {@link Numbas.jme.rules.matchTree} to match individual terms up.
 *
 * @param {Array.<Numbas.jme.rules.Term>} ruleTerms - The terms in the pattern.
 * @param {Array.<Numbas.jme.rules.Term>} exprTerms - The terms in the expression.
 * @param {boolean} commuting - Can the terms match in any order?
 * @param {boolean} allowOtherTerms - Allow extra terms which don't match any of the pattern terms?
 * @param {Numbas.jme.rules.matchTree_options} options
 * @param {Numbas.jme.rules.matchTree_options} term_options - Options to use when matching individual terms.
 * @returns {boolean | Object<Numbas.jme.jme_pattern_match>} - False if no match, or a dictionary mapping names to lists of subexpressions matching those names (it's up to whatever called this to join together subexpressions matched under the same name).
 */
function matchTermSequence(ruleTerms, exprTerms, commuting, allowOtherTerms, options, term_options) {
    term_options = term_options || options;
    var matches = {};
    exprTerms.forEach(function(_, i) {
        matches[i] = {}
    });

    /** Does the given input term match the given rule term?
     * The indices of the input and rule terms are given so the result of the match can be cached.
     *
     * @param {Numbas.jme.rules.term} exprTerm - The input term.
     * @param {Numbas.jme.rules.term} ruleTerm - The term in the pattern which must be matched.
     * @param {number} ic - The index of the input term.
     * @param {number} pc - The index of the rule term.
     * @returns {boolean}
     */
    function term_ok(exprTerm, ruleTerm, ic, pc) {
        if(matches[ic][pc] === undefined) {
            var m = matchTree(ruleTerm.term, exprTerm.term, term_options);
            var inside_equalnames = {};
            ruleTerm.inside_equalnames.forEach(function(name) {
                if(m[name]) {
                    inside_equalnames[name] = m[name];
                } else if(ruleTerm.names.some(function(n) {
                    return resolveName(n).name == name
                })) {
                    inside_equalnames[name] = m._match;
                }
            });
            var outside_equalnames = {};
            ruleTerm.outside_equalnames.forEach(function(name) {
                if(m[name]) {
                    outside_equalnames[name] = m[name];
                } else if(ruleTerm.names.some(function(n) {
                    return resolveName(n).name == name
                })) {
                    outside_equalnames[name] = m._match;
                }
            });
            matches[ic][pc] = {
                match: m,
                inside_equalnames: inside_equalnames,
                outside_equalnames: outside_equalnames
            }
        }
        return matches[ic][pc].match !== false;
    }

    /** Does the given assignment satisfy the constraints of the matching algorithm?
     * At the moment, the only constraint is that all subexpressions matched with the same name using the `;=` operator must be equal, according to {@link Numbas.jme.compareTrees}.
     *
     * @param {object} assignment - The result of {@link Numbas.jme.rules.findSequenceMatch}.
     * @param {number} ic - The current index in the list of input terms. Only matches introduced by this term are considered - previous terms are assumed to have already passed the constraint check.
     * @param {number} pc - The current index in the list of pattern terms.
     * @returns {boolean}
     */
    function constraint_ok(assignment, ic, pc) {
        var m1 = matches[ic][pc];
        var ruleTerm = ruleTerms[pc];
        if(ruleTerm.inside_equalnames.length == 0 && ruleTerm.outside_equalnames.length == 0) {
            return true;
        }
        var ok = assignment.every(function(p, i) {
            if(p < 0 || p >= ruleTerms.length) {
                return true;
            }
            var m2 = matches[i][p];
            var equalnames = p == pc ? 'inside_equalnames' : 'outside_equalnames';
            return ruleTerm[equalnames].every(function(name) {
                var e1 = m1[equalnames][name];
                var e2 = m2[equalnames][name];
                if(e1 === undefined || e2 === undefined) {
                    return true;
                }
                var res = jme.compareTrees(e1, e2) == 0;
                return res;
            });
        });
        return ok;
    }

    var assignment = findSequenceMatch(ruleTerms, exprTerms, {checkFn: term_ok, constraintFn: constraint_ok, commutative: commuting, allowOtherTerms: allowOtherTerms});
    if(assignment === false) {
        return false;
    }

    var namedTerms = {};

    var identified_names = {};
    ruleTerms.forEach(function(ruleTerm, i) {
        var equalnames = ruleTerm.outside_equalnames;
        equalnames.forEach(function(name) {
            identified_names[name] = identified_names[name] || ruleTerm;
        });
    });
    /** Record that `exprTree` was captured with the given name.
     *
     * @param {string} name
     * @param {Numbas.jme.tree} exprTree
     * @param {Numbas.jme.rules.Term} ruleTerm
     * @param {boolean} allowReservedName - If `false`, reserved names such as `_match` and `_rest`, which are introduced by the matching algorithm, will be ignored.
     */
    function nameTerm(name, exprTree, ruleTerm, allowReservedName) {
        if(!allowReservedName && name.match(/^_/)) {
            return;
        }
        if(!namedTerms[name]) {
            namedTerms[name] = [];
        }
        if(identified_names[name] !== undefined && identified_names[name] !== ruleTerm && namedTerms[name].length) {
            return;
        }
        namedTerms[name].push(exprTree);
    }
    /** Record that `exprTree` was matched against `ruleTerm` - add `exprTree` to all of `ruleTerm`'s names.
     *
     * @param {Numbas.jme.rules.term} ruleTerm
     * @param {Numbas.jme.tree} exprTree
     */
    function matchTerm(ruleTerm, exprTree) {
        ruleTerm.names.forEach(function(name) {
            var o = resolveName(name, exprTree);
            nameTerm(o.name, o.value, ruleTerm);
        });
    }

    assignment.result.forEach(function(is, j) {
        var ruleTerm = ruleTerms[j];

        if(is.length) {
            is.forEach(function(i) {
                var match = matches[i][j].match;
                for(var name in match) {
                    nameTerm(name, match[name], ruleTerm);
                }
                matchTerm(ruleTerm, exprTerms[i].term);
            });
        } else if(ruleTerm.defaultValue) {
            matchTerm(ruleTerm, ruleTerm.defaultValue);
        }
    });
    assignment.ignored_start_terms.forEach(function(i) {
        nameTerm('_rest', exprTerms[i].term, undefined, true);
        nameTerm('_rest_start', exprTerms[i].term, undefined, true);
    });
    assignment.ignored_end_terms.forEach(function(i) {
        nameTerm('_rest', exprTerms[i].term, undefined, true);
        nameTerm('_rest_end', exprTerms[i].term, undefined, true);
    });

    return namedTerms;
}

/** Options for {@link Numbas.jme.rules.findSequenceMatch}.
 *
 * @type {object}
 * @typedef Numbas.jme.rules.findSequenceMatch_options
 * @property {boolean} allowOtherTerms - If `true`, terms that don't match any term in the pattern can be ignored.
 * @property {boolean} commutative - Can the input terms be considered in any order?
 * @property {Function} constraintFn - Function to test if the current set of matches satisfies constraints.
 * @property {Function} checkFn - Function to test if an input term matches a given pattern term.
 */

/** Match a sequence of terms against a given pattern sequence of terms.
 * Try to find an assignment of input terms to the pattern, satisfying the quantifier for each term in the pattern.
 * The match is greedy - input terms will match earlier pattern terms in preference to later ones.
 *
 * @function
 * @memberof Numbas.jme.rules
 *
 * @param {Array.<Numbas.jme.rules.term>} pattern
 * @param {Array.<Numbas.jme.rules.term>} input
 * @param {Numbas.jme.rules.findSequenceMatch_options} options
 * @returns {object} - `ignored_start_terms` is terms at the start that weren't used in the match, `ignored_end_terms` is any other terms that weren't used, and `result[i]` is a list of indices of terms in the input that were matched against pattern term `i`.
 */
var findSequenceMatch = jme.rules.findSequenceMatch = function(pattern, input, options) {
    var capture = [];
    var start = 0;
    var done = false;
    var failed = false;
    var pc = 0;
    var ic = 0;

    /** Count the number of times we have matched pattern term `p` so far.
     *
     * @param {number} p - The index of the term.
     * @returns {number}
     */
    function count(p) {
        return capture.filter(function(x) {
            return x == p
        }).length;
    }
    /** Have we consumed pattern term `p` as many times as allowed?
     *
     * @param {number} p
     * @returns {boolean}
     */
    function consumed(p) {
        return count(p) >= pattern[p].max;
    }
    /** Have we matched this pattern term at least its minimum number of times?
     *
     * @param {number} p - The index of the pattern term.
     * @returns {boolean}
     */
    function enough(p) {
        return count(p) >= pattern[p].min;
    }
    /** Move the start pointer along one.
     * Terms before the start will be returned in `ignored_start_terms`.
     */
    function increment_start() {
        //debug('increment start position');
        start += 1;
        ic = start;
        pc = 0;
    }
    /** Backtrack to the last time we made a free choice.
     * If we're already at the start and `allowOtherTerms` is enabled, advance the start pointer.
     */
    function backtrack() {
        //debug('backtrack');
        if(options.allowOtherTerms && ic == start && capture.length == start && start < input.length - 1) {
            capture.push(-1);
            increment_start();
            return;
        }

        ic -= 1;
        while(ic >= start && (ic >= capture.length || capture[ic] >= pattern.length)) {
            ic -= 1;
        }
        //debug('backtracked to '+ic);

        if(ic < start) {
            if(options.allowOtherTerms && start < input.length - 1) {
                capture = [];
                increment_start();
                for(var i = 0;i < start;i++) {
                    capture.push(-1);
                }
                return;
            } else {
                failed = true;
                return;
            }
        }
        pc = capture[ic] + 1;
        capture = capture.slice(0, ic);
    }
    /** Move the input pointer along one.
     * If using commutativity, set the pattern pointer back to the start.
     */
    function advance_input() {
        ic += 1;
        if(options.commutative) {
            pc = 0;
        }
    }
    while(!done && !failed) {
        //show();
        while(pc < pattern.length && consumed(pc)) { // if have consumed this term fully, move on
            //debug('term '+pc+' consumed, move on');
            pc += 1;
        }
        if(ic == input.length) { // if we've reached the end of the input
            while(pc < pattern.length && enough(pc)) {
                //debug('got enough of '+pc+', skip forward');
                pc += 1;
            }
            if(pc == pattern.length) { // if we've consumed all the terms
                if(!pattern.every(function(_, p) {
                    return enough(p);
                })) {
                    //debug('reached end but some terms not matched enough times');
                    backtrack();
                } else {
                    //debug('reached end of pattern and end of input: done');
                    done = true;
                }
            } else {
                //debug('end of input but still pattern to match')
                backtrack();
            }
        } else if(pc >= pattern.length) {
            //debug("end of pattern but unconsumed input");
            if(pc == pattern.length && options.commutative && options.allowOtherTerms) {
                //debug('capturing '+ic+' as ignored end term');
                capture.push(pattern.length);
                advance_input();
            } else if(pc == pattern.length && !options.commutative && options.allowOtherTerms) {
                while(ic < input.length) {
                    //debug('capturing '+ic+' as ignored end term');
                    capture.push(pattern.length);
                    advance_input();
                }
            } else {
                backtrack();
            }
        } else if(options.checkFn(input[ic], pattern[pc], ic, pc) && options.constraintFn(capture, ic, pc)) {
            //debug('capture '+ic+' at '+pc);
            capture.push(pc);
            advance_input();
        } else if(options.commutative || enough(pc)) {
            //debug('trying the next pattern term');
            pc += 1;
        } else {
            //debug('can\'t match next input')
            backtrack();
        }
    }
    if(failed) {
        return false;
    }
    var result = pattern.map(function(p, i) {
        return capture.map(function(_, j) {
            return j
        }).filter(function(j) {
            return capture[j] == i;
        });
    });
    let ignored_start_terms, ignored_end_terms;
    if(options.commutative) {
        ignored_start_terms = [];
        ignored_end_terms = [];
        var ignored = ignored_start_terms;
        capture.forEach(function(p, i) {
            if(p == pattern.length) {
                ignored.push(i);
            } else {
                ignored = ignored_end_terms;
            }
        });
    } else {
        ignored_start_terms = input.slice(0, start).map(function(_, j) {
            return j
        });
        ignored_end_terms = capture.map(function(_, j) {
            return j
        }).filter(function(j) {
            return capture[j] == pattern.length
        });
    }
    //debug(result);
    return {ignored_start_terms: ignored_start_terms, result: result, ignored_end_terms: ignored_end_terms};
}

/** Match any of the given patterns.
 * The first pattern which successfully matches is used.
 *
 * @param {Array.<Numbas.jme.tree>} patterns
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchAny(patterns, exprTree, options) {
    for(var i = 0;i < patterns.length;i++) {
        var m = matchTree(patterns[i], exprTree, options);
        if(m) {
            return m;
        }
    }
    return false;
}

/** Perform a match with a default value.
 * This operation only makes sense when matching a sequence of terms, so just match the pattern.
 *
 * @param {Numbas.jme.tree} ruleTree
 * @param {Numbas.jme.tree} defaultValue - Ignored.
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchDefault(ruleTree, defaultValue, exprTree, options) {
    var m = matchTree(ruleTree, exprTree, options);
    return m;
}

/** Bring any unary minus to the top of the tree.
 *
 * @param {Numbas.jme.tree} tree
 * @returns {Numbas.jme.tree}
 */
var extractLeadingMinus = jme.rules.extractLeadingMinus = function(tree) {
    if(jme.isOp(tree.tok, '*') || jme.isOp(tree.tok, '/')) {
        if(jme.isOp(tree.args[0].tok, '-u')) {
            return {tok:tree.args[0].tok, args: [{tok:tree.tok, args: [tree.args[0].args[0], tree.args[1]]}]};
        } else {
            var left = extractLeadingMinus(tree.args[0]);
            if(jme.isOp(left.tok, '-u')) {
                return {tok: left.tok, args: [{tok: tree.tok, args: [left.args[0], tree.args[1]]}]};
            } else {
                return tree;
            }
        }
    } else {
        return tree;
    }
}

/** Match `rule`, or `prefix(rule)` - allow any of a list of optional unary operators at the top of the tree.
 *
 * @param {Array.<string>} prefixes - The names of the optional operators.
 * @param {Numbas.jme.tree} ruleTree
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchOptionalPrefix(prefixes, ruleTree, exprTree, options) {
    var originalExpr = exprTree;
    exprTree = extractLeadingMinus(exprTree);
    for(var i = 0;i < prefixes.length;i++) {
        var prefix = prefixes[i];
        if(jme.isOp(exprTree.tok, prefix)) {
            exprTree = exprTree.args[0];
            break;
        }
    }
    var m = matchTree(ruleTree, exprTree, options);
    if(m) {
        m._match = originalExpr;
        return m;
    } else {
        return false;
    }
}

/** Match if the expression doesn't match the given pattern.
 *
 * @param {Numbas.jme.tree} ruleTree - The pattern which must not be matched.
 * @param {Numbas.jme.tree} exprTree - The expression to test.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchNot(ruleTree, exprTree, options) {
    if(!matchTree(ruleTree, exprTree, options)) {
        return preserve_match({}, exprTree);
    } else {
        return false;
    }
}

/** Match if the expression uses all of the given names as free variables.
 *
 * @param {Array.<string>} names
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchUses(names, exprTree, options) {
    var vars = jme.findvars(exprTree, [], options.scope);
    for(var i = 0;i < names.length;i++) {
        if(!vars.contains(names[i])) {
            return false;
        }
    }
    return {};
}

/** Match if the top token of the given expression is of the given type.
 *
 * @param {string} wantedType - The required type.
 * @param {Numbas.jme.tree} exprTree
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchType(wantedType, exprTree) {
    if(exprTree.tok.type == wantedType) {
        return {};
    } else {
        return false;
    }
}

/** Match all of the given patterns against the given expression.
 * Return `false` if any of the patterns don't match.
 *
 * @param {Array.<Numbas.jme.tree>} patterns
 * @param {Numbas.jme.tree} exprTree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match}
 */
function matchAnd(patterns, exprTree, options) {
    var matches = [];
    for(var i = 0;i < patterns.length;i++) {
        var m = matchTree(patterns[i], exprTree, options);
        if(m) {
            matches.push(m);
        } else {
            return false;
        }
    }
    return mergeMatches(matches);
}

/** Find all matches for the rule, anywhere within the given expression.
 *
 * @memberof Numbas.jme.rules
 * @function
 * @param {Numbas.jme.tree} ruleTree - The pattern to match.
 * @param {Numbas.jme.tree} exprTree - The syntax tree to test.
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {Array.<Numbas.jme.rules.jme_pattern_match>}
 */
var matchAllTree = jme.rules.matchAllTree = function(ruleTree, exprTree, options) {
    var matches = [];
    var m = matchTree(ruleTree, exprTree, options);
    if(m) {
        matches = [m];
    }
    if(exprTree.args) {
        exprTree.args.forEach(function(arg) {
            var submatches = matchAllTree(ruleTree, arg, options);
            matches = matches.concat(submatches);
        });
    }
    return matches;
}

/** Merge a list of matches into one match object.
 * Later matches override earlier ones: if two matches have the same captured name, the later one is used.
 *
 * @param {Array.<Numbas.jme.rules.jme_pattern_match>} matches
 * @returns {Numbas.jme.rules.jme_pattern_match}
 */
function mergeMatches(matches) {
    var ms = matches.slice();
    ms.splice(0, 0, {});
    return util.extend_object.apply(this, ms);
}

/** Apply operations specified in the result of a tree transformation: `eval(x)` is replaced with the result of evaluating `x`.
 *
 * @memberof Numbas.jme.rules
 * @function
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.rules.matchTree_options} options
 * @returns {Numbas.jme.tree}
 */
var applyPostReplacement = jme.rules.applyPostReplacement = function(tree, options) {
    var tok = tree.tok;
    if(tree.args) {
        var args = tree.args.map(function(arg) {
            return applyPostReplacement(arg, options);
        });
        tree = {tok:tok, args: args};
    }
    if(jme.isFunction(tok, 'eval')) {
        return {tok: jme.evaluate(tree.args[0], options.scope)};
    } else if(jme.isFunction(tok, 'm_listval')) {
        var n = tree.args[1].tok.value;
        return tree.args[0].args[n];
    } else if(tok.type == 'op') {
        var filled_args = tree.args.filter(function(a) {
            return a.tok.type != 'nothing';
        });
        if(filled_args.length == 1 && filled_args.length < tree.args.length) {
            return filled_args[0];
        }
    }

    return tree;
}

/** Object returned by {@link Numbas.jme.rules.transform}.
 *
 * @type {object}
 * @typedef Numbas.jme.rules.transform_result
 * @property {boolean} changed - Is the result expression different to the input expression?
 * @property {Numbas.jme.tree} expression - The result expression.
 */

/** Replace one expression with another, if it matches the given rule.
 *
 * @memberof Numbas.jme.rules
 * @function
 * @param {Numbas.jme.tree} ruleTree - The rule to test against.
 * @param {Numbas.jme.tree} resultTree - The tree to output, with named groups from the rule substituted in.
 * @param {Numbas.jme.tree} exprTree - The expression to be tested.
 * @param {Numbas.jme.rules.matchTree_options} options - Options for the match.
 * @returns {Numbas.jme.rules.transform_result}
 */
var transform = jme.rules.transform = function(ruleTree, resultTree, exprTree, options) {
    var match = matchTree(ruleTree, exprTree, options);
    if(!match) {
        return {expression: exprTree, changed: false};
    }
    var names = findCapturedNames(ruleTree);
    names.forEach(function(name) {
        if(!(name in match)) {
            match[name] = {tok: new jme.types.TNothing()};
        }
    });

    var out = jme.substituteTree(resultTree, new jme.Scope([{variables: match}]), true);
    out = applyPostReplacement(out, options);
    if(match._rest_start) {
        out = {tok: new jme.types.TOp(match.__op__), args: [match._rest_start, out]};
    }
    if(match._rest_end) {
        out = {tok: new jme.types.TOp(match.__op__), args: [out, match._rest_end]};
    }
    return {expression: out, changed: !jme.treesSame(exprTree, out, options.scope)};
}

/** Replace anything matching the rule with the given result, at any position in the given expression.
 *
 * @memberof Numbas.jme.rules
 * @function
 * @param {Numbas.jme.tree} ruleTree - The rule to test against.
 * @param {Numbas.jme.tree} resultTree - The tree to output, with named groups from the rule substituted in.
 * @param {Numbas.jme.tree} exprTree - The expression to be tested.
 * @param {Numbas.jme.rules.matchTree_options} options - Options for the match.
 * @returns {Numbas.jme.rules.transform_result}
 */
var transformAll = jme.rules.transformAll = function(ruleTree, resultTree, exprTree, options) {
    var changed = false;
    if(exprTree.args) {
        var args = exprTree.args.map(function(arg) {
            var o = transformAll(ruleTree, resultTree, arg, options);
            changed = changed || o.changed;
            return o.expression;
        });
        exprTree = {tok: exprTree.tok, args: args};
    }

    var o = transform(ruleTree, resultTree, exprTree, options);
    changed = changed || o.changed;
    return {expression: o.expression, changed: changed};
}

/** A parser for JME patterns. Adds pattern-matching operators to the standard parser.
 *
 * @memberof Numbas.jme.rules
 */
var patternParser = jme.rules.patternParser = new jme.Parser();
patternParser.addTokenType(
    /^\$[a-zA-Z_]+/,
    function(result, tokens, expr, pos) {
        var name = result[0];
        var token;
        var lname = jme.normaliseName(name, this.options);
        token = new jme.types.TName(lname);
        return {tokens: [token], start: pos, end: pos + result[0].length};
    }
);
patternParser.addPostfixOperator('`?', '`?', {precedence: 0.5});  // optional
patternParser.addPostfixOperator('`*', '`*', {precedence: 0.5}); // any number of times
patternParser.addPostfixOperator('`+', '`+', {precedence: 0.5}); // at least one time

patternParser.addPrefixOperator('`!', '`!', {precedence: 0.5});  // not
patternParser.addPrefixOperator('`+-', '`+-', {precedence: 0.5});  // unary plus or minus
patternParser.addPrefixOperator('`*/', '`*/', {precedence: 0.5});  // unary multiply or divide

patternParser.addBinaryOperator(';', {precedence: 0.5});
patternParser.addBinaryOperator(';=', {precedence: 0.5});
patternParser.addBinaryOperator('`|', {precedence: 1000000});   // or
patternParser.addBinaryOperator('`:', {precedence: 1000000});   // default value
patternParser.addBinaryOperator('`&', {precedence: 100000});     // and
patternParser.addBinaryOperator('`where', {precedence: 1000000});   // condition
patternParser.addBinaryOperator('`@', {precedence: 1000000, rightAssociative: true});   // macro


/** Match expression against a pattern. Wrapper for {@link Numbas.jme.rules.matchTree}.
 *
 * @memberof Numbas.jme.rules
 * @function
 *
 * @param {JME} pattern
 * @param {JME} expr
 * @param {Numbas.jme.rules.matchTree_options} options - Default is `commutative`, `associative`, and `allowOtherTerms` all `true`, and using {@link Numbas.jme.builtinScope}.
 *
 * @returns {boolean|Numbas.jme.rules.jme_pattern_match} - `false` if no match, otherwise a dictionary of subtrees matched to variable names.
 */
jme.rules.matchExpression = function(pattern, expr, options) {
    var default_options = {
        commutative: true,
        associative: true,
        allowOtherTerms: true,
        gatherList: false,
        strictInverse: false,
        scope: Numbas.jme.builtinScope
    };
    options = extend_options(default_options, options);
    pattern = patternParser.compile(pattern);
    expr = jme.compile(expr);
    return matchTree(pattern, expr, options);
}
/** Flags used to control the behaviour of JME display functions.
 * Values are `undefined` so they can be overridden.
 *
 * @memberof Numbas.jme.rules
 */
var displayFlags = jme.rules.displayFlags = {
    fractionnumbers: undefined,
    rowvector: undefined,
    alwaystimes: undefined,
    mixedfractions: undefined,
    flatfractions: undefined,
    barematrices: undefined,
    timesdot: undefined,
    timesspace: undefined,
    noscientificnumbers: undefined
};
/** Flags used in JME simplification rulesets
 *
 * @type {Object<boolean>}
 * @typedef Numbas.jme.rules.ruleset_flags
 * @property {boolean} fractionnumbers - Show all numbers as fractions?
 * @property {boolean} rowvector - Display vectors as a horizontal list of components?
 * @property {boolean} alwaystimes - Always show the multiplication symbol between multiplicands?
 * @property {boolean} mixedfractions - Show top-heavy fractions as mixed fractions, e.g. 3 3/4?
 * @property {boolean} flatfractions - Display fractions horizontally?
 * @property {boolean} barematrices - Render matrices without wrapping them in parentheses.
 * @property {boolean} timesdot - Use a dot for the multiplication symbol instead of a cross?
 * @property {boolean} timesspace - Use a space for the multiplication symbol instead of a cross?
 * @property {boolean} noscientificnumbers - Numbers are never rendered in scientific notation.
 * @see Numbas.jme.rules.Ruleset
 */
/** Set of simplification rules.
 *
 * @class
 * @memberof Numbas.jme.rules
 * @param {Numbas.jme.rules.Rule[]} rules
 * @param {Numbas.jme.rules.ruleset_flags} flags
 */
var Ruleset = jme.rules.Ruleset = function(rules, flags) {
    this.rules = rules;
    this.flags = util.extend_object({}, displayFlags, flags);
}

Ruleset.prototype = /** @lends Numbas.jme.rules.Ruleset.prototype */ {
    /** Test whether flag is set.
     *
     * @param {string} flag
     * @returns {boolean}
     */
    flagSet: function(flag) {
        flag = jme.normaliseRulesetName(flag);
        if(Object.hasOwn(this.flags, flag)) {
            return this.flags[flag];
        } else {
            return false;
        }
    },

    /** Apply this set's rules to the given expression until they don't change any more.
     *
     * @param {Numbas.jme.tree} exprTree
     * @param {Numbas.jme.Scope} scope
     * @see Numbas.jme.rules.transform
     * @see Numbas.jme.rules.matchTree
     * @returns {Numbas.jme.tree}
     */
    simplify: function(exprTree, scope) {
        var rs = this;
        var changed = true;
        var depth = 0;
        var seen = [];
        while(changed) {
            if(exprTree.args) {
                var nargs = exprTree.args.map(function(arg) {
                    return rs.simplify(arg, scope);
                });
                exprTree = {tok: exprTree.tok, args: nargs};
            }
            changed = false;
            for(var i = 0;i < this.rules.length;i++) {
                var result = this.rules[i].replace(exprTree, scope);
                if(result.changed) {
                    if(depth > 100) {
                        var str = Numbas.jme.display.treeToJME(exprTree);
                        if(seen.indexOf(str) != -1) {
                            throw(new Numbas.Error("jme.display.simplifyTree.stuck in a loop", {expr:str}));
                        }
                        seen.push(str);
                    }
                    changed = true;
                    exprTree = result.expression;
                    depth += 1;
                    break;
                }
            }
        }
        return exprTree;
    }
}

/** Collect a ruleset together from a list of ruleset names, or rulesets.
 *
 * @memberof Numbas.jme.rules
 * @function
 * @param {string|Array.<string|Numbas.jme.rules.Ruleset>} set - A comma-separated string of ruleset names, or an array of names/Ruleset objects.
 * @param {Object<Numbas.jme.rules.Ruleset>} scopeSets - Dictionary of rulesets defined in the current scope.
 * @returns {Numbas.jme.rules.Ruleset}
 */
var collectRuleset = jme.rules.collectRuleset = function(set, scopeSets) {
    scopeSets = util.copyobj(scopeSets);
    if(!set) {
        return new Ruleset([], {});
    }
    if(!scopeSets) {
        throw(new Numbas.Error('jme.display.collectRuleset.no sets'));
    }

    var rules = [];
    var flags = {};
    if(typeof(set) == 'string') {
        set = set.split(',');
        set.splice(0, 0, 'basic');
    } else {
        flags = util.extend_object(flags, set.flags);
        if(set.rules) {
            set = set.rules;
        }
    }
    for(var i = 0; i < set.length; i++) {
        if(typeof(set[i]) == 'string') {
            var m = /^\s*(!)?(.*)\s*$/.exec(set[i]);
            var neg = m[1] == '!' ? true : false;
            var name = jme.normaliseRulesetName(m[2].trim());
            if(name in displayFlags) {
                flags[name] = !neg;
            } else if(name.length > 0) {
                if(!(name in scopeSets)) {
                    throw(new Numbas.Error('jme.display.collectRuleset.set not defined', {name:name}));
                }
                var sub = collectRuleset(scopeSets[name], scopeSets);
                flags = util.extend_object(flags, sub.flags);
                scopeSets[name] = sub;
                sub.rules.forEach(function(r) {
                    var m = rules.indexOf(r);
                    if(neg) {
                        if(m >= 0) {
                            rules.splice(m, 1);
                        }
                    } else {
                        if(m == -1) {
                            rules.push(r);
                        }
                    }
                });
            }
        } else {
            rules.push(set[i]);
        }
    }
    return new Ruleset(rules, flags);
}
/** Built-in simplification rules.
 *
 * @enum {Numbas.jme.rules.Rule[]}
 * @memberof Numbas.jme.rules
 */
var simplificationRules = jme.rules.simplificationRules = {
    basic: [
        ['negative:$n;x', '', '-eval(-x)'],       // The value of a number token should be non-negative - pull the negation out as unary minus
        ['+(?;x)', 's', 'x'],                     // Get rid of unary plus
        ['?;x+(-?;y)', 'ags', 'x-y'],             // Plus minus = minus
        ['?;x-(-?;y)', 'ags', 'x+y'],             // Minus minus = plus
        ['-(-?;x)', 's', 'x'],                    // Unary minus minus = plus
        ['(-?;x)/?;y', 's', '-(x/y)'],            // Take negation to the left of a fraction
        ['?;x/(-?;y)', 's', '-(x/y)'],
        ['-(`! complex:$n);x * (-?;y)', 'asg', 'x*y'], // Cancel the product of two negated things that aren't complex numbers
        ['`!-? `& (-(real:$n/real:$n`? `| imaginary:$n `| `!$n);x) * ?`+;y', 'sgc', '-(x*y)'],            // Take negation to the left of multiplication
        ['imaginary:$n;z * ?;y `where im(z)<0', 'acsg', '-(eval(-z)*y)'], // Pull negation out of products involving negative imaginary numbers
        ['-(?;a+?`+;b)', '', '-a-b'],             // Expand negated brackets
        ['?;a+(-?;b-?;c)', '', 'a-b-c'],          // Remove brackets involving subtraction
        ['?;a+(-?;b+?;c)', '', 'a-b+c'],          // Remove brackets involving subtraction
        ['?;a/?;b/?;c', '', 'a/(b*c)']            // Prefer a product on the denominator to a string of divisions
    ],
    collectComplex: [
        ['-complex:negative:$n;x', '', 'eval(-x)'],   // Cancel negation of a complex number with negative real part
        ['(`+- real:$n);x + (`+- imaginary:$n);y', 'cg', 'eval(x+y)'],    // Collect the two parts of a complex number
        ['$n;n*i', 'acsg', 'eval(n*i)'],            // Always collect multiplication by i
    ],
    unitFactor: [
        ['1*(`! (/?));x', 'acgs', 'x'],
    ],
    unitPower: [
        ['?;x^1', '', 'x']
    ],
    unitDenominator: [
        ['?;x/1', '', 'x']
    ],
    zeroFactor: [
        ['?;x*0', 'acg', '0'],
        ['0/?;x', '', '0']
    ],
    zeroTerm: [
        ['(`+-0) + (`+- ?);x', 'acg', 'x']
    ],
    zeroPower: [
        ['?;x^0', '', '1']
    ],
    powerPower: [
        ['(?;x^$n;a)^$n;b `where abs(a*b)<infinity', '', 'x^eval(a*b)']
    ],
    noLeadingMinus: [
        ['-?;x + ?;y', 's', 'y-x'],   // Don't start with a unary minus
        ['-0', '', '0']               // Cancel negative 0
    ],
    collectNumbers: [
        ['$n;a * (1/?;b)', 'ags', 'a/b'],
        ['(`+- $n);n1 + (`+- $n)`+;n2 `where abs(n1+n2)<infinity', 'acg', 'eval(n1+n2)'],                // Addition of two numbers
        ['$n;n * $n;m `where abs(n*m)<infinity', 'acg', 'eval(n*m)'],                                  // Product of two numbers
        ['(`! $n)`+;x * real:$n;n * ((`! $n )`* `| $z);y', 'ags', 'n*x*y']    // Shift numbers to left hand side of multiplication
    ],
    simplifyFractions: [
        ['($n;n * (?`* `: 1);top) / ($n;m * (?`* `: 1);bottom) `where gcd_without_pi_or_i(n,m)>1', 'acg', '(eval(n/gcd_without_pi_or_i(n,m))*top)/(eval(m/gcd_without_pi_or_i(n,m))*bottom)'],    // Cancel common factors of integers on top and bottom of a fraction
        ['imaginary:$n;n / imaginary:$n;m', '', 'eval(n/i)/eval(m/i)'],            // Cancel i when numerator and denominator are both purely imaginary
        ['?;=a / ?;=a', 'acg', '1'],              // Cancel fractions equal to 1
        ['?;a / (?;b/?;c * ?`*;rest)', 'acg', '(a*c)/(b * rest)']     // Un-nest nested fractions
    ],
    zeroBase: [
        ['0^?;x', '', '0']
    ],
    constantsFirst: [
        ['(`! `+- $n);x * (real:$n/real:$n`?);n', 'asg', 'n*x']
    ],
    sqrtProduct: [
        ['sqrt(?;x)*sqrt(?;y)', 'agc', 'sqrt(x*y)']
    ],
    sqrtDivision: [
        ['sqrt(?;x)/sqrt(?;y)', 'agc', 'sqrt(x/y)']
    ],
    sqrtSquare: [
        ['sqrt(?;x^2)', '', 'x'],
        ['sqrt(?;x)^2', '', 'x'],
        ['sqrt(integer:$n;n) `where isint(sqrt(n))', '', 'eval(sqrt(n))'] // Cancel square root of a square integer
    ],
    trig: [
        ['sin($n;n) `where isint(2*n/pi)', '', 'eval(sin(n))'],   // Evaluate sin on multiples of pi/2
        ['cos($n;n) `where isint(2*n/pi)', '', 'eval(cos(n))'],   // Evaluate cos on multiples of pi/2
        ['tan($n;n) `where isint(n/pi)', '', '0'],                // Evaluate tan on multiples of pi
        ['cosh(0)', '', '1'],
        ['sinh(0)', '', '0'],
        ['tanh(0)', '', '0']
    ],
    otherNumbers: [
        ['(`+-$n);n ^ $n;m `where abs(n^m)<infinity', '', 'eval(n^m)']
    ],
    cancelTerms: [
        ['["term": `!$n] `@ (m_exactly((`+- $n `: 1);n * (?`+ `& `! -? `& term);=x `| -term;=x;n:-1) + m_exactly((`+- $n `: 1);m * (?`+ `& `! -? `& term);=x `| -term;=x;m:-1))', 'acg', 'eval(n+m)*x']
    ],
    cancelFactors: [
        ['?;=x^(? `: 1);n * ?;=x^(? `: 1);m', 'acg', 'x^(m+n)'],
        ['?;=x^(? `: 1);n / ?;=x^(? `: 1);m', 'acg', 'x^(n-m)']
    ],
    collectLikeFractions: [
        ['(?`+);a/?;=d + `+- (?`+);b/?;=d', 'acg', '(a+b)/d']
    ]
    /*
        // x/y or rest*x/y
        ['(?;rest*(?;x)^(?;n)) / ((?;y)^(?;m))',['n isa "number"','m isa "number"','canonical_compare(x,y)=0'],'rest*x^eval(n-m)'],
        ['(?;rest*(?;x)^(?;n)) / ?;y',['n isa "number"','canonical_compare(x,y)=0'],'rest*x^eval(n-1)'],
        ['(?;rest*?;x) / ((?;y)^(?;n))',['n isa "number"','canonical_compare(x,y)=0'],'rest*x^eval(1-n)'],
        ['(?;rest*?;x) / ?;y',['canonical_compare(x,y)=0'],'rest*x^0'],
        ['(?;x)^(?;n) / (?;y)^(?;m)',['n isa "number"','m isa "number"','canonical_compare(x,y)=0'],'x^eval(n-m)'],
        ['(?;x)^(?;n) / ?;y',['n isa "number"','canonical_compare(x,y)=0'],'x^eval(n-1)'],
        ['?;x / ((?;y)^(?;n))',['n isa "number"','canonical_compare(x,y)=0'],'x^eval(1-n)'],
        ['?;x / ?;y',['canonical_compare(x,y)=0'],'x^0'],
        // rest/x/y or rest/x*y
        ['(?;rest/((?;x)^(?;n))) * (?;y)^(?;m)',['n isa "number"','m isa "number"','canonical_compare(x,y)=0'],'rest*x^eval(m-n)'],
        ['(?;rest/((?;x)^(?;n))) * ?;y',['n isa "number"','canonical_compare(x,y)=0'],'rest*x^eval(1-n)'],
        ['(?;rest/?;x) * (?;y)^(?;n)',['n isa "number"','canonical_compare(x,y)=0'],'rest*x^eval(1-n)'],
        ['(?;rest/((?;x)^(?;n))) / ((?;y)^(?;m))',['n isa "number"','m isa "number"','canonical_compare(x,y)=0'],'rest/(x^eval(n+m))'],
        ['(?;rest/((?;x)^(?;n))) / ?;y',['n isa "number"','canonical_compare(x,y)=0'],'rest/(x^eval(n+1))'],
        ['(?;rest/?;x) / ((?;y)^(?;n))',['n isa "number"','canonical_compare(x,y)=0'],'rest/(x^eval(1+n))'],
        ['(?;rest/?;x) / ?;y',['canonical_compare(x,y)=0'],'rest/(x^2)'],
        ['(?;rest/?;x) * ?;y',['canonical_compare(x,y)=0'],'rest/(x^0)']
    ],
    */
};
/**
 * Sets of rules that conflict with some of the rules in `simplificationRules`, so can't be enabled at the same time.
 * Or, sets of rules that shouldn't always be turned on.
 */
var conflictingSimplificationRules = {
    // these rules conflict with noLeadingMinus
    canonicalOrder: [
        ['(`+- ?);x+(`+- ?);y `where canonical_compare(x,y)=1', 'ag', 'y+x'],
        ['?;x*?;y `where canonical_compare(x,y)=-1', 'ag', 'y*x'],
    ],
    expandBrackets: [
        ['(?;x + ((`+- ?)`+);y) * ?;z', 'ag', 'x*z+y*z'],
        ['?;x * (?;y + ((`+- ?)`+);z)', 'ag', 'x*y+x*z']
    ],
    noDivision: [
        ['?;top/(?;base^(?`? `: 1);degree)', '', 'top * base^(-degree)']
    ],
    rationalDenominators: [
        ['?;a/(sqrt(?;surd)*?`*;rest)', 'acg', '(a*sqrt(surd))/(surd*rest)'],
    ],
    reduceSurds: [
        ['sqrt((`+-$n);n * (?`* `: 1);rest) `where abs(largest_square_factor(n))>1', 'acg', 'eval(sqrt(abs(largest_square_factor(n))))*sqrt(eval(n/abs(largest_square_factor(n))) * rest)'],
        ['sqrt((?;a)^(`+-$n;n) * (?`* `: 1);rest) `where abs(n)>1', 'acg', 'a^eval(trunc(n/2)) * sqrt(a^eval(mod(n,2))*rest)']
    ]
}
/** Compile an array of rules (in the form `[pattern,conditions[],result]` to {@link Numbas.jme.rules.Rule} objects.
 *
 * @memberof Numbas.jme.rules
 * @function
 * @param {Array} rules
 * @param {string} name - a name for this group of rules
 * @returns {Numbas.jme.rules.Ruleset}
 */
var compileRules = jme.rules.compileRules = function(rules, name) {
    for(var i = 0;i < rules.length;i++) {
        var pattern = rules[i][0];
        var options = rules[i][1];
        var result = rules[i][2];
        rules[i] = new Rule(pattern, result, options, name);
    }
    return new Ruleset(rules, {});
}
var all = [];
var compiledSimplificationRules = {};
var subscope = new jme.Scope();
subscope.setConstant('i', {value: new jme.types.TNum(math.complex(0, 1))});
subscope.setConstant('pi', {value: new jme.types.TNum(Math.PI)});
for(const [name, rule] of Object.entries(simplificationRules)) {
    compiledSimplificationRules[name] = compiledSimplificationRules[jme.normaliseRulesetName(name)] = compileRules(rule, name);
    all = all.concat(compiledSimplificationRules[name].rules);
}
for(const [name, rule] of Object.entries(conflictingSimplificationRules)) {
    compiledSimplificationRules[name] = compiledSimplificationRules[jme.normaliseRulesetName(name)] = compileRules(rule, name);
}
Object.values(compiledSimplificationRules).forEach(function(set) {
    set.rules.forEach(function(rule) {
        rule.pattern = jme.substituteTree(rule.pattern, subscope, true);
        rule.result = jme.substituteTree(rule.result, subscope, true);
    })
});
compiledSimplificationRules['all'] = new Ruleset(all, {});
jme.rules.simplificationRules = compiledSimplificationRules;
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Stuff to do with making new functions from JME or JavaScript code,
 * generating question variables,
 * and substituting variables into maths or the DOM.
 *
 * Provides {@link Numbas.jme.variables}
 */
Numbas.queueScript('jme-variables', ['base', 'jme-base', 'util'], function() {
var jme = Numbas.jme;
var util = Numbas.util;
/** @namespace Numbas.jme.variables */

/** A dictionary describing a variable to be evaluated.
 *
 * @typedef {object} Numbas.jme.variables.variable_data_dict
 * @property {Numbas.jme.tree} tree - Definition of the variable.
 * @property {string[]} vars - Names of variables this variable depends on.
 */

/** The definition of a custom JME function.
 *
 * @typedef Numbas.jme.variables.func_data
 * @type {object}
 * @property {string} name - The function's name.
 * @property {string} definition - Definition of the function, either in {@link JME} or JavaScript.
 * @property {string} language - Either `"jme"` or `"javascript"`.
 * @property {string} outtype - Name of the {@link Numbas.jme.token} type this function returns.
 * @property {Array.<object>} parameters - Definition of the function's calling signature: an array of objects with properties `name` and `type` for each of the function's parameters.
 */

jme.variables = /** @lends Numbas.jme.variables */ {
    /** Make a new function, whose definition is written in JME.
     *
     * @param {object} fn - Contains `definition` and `paramNames`.
     * @param {Numbas.jme.Scope} scope
     * @returns {Function} - Function which evaluates arguments and adds them to the scope, then evaluates `fn.definition` over that scope.
     */
    makeJMEFunction: function(fn, scope) {
        fn.tree = jme.compile(fn.definition, scope, true);
        const nscope = new jme.Scope([scope]);
        nscope.addFunction(fn);
        var finding = false;
        jme.findvarsOps[fn.name] = function(tree, boundvars, scope) {
            var vars = [];
            if(!finding) {
                finding = true;
                vars = jme.findvars(fn.tree, fn.paramNames.map(function(v) {
                    return jme.normaliseName(v, scope)
                }), scope);
                finding = false;
            }
            for(let i = 0;i < tree.args.length;i++) {
                vars = vars.merge(jme.findvars(tree.args[i], boundvars, scope));
            }
            return vars;
        }

        return function(args, scope) {
            scope = new jme.Scope(scope);
            for(let j = 0;j < args.length;j++) {
                scope.setVariable(fn.paramNames[j], args[j]);
            }
            return jme.evaluate(this.tree, scope);
        }
    },
    /** Make a new function, whose definition is written in JavaScript.
     *
     * The JavaScript is wrapped with `(function(<paramNames>){ ` and ` }`).
     *
     * @param {object} fn - Contains `definition` and `paramNames`.
     * @param {object} withEnv - Dictionary of local variables for javascript functions.
     * @returns {Function} - Function which evaluates arguments, unwraps them to JavaScript values, then evalutes the JavaScript function and returns the result, wrapped as a {@link Numbas.jme.token}.
     */
    makeJavascriptFunction: function(fn, withEnv) {
        var paramNames = fn.paramNames.slice();
        paramNames.push('scope');
        withEnv = withEnv || {};
        var env_args = Object.entries(withEnv).map(([name, v]) => {
            paramNames.push(name);
            return v;
        });
        delete jme.findvarsOps[fn.name];
        try {
            var jfn = new Function(paramNames, fn.definition);
        } catch {
            throw(new Numbas.Error('jme.variables.syntax error in function definition'));
        }
        return function(args, scope) {
            if(fn.definition.match(/variables/)) {
                // backwards-compatibility hack for functions that try to access scope.variables.varname
                // instead of scope.getVariable(varname)
                scope = new Numbas.jme.Scope([scope]);
                scope.flatten();
            }
            args = args.map(function(a) {
                return jme.unwrapValue(a)
            });
            args.push(scope);
            args = args.concat(env_args);
            try {
                var val = jfn.apply(this, args);
                if(val === undefined) {
                    throw(new Numbas.Error('jme.user javascript.returned undefined', {name:fn.name}));
                }
                val = jme.wrapValue(val, fn.outtype);
                if(!val.type) {
                    val = new fn.outcons(val);
                }
                return val;
            } catch(e) {
                throw(new Numbas.Error('jme.user javascript.error', {name:fn.name, message:e.message}));
            }
        }
    },
    /** Make a custom function.
     *
     * @param {Numbas.jme.variables.func_data} def - Contains `definition`, `name`, `language`, `parameters`.
     * @param {Numbas.jme.Scope} scope
     * @param {object} withEnv - Dictionary of local variables for javascript functions.
     * @returns {Numbas.jme.funcObj}
     */
    makeFunction: function(def, scope, withEnv) {
        var intype = [],
            paramNames = [];
        def.parameters.map(function(p) {
            intype.push(p.type);
            paramNames.push(p.name);
        });
        var outcons = jme.types[def.outtype];
        var fn = new jme.funcObj(def.name, intype, outcons, null, true);
        fn.paramNames = paramNames;
        fn.definition = def.definition;
        fn.name = jme.normaliseName(def.name, scope);
        fn.language = def.language;
        try {
            switch(fn.language) {
            case 'jme':
                fn.evaluate = jme.variables.makeJMEFunction(fn, scope);
                break;
            case 'javascript':
                fn.evaluate = jme.variables.makeJavascriptFunction(fn, withEnv);
                break;
            default:
                throw(new Numbas.Error('jme.variables.invalid function language', {language: fn.language}));
            }
        } catch(e) {
            throw(new Numbas.Error('jme.variables.error making function', {name:fn.name, message:e.message}));
        }
        return fn
    },
    /** Make up custom functions.
     *
     * @param {Numbas.jme.variables.func_data[]} tmpFunctions
     * @param {Numbas.jme.Scope} scope
     * @param {object} withEnv - Dictionary of local variables for javascript functions.
     * @returns {Object<Numbas.jme.funcObj>}
     * @see Numbas.jme.variables.makeFunction
     */
    makeFunctions: function(tmpFunctions, scope, withEnv) {
        scope = new jme.Scope(scope);

        tmpFunctions.forEach(function(def) {
            var cfn = jme.variables.makeFunction(def, scope, withEnv);
            scope.addFunction(cfn);
        });

        return scope.functions;
    },
    /** Evaluate a variable, evaluating all its dependencies first.
     *
     * @param {string} name - The name of the variable to evaluate.
     * @param {Numbas.jme.variables.variable_data_dict} todo - Dictionary of variables still to evaluate.
     * @param {Numbas.jme.Scope} scope
     * @param {string[]} path - Breadcrumbs - Variable names currently being evaluated, so we can detect circular dependencies.
     * @param {Function} [computeFn=Numbas.jme.variables.computeVariable] - A function to call when a dependency needs to be computed.
     * @returns {Numbas.jme.token}
     */
    computeVariable: function(name, todo, scope, path, computeFn) {
        var originalName = (todo[name] && todo[name].originalName) || name;
        var existing_value = scope.getVariable(name);
        if(existing_value !== undefined) {
            return existing_value;
        }
        if(path === undefined) {
            path = [];
        }
        computeFn = computeFn || jme.variables.computeVariable;
        if(name == '') {
            throw(new Numbas.Error('jme.variables.empty name'));
        }
        if(path.contains(name)) {
            throw(new Numbas.Error('jme.variables.circular reference', {name:name, path:path}));
        }
        var v = todo[name];
        if(v === undefined) {
            var c = scope.getConstant(name);
            if(c) {
                return c.value;
            }
            throw(new Numbas.Error('jme.variables.variable not defined', {name:name}));
        }
        //work out dependencies
        for(let i = 0;i < v.vars.length;i++) {
            var x = v.vars[i];
            if(scope.variables[x] === undefined) {
                var newpath = path.slice(0);
                newpath.splice(0, 0, name);
                try {
                    computeFn(x, todo, scope, newpath, computeFn);
                } catch(e) {
                    if(e.originalMessage == 'jme.variables.circular reference' || e.originalMessage == 'jme.variables.variable not defined') {
                        throw(e);
                    } else {
                        throw(new Numbas.Error('jme.variables.error computing dependency', {name:x, message: e.message}, e));
                    }
                }
            }
        }
        if(!v.tree) {
            throw(new Numbas.Error('jme.variables.empty definition', {name: originalName}));
        }
        try {
            var value = jme.evaluate(v.tree, scope);
            if(v.names) {
                value = jme.castToType(value, 'list');
            }
            scope.setVariable(name, value);
        } catch(e) {
            throw(new Numbas.Error('jme.variables.error evaluating variable', {name:originalName, message:e.message}, e));
        }
        return value;
    },

    /** Split up a list of variable names separated by commas, for destructuring assignment.
     *
     * @param {string} s
     * @returns {Array.<string>}
     */
    splitVariableNames: function(s) {
        return s.split(/\s*,\s*/).filter(function(n) {
            return n.trim();
        })
    },
    /**
     * Evaluate dictionary of variables.
     *
     * @param {Numbas.jme.variables.variable_data_dict} todo - Dictionary of variables mapped to their definitions.
     * @param {Numbas.jme.Scope} scope
     * @param {Numbas.jme.tree} condition - Condition on the values of the variables which must be satisfied.
     * @param {Function} computeFn - A function to compute a variable. Default is Numbas.jme.variables.computeVariable.
     * @param {Array.<string>} targets - Variables which must be re-evaluated, even if they're already present in the scope.
     * @returns {object} - `variables`: a dictionary of evaluated variables, and `conditionSatisfied`: was the condition satisfied?
     */
    makeVariables: function(todo, scope, condition, computeFn, targets) {
        var multis = {};
        var multi_acc = 0;
        var ntodo = {};
        Object.keys(todo).forEach(function(name) {
            var names = jme.variables.splitVariableNames(name);
            if(names.length == 0) {
                return;
            }
            if(names.length > 1) {
                var mname;
                while(true) {
                    mname = '$multi_' + (multi_acc++);
                    if(todo[mname] === undefined) {
                        break;
                    }
                }
                multis[mname] = name;
                ntodo[mname] = todo[name];
                ntodo[mname].names = names;
                ntodo[mname].originalName = name;
                names.forEach(function(sname, i) {
                    ntodo[sname] = {
                        tree: jme.compile(mname + '[' + i + ']'),
                        vars: [mname]
                    }
                });
            } else {
                ntodo[name] = todo[name];
            }
        });
        todo = ntodo;
        computeFn = computeFn || jme.variables.computeVariable;
        var conditionSatisfied = true;
        if(condition) {
            var condition_vars = jme.findvars(condition, [], scope);
            condition_vars.map(function(v) {
                computeFn(v, todo, scope, undefined, computeFn);
            });
            conditionSatisfied = jme.evaluate(condition, scope).value;
        }
        if(conditionSatisfied) {
            if(!targets) {
                targets = Object.keys(todo);
            }
            targets.forEach(function(x) {
                computeFn(x, todo, scope, undefined, computeFn);
            });
        }
        var variables = scope.variables;
        Object.keys(multis).forEach(function(mname) {
            variables[multis[mname]] = variables[mname];
            delete variables[mname];
        });
        return {variables: variables, conditionSatisfied: conditionSatisfied, scope: scope};
    },

    /**
     * Remake a dictionary of variables, only re-evaluating variables which depend on the changed_variables.
     * A new scope is created with the values from `changed_variables`, and then the dependent variables are evaluated in that scope.
     *
     * @param {Numbas.jme.variables.variable_data_dict} todo - Dictionary of variables mapped to their definitions.
     * @param {Object<Numbas.jme.token>} changed_variables - Dictionary of changed variables. These will be added to the scope, and will not be re-evaluated.
     * @param {Numbas.jme.Scope} scope
     * @param {Function} [computeFn] - A function to compute a variable. Default is Numbas.jme.variables.computeVariable.
     * @param {Array.<string>} targets - Variables which must be re-evaluated, even if they're already present in the scope.
     * @returns {Numbas.jme.Scope}
     */
    remakeVariables: function(todo, changed_variables, scope, computeFn, targets) {
        var variables = {};
        Object.entries(changed_variables).forEach(([name, value]) => {
            var names = jme.variables.splitVariableNames(name);
            if(names.length == 1) {
                variables[name] = value;
            } else {
                value = jme.castToType(value, 'list');
                names.forEach(function(n, i) {
                    variables[n] = value.value[i];
                });
            }
        });
        scope = new Numbas.jme.Scope([scope, {variables: variables}]);
        var replaced = Object.keys(changed_variables);
        // find dependent variables which need to be recomputed
        var dependents_todo = jme.variables.variableDependants(todo, replaced, scope);
        for(const name of Object.keys(dependents_todo)) {
            if(name in variables) {
                delete dependents_todo[name];
            } else {
                var names = jme.variables.splitVariableNames(name);
                for(const sname of names) {
                    scope.deleteVariable(sname);
                }
            }
        }
        if(targets) {
            targets.forEach(function(name) {
                scope.deleteVariable(name);
            });
        }
        for(const name of Object.keys(todo)) {
            if(name in dependents_todo) {
                continue;
            }
            if(scope.getVariable(name) === undefined) {
                dependents_todo[name] = todo[name];
            }
        }
        // compute those variables
        var nv = jme.variables.makeVariables(dependents_todo, scope, null, computeFn, targets);
        scope = new Numbas.jme.Scope([scope, {variables:nv.variables}]);
        return scope;
    },

    /** Collect together a ruleset, evaluating all its dependencies first.
     *
     * @param {string} name - The name of the ruleset to evaluate.
     * @param {Object<string[]>} todo - Dictionary of rulesets still to evaluate.
     * @param {Numbas.jme.Scope} scope
     * @param {string[]} path - Breadcrumbs - Rulesets names currently being evaluated, so we can detect circular dependencies.
     * @returns {Numbas.jme.rules.Ruleset}
     */
    computeRuleset: function(name, todo, scope, path) {
        var existing_ruleset = scope.getRuleset(jme.normaliseName(name, scope));
        if(existing_ruleset) {
            return existing_ruleset;
        }
        if(jme.normaliseName(name, scope) in jme.displayFlags) {
            return undefined;
        }
        if(path.contains(name)) {
            throw(new Numbas.Error('ruleset.circular reference', {name:name}));
        }
        var newpath = path.slice();
        newpath.push(name);
        if(todo[name] === undefined) {
            throw(new Numbas.Error('ruleset.set not defined', {name:name}));
        }
        todo[name].forEach(function(name) {
            if(typeof(name) !== 'string') {
                return;
            }
            var m = /^\s*(!)?(.*)\s*$/.exec(name);
            var name2 = m[2].trim();
            jme.variables.computeRuleset(name2, todo, scope, newpath);
        });
        var ruleset = Numbas.jme.collectRuleset(todo[name], scope.allRulesets());
        scope.setRuleset(name, ruleset);
        return ruleset;
    },
    /** Gather together a set of ruleset definitions.
     *
     * @param {Object<string[]>} todo - A dictionary mapping ruleset names to definitions.
     * @param {Numbas.jme.Scope} scope - The scope to gather the rulesets in. The rulesets are added to this scope as a side-effect.
     * @returns {Object<Numbas.jme.rules.Ruleset>} A dictionary of rulesets.
     */
    makeRulesets: function(todo, scope) {
        var out = {};
        for(const name of Object.keys(todo)) {
            out[name] = jme.variables.computeRuleset(name, todo, scope, []);
        }
        return out;
    },

    /** Add a list of constants to the scope.
     *
     * @param {Array.<Numbas.jme.constant_definition>} definitions
     * @param {Numbas.jme.Scope} scope
     * @param {Object<boolean>} enabled - For each constant name, is it enabled? If not given, then the `enabled` value in the definition is used.
     * @returns {Array.<string>} - The names of constants added to the scope.
     */
    makeConstants: function(definitions, scope, enabled) {
        var defined_names = [];
        definitions.forEach(function(def) {
            var names = def.name.split(/\s*,\s*/);
            var value = def.value;
            if(typeof value != 'object') {
                value = scope.evaluate(value + '');
            }
            names.forEach(function(name) {
                var def_enabled = def.enabled === undefined || def.enabled;
                var q_enabled = enabled !== undefined && (enabled[name] || (enabled[name] === undefined && def_enabled));
                if(!(enabled === undefined ? def_enabled : q_enabled)) {
                    scope.deleteConstant(name);
                    return;
                }
                defined_names.push(jme.normaliseName(name, scope));
                scope.setConstant(name, {value:value, tex:def.tex});
            });
        });
        return defined_names
    },
    /** Given a todo dictionary of variables, return a dictionary with only the variables depending on the given list of variables.
     *
     * @param {object} todo - Dictionary of variables mapped to their definitions.
     * @param {string[]} ancestors - List of variable names whose dependants we should find.
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {object} - A copy of the todo list, only including the dependants of the given variables.
     */
    variableDependants: function(todo, ancestors, scope) {

        ancestors = ancestors.flatMap((name) => jme.variables.splitVariableNames(name));

        // a dictionary mapping variable names to lists of names of variables they depend on
        var dependants = {};
        /** Find the names of the variables this variable depends on.
         *
         * @param {string} name - The name of the variable to consider.
         * @param {Array.<string>} path - The chain of variables that have led to the one being considered, used to detect circular references.
         * @returns {Array.<string>} - The names of the variables this one depends on.
         */
        var multis = {};

        /** Recursively find the variables which depend on the given name.
         *
         * @param {string} name - The name of the variable whose dependants should be found.
         * @param {Array.<string>} path - The names already under consideration.
         * @returns {Array.<string>} - The names of dependent variables.
         */
        function findDependants(name, path) {
            path = path || [];
            // stop at circular references
            if(path.contains(name)) {
                return [];
            }
            // if we've already done this, variable, return it
            if(name in dependants) {
                return dependants[name];
            }
            if(name in multis) {
                return dependants[multis[name]];
            }

            var names = jme.variables.splitVariableNames(name);

            // for each variable used in this variable, find its dependants
            var d = [];
            if(name in todo) {
                var newpath = path.slice();
                newpath.push(name);
                todo[name].vars.map(function(name2) {
                    d = d.concat(name2, findDependants(name2, newpath));
                });
            }
            // make a new list with duplicates removed
            var o = [];
            d.map(function(name2) {
                if(!o.contains(name2)) {
                    o.push(name2);
                }
            });

            dependants[name] = o;
            names.forEach(function(n) {
                multis[n] = name;
            });

            return o;
        }
        for(const name of Object.keys(todo)) {
            findDependants(name);
        }
        var out = {};
        for(const name of Object.keys(dependants)) {
            for(let i = 0;i < ancestors.length;i++) {
                var ancestor = jme.normaliseName(ancestors[i], scope)
                if(dependants[name].contains(ancestor)) {
                    out[name] = todo[name];
                    break;
                }
            }
        }
        return out;
    },
    /** Substitute variables into a DOM element (works recursively on the element's children).
     *
     * Ignores iframes and elements with the attribute `nosubvars`.
     *
     * @param {Element} element
     * @param {Numbas.jme.Scope} scope
     * @see Numbas.jme.variables.DOMcontentsubber
     * @returns {Element}
     */
    DOMcontentsubvars: function(element, scope) {
        var subber = new DOMcontentsubber(scope);
        return subber.subvars(element);
    },
    /** Substitute variables into the contents of a text node. Substituted values might contain HTML elements, so the return value is a collection of DOM elements, not another string.
     *
     * @param {string} str - The contents of the text node.
     * @param {Numbas.jme.Scope} scope
     * @param {Document} doc - The document the text node belongs to.
     * @returns {Array.<Array.<Node>>} - Array of DOM nodes to replace the string with.
     */
    DOMsubvars: function(str, scope, doc) {
        doc = doc || document;
        var bits = util.splitbrackets(str, '{', '}', '(', ')');
        if(bits.length == 1) {
            return [[doc.createTextNode(str)]];
        }
        /** Get HTML content for a given JME token.
         *
         * @param {Numbas.jme.token} token
         * @returns {Element|string}
         */
        function doToken(token) {
            if(jme.isType(token, 'html')) {
                token = jme.castToType(token, 'html');
                if(!token.isInteractive()) {
                    return token.value.map((e) => e.cloneNode(true));
                }
                if(token.value.numbas_embedded) {
                    throw(new Numbas.Error('jme.subvars.html inserted twice'))
                }
                token.value.numbas_embedded = true;
                return token.value;
            } else if(jme.isType(token, 'string')) {
                token = jme.castToType(token, 'string');
                var html = token.value;
                if(!token.safe) {
                    html = html.replace(/\\([{}])/g, '$1');
                }
                if(token.latex && token.display_latex) {
                    html = '\\(' + html + '\\)';
                }
                return html;
            } else if(jme.isType(token, 'list')) {
                token = jme.castToType(token, 'list');
                return '[ ' + token.value.map(function(item) {
                    return doToken(item)
                }).join(', ') + ' ]';
            } else {
                return jme.tokenToDisplayString(token, scope);
            }
        }
        var out = [];
        for(let i = 0; i < bits.length; i++) {
            if(i % 2) {
                try {
                    var tree = jme.compile(bits[i]);
                } catch(e) {
                    throw(new Numbas.Error('jme.subvars.error compiling', {message: e.message, expression: bits[i]}, e));
                }
                var v = scope.evaluate(tree);
                if(v === null) {
                    throw(new Numbas.Error('jme.subvars.null substitution', {str:bits[i]}));
                }
                v = doToken(v);
            } else {
                v = bits[i];
            }
            if(typeof v == 'string') {
                if(out.length > 0 && typeof out.at(-1) == 'string') {
                    out[out.length - 1] += v;
                } else {
                    out.push(v);
                }
            } else {
                out.push(v);
            }
        }
        for(let i = 0;i < out.length;i++) {
            if(typeof out[i] == 'string') {
                var d = document.createElement('div');
                d.innerHTML = out[i];
                d = doc.importNode(d, true);
                out[i] = Array.from(d.childNodes);
            }
        }
        return out;
    }
};

/** A definition of a note.
 *
 * The note's name, followed by an optional description enclosed in parentheses, then a colon, and finally a {@link JME} expression to evaluate.
 *
 * @typedef {string} Numbas.jme.variables.note_definition
 */


var re_note = /^(\$?[a-zA-Z_][a-zA-Z0-9_]*'*)(?:\s*\(([^)]*)\))?\s*:\s*((?:.|\n)*)$/m;

/** A note forming part of a notes script.
 *
 * @memberof Numbas.jme.variables
 * @class
 *
 * @property {string} name - The note's name.
 * @property {string} description - Text describing the note.
 * @property {Numbas.jme.variables.note_definition} expr - The JME expression to evaluate to compute this note.
 * @property {Numbas.jme.tree} tree - The compiled form of the expression.
 * @property {string[]} vars - The names of the variables this note depends on.
 *
 * @param {JME} source
 * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
 *
 */
var ScriptNote = jme.variables.ScriptNote = function(source, scope) {
    source = source.trim();
    var m = re_note.exec(source);
    if(!m) {
        var hint;
        if(/^[a-zA-Z_][a-zA-Z0-9+]*'*(?:\s*\(([^)]*)\))?$/.test(source)) {
            hint = R('jme.script.note.invalid definition.missing colon');
        } else if(/^[a-zA-Z_][a-zA-Z0-9+]*'*\s*\(/.test(source)) {
            hint = R('jme.script.note.invalid definition.description missing closing bracket');
        }
        throw(new Numbas.Error("jme.script.note.invalid definition", {source: source, hint: hint}));
    }
    this.name = m[1];
    this.description = m[2];
    this.expr = m[3];
    if(!this.expr) {
        throw(new Numbas.Error("jme.script.note.empty expression", {name:this.name}));
    }
    try {
        this.tree = jme.compile(this.expr);
    } catch(e) {
        throw(new Numbas.Error("jme.script.note.compilation error", {name:this.name, message:e.message}));
    }
    this.vars = jme.findvars(this.tree, [], scope);
}

/** Create a constructor for a notes script.
 *
 * @param {Function} construct_scope - A function which takes a base scope and a dictionary of variables, and returns a new scope in which to evaluate notes.
 * @param {Function} process_result - A function which takes the result of evaluating a note, and a scope, and returns a potentially modified result.
 * @param {Function} compute_note - A function which computes a note.
 *
 * @returns {Function}
 */
jme.variables.note_script_constructor = function(construct_scope, process_result, compute_note) {
    construct_scope = construct_scope || function(scope, variables) {
        return new jme.Scope([scope, {variables:variables}]);
    };

    process_result = process_result || function(r) {
        return r;
    }
    /**
     * A notes script.
     *
     * @param {string} source - The source of the script.
     * @param {Numbas.jme.variables.Script} base - A base script to extend.
     * @param {Numbas.jme.Scope} scope
     * @memberof Numbas.jme.variables
     * @class
     */
    function Script(source, base, scope) {
        this.source = source;
        scope = construct_scope(scope || Numbas.jme.builtinScope);
        try {
            var notes = source.replace(/^\/\/.*$/gm, '').split(/\n(?:\s*\n)+(?!\s)/);
            var ntodo = {};
            var todo = {};
            notes.forEach(function(note) {
                if(note.trim().length) {
                    var res = new ScriptNote(note, scope);
                    var name = jme.normaliseName(res.name, scope);
                    ntodo[name] = todo[name] = res;
                }
            });
            if(base) {
                Object.keys(base.notes).forEach(function(name) {
                    if(name in ntodo) {
                        todo['base_' + name] = base.notes[name];
                    } else {
                        todo[name] = base.notes[name];
                    }
                });
            }
        } catch(e) {
            throw(new Numbas.Error("jme.script.error parsing notes", {message:e.message}));
        }
        this.notes = todo;
    }
    Script.prototype = /** @lends Numbas.jme.variables.Script.prototype */ {

        /** The source code of the script.
         *
         * @type {string}
         */
        source: '',


        construct_scope: function(scope, variables) {
            scope = construct_scope(scope, variables);

            // if any names used by notes are already defined as variables in this scope, delete them
            Object.keys(this.notes).forEach(function(name) {
                if(variables[name] === undefined) {
                    scope.deleteVariable(name);
                }
            });
            return scope;
        },

        /** Evaluate all of this script's notes in the given scope.
         *
         * @param {Numbas.jme.Scope} scope
         * @param {Object<Numbas.jme.token>} variables - Extra variables defined in the scope.
         *
         * @returns {object}
         */
        evaluate: function(scope, variables) {
            scope = this.construct_scope(scope, variables);

            var result = jme.variables.makeVariables(this.notes, scope, null, compute_note);
            return process_result(result, scope);
        },

        evaluate_note: function(note, scope, changed_variables) {
            changed_variables = changed_variables || {};
            var nscope = construct_scope(scope);
            var result = jme.variables.remakeVariables(this.notes, changed_variables, nscope, compute_note, [note]);
            for(const name of Object.keys(result.variables)) {
                nscope.setVariable(name, result.variables[name]);
            }
            return {value: result.variables[note], scope: nscope};
        }
    }

    return Script
}


/** An object which substitutes JME values into HTML.
 * JME expressions found inside text nodes are evaluated with respect to the given scope.
 *
 * @param {Numbas.jme.Scope} scope
 * @memberof Numbas.jme.variables
 * @class
 */
var DOMcontentsubber = Numbas.jme.variables.DOMcontentsubber = function(scope) {
    this.scope = scope;
    this.re_end = undefined;

    this.IGNORE_TAGS = ['iframe', 'script', 'style'];
}
DOMcontentsubber.prototype = {
    /** Substitute JME values into the given element and any children.
     *
     * @param {Element} element
     * @returns {Element}
     */
    subvars: function(element) {
        try {
            switch(element.nodeType) {
                case 1: //element
                    element = this.sub_element(element);
                    break;
                case 3: //text
                    element = this.sub_text(element);
                    break;
                default:
                    return element;
            }
        } catch(error) {
            error.element = error.element || element;
            throw(error);
        }
        return element;
    },

    sub_element: function(element) {
        var subber = this;
        var scope = this.scope;
        var tagName = element.tagName.toLowerCase();
        if(this.IGNORE_TAGS.indexOf(tagName) >= 0) {
            return element;
        } else if(element.hasAttribute('nosubvars')) {
            return element;
        } else if(tagName == 'img') {
            const url = new URL(element.getAttribute('src'), window.location);
            if(url.pathname.match(/\.svg$/i)) {
                var object = element.ownerDocument.createElement('object');
                for(let i = 0;i < element.attributes.length;i++) {
                    const attr = element.attributes[i];
                    if(attr.name != 'src') {
                        object.setAttribute(attr.name, attr.value);
                    }
                }
                object.setAttribute('type', 'image/svg+xml');
                object.setAttribute('data', element.getAttribute('src'));
                if(element.hasAttribute('alt')) {
                    object.setAttribute('aria-label', element.getAttribute('alt'));
                }
                if(element.parentElement) {
                    element.parentElement.replaceChild(object, element);
                }
                subber.sub_element(object);
                return object;
            }
        } else if(tagName == 'object') {
            /** Substitute content into the object's root element.
             */
            function go() {
                const rootElement = element.contentDocument?.rootElement;
                if(!rootElement) {
                    return;
                }
                jme.variables.DOMcontentsubvars(rootElement, scope);
            }
            if(element.contentDocument && element.contentDocument.rootElement) {
                go();
            } else {
                element.addEventListener('load', go, false);
            }
            return element;
        }
        if(element.hasAttribute('data-jme-visible')) {
            var condition = element.getAttribute('data-jme-visible');
            var result = scope.evaluate(condition);
            if(!(result.type == 'boolean' && result.value == true)) {
                var el = element;
                while(el.parentElement) {
                    var p = el.parentElement;
                    p.removeChild(el);
                    el = p;
                    if(p.childNodes.length > 0) {
                        break;
                    }
                }
                return element;
            }
        }
        var new_attrs = {};
        for(let i = 0;i < element.attributes.length;i++) {
            var m;
            const attr = element.attributes[i];
            if((m = attr.name.match(/^eval-(.*)/) || (m = attr.name.match(/^(alt)/)))) {
                var name = m[1];
                var value = jme.subvars(attr.value, scope, true);
                new_attrs[name] = value;
            }
        }
        for(const [name, value] of Object.entries(new_attrs)) {
            element.setAttribute(name, value);
        }
        var o_re_end = this.re_end;
        for(const child of Array.from(element.childNodes)) {
            subber.subvars(child);
        }
        this.re_end = o_re_end; // make sure that any maths environment only applies to children of this element; otherwise, an unended maths environment could leak into later tags
        return element;
    },
    sub_text: function(node) {
        var str = node.nodeValue;
        var bits = util.contentsplitbrackets(str, this.re_end);    //split up string by TeX delimiters. eg "let $X$ = \[expr\]" becomes ['let ','$','X','$',' = ','\[','expr','\]','']
        this.re_end = bits.re_end;
        var l = bits.length;
        for(let i = 0; i < l; i += 4) {
            var textsubs = jme.variables.DOMsubvars(bits[i], this.scope, node.ownerDocument);
            for(let j = 0;j < textsubs.length;j++) {
                textsubs[j].forEach(function(t) {
                    node.parentElement.insertBefore(t, node);
                });
            }
            var startDelimiter = bits[i + 1] || '';
            var tex = bits[i + 2] || '';
            var endDelimiter = bits[i + 3] || '';
            var n = node.ownerDocument.createTextNode(startDelimiter + tex + endDelimiter);
            node.parentElement.insertBefore(n, node);
        }
        node.parentElement.removeChild(node);
        return node;
    },

    /**
     * Find all variables which would be used when substituting into the given HTML node.
     * If the node is an element, use `findvars_element`; if it's text, use `findvars_text`.
     *
     * @param {Node} element
     * @returns {Array.<string>}
     */
    findvars: function(element) {
        switch(element.nodeType) {
            case 1: //element
                return this.findvars_element(element);
            case 3: //text
                return this.findvars_text(element);
            default:
                return [];
        }
    },

    /** Find all variables which would be used when substituting into the given element.
     *
     * @param {Element} element
     * @returns {Array.<string>}
     */
    findvars_element: function(element) {
        var subber = this;
        var scope = this.scope;
        var tagName = element.tagName.toLowerCase();
        if(this.IGNORE_TAGS.indexOf(tagName) >= 0) {
            return [];
        } else if(element.hasAttribute('nosubvars')) {
            return [];
        } else if(tagName == 'img') {
            return [];
        } else if(tagName == 'object') {
            if(element.contentDocument && element.contentDocument.rootElement) {
                return this.findvars_element(element.contentDocument.rootElement);
            }
            return;
        }
        var foundvars = [];
        if(element.hasAttribute('data-jme-visible')) {
            var condition = element.getAttribute('data-jme-visible');
            let tree;
            try {
                tree = scope.parser.compile(condition);
            } catch {
                return [];
            }
            foundvars = foundvars.merge(jme.findvars(tree, [], scope));
        }
        for(let i = 0;i < element.attributes.length;i++) {
            const attr = element.attributes[i];
            if(attr.name.match(/^eval-(.*)/)) {
                let tree;
                try {
                    tree = scope.parser.compile(attr.value);
                } catch {
                    continue;
                }
                foundvars = foundvars.merge(jme.findvars(tree, [], scope));
            }
        }
        var o_re_end = this.re_end;
        for(const child of Array.from(element.childNodes)) {
            var vars = subber.findvars(child, scope);
            if(vars.length) {
                foundvars = foundvars.merge(vars);
            }
        }
        this.re_end = o_re_end; // make sure that any maths environment only applies to children of this element; otherwise, an unended maths environment could leak into later tags
        return foundvars;
    },

    findvars_text: function(node) {
        var scope = this.scope;
        var foundvars = [];
        var str = node.nodeValue;
        var bits = util.contentsplitbrackets(str, this.re_end);    //split up string by TeX delimiters. eg "let $X$ = \[expr\]" becomes ['let ','$','X','$',' = ','\[','expr','\]','']
        this.re_end = bits.re_end;

        /**
         * Find variables used in plain text: look for substitutions between curly braces.
         *
         * @param {string} text
         */
        function findvars_plaintext(text) {
            var tbits = util.splitbrackets(text, '{', '}', '(', ')');
            for(let j = 1;j < tbits.length;j += 2) {
                try {
                    var tree = scope.parser.compile(tbits[j]);
                } catch {
                    continue;
                }
                foundvars = foundvars.merge(jme.findvars(tree, [], scope));
            }
        }

        for(let i = 0; i < bits.length; i += 4) {
            findvars_plaintext(bits[i]);
            var tex = bits[i + 2] || '';
            var texbits = jme.texsplit(tex);
            for(let j = 0;j < texbits.length;j += 4) {
                var command = texbits[j + 1];
                var content = texbits[j + 3];
                switch(command) {
                    case 'var':
                        try {
                            var tree = scope.parser.compile(content);
                            foundvars = foundvars.merge(jme.findvars(tree, [], scope));
                            break;
                        } catch {
                            continue;
                        }
                    case 'simplify':
                        findvars_plaintext(content);
                        break;
                }
            }
        }
        return foundvars;
    }
}
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Sets up the JME compiler and evaluator.
 *
 * Provides {@link Numbas.jme}
 */
Numbas.queueScript('jme', ['jme-base', 'jme-builtins', 'jme-rules', 'unicode-mappings'], function() {
    var jme = Numbas.jme;
    /** For backwards compatibility, copy references to some members of jme.rules to jme.
     * These items used to belong to Numbas.jme, but were spun out to Numbas.jme.rules.
     */
    ['displayFlags', 'Ruleset', 'collectRuleset'].forEach(function(name) {
        jme[name] = jme.rules[name];
    });
});
Numbas.queueScript('jme-base', ['base', 'math', 'util'], function() {
var util = Numbas.util;
var math = Numbas.math;

/** A JME expression.
 *
 * @typedef JME
 * @type {string}
 * @see {@link https://docs.numbas.org.uk/en/latest/jme-reference.html}
 */

/** A string of TeX code.
 *
 * @typedef TeX
 * @type {string}
 */

/** @typedef Numbas.jme.tree
 * @type {object}
 * @property {Array.<Numbas.jme.tree>} args - The token's arguments (if it's an op or function).
 * @property {Numbas.jme.token} tok - The token at this node.
 */

/** @typedef {object} Numbas.jme.call_signature
 * @property {Numbas.jme.funcObj} fn - The function to call.
 * @property {Numbas.jme.signature} signature - The signature to use.
 */

/** A definition of a custom constant.
 *
 * @typedef Numbas.jme.constant_definition
 * @property {TeX} tex - A TeX rendering of the constant
 * @property {Numbas.jme.token} value - The JME value of the constant.
 * @property {boolean} enabled - Is the constant enabled? True by default.
 */


/** @namespace Numbas.jme */
var jme = Numbas.jme = /** @lends Numbas.jme */ {
    normaliseRulesetName: function(name) {
        return name.toLowerCase();
    },

    normaliseName: function(name, settings) {
        settings = settings || {caseSensitive: false};

        if(!settings.caseSensitive) {
            name = name.toLowerCase();
        }

        return name;
    },

    /** Escape a string so that it will be interpreted correctly by the JME parser.
     *
     * @param {string} str
     * @returns {string}
     * @see Numbas.jme.unescape
     */
    escape: function(str) {
        return str
            .replace(/\\/g, '\\\\')
            .replace(/\\([{}])/g, '$1')
            .replace(/\n/g, '\\n')
            .replace(/"/g, '\\"')
            .replace(/'/g, "\\'")
        ;
    },

    /**
     * Copy a tree, but keep the original token objects.
     *
     * @param {Numbas.jme.tree} tree
     * @returns {Numbas.jme.tree}
     */
    copy_tree: function(tree) {
        var o = {tok: tree.tok};
        if(tree.args) {
            o.args = tree.args.map(jme.copy_tree);
        }
        return o;
    },

    /** Wrapper around {@link Numbas.jme.Parser#compile}.
     *
     * @param {JME} expr
     * @see Numbas.jme.Parser#compile
     * @returns {Numbas.jme.tree}
     */
    compile: function(expr) {
        return jme.standardParser.compile(expr);
    },

    /** Options for a JME operator.
     *
     * @typedef {object} Numbas.jme.operatorOptions
     * @property {Array.<string>} synonyms - Synonyms for this operator. See {@link Numbas.jme.opSynonyms}.
     * @property {number} precedence - An operator with lower precedence is evaluated before one with high precedence. Only makes sense for binary operators. See {@link Numbas.jme.precedence}.
     * @property {boolean} commutative - Is this operator commutative? Only makes sense for binary operators.
     * @property {boolean} rightAssociative - Is this operator right-associative? Only makes sense for unary operators.
     */

    /** Add a binary operator to the standard parser.
     *
     * @param {string} name
     * @param {Numbas.jme.operatorOptions} options
     */
    addBinaryOperator: function(name, options) {
        jme.standardParser.addBinaryOperator(name, options);
    },

    /** Add a prefix operator to the parser.
     *
     * @param {string} name
     * @param {string} alt - The "interpreted" name of the operator, e.g. '!' is interpreted as 'fact'. If not given, the value of `name` is used.
     * @param {Numbas.jme.operatorOptions} options
     */
    addPrefixOperator: function(name, alt, options) {
        jme.standardParser.addPrefixOperator(name, alt, options);
    },

    /** Add a postfix operator to the parser.
     *
     * @param {string} name
     * @param {string} alt - The "interpreted" name of the operator, e.g. '!' is interpreted as 'fact'. If not given, the value of `name` is used.
     * @param {Numbas.jme.operatorOptions} options
     */
    addPostfixOperator: function(name, alt, options) {
        jme.standardParser.addPostfixOperator(name, alt, options);
    },


    /** Wrapper around {@link Numbas.jme.Parser#tokenise}.
     *
     * @param {JME} expr
     * @see Numbas.jme.Parser#tokenise
     * @returns {Numbas.jme.token[]}
     */
    tokenise: function(expr) {
        return jme.standardParser.tokenise(expr);
    },

    /** Wrapper around {@link Numbas.jme.Parser#shunt}.
     *
     * @param {Numbas.jme.token[]} tokens
     * @see Numbas.jme.Parser#shunt
     * @returns {Numbas.jme.tree}
     */
    shunt: function(tokens) {
        return jme.standardParser.shunt(tokens);
    },

    /** Unescape a string - backslashes escape special characters.
     *
     * @param {string} str
     * @returns {string}
     * @see Numbas.jme.escape
     */
    unescape: function(str) {
        var estr = '';
        while(true) {
            var i = str.indexOf('\\');
            if(i == -1) {
 break;
} else {
                estr += str.slice(0, i);
                var c;
                if((c = str.charAt(i + 1)) == 'n') {
                    estr += '\n';
                } else if(c == '{' || c == '}') {
                    estr += '\\' + c;
                } else {
                    estr += c;
                }
                str = str.slice(i + 2);
            }
        }
        estr += str;
        return estr;
    },
    /** Substitute variables defined in `scope` into the given syntax tree (in place).
     *
     * @param {Numbas.jme.tree} tree
     * @param {Numbas.jme.Scope} scope
     * @param {boolean} [allowUnbound=false] - Allow unbound variables to remain in the returned tree.
     * @param {boolean} [unwrapExpressions=false] - Unwrap TExpression tokens?
     * @returns {Numbas.jme.tree}
     */
    substituteTree: function(tree, scope, allowUnbound, unwrapExpressions) {
        if(!tree) {
            return null;
        }
        if(tree.tok.bound) {
            return tree;
        }
        if(tree.args === undefined) {
            if(tree.tok.type == 'name') {
                var name = jme.normaliseName(tree.tok.name, scope);
                var v = scope.getVariable(name);
                if(v === undefined) {
                    var c = scope.getConstant(name);
                    if(c) {
                        return {tok: c.value};
                    }
                    if(allowUnbound) {
                        return {tok: new TName(tree.tok.nameWithoutAnnotation, tree.tok.annotation)};
                    } else {
                        throw new Numbas.Error('jme.substituteTree.undefined variable', {name:name});
                    }
                } else {
                    if(v.tok) {
                        return v;
                    } else if(unwrapExpressions) {
                        return jme.unwrapSubexpression({tok:v});
                    } else {
                        return {tok: v};
                    }
                }
            } else {
                return tree;
            }
        } else if((tree.tok.type == 'function' || tree.tok.type == 'op') && tree.tok.name in substituteTreeOps) {
            tree = {tok: tree.tok,
                    args: tree.args.slice()};
            substituteTreeOps[tree.tok.name](tree, scope, allowUnbound, unwrapExpressions);
            return tree;
        } else {
            tree = {
                tok: tree.tok,
                args: tree.args.slice()
            };
            for(let i = 0;i < tree.args.length;i++) {
                tree.args[i] = jme.substituteTree(tree.args[i], scope, allowUnbound, unwrapExpressions);
            }
            return tree;
        }
    },
    /** Evaluate a syntax tree (or string, which is compiled to a syntax tree), with respect to the given scope.
     *
     * @param {Numbas.jme.tree|string} tree
     * @param {Numbas.jme.Scope} scope
     * @returns {Numbas.jme.token}
     */
    evaluate: function(tree, scope) {
        if(!scope) {
            throw(new Numbas.Error('jme.evaluate.no scope given'));
        }
        return scope.evaluate(tree);
    },
    /** Compile a list of expressions, separated by commas.
     *
     * @param {JME} expr
     * @see Numbas.jme.tokenise
     * @see Numbas.jme.shunt
     * @returns {Numbas.jme.tree[]}
     */
    compileList: function(expr) {
        expr += '';    //make sure expression is a string and not a number or anything like that
        if(!expr.trim().length) {
            return null;
        }
        //typecheck
        //tokenise expression
        var tokens = jme.tokenise(expr);
        var bits = [];
        var brackets = [];
        var start = 0;
        for(let i = 0;i < tokens.length;i++) {
            switch(tokens[i].type) {
                case '(':
                case '[':
                    brackets.push(tokens[i]);
                    break;
                case ')':
                    if(!brackets.length || brackets.pop().type != '(') {
                        throw(new Numbas.Error('jme.compile list.mismatched bracket'));
                    }
                    break;
                case ']':
                    if(!brackets.length || brackets.pop().type != '[') {
                        throw(new Numbas.Error('jme.compile list.mismatched bracket'));
                    }
                    break;
                case ',':
                    if(brackets.length == 0) {
                        bits.push(tokens.slice(start, i));
                        start = i + 1;
                    }
                    break;
            }
        }
        if(brackets.length) {
            throw(new Numbas.Error('jme.compile list.missing right bracket'));
        }
        bits.push(tokens.slice(start));
        //compile to parse tree
        var trees = bits.map(function(b) {
            return jme.shunt(b)
        });
        return trees;
    },
    /** Settings for {@link Numbas.jme.compare}.
     *
     * @typedef {object} Numbas.jme.compare_settings
     * @property {string} checkingType - The name of the method to determine if two results are equal. See {@link Numbas.jme.checkingFunctions}.
     * @property {number} vsetRangeStart - The lower bound of the range to pick variable values from.
     * @property {number} vsetRangeEnd - The upper bound of the range to pick variable values from.
     * @property {number} vsetRangePoints - The number of values to pick for each variable.
     * @property {number} checkingAccuracy - A parameter for the checking function to determine if two results are equal. See {@link Numbas.jme.checkingFunctions}.
     * @property {number} failureRate - The number of times the comparison must fail to declare that the expressions are unequal.
     * @property {boolean} sameVars - If true, then both expressions should have exactly the same free variables.
     */
    /** Compare two expressions over some randomly selected points in the space of variables, to decide if they're equal.
     *
     * @param {JME} tree1
     * @param {JME} tree2
     * @param {Numbas.jme.compare_settings} settings
     * @param {Numbas.jme.Scope} scope
     * @returns {boolean}
     */
    compare: function(tree1, tree2, settings, scope) {
        var default_settings = {
            vsetRangeStart: 0,
            vsetRangeEnd: 1,
            vsetRangePoints: 5,
            checkingType: 'absdiff',
            checkingAccuracy: 0.0001,
            failureRate: 1
        }
        settings = util.extend_object({}, default_settings, settings);
        var checkingFunction = checkingFunctions[settings.checkingType.toLowerCase()];    //work out which checking type is being used
        try {
            if(tree1 == null || tree2 == null) {
                //one or both expressions are invalid, can't compare
                return false;
            }
            //find variable names used in both expressions - can't compare if different
            var vars1 = findvars(tree1, [], scope);
            var vars2 = findvars(tree2, [], scope);
            for(const v of Object.keys(scope.allVariables())) {
                delete vars1[v];
                delete vars2[v];
            }
            if(settings.sameVars) {
                if(!varnamesAgree(vars1, vars2)) {    //whoops, differing variables
                    return false;
                }
            } else {
                vars2.forEach(function(n) {
                    if(vars1.indexOf(n) == -1) {
                        vars1.push(n);
                    }
                });
            }
            var hasNames = vars1.length > 0;
            var numRuns = hasNames ? settings.vsetRangePoints : 1;
            var failureRate = hasNames ? settings.failureRate : 1;
            // if variables are used,  evaluate both expressions over a random selection of values and compare results
            var errors = 0;
            var rs = randoms(vars1, settings.vsetRangeStart, settings.vsetRangeEnd, numRuns);
            for(let i = 0; i < rs.length; i++) {
                var nscope = new jme.Scope([scope, {variables:rs[i]}]);
                var r1 = nscope.evaluate(tree1);
                var r2 = nscope.evaluate(tree2);
                if(!resultsEqual(r1, r2, checkingFunction, settings.checkingAccuracy, scope)) {
                    errors++;
                }
            }
            return errors < failureRate;
        } catch {
            return false;
        }
    },
    /** Substitute variables into a string. To substitute variables into an HTML element, use {@link Numbas.jme.variables.DOMcontentsubvars}.
     *
     * @param {string} str
     * @param {Numbas.jme.Scope} scope
     * @param {boolean} [sub_tex=false] - Substitute into TeX? Normally this is left to MathJax.
     * @returns {string}
     */
    contentsubvars: function(str, scope, sub_tex) {
        var bits = util.contentsplitbrackets(str);    //split up string by TeX delimiters. eg "let $X$ = \[expr\]" becomes ['let ','$','X','$',' = ','\[','expr','\]','']
        for(let i = 0; i < bits.length; i += 4) {
            bits[i] = jme.subvars(bits[i], scope, true);
            if(sub_tex && i + 3 < bits.length) {
                var tbits = jme.texsplit(bits[i + 2]);
                var out = '';
                for(let j = 0;j < tbits.length;j += 4) {
                    out += tbits[j];
                    if(j + 3 < tbits.length) {
                        var cmd = tbits[j + 1];
                        var rules = jme.collectRuleset(tbits[j + 2], scope.allRulesets());
                        var expr = tbits[j + 3];
                        switch(cmd) {
                        case 'var':
                            var v = scope.evaluate(expr);
                            var tex = jme.display.texify({tok: v}, rules, scope);
                            out += '{' + tex + '}';
                            break;
                        case 'simplify':
                            expr = jme.subvars(expr, scope);
                            out += '{' + jme.display.exprToLaTeX(expr, rules, scope) + '}';
                            break;
                        }
                    }
                }
                bits[i + 2] = out;
            }
        }
        return bits.join('');
    },
    /** Split up a TeX expression, finding the \var and \simplify commands.
     * Returns an array `[normal tex,var or simplify,options,argument,normal tex,...]`.
     *
     * @param {string} s
     * @returns {Array.<string>}
     */
    texsplit: function(s) {
        var cmdre = /^((?:.|[\n\r])*?)\\(var|simplify)/m;
        var out = [];
        var m = s.match(cmdre);
        while(m) {
            out.push(m[1]);
            var cmd = m[2];
            out.push(cmd);
            var i = m[0].length;
            var args = '';
            var argbrackets = false;
            if(s.charAt(i) == '[') {
                argbrackets = true;
                const si = i + 1;
                while(i < s.length && s.charAt(i) != ']') {
                    i++;
                }
                if(i == s.length) {
                    throw(new Numbas.Error('jme.texsubvars.no right bracket', {op:cmd}));
                } else {
                    args = s.slice(si, i);
                    i++;
                }
            }
            if(!argbrackets) {
                args = 'all';
            }
            out.push(args);
            if(s.charAt(i) != '{') {
                throw(new Numbas.Error('jme.texsubvars.missing parameter', {op:cmd, parameter:s}));
            }
            var brackets = 1;
            const si = i + 1;
            while(i < s.length - 1 && brackets > 0) {
                i++;
                if(s.charAt(i) == '{') {
                    brackets++;
                } else if(s.charAt(i) == '}') {
                    brackets--;
                }
            }
            if(i == s.length - 1 && brackets > 0) {
                throw(new Numbas.Error('jme.texsubvars.no right brace', {op:cmd}));
            }
            var expr = s.slice(si, i);
            s = s.slice(i + 1);
            out.push(expr);

            m = s.match(cmdre);
        }
        out.push(s);
        return out;
    },
    /** Dictionary of functions which convert a JME token to a string for display.
     *
     * @enum {Function}
     */
    typeToDisplayString: {
        'number': function(v, scope) {
            var jmeifier = new Numbas.jme.display.JMEifier({}, scope);
            return jmeifier.niceNumber(v.value, Numbas.jme.display.number_options(v));
        },
        'rational': function(v) {
            var f = v.value.reduced();
            return f.toString();
        },
        'decimal': function(v, scope) {
            var jmeifier = new Numbas.jme.display.JMEifier({}, scope);
            var options = Numbas.jme.display.number_options(v);
            return jmeifier.niceDecimal(v.value, options);
        },
        'string': function(v, display) {
            return v.value;
        },
        'html': function(v) {
            v = v.value;
            if(window.jQuery) {
                v = v.toArray();
            }
            return v.map(function(e) {
                return e.outerHTML;
            }).join('');
        }
    },
    /** Produce a string representation of the given token, for display.
     *
     * @param {Numbas.jme.token} v
     * @param {Numbas.jme.Scope} scope
     * @see Numbas.jme.typeToDisplayString
     * @returns {string}
     */
    tokenToDisplayString: function(v, scope) {
        if(v.type in jme.typeToDisplayString) {
            return jme.typeToDisplayString[v.type](v, scope);
        } else {
            return jme.display.treeToJME({tok:v}, {}, scope);
        }
    },
    /** Substitute variables into a text string (not maths).
     *
     * Warning: when `display = true`, subbed-in values might not be bracketed correctly. Use {@link Numbas.jme.display.subvars} to substitute values into JME expressions.
     *
     * @param {string} str
     * @param {Numbas.jme.Scope} scope
     * @param {boolean} [display=false] - Is this string going to be displayed to the user? If so, avoid unnecessary brackets and quotes.
     * @returns {string}
     */
    subvars: function(str, scope, display) {
        var bits = util.splitbrackets(str, '{', '}', '(', ')');
        if(bits.length == 1) {
            return str;
        }
        var out = '';
        for(let i = 0; i < bits.length; i++) {
            if(i % 2) {
                try {
                    var tree = scope.parser.compile(bits[i]);
                } catch(e) {
                    throw(new Numbas.Error('jme.subvars.error compiling', {message: e.message, expression: bits[i]}, e));
                }
                var v = scope.evaluate(tree);
                if(v === null) {
                    throw(new Numbas.Error('jme.subvars.null substitution', {str:str}));
                }
                var ov;
                if(display) {
                    ov = jme.tokenToDisplayString(v, scope);
                } else {
                    if(jme.isType(v, 'number')) {
                        ov = '(' + Numbas.jme.display.treeToJME({tok:v}, {nicenumber: false, noscientificnumbers: true}, scope) + ')';
                    } else if(v.type == 'string') {
                        ov = "'" + jme.escape(v.value) + "'";
                    } else {
                        ov = jme.display.treeToJME({tok:v}, {nicenumber: false, noscientificnumbers: true}, scope);
                    }
                }
                out += ov;
            } else {
                out += bits[i];
            }
        }
        return out;
    },
    /** Unwrap a {@link Numbas.jme.token} into a plain JavaScript value.
     *
     * @param {Numbas.jme.token} v
     * @returns {object}
     */
    unwrapValue: function(v) {
        switch(v.type) {
            case 'list':
                return v.value.map(jme.unwrapValue);
            case 'dict':
                var o = {};
                Object.keys(v.value).forEach(function(key) {
                    o[key] = jme.unwrapValue(v.value[key]);
                });
                return o;
            case 'name':
                return v.name;
            case 'expression':
                return v.tree;
            case 'nothing':
                return undefined;
            default:
                return v.value;
        }
    },

    /**
     * Unwrap TExpression tokens: if `tree.tok` is a TExpression token, just return its `tree` property.
     * Applies recursively.
     *
     * @param {Numbas.jme.tree} tree
     * @returns {Numbas.jme.tree}
     */
    unwrapSubexpression: function(tree) {
        if(tree.tok.type == 'expression') {
            return jme.unwrapSubexpression(tree.tok.tree);
        } else {
            return tree;
        }
    },

    /** Mark a token as 'safe', so it doesn't have {@link Numbas.jme.subvars} applied to it, or any strings it contains, when it's evaluated.
     *
     * @param {Numbas.jme.token} t
     * @returns {Numbas.jme.token}
     */
    makeSafe: function(t) {
        if(!t) {
            return t;
        }
        switch(t.type) {
            case 'string':
                t.safe = true;
                var t2 = new TString(t.value);
                if(t.latex !== undefined) {
                    t2.latex = t.latex;
                }
                t2.safe = true;
                return t2;
            case 'list':
                return new TList(t.value.map(jme.makeSafe));
            case 'dict':
                var o = {};
                for(const [k, v] of Object.entries(t.value)) {
                    o[k] = jme.makeSafe(v);
                }
                return new TDict(o);
            default:
                return t;
        }
    },

    /** Wrap up a plain JavaScript value (number, string, bool or array) as a {@link Numbas.jme.token}.
     *
     * @param {object} v
     * @param {string} typeHint - Name of the expected type (to differentiate between, for example, matrices, vectors and lists.
     * @returns {Numbas.jme.token}
     */
    wrapValue: function(v, typeHint) {
        switch(typeof v) {
        case 'number':
            return new jme.types.TNum(v);
        case 'string':
            var s = new jme.types.TString(v);
            s.safe = true;
            return s;
        case 'boolean':
            return new jme.types.TBool(v);
        default:
            switch(typeHint) {
                case 'html':
                    return v;
                default:
                    if(Array.isArray(v)) {
                        // it would be nice to abstract this, but some types need the arguments to be wrapped, while others don't
                        switch(typeHint) {
                        case 'matrix':
                            return new jme.types.TMatrix(v);
                        case 'vector':
                            return new jme.types.TVector(v);
                        case 'range':
                            return new jme.types.TRange(v);
                        case 'set':
                            v = v.map(jme.wrapValue);
                            return new jme.types.TSet(v);
                        default:
                            v = v.map(jme.wrapValue);
                            return new jme.types.TList(v);
                        }
                    } else if(v instanceof math.ComplexDecimal) {
                        return new jme.types.TDecimal(v);
                    } else if(typeof v == 'object' && v && v.complex && v.hasOwnProperty('re') && v.hasOwnProperty('im')) {
                        return new jme.types.TNum(v);
                    } else if(v instanceof Decimal) {
                        return new jme.types.TDecimal(v);
                    } else if(v instanceof math.Fraction) {
                        return new jme.types.TRational(v);
                    } else if(v === null || v === undefined) { // CONTROVERSIAL! Cast null to the empty string, because we don't have a null type.
                        return new jme.types.TString('');
                    } else if(v !== null && typeof v == 'object' && v.type === undefined) {
                        var o = {};
                        Object.keys(v).forEach(function(key) {
                            o[key] = jme.wrapValue(v[key]);
                        });
                        return new jme.types.TDict(o);
                    }
                    return v;
            }
        }
    },
    /** Is a token of the given type, or can it be automatically cast to the given type?
     *
     * @param {Numbas.jme.token} tok
     * @param {string} type
     * @returns {boolean}
     */
    isType: function(tok, type) {
        if(!tok) {
            return false;
        }
        if(tok.type == type) {
            return true;
        }
        if(tok.casts) {
            return tok.casts[type] !== undefined;
        }
        return false;
    },
    /** Cast a token to the given type, if possible.
     * If `type` is an object, it can give more detailed information about the types of items in a collection.
     * The object should have a property `type` describing the type of the resulting collection object, and one of `items`, an array or object describing the type of each item individually, or `all_items`, a string or object describing the type of every item.
     * For lists, the `items` array can contain an object with property `missing: true` for items that are not present in the input token. A placeholder `'nothing'` value is included in the output list instead.
     *
     * @param {Numbas.jme.token} tok
     * @param {string|object} type
     * @returns {Numbas.jme.token}
     */
    castToType: function(tok, type) {
        var typeDescription = {};
        if(typeof(type) == 'object') {
            typeDescription = type;
            type = typeDescription.type;
        }
        var ntok;
        if(tok.type != type) {
            if(!tok.casts || !tok.casts[type]) {
                throw(new Numbas.Error('jme.type.no cast method', {from: tok.type, to: type}));
            }
            ntok = tok.casts[type](tok);
        } else {
            ntok = tok;
        }
        if(type == 'dict') {
            if(typeDescription.items) {
                ntok = new TDict(ntok.value);
                for(const [k, v] of Object.entries(typeDescription.items)) {
                    ntok.value[k] = jme.castToType(ntok.value[k], v);
                }
            } else if(typeDescription.all_items) {
                ntok = new TDict(ntok.value);
                for(const x of Object.keys(ntok.value)) {
                    ntok.value[x] = jme.castToType(ntok.value[x], typeDescription.all_items);
                }
            }
        }
        if(type == 'list') {
            let nvalue;
            if(typeDescription.items) {
                nvalue = [];
                var j = 0;
                for(let i = 0;i < typeDescription.items.length;i++) {
                    if(typeDescription.items[i].missing) {
                        nvalue.push(new TNothing());
                        continue;
                    }
                    var item = ntok.value[j];
                    nvalue.push(jme.castToType(item, typeDescription.items[i]));
                    j += 1;
                }
                ntok = new TList(nvalue);
            } else if(typeDescription.all_items) {
                nvalue = ntok.value.map(function(item) {
                    return jme.castToType(item, typeDescription.all_items);
                });
                ntok = new TList(nvalue);
            }
        }
        return ntok;
    },
    /** Can type `a` be automatically cast to type `b`?
     *
     * @param {string} a
     * @param {string} b
     * @returns {boolean}
     */
    isTypeCompatible: function(a, b) {
        if(b === undefined) {
            return true;
        }
        if(a == b) {
            return true;
        }
        var ta = jme.types[a];
        return ta && ta.prototype && ta.prototype.casts && ta.prototype.casts[b];
    },
    /** Find a type that both types `a` and `b` can be automatically cast to, or return `undefined`.
     *
     * @param {string} a
     * @param {string} b
     * @returns {string}
     */
    findCompatibleType: function(a, b) {
        a = jme.types[a];
        b = jme.types[b];
        if(a === undefined || b === undefined) {
            return undefined;
        }
        a = a.prototype;
        b = b.prototype;
        if(a.type == b.type) {
            return a.type;
        }
        if(a.casts) {
            if(a.casts[b.type]) {
                return b.type;
            }
            if(b.casts) {
                if(b.casts[a.type]) {
                    return a.type;
                }
                for(const x of Object.keys(a.casts)) {
                    if(b.casts[x]) {
                        return x;
                    }
                }
            }
        } else if(b.casts) {
            if(b.casts[a.type]) {
                return a.type;
            }
        }
        return undefined;
    },
    /** Is a token an operator with the given name?
     *
     * @param {Numbas.jme.token} tok
     * @param {string} op
     *
     * @returns {boolean}
     */
    isOp: function(tok, op) {
        return tok.type == 'op' && tok.name == op;
    },
    /** Is a token the given name?
     *
     * @param {Numbas.jme.token} tok
     * @param {string} name
     *
     * @returns {boolean}
     */
    isName: function(tok, name) {
        return tok.type == 'name' && tok.name == name;
    },
    /** Is a token a function with the given name?
     *
     * @param {Numbas.jme.token} tok
     * @param {string} name
     *
     * @returns {boolean}
     */
    isFunction: function(tok, name) {
        return tok.type == 'function' && tok.name == name;
    },

    /**
     * Does this expression behave deterministically?
     *
     * True if all functions or operations in the expression are marked `deterministic`.
     *
     * Note that this is _not_ just the converse of `Numbas.jme.isRandom`: to be conservative, a third option of "unknown", corresponding to "not isRandom and not isDeterministic", is possible.
     * In that case, this function returns `false`.
     *
     * @param {Numbas.jme.tree} expr
     * @param {Numbas.jme.Scope} scope
     * @returns {boolean}
     */
    isDeterministic: function(expr, scope) {
        switch(expr.tok.type) {
            case 'op':
            case 'function':
                // a function application is deterministic if its definition is marked as not random,
                // and all of its arguments are deterministic
                var op = jme.normaliseName(expr.tok.name, scope);
                if(isDeterministicOps[op]) {
                    return isDeterministicOps[op](expr, scope);
                }
                var fns = scope.getFunction(op);
                if(!fns || fns.length == 0) {
                    return false;
                }
                if(fns.some((fn) => fn.random !== false)) {
                    return false;
                }
                for(let i = 0;i < expr.args.length;i++) {
                    if(op == 'safe' && expr.args[i].tok.type == 'string') {
                        continue;
                    }
                    if(!jme.isDeterministic(expr.args[i], scope)) {
                        return false;
                    }
                }
                return true;
            case 'string':
                if(expr.tok.safe) {
                    return true;
                }
                var bits = util.splitbrackets(expr.tok.value, '{', '}', '(', ')');
                for(let i = 1;i < bits.length;i += 2) {
                    try {
                        var subexpr = Numbas.jme.compile(bits[i]);
                    } catch {
                        continue;
                    }
                    if(!jme.isDeterministic(subexpr, scope)) {
                        return false;
                    }
                }
                return true;
            case 'lambda':
                return jme.isDeterministic(expr.tok.expr, scope);
            default:
                if(!expr.args) {
                    return true;
                }
                for(let i = 0;i < expr.args.length;i++) {
                    if(!jme.isDeterministic(expr.args[i], scope)) {
                        return false;
                    }
                }
                return true;
        }
    },

    /** Does this expression behave randomly?
     * True if it contains any instances of functions or operations, defined in the given scope, which could behave randomly.
     *
     * @param {Numbas.jme.tree} expr
     * @param {Numbas.jme.Scope} scope
     * @returns {boolean}
     */
    isRandom: function(expr, scope) {
        switch(expr.tok.type) {
            case 'op':
            case 'function':
                // a function application is random if its definition is marked as random,
                // or if any of its arguments are random
                var op = jme.normaliseName(expr.tok.name, scope);
                var fns = scope.getFunction(op);
                if(fns) {
                    for(let i = 0;i < fns.length;i++) {
                        var fn = fns[i]
                        if(fn.random === undefined && fn.language == 'jme') {
                            fn.random = false; // put false in to avoid infinite recursion if fn is defined in terms of another function which itself uses fn
                            fn.random = jme.isRandom(fn.tree, scope);
                        }
                        if(fn.random) {
                            return true;
                        }
                    }
                }
                for(let i = 0;i < expr.args.length;i++) {
                    if(jme.isRandom(expr.args[i], scope)) {
                        return true;
                    }
                }
                return false;
            case 'string':
                var bits = util.splitbrackets(expr.tok.value, '{', '}', '(', ')');
                for(let i = 1;i < bits.length;i += 2) {
                    try {
                        var subexpr = Numbas.jme.compile(bits[i]);
                    } catch {
                        continue;
                    }
                    if(jme.isRandom(subexpr, scope)) {
                        return true;
                    }
                }
                return false;
            case 'lambda':
                return jme.isRandom(expr.tok.expr, scope);
            default:
                if(!expr.args) {
                    return false;
                }
                for(let i = 0;i < expr.args.length;i++) {
                    if(jme.isRandom(expr.args[i], scope)) {
                        return true;
                    }
                }
                return false;
        }
    },

    /** Is this a monomial - a single term of the form x^n or m*x^n, where m and n are numbers?
     *
     * @param {Numbas.jme.tree} tree
     * @returns {object} The base, degree and coefficient of the monomial, as trees.
     */
    isMonomial: function(tree) {
        /** Remove unary minuses from the top of the tree.
         *
         * @param {Numbas.jme.tree} tree
         * @returns {Numbas.jme.tree}
         */
        function unwrapUnaryMinus(tree) {
            while(jme.isOp(tree.tok, '-u')) {
                tree = tree.args[0];
            }
            return tree;
        }
        var coefficient;
        if(jme.isOp(tree.tok, '*')) {
            if(!jme.isType(unwrapUnaryMinus(tree.args[0]).tok, 'number')) {
                return false;
            }
            coefficient = tree.args[0];
            tree = tree.args[1];
        } else if(jme.isOp(tree.tok, '-u')) {
            coefficient = {tok:new TNum(-1)};
            tree = tree.args[0];
        } else {
            coefficient = {tok:new TNum(1)};
        }
        if(tree.tok.type == 'name') {
            return {base:tree, degree:{tok:new TInt(1)}, coefficient: coefficient};
        }
        if(jme.isOp(tree.tok, '^') && jme.isType(tree.args[0].tok, 'name') && jme.isType(unwrapUnaryMinus(tree.args[1]).tok, 'number')) {
            return {base:tree.args[0], degree:tree.args[1], coefficient: coefficient};
        }
        return false;
    },

    /**
     * Cast a list of arguments to match a function signature.
     *
     * @param {Array.<Numbas.jme.signature_grammar_match>} signature - A list of either types to cast to, or 'missing', representing a space that should be fillined in with 'nothing'.
     * @param {Array.<Numbas.jme.token>} args - The arguments to the function.
     * @returns {Array.<Numbas.jme.token>}
     */
    castArgumentsToSignature: function(signature, args) {
        var castargs = [];
        var j = 0;
        for(let i = 0;i < signature.length;i++) {
            if(signature[i].missing) {
                castargs.push(new TNothing());
                continue;
            }
            var arg = args[j];
            if(signature[i]) {
                castargs.push(jme.castToType(arg, signature[i]));
            } else {
                castargs.push(arg);
            }
            j += 1;
        }
        return castargs;
    }
};

/** Options for {@link Numbas.jme.Parser}
 *
 * @typedef {object} Numbas.jme.parser_options
 * @property {boolean} closeMissingBrackets - Silently ignore "missing right bracket" errors?
 * @property {boolean} addMissingArguments - When an op or function call is missing required arguments, insert `?` as a placeholder.
 */

/** A parser for {@link JME} expressions.
 *
 * @memberof Numbas.jme
 * @class
 *
 * @param {Numbas.jme.parser_options} options
 */
jme.Parser = function(options) {
    this.options = util.extend_object({}, this.option_defaults, options);
    this.ops = this.ops.slice();
    this.re = util.extend_object({}, this.re);
    this.tokeniser_types = this.tokeniser_types.slice();
    this.constants = {};
    this.prefixForm = {};
    this.postfixForm = {};
    this.arity = {};
    this.precedence = {};
    this.relations = {};
    this.commutative = {};
    this.associative = {};
    this.funcSynonyms = {};
    this.opSynonyms = {};
    this.rightAssociative = {};
    this.make_re();
}
jme.Parser.prototype = /** @lends Numbas.jme.Parser.prototype */ {
    /** Default options for new parsers.
     *
     * @type {Numbas.jme.parser_options}
     */
    option_defaults: {
        closeMissingBrackets: false,
        addMissingArguments: false
    },

    /** There are many dictionaries storing definitions of things like constants and alternate names, which are defined both globally in Numbas.jme and locally in a Parser.
     * This is a wrapper to load the value of the setting if it exists, and return `undefined` otherwise.
     *
     * @param {string} setting - The name of the dictionary. Both `this` and of `Numbas.jme` must have members with this name.
     * @param {string} name - The name of the setting to try to load from the dictionary.
     * @returns {*}
     */
    getSetting: function(setting, name) {
        if(name in this[setting]) {
            return this[setting][name];
        }
        if(name in jme[setting]) {
            return jme[setting][name];
        }
        return undefined;
    },

    /** If the given name is defined as a constant, return its value, otherwise return `undefined`.
     *
     * @param {string} name
     * @returns {number}
     */
    getConstant: function(name) {
        return this.getSetting('constants', name);
    },

    /** If the given operator name has a defined prefix form, return it, otherwise return `undefined`.
     *
     * @param {string} name
     * @returns {string}
     */
    getPrefixForm: function(name) {
        return this.getSetting('prefixForm', name);
    },

    /** If the given operator name has a defined postfix form, return it, otherwise return `undefined`.
     *
     * @param {string} name
     * @returns {string}
     */
    getPostfixForm: function(name) {
        return this.getSetting('postfixForm', name);
    },

    /** Get the arity of the given operator.
     *
     * @param {string} name
     * @returns {number}
     */
    getArity: function(name) {
        return this.getSetting('arity', name) || 2;
    },

    /** Get the precedence of the given operator.
     *
     * @param {string} name
     * @returns {number}
     */
    getPrecedence: function(name) {
        return this.getSetting('precedence', name);
    },

    /** Is the given operator a relation?
     *
     * @param {string} name
     * @returns {boolean}
     */
    isRelation: function(name) {
        return this.getSetting('relations', name) || false;
    },

    /** Is the given operator commutative?
     *
     * @param {string} name
     * @returns {boolean}
     */
    isCommutative: function(name) {
        return this.getSetting('commutative', name) || false;
    },

    /** Is the given operator associative?
     *
     * @param {string} name
     * @returns {boolean}
     */
    isAssociative: function(name) {
        return this.getSetting('associative', name) || false;
    },

    /** Is the given operator right-associative?
     *
     * @param {string} name
     * @returns {boolean}
     */
    isRightAssociative: function(name) {
        return this.getSetting('rightAssociative', name) || false;
    },

    /** If the given function name has a synonym, use it, otherwise return the original name.
     *
     * @see Numbas.jme.funcSynonyms
     * @param {string} name
     * @returns {string}
     */
    funcSynonym: function(name) {
        return this.getSetting('funcSynonyms', name) || name;
    },

    /** If the given operator name has a synonym, use it, otherwise return the original name.
     *
     * @see Numbas.jme.opSynonyms
     * @param {string} name
     * @returns {string}
     */
    opSynonym: function(name) {
        return this.getSetting('opSynonyms', name) || name;
    },

    /** Binary operations.
     *
     * @type {Array.<string>}
     */
    ops: ['not', 'and', 'or', 'xor', 'nand', 'nor', 'implies', 'isa', 'except', 'in', 'for:', 'of:', 'where:', 'divides', 'as', '..', '#', '<=', '>=', '<>', '&&', '||', '|', '*', '+', '-', '/', '^', '<', '>', '=', '!', '&', '|>'].concat(Object.keys(Numbas.unicode_mappings.symbols)),

    /** Superscript characters, and their normal-script replacements.
     *
     * @type {Array.<string>}
     */
    superscript_replacements: [
        Object.values(Numbas.unicode_mappings.superscripts).join(''),
        Object.keys(Numbas.unicode_mappings.superscripts).join('')
    ],

    /** Regular expressions to match tokens.
     *
     * @type {Object<RegExp>}
     */
    re: {
        re_bool: /^(true|false)(?![a-zA-Z_0-9'])/i,
        re_integer: /^\p{Nd}+(?!\.|\p{Nd})/u,
        re_number: /^\p{Nd}+(?:\.\p{Nd}+)?/u,
        re_name: new RegExp(
            "^" +
            "\\{?" + //optionally wrapped in curly braces
            "((?:(?:[\\p{Ll}\\p{Lu}\\p{Lo}\\p{Lt}]+):)*)" + // annotations
            "(" + // main name part
                "(?:" + // a string:
                    "\\$?" + // optional dollar sign prefix
                    "[\\p{Ll}\\p{Lu}\\p{Lo}\\p{Lt}_\\p{Pc}]" + // at least one letter or underscore
                    "[\\p{Ll}\\p{Lu}\\p{Lo}\\p{Lt}\\p{Nl}\\p{Nd}_\\p{Pc}]*" + // any number of letters, number symbols, or underscores
                    "[" + Object.keys(Numbas.unicode_mappings.subscripts).join('') + "]*" +  // any number of subscript characters
                    "'*" + // any number of primes
                ")" +
                "|" +   // or
                "\\?\\??" + // one or two question marks
                "|" +   // or
                "[π∞]" + // special name symbols
            ")" +
            "\\}?" // optional closing curly brace

        , 'iu'),

        re_string: util.re_jme_string,
        re_comment: /^\/\/.*?(?:\n|$)/,
        re_keypair: /^:/,
        re_lambda: /^(?:->|→)/u,

        /** A regular expression matching a string of subscript characters at the end of a name token.
         */
        re_subscript_character: new RegExp('[' + Object.keys(Numbas.unicode_mappings.subscripts).join('') + ']+$', 'u'),

        /** A regular expression matching a mathematical letter character.
         */
        re_math_letter: new RegExp('^[' + Object.keys(Numbas.unicode_mappings.letters).join('') + ']', 'u'),
    },

    /** Set properties for a given operator.
     *
     * @param {string} name - The name of the operator.
     * @param {Numbas.jme.operatorOptions} options
     */
    setOperatorProperties: function(name, options) {
        if(!options) {
            return;
        }
        if('precedence' in options) {
            this.precedence[name] = options.precedence;
        }
        if('synonyms' in options) {
            options.synonyms.forEach(function(synonym) {
                if(opSynonyms[synonym] === undefined) {
                    this.opSynonyms[synonym] = name;
                }
            });
        }
        if(options.rightAssociative) {
            this.rightAssociative[name] = true;
        }
        if(options.commutative) {
            this.commutative[name] = true;
        }
    },

    addTokenType: function(re, parse) {
        this.tokeniser_types.splice(0, 0, {re:re, parse:parse});
    },

    /** Add an operator to the parser.
     *
     * @param {string} name
     * @see Numbas.jme.Parser#addBinaryOperator
     * @see Numbas.jme.Parser#addPrefixOperator
     * @see Numbas.jme.Parser#addPostfixOperator
     */
    addOperator: function(name) {
        if(this.ops.contains(name)) {
            return;
        }
        this.ops.push(name);
        this.make_re();
    },

    /** Add a binary operator to the parser.
     *
     * @param {string} name
     * @param {Numbas.jme.operatorOptions} options
     */
    addBinaryOperator: function(name, options) {
        this.addOperator(name);
        this.setOperatorProperties(name, options);
    },

    /** Add a prefix operator to the parser.
     *
     * @param {string} name
     * @param {string} alt - The "interpreted" name of the operator, e.g. '!' is interpreted as 'fact'. If not given, the value of `name` is used.
     * @param {Numbas.jme.operatorOptions} options
     */
    addPrefixOperator: function(name, alt, options) {
        this.addOperator(name);
        alt = alt || name;
        this.prefixForm[name] = alt;
        this.arity[alt] = 1;
        this.setOperatorProperties(alt, options);
    },

    /** Add a postfix operator to the parser.
     *
     * @param {string} name
     * @param {string} alt - The "interpreted" name of the operator, e.g. '!' is interpreted as 'fact'. If not given, the value of `name` is used.
     * @param {Numbas.jme.operatorOptions} options
     */
    addPostfixOperator: function(name, alt, options) {
        this.addOperator(name);
        alt = alt || name;
        this.postfixForm[name] = alt;
        this.arity[alt] = 1;
        this.setOperatorProperties(alt, options);
    },

    /** Create an operator token with the given name.
     *
     * @param {string} name - The name of the operator.
     * @param {boolean} postfix - Is the operator postfix?
     * @param {boolean} prefix - Is the operator prefix?
     * @param {boolean} negated - Is this operator negated?
     * @returns {Numbas.jme.token}
     */
    op: function(name, postfix, prefix, negated) {
        var arity = this.getArity(name);
        var commutative = arity > 1 && this.isCommutative(name);
        var associative = arity > 1 && this.isAssociative(name);

        return new TOp(name, postfix, prefix, arity, commutative, associative, negated);
    },

    /** A dictionary mapping the descriptive tags in `Numbas.unicode_mappings.letters` to JME name annotations.
     */
    unicode_annotations: {
        'FRAKTUR': 'frak',
        'BLACK-LETTER': 'frak',
        'DOUBLE-STRUCK': 'bb',
        'MONOSPACE': 'tt',
        'SCRIPT': 'cal',
        'BOLD': 'bf',
    },

    /**
     * Normalise a name token, returning a name string and a list of annotations.
     * Don't confuse this with {@link Numbas.jme.normaliseName}, which applies scope-dependent normalisation, e.g. case-insensitivity, after parsing.
     *
     * @param {string} name
     * @returns {object}
     */
    normaliseName: function(name) {
        let annotations = [];
        let m;

        if(name.match(/^[a-zA-Z0-9_']*$/)) {
            return {name, annotations};
        }

        name = name.replace(/\p{Pc}/ug, (c) => c.normalize('NFKD'));

        let math_prefix = ''
        m = name.match(this.re.re_math_letter);
        while(m) {
            const letter = m[0];
            const [c, anns] = Numbas.unicode_mappings.letters[letter];
            name = name.slice(letter.length);
            annotations = annotations.merge(anns);
            math_prefix += c;
            m = name.match(this.re.re_math_letter);
        }
        annotations = annotations.map((a) => this.unicode_annotations[a]).filter((a) => a);
        name = math_prefix + name;

        for(const [k, v] of Object.entries(Numbas.unicode_mappings.greek)) {
            name = name.replaceAll(k, v);
        }

        name = name.replace(this.re.re_subscript_character, (m) => (name.match(/_/) ? '' : '_') + m.split('').map((c) => Numbas.unicode_mappings.subscripts[c]).join(''));

        return {name, annotations};
    },

    /** Normalise a string containing a single string literal, using the Unicode normalization algorithm NFKD.
     *
     * @param {string} literal
     * @returns {string}
     */
    normaliseNumber: function(literal) {
        return literal.normalize('NFKD');
    },

    /** Normalise a string containing a single punctuation character, using the Unicode normalization algorithm NFKD.
     *
     * @param {string} c
     * @returns {string}
     */
    normalisePunctuation: function(c) {
        c = c.normalize('NFKD');
        if(Numbas.unicode_mappings.brackets[c]) {
            c = Numbas.unicode_mappings.brackets[c][0];
        }
        return c;
    },

    /** Normalise a string containing a single operator name or symbol.
     *
     * @param {string} op
     * @returns {string}
     */
    normaliseOp: function(op) {
        op = op.replace(/\p{Pd}/gu, '-');
        if(Numbas.unicode_mappings.symbols[op]) {
            op = Numbas.unicode_mappings.symbols[op][0];
        }
        return jme.normaliseName(op, this.options);
    },

    /** Is this token an opening bracket, such as `(` or `[`?
     *
     * @param {Numbas.jme.token} tok
     * @returns {boolean}
     */
    is_opening_bracket: function(tok) {
        return tok.type.match(/^\p{Ps}$/u);
    },

    /** Is this token a closing bracket, such as `(` or `[`?
     *
     * @param {Numbas.jme.token} tok
     * @returns {boolean}
     */
    is_closing_bracket: function(tok) {
        return tok.type.match(/^\p{Pe}$/u);
    },

    /** Descriptions of kinds of token that the tokeniser can match.
     * `re` is a regular expression matching the token.
     * `parse` is a function which takes a RegEx match object, the tokens produced up to this point, the input expression, and the current position in the expression.
     * It should return an object `{tokens, start, end}`.
     */
    tokeniser_types: [
        {
            re: 're_strip_whitespace',
            parse: function(result, tokens, expr, pos) {
                return {tokens: [], start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_comment',
            parse: function(result, tokens, expr, pos) {
                return {tokens: [], start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_integer',
            parse: function(result, tokens, expr, pos) {
                const literal = this.normaliseNumber(result[0]);
                var token = new TInt(literal);
                var new_tokens = [token];
                if(tokens.length > 0) {
                    var prev = tokens.at(-1);
                    if(jme.isType(prev, ')') || jme.isType(prev, 'name') || (jme.isType(prev, 'op') && prev.postfix)) {    //right bracket, name or postfix op followed by a number is interpreted as multiplying contents of brackets by number
                        new_tokens.splice(0, 0, this.op('*'));
                    }
                }
                return {tokens: new_tokens, start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_number',
            parse: function(result, tokens, expr, pos) {
                const literal = this.normaliseNumber(result[0]);
                var token = new TNum(literal);
                token.precisionType = 'dp';
                token.precision = math.countDP(literal);
                var new_tokens = [token];
                if(tokens.length > 0) {
                    var prev = tokens.at(-1);
                    if(jme.isType(prev, ')') || jme.isType(prev, 'name') || (jme.isType(prev, 'op') && prev.postfix)) {    //right bracket, name or postfix op followed by a number is interpreted as multiplying contents of brackets by number
                        new_tokens.splice(0, 0, this.op('*'));
                    }
                }
                return {tokens: new_tokens, start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_bool',
            parse: function(result, tokens, expr, pos) {
                var token = new TBool(util.parseBool(result[0]));
                return {tokens: [token], start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_lambda',
            parse: function(result, tokens, expr, pos) {
                var token = new TLambda();
                return {tokens: [token], start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_op',
            parse: function(result, tokens, expr, pos) {
                var matched_name = result[0];
                var name = this.normaliseOp(matched_name);
                var m = name.match(/^not (\w+)$/);
                var negated = false;
                if(m) {
                    name = m[1];
                    negated = true;
                }
                var nt;
                var postfix = false;
                var prefix = false;
                name = this.opSynonym(name);
                if(tokens.length == 0 || this.is_opening_bracket(nt = tokens.at(-1)) || nt.type == ',' || nt.type == 'lambda' || (nt.type == 'op' && !nt.postfix) || nt.type == 'keypair') {
                    var prefixForm = this.getPrefixForm(name);
                    if(prefixForm !== undefined) {
                        name = prefixForm;
                        prefix = true;
                    }
                } else {
                    var postfixForm = this.getPostfixForm(name);
                    if(postfixForm !== undefined) {
                        name = postfixForm;
                        postfix = true;
                    }
                }
                var token = this.op(name, postfix, prefix, negated);
                return {tokens: [token], start: pos, end: pos + matched_name.length};
            }
        },
        {
            re: 're_name',
            parse: function(result, tokens, expr, pos) {
                const {name, annotations} = this.normaliseName(result[2]);
                var annotation = result[1] ? result[1].split(':').slice(0, -1) : null;
                annotation = annotation === null ? annotations.length ? annotations : null : annotation.concat(annotations);
                var token;
                if(!annotation) {
                    token = new TName(name);
                } else {
                    token = new TName(name, annotation);
                }
                var new_tokens = [token];
                if(tokens.length > 0) {
                    var prev = tokens.at(-1);
                    if(jme.isType(prev, 'number') || jme.isType(prev, 'name') || jme.isType(prev, ')') || (jme.isType(prev, 'op') && prev.postfix)) {    //number, right bracket, name or postfix op followed by a name, eg '3y', is interpreted to mean multiplication, eg '3*y'
                        new_tokens.splice(0, 0, this.op('*'));
                    }
                }
                return {tokens: new_tokens, start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_string',
            parse: function(result, tokens, expr, pos) {
                var str = result[2];
                var token = new TString(jme.unescape(str));
                return {tokens: [token], start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_superscript',
            parse: function(result, tokens, expr, pos) {
                var normals = this.superscript_replacements[0];
                var superscripts = this.superscript_replacements[1];
                var n = result[0].replace(/./g, function(d) {
                    return normals[superscripts.indexOf(d)];
                });
                const otokens = this.tokenise(n);
                return {tokens: [this.op('^'), new TPunc('(')].concat(otokens).concat([new TPunc(')')]), start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_punctuation',
            parse: function(result, tokens, expr, pos) {
                var c = this.normalisePunctuation(result[0]);
                var new_tokens = [new TPunc(c)];
                if(c == '(' && tokens.length > 0) {
                    var prev = tokens.at(-1);
                    if(jme.isType(prev, 'number') || jme.isType(prev, ')') || (jme.isType(prev, 'op') && prev.postfix)) {    //number, right bracket or postfix op followed by left parenthesis is also interpreted to mean multiplication
                        new_tokens.splice(0, 0, this.op('*'));
                    }
                }
                return {tokens: new_tokens, start: pos, end: pos + result[0].length};
            }
        },
        {
            re: 're_keypair',
            parse: function(result, tokens, expr, pos) {
                if(tokens.length == 0 || !(tokens.at(-1).type == 'string' || tokens.at(-1).type == 'name')) {
                    throw(new Numbas.Error('jme.tokenise.keypair key not a string', {type: tokens.at(-1).type}));
                }
                var token = new TKeyPair(tokens.pop().value);
                return {tokens: [token], start: pos, end: pos + result[0].length};
            }
        },
    ],


    /** Update regular expressions for matching tokens.
     *
     * @see Numbas.jme.Parser#re
     */
    make_re: function() {
        /** Put operator symbols in reverse length order (longest first), and escape regex punctuation.
         *
         * @param {Array.<string>} ops
         * @returns {Array.<string>} ops
         */
        function clean_ops(ops) {
            return ops.sort().reverse().map(function(op) {
                return op.replace(/[.?*+^$[\]\\(){}|]/g, "\\$&");
            });
        };
        var word_ops = clean_ops(this.ops.filter(function(o) {
            return o.match(/[a-zA-Z0-9_']$/);
        }));
            var other_ops = clean_ops(this.ops.filter(function(o) {
                return !o.match(/[a-zA-Z0-9_']$/);
            }));
        var any_op_bits = [];
        if(word_ops.length) {
            any_op_bits.push('(?:' + word_ops.join('|') + ')(?![a-zA-Z0-9_\'])');
        }
        if(other_ops.length) {
            any_op_bits.push('(?:' + other_ops.join('|') + ')');
        }
        this.re.re_op = new RegExp('^(?:' + any_op_bits.join('|') + '|\\p{Pd})', 'iu');

        this.re.re_superscript = new RegExp('^[' + this.superscript_replacements[1] + ']+', 'u');

        this.re.re_punctuation = new RegExp('^(?!["\'.])([,\\[\\]\\p{Ps}\\p{Pe}])', 'u');
    },

    /** Convert given expression string to a list of tokens. Does some tidying, e.g. inserts implied multiplication symbols.
     *
     * @param {JME} expr
     * @returns {Array.<Numbas.jme.token>}
     * @see Numbas.jme.Parser#compile
     */
    tokenise: function(expr) {
        if(!expr) {
            return [];
        }
        expr += '';
        var pos = 0;
        var tokens = [];
        while(pos < expr.length) {
            var got = false;
            for(let i = 0;i < this.tokeniser_types.length;i++) {
                var tt = this.tokeniser_types[i];
                var regex = (tt.re instanceof RegExp) ? tt.re : this.re[tt.re];
                var m = expr.slice(pos).match(regex);
                if(m) {
                    var result = tt.parse.apply(this, [m, tokens, expr, pos]);
                    result.tokens.forEach(function(t) {
                        t.pos = result.start;
                    });
                    pos = result.end;
                    tokens.push.apply(tokens, result.tokens);
                    got = true;
                    break;
                }
            }
            if(!got && pos < expr.length) {
                var nearby = expr.slice(Math.max(0, pos), pos + 5);
                throw(new Numbas.Error('jme.tokenise.invalid near', {expression: expr, position: pos, nearby: nearby}));
            }
        }
        return tokens;
    },

    shunt_type_actions: {
        'number': function(tok) {
            this.addoutput(tok);
        },
        'integer': function(tok) {
            this.addoutput(tok);
        },
        'string': function(tok) {
            this.addoutput(tok);
        },
        'boolean': function(tok) {
            this.addoutput(tok);
        },
        'name': function(tok) {
            var i = this.i;
            // if followed by an open bracket, this is a function application
            if(i < this.tokens.length - 1 && this.tokens[i + 1].type == "(") {
                var name = this.funcSynonym(tok.nameWithoutAnnotation);
                var ntok = new TFunc(name, tok.annotation);
                ntok.pos = tok.pos;
                this.addstack(ntok);
            } else {
                //this is a variable otherwise
                this.addoutput(tok);
            }
        },
        ',': function(tok) {
            if(this.is_opening_bracket(this.tokens.at(this.i-1))) {
                throw(new Numbas.Error('jme.shunt.expected argument before comma'));
            }
            //reached end of expression defining function parameter, so pop all of its operations off stack and onto output
            while(this.stack.length > 0 && !this.is_opening_bracket(this.stack.at(-1))) {
                this.addoutput(this.popstack())
            }
            this.numvars[this.numvars.length - 1]++;
            if(! this.stack.length) {
                throw(new Numbas.Error('jme.shunt.no left bracket in function'));
            }
        },
        'op': function(tok) {
            if(tok.name == '*' && this.output.at(-1)?.tok.type == 'lambda' && !this.output.at(-1)?.args) {
                this.addstack(this.popoutput().tok);
                this.numvars.push(0);
                return;
            }

            if(!tok.prefix) {
                var o1 = this.getPrecedence(tok.name);
                //while ops on stack have lower precedence, pop them onto output because they need to be calculated before this one. left-associative operators also pop off operations with equal precedence

                /** Should the next token on the stack be popped off?
                 *
                 * @returns {boolean}
                 */
                function should_pop() {
                    if(this.stack.length == 0) {
                        return false;
                    }
                    var prev = this.stack.at(-1);
                    if(prev.type == "op" && ((o1 > this.getPrecedence(prev.name)) || (!this.isRightAssociative(tok.name) && o1 == this.getPrecedence(prev.name)))) {
                        return true;
                    }
                    if(prev.type == 'keypair' && prev.pairmode == 'match') {
                        return true;
                    }
                    return false;
                }
                while(should_pop.apply(this)) {
                    this.addoutput(this.popstack());
                }
            }
            this.addstack(tok);
        },
        '[': function(tok) {
            var i = this.i;
            var tokens = this.tokens;
            var last_token = i == 0 ? null : tokens[i - 1].type;
            if(i == 0 || this.is_opening_bracket(tokens.at(i-1)) || last_token == ',' || last_token == 'op' || last_token == 'keypair' || last_token == 'lambda') {
                this.listmode.push('new');
            } else {
                this.listmode.push('index');
            }
            this.addstack(tok);
            this.numvars.push(0);
        },
        ']': function(tok) {
            while(this.stack.length > 0 && this.stack.at(-1).type != "[") {
                this.addoutput(this.popstack());
            }
            if(this.tokens[this.i - 1].type != ',' && this.tokens[this.i - 1].type != '[') {
                this.numvars[this.numvars.length - 1] += 1;
            }
            if(! this.stack.length) {
                throw(new Numbas.Error('jme.shunt.no left square bracket'));
            } else {
                this.popstack();    //get rid of left bracket
            }
            //work out size of list
            var n = this.numvars.pop();
            switch(this.listmode.pop()) {
            case 'new':
                var ntok = new TList(n);
                ntok.pos = tok.pos;
                this.addoutput(ntok)
                break;
            case 'index':
                var f = new TFunc('listval');
                f.pos = tok.pos;
                f.vars = 2;
                this.addoutput(f);
                break;
            }
        },
        '(': function(tok) {
            this.addstack(tok);
            this.numvars.push(0);
        },
        ')': function(tok) {
            while(this.stack.length > 0 && this.stack.at(-1)?.type != "(") {
                this.addoutput(this.popstack());
            }
            if(! this.stack.length) {
                throw(new Numbas.Error('jme.shunt.no left bracket'));
            }
            this.popstack();    //get rid of left bracket

            //work out number of items between the brackets
            var n = this.numvars.pop();
            if(this.tokens[this.i - 1].type != ',' && this.tokens[this.i - 1].type != '(') {
                n += 1;
            }

            //if this is a function call, then the next thing on the stack should be a function name, which we need to pop
            if(this.stack.length > 0 && this.stack.at(-1)?.type == "function" || (this.stack.at(-1)?.type == "lambda" && this.stack.at(-1)?.names !== undefined && (this.i == this.tokens.length - 1 || !jme.isOp(this.tokens[this.i + 1], '*')))) {
                var f = this.popstack();
                f.vars = n;
                this.addoutput(f);
            //if this is the list of argument names for an anonymous function, add them to the lambda token, which is next.
            } else if(this.i < this.tokens.length - 1 && this.tokens[this.i + 1].type == 'lambda') {
                var names = this.output.splice(this.output.length - n, n);
                var lambda = this.tokens[this.i + 1];
                lambda.set_names(names);
                lambda.vars = 1;
            } else if(this.output.length) {
                this.output.at(-1).bracketed = true;
            }
        },
        'keypair': function(tok) {
            var pairmode = null;
            for(let i = this.stack.length - 1;i >= 0;i--) {
                if(this.stack[i].type == '[' || jme.isFunction(this.stack[i], 'dict')) {
                    pairmode = 'dict';
                    break;
                } else if(jme.isOp(this.stack[i], ';')) {
                    pairmode = 'match';
                    break;
                } else if(this.stack[i].type == '(' && (this.stack.length == 1 || !jme.isFunction(this.stack[i - 1], 'dict'))) {
                    break;
                }
            }
            if(pairmode === null) {
                throw(new Numbas.Error('jme.shunt.keypair in wrong place'));
            }
            tok.pairmode = pairmode;
            this.addstack(tok);
        },
        'lambda': function(tok) {
            this.addstack(tok);
        }
    },

    addoutput: function(tok) {
        if(tok.vars !== undefined) {
            if(this.output.length < tok.vars) {
                // Not enough terms have been output for this operation
                if(!this.options.addMissingArguments) {
                    throw(new Numbas.Error('jme.shunt.not enough arguments', {op:tok.name || tok.type}));
                } else {
                    for(let i = this.output.length;i < tok.vars;i++) {
                        var tvar = new types.TName('?');
                        tvar.added_missing = true;
                        this.output.push({tok:tvar});
                    }
                }
            }

            var thing = {
                tok: tok,
                args: this.output.splice(this.output.length - tok.vars, tok.vars)
            };

            if(tok.type == 'lambda') {
                if(tok.expr === undefined) {
                    if(tok.names == undefined) {
                        tok.set_names([thing.args[0]]);
                    }
                    tok.set_expr(thing.args[tok.vars - 1]);
                    thing = {tok: tok};
                }
            }

            if(tok.type == 'list') {
                // If this is a list of keypairs, construct a dictionary instead
                var mode = null;
                for(let i = 0;i < thing.args.length;i++) {
                    var argmode = thing.args[i].tok.type == 'keypair' ? 'dictionary' : 'list';
                    if(i > 0 && argmode != mode) {
                        throw(new Numbas.Error('jme.shunt.list mixed argument types', {mode: mode, argmode: argmode}));
                    }
                    mode = argmode;
                }
                if(mode == 'dictionary') {
                    thing.tok = new TDict();
                }
            }
            if(tok.type == 'op' && this.isRelation(tok.name)) {
                // Rewrite chained relations: e.g. `a<b<c` to `a<b and b<c`
                var lhs = thing.args[0];
                var ltop = lhs;

                while(jme.isOp(ltop.tok, 'and')) {
                    ltop = ltop.args[1];
                }

                var lbottom = ltop;
                while(lbottom.tok.type == 'op' && this.isRelation(lbottom.tok.name)) {
                    lbottom = lbottom.args[1];
                }

                var rhs = thing.args[1];
                var rtop = rhs;

                while(jme.isOp(rtop.tok, 'and')) {
                    rtop = rtop.args[0];
                }

                var rbottom = rtop;
                while(rbottom.tok.type == 'op' && this.isRelation(rbottom.tok.name)) {
                    rbottom = rbottom.args[0];
                }

                /** Create a binary operation tree with the given token, and left and right arguments.
                 *
                 * @param {Numbas.jme.token} tok
                 * @param {Numbas.jme.tree} lhs
                 * @param {Numbas.jme.tree} rhs
                 * @returns {Numbas.jme.tree}
                 */
                function bin(tok, lhs, rhs) {
                    if(!tok.pos) {
                        tok.pos = lhs.tok.pos;
                    }
                    return {tok: tok, args: [lhs, rhs]};
                }

                if(lbottom != ltop) {
                    if(rbottom != rtop) {
                        thing = bin(this.op('and'), bin(this.op('and'), lhs, bin(tok, lbottom, rbottom)), rhs);
                    } else {
                        thing = bin(this.op('and'), lhs, bin(tok, lbottom, rhs));
                    }
                } else if(rbottom != rtop) {
                    thing = bin(this.op('and'), bin(tok, lhs, rbottom), rhs);
                }
            }
            if(thing.tok.type == 'op' && thing.tok.negated) {
                thing.tok.negated = false;
                thing = {tok:this.op('not', false, true), args: [thing]};
            }
            if(thing.tok.type == 'op' && thing.tok.name == '|>') {
                if(thing.args[1].tok.type == 'lambda') {
                    thing = {
                        tok: thing.args[1].tok,
                        args: [thing.args[0]]
                    };
                } else if(thing.args[1].args === undefined) {
                    throw(new Numbas.Error("jme.shunt.pipe right hand takes no arguments"));
                } else {
                    thing = {
                        tok: thing.args[1].tok,
                        args: [thing.args[0]].concat(thing.args[1].args)
                    };
                }
            }

            if(thing.tok.type == 'lambda') {
                thing.tok.vars = thing.tok.names.length;
            }
            this.output.push(thing);
        } else {
            this.output.push({tok:tok});
        }
    },

    popoutput: function() {
        const tree = this.output.pop();
        return tree;
    },

    addstack: function(tok) {
        this.stack.push(tok);
    },

    popstack: function() {
        const tok = this.stack.pop();
        return tok;
    },

    /** Shunt list of tokens into a syntax tree. Uses the shunting yard algorithm.
     *
     * @param {Array.<Numbas.jme.token>} tokens
     * @returns {Numbas.jme.tree}
     * @see Numbas.jme.Parser#tokenise
     * @see Numbas.jme.Parser#compile
     */
    shunt: function(tokens) {
        var parser = this;

        this.tokens = tokens;
        this.output = [];
        this.stack = [];
        this.numvars = [];
        this.listmode = [];


        var type_actions = this.shunt_type_actions;

        /** Shunt the given token onto the output.
         *
         * @param {Numbas.jme.token} tok
         * @see Numbas.jme.Parser.shunt_type_actions
         */
        function shunt_token(tok) {
            if(tok.type in type_actions) {
                type_actions[tok.type].apply(parser, [tok]);
            }
        }
        for(this.i = 0; this.i < tokens.length; this.i++) {
            var tok = tokens[this.i];
            shunt_token(tok);
        }
        //pop all remaining ops on stack into output
        while(this.stack.length) {
            var x = this.stack.at(-1);
            if(x.type == "(") {
                if(!this.options.closeMissingBrackets) {
                    throw(new Numbas.Error('jme.shunt.no right bracket'));
                } else {
                    type_actions[')'].apply(this);
                }
            } else {
                this.popstack();
                this.addoutput(x);
            }
        }
        if(this.listmode.length > 0) {
            throw(new Numbas.Error('jme.shunt.no right square bracket'));
        }
        if(this.output.length > 1) {
            throw(new Numbas.Error('jme.shunt.missing operator'));
        }
        return this.output[0];
    },

    /** Compile an expression string to a syntax tree. (Runs {@link Numbas.jme.tokenise} then {@Link Numbas.jme.shunt}).
     *
     * @param {JME} expr
     * @see Numbas.jme.Parser#tokenise
     * @see Numbas.jme.Parser#shunt
     * @returns {Numbas.jme.tree}
     */
    compile: function(expr) {
        //make sure expression is a string and not a number or anything like that
        expr += '';
        if(!expr.trim().length) {
            return null;
        }
        //tokenise expression
        var tokens = this.tokenise(expr);
        //compile to parse tree
        var tree = this.shunt(tokens);
        if(tree === null) {
            return undefined;
        }
        return tree;
    },
}
/** Regular expression to match whitespace (because '\s' doesn't match *everything*) */
jme.Parser.prototype.re.re_whitespace = '(?:\\p{White_Space}|(?:&nbsp;))';
jme.Parser.prototype.re.re_strip_whitespace = new RegExp('^' + jme.Parser.prototype.re.re_whitespace + '+', 'u');

/** Regular expressions for parser tokens.
 * Included for backwards-compatibility.
 *
 * @type {Object<RegExp>}
 * @see Numbas.jme.Parser#re
 */
jme.re = jme.Parser.prototype.re;

var fnSort = util.sortBy('id');
/** Options for the {@link Numbas.jme.funcObj} constructor.
 *
 * @typedef {object} Numbas.jme.scope_deletions
 * @property {object} variables - Names of deleted variables.
 * @property {object} functions - Names of deleted functions.
 * @property {object} rulesets - Names of deleted rulesets.
 */

/**
 * A JME evaluation environment.
 * Stores variable, function, and ruleset definitions.
 *
 * A scope may have a parent; elements of the scope are resolved by searching up through the hierarchy of parents until a match is found.
 *
 * @memberof Numbas.jme
 * @class
 * @property {Object<Numbas.jme.token>} variables - Dictionary of variables defined **at this level in the scope**. To resolve a variable in the scope, use {@link Numbas.jme.Scope.getVariable}.
 * @property {Object<Array.<Numbas.jme.funcObj>>} functions - Dictionary of functions defined at this level in the scope. Function names map to lists of functions: there can be more than one function for each name because of multiple dispatch. To resolve a function name in the scope, use {@link Numbas.jme.Scope.getFunction}.
 * @property {Object<Numbas.jme.rules.Ruleset>} rulesets - Dictionary of rulesets defined at this level in the scope. To resolve a ruleset in the scope, use {@link Numbas.jme.Scope.getRuleset}.
 * @property {Numbas.jme.scope_deletions} deleted - Names of deleted variables/functions/rulesets.
 * @property {Numbas.Question} question - The question this scope belongs to.
 *
 * @param {Numbas.jme.Scope[]} scopes - Either: nothing, in which case this scope has no parents; a parent Scope object; a list whose first element is a parent scope, and the second element is a dictionary of extra variables/functions/rulesets to store in this scope.
 */
var Scope = jme.Scope = function(scopes) {
    var s = this;
    this.parser = jme.standardParser;
    this.constants = {};
    this.variables = {};
    this.functions = {};
    this._resolved_functions = {};
    this.rulesets = {};
    this.deleted = {
        constants: {},
        variables: {},
        functions: {},
        rulesets: {}
    }
    if(scopes === undefined) {
        return;
    }
    if(!Array.isArray(scopes)) {
        scopes = [scopes, undefined];
    }
    this.question = scopes[0].question || this.question;
    var extras;
    if(!scopes[0].evaluate) {
        extras = scopes[0];
    } else {
        this.parent = scopes[0];
        this.parser = this.parent.parser;
        this.caseSensitive = this.parent.caseSensitive;
        extras = scopes[1] || {};
    }
    if(extras) {
        if(extras.constants) {
            for(const [k, v] of Object.entries(extras.constants)) {
                this.setConstant(k, v);
            }
        }
        if(extras.variables) {
            for(const [k, v] of Object.entries(extras.variables)) {
                this.setVariable(k, v);
            }
        }
        if(extras.rulesets) {
            for(const [k, v] of Object.entries(extras.rulesets)) {
                this.addRuleset(k, v);
            }
        }
        if(extras.functions) {
            for(const fns of Object.values(extras.functions)) {
                fns.forEach(function(fn) {
                    s.addFunction(fn);
                });
            }
        }
        if(extras.caseSensitive !== undefined) {
            s.caseSensitive = extras.caseSensitive;
        }
    }
    return;
}
Scope.prototype = /** @lends Numbas.jme.Scope.prototype */ {
    /** Parser to use when compiling expressions.
     *
     * @type {Numbas.jme.Parser}
     */
    parser: jme.standardParser,

    /** Set the given constant name.
     *
     * @param {string} name
     * @param {Numbas.jme.constant_definition} data
     */
    setConstant: function(name, data) {
        data = {
            name: name,
            value: data.value,
            tex: data.tex || name
        };
        name = jme.normaliseName(name, this);
        this.constants[name] = data;
        this.deleted.constants[name] = false;
    },

    /** Set the given variable name.
     *
     * @param {string} name
     * @param {Numbas.jme.token} value
     */
    setVariable: function(name, value) {
        name = jme.normaliseName(name, this);
        this.variables[name] = value;
        this.deleted.variables[name] = false;
    },
    /** Add a JME function to the scope.
     *
     * @param {Numbas.jme.funcObj} fn - Function to add.
     * @returns {Numbas.jme.funcObj} - The function.
     */
    addFunction: function(fn) {
        var name = jme.normaliseName(fn.name, this);
        if(!(name in this.functions)) {
            this.functions[name] = [fn];
        } else {
            this.functions[name].push(fn);
            delete this._resolved_functions[name];
        }
        this.deleted.functions[name] = false;
        return fn;
    },
    /** Add a ruleset to the scope.
     *
     * @param {string} name
     * @param {Numbas.jme.rules.Ruleset} set
     */
    addRuleset: function(name, set) {
        this.rulesets[name] = set;
        this.deleted.rulesets[name] = false;
    },
    /** Mark the given constant name as deleted from the scope.
     *
     * @param {string} name
     */
    deleteConstant: function(name) {
        name = jme.normaliseName(name, this);
        this.deleted.constants[name] = true;
    },
    /** Mark the given variable name as deleted from the scope.
     *
     * @param {string} name
     * @param {{delete_constant: boolean}} options
     */
    deleteVariable: function(name, options) {
        options = options || {};
        name = jme.normaliseName(name, this);
        this.deleted.variables[name] = true;
        if(options.delete_constant !== false) {
            this.deleted.constants[name] = true;
        }
    },
    /** Mark the given function name as deleted from the scope.
     *
     * @param {string} name
     */
    deleteFunction: function(name) {
        name = jme.normaliseName(name, this);
        this.deleted.functions[name] = true;
    },
    /** Mark the given ruleset name as deleted from the scope.
     *
     * @param {string} name
     */
    deleteRuleset: function(name) {
        name = jme.normaliseName(name, this);
        this.deleted.rulesets[name] = true;
    },
    /** Get the object with given name from the given collection.
     *
     * @param {string} collection - The name of the collection. A property of this Scope object, i.e. one of `constants`, `variables`, `functions`, `rulesets`.
     * @param {string} name - The name of the object to retrieve.
     * @returns {object}
     */
    resolve: function(collection, name) {
        var scope = this;
        while(scope) {
            var sname = jme.normaliseName(name, scope);
            if(scope.deleted[collection][sname]) {
                return undefined;
            }
            if(scope[collection][sname] !== undefined) {
                return scope[collection][sname];
            }
            scope = scope.parent;
        }
        return undefined;
    },
    /** Find the value of the variable with the given name, if it's defined.
     *
     * @param {string} name
     * @returns {Numbas.jme.token}
     */
    getConstant: function(name) {
        return this.resolve('constants', name);
    },

    /** If the given value is equal to one of the constant defined in this scope, return the constant.
     *
     * @param {Numbas.jme.token} value
     * @returns {object}
     */
    isConstant: function(value) {
        for(const [k, v] of Object.entries(this.constants)) {
            if(!this.deleted.constants[k]) {
                if(util.eq(value, v.value, this)) {
                    return v;
                }
            }
        }
        if(this.parent) {
            return this.parent.isConstant(value);
        }
        return undefined;
    },
    /** Find the value of the variable with the given name, if it's defined.
     *
     * @param {string} name
     * @returns {Numbas.jme.token}
     */
    getVariable: function(name) {
        return this.resolve('variables', name);
    },
    /** Get all definitions of the given function name.
     *
     * @param {string} name
     * @returns {Numbas.jme.funcObj[]} A list of all definitions of the given name.
     */
    getFunction: function(name) {
        name = jme.normaliseName(name, this);
        if(jme.funcSynonyms[name]) {
            name = jme.funcSynonyms[name];
        }
        if(!this._resolved_functions[name]) {
            var scope = this;
            var o = [];
            while(scope) {
                if(scope.functions[name] !== undefined) {
                    o = o.merge(scope.functions[name], fnSort);
                }
                scope = scope.parent;
            }
            this._resolved_functions[name] = o;
        }
        return this._resolved_functions[name];
    },

    /** Get the definition of the function with the given name which matches the types of the given arguments.
     *
     * @param {Numbas.jme.token} tok - The token of the function or operator.
     * @param {Array.<Numbas.jme.token>} args
     * @returns {Numbas.jme.call_signature}
     */
    matchFunctionToArguments: function(tok, args) {
        var op = jme.normaliseName(tok.name, this);
        var fns = this.getFunction(op);
        if(fns.length == 0) {
            if(tok.type == 'function') {
                //check if the user typed something like xtan(y), when they meant x*tan(y)
                var possibleOp = op.slice(1);
                if(op.length > 1 && this.getFunction(possibleOp).length) {
                    throw(new Numbas.Error('jme.typecheck.function maybe implicit multiplication', {name:op, first:op[0], possibleOp:possibleOp}));
                } else {
                    throw(new Numbas.Error('jme.typecheck.function not defined', {op:op, suggestion:op}));
                }
            } else {
                throw(new Numbas.Error('jme.typecheck.op not defined', {op:op}));
            }
        }

        /** Represent the difference between an input token and the description of the desired type returned by a signature checker.
         *
         * @param {Numbas.jme.token} tok
         * @param {Numbas.jme.signature_result_argument} typeDescription
         * @returns {Array.<string>} - The difference between the input argument and any of its child tokens, and the type described by `typeDescription`.
         */
        function type_difference(tok, typeDescription) {
            if(tok.type != typeDescription.type) {
                return [typeDescription.type];
            }
            var out = [typeDescription.nonspecific ? tok.type : null];
            switch(typeDescription.type) {
                case 'list':
                    if(typeDescription.items) {
                        var items = sig_remove_missing(typeDescription.items);
                        for(let i = 0;i < tok.value.length;i++) {
                            out = out.concat(type_difference(tok.value[i], items[i]));
                        }
                    }
            }
            return out;
        }

        /** Compare two function matches. A match is sorted earlier if, considering each argument in turn:
         * - it's more specific about a argument whose type is a collection;
         * - it matches the type of the corresponding argument exactly;
         * - the type it casts to is preferred over the other match's (occurs earlier in the input token's list of casts).
         *
         * @param {Numbas.jme.signature_result} m1
         * @param {Numbas.jme.signature_result} m2
         * @returns {number}
         */
        function compare_matches(m1, m2) {
            m1 = sig_remove_missing(m1);
            m2 = sig_remove_missing(m2);
            for(let i = 0;i < args.length;i++) {
                var d1 = type_difference(args[i], m1[i]);
                var d2 = type_difference(args[i], m2[i]);
                for(let j = 0;j < d1.length && j < d2.length;j++) {
                    if(j >= d1.length) {
                        return 1;
                    } else if(j >= d2.length) {
                        return -1;
                    }
                    if(d1[j] === null) {
                        if(d2[j] === null) {
                            continue;
                        } else {
                            return -1;
                        }
                    } else {
                        if(d2[j] === null) {
                            return 1;
                        } else {
                            if(args[i].casts) {
                                var casts = Object.keys(args[i].casts);
                                var i1 = casts.indexOf(d1[j]);
                                if(i1 == -1) {
                                    i1 = Infinity;
                                }
                                var i2 = casts.indexOf(d2[j]);
                                if(i2 == -1) {
                                    i2 = Infinity;
                                }
                                if(i1 != i2) {
                                    return i1 < i2 ? -1 : 1;
                                }
                            }
                            continue;
                        }
                    }
                }
            }
            return 0;
        }
        var candidate = null;
        for(let j = 0;j < fns.length; j++) {
            var fn = fns[j];
            if(fn.typecheck(args)) {
                var match = fn.intype(args);

                /** Does this match exactly describe the type of the given items?
                 *
                 * @param {Numbas.jme.signature_result} match
                 * @param {Array.<Numbas.jme.token>} items
                 * @returns {boolean}
                 */
                function exactType(match, items) {
                    var k = 0;
                    return match.every(function(m, i) {
                        if(m.missing) {
                            return false;
                        }
                        var ok = items[k] && items[k].type == m.type;
                        if(ok) {
                            if(m.items && items[k].type == 'list') {
                                ok = exactType(m.items, items[k].value);
                            }
                        }
                        k += 1;
                        return ok;
                    });
                }
                var exact_match = exactType(match, args);
                if(exact_match) {
                    return {fn: fn, signature: match};
                }
                var pcandidate = {fn: fn, signature: match};
                if(candidate === null || compare_matches(pcandidate.signature, candidate.signature) == -1) {
                    candidate = pcandidate;
                }
            }
        }
        return candidate;
    },
    /** Get the ruleset with the gien name.
     *
     * @param {string} name
     * @returns {Numbas.jme.rules.Ruleset}
     */
    getRuleset: function(name) {
        return this.resolve('rulesets', name);
    },
    /** Set the given ruleset name.
     *
     * @param {string} name
     * @param {Numbas.jme.rules.Ruleset[]} rules
     */
    setRuleset: function(name, rules) {
        name = jme.normaliseName(name, this);
        this.rulesets[name] = rules;
        this.deleted.rulesets[name] = false;
    },
    /** Collect together all items from the given collection.
     *
     * @param {string} collection - The name of the collection. A property of this Scope object, i.e. one of `variables`, `functions`, `rulesets`.
     * @returns {object} a dictionary of names to values
     */
    collect: function(collection) {
        var scope = this;
        var deleted = {};
        var out = {};
        var name;
        while(scope) {
            for(const name of Object.keys(scope.deleted[collection])) {
                deleted[name] = scope.deleted[collection][name] || deleted[name];
            }
            for(name in scope[collection]) {
                if(!deleted[name]) {
                    out[name] = out[name] || scope[collection][name];
                }
            }
            scope = scope.parent;
        }
        return out;
    },
    /** Gather all variables defined in this scope.
     *
     * @returns {Object<Numbas.jme.token>} A dictionary of variables.
     */
    allConstants: function() {
        return this.collect('constants');
    },
    /** Gather all variables defined in this scope.
     *
     * @returns {Object<Numbas.jme.token>} A dictionary of variables.
     */
    allVariables: function() {
        return this.collect('variables');
    },
    /** Gather all rulesets defined in this scope.
     *
     * @returns {Object<Numbas.jme.rules.Ruleset>} A dictionary of rulesets.
     */
    allRulesets: function() {
        return this.collect('rulesets');
    },
    /** Gather all functions defined in this scope.
     *
     * @returns {Object<Numbas.jme.funcObj[]>} A dictionary of function definitions: each name maps to a list of @link{Numbas.jme.funcObj}.
     */
    allFunctions: function() {
        var scope = this;
        var out = {}
        /** Merge the given list of functions with any existing functions under that name.
         *
         * @param {string} name
         * @param {Array.<Numbas.jme.funcObj>} fns
         */
        function add(name, fns) {
            if(!out[name]) {
                out[name] = [];
            }
            out[name] = out[name].merge(fns, fnSort);
        }
        while(scope) {
            for(const [name, fns] of Object.entries(scope.functions)) {
                add(name, fns)
            }
            scope = scope.parent;
        }
        return out;
    },
    /** Gather all members of this scope into this scope object.
     * A backwards-compatibility hack for questions that use `question.scope.variables.x`
     * Shouldn't be applied to any scope other than the question scope.
     */
    flatten: function() {
        this.variables = this.allVariables();
        this.rulesets = this.allRulesets();
    },

    /** Return a new scope created by unsetting the members specified by the given object.
     *
     * @param {object} defs - A dictionary with elements `variables`, `rulesets` and `functions`, each lists of names to unset.
     * @returns {Numbas.jme.Scope}
     */
    unset: function(defs) {
        var s = new Scope([this]);
        if(defs.variables) {
            defs.variables.forEach(function(v) {
                s.deleteVariable(v, {delete_constant: false});
            });
        }
        if(defs.functions) {
            defs.functions.forEach(function(f) {
                s.deleteFunction(f);
            });
        }
        if(defs.rulesets) {
            defs.rulesets.forEach(function(r) {
                s.deleteRuleset(r);
            });
        }
        return s;
    },

    /** Evaluate an expression in this scope - equivalent to `Numbas.jme.evaluate(expr,this)`.
     *
     * @param {JME} expr
     * @param {Object<Numbas.jme.token | object>} [variables] - Dictionary of variables to sub into expression. Values are automatically wrapped up as JME types, so you can pass raw JavaScript values.
     * @param {boolean} [noSubstitution] - If true, don't substitute variable values from the scope into the expression.
     * @returns {Numbas.jme.token}
     */
    evaluate: function(expr, variables, noSubstitution) {
        var scope = this;
        if(variables) {
            scope = new Scope([this]);
            for(const [name, v] of Object.entries(variables)) {
                scope.setVariable(name, jme.wrapValue(v));
            }
        }
        //if a string is given instead of an expression tree, compile it to a tree
        var tree;
        if(typeof(expr) == 'string') {
            tree = this.parser.compile(expr, scope);
        } else {
            tree = expr;
        }
        if(!tree) {
            return null;
        }
        if(!noSubstitution) {
            tree = jme.substituteTree(tree, scope, true);
        }
        var tok = tree.tok;
        var eargs = [];
        var value;

        switch(tok.type) {
        case 'number':
        case 'boolean':
        case 'range':
            return tok;
        case 'list':
            if(tok.value === undefined) {
                value = [];
                for(let i = 0;i < tree.args.length;i++) {
                    value[i] = scope.evaluate(tree.args[i], null, noSubstitution);
                }
                tok = new TList(value);
            }
            return tok;
        case 'dict':
            if(tok.value === undefined) {
                value = {};
                for(let i = 0;i < tree.args.length;i++) {
                    var kp = tree.args[i];
                    value[kp.tok.key] = scope.evaluate(kp.args[0], null, noSubstitution);
                }
                tok = new TDict(value);
            }
            return tok;
        case 'string':
            value = tok.value;
            if(!tok.safe && value.contains('{')) {
                if(tok.subjme) {
                    value = jme.display.treeToJME(jme.display.subvars(value, scope));
                } else {
                    value = jme.contentsubvars(value, scope)
                }
                var t = new TString(value);
                if(tok.latex !== undefined) {
                    t.latex = tok.latex
                    t.display_latex = tok.display_latex;
                }
                return t;
            } else {
                return tok;
            }
        case 'name':
            var v = scope.getVariable(tok.name);
            if(v && !noSubstitution) {
                return v;
            } else {
                var c = scope.getConstant(tok.name)
                if(c) {
                    return c.value;
                }
                tok = new TName(tok.name);
                tok.unboundName = true;
                return tok;
            }
        case 'op':
        case 'function':
            var op = jme.normaliseName(tok.name, scope);

            if(jme.lazyOps.indexOf(op) >= 0) {
                return scope.getFunction(op)[0].evaluate(tree.args, scope);
            } else {
                for(let i = 0;i < tree.args.length;i++) {
                    eargs.push(scope.evaluate(tree.args[i], null, noSubstitution));
                }

                var op_variable = scope.getVariable(op);

                if(op_variable && op_variable.type == 'lambda') {
                    return op_variable.evaluate(eargs, this);
                }

                var matchedFunction = scope.matchFunctionToArguments(tok, eargs);
                if(matchedFunction) {
                    var signature = matchedFunction.signature;
                    var castargs = jme.castArgumentsToSignature(signature, eargs);
                    return matchedFunction.fn.evaluate(castargs, scope);
                } else {
                    for(let i = 0;i <= eargs.length;i++) {
                        if(eargs[i] && eargs[i].unboundName) {
                            throw(new Numbas.Error('jme.typecheck.no right type unbound name', {name:eargs[i].name}));
                        }
                    }
                    throw(new Numbas.Error('jme.typecheck.no right type definition', {op:op, eargs: eargs}));
                }
            }
        case 'lambda':
            if(tree.args) {
                for(let i = 0;i < tree.args.length;i++) {
                    eargs.push(scope.evaluate(tree.args[i], null, noSubstitution));
                }
                return tok.evaluate(eargs, scope);
            } else {
                var nlambda = new types.TLambda();
                nlambda.names = tok.names;
                nlambda.make_signature();
                var nscope = new Numbas.jme.Scope([scope]);
                nlambda.all_names.forEach(function(name) {
                    nscope.deleteVariable(name);
                });
                nlambda.set_expr(jme.substituteTree(tok.expr, nscope, true, false));
                return nlambda;
            }

        default:
            return tok;
        }
    },

    /**
     * Normalise the subscripts in a `TName` token.
     *
     * @param {Numbas.jme.token} tok
     * @returns {Numbas.jme.token}
     */
    normaliseSubscripts: function(tok) {
        if(this.getConstant(tok.name)) {
            return tok;
        }
        var info = getNameInfo(tok.nameWithoutAnnotation);
        var name = info.root;
        if(info.subscript) {
            name += '_' + info.subscript;
        }
        if(info.primes) {
            name += info.primes;
        }
        return new TName(name, tok.annotation);
    },

    /** Options for {@link Numbas.jme.Scope.expandJuxtapositions}.
     *
     * @typedef {object} Numbas.jme.expand_juxtapositions_options
     * @property {boolean} singleLetterVariables - Enforce single-letter variables names: a name token like `xy` is rewritten to `x*y`.
     * @property {boolean} noUnknownFunctions - Rewrite applications of functions not defined in this scope to products, e.g. `x(y)` is rewritten to `x*y`.
     * @property {boolean} implicitFunctionComposition - If function names are juxtaposed, either as a single token or as (implicit) multiplication, rewrite as composition: e.g. `lnabs(x)` and `ln abs(x)` are both rewritten to `ln(abs(x))`.
     */

    /** Expand juxtapositions in variable and function names for implicit multiplication or composition.
     *
     * @param {Numbas.jme.tree} tree
     * @param {Numbas.jme.expand_juxtapositions_options} options
     * @returns {Numbas.jme.tree}
     */
    expandJuxtapositions: function(tree, options) {
        var scope = this;
        var default_options = {
            singleLetterVariables: true,    // `xy = x*y`
            noUnknownFunctions: true,    // `x(y) = x*y` when `x` is not the name of a function defined in this scope
            implicitFunctionComposition: true,  // `lnabs(x) = ln(abs(x))`, only applied when `noUnknownFunctions` is true, and `ln abs(x) = ln(abs(x))`
            normaliseSubscripts: true
        }
        options = options || default_options;

        if(!(options.singleLetterVariables || options.noUnknownFunctions || options.implicitFunctionComposition || options.normaliseSubscripts)) {
            return tree;
        }

        /** Construct a TFunc token with the given name, applying any synonyms.
         *
         * @param {string} name
         * @returns {Numbas.jme.token}
         */
        function tfunc(name) {
            return new TFunc(scope.parser.funcSynonym(name));
        }

        /** Get the names of all functions defined in the scope.
         *
         * @returns {object}
         */
        function get_function_names() {
            var defined_names = {};
            var s = scope;
            while(s) {
                for(const name of Object.keys(s.functions)) {
                    defined_names[jme.normaliseName(name, scope)] = true;
                }
                for(const name of Object.keys(jme.funcSynonyms)) {
                    defined_names[jme.normaliseName(name, scope)] = true;
                }
                if(s.parser.funcSynonyms) {
                    for(const name of Object.keys(s.parser.funcSynonyms)) {
                        defined_names[jme.normaliseName(name, scope)] = true;
                    }
                }
                s = s.parent
            }
            return defined_names;
        }

        var tok = tree.tok;

        if(options.implicitFunctionComposition && jme.isOp(tok, '*') && tree.args[1].tok.type == 'function') {
            var search = true;
            var defined_names = get_function_names();
            while(search) {
                if(!jme.isOp(tree.tok, '*')) {
                    break;
                }
                search = false;
                var c = tree.args[0];
                while(jme.isOp(c.tok, '*')) {
                    c = c.args[1];
                }
                if(c.tok.type == 'name' && defined_names[jme.normaliseName(c.tok.name, scope)]) {
                    search = true;
                    var composed_fn = {tok: tfunc(c.tok.name), args: [tree.args[1]]};
                    composed_fn.tok.vars = 1;
                    if(c == tree.args[0]) {
                        tree = composed_fn;
                    } else {
                        /** Remove the multiplicand from an n-ary multiplication.
                         *
                         * @param {Numbas.jme.tree} t
                         * @returns {Numbas.jme.tree}
                         */
                        function remove_multiplicand(t) {
                            if(t.args[1] == c) {
                                return t.args[0];
                            } else {
                                return {tok: t.tok, args: [t.args[0], remove_multiplicand(t.args[1])]};
                            }
                        }
                        tree = {tok: tree.tok, args: [remove_multiplicand(tree.args[0]), composed_fn]};
                    }
                }
            }

        }

        if(tree.args) {
            var oargs = tree.args;
            tree = {
                tok: tree.tok,
                args: tree.args.map(function(arg) {
                    return scope.expandJuxtapositions(arg, options);
                })
            };
        }

        /** Normalise the subscripts on name tokens.
         *
         * @param {Numbas.jme.token} tok
         * @returns {Numbas.jme.token}
         */
        function normaliseSubscripts(tok) {
            if(!options.normaliseSubscripts) {
                return tok;
            }

            return scope.normaliseSubscripts(tok);
        }

        const type_handlers = {
            'name': () => {
                if(options.singleLetterVariables && tok.nameInfo.letterLength > 1) {
                    var bits = [];
                    var s = tok.nameWithoutAnnotation;
                    var annotation = tok.annotation;
                    while(s.length) {
                        var i = s.length;
                        while(i > 1) {
                            var info = getNameInfo(s.slice(0, i));
                            if(info.letterLength == 1 && (!info.subscript || !info.subscript.match(/.[a-zA-Z]$/))) {
                                break;
                            }
                            i -= 1;
                        }
                        var ntok = normaliseSubscripts(new TName(s.slice(0, i), annotation));
                        bits.push(ntok);
                        annotation = undefined;
                        s = s.slice(i);
                    }
                    let tree = {tok: bits[0]};
                    for(let i = 1;i < bits.length;i++) {
                        tree = {tok: this.parser.op('*'), args: [tree, {tok: bits[i]}]};
                    }
                    return tree;
                } else {
                    return {tok: normaliseSubscripts(tok)};
                }
            },
            'function': () => {
                if(options.noUnknownFunctions) {
                    var defined_names = get_function_names();
                    var name = tok.name;
                    var breaks = [name.length];
                    for(let i = name.length - 1;i >= 0;i--) {
                        for(let j = 0;j < breaks.length;j++) {
                            var sub = jme.normaliseName(name.slice(i, breaks[j]), scope);
                            if(defined_names[sub]) {
                                breaks = breaks.slice(0, j + 1);
                                breaks.push(i);
                            }
                        }
                    }
                    var bits = [];
                    var remainder;
                    if(options.implicitFunctionComposition) {
                        breaks.reverse();
                        for(let i = 0;i < breaks.length - 1;i++) {
                            bits.push(name.slice(breaks[i], breaks[i + 1]));
                        }
                        remainder = name.slice(0, breaks[0]);
                    } else {
                        if(breaks.length > 1) {
                            bits.push(name.slice(breaks[1], breaks[0]));
                        }
                        remainder = name.slice(0, breaks[1]);
                    }
                    if(!bits.length) {
                        if(tree.args.length == 1) {
                            var arg = tree.args[0];
                            arg.bracketed = true;
                            return {tok: this.parser.op('*'), args: [this.expandJuxtapositions({tok: new TName(name)}, options), arg]};
                        }
                    } else {
                        var args = tree.args;
                        for(let i = bits.length - 1;i >= 0;i--) {
                            tree = {tok: tfunc(bits[i]), args: args};
                            tree.tok.vars = 1;
                            args = [tree];
                        }

                        // then interpret anything remaining on the left as multiplication by variables
                        if(remainder.length) {
                            var left = this.expandJuxtapositions({tok: new TName(remainder)}, options);
                            tree = {tok: this.parser.op('*'), args: [left, tree]};
                        }
                    }
                }
                return tree;
            },
            'op': () => {
                var mult_precedence = this.parser.getPrecedence('*');
                var op_precedence = this.parser.getPrecedence(tok.name);


                /** In a tree of the form `((x*y)*z)*w`, return `[x,(y*z)*w]` - pull out the leftmost multiplicand and return it along with the remaining tree.
                 *
                 * @param {Numbas.jme.tree} tree
                 * @returns {Array.<Numbas.jme.tree,Numbas.jme.tree>}
                 */
                function extract_leftmost(tree) {
                    if(!tree.bracketed && jme.isOp(tree.tok, '*')) {
                        var bits = extract_leftmost(tree.args[0]);
                        var leftmost = bits[0];
                        var rest = bits[1];
                        if(rest) {
                            return [leftmost, {tok:tree.tok, args:[rest, tree.args[1]]}];
                        } else {
                            return [leftmost, tree.args[1]];
                        }
                    } else {
                        return [tree];
                    }
                }
                /** In a tree of the form `x*(y*(z*w))`, return `[w,x*(y*z)]` - pull out the rightmost multiplicand and return it along with the remaining tree.
                 *
                 * @param {Numbas.jme.tree} tree
                 * @returns {Array.<Numbas.jme.tree,Numbas.jme.tree>}
                 */
                function extract_rightmost(tree) {
                    if(!tree.bracketed && jme.isOp(tree.tok, '*')) {
                        var bits = extract_rightmost(tree.args[1]);
                        var rightmost = bits[0];
                        var rest = bits[1];
                        if(rest) {
                            return [rightmost, {tok:tree.tok, args:[tree.args[0], rest]}];
                        } else {
                            return [rightmost, tree.args[0]];
                        }
                    } else {
                        return [tree];
                    }
                }

                /** Was the ith argument rewritten?
                 *
                 * @param {number} i
                 * @returns {boolean}
                 */
                function arg_was_rewritten(i) {
                    return !oargs[i].bracketed && (oargs[i].tok.type == 'name' || oargs[i].tok.type == 'function') && jme.isOp(tree.args[i].tok, '*');
                }


                if(tree.args.length == 1) {
                    if(tok.postfix) {
                        if(arg_was_rewritten(0)) {
                            var bits = extract_rightmost(tree.args[0]);
                            return {
                                tok: this.parser.op('*'),
                                args: [bits[1], {tok: tok, args: [bits[0]]}]
                            }
                        }
                    }
                } else if(tree.args.length == 2) {
                    if(op_precedence < mult_precedence) {
                        var lrest, l, r, rrest;
                        if(arg_was_rewritten(0)) {
                            var lbits = extract_rightmost(tree.args[0]);
                            l = lbits[0];
                            lrest = lbits[1];
                        } else {
                            l = tree.args[0];
                        }
                        if(arg_was_rewritten(1)) {
                            var rbits = extract_leftmost(tree.args[1]);
                            r = rbits[0];
                            rrest = rbits[1];
                        } else {
                            r = tree.args[1];
                        }
                        tree = {
                            tok: tok,
                            args: [l, r]
                        };
                        if(lrest) {
                            tree = {
                                tok: this.parser.op('*'),
                                args: [lrest, tree]
                            }
                        }
                        if(rrest) {
                            tree = {
                                tok: this.parser.op('*'),
                                args: [tree, rrest]
                            }
                        }
                    }
                }
                return tree;
            }
        }
        const handler = type_handlers[tok.type];
        if(handler) {
            return handler();
        } else {
            return tree;
        }
    }
};
/** @typedef {object} Numbas.jme.token
 * @property {string} type - The token's data type.
 * @see Numbas.jme.types
 */
/** The data types supported by JME expressions.
 *
 * @namespace Numbas.jme.types
 */
var types = jme.types = {}

jme.registerType = function(constructor, name, casts) {
    if(jme.types[name]) {
        throw(new Numbas.Error('jme.type.type already registered', {type:name}));
    }
    jme.types[name] = constructor;
    constructor.prototype.type = name;
    constructor.prototype.casts = casts;
}

/** Nothing type.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @class
 */
var TNothing = types.TNothing = function() {};
jme.registerType(TNothing, 'nothing');
/** Number type.
 *
 * The `precisionType` and `precision` properties are optional. If given, they describe the precision to which the number is known.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {number} value - The value.
 * @property {string|number|complex} originalValue - The value used to construct the token - either a string, a number, or a complex number object.
 * @property {string} precisionType - The type of precision of the value; either "dp" or "sigfig".
 * @property {number} precision - The number of digits of precision in the number.
 * @property {string} type - "number"
 * @class
 * @param {number} num
 */
var TNum = types.TNum = function(num) {
    if(num === undefined) {
        return;
    }
    this.originalValue = num;
    switch(typeof(num)) {
        case 'object':
            if(num.complex) {
                this.value = num;
            } else {
                throw(new Numbas.Error("jme.tokenise.number.object not complex"));
            }
            break;
        case "number":
            this.value = num;
            break;
        case "string":
            this.value = parseFloat(num);
            break;
    }
    this.value = num.complex ? num : parseFloat(num);
}

/** Convert a plain number to a `ComplexDecimal` value.
 *
 * @param {number} n
 * @param {string} precisionType - The type of precision of the value; either "dp" or "sigfig".
 * @param {number} precision - The number of digits of precision in the number.
 * @returns {Numbas.math.ComplexDecimal}
 */
function number_to_decimal(n, precisionType, precision) {
    var dp = 15;
    if(precisionType == 'dp' && isFinite(precision)) {
        dp = precision;
        dp = Math.min(dp, precision);
    }
    var re, im;

    /** Round the number to the appropriate level of precision.
     * @param {number} x
     * @returns {number}
     */
    function round(x) {
        switch(precisionType) {
            case 'sigfig':
                return x.toPrecision(precision);
            default:
                return x.toFixed(Math.max(0, dp));
        }
    }
    if(n.complex) {
        re = round(n.re);
        im = round(n.im);
    } else {
        // If the original string value is kept, use that to avoid any precision lost when parsing it to a float.
        if(n.originalValue) {
            return new math.ComplexDecimal(new Decimal(n.originalValue));
        }
        re = round(n);
        im = 0;
    }
    return new math.ComplexDecimal(new Decimal(re), new Decimal(im));
}

jme.registerType(
    TNum,
    'number',
    {
        'decimal': function(n) {
            return new TDecimal(number_to_decimal(n.value, n.precisionType, n.precision));
        }
    }
);

var TInt = types.TInt = function(num) {
    this.originalValue = num;
    this.value = Math.round(num);
}
jme.registerType(
    TInt,
    'integer',
    {
        'rational': function(n) {
            return new TRational(new math.Fraction(n.value, 1));
        },
        'number': function(n) {
            var t = new TNum(n.value);
            t.originalValue = this.originalValue;
            return t;
        },
        'decimal': function(n) {
            return new TDecimal(new Decimal(n.value));
        }
    }
);

var TRational = types.TRational = function(value) {
    this.value = value;
}
jme.registerType(
    TRational,
    'rational',
    {
        'decimal': function(n) {
            return new TDecimal((new Decimal(n.value.numerator)).dividedBy(new Decimal(n.value.denominator)));
        },
        'number': function(n) {
            return new TNum(n.value.numerator / n.value.denominator);
        }
    }
);

/** A Decimal number.
 * Powered by [decimal.js](http://mikemcl.github.io/decimal.js/).
 *
 * @param {Numbas.math.ComplexDecimal|Decimal} value - If just a `Decimal` is given, it's turned into a `ComplexDecimal` with zero imaginary part.
 * @property {Numbas.jme.ComplexDecimal} value - The value.
 */
var TDecimal = types.TDecimal = function(value) {
    if(value instanceof Decimal) {
        value = new math.ComplexDecimal(value, new Decimal(0));
    }
    this.value = value;
}

/** Convert a `ComplexDecimal` value to a plain number.
 *
 * @param {Numbas.math.ComplexDecimal} n
 * @returns {number}
 */
function decimal_to_number(n) {
    if(n.im.isZero()) {
        return n.re.toNumber();
    } else {
        return {complex: true, re: n.re.toNumber(), im: n.im.toNumber()};
    }
}

jme.registerType(
    TDecimal,
    'decimal',
    {
        'number': function(n) {
            return new TNum(decimal_to_number(n.value));
        }
    }
);

/** String type.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {string} value - The value.
 * @property {boolean} latex - Is this string LaTeX code? If so, it's displayed as-is in math mode.
 * @property {boolean} display_latex - Should this string be rendered as LaTeX when substituted into plain text?
 * @property {boolean} safe - If true, don't run {@link Numbas.jme.subvars} on this token when it's evaluated.
 * @property {boolean} subjme - If true, then this string represents JME code and variables should be substituted in using JME semantics instead of plain-text.
 * @property {string} type "string"
 * @class
 * @param {string} s
 */
var TString = types.TString = function(s) {
    this.value = s;
}
jme.registerType(TString, 'string');

/** Boolean type.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {boolean} value - The value.
 * @property {string} type - "boolean"
 * @class
 * @param {boolean} b
 */
var TBool = types.TBool = function(b) {
    this.value = b;
}
jme.registerType(TBool, 'boolean');

/** HTML DOM element.
 *
 * If the element has the attribute `data-interactive="false"` then it can be safely copied and embedded multiple times.
 * If the attribute is not present or has any other value, then it's assumed that it can't be safely copied.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Array.<Element>} value - The HTML element.
 * @property {string} type - "html"
 * @class
 * @param {Element} html
 */
var THTML = types.THTML = function(html) {
    if(html.ownerDocument === undefined && !html.jquery && !(typeof html == 'string' || Array.isArray(html))) {
        throw(new Numbas.Error('jme.thtml.not html'));
    }
    var elem = document.createElement('div');
    if(typeof html == 'string') {
        elem.innerHTML = html;
    } else if(Array.isArray(html)) {
        for(const child of html) {
            elem.appendChild(child);
        }
    } else {
        elem.appendChild(html);
    }
    this.value = Array.from(elem.childNodes);
    this.html = elem.innerHTML;
}
THTML.prototype = {
    isInteractive: function() {
        return this.value.some((e) => e.nodeType == e.ELEMENT_NODE && e.getAttribute('data-interactive') !== 'false');
    }
}
jme.registerType(THTML, 'html');

/** List of elements of any data type.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {number} vars - Length of list.
 * @property {Array.<Numbas.jme.token>} value - Values (may not be filled in if the list was created empty).
 * @property {string} type - "html"
 * @class
 * @param {number|Array.<Numbas.jme.token>} value - Either the size of the list, or an array of values.
 */
var TList = types.TList = function(value) {
    switch(typeof(value)) {
        case 'number':
            this.vars = value;
            break;
        case 'object':
            this.value = value;
            this.vars = value.length;
            break;
        default:
            this.vars = 0;
    }
}
jme.registerType(TList, 'list');

/** Key-value pair assignment.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {string} key - Key.
 * @class
 * @param {string} key
 */
var TKeyPair = types.TKeyPair = function(key) {
    this.key = key;
}
TKeyPair.prototype = {
    vars: 1
}
jme.registerType(TKeyPair, 'keypair');

/** Dictionary: map strings to values.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Object<Numbas.jme.token>} value - Map strings to tokens. Undefined until this token is evaluated.
 * @property {string} type - "dict"
 * @class
 * @param {Object<Numbas.jme.token>} value
 */
var TDict = types.TDict = function(value) {
    this.value = value;
}
jme.registerType(TDict, 'dict');

/** Set type: a collection of elements, with no duplicates.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Array.<Numbas.jme.token>} value - Array of elements. Constructor assumes all elements are distinct
 * @property {string} type - "set"
 * @class
 * @param {Array.<Numbas.jme.token>} value
 */
var TSet = types.TSet = function(value) {
    this.value = value;
}
jme.registerType(
    TSet,
    'set',
    {
        'list': function(s) {
            return new TList(s.value);
        }
    }
);

/** Vector type.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Array.<number>} value - Array of components
 * @property {string} type - "vector"
 * @class
 * @param {Array.<number>} value
 */
var TVector = types.TVector = function(value) {
    if(!(Array.isArray(value) && value.every(function(e) {
        return typeof e == 'number' || e.complex;
    }))) {
        throw(new Numbas.Error('jme.vector.value not an array of numbers'));
    }
    this.value = value;
}
jme.registerType(
    TVector,
    'vector',
    {
        'list': function(v) {
            return new TList(v.value.map(function(n) {
                var t = new TNum(n);
                t.precisionType = v.precisionType;
                t.precision = v.precision;
                return t;
            }));
        }
    }
);

/** Matrix type.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {matrix} value - Array of rows (which are arrays of numbers)
 * @property {string} type - "matrix"
 * @class
 * @param {matrix} value
 */
var TMatrix = types.TMatrix = function(value) {
    this.value = value;
    if(value.rows === undefined || value.columns === undefined || !(Array.isArray(value) && value.every(function(row) {
        return Array.isArray(row) && row.every(function(n) {
            return typeof n == 'number' || n.complex;
        });
    }))) {
        throw(new Numbas.Error("jme.matrix.value not the right type"));
    }
    if(arguments.length > 0) {
        if(value.length != value.rows) {
            throw(new Numbas.Error("jme.matrix.reports bad size"));
        }
        if(value.rows > 0 && value[0].length != value.columns) {
            throw(new Numbas.Error("jme.matrix.reports bad size"));
        }
    }
}
jme.registerType(
    TMatrix,
    'matrix',
    {
        'list': function(m) {
            return new TList(m.value.map(function(r) {
                var t = new TVector(r);
                t.precisionType = m.precisionType;
                t.precision = m.precision;
                return t;
            }));
        }
    }
);

/** A range of numerical values - either discrete or continuous.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Array.<number>} value - `[start,end,step]`
 * @property {number} size - The number of values in the range (if it's discrete, `undefined` otherwise).
 * @property {number} start - The lower bound of the range.
 * @property {number} end - The upper bound of the range.
 * @property {number} step - The difference between elements in the range.
 * @property {string} type - "range"
 * @class
 * @param {Array.<number>} range - `[start,end,step]`
 */
var TRange = types.TRange = function(range) {
    this.value = range;
    if(this.value !== undefined) {
        this.start = this.value[0];
        this.end = this.value[1];
        this.step = this.value[2];
        this.size = Math.floor((this.end - this.start) / this.step);
    }
}
jme.registerType(
    TRange,
    'range',
    {
        'list': function(r) {
            return new TList(math.rangeToList(r.value).map(function(n) {
                return new TNum(n)
            }));
        }
    }
);

/**
 *
 * @typedef {object} Numbas.jme.name_info
 * @property {string} root - The 'letters' part of the name, without subscripts or primes.
 * @property {number} letterLength - The number of letters in the name's root. For Greek letters, this is 1, not the the number of characters in `root`.
 * @property {boolean} isGreek - Is the root a Greek letter?
 * @property {boolean} isLong - Is this name 'long'? True if `letterLength` is more than 1.
 * @property {string} subscript - The subscript part of the name.
 * @property {string} subscriptGreek - Is the subscript a Greek letter?
 * @property {string} primes - The primes part of the name - a string of zero or more `'` characters.
 */

jme.re_greek = new RegExp('^(?:' + Object.values(Numbas.unicode_mappings.greek).join('|') + ')$');

/** Establish properties of a variable name, for the purposes of display.
 *
 * @memberof Numbas.jme
 * @param {string} name
 * @returns {Numbas.jme.name_info}
 */
var getNameInfo = jme.getNameInfo = function(name) {
    var nameInfo = {
        root: name,
        letterLength: name.length,
        isGreek: false,
        isLong: false,
        subscript: '',
        subscriptGreek: false,
        primes: ''
    };
    var re_math_variable = /^([^_]*[\p{Ll}\p{Lu}\p{Lo}\p{Lt}])(?:([\p{Nl}\p{Nd}]+)|_([\p{Nl}\p{Nd}]+)|_([^'_]+))?('+)?$/u;

    var m = name.match(re_math_variable);
    if(m) {
        nameInfo.root = m[1];
        nameInfo.letterLength = m[1].length;
        if(nameInfo.root.match(jme.re_greek)) {
            nameInfo.isGreek = true;
            nameInfo.letterLength = 1;
        }
        nameInfo.subscript = m[2] || m[3] || m[4];
        if(nameInfo.subscript && nameInfo.subscript.match(jme.re_greek)) {
            nameInfo.subscriptGreek = true;
        } else if(nameInfo.subscript && !nameInfo.subscript.match(/^[\p{Nl}\p{Nd}]*$/u) && nameInfo.subscript.length > 2) {
            nameInfo.letterLength += nameInfo.subscript.length;
        }
        nameInfo.primes = m[5];
    }
    if(!m || nameInfo.letterLength > 1) {
        nameInfo.root = name;
        nameInfo.subscript = '';
        nameInfo.subscriptGreek = false;
        nameInfo.primes = '';
        nameInfo.letterLength = name.length;
    }
    nameInfo.isLong = nameInfo.letterLength > 1 || nameInfo.root.match(/^_/);

    return nameInfo;
}

/** Variable name token.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {string} name - The name, prefixed with any annotations joined by colons.
 * @property {string} nameWithoutAnnotation - The name without the annotations.
 * @property {string} value - Same as `name`.
 * @property {Array.<string>} annotation - List of annotations (used to modify display).
 * @property {string} type - "name"
 * @class
 * @param {string} name
 * @param {Array.<string>} annotation
 */
var TName = types.TName = function(name, annotation) {
    this.annotation = annotation;
    this.name = name;
    this.nameWithoutAnnotation = name;
    if(this.annotation && this.annotation.length) {
        this.name = this.annotation.join(':') + ':' + this.name;
    }
    this.value = this.name;
    this.nameInfo = getNameInfo(this.nameWithoutAnnotation);
}
jme.registerType(TName, 'name');

/** JME function token.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {string} name - The function's name, prefixed with any annotations joined by colons.
 * @property {string} nameWithoutAnnotation - The name without the annotations.
 * @property {Array.<string>} annotation - List of annotations (used to modify display).
 * @property {number} vars - Arity of the function.
 * @property {string} type - "function"
 * @class
 * @param {string} name
 * @param {Array.<string>} [annotation] - Any annotations for the function's name.
 */
var TFunc = types.TFunc = function(name, annotation) {
    this.name = name;
    this.annotation = annotation;
    this.nameWithoutAnnotation = name;
    if(this.annotation && this.annotation.length) {
        this.name = this.annotation.join(':') + ':' + this.name;
    }
    this.nameInfo = getNameInfo(this.nameWithoutAnnotation);
}
TFunc.prototype = {
    vars: 0
}
jme.registerType(TFunc, 'function');

/** Unary/binary operation token.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {string} name - The name of the operation.
 * @property {number} vars - Arity of the operation.
 * @property {boolean} postfix - Is this operator applied postfix?
 * @property {boolean} prefix - Is this operator applied prefix?
 * @property {boolean} commutative - Is this operation commutative?
 * @property {boolean} associative - Is this operation associative?
 * @property {string} type - "op"
 * @class
 * @param {string} op - Name of the operation.
 * @param {boolean} postfix
 * @param {boolean} prefix
 * @param {number} arity - The number of parameters the operation takes.
 * @param {boolean} commutative
 * @param {boolean} associative
 * @param {negated} negated
 */
var TOp = types.TOp = function(op, postfix, prefix, arity, commutative, associative, negated) {
    this.name = op;
    this.postfix = postfix || false;
    this.prefix = prefix || false;
    this.vars = arity || 2;
    this.commutative = commutative || false;
    this.associative = associative || false;
    this.negated = negated || false;
}
jme.registerType(TOp, 'op');

/** An anonymous function.
 *
 * @param {Array.<Numbas.jme.tree>} names - Specification of the arguments. Each argument is either a name token, or a list of (lists of) names.
 * @param {Numbas.jme.tree} expr - The body of the function.
 */
var TLambda = types.TLambda = function(names, expr) {
    if(names !== undefined) {
        this.set_names(names);
    }
    if(expr !== undefined) {
        this.set_expr(expr);
    }
}
TLambda.prototype = {
    vars: 2,

    evaluate: function(args, scope) {
        return this.fn.evaluate(args, scope);
    },

    /** Set the argument names for this function.
     *
     * @param {Array.<Numbas.jme.tree>} names
     */
    set_names: function(names) {
        this.names = names;
    },

    make_signature: function(expr) {
        var all_names = [];

        /** Make the signature for the given argument.
         *
         * @param {Numbas.jme.tree} name
         * @returns {Numbas.jme.signature}
         */
        function make_signature(name) {
            if(name.tok.type == 'name') {
                all_names.push(name.tok.name);
                return jme.signature.anything();
            } else if(name.tok.type == 'list') {
                const items = name.args.map(make_signature);
                items.push(jme.signature.multiple(jme.signature.anything()));
                return jme.signature.list(...items);
            } else {
                throw(new Numbas.Error('jme.typecheck.wrong names for anonymous function', {names_type: name.tok.type}));
            }
        };

        const signature = this.names.map(make_signature);

        this.all_names = all_names;

        return signature;
    },

    /** Set the body of this function. The argument names must already have been set.
     *
     * @param {Numbas.jme.tree} expr
     */
    set_expr: function(expr) {
        const lambda = this;
        this.expr = expr;

        const signature = this.make_signature();

        this.fn = new jme.funcObj('', signature, '?', null, {
            evaluate: function(args, scope) {
                var nscope = new jme.Scope([scope]);
                var signature = lambda.fn.intype(args);
                if(!signature) {
                    throw(new Numbas.Error("jme.typecheck.wrong arguments for anonymous function"));
                }
                var castargs = jme.castArgumentsToSignature(signature, args);

                if(castargs.length < args.length) {
                    throw(new Numbas.Error("jme.typecheck.wrong arguments for anonymous function"));
                }

                /** Assign values to the function's named arguments.
                 *
                 * @param {Numbas.jme.tree} name - The specification of the name.
                 * @param {Numbas.jme.token} arg - The value to bind to this name.
                 */
                function assign_names(name, arg) {
                    if(name.tok.type == 'name') {
                        nscope.setVariable(name.tok.name, arg);
                    } else if(name.tok.type == 'list') {
                        name.args.forEach((lname, i) => assign_names(lname, arg.value[i]));
                    }
                }
                lambda.names.forEach((name, i) => assign_names(name, castargs[i]));

                return nscope.evaluate(jme.copy_tree(lambda.expr));
            }
        });
    }
}
jme.registerType(TLambda, 'lambda');

/** Punctuation token.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {string} type - The punctuation character.
 * @class
 * @param {string} kind - The punctuation character.
 */
var TPunc = types.TPunc = function(kind) {
    this.type = kind;
}

/** A JavaScript Promise, as a token.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Promise} promise - The promise this token represents.
 * @class
 * @param {string} promise - The promise this token represents.
 */
var TPromise = types.TPromise = function(promise) {
    this.promise = promise;
}
jme.registerType(TPromise, 'promise');

/** A JME expression, as a token.
 *
 * @memberof Numbas.jme.types
 * @augments Numbas.jme.token
 * @property {Numbas.jme.tree} tree - The expression tree.
 * @class
 * @param {string|Numbas.jme.tree} tree
 */
var TExpression = types.TExpression = function(tree) {
    if(typeof(tree) == 'string') {
        tree = jme.compile(tree);
    }
    if(tree) {
        tree = jme.unwrapSubexpression(tree);
    }
    this.tree = tree;
}
jme.registerType(TExpression, 'expression');

/** Arities of built-in operations.
 *
 * @readonly
 * @memberof Numbas.jme
 * @enum {number}
 */
jme.arity = {
    '!': 1,
    'not': 1,
    'fact': 1,
    '+u': 1,
    '-u': 1,
    '/u': 1,
    'sqrt': 1
}
/** Some names represent different operations when used as prefix. This dictionary translates them.
 *
 * @readonly
 * @memberof Numbas.jme
 * @enum {string}
 */
jme.prefixForm = {
    '+': '+u',
    '-': '-u',
    '/': '/u',
    '!': 'not',
    'not': 'not',
    'sqrt': 'sqrt'
}
/** Some names represent different operations when used as prefix. This dictionary translates them.
 *
 * @readonly
 * @memberof Numbas.jme
 * @enum {string}
 */
jme.postfixForm = {
    '!': 'fact'
}
/** Operator precedence - operators with lower precedence are evaluated first.
 *
 * @enum {number}
 * @memberof Numbas.jme
 * @readonly
 */
jme.precedence = {
    ';': 0,
    'fact': 1,
    'not': 1,
    'sqrt': 1,
    '+u': 2.5,
    '-u': 2.5,
    '/u': 2.5,
    '^': 2,
    '*': 3,
    '/': 3,
    '+': 4,
    '-': 4,
    '|': 5,
    '..': 5,
    '#':6,
    'except': 6.5,
    'in': 6.5,
    '<': 7,
    '>': 7,
    '<=': 7,
    '>=': 7,
    '<>': 8,
    '=': 8,
    'isa': 9,
    'and': 11,
    'nand': 11,
    'or': 12,
    'nor': 12,
    'xor': 13,
    'implies': 14,
    'of:': 48,
    'where:': 49,
    'for:': 50,
    ':': 100
};
/** Synonyms of operator names - keys in this dictionary are translated to their corresponding values.
 *
 * @enum {string}
 * @memberof Numbas.jme
 * @readonly
 */
var opSynonyms = jme.opSynonyms = {
    '&':'and',
    '&&':'and',
    'divides': '|',
    '||':'or',
    '÷': '/',
    '×': '*',
    '∈': 'in',
    '∧': 'and',
    '∨': 'or',
    '¬': 'not',
    '⟹': 'implies',
    '≠': '<>',
    '≥': '>=',
    '≤': '<=',
    'ˆ': '^',
    'identical': '='
}
/** Synonyms of function names - keys in this dictionary are translated to their corresponding values.
 *
 * @enum {string}
 * @memberof Numbas.jme
 * @readonly
 */
jme.funcSynonyms = {
    'sqr':'sqrt',
    'gcf': 'gcd',
    'sgn':'sign',
    'len': 'abs',
    'length': 'abs',
    'dec': 'decimal'
};
/** Operations which evaluate lazily - they don't need to evaluate all of their arguments.
 *
 * @memberof Numbas.jme
 */
jme.lazyOps = [];

/** Right-associative operations.
 *
 * @memberof Numbas.jme
 */
jme.rightAssociative = {
    '^': true,
    '+u': true,
    '-u': true,
    '/u': true,
    'for:': true
}
/** Operations representing relations.
 *
 * @enum {boolean}
 * @memberof Numbas.jme
 * @readonly
 */
jme.relations =
{
    '<': true,
    '>': true,
    '<=': true,
    '>=': true,
    '=': true,
    '<>': true,
    'in': true
};

/** Operations which commute.
 *
 * @enum {boolean}
 * @memberof Numbas.jme
 * @readonly
 */
jme.commutative =
{
    '*': true,
    '+': true,
    'and': true,
    'or': true,
    'nand': true,
    'nor': true,
    '=': true,
    'xor': true
};

/** Operations which are associative, i.e. (a∘b)∘c = a∘(b∘c).
 *
 * @enum {boolean}
 * @memberof Numbas.jme
 * @readonly
 */
jme.associative =
{
    '*': true,
    '+': true,
    'and': true,
    'or': true,
    'nand': true,
    'nor': true,
    'xor': true
};

/** Binary operations which have an equivalent operation written the other way round.
 *
 * @enum {string}
 * @memberof Numbas.jme
 */
jme.converseOps = {
    '<': '>',
    '>': '<',
    '<=': '>=',
    '>=': '<='
}


/** A standard parser for JME expressions.
 *
 * @memberof Numbas.jme
 * @type {Numbas.jme.Parser}
 */
jme.standardParser = new jme.Parser();
jme.standardParser.addBinaryOperator(';', {precedence:0});


/** A function which checks whether a {@link Numbas.jme.funcObj} can be applied to the given arguments.
 *
 * @callback Numbas.jme.typecheck_fn
 * @param {Array.<Numbas.jme.token>} variables
 * @returns {boolean}
 */

/** Evaluate a JME function on a list of arguments and in a given scope.
 *
 * @callback Numbas.jme.evaluate_fn
 * @param {Array.<Numbas.jme.tree|Numbas.jme.token|object>} args - Arguments of the function. If the function is {@link Numbas.jme.lazyOps|lazy}, syntax trees are passed, otherwise arguments are evaluated to JME tokens first. If the {@link Numbas.jme.funcObj_options|unwrapValues} option is set, the arguments are unwrapped to raw JavaScript values.
 * @param {Numbas.jme.Scope} scope - Scope in which the function is evaluated.
 * @returns {Numbas.jme.token|object} If {@link Numbas.jme.funcObj_options|unwrapValues} is set, the raw value of the result, otherwise a JME token.
 */

/** Options for the {@link Numbas.jme.funcObj} constructor.
 *
 * @typedef {object} Numbas.jme.funcObj_options
 * @property {Numbas.jme.typecheck_fn} typecheck - Check that this function can be evaluated on the given arguments.
 * @property {Numbas.jme.evaluate_fn} evaluate - Evaluate the function on a list of arguments and in a given scope.
 * @property {boolean} unwrapValues - Unwrap list elements in arguments into javascript primitives before passing to the evaluate function?
 */

var funcObjAcc = 0;    //accumulator for ids for funcObjs, so they can be sorted

/**
 * A JME function. Capable of confirming that it can be evaluated on a given list of arguments, and returning the result of its evaluation on a list of arguments inside a given scope.
 *
 * @memberof Numbas.jme
 * @class
 * @param {string} name
 * @param {Array.<Function|string>} intype - A list of data type constructors for the function's parameters' types. Use the string '?' to match any type. Or, give the type's name with a '*' in front to match any number of that type. If `null`, then `options.typecheck` is used.
 * @param {Function} outcons - The constructor for the output value of the function.
 * @param {Numbas.jme.evaluate_fn} fn - JavaScript code which evaluates the function.
 * @param {Numbas.jme.funcObj_options} options
 *
 */
jme.funcObj = function(name, intype, outcons, fn, options) {
    /** Globally unique ID of this function object.
     *
     * @name id
     * @member {number}
     * @memberof Numbas.jme.funcObj
     */
    this.id = funcObjAcc++;
    options = this.options = options || {};

    /** The function's name.
     *
     * @name name
     * @member {string}
     * @memberof Numbas.jme.funcObj
     */
    this.name = name;

    /** A description of what the function does.
     *
     * @name description
     * @member {string}
     * @memberof Numbas.jme.funcObj
     */
    this.description = options.description || '';

    /** Check the given list of arguments against this function's calling signature.
     *
     * @name intype
     * @memberof Numbas.jme.funcObj
     * @member {Function}
     * @param {Array.<Numbas.jme.token>}
     * @returns {Array.<string>|boolean} `false` if the given arguments are not valid for this function, or a list giving the desired type for each argument - arguments shouldbe cast to these types before evaluating.
     */
    this.intype = jme.signature.sequence.apply(this, intype.map(jme.parse_signature));
    /** The return type of this function. Either a Numbas.jme.token constructor function, or the string '?', meaning unknown type.
     *
     * @name outtype
     * @member {Function|string}
     * @memberof Numbas.jme.funcObj
     */
    if(typeof(outcons) == 'function') {
        this.outtype = outcons.prototype.type;
    } else {
        this.outtype = '?';
    }
    this.outcons = outcons;
    /** Javascript function for the body of this function.
     *
     * @name fn
     * @member {Function}
     * @memberof Numbas.jme.funcObj
     */
    this.fn = fn;
    /** Can this function be called with the given list of arguments?
     *
     * @function typecheck
     * @param {Numbas.jme.token[]} variables
     * @returns {boolean}
     * @memberof Numbas.jme.funcObj
     */
    var check_signature = this.intype;
    this.typecheck = options.typecheck || function(variables) {
        var match = check_signature(variables);
        return match !== false && sig_remove_missing(match).length == variables.length;
    }
    /** Evaluate this function on the given arguments, in the given scope.
     *
     * @function evaluate
     * @param {Numbas.jme.token[]} args
     * @param {Numbas.jme.Scope} scope
     * @returns {Numbas.jme.token}
     * @memberof Numbas.jme.funcObj
     */
    this.evaluate = options.evaluate || function(args, scope) {
        var nargs = [];
        for(let i = 0; i < args.length; i++) {
            if(options.unwrapValues) {
 nargs.push(jme.unwrapValue(args[i]));
} else {
 nargs.push(args[i].value);
}
        }
        var result = this.fn.apply(null, nargs);
        if(options.unwrapValues) {
            result = jme.wrapValue(result);
            if(!result.type) {
 result = new this.outcons(result);
}
        } else {
            result = new this.outcons(result);
        }
        if(options.latex) {
            result.latex = true;
        }
        return result;
    }
    /** Does this function behave randomly?
     *
     * @name random
     * @member {boolean}
     * @memberof Numbas.jme.funcObj
     */
    this.random = options.random;
}
/** Randoly generate values for each of the given names between `min` and `max`.
 *
 * @param {Array.<string>} varnames
 * @param {number} min
 * @param {number} max
 * @param {number} times - The number of values to produce for each name.
 * @returns {Array.<object>} - The list of dictionaries mapping names to their values.
 */
function randoms(varnames, min, max, times) {
    times *= varnames.length || 1;
    var rs = [];
    for(var i = 0; i < times; i++) {
        var r = {};
        for(var j = 0; j < varnames.length; j++) {
            r[varnames[j]] = new TNum(Numbas.math.randomrange(min, max));
        }
        rs.push(r);
    }
    return rs;
}
/** Does every name in `array1` occur in `array2`?
 *
 * @param {Array.<string>} array1
 * @param {Array.<string>} array2
 * @returns {boolean}
 */
function varnamesAgree(array1, array2) {
    var name;
    for(let i = 0; i < array1.length; i++) {
        if((name = array1[i])[0] != '$' && !array2.contains(name)) {
 return false;
}
    }
    return true;
};
/** Decide if two numbers are close enough to count as equal.
 *
 * @callback Numbas.jme.checkingFunction
 * @param {number|Numbas.math.ComplexDecimal} r1
 * @param {number|Numbas.math.ComplexDecimal} r2
 * @param {number} tolerance - A measure of how close the results need to be to count as equal. What this means depends on the checking function.
 * @returns {boolean} - True if `r1` and `r2` are close enough to be equal.
 */
/**
 * Numerical comparison functions.
 *
 * @enum {Numbas.jme.checkingFunction}
 * @memberof Numbas.jme
 */
var checkingFunctions = jme.checkingFunctions =
{
    /** Absolute difference between variables - fail if `Math.abs(r1-r2)` is bigger than `tolerance`.
     *
     * @param {number|Numbas.math.ComplexDecimal} r1
     * @param {number|Numbas.math.ComplexDecimal} r2
     * @param {number} tolerance
     * @returns {boolean}
     */
    absdiff: function(r1, r2, tolerance) {
        if(math.isComplexDecimal(r1) || math.isComplexDecimal(r2)) {
            r1 = math.ensure_decimal(r1);
            r2 = math.ensure_decimal(r2);
            return r1.minus(r2).absoluteValue().re.lessThan(Math.abs(tolerance));
        }

        if(r1 === Infinity || r1 === -Infinity) {
 return r1 === r2;
}
        return math.leq(math.abs(math.sub(r1, r2)), Math.abs(tolerance));
    },
    /** Relative (proportional) difference between variables - fail if `r1/r2 - 1` is bigger than `tolerance`.
     *
     * @param {number|Numbas.math.ComplexDecimal} r1
     * @param {number|Numbas.math.ComplexDecimal} r2
     * @param {number} tolerance
     * @returns {boolean}
     */
    reldiff: function(r1, r2, tolerance) {
        if(math.isComplexDecimal(r1) || math.isComplexDecimal(r2)) {
            r1 = math.ensure_decimal(r1);
            r2 = math.ensure_decimal(r2);
            return r1.minus(r2).absoluteValue().re.lessThan(r2.re.times(tolerance).absoluteValue());
        }

        if(r1 === Infinity || r1 === -Infinity) {
 return r1 === r2;
}
        //
        if(r2 != 0) {
            return math.leq(Math.abs(math.sub(r1, r2)), Math.abs(math.mul(tolerance, r2)));
        } else {    //or if correct answer is 0, checks abs difference
            return math.leq(Math.abs(math.sub(r1, r2)), tolerance);
        }
    },
    /** Round both values to `tolerance` decimal places, and fail if unequal.
     *
     * @param {number|Numbas.math.ComplexDecimal} r1
     * @param {number|Numbas.math.ComplexDecimal} r2
     * @param {number} tolerance
     * @returns {boolean}
     */
    dp: function(r1, r2, tolerance) {
        if(math.isComplexDecimal(r1) || math.isComplexDecimal(r2)) {
            r1 = math.ensure_decimal(r1);
            r2 = math.ensure_decimal(r2);
            return r1.toDecimalPlaces(tolerance).equals(r2.toDecimalPlaces(tolerance));
        }

        if(r1 === Infinity || r1 === -Infinity) {
 return r1 === r2;
}
        tolerance = Math.floor(Math.abs(tolerance));
        return math.eq(math.precround(r1, tolerance), math.precround(r2, tolerance));
    },
    /** Round both values to `tolerance` significant figures, and fail if unequal.
     *
     * @param {number|Numbas.math.ComplexDecimal} r1
     * @param {number|Numbas.math.ComplexDecimal} r2
     * @param {number} tolerance
     * @returns {boolean}
     */
    sigfig: function(r1, r2, tolerance) {
        if(math.isComplexDecimal(r1) || math.isComplexDecimal(r2)) {
            r1 = math.ensure_decimal(r1);
            r2 = math.ensure_decimal(r2);
            return r1.toSignificantDigits(tolerance).equals(r2.toSignificantDigits(tolerance));
        }

        if(r1 === Infinity || r1 === -Infinity) {
 return r1 === r2;
}
        tolerance = Math.floor(Math.abs(tolerance));
        return math.eq(math.siground(r1, tolerance), math.siground(r2, tolerance));
    }
};

/** Custom substituteTree behaviour for specific functions - for a given usage of a function, substitute in variable values from the scope.
 *
 * Functions have the signature `<tree with function call at the top, scope, allowUnbound>`.
 *
 * @memberof Numbas.jme
 * @enum {Numbas.jme.substituteTree}
 * @see Numbas.jme.substituteTree
 */
var substituteTreeOps = jme.substituteTreeOps = {};

/** Custom findvars behaviour for specific functions - for a given usage of a function, work out which variables it depends on.
 *
 * @memberof Numbas.jme
 * @enum {Numbas.jme.findvars}
 * @see Numbas.jme.findvars
 */
var findvarsOps = jme.findvarsOps = {}

/** Custom isDeterministic behaviour for specific functions.
 *
 * @memberof Numbas.jme
 * @enum {Numbas.jme.isDeterministic}
 * @see Numbas.jme.isDeterministic
 */
var isDeterministicOps = jme.isDeterministicOps = {};

/** Find all variables used in given syntax tree.
 *
 * @memberof Numbas.jme
 * @function
 * @param {Numbas.jme.tree} tree
 * @param {Array.<string>} boundvars - Variables to be considered as bound (don't include them).
 * @param {Numbas.jme.Scope} scope
 * @returns {Array.<string>}
 */
var findvars = jme.findvars = function(tree, boundvars, scope) {
    if(!scope) {
        scope = jme.builtinScope;
    }
    if(boundvars === undefined) {
        boundvars = [];
    }
    if(!tree) {
        return [];
    }
    if((tree.tok.type == 'function' || tree.tok.type == 'op') && tree.tok.name in findvarsOps) {
        return findvarsOps[tree.tok.name](tree, boundvars, scope);
    }
    if(tree.args === undefined) {
        switch(tree.tok.type) {
        case 'name':
            var name = jme.normaliseName(tree.tok.name, scope);
            if(boundvars.indexOf(name) == -1 && !scope.getConstant(name)) {
                return [name];
            } else {
                return [];
            }
        case 'string':
            if(tree.tok.safe) {
                return [];
            }
            var bits = util.contentsplitbrackets(tree.tok.value);
            var out = [];
            for(let i = 0;i < bits.length;i += 4) {
                var plain = bits[i];
                const sbits = util.splitbrackets(plain, '{', '}', '(', ')');
                for(let k = 1;k <= sbits.length - 1;k += 2) {
                    var tree2 = scope.parser.compile(sbits[k]);
                    out = out.merge(findvars(tree2, boundvars, scope));
                }
                if(i <= bits.length - 3) {
                    var tex = bits[i + 2];
                    var tbits = jme.texsplit(tex);
                    for(let j = 0;j < tbits.length;j += 4) {
                        var cmd = tbits[j + 1];
                        var expr = tbits[j + 3];
                        switch(cmd) {
                        case 'var': {
                            const tree2 = scope.parser.compile(expr);
                            out = out.merge(findvars(tree2, boundvars, scope));
                            break;
                        }
                        case 'simplify': {
                            const sbits = util.splitbrackets(expr, '{', '}', '(', ')');
                            for(let k = 1;k < sbits.length - 1;k += 2) {
                                const tree2 = scope.parser.compile(sbits[k]);
                                out = out.merge(findvars(tree2, boundvars, scope));
                            }
                            break;
                        }
                        }
                    }
                }
            }
            return out;
        case 'lambda':
            var mapped_boundvars = boundvars.concat(tree.tok.all_names.map((name) => jme.normaliseName(name, scope)));
            return jme.findvars(tree.tok.expr, mapped_boundvars, scope);
        default:
            return [];
        }
    } else {
        var argvars = jme.findvars_args(tree.args, boundvars, scope);
        if(tree.tok.type == 'function') {
            const fn_name = jme.normaliseName(tree.tok.name, scope);
            if(boundvars.indexOf(fn_name) == -1 && scope.getFunction(fn_name).length == 0) {
                argvars.push(fn_name);
            }
        }
        return argvars;
    }
}

/**
 * Find variables used in any of a list of trees.
 * Used to find variables used in arguments to functions / operations.
 *
 * @param {Array.<Numbas.jme.tree>} trees
 * @param {Array.<string>} boundvars - Variables to be considered as bound (don't include them).
 * @param {Numbas.jme.Scope} scope
 * @returns {Array.<string>}
 */
jme.findvars_args = function(trees, boundvars, scope) {
    return trees.reduce((vars, tree) => vars.merge(findvars(tree, boundvars, scope)), []);
}

/** Check that two values are equal.
 *
 * @memberof Numbas.jme
 * @function
 * @param {Numbas.jme.token} r1
 * @param {Numbas.jme.token} r2
 * @param {Function} checkingFunction - One of {@link Numbas.jme.checkingFunctions}.
 * @param {number} checkingAccuracy
 * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
 * @returns {boolean}
 */
var resultsEqual = jme.resultsEqual = function(r1, r2, checkingFunction, checkingAccuracy, scope) {    // first checks both expressions are of same type, then uses given checking type to compare results
    var type = jme.findCompatibleType(r1.type, r2.type);
    if(!type) {
        return false;
    }
    r1 = jme.castToType(r1, type);
    r2 = jme.castToType(r2, type);
    var v1 = r1.value;
    var v2 = r2.value;

    switch(type) {
        case 'rational':
            return checkingFunction(v1.toDecimal(), v2.toDecimal(), checkingAccuracy);
        case 'number':
        case 'decimal':
        case 'integer':
            if(v1.complex || v2.complex) {
                if(!v1.complex) {
                    v1 = {re:v1, im:0, complex:true};
                }
                if(!v2.complex) {
                    v2 = {re:v2, im:0, complex:true};
                }
                return checkingFunction(v1.re, v2.re, checkingAccuracy) && checkingFunction(v1.im, v2.im, checkingAccuracy);
            } else {
                return checkingFunction(v1, v2, checkingAccuracy);
            }
        case 'vector':
            if(v1.length != v2.length) {
                return false;
            }
            for(let i = 0;i < v1.length;i++) {
                if(!resultsEqual(new TNum(v1[i]), new TNum(v2[i]), checkingFunction, checkingAccuracy, scope)) {
                    return false;
                }
            }
            return true;
        case 'matrix':
            if(v1.rows != v2.rows || v1.columns != v2.columns) {
                return false;
            }
            for(let i = 0;i < v1.rows;i++) {
                for(let j = 0;j < v1.columns;j++) {
                    if(!resultsEqual(new TNum(v1[i][j] || 0), new TNum(v2[i][j] || 0), checkingFunction, checkingAccuracy, scope)) {
                        return false;
                    }
                }
            }
            return true;
        case 'list':
            if(v1.length != v2.length) {
                return false;
            }
            for(let i = 0;i < v1.length;i++) {
                if(!resultsEqual(v1[i], v2[i], checkingFunction, checkingAccuracy, scope)) {
                    return false;
                }
            }
            return true;
        default: {
            return util.eq(r1, r2, scope);
        }
    }
};

/** List names of variables used in `tree`, obtained by depth-first search.
 *
 * Differs from {@link Numbas.jme.findvars} by including duplicates, and ignoring {@link Numbas.jme.findvarsOps}.
 *
 * @memberof Numbas.jme
 * @function
 * @param {Numbas.jme.tree} tree
 * @returns {string[]}
 */
jme.varsUsed = function(tree) {
    switch(tree.tok.type) {
        case 'name':
            return [tree.tok.name];
        case 'op':
        case 'function':
            var o = [];
            for(let i = 0;i < tree.args.length;i++) {
                o = o.concat(jme.varsUsed(tree.args[i]));
            }
            return o;
        default:
            return [];
    }
};

/** Use JS comparison operators to compare the `value` property of both tokens.
 * Used when the token wraps a JS built-in type, such as string, number or boolean.
 *
 * @memberof Numbas.jme
 * @function
 * @see Numbas.jme.tokenComparisons
 * @param {Numbas.jme.token} a
 * @param {Numbas.jme.token} b
 * @returns {boolean}
 */
var compareTokensByValue = jme.compareTokensByValue = function(a, b) {
    return a.value > b.value ? 1 : a.value < b.value ? -1 : 0;
}

/** Functions to compare two tokens of the same type.
 * Returns -1 if a<b, 0 if a=b, and 1 if a>b.
 *
 * @see Numbas.jme.compareTokens
 * @memberof Numbas.jme
 */
var tokenComparisons = Numbas.jme.tokenComparisons = {
    'number': compareTokensByValue,
    'integer': compareTokensByValue,
    'rational': function(a, b) {
        a = a.value.toFloat();
        b = b.value.toFloat();
        return a > b ? 1 : a < b ? -1 : 0;
    },
    'string': compareTokensByValue,
    'boolean': compareTokensByValue
}

/** Compare two tokens, for the purposes of sorting.
 * Uses JavaScript comparison for numbers, strings and booleans, and {@link Numbas.jme.compareTrees} for everything else, or when types differ.
 *
 * @memberof Numbas.jme
 * @function
 * @param {Numbas.jme.token} a
 * @param {Numbas.jme.token} b
 * @see Numbas.jme.tokenComparisons
 * @returns {number} -1 if `a < b`, 1 if `a > b`, else 0.
 */
var compareTokens = jme.compareTokens = function(a, b) {
    if(a.type != b.type) {
        var type = jme.findCompatibleType(a.type, b.type);
        if(type) {
            var ca = jme.castToType(a, type);
            var cb = jme.castToType(b, type);
            return compareTokens(ca, cb);
        } else {
            return jme.compareTrees({tok:a}, {tok:b});
        }
    } else {
        var compare = tokenComparisons[a.type];
        if(compare) {
            return compare(a, b);
        } else {
            return jme.compareTrees({tok:a}, {tok:b});
        }
    }
}

/** Produce a comparison function which sorts tokens after applying a function to them.
 *
 * @memberof Numbas.jme
 * @function
 * @param {Function} fn - take a token and return a token
 * @returns {Function}
 */
jme.sortTokensBy = function(fn) {
    return function(a, b) {
        a = fn(a);
        b = fn(b);
        if(a === undefined) {
            return b === undefined ? 0 : 1;
        } else if(b === undefined) {
            return -1;
        } else {
            return jme.compareTokens(a, b);
        }
    }
}

/** Are the two given trees exactly the same?
 *
 * @memberof Numbas.jme
 * @param {Numbas.jme.tree} a
 * @param {Numbas.jme.tree} b
 * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
 * @returns {boolean}
 */
var treesSame = jme.treesSame = function(a, b, scope) {
    if(a == undefined || b == undefined) {
        return a == undefined && b == undefined;
    }
    var ta = a.tok;
    var tb = b.tok;
    if(a.args || b.args) {
        if(!(a.args && b.args && a.args.length == b.args.length)) {
            return false;
        }
        for(let i = 0; i < a.args.length;i++) {
            if(!treesSame(a.args[i], b.args[i], scope)) {
                return false;
            }
        }
    } else {
        var type = jme.findCompatibleType(ta.type, tb.type);
        if(!type) {
            return false;
        } else {
            ta = jme.castToType(ta, type);
            tb = jme.castToType(tb, type);
        }
    }
    return util.eq(a.tok, b.tok, scope);
}

/** Compare two trees.
 *
 * - Compare lists of variables lexically using {@link Numbas.jme.varsUsed}; longest goes first if one is a prefix of the other
 * - then monomials before anything else
 * - then by data type
 * - then by function name
 * - otherwise return 0.
 *
 * @memberof Numbas.jme
 * @function
 * @param {Numbas.jme.tree} a
 * @param {Numbas.jme.tree} b
 * @returns {number} -1 if `a` should appear to the left of `b`, 0 if equal, 1 if `a` should appear to the right of `b`.
 */
var compareTrees = jme.compareTrees = function(a, b) {
    var sign_a = 1;
    while(jme.isOp(a.tok, '-u')) {
        a = a.args[0];
        sign_a *= -1;
    }
    var sign_b = 1;
    while(jme.isOp(b.tok, '-u')) {
        b = b.args[0];
        sign_b *= -1;
    }
    var va = jme.varsUsed(a);
    var vb = jme.varsUsed(b);
    for(let i = 0;i < va.length;i++) {
        if(i >= vb.length) {
            return -1;
        }
        if(va[i] != vb[i]) {
            return va[i] < vb[i] ? -1 : 1;
        }
    }
    if(vb.length > va.length) {
        return 1;
    }

    var ma = jme.isMonomial(a);
    var mb = jme.isMonomial(b);
    var isma = ma !== false;
    var ismb = mb !== false;
    if(isma != ismb) {
        return isma ? -1 : 1;
    }
    if(isma && ismb && !(a.tok.type == 'name' && b.tok.type == 'name')) {
        var d = jme.compareTrees(ma.base, mb.base);
        if(d == 0) {
            var dd = jme.compareTrees(mb.degree, ma.degree);
            if(dd != 0) {
                return dd;
            } else {
                var dc = compareTrees(ma.coefficient, mb.coefficient);
                return dc != 0 ? dc : sign_a == sign_b ? 0 : sign_a ? 1 : -1;
            }
        } else {
            return d;
        }
    }

    if(a.tok.type != b.tok.type) {
        var order = ['op', 'function'];
        var oa = order.indexOf(a.tok.type);
        var ob = order.indexOf(b.tok.type);
        if(oa != ob) {
            return oa > ob ? -1 : 1;
        } else {
            return a.tok.type < b.tok.type ? -1 : 1;
        }
    }

    if(a.args || b.args) {
        var aargs = a.args || [];
        var bargs = b.args || [];
        if(aargs.length != bargs.length) {
            return aargs.length < bargs.length ? -1 : 1;
        }
        for(let i = 0;i < aargs.length;i++) {
            var c = jme.compareTrees(aargs[i], bargs[i]);
            if(c != 0) {
                return c;
            }
        }
    }

    switch(a.tok.type) {
        case 'op':
        case 'function': {
            /** Is the given tree of the form `?^?`, `?*(?^?)` or `?/(?^?)`.
             *
             * @param {Numbas.jme.tree} t
             * @returns {boolean}
             */
            function is_pow(t) {
                return t.tok.name == '^' || (t.tok.name == '*' && t.args[1].tok.name == '^') || (t.tok.name == '/' && t.args[1].tok.name == '^');
            }
            var pa = is_pow(a);
            var pb = is_pow(b);
            if(pa && !pb) {
                return -1;
            } else if(!pa && pb) {
                return 1;
            }
            if(a.tok.name != b.tok.name) {
                return a.tok.name < b.tok.name ? -1 : 1;
            }
            break;
        }
        case 'expression':
            return jme.compareTrees(a.tok.tree, b.tok.tree);
        default:
            if(jme.isType(a.tok, 'number')) {
                var na = jme.castToType(a.tok, 'number').value;
                var nb = jme.castToType(b.tok, 'number').value;
                if(na.complex || nb.complex) {
                    na = na.complex ? na : {re:na, im:0};
                    nb = nb.complex ? nb : {re:nb, im:0};
                    var gt = na.re > nb.re || (na.re == nb.re && na.im > nb.im);
                    var eq = na.re == nb.re && na.im == nb.im && sign_a == sign_b;
                    return gt ? 1 : eq ? 0 : -1;
                } else {
                    return na < nb ? -1 : na > nb ? 1 : sign_a == sign_b ? 0 : sign_a ? 1 : -1;
                }
            }
    }
    return sign_a == sign_b ? 0 : sign_a ? 1 : -1;
}

/** Infer the types of variables in an expression, by trying all definitions of functions and returning only those that can be satisfied by an assignment of types to variable names.
 * Doesn't work well on functions with unknown return type, like `if` and `switch`. In these cases, it assumes the return type of the function is whatever it needs to be, even if that is inconsistent with what the function would actually do.
 *
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.Scope} scope
 * @returns {Object<string>} A dictionary mapping names to types.
 */
jme.inferVariableTypes = function(tree, scope) {
    const annotated_assignments = find_valid_assignments(tree, scope);
    return Object.fromEntries(Object.entries(annotated_assignments).map(([name, assignment]) => [name, assignment.type]));
}

/** Enumerate lists of `n` arguments matching the signature `sig`.
 *
 * @param {Numbas.jme.signature} sig
 * @param {number} n
 * @returns {Array.<Array.<string>>} - A list of lists of type names. Each list of type names has `n` elements.
 */
function enumerate_signatures(sig, n) {
    let out;
    switch(sig.kind) {
        case 'multiple':
            if(n == 0) {
                return [[]];
            } else {
                const o = [];
                for(let i = 1; i <= n; i++) {
                    const subs = enumerate_signatures(sig.signature, i);
                    const rest = enumerate_signatures(sig, n - i);
                    subs.forEach((s) => {
                        for(const r of rest) {
                            o.push(s.concat(r));
                        }
                    });
                }
                return o;
            }
        case 'optional':
            if(n == 0) {
                return [[]];
            } else {
                return enumerate_signatures(sig.signature, n);
            }
        case 'label':
            return enumerate_signatures(sig.signature, n);
        case 'sequence':
            var partitions = math.integer_partitions(n, sig.signatures.length);
            out = [];
            partitions.forEach((p) => {
                const bits = sig.signatures.map((s, i) => {
                    return enumerate_signatures(s, p[i]);
                });
                let o = [[]];
                for(const bit of bits) {
                    const no = [];
                    for(const a of o) {
                        for(const b of bit) {
                            no.push(a.concat(b));
                        }
                    }
                    o = no;
                }
                out = out.concat(o);
            });
            return out;
        case 'or':
            out = [];
            for(const s of sig.signatures) {
                out = out.concat(enumerate_signatures(s, n));
            }
            return out;
        case 'type':
            if(n == 1) {
                return [[sig.type]];
            } else {
                return [];
            }
        case 'anything':
            if(n == 1) {
                return [[undefined]];
            } else {
                return [];
            }
        case 'list':
            if(n == 1) {
                return [['list']];
            } else {
                return [];
            }
        case 'dict':
            if(n == 1) {
                return [['dict']];
            } else {
                return [];
            }
    }
}
jme.enumerate_signatures = enumerate_signatures;

/** Find a type which can be cast to all of the desired types.
 *
 * @param {Array.<string>} types - The names of the desired types.
 * @returns {string}
 */
function mutually_compatible_type(types) {
    var preferred_types = ['number', 'decimal'];
    /** Can the given type be cast to all of the desired types?
     *
     * @param {string} x - The name of a type.
     * @returns {boolean}
     */
    function mutually_compatible(x) {
        var casts = jme.types[x].prototype.casts || {};
        return types.every(function(t) {
            return t == x || casts[t];
        });
    }
    for(let i = 0;i < preferred_types.length;i++) {
        var type = preferred_types[i];
        if(mutually_compatible(type)) {
            return type;
        }
    }
    for(const x of Object.keys(jme.types)) {
        if(mutually_compatible(x)) {
            return x;
        }
    }
    return undefined;
}
jme.mutually_compatible_type = mutually_compatible_type

/** Find an assignment of types to free variables in an expression such that it can be evaluated in the given scope.
 *
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.Scope} scope
 * @param {object} [assignments] - A dictionary mapping variable names to their types. A missing entry implies that the variable can have any type.
 * @param {string} [outtype] - The desired type of the result of the expression. `undefined` means that any type is fine.
 * @returns {object} - A dictionary mapping variable names to their types.
 */
function find_valid_assignments(tree, scope, assignments, outtype) {
    if(assignments === undefined) {
        assignments = {};
    }
    switch(tree.tok.type) {
        case 'op':
        case 'function': {
            let fns = scope.getFunction(tree.tok.name);
            if(outtype !== undefined) {
                fns = fns.filter((fn) => fn.outtype == '?' || fn.outtype == outtype);
            }
            for(const fn of fns) {
                /* For each definition of the function, find input types that it can work on.
                 * For each list of input types, check if the given arguments can produce that input type, and if so, how they change the variable type assignments.
                 */
                let options = enumerate_signatures(fn.intype, tree.args.length).map((arg_types) => {
                    return {arg_types, sub_assignments: assignments}
                });
                if(options.length == 0) {
                    continue;
                }
                /* TODO: group options by type of each arg */
                tree.args.forEach((arg, i) => {
                    options = options.map(({arg_types, sub_assignments}) => {
                        const arg_type = arg_types[i];
                        const arg_assignments = find_valid_assignments(arg, scope, sub_assignments, arg_type);
                        return {arg_types, sub_assignments: arg_assignments};
                    }).filter(({arg_types, sub_assignments}) => sub_assignments !== false);
                });
                if(options.length > 0) {
                    return options[0].sub_assignments;
                }
            };
            return false;
        }
        case 'name': {
            const name = jme.normaliseName(tree.tok.name, scope);
            if(scope.getConstant(name)) {
                return assignments;
            }
            // don't care what type is produced: this assignment is fine by default
            // or this name is already assigned to the desired type
            if(outtype === undefined || assignments[name] === outtype) {
                return assignments;

            // this name has been assigned, but not to the desired outtype:
            // find a mututally compatible type to assign to this name, compatible with the desired use and all previous uses
            } else if(assignments[name] !== undefined && assignments[name].type != outtype) {
                var type = mutually_compatible_type(Object.keys(assignments[name].casts));
                if(type) {
                    assignments = util.copyobj(assignments, true);
                    assignments[name].casts[outtype] = true;
                    assignments[name].type = type;
                    return assignments;
                } else {
                    return false;
                }

            // this name has not been assigned: assign it to the desired type
            } else {
                assignments = util.copyobj(assignments, true);
                var casts = {};
                casts[outtype] = true;
                assignments[name] = {
                    type: outtype,
                    casts: casts
                }
                return assignments;
            }
        }
        // all other token types: must be compatible with desired outtype, or we mustn't care what the output type is.
        default: {
            if(outtype && !jme.isTypeCompatible(tree.tok.type, outtype)) {
                return false;
            }

            if(!tree.args) {
                return assignments;
            }

            for(const arg of tree.args) {
                assignments = find_valid_assignments(arg, scope, assignments, undefined);
                if(assignments === false) {
                    return false;
                }
            };
            return assignments;
        }
    }
}
jme.find_valid_assignments = find_valid_assignments;

/** Infer the type of each part of a tree by inferring the types of free variables, then finding definitions of operators and functions which work.
 *
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.Scope} scope
 * @returns {Numbas.jme.tree} Each node in the tree has an `inferred_type` property giving the name of that subtree's inferred type. Function and operator nodes also have a `matched_function` property giving the `Numbas.jme.funcObj` object that would be used.
 */
jme.inferTreeType = function(tree, scope) {
    var assignments = jme.inferVariableTypes(tree, scope);

    /** Construct a stub of a token of the given type, for the type-checker to work against.
     *
     * @param {string} type
     * @returns {Numbas.jme.token}
     */
    function fake_token(type) {
        var tok = {type: type};
        if(jme.types[type]) {
            tok.__proto__ = jme.types[type].prototype;
        }
        return tok;
    }
    for(const [name, assignment] of Object.entries(assignments)) {
        assignments[name] = fake_token(assignment);
    }
    /** Infer the type of a tree.
     *
     * @param {Numbas.jme.tree} tree
     * @returns {string}
     */
    function infer_type(tree) {
        var tok = tree.tok;
        switch(tok.type) {
            case 'name':
                var normalised_name = jme.normaliseName(tok.name, scope);
                var assignment = assignments[normalised_name];
                var constant;
                if(assignment) {
                    inferred_type = assignment.type;
                } else {
                    constant = scope.getConstant(tok.name)
                    if(constant) {
                        inferred_type = constant.value.type;
                    }
                }
                return {tok: tok, inferred_type: inferred_type, constant: constant, normalised_name: normalised_name};
            case 'op':
            case 'function':
                var op = jme.normaliseName(tok.name, scope);
                if(jme.lazyOps.indexOf(op) >= 0) {
                    return {tok: tok, inferred_type: scope.getFunction(op)[0].outtype};
                } else {
                    var iargs = [];
                    var eargs = [];
                    for(let i = 0;i < tree.args.length;i++) {
                        var iarg = infer_type(tree.args[i]);
                        eargs.push(fake_token(iarg.inferred_type));
                        iargs.push(iarg);
                    }
                    var matched_function = scope.matchFunctionToArguments(tok, eargs);
                    var inferred_type = matched_function ? matched_function.fn.outtype : '?';
                    return {tok: tok, args: iargs, inferred_type: inferred_type, matched_function: matched_function};
                }
            default:
                return {tok: tok, inferred_type: tok.type};
        }
    }

    return infer_type(tree);
}

/** Infer the type of an expression by inferring the types of free variables, then finding definitions of operators and functions which work.
 *
 * @param {Numbas.jme.tree} tree
 * @param {Numbas.jme.Scope} scope
 * @see Numbas.jme.inferTreeType
 * @returns {string}
 */
jme.inferExpressionType = function(tree, scope) {
    var inferred_tree = jme.inferTreeType(tree, scope);
    return inferred_tree.inferred_type;
}

/** A dictionary of methods to cast the underlying JS values of JME types to other types.
 * `Numbas.jme.makeFast` uses these to avoid constructing tokens when it has to cast values to other types.
 *
 * @enum {Object<Function>}
 */
const fast_casters = jme.fast_casters = {
    'number': {
        'decimal': number_to_decimal
    },
    'integer': {
        'rational': (n) => new math.Fraction(n, 1),
        'number': (n) => n,
        'decimal': (n) => new math.ComplexDecimal(n)
    },
    'rational': {
        'decimal': (r) => new math.ComplexDecimal((new Decimal(r.numerator)).dividedBy(new Decimal(r.denominator))),
        'number': (r) => r.numerator / r.denominator
    },
    'decimal': {
        'number': decimal_to_number
    }
};


/**
 * Make a function version of an expression tree which can be evaluated quickly by assuming that:
 * - The arguments will always have the same type
 * - All operations have non-lazy, native JS implementations.
 *
 * All of the control flow functions, such as `if` and `switch`, are lazy so can't be used here. Many other functions have implementations which operate on JME tokens, so can't be used either, typically functions operating on collections or sub-expressions.
 * All of the arithmetic and trigonometric operations can be used, so this is good for speeding up the kinds of expressions a student might enter.
 *
 * Giving the names of the arguments makes this much faster: otherwise, each operation involves an Array.map() operation which is very slow.
 * If there are more than 5 free variables or an operation takes more than 5 arguments, a slower method is used.
 *
 * @example
 * const tree = Numbas.jme.compile('(x/2)^y');
 * const f = Numbas.jme.makeFast(tree, Numbas.jme.builtinScope, ['x', 'y']);
 * const a = f(1,2);
 * // a = 0.25;
 *
 * @param {Numbas.jme.tree} tree - The expression tree to be evaluated.
 * @param {Numbas.jme.Scope} scope
 * @param {Array.<string>} [names] - The order of arguments in the returned function, mapping to variable names. If not given, then the function will take a dictionary mapping variable names to values.
 * @returns {Function}
 */
jme.makeFast = function(tree, scope, names) {
    const given_names = names !== undefined;

    /** Make a function which evaluates the given expression tree quickly.
     *
     * @param {Numbas.jme.tree} t
     * @returns {Function}
     */
    function fast_eval(t) {
        switch(t.tok.type) {
            case 'name': {
                if(t.constant) {
                    var constant = jme.unwrapValue(t.constant);
                    return function() {
                        return constant;
                    }
                }
                var name = t.normalised_name;
                if(given_names) {
                    const i = names.indexOf(name);
                    return function() {
                        return arguments[i];
                    }
                } else {
                    return function(params) {
                        return params[name];
                    }
                }
            }
            case 'function':
            case 'op': {
                const args = t.args.map((t2) => fast_eval(t2));
                const fn = t.matched_function && t.matched_function.fn && t.matched_function.fn.fn;
                if(!fn) {
                    throw(new Numbas.Error("jme.makeFast.no fast definition of function", {name: t.tok.name}));
                }
                if(given_names) {
                    if(names.length > 5 || args.length > 5) {
                        return function() {
                            const fargs = arguments;
                            return fn(...args.map((fn) => fn(...fargs)));
                        }
                    }
                    var sig = sig_remove_missing(t.matched_function.signature);

                    /** Wrap a fast function so that it casts the output to the desired type.
                     *
                     * @param {Function} f
                     * @param {string} from_type
                     * @param {string} to_type
                     * @returns {Function}
                     */
                    function make_caster(f, from_type, to_type) {
                        const fast_cast = fast_casters[from_type] && fast_casters[from_type][to_type];
                        const caster = jme.types[from_type].prototype.casts[to_type];
                        if(fast_cast) {
                            if(f.uses_maps) {
                                return function(...params) {
                                    var res = f(...params);
                                    return fast_cast(res);
                                }
                            } else {
                                return function(a1, a2, a3, a4, a5) {
                                    var res = f(a1, a2, a3, a4, a5);
                                    return fast_cast(res);
                                }
                            }
                        } else if(caster) {
                            return function(...params) {
                                var res = f(...params);
                                var tok = new jme.types[from_type](res);
                                var otok = caster.call(tok, tok);
                                return jme.unwrapValue(otok);
                            }
                        } else {
                            return function(...params) {
                                var res = f(...params);
                                var tok = new jme.types[from_type](res);
                                var otok = jme.castToType(tok, to_type);
                                return jme.unwrapValue(otok);
                            }
                        }
                    }
                    for(let i = 0;i < args.length;i++) {
                        const from_type = t.args[i].inferred_type;
                        const to_type = sig[i].type;
                        if(to_type != from_type) {
                            args[i] = make_caster(args[i], from_type, to_type);
                        }
                    }
                    const [f1, f2, f3, f4, f5] = args;
                    if(f5) {
                        return function(a1, a2, a3, a4, a5) {
                            return fn(
                                f1(a1, a2, a3, a4, a5),
                                f2(a1, a2, a3, a4, a5),
                                f3(a1, a2, a3, a4, a5),
                                f4(a1, a2, a3, a4, a5),
                                f5(a1, a2, a3, a4, a5),
                            );
                        }
                    } else if(f4) {
                        return function(a1, a2, a3, a4, a5) {
                            return fn(
                                f1(a1, a2, a3, a4, a5),
                                f2(a1, a2, a3, a4, a5),
                                f3(a1, a2, a3, a4, a5),
                                f4(a1, a2, a3, a4, a5)
                            );
                        }
                    } else if(f3) {
                        return function(a1, a2, a3, a4, a5) {
                            return fn(
                                f1(a1, a2, a3, a4, a5),
                                f2(a1, a2, a3, a4, a5),
                                f3(a1, a2, a3, a4, a5)
                            );
                        }
                    } else if(f2) {
                        return function(a1, a2, a3, a4, a5) {
                            return fn(
                                f1(a1, a2, a3, a4, a5),
                                f2(a1, a2, a3, a4, a5),
                            );
                        }
                    } else if(f1) {
                        return function(a1, a2, a3, a4, a5) {
                            return fn(
                                f1(a1, a2, a3, a4, a5)
                            );
                        }
                    } else {
                        return function(a1, a2, a3, a4, a5) {
                            return fn(a1, a2, a3, a4, a5);
                        }
                    }

                } else {
                    const f = function(params) {
                        const eargs = args.map((f) => f(params));
                        return fn(...eargs);
                    }
                    f.uses_maps = true;
                }
                break;
            }
            default: {
                const value = jme.unwrapValue(t.tok);
                return function() {
                    return value;
                }
            }
        }
    }

    const subbed_tree = jme.substituteTree(tree, scope, true, true);

    /** Replace all integer constants with equivalent numbers, in order to avoid casting to rationals.
     *
     * @param {Numbas.jme.tree} t
     * @returns {Numbas.jme.tree}
     */
    function replace_integers(t) {
        if(t.tok.type == 'integer') {
            return {tok: jme.castToType(t.tok, 'number')};
        }
        if(t.args) {
            t.args = t.args.map((a) => replace_integers(a));
        }
        return t;
    }

    const typed_tree = jme.inferTreeType(replace_integers(subbed_tree), scope);

    const f = fast_eval(typed_tree);

    if(tree.tok.name) {
        Object.defineProperty(f, 'name', {value:tree.tok.name});
    }

    return f;
}

/** Remove "missing" arguments from a signature-checker result.
 *
 * @param {Numbas.jme.signature_result} items
 * @returns {Numbas.jme.signature_result}
 */
function sig_remove_missing(items) {
    return items.filter(function(d) {
        return !d.missing
    });
}

/** A signature-checker function. Takes a list of {@link Numbas.jme.token} objects, and returns a {@link Numbas.jme.signature_result} representing the matched arguments, or `false` if the signature doesn't match.
 *
 * @typedef Numbas.jme.signature
 * @type {Function}
 * @property {string} kind - The kind of this signature checker, e.g. "type", "anything", "multiple". Used by the type inference routine, among other things.
 */

/** A list of arguments matched by a signature checker. At most one per argument passed in.
 *
 * @typedef Numbas.jme.signature_result
 * @type {Array.<Numbas.jme.signature_result_argument>}
 */

/** Information about an argument matched by a signature checker.
 * The main purpose is to specify the desired type of the argument, but there are other properties for certain types.
 *
 * @typedef Numbas.jme.signature_result_argument
 * @type {object}
 * @property {string} type - The data type that the argument should be cast to.
 * @property {boolean} missing - Does this represent an optional argument that wasn't given?
 * @property {boolean} nonspecific - Does this represent an argument matched with an 'anything' signature? If so, don't use it when comparing two signature results.
 */

/** Signature-checking function constructors.
 *
 * @see {Numbas.jme.signature}
 * @enum {Function}
 */
jme.signature = {
    label: function(name, sig) {
        var f = function(args) {
            var result = sig(args);
            if(!result) {
                return false;
            }
            result.forEach(function(r) {
                r.name = name;
            });
            return result;
        };
        f.kind = 'label';
        f.signature = sig;
        return f;
    },
    anything: function() {
        var f = function(args) {
            return args.length > 0 ? [{type: args[0].type, nonspecific: true}] : false;
        }
        f.kind = 'anything';
        return f;
    },
    type: function(type) {
        var f = function(args) {
            if(args.length == 0) {
                return false;
            }
            if(args[0].type != type) {
                var casts = args[0].casts;
                if(!casts || !casts[type]) {
                    return false;
                }
            }
            return [{type: type}];
        }
        f.kind = 'type';
        f.type = type;
        return f;
    },
    multiple: function(sig) {
        var f = function(args) {
            var got = [];
            while(true) {
                var match = sig(args);
                if(match === false) {
                    break;
                }
                args = args.slice(match.length);
                got = got.concat(match);
                if(match.length == 0) {
                    break;
                }
            }
            return got;
        };
        f.kind = 'multiple';
        f.signature = sig;
        return f;
    },
    optional: function(sig) {
        var f = function(args) {
            var match = sig(args);
            if(match) {
                return match;
            } else {
                return [{missing: true}];
            }
        }
        f.kind = 'optional';
        f.signature = sig;
        return f;
    },
    sequence: function() {
        var bits = Array.prototype.slice.apply(arguments);
        var f = function(args) {
            var match = [];
            for(let i = 0;i < bits.length;i++) {
                var bitmatch = bits[i](args);
                if(bitmatch === false) {
                    return false;
                }
                match = match.concat(bitmatch);
                args = args.slice(sig_remove_missing(bitmatch).length);
            }
            return match;
        }
        f.kind = 'sequence';
        f.signatures = bits;
        return f;
    },
    list: function() {
        var bits = Array.prototype.slice.apply(arguments);
        var seq = jme.signature.sequence.apply(this, bits);
        var f = function(args) {
            if(args.length == 0) {
                return false;
            }
            if(!jme.isType(args[0], 'list')) {
                return false;
            }
            var arg = jme.castToType(args[0], 'list');
            var items = seq(arg.value);
            if(items === false || items.length < arg.value.length) {
                return false;
            }
            return [{type: 'list', items: items}];
        }
        f.kind = 'list';
        f.signatures = bits;
        return f;
    },
    listof: function(sig) {
        return jme.signature.list(jme.signature.multiple(sig));
    },
    dict: function(sig) {
        var f = function(args) {
            if(args.length == 0) {
                return false;
            }
            if(!jme.isType(args[0], 'dict')) {
                return false;
            }
            var items = {};
            var entries = Object.entries(args[0].value);
            for(let i = 0;i < entries.length;i++) {
                var key = entries[i][0];
                var value = entries[i][1];
                var m = sig([value]);
                if(m === false) {
                    return false;
                }
                items[key] = m[0];
            }
            return [{type: 'dict', items: items}];
        }
        f.kind = 'dict';
        f.signature = sig;
        return f;
    },
    or: function() {
        var bits = Array.prototype.slice.apply(arguments);
        var f = function(args) {
            for(let i = 0;i < bits.length;i++) {
                var m = bits[i](args);
                if(m !== false) {
                    return m;
                }
            }
            return false;
        }
        f.kind = 'or';
        f.signatures = bits;
        return f;
    }
};

/** A match returned by @ref{Numbas.jme.parse_signature}.
 *
 * @typedef Numbas.jme.signature_grammar_match
 * @type {Array}
 * @property {Numbas.jme.signature} 0 - The matched signature function.
 * @property {string} 1 - The corresponding bit of the definition.
 */

/** Parse a signature definition.
 *
 * Grammar: (there can be any amount of whitespace between tokens)
 *
 * ```
 * SIGNATURE = MULTIPLE | OPTIONAL | EITHER | SINGLE
 * MULTIPLE = "*" SINGLE
 * OPTIONAL = "[" SIGNATURE "]"
 * EITHER = SINGLE "or" SINGLE
 * SINGLE = BRACKETED | LISTOF | DICTOF | ANY | TYPE
 * BRACKETED = "(" SIGNATURE ")"
 * LISTOF = "list of" SIGNATURE
 * DICTOF = "dict of" SIGNATURE
 * ANY = "?"
 * TYPE = \w+
 * ```
 *
 * @param {string|Function} sig - Either a string consisting of an expression in the above grammar, a {@link Numbas.jme.token} constructor, or a {@link Numbas.jme.signature} function.
 * @memberof Numbas.jme
 * @returns {Numbas.jme.signature}
 */
jme.parse_signature = function(sig) {

    /** Return the position of the first non-space character after `pos` in `str`.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {number}
     */
    function strip_space(str, pos) {
        var leading_space = str.slice(pos).match(/^\s*/);
        return pos + leading_space[0].length;
    }

    /** Create a function to exactly match a literal token.
     *
     * @param {string} token
     * @returns {Function}
     */
    function literal(token) {
        return function(str, pos) {
            pos = strip_space(str, pos);
            if(str.slice(pos, token.length + pos) == token) {
                return [token, pos + token.length];
            }
        }
    }

    /** Parse a type description: multiple, optional, either or a single argument or bracketed expression.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function parse_expr(str, pos) {
        pos = strip_space(str, pos || 0);
        return multiple(str, pos) || optional(str, pos) || either(str, pos) || plain_expr(str, pos);
    }
    /** Parse a description of a single argument or bracketed expression: bracketed, list of, dict of, "?" or a type name.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function plain_expr(str, pos) {
        return bracketed(str, pos) || listof(str, pos) || dictof(str, pos) || any(str, pos) || type(str, pos);
    }
    /** Parse an "any number of this" description: "*" EXPR.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function multiple(str, pos) {
        var star = literal("*")(str, pos);
        if(!star) {
            return undefined;
        }
        pos = star[1];
        var expr = plain_expr(str, pos);
        if(!expr) {
            return undefined;
        }
        return [jme.signature.multiple(expr[0]), expr[1]];
    }
    /** Parse an optional argument description: "[" EXPR "]".
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function optional(str, pos) {
        var open = literal("[")(str, pos);
        if(!open) {
            return undefined;
        }
        pos = open[1];
        var expr = parse_expr(str, pos);
        if(!expr) {
            return undefined;
        }
        pos = expr[1];
        var end = literal("]")(str, pos);
        if(!end) {
            return undefined;
        }
        return [jme.signature.optional(expr[0]), end[1]];
    }
    /** Parse a bracketed description: "(" EXPR ")".
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function bracketed(str, pos) {
        var open = literal("(")(str, pos);
        if(!open) {
            return undefined;
        }
        pos = open[1];
        var expr = parse_expr(str, pos);
        if(!expr) {
            return undefined;
        }
        pos = expr[1];
        var end = literal(")")(str, pos);
        if(!pos || !end) {
            return undefined;
        }
        return [expr[0], end[1]];
    }
    /** Parse a "list of" description: "list of" EXPR.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function listof(str, pos) {
        var start = literal("list of")(str, pos);
        if(!start) {
            return undefined;
        }
        pos = start[1];
        var expr = parse_expr(str, pos);
        if(!expr) {
            return undefined;
        }
        return [jme.signature.listof(expr[0]), expr[1]];
    }

    /** Parse a "dict" of description: "dict of" EXPR.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function dictof(str, pos) {
        var start = literal("dict of")(str, pos);
        if(!start) {
            return undefined;
        }
        pos = start[1];
        var expr = parse_expr(str, pos);
        if(!expr) {
            return undefined;
        }
        return [jme.signature.dict(expr[0]), expr[1]];
    }

    /** Parse an "either" description: EXPR "or" EXPR.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function either(str, pos) {
        var expr1 = plain_expr(str, pos);
        if(!expr1) {
            return undefined;
        }
        pos = expr1[1];
        var middle = literal("or")(str, pos);
        if(!middle) {
            return undefined;
        }
        pos = middle[1];
        var expr2 = plain_expr(str, pos);
        if(!expr2) {
            return undefined;
        }
        return [jme.signature.or(expr1[0], expr2[0]), expr2[1]];
    }

    /** Parse an "anything" argument: exactly the string "?".
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function any(str, pos) {
        pos = strip_space(str, pos);
        var m = literal("?")(str, pos);
        if(!m) {
            return undefined;
        }
        return [jme.signature.anything(), m[1]];
    }

    /** Parse a data type name: any string of word characters.
     *
     * @param {string} str
     * @param {number} pos
     * @returns {Numbas.jme.signature_grammar_match}
     */
    function type(str, pos) {
        pos = strip_space(str, pos);
        var m = str.slice(pos).match(/^\w+/);
        if(!m) {
            return undefined;
        }
        var name = m[0];
        return [jme.signature.type(name), pos + name.length];
    }


    if(typeof(sig) == 'function') {
        if(sig.kind !== undefined) {
            return sig;
        }
        return jme.signature.type(sig.prototype.type);
    } else {
        var m = parse_expr(sig);
        if(!m) {
            throw(new Numbas.Error("jme.parse signature.invalid signature string", {str: sig}));
        }
        return m[0];
    }
}

var describe_signature = jme.describe_signature = function(sig) {
    switch(sig.kind) {
        case 'sequence':
            return sig.signatures.map(describe_signature).join(', ');
        case 'anything':
            return '?';
        case 'type':
            return sig.type;
        case 'multiple':
            return describe_signature(sig.signature) + '*';
        case 'optional':
            return '[' + describe_signature(sig.signature) + ']';
        case 'list':
            return 'list of (' + sig.signatures.map(describe_signature) + ')';
        case 'dict':
            return 'dict of ' + describe_signature(sig.signature);
        case 'or':
            return sig.signatures.map(describe_signature).join(' or ');
    }
}


});
;
/** @file Stuff to do with loading from JSON objects. Provides {@link Numbas.json}. */
Numbas.queueScript('json', ['base'], function() {
/** @namespace Numbas.json */
var json = Numbas.json = {
    /** Try to load an attribute with name from `attr` from `source` into `target`.
     * Tries lower-case.
     *
     * @param {object} source - Object to load value(s) from.
     * @param {string|Array.<string>} attrs - The name, or list of names, of attributes to load.
     * @param {object} target - Object to set values in.
     * @param {string|Array.<string>} altnames - The name, or list of names, to set in the target object.
     */
    tryLoad: function(source, attrs, target, altnames) {
        if(!source) {
            return;
        }
        if(typeof(attrs) == 'string') {
            attrs = [attrs];
            altnames = altnames && [altnames];
        }
        altnames = altnames || [];
        for(var i = 0;i < attrs.length;i++) {
            var attr = attrs[i];
            var target_attr = altnames[i] || attr;
            var value = json.tryGet(source, attr);
            if(value !== undefined) {
                if(target_attr in target && typeof target[target_attr] == 'string') {
                    value += '';
                }
                if(target_attr in target && typeof target[target_attr] == 'number') {
                    value = parseFloat(value);
                }
                target[target_attr] = value;
            }
        }
    },
    /** Try to load an attribute with the given name from `source`. The given name and its lower-case equivalent are tried.
     *
     * @param {object} source
     * @param {string} attr
     * @returns {*}
     */
    tryGet: function(source, attr) {
        if(attr in source) {
            return source[attr];
        } else if(attr.toLowerCase() in source) {
            return source[attr.toLowerCase()]
        }
        return undefined;
    }
}
});
;
Numbas.queueScript('knockout',[],function(module) {
/*!
 * Knockout JavaScript library v3.5.0
 * (c) The Knockout.js team - http://knockoutjs.com/
 * License: MIT (http://www.opensource.org/licenses/mit-license.php)
 */

(function() {(function(p){var z=this||(0,eval)("this"),w=z.document,R=z.navigator,v=z.jQuery,H=z.JSON;v||"undefined"===typeof jQuery||(v=jQuery);(function(p){"function"===typeof define&&define.amd?define(["exports","require"],p):"object"===typeof exports&&"object"===typeof module?p(module.exports||exports):p(z.ko={})})(function(S,T){function K(a,c){return null===a||typeof a in W?a===c:!1}function X(b,c){var d;return function(){d||(d=a.a.setTimeout(function(){d=p;b()},c))}}function Y(b,c){var d;return function(){clearTimeout(d);
d=a.a.setTimeout(b,c)}}function Z(a,c){c&&"change"!==c?"beforeChange"===c?this.oc(a):this.bb(a,c):this.pc(a)}function aa(a,c){null!==c&&c.s&&c.s()}function ba(a,c){var d=this.pd,e=d[t];e.qa||(this.Pb&&this.kb[c]?(d.tc(c,a,this.kb[c]),this.kb[c]=null,--this.Pb):e.F[c]||d.tc(c,a,e.G?{da:a}:d.Zc(a)),a.Ka&&a.fd())}var a="undefined"!==typeof S?S:{};a.b=function(b,c){for(var d=b.split("."),e=a,f=0;f<d.length-1;f++)e=e[d[f]];e[d[d.length-1]]=c};a.J=function(a,c,d){a[c]=d};a.version="3.5.0";a.b("version",
a.version);a.options={deferUpdates:!1,useOnlyNativeEvents:!1,foreachHidesDestroyed:!1};a.a=function(){function b(a,b){for(var c in a)f.call(a,c)&&b(c,a[c])}function c(a,b){if(b)for(var c in b)f.call(b,c)&&(a[c]=b[c]);return a}function d(a,b){a.__proto__=b;return a}function e(b,c,d,e){var k=b[c].match(n)||[];a.a.C(d.match(n),function(b){a.a.Oa(k,b,e)});b[c]=k.join(" ")}var f=Object.prototype.hasOwnProperty,g={__proto__:[]}instanceof Array,h="function"===typeof Symbol,m={},l={};m[R&&/Firefox\/2/i.test(R.userAgent)?
"KeyboardEvent":"UIEvents"]=["keyup","keydown","keypress"];m.MouseEvents="click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave".split(" ");b(m,function(a,b){if(b.length)for(var c=0,d=b.length;c<d;c++)l[b[c]]=a});var k={propertychange:!0},q=w&&function(){for(var a=3,b=w.createElement("div"),c=b.getElementsByTagName("i");b.innerHTML="\x3c!--[if gt IE "+ ++a+"]><i></i><![endif]--\x3e",c[0];);return 4<a?a:p}(),n=/\S+/g,r;return{Ic:["authenticity_token",/^__RequestVerificationToken(_.*)?$/],
C:function(a,b,c){for(var d=0,e=a.length;d<e;d++)b.call(c,a[d],d,a)},A:"function"==typeof Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b)}:function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},Lb:function(a,b,c){for(var d=0,e=a.length;d<e;d++)if(b.call(c,a[d],d,a))return a[d];return p},hb:function(b,c){var d=a.a.A(b,c);0<d?b.splice(d,1):0===d&&b.shift()},vc:function(b){var c=[];b&&a.a.C(b,function(b){0>a.a.A(c,b)&&c.push(b)});return c},Mb:function(a,
b,c){var d=[];if(a)for(var e=0,k=a.length;e<k;e++)d.push(b.call(c,a[e],e));return d},fb:function(a,b,c){var d=[];if(a)for(var e=0,k=a.length;e<k;e++)b.call(c,a[e],e)&&d.push(a[e]);return d},gb:function(a,b){if(b instanceof Array)a.push.apply(a,b);else for(var c=0,d=b.length;c<d;c++)a.push(b[c]);return a},Oa:function(b,c,d){var e=a.a.A(a.a.$b(b),c);0>e?d&&b.push(c):d||b.splice(e,1)},Ba:g,extend:c,setPrototypeOf:d,zb:g?d:c,O:b,Ha:function(a,b,c){if(!a)return a;var d={},e;for(e in a)f.call(a,e)&&(d[e]=
b.call(c,a[e],e,a));return d},Sb:function(b){for(;b.firstChild;)a.removeNode(b.firstChild)},Xb:function(b){b=a.a.la(b);for(var c=(b[0]&&b[0].ownerDocument||w).createElement("div"),d=0,e=b.length;d<e;d++)c.appendChild(a.na(b[d]));return c},Ca:function(b,c){for(var d=0,e=b.length,k=[];d<e;d++){var f=b[d].cloneNode(!0);k.push(c?a.na(f):f)}return k},ua:function(b,c){a.a.Sb(b);if(c)for(var d=0,e=c.length;d<e;d++)b.appendChild(c[d])},Wc:function(b,c){var d=b.nodeType?[b]:b;if(0<d.length){for(var e=d[0],
k=e.parentNode,f=0,l=c.length;f<l;f++)k.insertBefore(c[f],e);f=0;for(l=d.length;f<l;f++)a.removeNode(d[f])}},Ua:function(a,b){if(a.length){for(b=8===b.nodeType&&b.parentNode||b;a.length&&a[0].parentNode!==b;)a.splice(0,1);for(;1<a.length&&a[a.length-1].parentNode!==b;)a.length--;if(1<a.length){var c=a[0],d=a[a.length-1];for(a.length=0;c!==d;)a.push(c),c=c.nextSibling;a.push(d)}}return a},Yc:function(a,b){7>q?a.setAttribute("selected",b):a.selected=b},Cb:function(a){return null===a||a===p?"":a.trim?
a.trim():a.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},Td:function(a,b){a=a||"";return b.length>a.length?!1:a.substring(0,b.length)===b},ud:function(a,b){if(a===b)return!0;if(11===a.nodeType)return!1;if(b.contains)return b.contains(1!==a.nodeType?a.parentNode:a);if(b.compareDocumentPosition)return 16==(b.compareDocumentPosition(a)&16);for(;a&&a!=b;)a=a.parentNode;return!!a},Rb:function(b){return a.a.ud(b,b.ownerDocument.documentElement)},jd:function(b){return!!a.a.Lb(b,a.a.Rb)},P:function(a){return a&&
a.tagName&&a.tagName.toLowerCase()},zc:function(b){return a.onError?function(){try{return b.apply(this,arguments)}catch(c){throw a.onError&&a.onError(c),c;}}:b},setTimeout:function(b,c){return setTimeout(a.a.zc(b),c)},Fc:function(b){setTimeout(function(){a.onError&&a.onError(b);throw b;},0)},H:function(b,c,d){var e=a.a.zc(d);d=k[c];if(a.options.useOnlyNativeEvents||d||!v)if(d||"function"!=typeof b.addEventListener)if("undefined"!=typeof b.attachEvent){var f=function(a){e.call(b,a)},l="on"+c;b.attachEvent(l,
f);a.a.I.za(b,function(){b.detachEvent(l,f)})}else throw Error("Browser doesn't support addEventListener or attachEvent");else b.addEventListener(c,e,!1);else r||(r="function"==typeof v(b).on?"on":"bind"),v(b)[r](c,e)},Fb:function(b,c){if(!b||!b.nodeType)throw Error("element must be a DOM node when calling triggerEvent");var d;"input"===a.a.P(b)&&b.type&&"click"==c.toLowerCase()?(d=b.type,d="checkbox"==d||"radio"==d):d=!1;if(a.options.useOnlyNativeEvents||!v||d)if("function"==typeof w.createEvent)if("function"==
typeof b.dispatchEvent)d=w.createEvent(l[c]||"HTMLEvents"),d.initEvent(c,!0,!0,z,0,0,0,0,0,!1,!1,!1,!1,0,b),b.dispatchEvent(d);else throw Error("The supplied element doesn't support dispatchEvent");else if(d&&b.click)b.click();else if("undefined"!=typeof b.fireEvent)b.fireEvent("on"+c);else throw Error("Browser doesn't support triggering events");else v(b).trigger(c)},c:function(b){return a.N(b)?b():b},$b:function(b){return a.N(b)?b.w():b},Eb:function(b,c,d){var k;c&&("object"===typeof b.classList?
(k=b.classList[d?"add":"remove"],a.a.C(c.match(n),function(a){k.call(b.classList,a)})):"string"===typeof b.className.baseVal?e(b.className,"baseVal",c,d):e(b,"className",c,d))},Ab:function(b,c){var d=a.a.c(c);if(null===d||d===p)d="";var e=a.h.firstChild(b);!e||3!=e.nodeType||a.h.nextSibling(e)?a.h.ua(b,[b.ownerDocument.createTextNode(d)]):e.data=d;a.a.zd(b)},Xc:function(a,b){a.name=b;if(7>=q)try{var c=a.name.replace(/[&<>'"]/g,function(a){return"&#"+a.charCodeAt(0)+";"});a.mergeAttributes(w.createElement("<input name='"+
c+"'/>"),!1)}catch(d){}},zd:function(a){9<=q&&(a=1==a.nodeType?a:a.parentNode,a.style&&(a.style.zoom=a.style.zoom))},vd:function(a){if(q){var b=a.style.width;a.style.width=0;a.style.width=b}},Od:function(b,c){b=a.a.c(b);c=a.a.c(c);for(var d=[],e=b;e<=c;e++)d.push(e);return d},la:function(a){for(var b=[],c=0,d=a.length;c<d;c++)b.push(a[c]);return b},Da:function(a){return h?Symbol(a):a},Xd:6===q,Yd:7===q,W:q,Kc:function(b,c){for(var d=a.a.la(b.getElementsByTagName("input")).concat(a.a.la(b.getElementsByTagName("textarea"))),
e="string"==typeof c?function(a){return a.name===c}:function(a){return c.test(a.name)},k=[],f=d.length-1;0<=f;f--)e(d[f])&&k.push(d[f]);return k},Md:function(b){return"string"==typeof b&&(b=a.a.Cb(b))?H&&H.parse?H.parse(b):(new Function("return "+b))():null},fc:function(b,c,d){if(!H||!H.stringify)throw Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
return H.stringify(a.a.c(b),c,d)},Nd:function(c,d,e){e=e||{};var k=e.params||{},f=e.includeFields||this.Ic,l=c;if("object"==typeof c&&"form"===a.a.P(c))for(var l=c.action,h=f.length-1;0<=h;h--)for(var g=a.a.Kc(c,f[h]),m=g.length-1;0<=m;m--)k[g[m].name]=g[m].value;d=a.a.c(d);var n=w.createElement("form");n.style.display="none";n.action=l;n.method="post";for(var q in d)c=w.createElement("input"),c.type="hidden",c.name=q,c.value=a.a.fc(a.a.c(d[q])),n.appendChild(c);b(k,function(a,b){var c=w.createElement("input");
c.type="hidden";c.name=a;c.value=b;n.appendChild(c)});w.body.appendChild(n);e.submitter?e.submitter(n):n.submit();setTimeout(function(){n.parentNode.removeChild(n)},0)}}}();a.b("utils",a.a);a.b("utils.arrayForEach",a.a.C);a.b("utils.arrayFirst",a.a.Lb);a.b("utils.arrayFilter",a.a.fb);a.b("utils.arrayGetDistinctValues",a.a.vc);a.b("utils.arrayIndexOf",a.a.A);a.b("utils.arrayMap",a.a.Mb);a.b("utils.arrayPushAll",a.a.gb);a.b("utils.arrayRemoveItem",a.a.hb);a.b("utils.cloneNodes",a.a.Ca);a.b("utils.createSymbolOrString",
a.a.Da);a.b("utils.extend",a.a.extend);a.b("utils.fieldsIncludedWithJsonPost",a.a.Ic);a.b("utils.getFormFields",a.a.Kc);a.b("utils.objectMap",a.a.Ha);a.b("utils.peekObservable",a.a.$b);a.b("utils.postJson",a.a.Nd);a.b("utils.parseJson",a.a.Md);a.b("utils.registerEventHandler",a.a.H);a.b("utils.stringifyJson",a.a.fc);a.b("utils.range",a.a.Od);a.b("utils.toggleDomNodeCssClass",a.a.Eb);a.b("utils.triggerEvent",a.a.Fb);a.b("utils.unwrapObservable",a.a.c);a.b("utils.objectForEach",a.a.O);a.b("utils.addOrRemoveItem",
a.a.Oa);a.b("utils.setTextContent",a.a.Ab);a.b("unwrap",a.a.c);Function.prototype.bind||(Function.prototype.bind=function(a){var c=this;if(1===arguments.length)return function(){return c.apply(a,arguments)};var d=Array.prototype.slice.call(arguments,1);return function(){var e=d.slice(0);e.push.apply(e,arguments);return c.apply(a,e)}});a.a.g=new function(){var b=0,c="__ko__"+(new Date).getTime(),d={},e,f;a.a.W?(e=function(a,e){var f=a[c];if(!f||"null"===f||!d[f]){if(!e)return p;f=a[c]="ko"+b++;d[f]=
{}}return d[f]},f=function(a){var b=a[c];return b?(delete d[b],a[c]=null,!0):!1}):(e=function(a,b){var d=a[c];!d&&b&&(d=a[c]={});return d},f=function(a){return a[c]?(delete a[c],!0):!1});return{get:function(a,b){var c=e(a,!1);return c&&c[b]},set:function(a,b,c){(a=e(a,c!==p))&&(a[b]=c)},Tb:function(a,b,c){a=e(a,!0);return a[b]||(a[b]=c)},clear:f,Z:function(){return b++ +c}}};a.b("utils.domData",a.a.g);a.b("utils.domData.clear",a.a.g.clear);a.a.I=new function(){function b(b,c){var d=a.a.g.get(b,e);
d===p&&c&&(d=[],a.a.g.set(b,e,d));return d}function c(c){var e=b(c,!1);if(e)for(var e=e.slice(0),f=0;f<e.length;f++)e[f](c);a.a.g.clear(c);a.a.I.cleanExternalData(c);g[c.nodeType]&&d(c.childNodes,!0)}function d(b,d){for(var e=[],k,f=0;f<b.length;f++)if(!d||8===b[f].nodeType)if(c(e[e.length]=k=b[f]),b[f]!==k)for(;f--&&-1==a.a.A(e,b[f]););}var e=a.a.g.Z(),f={1:!0,8:!0,9:!0},g={1:!0,9:!0};return{za:function(a,c){if("function"!=typeof c)throw Error("Callback must be a function");b(a,!0).push(c)},xb:function(c,
d){var f=b(c,!1);f&&(a.a.hb(f,d),0==f.length&&a.a.g.set(c,e,p))},na:function(a){f[a.nodeType]&&(c(a),g[a.nodeType]&&d(a.getElementsByTagName("*")));return a},removeNode:function(b){a.na(b);b.parentNode&&b.parentNode.removeChild(b)},cleanExternalData:function(a){v&&"function"==typeof v.cleanData&&v.cleanData([a])}}};a.na=a.a.I.na;a.removeNode=a.a.I.removeNode;a.b("cleanNode",a.na);a.b("removeNode",a.removeNode);a.b("utils.domNodeDisposal",a.a.I);a.b("utils.domNodeDisposal.addDisposeCallback",a.a.I.za);
a.b("utils.domNodeDisposal.removeDisposeCallback",a.a.I.xb);(function(){var b=[0,"",""],c=[1,"<table>","</table>"],d=[3,"<table><tbody><tr>","</tr></tbody></table>"],e=[1,"<select multiple='multiple'>","</select>"],f={thead:c,tbody:c,tfoot:c,tr:[2,"<table><tbody>","</tbody></table>"],td:d,th:d,option:e,optgroup:e},g=8>=a.a.W;a.a.ta=function(c,d){var e;if(v)if(v.parseHTML)e=v.parseHTML(c,d)||[];else{if((e=v.clean([c],d))&&e[0]){for(var k=e[0];k.parentNode&&11!==k.parentNode.nodeType;)k=k.parentNode;
k.parentNode&&k.parentNode.removeChild(k)}}else{(e=d)||(e=w);var k=e.parentWindow||e.defaultView||z,q=a.a.Cb(c).toLowerCase(),n=e.createElement("div"),r;r=(q=q.match(/^(?:\x3c!--.*?--\x3e\s*?)*?<([a-z]+)[\s>]/))&&f[q[1]]||b;q=r[0];r="ignored<div>"+r[1]+c+r[2]+"</div>";"function"==typeof k.innerShiv?n.appendChild(k.innerShiv(r)):(g&&e.body.appendChild(n),n.innerHTML=r,g&&n.parentNode.removeChild(n));for(;q--;)n=n.lastChild;e=a.a.la(n.lastChild.childNodes)}return e};a.a.Ld=function(b,c){var d=a.a.ta(b,
c);return d.length&&d[0].parentElement||a.a.Xb(d)};a.a.dc=function(b,c){a.a.Sb(b);c=a.a.c(c);if(null!==c&&c!==p)if("string"!=typeof c&&(c=c.toString()),v)v(b).html(c);else for(var d=a.a.ta(c,b.ownerDocument),e=0;e<d.length;e++)b.appendChild(d[e])}})();a.b("utils.parseHtmlFragment",a.a.ta);a.b("utils.setHtml",a.a.dc);a.aa=function(){function b(c,e){if(c)if(8==c.nodeType){var f=a.aa.Tc(c.nodeValue);null!=f&&e.push({sd:c,Jd:f})}else if(1==c.nodeType)for(var f=0,g=c.childNodes,h=g.length;f<h;f++)b(g[f],
e)}var c={};return{Wb:function(a){if("function"!=typeof a)throw Error("You can only pass a function to ko.memoization.memoize()");var b=(4294967296*(1+Math.random())|0).toString(16).substring(1)+(4294967296*(1+Math.random())|0).toString(16).substring(1);c[b]=a;return"\x3c!--[ko_memo:"+b+"]--\x3e"},ad:function(a,b){var f=c[a];if(f===p)throw Error("Couldn't find any memo with ID "+a+". Perhaps it's already been unmemoized.");try{return f.apply(null,b||[]),!0}finally{delete c[a]}},bd:function(c,e){var f=
[];b(c,f);for(var g=0,h=f.length;g<h;g++){var m=f[g].sd,l=[m];e&&a.a.gb(l,e);a.aa.ad(f[g].Jd,l);m.nodeValue="";m.parentNode&&m.parentNode.removeChild(m)}},Tc:function(a){return(a=a.match(/^\[ko_memo\:(.*?)\]$/))?a[1]:null}}}();a.b("memoization",a.aa);a.b("memoization.memoize",a.aa.Wb);a.b("memoization.unmemoize",a.aa.ad);a.b("memoization.parseMemoText",a.aa.Tc);a.b("memoization.unmemoizeDomNodeAndDescendants",a.aa.bd);a.ma=function(){function b(){if(f)for(var b=f,c=0,d;h<f;)if(d=e[h++]){if(h>b){if(5E3<=
++c){h=f;a.a.Fc(Error("'Too much recursion' after processing "+c+" task groups."));break}b=f}try{d()}catch(g){a.a.Fc(g)}}}function c(){b();h=f=e.length=0}var d,e=[],f=0,g=1,h=0;z.MutationObserver?d=function(a){var b=w.createElement("div");(new MutationObserver(a)).observe(b,{attributes:!0});return function(){b.classList.toggle("foo")}}(c):d=w&&"onreadystatechange"in w.createElement("script")?function(a){var b=w.createElement("script");b.onreadystatechange=function(){b.onreadystatechange=null;w.documentElement.removeChild(b);
b=null;a()};w.documentElement.appendChild(b)}:function(a){setTimeout(a,0)};return{scheduler:d,yb:function(b){f||a.ma.scheduler(c);e[f++]=b;return g++},cancel:function(a){a=a-(g-f);a>=h&&a<f&&(e[a]=null)},resetForTesting:function(){var a=f-h;h=f=e.length=0;return a},Rd:b}}();a.b("tasks",a.ma);a.b("tasks.schedule",a.ma.yb);a.b("tasks.runEarly",a.ma.Rd);a.Ta={throttle:function(b,c){b.throttleEvaluation=c;var d=null;return a.$({read:b,write:function(e){clearTimeout(d);d=a.a.setTimeout(function(){b(e)},
c)}})},rateLimit:function(a,c){var d,e,f;"number"==typeof c?d=c:(d=c.timeout,e=c.method);a.Hb=!1;f="function"==typeof e?e:"notifyWhenChangesStop"==e?Y:X;a.tb(function(a){return f(a,d,c)})},deferred:function(b,c){if(!0!==c)throw Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");b.Hb||(b.Hb=!0,b.tb(function(c){var e,f=!1;return function(){if(!f){a.ma.cancel(e);e=a.ma.yb(c);try{f=!0,b.notifySubscribers(p,"dirty")}finally{f=
!1}}}}))},notify:function(a,c){a.equalityComparer="always"==c?null:K}};var W={undefined:1,"boolean":1,number:1,string:1};a.b("extenders",a.Ta);a.gc=function(b,c,d){this.da=b;this.kc=c;this.lc=d;this.Ib=!1;this.ab=this.Jb=null;a.J(this,"dispose",this.s);a.J(this,"disposeWhenNodeIsRemoved",this.l)};a.gc.prototype.s=function(){this.Ib||(this.ab&&a.a.I.xb(this.Jb,this.ab),this.Ib=!0,this.lc(),this.da=this.kc=this.lc=this.Jb=this.ab=null)};a.gc.prototype.l=function(b){this.Jb=b;a.a.I.za(b,this.ab=this.s.bind(this))};
a.R=function(){a.a.zb(this,D);D.ob(this)};var D={ob:function(a){a.S={change:[]};a.rc=1},subscribe:function(b,c,d){var e=this;d=d||"change";var f=new a.gc(e,c?b.bind(c):b,function(){a.a.hb(e.S[d],f);e.cb&&e.cb(d)});e.Qa&&e.Qa(d);e.S[d]||(e.S[d]=[]);e.S[d].push(f);return f},notifySubscribers:function(b,c){c=c||"change";"change"===c&&this.Gb();if(this.Wa(c)){var d="change"===c&&this.dd||this.S[c].slice(0);try{a.v.wc();for(var e=0,f;f=d[e];++e)f.Ib||f.kc(b)}finally{a.v.end()}}},mb:function(){return this.rc},
Cd:function(a){return this.mb()!==a},Gb:function(){++this.rc},tb:function(b){var c=this,d=a.N(c),e,f,g,h,m;c.bb||(c.bb=c.notifySubscribers,c.notifySubscribers=Z);var l=b(function(){c.Ka=!1;d&&h===c&&(h=c.mc?c.mc():c());var a=f||m&&c.qb(g,h);m=f=e=!1;a&&c.bb(g=h)});c.pc=function(a,b){b&&c.Ka||(m=!b);c.dd=c.S.change.slice(0);c.Ka=e=!0;h=a;l()};c.oc=function(a){e||(g=a,c.bb(a,"beforeChange"))};c.qc=function(){m=!0};c.fd=function(){c.qb(g,c.w(!0))&&(f=!0)}},Wa:function(a){return this.S[a]&&this.S[a].length},
Ad:function(b){if(b)return this.S[b]&&this.S[b].length||0;var c=0;a.a.O(this.S,function(a,b){"dirty"!==a&&(c+=b.length)});return c},qb:function(a,c){return!this.equalityComparer||!this.equalityComparer(a,c)},toString:function(){return"[object Object]"},extend:function(b){var c=this;b&&a.a.O(b,function(b,e){var f=a.Ta[b];"function"==typeof f&&(c=f(c,e)||c)});return c}};a.J(D,"init",D.ob);a.J(D,"subscribe",D.subscribe);a.J(D,"extend",D.extend);a.J(D,"getSubscriptionsCount",D.Ad);a.a.Ba&&a.a.setPrototypeOf(D,
Function.prototype);a.R.fn=D;a.Pc=function(a){return null!=a&&"function"==typeof a.subscribe&&"function"==typeof a.notifySubscribers};a.b("subscribable",a.R);a.b("isSubscribable",a.Pc);a.U=a.v=function(){function b(a){d.push(e);e=a}function c(){e=d.pop()}var d=[],e,f=0;return{wc:b,end:c,ac:function(b){if(e){if(!a.Pc(b))throw Error("Only subscribable things can act as dependencies");e.nd.call(e.od,b,b.ed||(b.ed=++f))}},K:function(a,d,e){try{return b(),a.apply(d,e||[])}finally{c()}},pa:function(){if(e)return e.o.pa()},
Va:function(){if(e)return e.o.Va()},rb:function(){if(e)return e.rb},o:function(){if(e)return e.o}}}();a.b("computedContext",a.U);a.b("computedContext.getDependenciesCount",a.U.pa);a.b("computedContext.getDependencies",a.U.Va);a.b("computedContext.isInitial",a.U.rb);a.b("computedContext.registerDependency",a.U.ac);a.b("ignoreDependencies",a.Wd=a.v.K);var I=a.a.Da("_latestValue");a.sa=function(b){function c(){if(0<arguments.length)return c.qb(c[I],arguments[0])&&(c.xa(),c[I]=arguments[0],c.wa()),this;
a.v.ac(c);return c[I]}c[I]=b;a.a.Ba||a.a.extend(c,a.R.fn);a.R.fn.ob(c);a.a.zb(c,F);a.options.deferUpdates&&a.Ta.deferred(c,!0);return c};var F={equalityComparer:K,w:function(){return this[I]},wa:function(){this.notifySubscribers(this[I],"spectate");this.notifySubscribers(this[I])},xa:function(){this.notifySubscribers(this[I],"beforeChange")}};a.a.Ba&&a.a.setPrototypeOf(F,a.R.fn);var G=a.sa.Na="__ko_proto__";F[G]=a.sa;a.N=function(b){if((b="function"==typeof b&&b[G])&&b!==F[G]&&b!==a.o.fn[G])throw Error("Invalid object that looks like an observable; possibly from another Knockout instance");
return!!b};a.Ya=function(b){return"function"==typeof b&&(b[G]===F[G]||b[G]===a.o.fn[G]&&b.Mc)};a.b("observable",a.sa);a.b("isObservable",a.N);a.b("isWriteableObservable",a.Ya);a.b("isWritableObservable",a.Ya);a.b("observable.fn",F);a.J(F,"peek",F.w);a.J(F,"valueHasMutated",F.wa);a.J(F,"valueWillMutate",F.xa);a.Ia=function(b){b=b||[];if("object"!=typeof b||!("length"in b))throw Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");b=a.sa(b);a.a.zb(b,
a.Ia.fn);return b.extend({trackArrayChanges:!0})};a.Ia.fn={remove:function(b){for(var c=this.w(),d=[],e="function"!=typeof b||a.N(b)?function(a){return a===b}:b,f=0;f<c.length;f++){var g=c[f];if(e(g)){0===d.length&&this.xa();if(c[f]!==g)throw Error("Array modified during remove; cannot remove item");d.push(g);c.splice(f,1);f--}}d.length&&this.wa();return d},removeAll:function(b){if(b===p){var c=this.w(),d=c.slice(0);this.xa();c.splice(0,c.length);this.wa();return d}return b?this.remove(function(c){return 0<=
a.a.A(b,c)}):[]},destroy:function(b){var c=this.w(),d="function"!=typeof b||a.N(b)?function(a){return a===b}:b;this.xa();for(var e=c.length-1;0<=e;e--){var f=c[e];d(f)&&(f._destroy=!0)}this.wa()},destroyAll:function(b){return b===p?this.destroy(function(){return!0}):b?this.destroy(function(c){return 0<=a.a.A(b,c)}):[]},indexOf:function(b){var c=this();return a.a.A(c,b)},replace:function(a,c){var d=this.indexOf(a);0<=d&&(this.xa(),this.w()[d]=c,this.wa())},sorted:function(a){var c=this().slice(0);
return a?c.sort(a):c.sort()},reversed:function(){return this().slice(0).reverse()}};a.a.Ba&&a.a.setPrototypeOf(a.Ia.fn,a.sa.fn);a.a.C("pop push reverse shift sort splice unshift".split(" "),function(b){a.Ia.fn[b]=function(){var a=this.w();this.xa();this.yc(a,b,arguments);var d=a[b].apply(a,arguments);this.wa();return d===a?this:d}});a.a.C(["slice"],function(b){a.Ia.fn[b]=function(){var a=this();return a[b].apply(a,arguments)}});a.Oc=function(b){return a.N(b)&&"function"==typeof b.remove&&"function"==
typeof b.push};a.b("observableArray",a.Ia);a.b("isObservableArray",a.Oc);a.Ta.trackArrayChanges=function(b,c){function d(){function c(){if(h){var d=[].concat(b.w()||[]);if(b.Wa("arrayChange")){var e;if(!f||1<h)f=a.a.Ob(m,d,b.Nb);e=f}m=d;f=null;h=0;e&&e.length&&b.notifySubscribers(e,"arrayChange")}}e?c():(e=!0,l=b.notifySubscribers,b.notifySubscribers=function(a,b){b&&"change"!==b||++h;return l.apply(this,arguments)},m=[].concat(b.w()||[]),f=null,g=b.subscribe(c))}b.Nb={};c&&"object"==typeof c&&a.a.extend(b.Nb,
c);b.Nb.sparse=!0;if(!b.yc){var e=!1,f=null,g,h=0,m,l,k=b.Qa,q=b.cb;b.Qa=function(a){k&&k.call(b,a);"arrayChange"===a&&d()};b.cb=function(a){q&&q.call(b,a);"arrayChange"!==a||b.Wa("arrayChange")||(l&&(b.notifySubscribers=l,l=p),g&&g.s(),g=null,e=!1,m=p)};b.yc=function(b,c,d){function k(a,b,c){return l[l.length]={status:a,value:b,index:c}}if(e&&!h){var l=[],g=b.length,q=d.length,m=0;switch(c){case "push":m=g;case "unshift":for(c=0;c<q;c++)k("added",d[c],m+c);break;case "pop":m=g-1;case "shift":g&&
k("deleted",b[m],m);break;case "splice":c=Math.min(Math.max(0,0>d[0]?g+d[0]:d[0]),g);for(var g=1===q?g:Math.min(c+(d[1]||0),g),q=c+q-2,m=Math.max(g,q),U=[],L=[],p=2;c<m;++c,++p)c<g&&L.push(k("deleted",b[c],c)),c<q&&U.push(k("added",d[p],c));a.a.Jc(L,U);break;default:return}f=l}}}};var t=a.a.Da("_state");a.o=a.$=function(b,c,d){function e(){if(0<arguments.length){if("function"===typeof f)f.apply(g.lb,arguments);else throw Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
return this}g.qa||a.v.ac(e);(g.ka||g.G&&e.Xa())&&e.ha();return g.X}"object"===typeof b?d=b:(d=d||{},b&&(d.read=b));if("function"!=typeof d.read)throw Error("Pass a function that returns the value of the ko.computed");var f=d.write,g={X:p,ra:!0,ka:!0,pb:!1,hc:!1,qa:!1,vb:!1,G:!1,Vc:d.read,lb:c||d.owner,l:d.disposeWhenNodeIsRemoved||d.l||null,Sa:d.disposeWhen||d.Sa,Qb:null,F:{},V:0,Hc:null};e[t]=g;e.Mc="function"===typeof f;a.a.Ba||a.a.extend(e,a.R.fn);a.R.fn.ob(e);a.a.zb(e,C);d.pure?(g.vb=!0,g.G=!0,
a.a.extend(e,da)):d.deferEvaluation&&a.a.extend(e,ea);a.options.deferUpdates&&a.Ta.deferred(e,!0);g.l&&(g.hc=!0,g.l.nodeType||(g.l=null));g.G||d.deferEvaluation||e.ha();g.l&&e.ja()&&a.a.I.za(g.l,g.Qb=function(){e.s()});return e};var C={equalityComparer:K,pa:function(){return this[t].V},Va:function(){var b=[];a.a.O(this[t].F,function(a,d){b[d.La]=d.da});return b},Ub:function(b){if(!this[t].V)return!1;var c=this.Va();return-1!==a.a.A(c,b)?!0:!!a.a.Lb(c,function(a){return a.Ub&&a.Ub(b)})},tc:function(a,
c,d){if(this[t].vb&&c===this)throw Error("A 'pure' computed must not be called recursively");this[t].F[a]=d;d.La=this[t].V++;d.Ma=c.mb()},Xa:function(){var a,c,d=this[t].F;for(a in d)if(Object.prototype.hasOwnProperty.call(d,a)&&(c=d[a],this.Ja&&c.da.Ka||c.da.Cd(c.Ma)))return!0},Id:function(){this.Ja&&!this[t].pb&&this.Ja(!1)},ja:function(){var a=this[t];return a.ka||0<a.V},Qd:function(){this.Ka?this[t].ka&&(this[t].ra=!0):this.Gc()},Zc:function(a){if(a.Hb){var c=a.subscribe(this.Id,this,"dirty"),
d=a.subscribe(this.Qd,this);return{da:a,s:function(){c.s();d.s()}}}return a.subscribe(this.Gc,this)},Gc:function(){var b=this,c=b.throttleEvaluation;c&&0<=c?(clearTimeout(this[t].Hc),this[t].Hc=a.a.setTimeout(function(){b.ha(!0)},c)):b.Ja?b.Ja(!0):b.ha(!0)},ha:function(b){var c=this[t],d=c.Sa,e=!1;if(!c.pb&&!c.qa){if(c.l&&!a.a.Rb(c.l)||d&&d()){if(!c.hc){this.s();return}}else c.hc=!1;c.pb=!0;try{e=this.yd(b)}finally{c.pb=!1}return e}},yd:function(b){var c=this[t],d=!1,e=c.vb?p:!c.V,d={pd:this,kb:c.F,
Pb:c.V};a.v.wc({od:d,nd:ba,o:this,rb:e});c.F={};c.V=0;var f=this.xd(c,d);c.V?d=this.qb(c.X,f):(this.s(),d=!0);d&&(c.G?this.Gb():this.notifySubscribers(c.X,"beforeChange"),c.X=f,this.notifySubscribers(c.X,"spectate"),!c.G&&b&&this.notifySubscribers(c.X),this.qc&&this.qc());e&&this.notifySubscribers(c.X,"awake");return d},xd:function(b,c){try{var d=b.Vc;return b.lb?d.call(b.lb):d()}finally{a.v.end(),c.Pb&&!b.G&&a.a.O(c.kb,aa),b.ra=b.ka=!1}},w:function(a){var c=this[t];(c.ka&&(a||!c.V)||c.G&&this.Xa())&&
this.ha();return c.X},tb:function(b){a.R.fn.tb.call(this,b);this.mc=function(){this[t].G||(this[t].ra?this.ha():this[t].ka=!1);return this[t].X};this.Ja=function(a){this.oc(this[t].X);this[t].ka=!0;a&&(this[t].ra=!0);this.pc(this,!a)}},s:function(){var b=this[t];!b.G&&b.F&&a.a.O(b.F,function(a,b){b.s&&b.s()});b.l&&b.Qb&&a.a.I.xb(b.l,b.Qb);b.F=p;b.V=0;b.qa=!0;b.ra=!1;b.ka=!1;b.G=!1;b.l=p;b.Sa=p;b.Vc=p;this.Mc||(b.lb=p)}},da={Qa:function(b){var c=this,d=c[t];if(!d.qa&&d.G&&"change"==b){d.G=!1;if(d.ra||
c.Xa())d.F=null,d.V=0,c.ha()&&c.Gb();else{var e=[];a.a.O(d.F,function(a,b){e[b.La]=a});a.a.C(e,function(a,b){var e=d.F[a],m=c.Zc(e.da);m.La=b;m.Ma=e.Ma;d.F[a]=m});c.Xa()&&c.ha()&&c.Gb()}d.qa||c.notifySubscribers(d.X,"awake")}},cb:function(b){var c=this[t];c.qa||"change"!=b||this.Wa("change")||(a.a.O(c.F,function(a,b){b.s&&(c.F[a]={da:b.da,La:b.La,Ma:b.Ma},b.s())}),c.G=!0,this.notifySubscribers(p,"asleep"))},mb:function(){var b=this[t];b.G&&(b.ra||this.Xa())&&this.ha();return a.R.fn.mb.call(this)}},
ea={Qa:function(a){"change"!=a&&"beforeChange"!=a||this.w()}};a.a.Ba&&a.a.setPrototypeOf(C,a.R.fn);var N=a.sa.Na;C[N]=a.o;a.Nc=function(a){return"function"==typeof a&&a[N]===C[N]};a.Ed=function(b){return a.Nc(b)&&b[t]&&b[t].vb};a.b("computed",a.o);a.b("dependentObservable",a.o);a.b("isComputed",a.Nc);a.b("isPureComputed",a.Ed);a.b("computed.fn",C);a.J(C,"peek",C.w);a.J(C,"dispose",C.s);a.J(C,"isActive",C.ja);a.J(C,"getDependenciesCount",C.pa);a.J(C,"getDependencies",C.Va);a.wb=function(b,c){if("function"===
typeof b)return a.o(b,c,{pure:!0});b=a.a.extend({},b);b.pure=!0;return a.o(b,c)};a.b("pureComputed",a.wb);(function(){function b(a,f,g){g=g||new d;a=f(a);if("object"!=typeof a||null===a||a===p||a instanceof RegExp||a instanceof Date||a instanceof String||a instanceof Number||a instanceof Boolean)return a;var h=a instanceof Array?[]:{};g.save(a,h);c(a,function(c){var d=f(a[c]);switch(typeof d){case "boolean":case "number":case "string":case "function":h[c]=d;break;case "object":case "undefined":var k=
g.get(d);h[c]=k!==p?k:b(d,f,g)}});return h}function c(a,b){if(a instanceof Array){for(var c=0;c<a.length;c++)b(c);"function"==typeof a.toJSON&&b("toJSON")}else for(c in a)b(c)}function d(){this.keys=[];this.values=[]}a.$c=function(c){if(0==arguments.length)throw Error("When calling ko.toJS, pass the object you want to convert.");return b(c,function(b){for(var c=0;a.N(b)&&10>c;c++)b=b();return b})};a.toJSON=function(b,c,d){b=a.$c(b);return a.a.fc(b,c,d)};d.prototype={constructor:d,save:function(b,
c){var d=a.a.A(this.keys,b);0<=d?this.values[d]=c:(this.keys.push(b),this.values.push(c))},get:function(b){b=a.a.A(this.keys,b);return 0<=b?this.values[b]:p}}})();a.b("toJS",a.$c);a.b("toJSON",a.toJSON);a.Vd=function(b,c,d){function e(c){var e=a.wb(b,d).extend({Ga:"always"}),h=e.subscribe(function(a){a&&(h.s(),c(a))});e.notifySubscribers(e.w());return h}return"function"!==typeof Promise||c?e(c.bind(d)):new Promise(e)};a.b("when",a.Vd);(function(){a.u={L:function(b){switch(a.a.P(b)){case "option":return!0===
b.__ko__hasDomDataOptionValue__?a.a.g.get(b,a.f.options.Yb):7>=a.a.W?b.getAttributeNode("value")&&b.getAttributeNode("value").specified?b.value:b.text:b.value;case "select":return 0<=b.selectedIndex?a.u.L(b.options[b.selectedIndex]):p;default:return b.value}},ya:function(b,c,d){switch(a.a.P(b)){case "option":"string"===typeof c?(a.a.g.set(b,a.f.options.Yb,p),"__ko__hasDomDataOptionValue__"in b&&delete b.__ko__hasDomDataOptionValue__,b.value=c):(a.a.g.set(b,a.f.options.Yb,c),b.__ko__hasDomDataOptionValue__=
!0,b.value="number"===typeof c?c:"");break;case "select":if(""===c||null===c)c=p;for(var e=-1,f=0,g=b.options.length,h;f<g;++f)if(h=a.u.L(b.options[f]),h==c||""===h&&c===p){e=f;break}if(d||0<=e||c===p&&1<b.size)b.selectedIndex=e,6===a.a.W&&a.a.setTimeout(function(){b.selectedIndex=e},0);break;default:if(null===c||c===p)c="";b.value=c}}}})();a.b("selectExtensions",a.u);a.b("selectExtensions.readValue",a.u.L);a.b("selectExtensions.writeValue",a.u.ya);a.m=function(){function b(b){b=a.a.Cb(b);123===b.charCodeAt(0)&&
(b=b.slice(1,-1));b+="\n,";var c=[],d=b.match(e),q,n=[],h=0;if(1<d.length){for(var y=0,A;A=d[y];++y){var u=A.charCodeAt(0);if(44===u){if(0>=h){c.push(q&&n.length?{key:q,value:n.join("")}:{unknown:q||n.join("")});q=h=0;n=[];continue}}else if(58===u){if(!h&&!q&&1===n.length){q=n.pop();continue}}else if(47===u&&1<A.length&&(47===A.charCodeAt(1)||42===A.charCodeAt(1)))continue;else 47===u&&y&&1<A.length?(u=d[y-1].match(f))&&!g[u[0]]&&(b=b.substr(b.indexOf(A)+1),d=b.match(e),y=-1,A="/"):40===u||123===
u||91===u?++h:41===u||125===u||93===u?--h:q||n.length||34!==u&&39!==u||(A=A.slice(1,-1));n.push(A)}if(0<h)throw Error("Unbalanced parentheses, braces, or brackets");}return c}var c=["true","false","null","undefined"],d=/^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i,e=RegExp("\"(?:\\\\.|[^\"])*\"|'(?:\\\\.|[^'])*'|`(?:\\\\.|[^`])*`|/\\*(?:[^*]|\\*+[^*/])*\\*+/|//.*\n|/(?:\\\\.|[^/])+/w*|[^\\s:,/][^,\"'`{}()/:[\\]]*[^\\s,\"'`{}()/:[\\]]|[^\\s]","g"),f=/[\])"'A-Za-z0-9_$]+$/,g={"in":1,"return":1,
"typeof":1},h={};return{Ra:[],va:h,Zb:b,ub:function(e,f){function k(b,e){var f;if(!y){var l=a.getBindingHandler(b);if(l&&l.preprocess&&!(e=l.preprocess(e,b,k)))return;if(l=h[b])f=e,0<=a.a.A(c,f)?f=!1:(l=f.match(d),f=null===l?!1:l[1]?"Object("+l[1]+")"+l[2]:f),l=f;l&&n.push("'"+("string"==typeof h[b]?h[b]:b)+"':function(_z){"+f+"=_z}")}g&&(e="function(){return "+e+" }");q.push("'"+b+"':"+e)}f=f||{};var q=[],n=[],g=f.valueAccessors,y=f.bindingParams,A="string"===typeof e?b(e):e;a.a.C(A,function(a){k(a.key||
a.unknown,a.value)});n.length&&k("_ko_property_writers","{"+n.join(",")+" }");return q.join(",")},Hd:function(a,b){for(var c=0;c<a.length;c++)if(a[c].key==b)return!0;return!1},$a:function(b,c,d,e,f){if(b&&a.N(b))!a.Ya(b)||f&&b.w()===e||b(e);else if((b=c.get("_ko_property_writers"))&&b[d])b[d](e)}}}();a.b("expressionRewriting",a.m);a.b("expressionRewriting.bindingRewriteValidators",a.m.Ra);a.b("expressionRewriting.parseObjectLiteral",a.m.Zb);a.b("expressionRewriting.preProcessBindings",a.m.ub);a.b("expressionRewriting._twoWayBindings",
a.m.va);a.b("jsonExpressionRewriting",a.m);a.b("jsonExpressionRewriting.insertPropertyAccessorsIntoJson",a.m.ub);(function(){function b(a){return 8==a.nodeType&&g.test(f?a.text:a.nodeValue)}function c(a){return 8==a.nodeType&&h.test(f?a.text:a.nodeValue)}function d(d,e){for(var f=d,g=1,h=[];f=f.nextSibling;){if(c(f)&&(a.a.g.set(f,l,!0),g--,0===g))return h;h.push(f);b(f)&&g++}if(!e)throw Error("Cannot find closing comment tag to match: "+d.nodeValue);return null}function e(a,b){var c=d(a,b);return c?
0<c.length?c[c.length-1].nextSibling:a.nextSibling:null}var f=w&&"\x3c!--test--\x3e"===w.createComment("test").text,g=f?/^\x3c!--\s*ko(?:\s+([\s\S]+))?\s*--\x3e$/:/^\s*ko(?:\s+([\s\S]+))?\s*$/,h=f?/^\x3c!--\s*\/ko\s*--\x3e$/:/^\s*\/ko\s*$/,m={ul:!0,ol:!0},l="__ko_matchedEndComment__";a.h={ea:{},childNodes:function(a){return b(a)?d(a):a.childNodes},Ea:function(c){if(b(c)){c=a.h.childNodes(c);for(var d=0,e=c.length;d<e;d++)a.removeNode(c[d])}else a.a.Sb(c)},ua:function(c,d){if(b(c)){a.h.Ea(c);for(var e=
c.nextSibling,f=0,l=d.length;f<l;f++)e.parentNode.insertBefore(d[f],e)}else a.a.ua(c,d)},Uc:function(a,c){b(a)?a.parentNode.insertBefore(c,a.nextSibling):a.firstChild?a.insertBefore(c,a.firstChild):a.appendChild(c)},Vb:function(c,d,e){e?b(c)?c.parentNode.insertBefore(d,e.nextSibling):e.nextSibling?c.insertBefore(d,e.nextSibling):c.appendChild(d):a.h.Uc(c,d)},firstChild:function(a){if(b(a))return!a.nextSibling||c(a.nextSibling)?null:a.nextSibling;if(a.firstChild&&c(a.firstChild))throw Error("Found invalid end comment, as the first child of "+
a);return a.firstChild},nextSibling:function(d){b(d)&&(d=e(d));if(d.nextSibling&&c(d.nextSibling)){var f=d.nextSibling;if(c(f)&&!a.a.g.get(f,l))throw Error("Found end comment without a matching opening comment, as child of "+d);return null}return d.nextSibling},Bd:b,Ud:function(a){return(a=(f?a.text:a.nodeValue).match(g))?a[1]:null},Rc:function(d){if(m[a.a.P(d)]){var f=d.firstChild;if(f){do if(1===f.nodeType){var l;l=f.firstChild;var g=null;if(l){do if(g)g.push(l);else if(b(l)){var h=e(l,!0);h?l=
h:g=[l]}else c(l)&&(g=[l]);while(l=l.nextSibling)}if(l=g)for(g=f.nextSibling,h=0;h<l.length;h++)g?d.insertBefore(l[h],g):d.appendChild(l[h])}while(f=f.nextSibling)}}}}})();a.b("virtualElements",a.h);a.b("virtualElements.allowedBindings",a.h.ea);a.b("virtualElements.emptyNode",a.h.Ea);a.b("virtualElements.insertAfter",a.h.Vb);a.b("virtualElements.prepend",a.h.Uc);a.b("virtualElements.setDomNodeChildren",a.h.ua);(function(){a.ga=function(){this.md={}};a.a.extend(a.ga.prototype,{nodeHasBindings:function(b){switch(b.nodeType){case 1:return null!=
b.getAttribute("data-bind")||a.i.getComponentNameForNode(b);case 8:return a.h.Bd(b);default:return!1}},getBindings:function(b,c){var d=this.getBindingsString(b,c),d=d?this.parseBindingsString(d,c,b):null;return a.i.sc(d,b,c,!1)},getBindingAccessors:function(b,c){var d=this.getBindingsString(b,c),d=d?this.parseBindingsString(d,c,b,{valueAccessors:!0}):null;return a.i.sc(d,b,c,!0)},getBindingsString:function(b){switch(b.nodeType){case 1:return b.getAttribute("data-bind");case 8:return a.h.Ud(b);default:return null}},
parseBindingsString:function(b,c,d,e){try{var f=this.md,g=b+(e&&e.valueAccessors||""),h;if(!(h=f[g])){var m,l="with($context){with($data||{}){return{"+a.m.ub(b,e)+"}}}";m=new Function("$context","$element",l);h=f[g]=m}return h(c,d)}catch(k){throw k.message="Unable to parse bindings.\nBindings value: "+b+"\nMessage: "+k.message,k;}}});a.ga.instance=new a.ga})();a.b("bindingProvider",a.ga);(function(){function b(b){var c=(b=a.a.g.get(b,B))&&b.M;c&&(b.M=null,c.Sc())}function c(c,d,e){this.node=c;this.xc=
d;this.ib=[];this.T=!1;d.M||a.a.I.za(c,b);e&&e.M&&(e.M.ib.push(c),this.Kb=e)}function d(a){return function(){return a}}function e(a){return a()}function f(b){return a.a.Ha(a.v.K(b),function(a,c){return function(){return b()[c]}})}function g(b,c,e){return"function"===typeof b?f(b.bind(null,c,e)):a.a.Ha(b,d)}function h(a,b){return f(this.getBindings.bind(this,a,b))}function m(b,c){var d=a.h.firstChild(c);if(d){var e,f=a.ga.instance,k=f.preprocessNode;if(k){for(;e=d;)d=a.h.nextSibling(e),k.call(f,e);
d=a.h.firstChild(c)}for(;e=d;)d=a.h.nextSibling(e),l(b,e)}a.j.Ga(c,a.j.T)}function l(b,c){var d=b,e=1===c.nodeType;e&&a.h.Rc(c);if(e||a.ga.instance.nodeHasBindings(c))d=q(c,null,b).bindingContextForDescendants;d&&!u[a.a.P(c)]&&m(d,c)}function k(b){var c=[],d={},e=[];a.a.O(b,function ca(f){if(!d[f]){var l=a.getBindingHandler(f);l&&(l.after&&(e.push(f),a.a.C(l.after,function(c){if(b[c]){if(-1!==a.a.A(e,c))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+e.join(", "));
ca(c)}}),e.length--),c.push({key:f,Lc:l}));d[f]=!0}});return c}function q(b,c,d){var f=a.a.g.Tb(b,B,{}),l=f.gd;if(!c){if(l)throw Error("You cannot apply bindings multiple times to the same element.");f.gd=!0}l||(f.context=d);var g;if(c&&"function"!==typeof c)g=c;else{var q=a.ga.instance,n=q.getBindingAccessors||h,m=a.$(function(){if(g=c?c(d,b):n.call(q,b,d)){if(d[r])d[r]();if(d[A])d[A]()}return g},null,{l:b});g&&m.ja()||(m=null)}var y=d,u;if(g){var J=function(){return a.a.Ha(m?m():g,e)},t=m?function(a){return function(){return e(m()[a])}}:
function(a){return g[a]};J.get=function(a){return g[a]&&e(t(a))};J.has=function(a){return a in g};a.j.T in g&&a.j.subscribe(b,a.j.T,function(){var c=(0,g[a.j.T])();if(c){var d=a.h.childNodes(b);d.length&&c(d,a.Dc(d[0]))}});a.j.oa in g&&(y=a.j.Bb(b,d),a.j.subscribe(b,a.j.oa,function(){var c=(0,g[a.j.oa])();c&&a.h.firstChild(b)&&c(b)}));f=k(g);a.a.C(f,function(c){var d=c.Lc.init,e=c.Lc.update,f=c.key;if(8===b.nodeType&&!a.h.ea[f])throw Error("The binding '"+f+"' cannot be used with virtual elements");
try{"function"==typeof d&&a.v.K(function(){var a=d(b,t(f),J,y.$data,y);if(a&&a.controlsDescendantBindings){if(u!==p)throw Error("Multiple bindings ("+u+" and "+f+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");u=f}}),"function"==typeof e&&a.$(function(){e(b,t(f),J,y.$data,y)},null,{l:b})}catch(l){throw l.message='Unable to process binding "'+f+": "+g[f]+'"\nMessage: '+l.message,l;}})}f=u===p;return{shouldBindDescendants:f,
bindingContextForDescendants:f&&y}}function n(b,c){return b&&b instanceof a.fa?b:new a.fa(b,p,p,c)}var r=a.a.Da("_subscribable"),y=a.a.Da("_ancestorBindingInfo"),A=a.a.Da("_dataDependency");a.f={};var u={script:!0,textarea:!0,template:!0};a.getBindingHandler=function(b){return a.f[b]};var J={};a.fa=function(b,c,d,e,f){function l(){var b=q?h():h,f=a.a.c(b);c?(a.a.extend(k,c),y in c&&(k[y]=c[y])):(k.$parents=[],k.$root=f,k.ko=a);k[r]=n;g?f=k.$data:(k.$rawData=b,k.$data=f);d&&(k[d]=f);e&&e(k,c,f);if(c&&
c[r]&&!a.U.o().Ub(c[r]))c[r]();m&&(k[A]=m);return k.$data}var k=this,g=b===J,h=g?p:b,q="function"==typeof h&&!a.N(h),n,m=f&&f.dataDependency;f&&f.exportDependencies?l():(n=a.wb(l),n.w(),n.ja()?n.equalityComparer=null:k[r]=p)};a.fa.prototype.createChildContext=function(b,c,d,e){!e&&c&&"object"==typeof c&&(e=c,c=e.as,d=e.extend);if(c&&e&&e.noChildContext){var f="function"==typeof b&&!a.N(b);return new a.fa(J,this,null,function(a){d&&d(a);a[c]=f?b():b},e)}return new a.fa(b,this,c,function(a,b){a.$parentContext=
b;a.$parent=b.$data;a.$parents=(b.$parents||[]).slice(0);a.$parents.unshift(a.$parent);d&&d(a)},e)};a.fa.prototype.extend=function(b,c){return new a.fa(J,this,null,function(c){a.a.extend(c,"function"==typeof b?b(c):b)},c)};var B=a.a.g.Z();c.prototype.Sc=function(){this.Kb&&this.Kb.M&&this.Kb.M.rd(this.node)};c.prototype.rd=function(b){a.a.hb(this.ib,b);!this.ib.length&&this.T&&this.Bc()};c.prototype.Bc=function(){this.T=!0;this.xc.M&&!this.ib.length&&(this.xc.M=null,a.a.I.xb(this.node,b),a.j.Ga(this.node,
a.j.oa),this.Sc())};a.j={T:"childrenComplete",oa:"descendantsComplete",subscribe:function(b,c,d,e){b=a.a.g.Tb(b,B,{});b.Fa||(b.Fa=new a.R);return b.Fa.subscribe(d,e,c)},Ga:function(b,c){var d=a.a.g.get(b,B);if(d&&(d.Fa&&d.Fa.notifySubscribers(b,c),c==a.j.T))if(d.M)d.M.Bc();else if(d.M===p&&d.Fa&&d.Fa.Wa(a.j.oa))throw Error("descendantsComplete event not supported for bindings on this node");},Bb:function(b,d){var e=a.a.g.Tb(b,B,{});e.M||(e.M=new c(b,e,d[y]));return d[y]==e?d:d.extend(function(a){a[y]=
e})}};a.Sd=function(b){return(b=a.a.g.get(b,B))&&b.context};a.eb=function(b,c,d){1===b.nodeType&&a.h.Rc(b);return q(b,c,n(d))};a.kd=function(b,c,d){d=n(d);return a.eb(b,g(c,d,b),d)};a.Pa=function(a,b){1!==b.nodeType&&8!==b.nodeType||m(n(a),b)};a.uc=function(a,b,c){!v&&z.jQuery&&(v=z.jQuery);if(2>arguments.length){if(b=w.body,!b)throw Error("ko.applyBindings: could not find document.body; has the document been loaded?");}else if(!b||1!==b.nodeType&&8!==b.nodeType)throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
l(n(a,c),b)};a.Cc=function(b){return!b||1!==b.nodeType&&8!==b.nodeType?p:a.Sd(b)};a.Dc=function(b){return(b=a.Cc(b))?b.$data:p};a.b("bindingHandlers",a.f);a.b("bindingEvent",a.j);a.b("bindingEvent.subscribe",a.j.subscribe);a.b("bindingEvent.startPossiblyAsyncContentBinding",a.j.Bb);a.b("applyBindings",a.uc);a.b("applyBindingsToDescendants",a.Pa);a.b("applyBindingAccessorsToNode",a.eb);a.b("applyBindingsToNode",a.kd);a.b("contextFor",a.Cc);a.b("dataFor",a.Dc)})();(function(b){function c(c,e){var l=
Object.prototype.hasOwnProperty.call(f,c)?f[c]:b,k;l?l.subscribe(e):(l=f[c]=new a.R,l.subscribe(e),d(c,function(b,d){var e=!(!d||!d.synchronous);g[c]={definition:b,Fd:e};delete f[c];k||e?l.notifySubscribers(b):a.ma.yb(function(){l.notifySubscribers(b)})}),k=!0)}function d(a,b){e("getConfig",[a],function(c){c?e("loadComponent",[a,c],function(a){b(a,c)}):b(null,null)})}function e(c,d,f,k){k||(k=a.i.loaders.slice(0));var g=k.shift();if(g){var n=g[c];if(n){var r=!1;if(n.apply(g,d.concat(function(a){r?
f(null):null!==a?f(a):e(c,d,f,k)}))!==b&&(r=!0,!g.suppressLoaderExceptions))throw Error("Component loaders must supply values by invoking the callback, not by returning values synchronously.");}else e(c,d,f,k)}else f(null)}var f={},g={};a.i={get:function(d,e){var f=Object.prototype.hasOwnProperty.call(g,d)?g[d]:b;f?f.Fd?a.v.K(function(){e(f.definition)}):a.ma.yb(function(){e(f.definition)}):c(d,e)},Ac:function(a){delete g[a]},nc:e};a.i.loaders=[];a.b("components",a.i);a.b("components.get",a.i.get);
a.b("components.clearCachedDefinition",a.i.Ac)})();(function(){function b(b,c,d,e){function g(){0===--A&&e(h)}var h={},A=2,u=d.template;d=d.viewModel;u?f(c,u,function(c){a.i.nc("loadTemplate",[b,c],function(a){h.template=a;g()})}):g();d?f(c,d,function(c){a.i.nc("loadViewModel",[b,c],function(a){h[m]=a;g()})}):g()}function c(a,b,d){if("function"===typeof b)d(function(a){return new b(a)});else if("function"===typeof b[m])d(b[m]);else if("instance"in b){var e=b.instance;d(function(){return e})}else"viewModel"in
b?c(a,b.viewModel,d):a("Unknown viewModel value: "+b)}function d(b){switch(a.a.P(b)){case "script":return a.a.ta(b.text);case "textarea":return a.a.ta(b.value);case "template":if(e(b.content))return a.a.Ca(b.content.childNodes)}return a.a.Ca(b.childNodes)}function e(a){return z.DocumentFragment?a instanceof DocumentFragment:a&&11===a.nodeType}function f(a,b,c){"string"===typeof b.require?T||z.require?(T||z.require)([b.require],c):a("Uses require, but no AMD loader is present"):c(b)}function g(a){return function(b){throw Error("Component '"+
a+"': "+b);}}var h={};a.i.register=function(b,c){if(!c)throw Error("Invalid configuration for "+b);if(a.i.sb(b))throw Error("Component "+b+" is already registered");h[b]=c};a.i.sb=function(a){return Object.prototype.hasOwnProperty.call(h,a)};a.i.unregister=function(b){delete h[b];a.i.Ac(b)};a.i.Ec={getConfig:function(b,c){c(a.i.sb(b)?h[b]:null)},loadComponent:function(a,c,d){var e=g(a);f(e,c,function(c){b(a,e,c,d)})},loadTemplate:function(b,c,f){b=g(b);if("string"===typeof c)f(a.a.ta(c));else if(c instanceof
Array)f(c);else if(e(c))f(a.a.la(c.childNodes));else if(c.element)if(c=c.element,z.HTMLElement?c instanceof HTMLElement:c&&c.tagName&&1===c.nodeType)f(d(c));else if("string"===typeof c){var h=w.getElementById(c);h?f(d(h)):b("Cannot find element with ID "+c)}else b("Unknown element type: "+c);else b("Unknown template value: "+c)},loadViewModel:function(a,b,d){c(g(a),b,d)}};var m="createViewModel";a.b("components.register",a.i.register);a.b("components.isRegistered",a.i.sb);a.b("components.unregister",
a.i.unregister);a.b("components.defaultLoader",a.i.Ec);a.i.loaders.push(a.i.Ec);a.i.cd=h})();(function(){function b(b,e){var f=b.getAttribute("params");if(f){var f=c.parseBindingsString(f,e,b,{valueAccessors:!0,bindingParams:!0}),f=a.a.Ha(f,function(c){return a.o(c,null,{l:b})}),g=a.a.Ha(f,function(c){var e=c.w();return c.ja()?a.o({read:function(){return a.a.c(c())},write:a.Ya(e)&&function(a){c()(a)},l:b}):e});Object.prototype.hasOwnProperty.call(g,"$raw")||(g.$raw=f);return g}return{$raw:{}}}a.i.getComponentNameForNode=
function(b){var c=a.a.P(b);if(a.i.sb(c)&&(-1!=c.indexOf("-")||"[object HTMLUnknownElement]"==""+b||8>=a.a.W&&b.tagName===c))return c};a.i.sc=function(c,e,f,g){if(1===e.nodeType){var h=a.i.getComponentNameForNode(e);if(h){c=c||{};if(c.component)throw Error('Cannot use the "component" binding on a custom element matching a component');var m={name:h,params:b(e,f)};c.component=g?function(){return m}:m}}return c};var c=new a.ga;9>a.a.W&&(a.i.register=function(a){return function(b){return a.apply(this,
arguments)}}(a.i.register),w.createDocumentFragment=function(b){return function(){var c=b(),f=a.i.cd,g;for(g in f);return c}}(w.createDocumentFragment))})();(function(){function b(b,c,d){c=c.template;if(!c)throw Error("Component '"+b+"' has no template");b=a.a.Ca(c);a.h.ua(d,b)}function c(a,b,c){var d=a.createViewModel;return d?d.call(a,b,c):b}var d=0;a.f.component={init:function(e,f,g,h,m){function l(){var a=k&&k.dispose;"function"===typeof a&&a.call(k);n&&n.s();q=k=n=null}var k,q,n,r=a.a.la(a.h.childNodes(e));
a.h.Ea(e);a.a.I.za(e,l);a.o(function(){var g=a.a.c(f()),h,u;"string"===typeof g?h=g:(h=a.a.c(g.name),u=a.a.c(g.params));if(!h)throw Error("No component name specified");var p=a.j.Bb(e,m),B=q=++d;a.i.get(h,function(d){if(q===B){l();if(!d)throw Error("Unknown component '"+h+"'");b(h,d,e);var f=c(d,u,{element:e,templateNodes:r});d=p.createChildContext(f,{extend:function(a){a.$component=f;a.$componentTemplateNodes=r}});f&&f.koDescendantsComplete&&(n=a.j.subscribe(e,a.j.oa,f.koDescendantsComplete,f));
k=f;a.Pa(d,e)}})},null,{l:e});return{controlsDescendantBindings:!0}}};a.h.ea.component=!0})();var V={"class":"className","for":"htmlFor"};a.f.attr={update:function(b,c){var d=a.a.c(c())||{};a.a.O(d,function(c,d){d=a.a.c(d);var g=c.indexOf(":"),g="lookupNamespaceURI"in b&&0<g&&b.lookupNamespaceURI(c.substr(0,g)),h=!1===d||null===d||d===p;h?g?b.removeAttributeNS(g,c):b.removeAttribute(c):d=d.toString();8>=a.a.W&&c in V?(c=V[c],h?b.removeAttribute(c):b[c]=d):h||(g?b.setAttributeNS(g,c,d):b.setAttribute(c,
d));"name"===c&&a.a.Xc(b,h?"":d)})}};(function(){a.f.checked={after:["value","attr"],init:function(b,c,d){function e(){var e=b.checked,f=g();if(!a.U.rb()&&(e||!m&&!a.U.pa())){var l=a.v.K(c);if(k){var n=q?l.w():l,B=r;r=f;B!==f?e&&(a.a.Oa(n,f,!0),a.a.Oa(n,B,!1)):a.a.Oa(n,f,e);q&&a.Ya(l)&&l(n)}else h&&(f===p?f=e:e||(f=p)),a.m.$a(l,d,"checked",f,!0)}}function f(){var d=a.a.c(c()),e=g();k?(b.checked=0<=a.a.A(d,e),r=e):b.checked=h&&e===p?!!d:g()===d}var g=a.wb(function(){if(d.has("checkedValue"))return a.a.c(d.get("checkedValue"));
if(n)return d.has("value")?a.a.c(d.get("value")):b.value}),h="checkbox"==b.type,m="radio"==b.type;if(h||m){var l=c(),k=h&&a.a.c(l)instanceof Array,q=!(k&&l.push&&l.splice),n=m||k,r=k?g():p;m&&!b.name&&a.f.uniqueName.init(b,function(){return!0});a.o(e,null,{l:b});a.a.H(b,"click",e);a.o(f,null,{l:b});l=p}}};a.m.va.checked=!0;a.f.checkedValue={update:function(b,c){b.value=a.a.c(c())}}})();a.f["class"]={update:function(b,c){var d=a.a.Cb(a.a.c(c()));a.a.Eb(b,b.__ko__cssValue,!1);b.__ko__cssValue=d;a.a.Eb(b,
d,!0)}};a.f.css={update:function(b,c){var d=a.a.c(c());null!==d&&"object"==typeof d?a.a.O(d,function(c,d){d=a.a.c(d);a.a.Eb(b,c,d)}):a.f["class"].update(b,c)}};a.f.enable={update:function(b,c){var d=a.a.c(c());d&&b.disabled?b.removeAttribute("disabled"):d||b.disabled||(b.disabled=!0)}};a.f.disable={update:function(b,c){a.f.enable.update(b,function(){return!a.a.c(c())})}};a.f.event={init:function(b,c,d,e,f){var g=c()||{};a.a.O(g,function(g){"string"==typeof g&&a.a.H(b,g,function(b){var l,k=c()[g];
if(k){try{var q=a.a.la(arguments);e=f.$data;q.unshift(e);l=k.apply(e,q)}finally{!0!==l&&(b.preventDefault?b.preventDefault():b.returnValue=!1)}!1===d.get(g+"Bubble")&&(b.cancelBubble=!0,b.stopPropagation&&b.stopPropagation())}})})}};a.f.foreach={Qc:function(b){return function(){var c=b(),d=a.a.$b(c);if(!d||"number"==typeof d.length)return{foreach:c,templateEngine:a.ba.Na};a.a.c(c);return{foreach:d.data,as:d.as,noChildContext:d.noChildContext,includeDestroyed:d.includeDestroyed,afterAdd:d.afterAdd,
beforeRemove:d.beforeRemove,afterRender:d.afterRender,beforeMove:d.beforeMove,afterMove:d.afterMove,templateEngine:a.ba.Na}}},init:function(b,c){return a.f.template.init(b,a.f.foreach.Qc(c))},update:function(b,c,d,e,f){return a.f.template.update(b,a.f.foreach.Qc(c),d,e,f)}};a.m.Ra.foreach=!1;a.h.ea.foreach=!0;a.f.hasfocus={init:function(b,c,d){function e(e){b.__ko_hasfocusUpdating=!0;var f=b.ownerDocument;if("activeElement"in f){var g;try{g=f.activeElement}catch(k){g=f.body}e=g===b}f=c();a.m.$a(f,
d,"hasfocus",e,!0);b.__ko_hasfocusLastValue=e;b.__ko_hasfocusUpdating=!1}var f=e.bind(null,!0),g=e.bind(null,!1);a.a.H(b,"focus",f);a.a.H(b,"focusin",f);a.a.H(b,"blur",g);a.a.H(b,"focusout",g);b.__ko_hasfocusLastValue=!1},update:function(b,c){var d=!!a.a.c(c());b.__ko_hasfocusUpdating||b.__ko_hasfocusLastValue===d||(d?b.focus():b.blur(),!d&&b.__ko_hasfocusLastValue&&b.ownerDocument.body.focus(),a.v.K(a.a.Fb,null,[b,d?"focusin":"focusout"]))}};a.m.va.hasfocus=!0;a.f.hasFocus=a.f.hasfocus;a.m.va.hasFocus=
"hasfocus";a.f.html={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.dc(b,c())}};(function(){function b(b,d,e){a.f[b]={init:function(b,c,h,m,l){var k,q,n={},r,p,A;if(d){m=h.get("as");var u=h.get("noChildContext");A=!(m&&u);n={as:m,noChildContext:u,exportDependencies:A}}p=(r="render"==h.get("completeOn"))||h.has(a.j.oa);a.o(function(){var h=a.a.c(c()),m=!e!==!h,u=!q,t;if(A||m!==k){p&&(l=a.j.Bb(b,l));if(m){if(!d||A)n.dataDependency=a.U.o();t=d?l.createChildContext("function"==
typeof h?h:c,n):a.U.pa()?l.extend(null,n):l}u&&a.U.pa()&&(q=a.a.Ca(a.h.childNodes(b),!0));m?(u||a.h.ua(b,a.a.Ca(q)),a.Pa(t,b)):(a.h.Ea(b),r||a.j.Ga(b,a.j.T));k=m}},null,{l:b});return{controlsDescendantBindings:!0}}};a.m.Ra[b]=!1;a.h.ea[b]=!0}b("if");b("ifnot",!1,!0);b("with",!0)})();a.f.let={init:function(b,c,d,e,f){c=f.extend(c);a.Pa(c,b);return{controlsDescendantBindings:!0}}};a.h.ea.let=!0;var Q={};a.f.options={init:function(b){if("select"!==a.a.P(b))throw Error("options binding applies only to SELECT elements");
for(;0<b.length;)b.remove(0);return{controlsDescendantBindings:!0}},update:function(b,c,d){function e(){return a.a.fb(b.options,function(a){return a.selected})}function f(a,b,c){var d=typeof b;return"function"==d?b(a):"string"==d?a[b]:c}function g(c,e){if(y&&k)a.u.ya(b,a.a.c(d.get("value")),!0);else if(r.length){var f=0<=a.a.A(r,a.u.L(e[0]));a.a.Yc(e[0],f);y&&!f&&a.v.K(a.a.Fb,null,[b,"change"])}}var h=b.multiple,m=0!=b.length&&h?b.scrollTop:null,l=a.a.c(c()),k=d.get("valueAllowUnset")&&d.has("value"),
q=d.get("optionsIncludeDestroyed");c={};var n,r=[];k||(h?r=a.a.Mb(e(),a.u.L):0<=b.selectedIndex&&r.push(a.u.L(b.options[b.selectedIndex])));l&&("undefined"==typeof l.length&&(l=[l]),n=a.a.fb(l,function(b){return q||b===p||null===b||!a.a.c(b._destroy)}),d.has("optionsCaption")&&(l=a.a.c(d.get("optionsCaption")),null!==l&&l!==p&&n.unshift(Q)));var y=!1;c.beforeRemove=function(a){b.removeChild(a)};l=g;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(l=function(b,c){g(0,c);
a.v.K(d.get("optionsAfterRender"),null,[c[0],b!==Q?b:p])});a.a.cc(b,n,function(c,e,g){g.length&&(r=!k&&g[0].selected?[a.u.L(g[0])]:[],y=!0);e=b.ownerDocument.createElement("option");c===Q?(a.a.Ab(e,d.get("optionsCaption")),a.u.ya(e,p)):(g=f(c,d.get("optionsValue"),c),a.u.ya(e,a.a.c(g)),c=f(c,d.get("optionsText"),g),a.a.Ab(e,c));return[e]},c,l);a.v.K(function(){if(k)a.u.ya(b,a.a.c(d.get("value")),!0);else{var c;h?c=r.length&&e().length<r.length:c=r.length&&0<=b.selectedIndex?a.u.L(b.options[b.selectedIndex])!==
r[0]:r.length||0<=b.selectedIndex;c&&a.a.Fb(b,"change")}});a.a.vd(b);m&&20<Math.abs(m-b.scrollTop)&&(b.scrollTop=m)}};a.f.options.Yb=a.a.g.Z();a.f.selectedOptions={after:["options","foreach"],init:function(b,c,d){a.a.H(b,"change",function(){var e=c(),f=[];a.a.C(b.getElementsByTagName("option"),function(b){b.selected&&f.push(a.u.L(b))});a.m.$a(e,d,"selectedOptions",f)})},update:function(b,c){if("select"!=a.a.P(b))throw Error("values binding applies only to SELECT elements");var d=a.a.c(c()),e=b.scrollTop;
d&&"number"==typeof d.length&&a.a.C(b.getElementsByTagName("option"),function(b){var c=0<=a.a.A(d,a.u.L(b));b.selected!=c&&a.a.Yc(b,c)});b.scrollTop=e}};a.m.va.selectedOptions=!0;a.f.style={update:function(b,c){var d=a.a.c(c()||{});a.a.O(d,function(c,d){d=a.a.c(d);if(null===d||d===p||!1===d)d="";if(v)v(b).css(c,d);else if(/^--/.test(c))b.style.setProperty(c,d);else{c=c.replace(/-(\w)/g,function(a,b){return b.toUpperCase()});var g=b.style[c];b.style[c]=d;d===g||b.style[c]!=g||isNaN(d)||(b.style[c]=
d+"px")}})}};a.f.submit={init:function(b,c,d,e,f){if("function"!=typeof c())throw Error("The value for a submit binding must be a function");a.a.H(b,"submit",function(a){var d,e=c();try{d=e.call(f.$data,b)}finally{!0!==d&&(a.preventDefault?a.preventDefault():a.returnValue=!1)}})}};a.f.text={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.Ab(b,c())}};a.h.ea.text=!0;(function(){if(z&&z.navigator){var b=function(a){if(a)return parseFloat(a[1])},c=z.navigator.userAgent,
d,e,f,g,h;(d=z.opera&&z.opera.version&&parseInt(z.opera.version()))||(h=b(c.match(/Edge\/([^ ]+)$/)))||b(c.match(/Chrome\/([^ ]+)/))||(e=b(c.match(/Version\/([^ ]+) Safari/)))||(f=b(c.match(/Firefox\/([^ ]+)/)))||(g=a.a.W||b(c.match(/MSIE ([^ ]+)/)))||(g=b(c.match(/rv:([^ )]+)/)))}if(8<=g&&10>g)var m=a.a.g.Z(),l=a.a.g.Z(),k=function(b){var c=this.activeElement;(c=c&&a.a.g.get(c,l))&&c(b)},q=function(b,c){var d=b.ownerDocument;a.a.g.get(d,m)||(a.a.g.set(d,m,!0),a.a.H(d,"selectionchange",k));a.a.g.set(b,
l,c)};a.f.textInput={init:function(b,c,l){function k(c,d){a.a.H(b,c,d)}function m(){var d=a.a.c(c());if(null===d||d===p)d="";L!==p&&d===L?a.a.setTimeout(m,4):b.value!==d&&(x=!0,b.value=d,x=!1,v=b.value)}function t(){w||(L=b.value,w=a.a.setTimeout(B,4))}function B(){clearTimeout(w);L=w=p;var d=b.value;v!==d&&(v=d,a.m.$a(c(),l,"textInput",d))}var v=b.value,w,L,z=9==a.a.W?t:B,x=!1;g&&k("keypress",B);11>g&&k("propertychange",function(a){x||"value"!==a.propertyName||z(a)});8==g&&(k("keyup",B),k("keydown",
B));q&&(q(b,z),k("dragend",t));(!g||9<=g)&&k("input",z);5>e&&"textarea"===a.a.P(b)?(k("keydown",t),k("paste",t),k("cut",t)):11>d?k("keydown",t):4>f?(k("DOMAutoComplete",B),k("dragdrop",B),k("drop",B)):h&&"number"===b.type&&k("keydown",t);k("change",B);k("blur",B);a.o(m,null,{l:b})}};a.m.va.textInput=!0;a.f.textinput={preprocess:function(a,b,c){c("textInput",a)}}})();a.f.uniqueName={init:function(b,c){if(c()){var d="ko_unique_"+ ++a.f.uniqueName.qd;a.a.Xc(b,d)}}};a.f.uniqueName.qd=0;a.f.using={init:function(b,
c,d,e,f){var g;d.has("as")&&(g={as:d.get("as"),noChildContext:d.get("noChildContext")});c=f.createChildContext(c,g);a.Pa(c,b);return{controlsDescendantBindings:!0}}};a.h.ea.using=!0;a.f.value={after:["options","foreach"],init:function(b,c,d){var e=a.a.P(b),f="input"==e;if(!f||"checkbox"!=b.type&&"radio"!=b.type){var g=["change"],h=d.get("valueUpdate"),m=!1,l=null;h&&("string"==typeof h&&(h=[h]),a.a.gb(g,h),g=a.a.vc(g));var k=function(){l=null;m=!1;var e=c(),f=a.u.L(b);a.m.$a(e,d,"value",f)};!a.a.W||
!f||"text"!=b.type||"off"==b.autocomplete||b.form&&"off"==b.form.autocomplete||-1!=a.a.A(g,"propertychange")||(a.a.H(b,"propertychange",function(){m=!0}),a.a.H(b,"focus",function(){m=!1}),a.a.H(b,"blur",function(){m&&k()}));a.a.C(g,function(c){var d=k;a.a.Td(c,"after")&&(d=function(){l=a.u.L(b);a.a.setTimeout(k,0)},c=c.substring(5));a.a.H(b,c,d)});var q;q=f&&"file"==b.type?function(){var d=a.a.c(c());null===d||d===p||""===d?b.value="":a.v.K(k)}:function(){var f=a.a.c(c()),g=a.u.L(b);if(null!==l&&
f===l)a.a.setTimeout(q,0);else if(f!==g||g===p)"select"===e?(g=d.get("valueAllowUnset"),a.u.ya(b,f,g),g||f===a.u.L(b)||a.v.K(k)):a.u.ya(b,f)};a.o(q,null,{l:b})}else a.eb(b,{checkedValue:c})},update:function(){}};a.m.va.value=!0;a.f.visible={update:function(b,c){var d=a.a.c(c()),e="none"!=b.style.display;d&&!e?b.style.display="":!d&&e&&(b.style.display="none")}};a.f.hidden={update:function(b,c){a.f.visible.update(b,function(){return!a.a.c(c())})}};(function(b){a.f[b]={init:function(c,d,e,f,g){return a.f.event.init.call(this,
c,function(){var a={};a[b]=d();return a},e,f,g)}}})("click");a.ca=function(){};a.ca.prototype.renderTemplateSource=function(){throw Error("Override renderTemplateSource");};a.ca.prototype.createJavaScriptEvaluatorBlock=function(){throw Error("Override createJavaScriptEvaluatorBlock");};a.ca.prototype.makeTemplateSource=function(b,c){if("string"==typeof b){c=c||w;var d=c.getElementById(b);if(!d)throw Error("Cannot find template with ID "+b);return new a.B.D(d)}if(1==b.nodeType||8==b.nodeType)return new a.B.ia(b);
throw Error("Unknown template type: "+b);};a.ca.prototype.renderTemplate=function(a,c,d,e){a=this.makeTemplateSource(a,e);return this.renderTemplateSource(a,c,d,e)};a.ca.prototype.isTemplateRewritten=function(a,c){return!1===this.allowTemplateRewriting?!0:this.makeTemplateSource(a,c).data("isRewritten")};a.ca.prototype.rewriteTemplate=function(a,c,d){a=this.makeTemplateSource(a,d);c=c(a.text());a.text(c);a.data("isRewritten",!0)};a.b("templateEngine",a.ca);a.ic=function(){function b(b,c,d,h){b=a.m.Zb(b);
for(var m=a.m.Ra,l=0;l<b.length;l++){var k=b[l].key;if(Object.prototype.hasOwnProperty.call(m,k)){var q=m[k];if("function"===typeof q){if(k=q(b[l].value))throw Error(k);}else if(!q)throw Error("This template engine does not support the '"+k+"' binding within its templates");}}d="ko.__tr_ambtns(function($context,$element){return(function(){return{ "+a.m.ub(b,{valueAccessors:!0})+" } })()},'"+d.toLowerCase()+"')";return h.createJavaScriptEvaluatorBlock(d)+c}var c=/(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'|[^>]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi,
d=/\x3c!--\s*ko\b\s*([\s\S]*?)\s*--\x3e/g;return{wd:function(b,c,d){c.isTemplateRewritten(b,d)||c.rewriteTemplate(b,function(b){return a.ic.Kd(b,c)},d)},Kd:function(a,f){return a.replace(c,function(a,c,d,e,k){return b(k,c,d,f)}).replace(d,function(a,c){return b(c,"\x3c!-- ko --\x3e","#comment",f)})},ld:function(b,c){return a.aa.Wb(function(d,h){var m=d.nextSibling;m&&m.nodeName.toLowerCase()===c&&a.eb(m,b,h)})}}}();a.b("__tr_ambtns",a.ic.ld);(function(){a.B={};a.B.D=function(b){if(this.D=b){var c=
a.a.P(b);this.Db="script"===c?1:"textarea"===c?2:"template"==c&&b.content&&11===b.content.nodeType?3:4}};a.B.D.prototype.text=function(){var b=1===this.Db?"text":2===this.Db?"value":"innerHTML";if(0==arguments.length)return this.D[b];var c=arguments[0];"innerHTML"===b?a.a.dc(this.D,c):this.D[b]=c};var b=a.a.g.Z()+"_";a.B.D.prototype.data=function(c){if(1===arguments.length)return a.a.g.get(this.D,b+c);a.a.g.set(this.D,b+c,arguments[1])};var c=a.a.g.Z();a.B.D.prototype.nodes=function(){var b=this.D;
if(0==arguments.length){var e=a.a.g.get(b,c)||{},f=e.jb||(3===this.Db?b.content:4===this.Db?b:p);if(!f||e.hd)if(e=this.text())f=a.a.Ld(e,b.ownerDocument),this.text(""),a.a.g.set(b,c,{jb:f,hd:!0});return f}a.a.g.set(b,c,{jb:arguments[0]})};a.B.ia=function(a){this.D=a};a.B.ia.prototype=new a.B.D;a.B.ia.prototype.constructor=a.B.ia;a.B.ia.prototype.text=function(){if(0==arguments.length){var b=a.a.g.get(this.D,c)||{};b.jc===p&&b.jb&&(b.jc=b.jb.innerHTML);return b.jc}a.a.g.set(this.D,c,{jc:arguments[0]})};
a.b("templateSources",a.B);a.b("templateSources.domElement",a.B.D);a.b("templateSources.anonymousTemplate",a.B.ia)})();(function(){function b(b,c,d){var e;for(c=a.h.nextSibling(c);b&&(e=b)!==c;)b=a.h.nextSibling(e),d(e,b)}function c(c,d){if(c.length){var e=c[0],f=c[c.length-1],g=e.parentNode,h=a.ga.instance,m=h.preprocessNode;if(m){b(e,f,function(a,b){var c=a.previousSibling,d=m.call(h,a);d&&(a===e&&(e=d[0]||b),a===f&&(f=d[d.length-1]||c))});c.length=0;if(!e)return;e===f?c.push(e):(c.push(e,f),a.a.Ua(c,
g))}b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.uc(d,b)});b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.aa.bd(b,[d])});a.a.Ua(c,g)}}function d(a){return a.nodeType?a:0<a.length?a[0]:null}function e(b,e,f,h,m){m=m||{};var p=(b&&d(b)||f||{}).ownerDocument,A=m.templateEngine||g;a.ic.wd(f,A,p);f=A.renderTemplate(f,h,m,p);if("number"!=typeof f.length||0<f.length&&"number"!=typeof f[0].nodeType)throw Error("Template engine must return an array of DOM nodes");p=!1;switch(e){case "replaceChildren":a.h.ua(b,
f);p=!0;break;case "replaceNode":a.a.Wc(b,f);p=!0;break;case "ignoreTargetNode":break;default:throw Error("Unknown renderMode: "+e);}p&&(c(f,h),m.afterRender&&a.v.K(m.afterRender,null,[f,h[m.as||"$data"]]),"replaceChildren"==e&&a.j.Ga(b,a.j.T));return f}function f(b,c,d){return a.N(b)?b():"function"===typeof b?b(c,d):b}var g;a.ec=function(b){if(b!=p&&!(b instanceof a.ca))throw Error("templateEngine must inherit from ko.templateEngine");g=b};a.bc=function(b,c,h,m,r){h=h||{};if((h.templateEngine||g)==
p)throw Error("Set a template engine before calling renderTemplate");r=r||"replaceChildren";if(m){var y=d(m);return a.$(function(){var g=c&&c instanceof a.fa?c:new a.fa(c,null,null,null,{exportDependencies:!0}),p=f(b,g.$data,g),g=e(m,r,p,g,h);"replaceNode"==r&&(m=g,y=d(m))},null,{Sa:function(){return!y||!a.a.Rb(y)},l:y&&"replaceNode"==r?y.parentNode:y})}return a.aa.Wb(function(d){a.bc(b,c,h,d,"replaceNode")})};a.Pd=function(b,d,g,h,m){function y(b,c){a.v.K(a.a.cc,null,[h,b,u,g,t,c]);a.j.Ga(h,a.j.T)}
function t(a,b){c(b,v);g.afterRender&&g.afterRender(b,a);v=null}function u(a,c){v=m.createChildContext(a,{as:B,noChildContext:g.noChildContext,extend:function(a){a.$index=c;B&&(a[B+"Index"]=c)}});var d=f(b,a,v);return e(h,"ignoreTargetNode",d,v,g)}var v,B=g.as,w=!1===g.includeDestroyed||a.options.foreachHidesDestroyed&&!g.includeDestroyed;if(w||g.beforeRemove||!a.Oc(d))return a.$(function(){var b=a.a.c(d)||[];"undefined"==typeof b.length&&(b=[b]);w&&(b=a.a.fb(b,function(b){return b===p||null===b||
!a.a.c(b._destroy)}));y(b)},null,{l:h});y(d.w());var z=d.subscribe(function(a){y(d(),a)},null,"arrayChange");z.l(h);return z};var h=a.a.g.Z(),m=a.a.g.Z();a.f.template={init:function(b,c){var d=a.a.c(c());if("string"==typeof d||d.name)a.h.Ea(b);else if("nodes"in d){d=d.nodes||[];if(a.N(d))throw Error('The "nodes" option must be a plain, non-observable array.');var e=d[0]&&d[0].parentNode;e&&a.a.g.get(e,m)||(e=a.a.Xb(d),a.a.g.set(e,m,!0));(new a.B.ia(b)).nodes(e)}else if(d=a.h.childNodes(b),0<d.length)e=
a.a.Xb(d),(new a.B.ia(b)).nodes(e);else throw Error("Anonymous template defined, but no template content was provided");return{controlsDescendantBindings:!0}},update:function(b,c,d,e,f){var g=c();c=a.a.c(g);d=!0;e=null;"string"==typeof c?c={}:(g=c.name,"if"in c&&(d=a.a.c(c["if"])),d&&"ifnot"in c&&(d=!a.a.c(c.ifnot)));"foreach"in c?e=a.Pd(g||b,d&&c.foreach||[],c,b,f):d?(d=f,"data"in c&&(d=f.createChildContext(c.data,{as:c.as,noChildContext:c.noChildContext,exportDependencies:!0})),e=a.bc(g||b,d,c,
b)):a.h.Ea(b);f=e;(c=a.a.g.get(b,h))&&"function"==typeof c.s&&c.s();a.a.g.set(b,h,!f||f.ja&&!f.ja()?p:f)}};a.m.Ra.template=function(b){b=a.m.Zb(b);return 1==b.length&&b[0].unknown||a.m.Hd(b,"name")?null:"This template engine does not support anonymous templates nested within its templates"};a.h.ea.template=!0})();a.b("setTemplateEngine",a.ec);a.b("renderTemplate",a.bc);a.a.Jc=function(a,c,d){if(a.length&&c.length){var e,f,g,h,m;for(e=f=0;(!d||e<d)&&(h=a[f]);++f){for(g=0;m=c[g];++g)if(h.value===m.value){h.moved=
m.index;m.moved=h.index;c.splice(g,1);e=g=0;break}e+=g}}};a.a.Ob=function(){function b(b,d,e,f,g){var h=Math.min,m=Math.max,l=[],k,p=b.length,n,r=d.length,t=r-p||1,A=p+r+1,u,v,w;for(k=0;k<=p;k++)for(v=u,l.push(u=[]),w=h(r,k+t),n=m(0,k-1);n<=w;n++)u[n]=n?k?b[k-1]===d[n-1]?v[n-1]:h(v[n]||A,u[n-1]||A)+1:n+1:k+1;h=[];m=[];t=[];k=p;for(n=r;k||n;)r=l[k][n]-1,n&&r===l[k][n-1]?m.push(h[h.length]={status:e,value:d[--n],index:n}):k&&r===l[k-1][n]?t.push(h[h.length]={status:f,value:b[--k],index:k}):(--n,--k,
g.sparse||h.push({status:"retained",value:d[n]}));a.a.Jc(t,m,!g.dontLimitMoves&&10*p);return h.reverse()}return function(a,d,e){e="boolean"===typeof e?{dontLimitMoves:e}:e||{};a=a||[];d=d||[];return a.length<d.length?b(a,d,"added","deleted",e):b(d,a,"deleted","added",e)}}();a.b("utils.compareArrays",a.a.Ob);(function(){function b(b,c,d,h,m){var l=[],k=a.$(function(){var k=c(d,m,a.a.Ua(l,b))||[];0<l.length&&(a.a.Wc(l,k),h&&a.v.K(h,null,[d,k,m]));l.length=0;a.a.gb(l,k)},null,{l:b,Sa:function(){return!a.a.jd(l)}});
return{Y:l,$:k.ja()?k:p}}var c=a.a.g.Z(),d=a.a.g.Z();a.a.cc=function(e,f,g,h,m,l){function k(b){x={Aa:b,nb:a.sa(w++)};v.push(x);t||F.push(x)}function q(b){x=r[b];w!==x.nb.w()&&D.push(x);x.nb(w++);a.a.Ua(x.Y,e);v.push(x)}function n(b,c){if(b)for(var d=0,e=c.length;d<e;d++)a.a.C(c[d].Y,function(a){b(a,d,c[d].Aa)})}f=f||[];"undefined"==typeof f.length&&(f=[f]);h=h||{};var r=a.a.g.get(e,c),t=!r,v=[],u=0,w=0,B=[],z=[],C=[],D=[],F=[],x,I=0;if(t)a.a.C(f,k);else{if(!l||r&&r._countWaitingForRemove){var E=
a.a.Mb(r,function(a){return a.Aa});l=a.a.Ob(E,f,{dontLimitMoves:h.dontLimitMoves,sparse:!0})}for(var E=0,G,H,K;G=l[E];E++)switch(H=G.moved,K=G.index,G.status){case "deleted":for(;u<K;)q(u++);H===p&&(x=r[u],x.$&&(x.$.s(),x.$=p),a.a.Ua(x.Y,e).length&&(h.beforeRemove&&(v.push(x),I++,x.Aa===d?x=null:C.push(x)),x&&B.push.apply(B,x.Y)));u++;break;case "added":for(;w<K;)q(u++);H!==p?(z.push(v.length),q(H)):k(G.value)}for(;w<f.length;)q(u++);v._countWaitingForRemove=I}a.a.g.set(e,c,v);n(h.beforeMove,D);a.a.C(B,
h.beforeRemove?a.na:a.removeNode);var M,O,P;try{P=e.ownerDocument.activeElement}catch(N){}if(z.length)for(;(E=z.shift())!=p;){x=v[E];for(M=p;E;)if((O=v[--E].Y)&&O.length){M=O[O.length-1];break}for(f=0;u=x.Y[f];M=u,f++)a.h.Vb(e,u,M)}E=0;for(z=a.h.firstChild(e);x=v[E];E++){x.Y||a.a.extend(x,b(e,g,x.Aa,m,x.nb));for(f=0;u=x.Y[f];z=u.nextSibling,M=u,f++)u!==z&&a.h.Vb(e,u,M);!x.Dd&&m&&(m(x.Aa,x.Y,x.nb),x.Dd=!0,M=x.Y[x.Y.length-1])}P&&e.ownerDocument.activeElement!=P&&P.focus();n(h.beforeRemove,C);for(E=
0;E<C.length;++E)C[E].Aa=d;n(h.afterMove,D);n(h.afterAdd,F)}})();a.b("utils.setDomNodeChildrenFromArrayMapping",a.a.cc);a.ba=function(){this.allowTemplateRewriting=!1};a.ba.prototype=new a.ca;a.ba.prototype.constructor=a.ba;a.ba.prototype.renderTemplateSource=function(b,c,d,e){if(c=(9>a.a.W?0:b.nodes)?b.nodes():null)return a.a.la(c.cloneNode(!0).childNodes);b=b.text();return a.a.ta(b,e)};a.ba.Na=new a.ba;a.ec(a.ba.Na);a.b("nativeTemplateEngine",a.ba);(function(){a.Za=function(){var a=this.Gd=function(){if(!v||
!v.tmpl)return 0;try{if(0<=v.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(a){}return 1}();this.renderTemplateSource=function(b,e,f,g){g=g||w;f=f||{};if(2>a)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var h=b.data("precompiled");h||(h=b.text()||"",h=v.template(null,"{{ko_with $item.koBindingContext}}"+h+"{{/ko_with}}"),b.data("precompiled",h));b=[e.$data];e=v.extend({koBindingContext:e},f.templateOptions);e=v.tmpl(h,b,e);e.appendTo(g.createElement("div"));
v.fragments={};return e};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+a+" })()) }}"};this.addTemplate=function(a,b){w.write("<script type='text/html' id='"+a+"'>"+b+"\x3c/script>")};0<a&&(v.tmpl.tag.ko_code={open:"__.push($1 || '');"},v.tmpl.tag.ko_with={open:"with($1) {",close:"} "})};a.Za.prototype=new a.ca;a.Za.prototype.constructor=a.Za;var b=new a.Za;0<b.Gd&&a.ec(b);a.b("jqueryTmplTemplateEngine",a.Za)})()})})();})();

var ko = window.ko || module.exports;
module.exports = {ko: ko, Knockout: ko};
});
;
Numbas.queueScript('knockout-handlers', ['display-util', 'display-base', 'answer-widgets'], function() {
    Knockout.onError = function(err) {
        Numbas.display.die(err);
    };

    Knockout.bindingHandlers.niceNumber = {
        update: function(element, valueAccessor) {
            var n = Knockout.utils.unwrapObservable(valueAccessor());
            element.textContent = Numbas.math.niceNumber(n);
        }
    }
    Knockout.bindingHandlers.percentage = {
        update: function(element, valueAccessor) {
            var n = Knockout.utils.unwrapObservable(valueAccessor());
            element.textContent = Numbas.math.niceNumber(n * 100, {precisionType: 'dp', precision: 1}) + '%';
        }
    }

    /** Set the text content and `datetime` attribute of a `<time>` tag based on the given Date object.
     *
     * @see Numbas.display_util.duration_observable
     */
    Knockout.bindingHandlers.datetime = {
        update: function(element, valueAccessor) {
            var time = Knockout.unwrap(valueAccessor());

            if(!time) {
                element.textContent = '';
                element.removeAttribute('datetime');
                return;
            }

            element.textContent = Numbas.util.formatTime(time);
            element.setAttribute('datetime', time.toISOString());
        }
    }

    /** Set the text content and `datetime` attribute of a `<time>` tag based on the given duration, a `Numbas.display_util.duration_observable` object.
     *
     * @see Numbas.display_util.duration_observable
     */
    Knockout.bindingHandlers.duration = {
        update: function(element, valueAccessor) {
            var obs = valueAccessor();
            element.textContent = obs.display();
            element.setAttribute('datetime', obs.machine());
        }
    }

    /** Resize an input element to fit its value.
     *
     * @param {Element} element
     */
    function resize_input_to_value(element) {
        var w = Numbas.display_util.measureText(element).width;
        element.style['width'] = Math.max(w + 30, 60) + 'px';
    };

    Knockout.bindingHandlers.autosize = {
        init: function(element) {
            //resize text inputs to just fit their contents
            element.addEventListener('keyup', () => resize_input_to_value(element));
            element.addEventListener('keydown', () => resize_input_to_value(element));
            element.addEventListener('change', () => resize_input_to_value(element));
            element.addEventListener('input', () => resize_input_to_value(element));
            resize_input_to_value(element);
        },
        update: function(element, valueAccessor, allBindings) {
            var textInput = allBindings.get('textInput');
            if(textInput) {
                textInput();
            }
            var value = allBindings.get('value');
            if(value) {
                value();
            }
            resize_input_to_value(element);
        }
    }
    Knockout.bindingHandlers.test = {
        update: function(element, valueAccessor) {
            console.log(Knockout.utils.unwrapObservable(valueAccessor()));
        }
    }
    Knockout.bindingHandlers.dom = {
        update: function(element, valueAccessor) {
            var nodes = Knockout.utils.unwrapObservable(valueAccessor());
            element.innerHTML = '';
            if(!Array.isArray(nodes)) {
                nodes = [nodes];
            }
            for(let node of nodes) {
                if(typeof node == 'string') {
                    const span = document.createElement('span');
                    span.innerHTML = node;
                    for(let subnode of [...span.childNodes]) {
                        element.appendChild(subnode);
                    }
                } else {
                    element.append(node);
                }
            }
        }
    }
    Knockout.bindingHandlers.latex = {
        update: function(element, valueAccessor) {
            var value = Knockout.unwrap(valueAccessor());
            Knockout.bindingHandlers.html.update.apply(this, arguments);

            if(value && value.toString().trim()) {
                Numbas.display.typeset(element);
            }
        }
    }

    /**
     * Render a TeX expression inside this element.
     * The element is hidden while MathJax typesets the TeX.
     */
    Knockout.bindingHandlers.maths = {
        update: function(element, valueAccessor) {
            var val = Knockout.utils.unwrapObservable(valueAccessor());
            element.innerHTML = '';
            const span = document.createElement('span');
            element.append(span);
            span.hidden = true;
            span.innerHTML = '\\(' + val + '\\)';
            Numbas.display.typeset(element, () => {
                span.hidden = false;
            });
        }
    }
    Knockout.bindingHandlers.jmescope = {
        update: function(element, valueAccessor) {
            Numbas.display_util.set_jme_scope(element, Knockout.unwrap(valueAccessor()));
        }
    };
    Knockout.bindingHandlers.typeset = {
        update: function(element, valueAccessor) {
            Knockout.utils.unwrapObservable(valueAccessor());
            Numbas.display.typeset(element);
        }
    }

    Knockout.bindingHandlers.realVisible = Knockout.bindingHandlers.visible;

    Knockout.bindingHandlers.visible = {
        init: function(element, valueAccessor) {
            element.style.display = '';
            Knockout.utils.domData.set(element, 'tabindex', element.getAttribute('tabindex'));
        },
        update: function(element, valueAccessor) {
            var val = Knockout.unwrap(valueAccessor());
            element.classList.toggle('invisible', !val);
            !val ? element.setAttribute('disabled', true) : element.removeAttribute('disabled');
            if(val) {
                const tabindex = Knockout.utils.domData.get(element, 'tabindex');
                if(tabindex !== null) {
                    element.setAttribute('tabindex', tabindex);
                }
            } else {
                element.removeAttribute('tabindex');
            }
        }
    }
    Knockout.bindingHandlers.visibleIf = Knockout.bindingHandlers.visible; // removed because it didn't work, but aliased to `visible` for backwards compatibility

    Knockout.bindingHandlers.promise = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var promise = Knockout.unwrap(valueAccessor());
            promise.then(function(html) {
                element.appendChild(html);
                Knockout.applyBindingsToDescendants(bindingContext, element);
            });
            return {controlsDescendantBindings: true};
        }
    }
    Knockout.bindingHandlers.reorder_table = {
        // reorder the rows and columns of a table, including the header
        // value is an object {rows, columns, leaders}
        // rows and columns are permutations
        // leaders is the number of columns at the start of each row to ignore (so column headers aren't moved)
        init: function(element, valueAccessor) {
            var value = Knockout.unwrap(valueAccessor());
            var row_order = value.rows;
            var column_order = value.columns;
            var leaders = value.leaders || 0;
            Array.prototype.forEach.call(element.querySelectorAll('tr:not([data-shuffle="no"])'), function(r) {
                var columns = Array.prototype.slice.call(r.querySelectorAll(':is(td,th):not([data-shuffle="no"])'), leaders);
                for(var i = 0;i < column_order.length;i++) {
                    r.appendChild(columns[column_order[i]]);
                }
            });
            Array.prototype.forEach.call(element.querySelectorAll('tbody'), function(body) {
                var rows = Array.prototype.slice.call(body.querySelectorAll('tr'));
                for(var i = 0;i < row_order.length;i++) {
                    body.appendChild(rows[row_order[i]]);
                }
            });
            const choice_header = element.querySelector('.choice-heading');
            if(choice_header) {
                const first_row = element.querySelector('tbody tr:first-child');
                first_row.insertBefore(choice_header, first_row.firstChild);
            }
        }
    }
    Knockout.bindingHandlers.reorder_list = {
        init: function(element, valueAccessor) {
            var value = Knockout.unwrap(valueAccessor());
            var order = value.order;
            var leaders = element.children.length - order.length;
            var items = Array.prototype.slice.call(element.children, leaders);
            for(var i = 0;i < order.length;i++) {
                element.appendChild(items[order[i]]);
            }
        }
    }


    Knockout.bindingHandlers.treeView = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var trees = (bindingContext.$trees || []).slice();
            trees.push({
                element: element,
                context: bindingContext
            })
            var innerBindingContext = bindingContext.extend(valueAccessor).extend({
                '$trees': trees
            });

            var options = {
              templateEngine: Knockout.nativeTemplateEngine.instance
            };

            element.addEventListener('keydown', (e) => {
                if(e.target == element) {
                    return;
                }

                const focused = document.activeElement;

                let el = focused;
                while(el.parentElement && el.parentElement.role != 'group' && el.parentElement.role != 'tree') {
                    el = el.parentElement;
                }
                if(!el.parentElement) {
                    return;
                }
                const all_items = Array.from(element.querySelectorAll('[role="treeitem"]'));
                const siblings = Array.from(el.parentElement.children);
                const i = siblings.indexOf(el);

                /** Focus the treeitem at the given index.
                 * @param {number} j
                 */
                function focus_item(j) {
                    if(el.parentElement.role == 'tree') {
                        return;
                    }
                    const item = siblings[j];
                    if(!item) {
                        return;
                    }
                    item.querySelector('[role="treeitem"]').focus();
                }

                const handlers = {
                    'ArrowLeft': () => {
                        let el = focused;
                        while(el && el.role != 'group') {
                            el = el.parentElement;
                        };
                        if(!el) {
                            return;
                        }
                        const item = el.parentElement.querySelector('[role="treeitem"]');
                        if(!item) {
                            return;
                        }
                        item.focus();
                    },
                    'ArrowRight': () => {
                        let el = focused;
                        let group;
                        while(el && !group) {
                            el = el.parentElement;
                            group = el.querySelector('[role="group"]');
                        }
                        if(!group) {
                            return;
                        }
                        const first_child = group.querySelector('[role="group"] [role="treeitem"]');
                        if(!first_child) {
                            return;
                        }
                        first_child.focus();
                    },
                    'ArrowUp': () => {
                        focus_item(i - 1);
                    },
                    'ArrowDown': () => {
                        focus_item(i + 1);
                    },
                    'Home': () => {
                        const first_item = element.querySelector('[role="treeitem"]');
                        if(!first_item) {
                            return;
                        }
                        first_item.focus();
                    },
                    'End': () => {
                        const last_item = all_items[all_items.length - 1];
                        if(!last_item) {
                            return;
                        }
                        last_item.focus();
                    },
                }
                if(handlers[e.key]) {
                    handlers[e.key]();
                    e.preventDefault();
                } else if(e.key.length == 1) {
                    const j = all_items.indexOf(focused);
                    const cycled_items = all_items.slice(j + 1).concat(all_items.slice(0, j));
                    const search = e.key.toLowerCase();
                    const item = cycled_items.find((item) => item.textContent.toLowerCase().includes(search));
                    if(item) {
                        item.focus();
                        e.preventDefault();
                    }
                }
            });

            return Knockout.bindingHandlers.template.init(element, function() {
                return options
            }, allBindings, viewModel, innerBindingContext);

        },
        'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var v = Knockout.unwrap(valueAccessor());
            if(v === undefined) {
                return;
            }
            var trees = (bindingContext.$trees || []).slice();
            var innerBindingContext = bindingContext.createChildContext(valueAccessor).extend({
                '$trees': trees,
            });
            trees.push({
                element: element,
                context: bindingContext.extend({'$trees':trees})
            })
            var options = {
                templateEngine: Knockout.nativeTemplateEngine.instance
            }
            return Knockout.bindingHandlers.template.update(element, function() {
                return options
            }, allBindings, viewModel, innerBindingContext)
        }
    };

    Knockout.bindingHandlers.treeNode = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            return {controlsDescendantBindings: true};
        },

        update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var trees = bindingContext.$trees;
            var tree = trees[trees.length - 1];
            var innerBindingContext = tree.context.createChildContext(valueAccessor);
            var options = {
              name: tree.element,
                templateEngine: Knockout.nativeTemplateEngine.instance
            }
            return Knockout.bindingHandlers.template.update(element, function() {
                return options
            }, allBindings, viewModel, innerBindingContext)
        }
    }

    Knockout.bindingHandlers.download_file = {
        update: function(element, valueAccessor) {
            const file = Knockout.unwrap(valueAccessor());
            try {
                URL.revokeObjectURL(element.getAttribute('href'));
            } catch {
            }
            element.setAttribute('href', window.URL.createObjectURL(file));
            element.setAttribute('download', file.name);
        }
    }

    Knockout.bindingHandlers.part_aria_validity = {
        update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            const pd = Knockout.unwrap(allBindings().part);
            const valid = Knockout.unwrap(valueAccessor());
            if(valid) {
                element.setAttribute('aria-invalid', true);
                element.setAttribute('aria-errormessage', pd.part.full_path + '-warnings');
            } else {
                element.removeAttribute('aria-invalid');
                element.removeAttribute('aria-errormessage');
            }
        }
    }

    /** Bind the `open` attribute of a `<details>` element.
     */
    Knockout.bindingHandlers.open = {
        init: function(element, valueAccessor) {
            const value = valueAccessor();
            if(typeof value == 'function') {
                element.addEventListener('toggle', function(e) {
                    value(element.open);
                });
            }
        },

        update: function(element, valueAccessor) {
            const open = Knockout.unwrap(valueAccessor());
            element.open = open;
        }
    }

    Knockout.bindingHandlers.modal = {
        init: function(element, valueAccessor) {
            const options = valueAccessor();

            element.addEventListener('close', (e) => {
                const submit = Knockout.unwrap(options.submit);
                const cancel = Knockout.unwrap(options.cancel);
                element.close();
                if(element.returnValue != 'cancel') {
                    if(submit) {
                        submit();
                    }
                } else {
                    if(cancel) {
                        cancel();
                    }
                }
            });
        },
        update: function(element, valueAccessor) {
            const {show} = Knockout.unwrap(valueAccessor());
            if(show === undefined) {
                return;
            }
            if(show) {
                element.showModal();
            } else {
                element.close();
            }
        }
    };

    Knockout.bindingHandlers.tablist = {
        init: function(element, valueAccessor) {

            let search = '';

            element.addEventListener('keydown', (e) => {
                if(e.target == element) {
                    return;
                }
                const tabs = Array.from(element.querySelectorAll('[role="tab"]'));
                const i = tabs.indexOf(e.target);
                const cycled_tabs = tabs.slice(i + 1).concat(tabs.slice(0, i + 1));

                if(!tabs.length) {
                    return;
                }

                /** Focus the tab at the given index. Also resets the search.
                 *
                 * @param {number} j
                 */
                function focus_tab(j) {
                    tabs[j].focus();
                    search = '';
                    e.preventDefault();
                }

                /** Find a tab whose name starts with or includes the search term.
                 * @returns {Element}
                 */
                function search_for_tab() {
                    const tab = cycled_tabs.find((tab) => tab.dataset.name && tab.dataset.name.toLowerCase().startsWith(search)) || cycled_tabs.find((tab) => tab.dataset.name && tab.dataset.name.toLowerCase().includes(search));
                    if(tab) {
                        tab.focus();
                    }
                    return tab;
                }

                const handlers = {
                    'ArrowUp': () => {
                        focus_tab((i + tabs.length - 1) % tabs.length);
                    },
                    'ArrowDown': () => {
                        focus_tab((i + 1) % tabs.length);
                    },
                    'Home': () => {
                        focus_tab(0);
                    },
                    'End': () => {
                        focus_tab(tabs.length - 1);
                    },
                    ' ': () => {
                        e.target.dispatchEvent(new Event('click'));
                        e.preventDefault();
                    }
                };

                if(handlers[e.key]) {
                    handlers[e.key]();
                } else if(e.key.length == 1) {
                    e.preventDefault();
                    search += e.key;
                    while(search.length) {
                        const moved = search_for_tab();
                        if(moved) {
                            break;
                        }
                        search = search.slice(1);
                    }
                }
            });
        }
    }

});
;
Numbas.queueScript('localisation', ['i18next', 'localisation-resources'], function(module) {
    module.exports.R = function() {
        { return i18next.t.apply(i18next, arguments) }
    };

    var plain_en = ['plain', 'en', 'si-en'];
    var plain_eu = ['plain-eu', 'eu', 'si-fr'];
    Numbas.locale.default_number_notations = {
        'ar-SA': plain_en,
        'en-GB': plain_en,
        'de-DE': plain_eu,
        'es-ES': plain_eu,
        'fr-FR': plain_eu,
        'he-IL': plain_en,
        'in-ID': plain_eu,
        'it-IT': plain_eu,
        'ja-JP': plain_en,
        'ko-KR': plain_en,
        'nb-NO': plain_eu,
        'nl-NL': plain_eu,
        'pl-PL': plain_eu,
        'pt-BR': plain_eu,
        'sq-AL': plain_eu,
        'sv-SR': plain_eu,
        'tr-TR': plain_eu,
        'vi-VN': plain_eu,
        'zh-CN': plain_en
    }

    Numbas.locale.default_list_separators = {
        'ar-SA': ',',
        'en-GB': ',',
        'de-DE': ';',
        'es-ES': ';',
        'fr-FR': ';',
        'he-IL': ',',
        'in-ID': ';',
        'it-IT': ';',
        'ja-JP': ',',
        'ko-KR': ',',
        'nb-NO': ';',
        'nl-NL': ';',
        'pl-PL': ';',
        'pt-BR': ';',
        'sq-AL': ';',
        'sv-SR': ';',
        'tr-TR': ';',
        'vi-VN': ';',
        'zh-CN': ','
    };

    Numbas.locale.set_preferred_locale = function(locale) {
        Numbas.locale.preferred_locale = locale;
        Numbas.locale.default_number_notation = Numbas.locale.default_number_notations[Numbas.locale.preferred_locale] || plain_en;
        Numbas.locale.default_list_separator = Numbas.locale.default_list_separators[Numbas.locale.preferred_locale] || ',';
    }

    Numbas.locale.init = function() {
        i18next.init({
            lng: Numbas.locale.preferred_locale,
            lowerCaseLng: true,
            keySeparator: false,
            nsSeparator: false,
            interpolation: {
                unescapePrefix: '-',
                format: function(value, format) {
                    if(format == 'niceNumber') {
                        return Numbas.math.niceNumber(value);
                    }
                }
            },
            resources: Numbas.locale.resources
        });
        Numbas.locale.set_preferred_locale(Numbas.locale.preferred_locale);
        Numbas.signals.trigger('localisation initialised');
    };
});
;
Numbas.queueScript('marking', ['util', 'jme', 'localisation', 'jme-variables', 'math'], function() {
    /** @namespace Numbas.marking */
    var marking = Numbas.marking = {};

    marking.ignore_note_errors = true;

    var jme = Numbas.jme;
    var sig = jme.signature;
    var math = Numbas.math;
    var TNothing = jme.types.TNothing;
    var TString = jme.types.TString;
    var THTML = jme.types.THTML;
    var TList = jme.types.TList;
    var TName = jme.types.TName;
    var TNum = jme.types.TNum;
    var TBool = jme.types.TBool;
    var TDict = jme.types.TDict;

    var Fraction = math.Fraction;

    /** A line of feedback to give to the student, produced while marking their answer.
     * Can modify the credit awarded.
     *
     * @typedef {object} Numbas.marking.feedback_item
     *
     * @property {string} op - The operation to perform. See {@link Numbas.marking.FeedbackOps}
     * @property {number} [credit] - Parameter to change the credit awarded. The exact meaning depends on `op`.
     * @property {string} [reason] - An extra note about why the op is being applied. For 'correct' and 'incorrect' feedback, this helps distinguish cases when the credit awarded doesn't change. 'invalid' means the answer could not be marked.
     * @property {string} [message] - A message to display to the student.
     * @property {number} [factor] - For `MULTIPLY_CREDIT` items, the factor to multiply the current credit by.
     * @property {number} [scale] - For `CONCAT` items, the amount to scale the credit awarded by the concatenated messages by.
     * @property {Numbas.marking.feedback_item[]} [messages] - For `CONCAT` items, the items to add to the state.
     * @property {boolean} [invalid] - For ``END`` items, does this item represent a decision that the answer is invalid?
     */

    /** Kinds of feedback item.
     *
     * @readonly
     * @enum {string}
     * @memberof Numbas.marking
     */
    var FeedbackOps = Numbas.marking.FeedbackOps = {
        /** Set the credit to the given value. */
        SET_CREDIT: 'set_credit',

        /** Add the given amount of credit. */
        ADD_CREDIT: 'add_credit',

        /** Multiply the current credit by the given amount. */
        MULTIPLY_CREDIT: 'multiply_credit',

        /** Subtract the given amount of credit. */
        SUB_CREDIT: 'sub_credit',

        /** End marking. */
        END: 'end',

        /** Give the student a warning next to the answer widget. */
        WARNING: 'warning',

        /** Give the student a message. */
        FEEDBACK: 'feedback',

        /** Add the given list of items to the end of the current list of feedback items. */
        CONCAT: 'concat'
    }

    /** Constructors for feedback items.
     *
     * @see Numbas.marking.feedback_item
     * @memberof Numbas.marking
     * @type {Object<Function>}
     */
    var feedback = Numbas.marking.feedback = {
        set_credit: function(credit, reason, message) {
            return {op: FeedbackOps.SET_CREDIT, credit: credit, reason: reason, message: message}
        },
        add_credit: function(credit, message) {
            return {op: FeedbackOps.ADD_CREDIT, credit: credit, message: message};
        },
        sub_credit: function(credit, message) {
            return {op: FeedbackOps.SUB_CREDIT, credit: credit, message: message};
        },
        multiply_credit: function(factor, message) {
            return {op: FeedbackOps.MULTIPLY_CREDIT, factor: factor, message: message}
        },
        end: function(invalid) {
            return {op: FeedbackOps.END, invalid: invalid || false}
        },
        warning: function(message) {
            return {op: FeedbackOps.WARNING, message: message}
        },
        feedback: function(message, reason, format) {
            return {op: FeedbackOps.FEEDBACK, message: message, reason: reason, format: format}
        },
        concat: function(messages, scale) {
            return {op: FeedbackOps.CONCAT, messages: messages, scale: scale};
        }
    }

    /** Create a JME function which modifies the state.
     *
     * @param {string} name
     * @param {Array.<Function|string>} args - A list of data type constructors for the function's paramters' types. Use the string '?' to match any type. Or, give the type's name with a '*' in front to match any number of that type. If `null`, then `options.typecheck` is used.
     * @param {Function} outtype - The constructor for the output value of the function
     * @param {Function} fn - A function which returns an object `{state,return}`, where `state` is a list of {@link Numbas.marking.feedback_item} to add to the state, and `return` is a {@link Numbas.jme.token}, the result of the function.
     * @see Numbas.marking.StatefulScope
     * @returns {Numbas.jme.funcObj}
     */
    var state_fn = marking.state_fn = function(name, args, outtype, fn) {
        return new jme.funcObj(name, args, outtype, null, {
            evaluate: function(args, scope) {
                let res;
                if(jme.lazyOps.contains(name)) {
                    res = fn.apply(this, arguments);
                } else {
                    res = fn.apply(this, args.map(jme.unwrapValue));
                }
                var p = scope;
                while(p.state === undefined) {
                    p = p.parent;
                }
                p.state = p.state.concat(res.state);
                return jme.wrapValue(res.return);
            }
        });
    }

    var state_functions = [];
    state_functions.push(state_fn('correct', [], TBool, function(message) {
        return {
            return: true,
            state: [feedback.set_credit(1, 'correct', R('part.marking.correct'))]
        };
    }));
    state_functions.push(state_fn('correct', [TString], TBool, function(message) {
        return {
            return: true,
            state: [feedback.set_credit(1, 'correct', message)]
        };
    }));
    state_functions.push(state_fn('incorrect', [], TBool, function(message) {
        return {
            return: false,
            state: [feedback.set_credit(0, 'incorrect', R('part.marking.incorrect'))]
        };
    }));
    state_functions.push(state_fn('incorrect', [TString], TBool, function(message) {
        return {
            return: false,
            state: [feedback.set_credit(0, 'incorrect', message)]
        };
    }));
    var correctif = function(condition, correctMessage, incorrectMessage) {
        var state;
        if(condition) {
            state = feedback.set_credit(1, 'correct', correctMessage || R('part.marking.correct'));
        } else {
            state = feedback.set_credit(0, 'incorrect', incorrectMessage || R('part.marking.incorrect'));
        }
        return {
            return: condition,
            state: [state]
        };
    }
    state_functions.push(state_fn('correctif', [TBool], TBool, correctif));
    state_functions.push(state_fn('correctif', [TBool, TString, TString], TBool, correctif));
    state_functions.push(state_fn('set_credit', [TNum, TString], TNum, function(n, message) {
        return {
            return: n,
            state: [feedback.set_credit(n, undefined, message)]
        }
    }));
    state_functions.push(state_fn('multiply_credit', [TNum, TString], TNum, function(n, message) {
        return {
            return: n,
            state: [feedback.multiply_credit(n, message)]
        }
    }));
    state_functions.push(state_fn('multiply_credit_if', [TBool, TNum, TString, TString], TBool, function(condition, n, positive_message, negative_message) {
        return {
            return: condition,
            state: [condition ? feedback.multiply_credit(n, positive_message) : feedback.feedback(negative_message)]
        }
    }));
    state_functions.push(state_fn('multiply_credit_if', [TBool, TNum, TString], TBool, function(condition, n, positive_message) {
        return {
            return: condition,
            state: condition ? [feedback.multiply_credit(n, positive_message)] : []
        }
    }));
    state_functions.push(state_fn('add_credit', [TNum, TString], TNum, function(n, message) {
        return {
            return: n,
            state: [feedback.add_credit(n, message)]
        }
    }));
    state_functions.push(state_fn('add_credit_if', [TBool, TNum, TString, TString], TBool, function(condition, n, positive_message, negative_message) {
        return {
            return: condition,
            state: [condition ? feedback.add_credit(n, positive_message) : feedback.feedback(negative_message, n < 0 ? 'neutral' : 'incorrect')]
        }
    }));
    state_functions.push(state_fn('add_credit_if', [TBool, TNum, TString], TBool, function(condition, n, positive_message) {
        return {
            return: condition,
            state: condition ? [feedback.add_credit(n, positive_message)] : []
        }
    }));
    state_functions.push(state_fn('sub_credit', [TNum, TString], TNum, function(n, message) {
        return {
            return: n,
            state: [feedback.sub_credit(n, message)]
        }
    }));
    state_functions.push(state_fn('end', [], TBool, function() {
        return {
            return: true,
            state: [feedback.end()]
        }
    }));
    state_functions.push(state_fn('fail', [TString], TString, function(message) {
        return {
            return: message,
            state: [
                feedback.set_credit(0, 'invalid', message),
                feedback.end(true)
            ]
        };
    }));
    state_functions.push(state_fn('warn', [TString], TString, function(message) {
        return {
            return: message,
            state: [feedback.warning(message)]
        }
    }));
    state_functions.push(state_fn('feedback', [TString], TString, function(message) {
        return {
            return: message,
            state: [feedback.feedback(message)]
        }
    }));
    state_functions.push(state_fn('positive_feedback', [TString], TString, function(message) {
        return {
            return: message,
            state: [feedback.feedback(message, 'correct')]
        }
    }));
    state_functions.push(state_fn('negative_feedback', [TString], TString, function(message) {
        return {
            return: message,
            state: [feedback.feedback(message, 'incorrect')]
        }
    }));
    state_functions.push(state_fn('feedback', [THTML], THTML, function(html) {
        return {
            return: html,
            state: [feedback.feedback(html, undefined, 'html')]
        }
    }));
    state_functions.push(state_fn('positive_feedback', [THTML], THTML, function(message) {
        return {
            return: message,
            state: [feedback.feedback(message, 'correct', 'html')]
        }
    }));
    state_functions.push(state_fn('negative_feedback', [THTML], THTML, function(message) {
        return {
            return: message,
            state: [feedback.feedback(message, 'incorrect', 'html')]
        }
    }));
    state_functions.push(new jme.funcObj(';', ['?', '?'], '?', null, {
        evaluate: function(args, cope) {
            return args[1];
        }
    }));
    state_functions.push(state_fn('apply', ['multiple (name or list)'], TName, function(args, scope) {
        var out = {
            return: new TNothing(),
            state: []
        }
        for(let i = 0;i < args.length;i++) {
            if(args[i].tok.type == 'name') {
                var name = jme.normaliseName(args[i].tok.name, scope);
                var p = scope;
                while(p && p.state === undefined) {
                    p = p.parent;
                }
                var state = p.states[name];
                out.return = new TNothing();
                out.state = out.state.concat(state || []);
            } else {
                var feedback = scope.evaluate(args[i]);
                if(feedback.type != 'list') {
                    throw(new Numbas.Error('marking.apply.not a list'));
                }
                out.return = feedback;
                out.state = out.state.concat(jme.unwrapValue(feedback));
            }
        }
        return out;
    }));
    jme.lazyOps.push('apply');
    jme.substituteTreeOps.apply = function(tree, scope, allowUnbound) {
        return tree;
    }

    /** Submit the given answer to the given part.
     *
     * @param {Numbas.parts.Part} part
     * @param {*} answer
     * @returns {Numbas.jme.token} - A dictionary with keys "credit", "marks", "feedback", "answered".
     */
    function submit_part(part, answer) {
        var originalAnswer = part.stagedAnswer;
        if(answer !== undefined) {
            part.stagedAnswer = answer;
        }
        part.submit();
        part.stagedAnswer = originalAnswer;
        part.setStudentAnswer();
        return jme.wrapValue({
            credit: part.credit,
            marks: part.availableMarks(),
            feedback: part.finalised_result.states,
            answered: part.answered
        });
    }

    state_functions.push(new jme.funcObj('submit_part', [TString], TDict, null, {
        evaluate: function(args, scope) {
            var part = scope.question.getPart(args[0].value);
            return submit_part(part);
        }
    }));
    state_functions.push(new jme.funcObj('submit_part', [TString, '?'], TDict, null, {
        evaluate: function(args, scope) {
            var part = scope.question.getPart(args[0].value);
            var answer = jme.unwrapValue(args[1]);
            return submit_part(part, answer);
        }
    }));

    state_functions.push(new jme.funcObj('check_pre_submit', [TString, '?', TString], '?', null, {
        evaluate: function(args, scope) {
            var part = scope.question.getPart(args[0].value);
            var answer = args[1];
            var exec_path = args[2].value
            var res = part.do_pre_submit_tasks(answer, scope, exec_path);
            if(res.waiting) {
                return new jme.types.TPromise(res.waiting.then(function(results) {
                    return {
                        gaps: new TList(results.map(function(r) {
 return new TDict(r);
}))
                    };
                }));
            } else {
                return new TNothing();
            }
        }
    }));

    state_functions.push(new jme.funcObj('apply_marking_script', [TString, '?', TDict, TNum], TDict, null, {
        evaluate: function(args, scope) {
            var script_name = args[0].value;
            var script = new marking.MarkingScript(Numbas.raw_marking_scripts[script_name], null, scope);
            if(!script) {
                throw(new Numbas.Error('marking.apply marking script.script not found', {name: script_name}));
            }
            var nscope = new StatefulScope([scope]);
            for(const x of Object.keys(scope.states)) {
                nscope.deleteVariable(x);
            }
            var result = script.evaluate(
                nscope,
                {
                    studentAnswer: args[1],
                    settings: args[2],
                    marks: args[3]
                }
            );
            if(result.state_errors.mark) {
                throw(result.state_errors.mark);
            }
            var notes = {};
            Object.keys(result.states).forEach(function(name) {
                notes[name] = {
                    feedback: result.states[name],
                    value: result.values[name],
                    valid: result.state_valid[name]
                }
            });
            return jme.wrapValue(notes);
        }
    }));
    state_functions.push(new jme.funcObj('mark_part', [TString, '?'], TDict, null, {
        evaluate: function(args, scope) {
            var part = scope.question.getPart(args[0].value);
            var answer = args[1];
            var part_result;
            if(answer.type == 'nothing') {
                part.setCredit(0, R('part.marking.nothing entered'));
                part_result = {
                    states: {mark: []},
                    state_valid: {},
                    state_errors: {},
                    values: {interpreted_answer:answer}
                }
            } else {
                part_result = part.mark_answer(answer, part.getScope());
            }
            if(part_result.waiting_for_pre_submit) {
                return jme.wrapValue({
                    marks: part.availableMarks(),
                    credit: 0,
                    feedback: [],
                    valid: false,
                    states: {},
                    state_valid: {},
                    values: {}
                });
            }
            var result = marking.finalise_state(part_result.states.mark);
            return jme.wrapValue({
                marks: part.availableMarks(),
                credit: result.credit,
                feedback: result.states,
                valid: result.valid,
                states: part_result.states,
                state_valid: part_result.state_valid,
                values: part_result.values
            });
        }
    }));
    state_functions.push(state_fn('concat_feedback', [TList, TNum, sig.optional(sig.type('boolean'))], TList, function(messages, scale, strip_messages) {
        if(strip_messages) {
            messages = messages.map(function(m) {
                return Numbas.util.extend_object({}, m, {message: ''});
            });
        }
        return {
            return: messages,
            state: [feedback.concat(messages, scale)]
        }
    }));


    /** A JME scope with marking state attached.
     * The "current" state is a list of feedback items.
     * The scope can also refer to previously computed states by name.
     * The state can be modified by functions as they are called.
     * This should be the base.
     *
     * @memberof Numbas.marking
     * @augments Numbas.jme.Scope
     * @class
     * @property {Numbas.marking.feedback_item[]} state - The list of feedback items produced so far.
     * @property {Object<Numbas.marking.feedback_item[]>} states - Previously computed states.
     * @property {Object<boolean>} state_valid - Record of whether previously computed states were valid.
     * @property {Object<Error>} state_errors - The errors that caused states to become invalid, if any.
     */
    var StatefulScope = marking.StatefulScope = function() {
        this.nesting_depth = 0;
        this.state = [];
        this.states = {};
        this.state_valid = {};
        this.state_errors = {};
        var scope = this;
        state_functions.forEach(function(fn) {
            scope.addFunction(fn);
        });
    }
    StatefulScope.prototype = /** @lends Numbas.marking.StatefulScope.prototype */ {
        evaluate: function(expr, variables) {
            var is_top = this.state === undefined || this.nesting_depth == 0;
            this.nesting_depth += 1;
            var old_state = is_top ? [] : (this.state || []);
            this.state = [];
            try {
                var v = jme.Scope.prototype.evaluate.apply(this, [expr, variables]);
            } catch(e) {
                this.nesting_depth -= 1;
                throw(e);
            }
            this.nesting_depth -= 1;
            this.state = old_state.concat(this.state);
            return v;
        }
    }
    StatefulScope = marking.StatefulScope = Numbas.util.extend(jme.Scope, StatefulScope);

    /** The result of a marking script.
     *
     * @typedef {object} Numbas.marking.marking_script_result
     *
     * @property {Object<Numbas.marking.feedback_item[]>} states - The feedback resulting from each of the notes.
     * @property {Object<Numbas.jme.token>} values - The values of each of the notes.
     * @property {Object<boolean>} state_valid - See {@link Numbas.marking.StatefulScope#state_valid}.
     * @property {Object<Error>} state_errors - See {@link Numbas.marking.StatefulScope#state_errors}.
     */

    /** Compute the marking note with the given name in the given scope.
     *
     * @memberof Numbas.marking
     * @function
     * @see Numbas.jme.variables.computeVariable
     *
     * @param {string} name
     * @param {object} todo - Dictionary of notes still to evaluate.
     * @param {Numbas.marking.StatefulScope} scope
     *
     * @returns {Numbas.jme.token}
     */
    var compute_note = marking.compute_note = function(name, todo, scope) {
        var existing_value = scope.getVariable(name);
        if(existing_value) {
            return existing_value;
        }
        var stateful_scope = scope;
        while(stateful_scope && !stateful_scope.state) {
            stateful_scope = stateful_scope.parent;
        }
        if(!stateful_scope.states[name]) {
            try {
                var res = jme.variables.computeVariable.apply(this, arguments);
                scope.setVariable(name, res);
                stateful_scope.state_valid[name] = true;
                for(let i = 0;i < stateful_scope.state.length;i++) {
                    if(stateful_scope.state[i].op == 'end' && stateful_scope.state[i].invalid) {
                        stateful_scope.state_valid[name] = false;
                        break;
                    }
                }
            } catch(e) {
                stateful_scope.state_errors[name] = e;
                var invalid_dep = null;
                for(let i = 0;i < todo[name].vars.length;i++) {
                    var x = todo[name].vars[i];
                    if(x in todo) {
                        if(!stateful_scope.state_valid[x]) {
                            invalid_dep = x;
                            break;
                        }
                    }
                }
                if(invalid_dep || marking.ignore_note_errors) {
                    stateful_scope.state_valid[name] = false;
                } else {
                    throw(new Numbas.Error("marking.note.error evaluating note", {name:name, message:e.message}));
                }
            }
            stateful_scope.states[name] = stateful_scope.state.slice().map(function(s) {
s.note = s.note || name; return s
});
        }
        return scope.getVariable(name);
    }

    /** A script to mark a part.
     * A list of notes, which can refer to each other. The dependencies must form a directed acyclic graph, like for JME variables.
     *
     * Two notes are required:
     *
     * - The `mark` note is the final note, used to provide feedback on the part.
     * - The value of the `interpreted_answer` note is used to represent the student's answer, as the script interpreted it.
     *
     * @memberof Numbas.marking
     * @class
     *
     * @param {string} source - The definitions of the script's notes.
     * @param {Numbas.marking.MarkingScript} [base] - A base script to extend.
     */
    marking.MarkingScript = jme.variables.note_script_constructor(
        function(scope, variables) {
            return new StatefulScope([
                scope, {variables: variables}
            ]);
        },
        function(result, scope) {
            return {
                states: scope.states,
                values: result.variables,
                state_valid: scope.state_valid,
                state_errors: scope.state_errors
            };
        },
        compute_note
    );

    /** The result of attempting to mark a part.
     *
     * @typedef Numbas.marking.finalised_state
     * @type {object}
     * @property {boolean} valid - Can the answer be marked?
     * @property {number} credit - Proportion of the credit to award.
     * @property {Array.<object>} states - Feedback actions.
     */

    /** Run through a sequence of state operations, accumulating credit.
     * It might look like this is duplicated in `Numbas.parts.Part#apply_feedback`, but we need to be able to get a description of what a sequence of operations does in abstract so it can be reused in marking scripts for parent parts.
     *
     * @see Numbas.parts.Part#apply_feedback
     * @function
     * @memberof Numbas.marking
     * @param {Numbas.marking.feedback_item[]} states
     * @returns {Numbas.marking.finalised_state}
     */
    marking.finalise_state = function(states) {
        var valid = true;
        var end = false;
        var credit = Fraction.zero;
        var out_states = [];
        var num_lifts = 0;
        var lifts = [];
        var scale = 1;
        for(let i = 0;i < states.length;i++) {
            var state = states[i];
            switch(state.op) {
                case FeedbackOps.SET_CREDIT:
                    out_states.push(state);
                    credit = Fraction.fromFloat(state.credit);
                    break;
                case FeedbackOps.MULTIPLY_CREDIT:
                    out_states.push(state);
                    credit = credit.multiply(Fraction.fromFloat(state.factor));
                    break;
                case FeedbackOps.ADD_CREDIT:
                    out_states.push(state);
                    credit = credit.add(Fraction.fromFloat(state.credit));
                    break;
                case FeedbackOps.SUB_CREDIT:
                    out_states.push(state);
                    credit = credit.subtract(Fraction.fromFloat(state.credit));
                    break;
                case FeedbackOps.END:
                    out_states.push(state);
                    if(state.invalid) {
                        valid = false;
                    }
                    if(num_lifts) {
                        while(i + 1 < states.length && states[i + 1].op != "end_lift") {
                            i += 1;
                        }
                    } else {
                        end = true;
                    }
                    break;
                case FeedbackOps.CONCAT:
                    states = states.slice(0, i + 1).concat(
                        [{op:"start_lift", scale:state.scale}],
                        state.messages,
                        [{op:"end_lift"}],
                        states.slice(i + 1)
                    );
                    break;
                case "start_lift":
                    num_lifts += 1;
                    lifts.push({credit: credit, scale: scale});
                    credit = Fraction.zero;
                    scale = state.scale;
                    out_states.push(state);
                    break;
                case "end_lift":
                    num_lifts -= 1;
                    var last_lift = lifts.pop();
                    var lift_credit = credit;
                    credit = last_lift.credit;
                    credit = credit.add(lift_credit.multiply(Fraction.fromFloat(scale)));
                    scale = last_lift.scale;
                    out_states.push(state);
                    break;
                default:
                    out_states.push(state);
            }
            if(end) {
                break;
            }
        }
        return {
            valid: valid,
            credit: credit.toFloat(),
            states: out_states
        }
    }
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Mathematical functions, providing stuff that the built-in `Math` object doesn't, as well as vector and matrix math operations.
 *
 * Provides {@link Numbas.math}, {@link Numbas.vectormath} and {@link Numbas.matrixmath}
 */
Numbas.queueScript('math', ['base', 'decimal'], function() {

    /** The maximum number of decimal places a float (JS Number object) can be rounded to.
     */
    var MAX_FLOAT_PRECISION = 17;

    Decimal.set({
        precision: 40,
        modulo: Decimal.EUCLID,
        toExpPos: 1000,
        toExpNeg: -1000
    });

/** Mathematical functions, providing stuff that the built-in `Math` object doesn't.
 *
 * @namespace Numbas.math
 */

/** A complex number.
 *
 * @typedef complex
 * @property {number} re - The real part.
 * @property {number} im - The imaginary part.
 */
/** @typedef range
 * A range of numbers, separated by a constant interval and between fixed lower and upper bounds.
 *
 * @type {Array.<number>}
 * @property {number} 0 Minimum value
 * @property {number} 1 Maximum value
 * @property {number} 2 Step size
 * @see Numbas.math.defineRange
 */
/** @typedef matrix
 * A 2D array of numbers.
 *
 * @type {Array.<Array.<number>>}
 * @property {number} rows - The number of rows in the matrix.
 * @property {number} columns - The number of columns in the matrix.
 */
/** @typedef fraction_matrix
 * A 2D array of fractions.
 *
 * @type {Array.<Array.<Numbas.math.Fraction>>}
 * @property {number} rows - The number of rows in the array.
 * @property {number} columns - The number of columns in the array.
 */

var math = Numbas.math = /** @lends Numbas.math */ {
    /** Regex to match numbers in scientific notation.
     *
     * @type {RegExp}
     * @memberof Numbas.math
     */
    re_scientificNumber: /(-?(?:0|[1-9]\d*)(?:\.\d+)?)[eE]([+-]?\d+)/,
    /** Construct a complex number from real and imaginary parts.
     *
     * Elsewhere in this documentation, `{number}` will refer to either a JavaScript float or a {@link complex} object, interchangeably.
     *
     * @param {number} re
     * @param {number} im
     * @returns {complex}
     */
    complex: function(re, im) {
        if(!im) {
            return re;
        } else {
            return {re: re, im: im, complex: true,
                toString: math.complexToString}
        }
    },
    /** String version of a complex number.
     *
     * @see Numbas.math.niceNumber
     * @function
     * @returns {string}
     */
    complexToString: function() {
        return math.niceNumber(this);
    },
    /** Negate a number.
     *
     * @param {number} n
     * @returns {number}
     */
    negate: function(n) {
        if(n.complex) {
            return math.complex(-n.re, -n.im);
        } else {
            return -n;
        }
    },
    /** Complex conjugate.
     *
     * @param {number} n
     * @returns {number}
     */
    conjugate: function(n) {
        if(n.complex) {
            return math.complex(n.re, -n.im);
        } else {
            return n;
        }
    },
    /** Add two numbers.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    add: function(a, b) {
        if(a.complex) {
            if(b.complex) {
                return math.complex(a.re + b.re, a.im + b.im);
            } else {
                return math.complex(a.re + b, a.im);
            }
        } else {
            if(b.complex) {
                return math.complex(a + b.re, b.im);
            } else {
                return a + b;
            }
        }
    },
    /** Subtract one number from another.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    sub: function(a, b) {
        if(a.complex) {
            if(b.complex) {
                return math.complex(a.re - b.re, a.im - b.im);
            } else {
                return math.complex(a.re - b, a.im);
            }
        } else {
            if(b.complex) {
                return math.complex(a - b.re, -b.im);
            } else {
                return a - b;
            }
        }
    },
    /** Multiply two numbers.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    mul: function(a, b) {
        if(a.complex) {
            if(b.complex) {
                return math.complex(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re);
            } else {
                return math.complex(a.re * b, a.im * b);
            }
        } else {
            if(b.complex) {
                return math.complex(a * b.re, a * b.im);
            } else {
                return a * b;
            }
        }
    },
    /** Divide one number by another.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    div: function(a, b) {
        if(a.complex) {
            if(b.complex) {
                const q = b.re * b.re + b.im * b.im;
                return math.complex((a.re * b.re + a.im * b.im) / q, (a.im * b.re - a.re * b.im) / q);
            } else {
                return math.complex(a.re / b, a.im / b);
            }
        } else {
            if(b.complex) {
                const q = b.re * b.re + b.im * b.im;
                return math.complex(a * b.re / q, -a * b.im / q);
            } else {
                return a / b;
            }
        }
    },
    /** Exponentiate a number.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    pow: function(a, b) {
        if(a.complex && Numbas.util.isInt(b) && Math.abs(b) < 100) {
            if(b < 0) {
                return math.div(1, math.pow(a, -b));
            }
            if(b == 0) {
                return 1;
            }
            var coeffs = math.binomialCoefficients(b);
            var re = 0;
            var im = 0;
            var sign = 1;
            for(let i = 0;i < b;i += 2) {
                re += coeffs[i] * Math.pow(a.re, b - i) * Math.pow(a.im, i) * sign;
                im += coeffs[i + 1] * Math.pow(a.re, b - i - 1) * Math.pow(a.im, i + 1) * sign;
                sign = -sign;
            }
            if(b % 2 == 0) {
                re += Math.pow(a.im, b) * sign;
            }
            return math.complex(re, im);
        }
        if(a.complex || b.complex || (a < 0 && math.fract(b) != 0)) {
            if(!a.complex) {
                a = {re: a, im: 0, complex: true};
            }
            if(!b.complex) {
                b = {re: b, im: 0, complex: true};
            }
            var ss = a.re * a.re + a.im * a.im;
            var arg1 = math.arg(a);
            var mag = Math.pow(ss, b.re / 2) * Math.exp(-b.im * arg1);
            var arg = b.re * arg1 + (b.im * Math.log(ss)) / 2;
            return math.complex(mag * Math.cos(arg), mag * Math.sin(arg));
        } else if(a == Math.E) {
            return Math.exp(b);
        } else {
            return Math.pow(a, b);
        }
    },
    /** Calculate the Nth row of Pascal's triangle.
     *
     * @param {number} n
     * @returns {Array.<number>}
     */
    binomialCoefficients: function(n) {
        var b = [1];
        var f = 1;
        for(let i = 1;i <= n;i++) {
            b.push(f *= (n + 1 - i) / i);
        }
        return b;
    },
    /** `a mod b`. Always returns a positive number.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    mod: function(a, b) {
        if(b == Infinity) {
            return a;
        }
        b = math.abs(b);
        return ((a % b) + b) % b;
    },
    /** Calculate the `b`-th root of `a`.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    root: function(a, b) {
        if(!a.complex && a < 0 && b % 2 == 1) {
            return -math.root(-a, b);
        }
        return math.pow(a, div(1, b));
    },
    /** Square root.
     *
     * @param {number} n
     * @returns {number}
     */
    sqrt: function(n) {
        if(n.complex) {
            var r = math.abs(n);
            return math.complex(Math.sqrt((r + n.re) / 2), (n.im < 0 ? -1 : 1) * Math.sqrt((r - n.re) / 2));
        } else if(n < 0) {
            return math.complex(0, Math.sqrt(-n));
        } else {
            return Math.sqrt(n)
        }
    },
    /** Natural logarithm (base `e`).
     *
     * @param {number} n
     * @returns {number}
     */
    log: function(n) {
        if(n.complex) {
            var mag = math.abs(n);
            var arg = math.arg(n);
            return math.complex(Math.log(mag), arg);
        } else if(n < 0) {
            return math.complex(Math.log(-n), Math.PI);
        } else {
            return Math.log(n);
        }
    },
    /** Calculate `e^n`.
     *
     * @param {number} n
     * @returns {number}
     */
    exp: function(n) {
        if(n.complex) {
            return math.complex(Math.exp(n.re) * Math.cos(n.im), Math.exp(n.re) * Math.sin(n.im));
        } else {
            return Math.exp(n);
        }
    },
    /** Magnitude of a number - absolute value of a real; modulus of a complex number.
     *
     * @param {number} n
     * @returns {number}
     */
    abs: function(n) {
        if(n.complex) {
            if(n.re == 0) {
                return Math.abs(n.im);
            } else if(n.im == 0) {
                return Math.abs(n.re);
            } else {
                return Math.sqrt(n.re * n.re + n.im * n.im)
            }
        } else {
            return Math.abs(n);
        }
    },
    /** Argument of a (complex) number.
     *
     * @param {number} n
     * @returns {number}
     */
    arg: function(n) {
        if(n.complex) {
            return Math.atan2(n.im, n.re);
        } else {
            return Math.atan2(0, n);
        }
    },
    /** Real part of a number.
     *
     * @param {number} n
     * @returns {number}
     */
    re: function(n) {
        if(n.complex) {
            return n.re;
        } else {
            return n;
        }
    },
    /** Imaginary part of a number.
     *
     * @param {number} n
     * @returns {number}
     */
    im: function(n) {
        if(n.complex) {
            return n.im;
        } else {
            return 0;
        }
    },
    /** Is `n` positive (Real, and greater than 0)?
     *
     * @param {number} n
     * @returns {boolean}
     */
    positive: function(n) {
        return !n.complex && math.gt(n, 0);
    },
    /** Is `n` negative (Real, and less than 0)?
     *
     * @param {number} n
     * @returns {boolean}
     */
    negative: function(n) {
        return math.lt(math.re(n), 0);
    },
    /** Is `n` nonnegative (Real, and greater than or equal to 0)?
     *
     * @param {number} n
     * @returns {boolean}
     */
    nonnegative: function(n) {
        return !math.negative(n);
    },
    /** Is `a` less than `b`?
     *
     * @throws {Numbas.Error} `math.order complex numbers` if `a` or `b` are complex numbers.
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     */
    lt: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return !math.geq(a, b);
    },
    /** Is `a` greater than `b`?
     *
     * @throws {Numbas.Error} `math.order complex numbers` if `a` or `b` are complex numbers.
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     */
    gt: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return !math.leq(a, b);
    },
    /** Is `a` less than or equal to `b`?
     *
     * @throws {Numbas.Error} `math.order complex numbers` if `a` or `b` are complex numbers.
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     */
    leq: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return a < b || math.eq(a, b);
    },
    /** Is `a` greater than or equal to `b`?
     *
     * @throws {Numbas.Error} `math.order complex numbers` if `a` or `b` are complex numbers.
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     */
    geq: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return a > b || math.eq(a, b);
    },
    /** Is `a` equal to `b`?
     *
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     */
    eq: function(a, b) {
        if(a.complex) {
            if(b.complex) {
                return math.eq(a.re, b.re) && math.eq(a.im, b.im);
            } else {
                return math.eq(a.re, b) && math.eq(a.im, 0);
            }
        } else {
            if(b.complex) {
                return math.eq(a, b.re) && math.eq(b.im, 0);
            } else {
                if(isNaN(a)) {
                    return isNaN(b);
                }
                return a == b || math.isclose(a, b);
            }
        }
    },

    /** Is `a` close to `b`?
     *
     * @param {number} a
     * @param {number} b
     * @param {number} [rel_tol=1e-15] - Relative tolerance: amount of error relative to `max(abs(a),abs(b))`.
     * @param {number} [abs_tol=1e-15] - Absolute tolerance: maximum absolute difference between `a` and `b`.
     * @returns {boolean}
     */
    isclose: function(a, b, rel_tol, abs_tol) {
        if(a === Infinity || b === Infinity || a == -Infinity || b == -Infinity) {
            return a === b;
        }
        rel_tol = rel_tol === undefined ? 1e-15 : rel_tol;
        abs_tol = abs_tol === undefined ? 1e-15 : abs_tol;

        if(a.complex || b.complex) {
            return math.abs(math.sub(a, b)) < abs_tol;
        }

        return Math.abs(a - b) <= Math.max(rel_tol * Math.max(Math.abs(a), Math.abs(b)), abs_tol);
    },

    /** Is `u` a scalar multiple `v`?
     *
     * @param {Array} u
     * @param {Array} v
     * @param {number} [rel_tol=1e-15] - Relative tolerance: amount of error relative to `max(abs(a),abs(b))`.
     * @param {number} [abs_tol=1e-15] - Absolute tolerance: maximum absolute difference between `a` and `b`.
     * @returns {boolean}
     */

    is_scalar_multiple: function(u, v, rel_tol, abs_tol) {
        // check edge case
        if(!Array.isArray(u) || !u.length || !Array.isArray(v) || !v.length) {
            return false;
        }
        // vector length must be the same
        if (u.length != v.length) {
            return false;
        }
        var n = u.length;
        var i = 0;
        var first_ratio;
        // corner case: denominator cannot be zero to avoid zero-division exception
        while (i < n) {
            if (v[i] == 0 && u[i] == 0) {
                i++;
            } else if (v[i] == 0 || u[i] == 0) {
                return false;
            } else {
                first_ratio = u[i] / v[i];
                break;
            }
        }
        for(; i < n; i++) {
            if (v[i] == 0 && u[i] == 0) {
                continue;
            } else if (v[i] == 0 || u[i] == 0) {
                return false;
            } else {
                var curr = u[i] / v[i];
                if (!math.isclose(curr, first_ratio, rel_tol, abs_tol)) {
                    return false;
                }
            }
        }
        return true;
    },

    /** Greatest of two numbers - wraps `Math.max`.
     *
     * @throws {Numbas.Error} `math.order complex numbers` if `a` or `b` are complex numbers.
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    max: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return Math.max(a, b);
    },
    /** Greatest of a list of numbers.
     *
     * @throws {Numbas.Error} `math.order complex numbers` if any element of the list is complex.
     * @param {Array} numbers
     * @param {Function} [maxfn=Numbas.math.max] - A function which returns the maximum of two values.
     * @returns {number}
     */
    listmax: function(numbers, maxfn) {
        if(numbers.length == 0) {
            return undefined;
        }
        maxfn = maxfn || math.max;
        var best = numbers[0];
        for(let i = 1;i < numbers.length;i++) {
            best = maxfn(best, numbers[i]);
        }
        return best;
    },
    /** Least of two numbers - wraps `Math.min`.
     *
     * @throws {Numbas.Error} `math.order complex numbers` if `a` or `b` are complex numbers.
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    min: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return Math.min(a, b);
    },
    /** Least of a list of numbers.
     *
     * @throws {Numbas.Error} `math.order complex numbers` if any element of the list is complex.
     * @param {Array} numbers
     * @param {Function} [minfn=Numbas.math.min] - A function which returns the minimum of two values.
     * @returns {number}
     */
    listmin: function(numbers, minfn) {
        if(numbers.length == 0) {
            return undefined;
        }
        minfn = minfn || math.min;
        var best = numbers[0];
        for(let i = 1;i < numbers.length;i++) {
            best = minfn(best, numbers[i]);
        }
        return best;
    },
    /** Are `a` and `b` unequal?
     *
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     * @see Numbas.math.eq
     */
    neq: function(a, b) {
        return !math.eq(a, b);
    },
    /** If `n` can be written in the form `a*pi^n`, with `a` an integer, return the biggest possible `n`, otherwise return `0`.
     * Also returns `1` for `n` of the form `pi/k`, with `k` an integer < 1000 if the parameter `allowFractions` is `true`.
     *
     * @param {number} n
     * @param {boolean} [allowFractions=true] - return 1 if `n` is of the form `pi/k`, for some integer `k < 1000`.
     * @returns {number}
     */
    piDegree: function(n, allowFractions) {
        if(allowFractions === undefined) {
            allowFractions = true;
        }

        n = Math.abs(n);
        if(n > 10000) {    //so big numbers don't get rounded to a power of pi accidentally
            return 0;
        }
        var degree, a;

        /* Check for pi/k, where k is an integer < 1000 */
        a = Math.PI / n;
        if(allowFractions && a < 1000 && Math.abs(a - math.round(a)) < 0.0000000001) {
            return 1;
        }

        for(degree = 1; (a = n / Math.pow(Math.PI, degree)) > 1 && (Math.abs(a - math.round(a)) > 0.00000001 && Math.abs(1 / a - math.round(1 / a)) > 0.00000001); degree++) {}
        return a >= 1 ? degree : 0;
    },
    /** Add the given number of zero digits to a string representation of a number.
     *
     * @param {string} n - A string representation of a number.
     * @param {number} digits - The number of digits to add.
     * @returns {string}
     */
    addDigits: function(n, digits) {
        n = n + '';
        var m = n.match(/^(-?\d+(?:\.\d+)?)(e[-+]?\d+)$/);
        if(m) {
            return math.addDigits(m[1], digits) + m[2];
        } else {
            if(n.indexOf('.') == -1) {
                n += '.';
            }
            for(let i = 0;i < digits;i++) {
                n += '0';
            }
            return n;
        }
    },

    /** Settings for {@link Numbas.math.niceNumber}.
     *
     * @typedef Numbas.math.niceNumber_settings
     * @property {string} precisionType - Either `"dp"` or `"sigfig"`.
     * @property {number} precision - Number of decimal places or significant figures to show.
     * @property {string} style - Name of a notational style to use. See {@link Numbas.util.numberNotationStyles}.
     * @property {string} scientificStyle - Name of a notational style to use for the significand in scientific notation. See {@link Numbas.util.numberNotationStyles}.
     * @property {string} syntax - The syntax to use for the rendered string. Either `"plain"` or `"latex"`.
     * @property {string} [infinity="infinity"] - The string to represent infinity.
     * @property {string} [imaginary_unit="i"] - The symbol to represent the imaginary unit.
     * @property {object} circle_constant - An object with attributes `scale` and `symbol` for the circle constant. `scale` is the ratio of the circle constant to pi, and `symbol` is the string to use to represent it.
     * @property {boolean} plaindecimal - Render `Decimal` values without the `dec("...")` wrapper?
     */

    /** Display a real number nicely. Unlike {@link Numbas.math.niceNumber}, doesn't deal with complex numbers or multiples of pi.
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @see Numbas.util.numberNotationStyles
     * @returns {string}
     */
    niceRealNumber: function(n, options) {
        options = options || {};
        if(n === undefined) {
            throw(new Numbas.Error('math.niceNumber.undefined'));
        }
        var out;
        var style = options.style || Numbas.locale.default_number_notation[0];
        if(options.style == 'scientific') {
            var s = n.toExponential();
            var bits = math.parseScientific(s);
            var noptions = {
                precisionType: options.precisionType,
                precision: options.precision,
                syntax: options.syntax,
                style: options.scientificStyle || Numbas.locale.default_number_notation[0]
            };
            var significand = math.niceNumber(bits.significand, noptions);
            var exponent = bits.exponent;
            if(exponent >= 0) {
                exponent = '+' + exponent;
            }
            return significand + 'e' + exponent;
        } else {
            let precision;
            switch(options.precisionType) {
            case 'sigfig':
                precision = options.precision;
                out = math.siground(n, precision) + '';
                var sigFigs = math.countSigFigs(out, true);
                if(sigFigs < precision) {
                    out = math.addDigits(out, precision - sigFigs);
                }
                break;
            case 'dp':
                precision = Math.min(options.precision, MAX_FLOAT_PRECISION);
                out = math.precround(n, precision) + '';
                var dp = math.countDP(out);
                if(dp < precision) {
                    out = math.addDigits(out, precision - dp);
                }
                break;
            default:
                var a = Math.abs(n);
                if(a < 1e-15) {
                    out = '0';
                } else if(Math.abs(n) < 1e-8) {
                    out = n + '';
                } else {
                    out = math.precround(n, 10) + '';
                }
            }
            out = math.unscientific(out);
            if(style && Numbas.util.numberNotationStyles[style]) {
                out = Numbas.util.formatNumberNotation(out, style, options.syntax);
            }
        }
        return out;
    },

    /** Display a number nicely - rounds off to 10dp so floating point errors aren't displayed.
     *
     * @param {number} n
     * @param {Numbas.math.niceNumber_settings} options
     * @see Numbas.util.numberNotationStyles
     * @returns {string}
     */
    niceNumber: function(n, options) {
        options = options || {};
        if(n === undefined) {
            throw(new Numbas.Error('math.niceNumber.undefined'));
        }
        if(n.complex) {
            var imaginary_unit = options.imaginary_unit || 'i';
            var re = math.niceNumber(n.re, options);
            var im = math.niceNumber(n.im, options);
            if(math.precround(n.im, 10) == 0) {
                return re + '';
            } else if(math.precround(n.re, 10) == 0) {
                if(n.im == 1) {
                    return imaginary_unit;
                } else if(n.im == -1) {
                    return '-' + imaginary_unit;
                } else {
                    return im + '*' + imaginary_unit;
                }
            } else if(n.im < 0) {
                if(n.im == -1) {
                    return re + ' - ' + imaginary_unit;
                } else {
                    return re + im + '*' + imaginary_unit;
                }
            } else {
                if(n.im == 1) {
                    return re + ' + ' + imaginary_unit;
                } else {
                    return re + ' + ' + im + '*' + imaginary_unit;
}
            }
        } else {
            var infinity = options.infinity || 'infinity';
            if(n == Infinity) {
                return infinity;
            } else if(n == -Infinity) {
                return '-' + infinity;
            }
            var piD = 0;
            var circle_constant_scale = 1;
            var circle_constant_symbol = 'pi';
            if(options.circle_constant) {
                circle_constant_scale = options.circle_constant.scale;
                circle_constant_symbol = options.circle_constant.symbol;
            }
            if(options.precisionType === undefined && (piD = math.piDegree(n, false)) > 0) {
                n /= Math.pow(Math.PI * circle_constant_scale, piD);
            }
            var out = math.niceRealNumber(n, options);
            switch(piD) {
                case 0:
                    return out;
                case 1:
                    if(n == 1) {
                        return circle_constant_symbol;
                    } else if(n == -1) {
                        return '-' + circle_constant_symbol;
                    } else {
                        return out + '*' + circle_constant_symbol;
                    }
                default:
                    if(n == 1) {
                        return circle_constant_symbol + '^' + piD;
                    } else if(n == -1) {
                        return '-' + circle_constant_symbol + '^' + piD;
                    } else {
                        return out + '*' + circle_constant_symbol + '^' + piD;
}
            }
        }
    },

    /** Display a {@link Numbas.math.ComplexDecimal} as a string.
     *
     * @param {Numbas.math.ComplexDecimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @see Numbas.util.numberNotationStyles
     * @returns {string}
     */
    niceComplexDecimal: function(n, options) {
        options = options || {};
        if(n === undefined) {
            throw(new Numbas.Error('math.niceNumber.undefined'));
        }
        var re = math.niceDecimal(n.re, options);
        if(n.isReal()) {
            return re;
        } else {
            var im = math.niceDecimal(n.im.absoluteValue(), options);
            if(options.style == 'scientific') {
                im = '(' + im + ')*i';
            } else {
                im = n.im.absoluteValue().equals(1) ? 'i' : im + '*i';
            }
            if(n.re.isZero()) {
                return (n.im.lessThan(0) ? '-' : '') + im;
            }
            var symbol = n.im.lessThan(0) ? '-' : '+';
            return re + ' ' + symbol + ' ' + im;
        }
    },

    /** Display a Decimal as a string.
     *
     * @param {Decimal} n
     * @param {Numbas.math.niceNumber_settings} options
     * @see Numbas.util.numberNotationStyles
     * @returns {string}
     */
    niceDecimal: function(n, options) {
        options = options || {};
        if(n === undefined) {
            throw(new Numbas.Error('math.niceNumber.undefined'));
        }
        if(!n.isFinite()) {
            return n.lessThan(0) ? '-infinity' : 'infinity';
        }

        var precision = options.precision;
        var style = options.style || Numbas.locale.default_number_notation[0];
        if(options.style == 'scientific') {
            var e = n.toExponential(options.precision);
            var m = e.match(/^(-?\d(?:\.\d+)?)(e[+-]\d+)$/);
            var significand = Numbas.util.formatNumberNotation(m[1], Numbas.locale.default_number_notation[0]);
            var exponential = m[2];
            return significand + exponential;
        } else {
            var out;
            switch(options.precisionType) {
            case 'sigfig':
                out = n.toPrecision(precision);
                break;
            case 'dp':
                out = n.toFixed(precision);
                break;
            default:
                out = n.toString();
            }
            if(style && Numbas.util.numberNotationStyles[style]) {
                out = Numbas.util.formatNumberNotation(out, style);
            }
            return out;
        }
    },

    /** Convert a JS Number to a Decimal.
     *
     * @param {number} x
     * @returns {Decimal}
     */
    numberToDecimal: function(x) {
        if(x.complex) {
            return new math.ComplexDecimal(math.numberToDecimal(x.re), math.numberToDecimal(x.im));
        } else {
            if(x == Math.PI) {
                return Decimal.acos(-1);
            } else if(x == Math.E) {
                return Decimal(1).exp();
            } else {
                return new Decimal(x);
            }
        }
    },

    /** Get a random number in range `[0..n-1]`.
     *
     * @param {number} n
     * @returns {number}
     */
    randomint: function(n) {
        return Math.floor(n * (Math.random() % 1));
    },
    /** Get a  random shuffling of the numbers `[0..n-1]`.
     *
     * @param {number} N
     * @returns {Array.<number>}
     */
    deal: function(N) {
        var J, K;
        var Q = new Array(N);
        for(J = 0 ; J < N ; J++) {
            K = math.randomint(J + 1) ; Q[J] = Q[K] ; Q[K] = J;
        }
        return Q;
    },
    /** Randomly shuffle a list. Returns a new list - the original is unmodified.
     *
     * @param {Array} list
     * @returns {Array}
     */
    shuffle: function(list) {
        var l = list.length;
        var permutation = math.deal(l);
        var list2 = new Array(l);
        for(let i = 0;i < l;i++) {
            list2[i] = (list[permutation[i]]);
        }
        return list2;
    },
    /** Calculate the inverse of a shuffling.
     *
     * @param {Array.<number>} l
     * @returns {Array.<number>} l
     * @see Numbas.math.deal
     */
    inverse: function(l) {
        var arr = new Array(l.length);
        for(let i = 0;i < l.length;i++) {
            arr[l[i]] = i;
        }
        return arr;
    },

    /** Reorder a list given a permutation.
     * The `i`th element of the output is the `order[i]`th element of `list`.
     *
     * @param {Array} list - The list to reorder.
     * @param {Array.<number>} order - The permutation to apply.
     * @returns {Array}
     */
    reorder: function(list, order) {
        return order.map(function(i) {
            return list[i];
        });
    },

    /** Shuffle a number of lists together - each list has the same permutation of its elements applied.
     * The lists must all be the same length, otherwise an error is thrown.
     *
     * @param {Array.<Array>} lists - The lists to reorder.
     * @returns {Array.<Array>}
     */
    shuffle_together: function(lists) {
        if(lists.length == 0) {
            return [];
        }
        var len = lists[0].length;
        for(let i = 1;i < lists.length;i++) {
            if(lists[i].length != len) {
                throw(new Numbas.Error("math.shuffle_together.lists not all the same length"));
            }
        }
        var order = math.deal(len);
        return lists.map(function(list) {
            return math.reorder(list, order);
        });
    },

    /** A random partition of the integer `n` into `k` non-zero parts.
     *
     * @param {number} n
     * @param {number} k
     * @returns {Array.<number>} - A list of `k` numbers whose sum is `n`.
     */
    random_integer_partition: function(n, k) {
        if(k > n || k < 1) {
            throw(new Numbas.Error("math.random_integer_partition.invalid k", {n:n, k:k}));
        }
        var shuffle = [];
        for(let i = 0;i < k - 1;i++) {
            if(shuffle[i] === undefined) {
                shuffle[i] = i;
            }
            var j = math.randomint(n - 1);
            if(shuffle[j] === undefined) {
                shuffle[j] = j;
            }
            var a = shuffle[i];
            shuffle[i] = shuffle[j];
            shuffle[j] = a;
        }
        shuffle = shuffle.slice(0, k - 1);
        shuffle.sort(function(a, b) {
            return a < b ? -1 : a > b ? 1 : 0;
        });
        var partition = [];
        var last = 0;
        for(let i = 0;i < k - 1;i++) {
            partition.push(shuffle[i] + 1 - last);
            last = shuffle[i] + 1;
        }
        partition.push(n - last);
        return partition;
    },

    /** Produce all of the ordered partitions of the integer `n` into `k` parts.
     *
     * @param {number} n
     * @param {number} k
     * @returns {Array.<Array.<number>>}
     */
    integer_partitions: function(n, k) {
        if(n < 0 || k <= 0) {
            if(k == 0 && n == 0) {
                return [[]];
            } else {
                return [];
            }
        }

        var out = [];
        for(let i = 0;i <= n;i++) {
            for(const p of math.integer_partitions(n - i, k - 1)) {
                out.push([i].concat(p));
            }
        }

        return out;
    },

    /* Just the numbers from 1 to `n` (inclusive) in an array!
     * @param {number} n
     * @returns {Array.<number>}
     */
    range: function(n) {
        var arr = new Array(n);
        for(let i = 0;i < n;i++) {
            arr[i] = i;
        }
        return arr;
    },
    /** Round `a` to `b` decimal places. Real and imaginary parts of complex numbers are rounded independently.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     * @throws {Numbas.Error} "math.precround.complex" if b is complex.
     */
    precround: function(a, b) {
        if(b.complex) {
            throw(new Numbas.Error('math.precround.complex'));
        }
        if(a.complex) {
            return math.complex(math.precround(a.re, b), math.precround(a.im, b));
        } else {
            b = Math.min(b, MAX_FLOAT_PRECISION);
            var be = Math.pow(10, b);
            var fracPart = a % 1;
            var intPart = a - fracPart;
            //test to allow a bit of leeway to account for floating point errors
            //if a*10^b is less than 1e-9 away from having a five as the last digit of its whole part, round it up anyway
            var v = fracPart * be * 10 % 1;
            var d = (fracPart > 0 ? Math.floor : Math.ceil)(fracPart * be * 10 % 10);
            // multiply fractional part by 10^b; we'll throw away the remaining fractional part (stuff < 10^b)
            fracPart *= be;
            if((d == 4 && 1 - v < 1e-9) || (d == -5 && v > -1e-9 && v < 0)) {
                fracPart += 1;
            }
            var rounded_fracPart = Math.round(fracPart);
            // if the fractional part has rounded up to a whole number, just add sgn(fracPart) to the integer part
            if(rounded_fracPart == be || rounded_fracPart == -be) {
                return intPart + math.sign(fracPart);
            }
            // get the fractional part as a string of decimal digits
            var fracPartString = Math.round(Math.abs(fracPart)) + '';
            while(fracPartString.length < b) {
                fracPartString = '0' + fracPartString;
            }
            // construct the rounded number as a string, then convert it to a JS float
            var out = parseFloat(intPart + '.' + fracPartString);
            // make sure a negative number remains negative
            if(intPart == 0 && a < 0) {
                return -out;
            } else {
                return out;
            }
        }
    },

    /** Get the significand and exponent of a number written in exponential form.
     *
     * @param {string} str
     * @param {boolean} [parse=true] - Parse the significand and exponent values to numbers, or leave them as strings?
     * @returns {object} `{significand: number, exponent: number}` if `parse` is true, or `{significand: string, exponent: string}`
     */
    parseScientific: function(str, parse) {
        var m = /(-?\d[ \d]*(?:\.\d[ \d]*)?)e([-+]?\d[ \d]*)/i.exec(str);
        var significand = m[1].replace(/ /g, '');
        var exponent = m[2].replace(/ /g, '').replace(/^\+/, '');
        parse = parse || (parse === undefined);
        if(parse) {
            return {significand: parseFloat(significand), exponent: parseInt(exponent)};
        } else {
            return {significand, exponent};
        }
    },

    /** If the given string is scientific notation representing a number, return a string of the form `\d+\.\d+`.
     * For example, '1.23e-5' is returned as '0.0000123'.
     *
     * @param {string} str
     * @returns {string}
     */
    unscientific: function(str) {
        var m = /(-)? *(0|[1-9][ \d]*)(?:\.([ \d]+))?e([-+]?[\d ]+)/i.exec(str);
        if(!m) {
            return str;
        }
        var minus = m[1] || '';
        var significand_integer = m[2].replace(/ /g, '');
        var significand_decimal = (m[3] || '').replace(/ /g, '');
        var digits = significand_integer + significand_decimal;
        var pow = parseInt(m[4].replace(/ /g, ''));
        pow += significand_integer.length
        var zm = digits.match(/^(0+)[^0]/);
        if(zm) {
            var num_zeros = zm[1].length;
            digits = digits.slice(num_zeros);
            pow -= num_zeros;
        }
        var l = digits.length;
        var out;
        if(l < pow) {
            out = digits;
            for(let i = l;i < pow;i++) {
                out += '0';
            }
        } else if(pow < 0) {
            out = digits;
            for(let i = 0;i < -pow;i++) {
                out = '0' + out;
            }
            out = '0.' + out;
        } else {
            out = digits.slice(0, pow);
            if(digits.length > pow) {
                out += '.' + digits.slice(pow);
            }
        }
        return minus + out;
    },
    /** Round `a` to `b` significant figures. Real and imaginary parts of complex numbers are rounded independently.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     * @throws {Numbas.Error} "math.precround.complex" if b is complex.
     */
    siground: function(a, b) {
        if(b.complex) {
            throw(new Numbas.Error('math.siground.complex'));
        }
        if(a.complex) {
            return math.complex(math.siground(a.re, b), math.siground(a.im, b));
        } else {
            if(math.isclose(a, 0)) {
                return 0;
            }
            return parseFloat(a.toPrecision(b))
        }
    },
    /** Count the number of decimal places used in the string representation of a number.
     *
     * @param {number|string} n
     * @returns {number}
     */
    countDP: function(n) {
        var m = (n + '').match(/(?:\.(\d*))?(?:[Ee]([-+])?(\d+))?$/);
        if(!m) {
            return 0;
        } else {
            var dp = m[1] ? m[1].length : 0;
            if(m[2] && m[2] == '-') {
                dp += parseInt(m[3]);
            }
            return dp;
        }
    },
    /**
     * Calculate the significant figures precision of a number.
     *
     * @param {number|string} n - if a string, only the "plain" number format or scientific notation is expected. Strings representing numbers should be cleaned first, using `Numbas.util.cleanNumber`.
     * @param {boolean} [max] - Be generous with calculating sig. figs. for whole numbers. e.g. '1000' could be written to 4 sig figs.
     * @returns {number}
     */
    countSigFigs: function(n, max) {
        n += '';
        var m;
        if(max) {
            m = n.match(/^-?(?:(\d0*)$|(?:([1-9]\d*[1-9]0*)$)|([1-9]\d*\.\d+$)|(0\.0+$)|(?:0\.0*([1-9]\d*))|(?:(\d*(?:\.\d+)?)\s*[Ee]\s*[+-]?\d+)$)/i);
        } else {
            m = n.match(/^-?(?:(\d)0*$|(?:([1-9]\d*[1-9])0*$)|([1-9]\d*\.\d+$)|(0\.0+$)|(?:0\.0*([1-9]\d*))|(?:(\d*(?:\.\d+)?)\s*[Ee]\s*[+-]?\d+)$)/i);
        }
        if(!m) {
            return 0;
        }
        var sigFigs = m[1] || m[2] || m[3] || m[4] || m[5] || m[6];
        return sigFigs.replace('.', '').length;
    },
    /** Is n given to the desired precision?
     *
     * @param {number|string} n
     * @param {string} precisionType - Either 'dp' or 'sigfig'.
     * @param {number} precision - Number of desired digits of precision.
     * @param {boolean} strictPrecision - Must trailing zeros be used to get to the desired precision (true), or is it allowed to give fewer digits in that case (false)?
     * @returns {boolean}
     */
    toGivenPrecision: function(n, precisionType, precision, strictPrecision) {
        if(precisionType == 'none') {
            return true;
        }
        n += '';
        var precisionOK = false;
        var counters = {'dp': math.countDP, 'sigfig': math.countSigFigs};
        var counter = counters[precisionType];
        var digits = counter(n);
        if(strictPrecision) {
            precisionOK = digits == precision;
        } else {
            precisionOK = digits <= precision;
        }
        if(precisionType == 'sigfig' && !precisionOK && digits < precision && /[1-9]\d*0+$/.test(n)) {    // in cases like 2070, which could be to either 3 or 4 sig figs
            var trailingZeroes = n.match(/0*$/)[0].length;
            if(digits + trailingZeroes >= precision) {
                precisionOK = true;
            }
        }
        return precisionOK;
    },

    /**
     * Is n given as a scientific number to the desired precision?
     *
     * This looks only at the significand part.
     * A significand of the form `D.DD` is considered to be given to 2 decimal places, or three significant figures.
     *
     * Trailing zeros must be given: `1.2` is only considered to be given to 1 decimal place, and `1.20` is only considered to be given to 2 decimal places.
     *
     * @param {number|string} n
     * @param {string} precisionType - Either 'dp' or 'sigfig'.
     * @param {number} precision - Number of desired digits of precision.
     * @see Numbas.math.toGivenPrecision
     * @returns {boolean}
     */
    toGivenPrecisionScientific(n, precisionType, precision) {
        if(precisionType == 'none') {
            return true;
        }
        n += '';
        var m = math.re_scientificNumber.exec(n);
        if(!m) {
            return false;
        }
        return math.toGivenPrecision(m[1], 'dp', precision + (precisionType == 'sigfig' ? -1 : 0), true);
    },
    /** Is a within +/- tolerance of b?
     *
     * @param {number} a
     * @param {number} b
     * @param {number} tolerance
     * @returns {boolean}
     */
    withinTolerance: function(a, b, tolerance) {
        if(a.complex || b.complex) {
            a = a.complex ? a : math.complex(a,0);
            b = b.complex ? b : math.complex(b,0);
            return math.withinTolerance(a.re, b.re, tolerance) && math.withinTolerance(a.im, b.im, tolerance);
        }
        if(tolerance == 0) {
            return math.eq(a, b);
        } else {
            var upper = math.add(b, tolerance);
            var lower = math.sub(b, tolerance);
            return math.geq(a, lower) && math.leq(a, upper);
        }
    },
    /** Factorial, or Gamma(n+1) if n is not a positive integer.
     *
     * @param {number} n
     * @returns {number}
     */
    factorial: function(n) {
        if(Numbas.util.isInt(n) && n >= 0) {
            if(n <= 1) {
                return 1;
            }else{
                var j = 1;
                for(let i = 2;i <= n;i++) {
                    j *= i;
                }
                return j;
            }
        } else {    //gamma function extends factorial to non-ints and negative numbers
            return math.gamma(math.add(n, 1));
        }
    },
    /** Lanczos approximation to the gamma function.
     *
     * @param {number} n
     * @returns {number}
     */
    gamma: function(n) {
        var g = 7;
        var p = [0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7];
        var mul = math.mul;
        var div = math.div;
        var exp = math.exp;
        var neg = math.negate;
        var pow = math.pow;
        var sqrt = math.sqrt;
        var sin = math.sin;
        var add = math.add;
        var sub = math.sub;
        var pi = Math.PI;
        if((n.complex && n.re < 0.5) || (!n.complex && n < 0.5)) {
            return div(pi, mul(sin(mul(pi, n)), math.gamma(sub(1, n))));
        } else {
            n = sub(n, 1);            //n -= 1
            var x = p[0];
            for(let i = 1;i < g + 2;i++) {
                x = add(x, div(p[i], add(n, i)));    // x += p[i]/(n+i)
            }
            var t = add(n, add(g, 0.5));        // t = n+g+0.5
            return mul(sqrt(2 * pi), mul(pow(t, add(n, 0.5)), mul(exp(neg(t)), x)));    // return sqrt(2*pi)*t^(z+0.5)*exp(-t)*x
        }
    },
    /** Base-10 logarithm.
     *
     * @param {number} n
     * @returns {number}
     */
    log10: function(n) {
        return mul(math.log(n), Math.LOG10E);
    },
    /** Arbitrary base logarithm.
     *
     * @param {number} n
     * @param {number} b
     * @returns {number} log(n)/log(b)
     */
    log_base: function(n, b) {
        return div(math.log(n), math.log(b));
    },
    /** Convert from degrees to radians.
     *
     * @param {number} x
     * @returns {number}
     * @see Numbas.math.degrees
     */
    radians: function(x) {
        return mul(x, Math.PI / 180);
    },
    /** Convert from radians to degrees.
     *
     * @param {number} x
     * @returns {number}
     * @see Numbas.math.radians
     */
    degrees: function(x) {
        return mul(x, 180 / Math.PI);
    },
    /** Cosine.
     *
     * @param {number} x
     * @returns {number}
     */
    cos: function(x) {
        if(x.complex) {
            return math.complex(Math.cos(x.re) * math.cosh(x.im), -Math.sin(x.re) * math.sinh(x.im));
        } else {
            return Math.cos(x);
        }
    },
    /** Sine.
     *
     * @param {number} x
     * @returns {number}
     */
    sin: function(x) {
        if(x.complex) {
            return math.complex(Math.sin(x.re) * math.cosh(x.im), Math.cos(x.re) * math.sinh(x.im));
        } else {
            return Math.sin(x);
        }
    },
    /** Tangent.
     *
     * @param {number} x
     * @returns {number}
     */
    tan: function(x) {
        if(x.complex) {
            return div(math.sin(x), math.cos(x));
        } else {
            return Math.tan(x);
        }
    },
    /** Cosecant.
     *
     * @param {number} x
     * @returns {number}
     */
    cosec: function(x) {
        return div(1, math.sin(x));
    },
    /** Secant.
     *
     * @param {number} x
     * @returns {number}
     */
    sec: function(x) {
        return div(1, math.cos(x));
    },
    /** Cotangent.
     *
     * @param {number} x
     * @returns {number}
     */
    cot: function(x) {
        return div(1, math.tan(x));
    },
    /** Inverse sine.
     *
     * @param {number} x
     * @returns {number}
     */
    arcsin: function(x) {
        if(x.complex || math.abs(x) > 1) {
            var i = math.complex(0, 1);
            var ni = math.complex(0, -1);
            var ex = add(mul(x, i), math.sqrt(sub(1, mul(x, x)))); //ix+sqrt(1-x^2)
            return mul(ni, math.log(ex));
        } else {
            return Math.asin(x);
        }
    },
    /** Inverse cosine.
     *
     * @param {number} x
     * @returns {number}
     */
    arccos: function(x) {
        if(x.complex || math.abs(x) > 1) {
            var ni = math.complex(0, -1);
            var ex = add(x, math.sqrt(sub(mul(x, x), 1)));    //x+sqrt(x^2-1)
            var result = mul(ni, math.log(ex));
            if(math.re(result) < 0 || math.re(result) == 0 && math.im(result) < 0) {
                result = math.negate(result);
            }
            return result;
        } else {
            return Math.acos(x);
        }
    },
    /** Inverse tangent.
     *
     * @param {number} x
     * @returns {number}
     */
    arctan: function(x) {
        if(x.complex) {
            var i = math.complex(0, 1);
            var ex = div(add(i, x), sub(i, x));
            return mul(math.complex(0, 0.5), math.log(ex));
        } else {
            return Math.atan(x);
        }
    },
    /** Angle between x-axis and the line through the origin and `(x,y)`.
     *
     * @param {number} y
     * @param {number} x
     * @returns {number}
     */
    atan2: function(y, x) {
        if(y.complex) {
            y = y.re;
        }
        if(x.complex) {
            x = x.re;
        }
        return Math.atan2(y, x);
    },
    /** Hyperbolic sine.
     *
     * @param {number} x
     * @returns {number}
     */
    sinh: function(x) {
        if(x.complex) {
            return div(sub(math.exp(x), math.exp(math.negate(x))), 2);
        } else {
            return (Math.exp(x) - Math.exp(-x)) / 2;
        }
    },
    /** Hyperbolic cosine.
     *
     * @param {number} x
     * @returns {number}
     */
    cosh: function(x) {
        if(x.complex) {
            return div(add(math.exp(x), math.exp(math.negate(x))), 2);
        } else {
            return (Math.exp(x) + Math.exp(-x)) / 2
        }
    },
    /** Hyperbolic tangent.
     *
     * @param {number} x
     * @returns {number}
     */
    tanh: function(x) {
        return div(math.sinh(x), math.cosh(x));
    },
    /** Hyperbolic cosecant.
     *
     * @param {number} x
     * @returns {number}
     */
    cosech: function(x) {
        return div(1, math.sinh(x));
    },
    /** Hyperbolic secant.
     *
     * @param {number} x
     * @returns {number}
     */
    sech: function(x) {
        return div(1, math.cosh(x));
    },
    /** Hyperbolic tangent.
     *
     * @param {number} x
     * @returns {number}
     */
    coth: function(x) {
        return div(1, math.tanh(x));
    },
    /** Inverse hyperbolic sine.
     *
     * @param {number} x
     * @returns {number}
     */
    arcsinh: function(x) {
        if(x.complex) {
            return math.log(add(x, math.sqrt(add(mul(x, x), 1))));
        } else {
            return Math.log(x + Math.sqrt(x * x + 1));
        }
    },
    /** Inverse hyperbolic cosine.
     *
     * @param {number} x
     * @returns {number}
     */
    arccosh: function(x) {
        if(x.complex) {
            return math.log(add(x, math.sqrt(sub(mul(x, x), 1))));
        } else {
            return Math.log(x + Math.sqrt(x * x - 1));
        }
    },
    /** Inverse hyperbolic tangent.
     *
     * @param {number} x
     * @returns {number}
     */
    arctanh: function(x) {
        if(x.complex) {
            return div(math.log(div(add(1, x), sub(1, x))), 2);
        } else {
            return 0.5 * Math.log((1 + x) / (1 - x));
        }
    },
    /** Round up to the nearest integer. For complex numbers, real and imaginary parts are rounded independently.
     *
     * @param {number} x
     * @returns {number}
     * @see Numbas.math.round
     * @see Numbas.math.floor
     */
    ceil: function(x) {
        if(x.complex) {
            return math.complex(math.ceil(x.re), math.ceil(x.im));
        } else {
            return Math.ceil(x);
        }
    },
    /** Round down to the nearest integer. For complex numbers, real and imaginary parts are rounded independently.
     *
     * @param {number} x
     * @returns {number}
     * @see Numbas.math.ceil
     * @see Numbas.math.round
     */
    floor: function(x) {
        if(x.complex) {
            return math.complex(math.floor(x.re), math.floor(x.im));
        } else {
            return Math.floor(x);
        }
    },
    /** Round to the nearest integer; fractional part >= 0.5 rounds up. For complex numbers, real and imaginary parts are rounded independently.
     *
     * @param {number} x
     * @returns {number}
     * @see Numbas.math.ceil
     * @see Numbas.math.floor
     */
    round: function(x) {
        if(x.complex) {
            return math.complex(Math.round(x.re), Math.round(x.im));
        } else {
            return Math.round(x);
        }
    },
    /** Round to the nearest multiple of `a`;For complex numbers, real and imaginary parts are rounded independently.
     *
     * @param {number} x
     * @param {number} a
     * @returns {number}
     * @see Numbas.math.round
     */
    toNearest: function(x, a) {
        if(a.complex) {
            throw(new Numbas.Error('math.toNearest.complex'));
        }
        if(a == 0) {
            return NaN;
        }
        if(x.complex) {
            return math.complex(math.toNearest(x.re, a), math.toNearest(x.im, a));
        } else {
            return Math.round(x / a) * a;
        }
    },
    /**
     * Integer part of a number - chop off the fractional part. For complex numbers, real and imaginary parts are rounded independently.
     * When `p` is given, truncate to that many decimal places.
     *
     * @param {number} x
     * @param {number} [p=0]
     * @returns {number}
     * @see Numbas.math.fract
     */
    trunc: function(x, p) {
        if(x.complex) {
            return math.complex(math.trunc(x.re, p), math.trunc(x.im, p));
        }
        p = Math.pow(10, p || 0);
        if(x > 0) {
            return Math.floor(x * p) / p;
        } else {
            return Math.ceil(x * p) / p;
        }
    },
    /** Fractional part of a number - Take away the whole number part. For complex numbers, real and imaginary parts are rounded independently.
     *
     * @param {number} x
     * @returns {number}
     * @see Numbas.math.trunc
     */
    fract: function(x) {
        if(x.complex) {
            return math.complex(math.fract(x.re), math.fract(x.im));
        }
        return x - math.trunc(x);
    },
    /** Sign of a number - +1, 0, or -1. For complex numbers, gives the sign of the real and imaginary parts separately.
     *
     * @param {number} x
     * @returns {number}
     */
    sign: function(x) {
        if(x.complex) {
            return math.complex(math.sign(x.re), math.sign(x.im));
        }
        if(x == 0) {
            return 0;
        }else if (x > 0) {
            return 1;
        }else {
            return -1;
        }
    },
    /** Get a random real number between `min` and `max` (inclusive).
     *
     * @param {number} min
     * @param {number} max
     * @returns {number}
     * @see Numbas.math.random
     * @see Numbas.math.choose
     */
    randomrange: function(min, max) {
        return Math.random() * (max - min) + min;
    },
    /** Get a random number in the specified range.
     *
     * Returns a random choice from `min` to `max` at `step`-sized intervals
     *
     * If all the values in the range are appended to the list, eg `[min,max,step,v1,v2,v3,...]`, just pick randomly from the values.
     *
     * @param {range} range - `[min,max,step]`
     * @returns {number}
     * @see Numbas.math.randomrange
     */
    random: function(range) {
        if(range[2] == 0) {
            return math.randomrange(range[0], range[1]);
        } else {
            var num_steps = math.rangeSize(range);
            var n = Math.floor(math.randomrange(0, num_steps));
            return range[0] + n * range[2];
        }
    },
    /** Remove all the values in the list `exclude` from the list `range`.
     *
     * @param {Array.<number>} range
     * @param {Array.<number>} exclude
     * @returns {Array.<number>}
     */
    except: function(range, exclude) {
        range = range.filter(function(r) {
            for(let i = 0;i < exclude.length;i++) {
                if(math.eq(r, exclude[i])) {
                    return false;
                }
            }
            return true;
        });
        return range;
    },
    /** Choose one item from an array, at random.
     *
     * @param {Array} selection
     * @returns {*}
     * @throws {Numbas.Error} "math.choose.empty selection" if `selection` has length 0.
     * @see Numbas.math.randomrange
     */
    choose: function(selection) {
        if(selection.length == 0) {
            throw(new Numbas.Error('math.choose.empty selection'));
        }
        var n = Math.floor(math.randomrange(0, selection.length));
        return selection[n];
    },
    /** Choose at random from a weighted list of items.
     *
     * @param {Array} list - A list of pairs of the form `[item, probability]`, where `probability` is a number.
     * @returns {*}
     * @throws {Numbas.Error} "math.choose.empty selection" if `selection` has length 0.
     */
    weighted_random: function(list) {
        var total = 0;
        for(var i = 0; i < list.length; i++) {
            const p = list[i][1];
            total += p > 0 ? p : 0;
        }
        if(total == 0) {
            throw(new Numbas.Error('math.choose.empty selection'));
        }
        var target = Math.random() * total;
        var acc = 0;
        for(let i = 0; i < list.length; i++) {
            const p = list[i][1];
            acc += p > 0 ? p : 0;
            if(acc >= target) {
                return list[i][0];
            }
        }
        return undefined;
    },
    /* Product of the numbers in the range `[a..b]`, i.e. $frac{a!}{b!}$.
     *
     * from http://dreaminginjavascript.wordpress.com/2008/11/08/combinations-and-permutations-in-javascript/
     *
     * (public domain)
     * @param {number} a
     * @param {number} b
     * @returns {number}
     */
    productRange: function(a, b) {
        if(a > b) {
            return 1;
        }
        var product = a;
        var i = a;
        while (i++ < b) {
            product *= i;
        }
        return product;
    },
    /** `nCk` - number of ways of picking `k` unordered elements from `n`.
     *
     * @param {number} n
     * @param {number} k
     * @returns {number}
     * @throws {Numbas.Error} "math.combinations.complex" if either of `n` or `k` is complex.
     */
    combinations: function(n, k) {
        if(n.complex || k.complex) {
            throw(new Numbas.Error('math.combinations.complex'));
        }
        if(n < 0) {
            throw(new Numbas.Error('math.combinations.n less than zero'));
        }
        if(k < 0) {
            throw(new Numbas.Error('math.combinations.k less than zero'));
        }
        if(n < k) {
            throw(new Numbas.Error('math.combinations.n less than k'));
        }
        k = Math.max(k, n - k);
        return math.productRange(k + 1, n) / math.productRange(1, n - k);
    },
    /** `nPk` - number of ways of picking `k` ordered elements from `n`.
     *
     * @param {number} n
     * @param {number} k
     * @returns {number}
     * @throws {Numbas.Error} "math.combinations.complex" if either of `n` or `k` is complex.
     */
    permutations: function(n, k) {
        if(n.complex || k.complex) {
            throw(new Numbas.Error('math.permutations.complex'));
        }
        if(n < 0) {
            throw(new Numbas.Error('math.permutations.n less than zero'));
        }
        if(k < 0) {
            throw(new Numbas.Error('math.permutations.k less than zero'));
        }
        if(n < k) {
            throw(new Numbas.Error('math.permutations.n less than k'));
        }
        return math.productRange(n - k + 1, n);
    },
    /** Does `a` divide `b`? If either of `a` or `b` is not an integer, return `false`.
     *
     * @param {number} a
     * @param {number} b
     * @returns {boolean}
     */
    divides: function(a, b) {
        if(a.complex || b.complex || !Numbas.util.isInt(a) || !Numbas.util.isInt(b)) {
            return false;
        }
        return (b % a) == 0;
    },
    /** Greatest common factor (GCF), or greatest common divisor (GCD), of `a` and `b`.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     * @throws {Numbas.Error} "math.gcf.complex" if either of `a` or `b` is complex.
     */
    gcd: function(a, b) {
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.gcf.complex'));
        }
        if(Math.floor(a) != a || Math.floor(b) != b || Math.abs(a) == Infinity || Math.abs(b) == Infinity) {
            return 1;
        }
        a = Math.floor(Math.abs(a));
        b = Math.floor(Math.abs(b));
        var c = 0;
        if(a < b) {
            c = a; a = b; b = c;
        }
        if(b == 0) {
            return a;
        }
        while(a % b != 0) {
            c = b;
            b = a % b;
            a = c;
        }
        return b;
    },
    /** Are `a` and `b` coprime? If either of `a` or `b` is not an integer, return `false`.
     * Equivalent to `gcd(a,b) = 1`.
     *
     * @param {number} a
     * @param {number} b
     * @see Numbas.math.gcd
     * @returns {boolean}
     */
    coprime: function(a, b) {
        if(a.complex || b.complex || !Numbas.util.isInt(a) || !Numbas.util.isInt(b)) {
            return true;
        }
        return math.gcd(a, b) == 1;
    },
    /** Lowest common multiple (LCM) of `a` and `b`.
     *
     * @param {number} a
     * @param {number} b
     * @returns {number}
     * @throws {Numbas.Error} "math.gcf.complex" if either of `a` or `b` is complex.
     */
    lcm: function(a, b) {
        if(arguments.length == 0) {
            return 1;
        } else if(arguments.length == 1) {
            return a;
        }
        if(a.complex || b.complex) {
            throw(new Numbas.Error('math.lcm.complex'));
        }
        if(arguments.length > 2) {
            a = Math.floor(Math.abs(a));
            for(let i = 1;i < arguments.length;i++) {
                if(arguments[i].complex) {
                    throw(new Numbas.Error('math.lcm.complex'));
                }
                b = Math.floor(Math.abs(arguments[i]));
                a = a * b / math.gcf(a, b);
            }
            return a;
        }
        a = Math.floor(Math.abs(a));
        b = Math.floor(Math.abs(b));
        var c = math.gcf(a, b);
        return a * b / c;
    },
    /** Write the range of integers `[a..b]` as an array of the form `[min,max,step]`, for use with {@link Numbas.math.random}. If either number is complex, only the real part is used.
     *
     * @param {number} a
     * @param {number} b
     * @returns {range}
     * @see Numbas.math.random
     */
    defineRange: function(a, b) {
        if(a.complex) {
            a = a.re;
        }
        if(b.complex) {
            b = b.re;
        }
        return [a, b, 1];
    },
    /** Change the step size of a range created with {@link Numbas.math.defineRange}.
     *
     * @param {range} range
     * @param {number} step
     * @returns {range}
     */
    rangeSteps: function(range, step) {
        if(step.complex) {
            step = step.re;
        }
        return [range[0], range[1], step];
    },

    /** Convert a range to a list of Decimal values - enumerate all the elements of the range.
     *
     * @param {range} range
     * @returns {Decimal[]}
     */
    rangeToDecimalList: function(range) {
        const start = new Decimal(range[0]);
        const end = new Decimal(range[1]);
        const step_size = new Decimal(range[2]);
        const out = [];
        if(step_size.isZero()) {
            throw(new Numbas.Error('math.rangeToList.zero step size'));
        }
        if(end.minus(start).times(step_size).isNegative()) {
            return [];
        }
        if(start.equals(end)) {
            return [start];
        }
        let n = 0;
        let t = start;
        while(start.lessThan(end) ? t.lessThanOrEqualTo(end) : t.greaterThanOrEqualTo(end)) {
            out.push(t);
            n += 1;
            t = start.plus(step_size.times(n));
        }
        return out;
    },

    /** Convert a range to a list - enumerate all the elements of the range.
     *
     * @param {range} range
     * @returns {number[]}
     */
    rangeToList: function(range) {
        return math.rangeToDecimalList(range).map((x) => x.toNumber());
    },
    /** Calculate the number of elements in a range.
     *
     * @param {range} range
     * @returns {number}
     */
    rangeSize: function(range) {
        var diff = range[1] - range[0];
        var num_steps = Math.floor(diff / range[2]) + 1;
        num_steps += (math.isclose(range[0] + num_steps * range[2], range[1]) ? 1 : 0);
        return num_steps;
    },
    /** Get a rational approximation to a real number by the continued fractions method.
     *
     * If `accuracy` is given, the returned answer will be within `Math.exp(-accuracy)` of the original number.
     *
     * Based on frap.c by David Eppstein - https://www.ics.uci.edu/~eppstein/numth/frap.c.
     *
     * @param {number} n
     * @param {number} [accuracy]
     * @returns {Array.<number>} - [numerator,denominator]
     */
    rationalApproximation: function(n, accuracy) {
        /** Find a rational approximation to `t` with maximum denominator `limit`.
         *
         * @param {number} limit
         * @param {number} t
         * @returns {Array.<number>} `[error,numerator,denominator]`
         */
        function rat_to_limit(limit, t) {
            limit = Math.max(limit, 1);
            if(t == 0) {
                return [0, t, 1, 0];
            }
            var m00 = 1;
            var m01 = 0;
            var m10 = 0;
            var m11 = 1;

            var x = t;
            var ai = Math.floor(x);
            while((m10 * ai + m11) <= limit) {
                var tmp = m00 * ai + m01;
                m01 = m00;
                m00 = tmp;
                tmp = m10 * ai + m11;
                m11 = m10;
                m10 = tmp;
                if(x == ai) {
                    break;
                }
                x = 1 / (x - ai);
                ai = Math.floor(x);
            }

            var n1 = m00;
            var d1 = m10;
            var err1 = (t - n1 / d1);

            ai = Math.floor((limit - m11) / m10);
            var n2 = m00 * ai + m01;
            var d2 = m10 * ai + m11;
            var err2 = (t - n2 / d2);
            if(Math.abs(err1) <= Math.abs(err2)) {
                return [err1, n1, d1];
            } else {
                return [err2, n2, d2];
            }
        }

        if(accuracy == undefined) {
            accuracy = 15;
        }
        var err_in = Math.exp(-accuracy);
        var limit = 100000000000;
        var l_curr = 1;
        var res = rat_to_limit(l_curr, n);
        while(Math.abs(res[0]) > err_in && l_curr < limit) {
            l_curr *= 10;
            res = rat_to_limit(l_curr, n);
        }
        return [res[1], res[2]];
    },

    /** The first 1000 primes. */
    primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987, 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907, 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057, 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, 5939, 5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133, 6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301, 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473, 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997, 7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 72077211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297, 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561, 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, 7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919],

    /** Divisors of `n`. When `n = 210`, this returns the divisors `[1, 2, 3, 5, 6, 7, 10, 14, 15, 21, 30, 35, 42, 70, 105, 210]`.
     *
     * @param {number} n
     * @returns {Array.<number>} - Divisors of n.
     */
    divisors: function(n) {
        n = Math.abs(n);
        if(n < 1) {
            return [];
        }
        var divisor_arr = [1];
        var exponents = math.factorise(n);
        for(var i = 0; i < exponents.length; i++) {
            var divisor_arr_copy = [];
            for(var j = 0; j <= exponents[i]; j++) {
                divisor_arr_copy = divisor_arr_copy.concat(divisor_arr.map((number) => number * math.primes[i] ** j));
            }
            divisor_arr = divisor_arr_copy;
        }
        return divisor_arr;
    },


    /** Proper divisors of `n`: the divisors of `n`, excluding `n` itself. When `n = 210`, this returns the divisors `[2, 3, 5, 6, 7, 10, 14, 15, 21, 30, 35, 42, 70, 105]`.
     *
     * @param {number} n
     * @returns {Array.<number>} - Proper divisors of n.
     */
    proper_divisors: function(n) {
        var divisors = math.divisors(n);
        return divisors.slice(0, divisors.length - 1);
    },

    /** Factorise `n`. When `n=2^(a1)*3^(a2)*5^(a3)*...`, this returns the powers `[a1,a2,a3,...]`.
     *
     * @param {number} n
     * @returns {Array.<number>} - Exponents of the prime factors of n.
     */
    factorise: function(n) {
        n = Math.floor(Math.abs(n));
        if(n <= 0) {
            return [];
        }
        var factors = [];
        for(let i = 0;i < math.primes.length;i++) {
            var acc = 0;
            var p = math.primes[i];
            while(n % p == 0) {
                acc += 1;
                n /= p;
            }
            factors.push(acc);
            if(n == 1) {
                break;
            }
        }
        return factors;
    },

    /**
     * The largest perfect square factor of the given number.
     *
     * When the prime factorisation of `n` is `p_1^x_1 * p_2^x_2 ... p_k^x_k`, the largest perfect square factor is `p_1^(2*floor(x_1/2)) * ... p_k^(2*floor(x_k)/2)`.
     *
     * @param {number} n
     * @returns {number}
     */
    largest_square_factor: function(n) {
        n = Math.floor(Math.abs(n));
        var factors = math.factorise(n).map(function(f) {
            return f - f % 2;
        });
        var t = 1;
        factors.forEach(function(f, i) {
            t *= Math.pow(math.primes[i], f);
        });
        return t;
    },

    /** Sum the elements in the given list.
     *
     * @param {Array.<number>} list
     * @returns {number}
     */
    sum: function(list) {
        var total = 0;
        var l = list.length;
        if(l == 0) {
            return 0;
        }
        for(let i = 0;i < l;i++) {
            total = math.add(total, list[i]);
        }
        return total;
    },
    /** Multiplies the elements in the given list.
     *
     * @param {Array.<number>} list
     * @returns {number}
     */
    prod: function(list) {
        var product = 1;
        for(var i = 0; i < list.length; i++) {
            product = math.mul(product, list[i]);
        }
        return product;
    }
};
math.gcf = math.gcd;

var add = math.add;
var sub = math.sub;
var mul = math.mul;
var div = math.div;
var eq = math.eq;
var negate = math.negate;


/** A rational number.
 *
 * @class
 * @param {number} numerator
 * @param {number} denominator
 *
 * @property {number} numerator - The numerator.
 * @property {number} denominator - The denominator.
 * @memberof Numbas.math
 */
var Fraction = math.Fraction = function(numerator, denominator) {
    if(denominator === undefined) {
        denominator = 1;
    }
    if(denominator < 0) {
        numerator = -numerator;
        denominator = -denominator;
    }
    while(numerator % 1 != 0 || denominator % 1 != 0) {
        numerator *= 2;
        denominator *= 2;
    }
    this.numerator = Math.round(numerator);
    this.denominator = Math.round(denominator);
}
Fraction.prototype = {
    toString: function() {
        if(this.denominator == 1) {
            return this.numerator + '';
        } else {
            return this.numerator + '/' + this.denominator;
        }
    },
    toFloat: function() {
        return this.numerator / this.denominator;
    },
    toDecimal: function() {
        return (new Decimal(this.numerator)).div(new Decimal(this.denominator));
    },
    reduce: function() {
        if(this.denominator == 0) {
            return;
        }
        if(this.denominator < 0) {
            this.numerator = -this.numerator;
            this.denominator = -this.denominator;
        }
        var g = math.gcd(this.numerator, this.denominator);
        this.numerator /= g;
        this.denominator /= g;
    },

    /** Returns a copy of this fraction reduced to lowest terms.
     *
     * @returns {Numbas.math.Fraction}
     */
    reduced: function() {
        var f = new Fraction(this.numerator, this.denominator);
        f.reduce();
        return f;
    },
    add: function(b) {
        if(typeof(b) === 'number') {
            b = Fraction.fromFloat(b);
        }
        var numerator, denominator;
        if(this.denominator == b.denominator) {
            numerator = this.numerator + b.numerator;
            denominator = this.denominator;
        } else {
            numerator = this.numerator * b.denominator + b.numerator * this.denominator;
            denominator = this.denominator * b.denominator;
        }
        var f = new Fraction(numerator, denominator);
        f.reduce();
        return f;
    },
    subtract: function(b) {
        if(typeof(b) === 'number') {
            b = Fraction.fromFloat(b);
        }
        var numerator, denominator;
        if(this.denominator == b.denominator) {
            numerator = this.numerator - b.numerator;
            denominator = this.denominator;
        } else {
            numerator = this.numerator * b.denominator - b.numerator * this.denominator;
            denominator = this.denominator * b.denominator;
        }
        var f = new Fraction(numerator, denominator);
        f.reduce();
        return f;
    },
    multiply: function(b) {
        if(typeof(b) === 'number') {
            b = Fraction.fromFloat(b);
        }
        var numerator = this.numerator * b.numerator;
        var denominator = this.denominator * b.denominator;
        var f = new Fraction(numerator, denominator);
        f.reduce();
        return f;
    },
    divide: function(b) {
        if(typeof(b) === 'number') {
            b = Fraction.fromFloat(b);
        }
        var numerator = this.numerator * b.denominator;
        var denominator = this.denominator * b.numerator;
        var f = new Fraction(numerator, denominator);
        f.reduce();
        return f;
    },
    reciprocal: function() {
        return new Fraction(this.denominator, this.numerator);
    },
    negate: function() {
        return new Fraction(-this.numerator, this.denominator);
    },
    equals: function(b) {
        return this.subtract(b).numerator == 0;
    },
    lt: function(b) {
        return this.subtract(b).numerator < 0;
    },
    gt: function(b) {
        return this.subtract(b).numerator > 0;
    },
    leq: function(b) {
        return this.subtract(b).numerator <= 0;
    },
    geq: function(b) {
        return this.subtract(b).numerator >= 0;
    },
    pow: function(n) {
        var numerator = n >= 0 ? this.numerator : this.denominator;
        var denominator = n >= 0 ? this.denominator : this.numerator;
        n = Math.abs(n);
        return new Fraction(Math.pow(numerator, n), Math.pow(denominator, n));
    },
    trunc: function() {
        var sign = math.sign(this.numerator);
        var n = Math.abs(this.numerator);
        var d = this.denominator;
        return sign * (n - n % d) / d;
    },
    floor: function() {
        var t = this.trunc();
        return (this.numerator < 0) && (this.numerator % this.denominator != 0) ? t - 1 : t;
    },
    ceil: function() {
        var t = this.trunc();
        return this.numerator > 0 && (this.numerator % this.denominator != 0) ? t + 1 : t;
    },
    fract: function() {
        return new Fraction(this.numerator % this.denominator, this.denominator);
    },
    is_zero: function() {
        return this.numerator == 0;
    },
    is_one: function() {
        return this.numerator == this.denominator;
    }
}
Fraction.zero = new Fraction(0, 1);
Fraction.one = new Fraction(1, 1);
Fraction.fromFloat = function(n) {
    var approx = math.rationalApproximation(n);
    return new Fraction(approx[0], approx[1]);
}
Fraction.fromDecimal = function(n, accuracy) {
    accuracy = accuracy === undefined ? 1e15 : accuracy;
    var approx = n.toFraction(accuracy);
    return new Fraction(approx[0].toNumber(), approx[1].toNumber());
}
Fraction.common_denominator = function(fractions) {
    var d = 1;
    fractions.forEach(function(f) {
        d = math.lcm(d, f.denominator);
    });
    return fractions.map(function(f) {
        var m = d / f.denominator;
        return new Fraction(f.numerator * m, d);
    });
}
Fraction.min = function() {
    if(arguments.length == 0) {
        return;
    }
    var commons = Fraction.common_denominator(Array.prototype.slice.apply(arguments));
    var best = 0;
    for(let i = 1;i < commons.length;i++) {
        if(commons[i].numerator < commons[best].numerator) {
            best = i;
        }
    }
    return arguments[best];
}
Fraction.max = function() {
    if(arguments.length == 0) {
        return;
    }
    var commons = Fraction.common_denominator(Array.prototype.slice.apply(arguments));
    var best = 0;
    for(let i = 1;i < commons.length;i++) {
        if(commons[i].numerator > commons[best].numerator) {
            best = i;
        }
    }
    return arguments[best];
}


/** Coerce the given number to a {@link Numbas.math.ComplexDecimal} value.
 *
 * @param {number|Decimal|Numbas.math.ComplexDecimal} n
 * @returns {Numbas.math.ComplexDecimal}
 */
var ensure_decimal = math.ensure_decimal = function(n) {
    if(n instanceof ComplexDecimal) {
        return n;
    } else if(n instanceof Decimal) {
        return new ComplexDecimal(n);
    } else if(n.complex) {
        return new ComplexDecimal(new Decimal(n.re), new Decimal(n.im));
    }
    return new ComplexDecimal(new Decimal(n));
}

/**
 * Is the given argument a `ComplexDecimal` value?
 *
 * @param {object} n
 * @returns {boolean}
 */
math.isComplexDecimal = function(n) {
    return n instanceof ComplexDecimal;
}

/** A complex number with components stored as `Decimal` objects.
 *
 * @param {Decimal} re
 * @param {Decimal} [im]
 * @property {Decimal} re - The real part.
 * @property {Decimal} im - The imaginary part.
 * @class
 * @memberof Numbas.math
 */
var ComplexDecimal = math.ComplexDecimal = function(re, im) {
    this.re = re;
    if(im === undefined) {
        im = new Decimal(0);
    }
    this.im = im;
}
ComplexDecimal.prototype = {
    toString: function() {
        var re = this.re.toString();
        if(this.isReal()) {
            return re;
        } else {
            var symbol = this.im.isNegative() ? '-' : '+';
            var im = this.im.absoluteValue().toString();
            return re + ' ' + symbol + ' ' + im + 'i';
        }
    },

    toNumber: function() {
        return this.re.toNumber();
    },

    toComplexNumber: function() {
        if(this.isReal()) {
            return this.re.toNumber();
        } else {
            return {complex: true, re: this.re.toNumber(), im: this.im.toNumber()};
        }
    },

    isReal: function() {
        return this.im.isZero();
    },

    equals: function(b) {
        b = ensure_decimal(b);
        return this.re.equals(b.re) && this.im.equals(b.im);
    },

    lessThan: function(b) {
        b = ensure_decimal(b);
        if(!(this.isReal() && b.isReal())) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return this.re.lessThan(b.re);
    },

    lessThanOrEqualTo: function(b) {
        b = ensure_decimal(b);
        if(!(this.isReal() && b.isReal())) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return this.re.lessThanOrEqualTo(b.re);
    },

    greaterThan: function(b) {
        b = ensure_decimal(b);
        if(!(this.isReal() && b.isReal())) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return this.re.greaterThan(b.re);
    },

    greaterThanOrEqualTo: function(b) {
        b = ensure_decimal(b);
        if(!(this.isReal() && b.isReal())) {
            throw(new Numbas.Error('math.order complex numbers'));
        }
        return this.re.greaterThanOrEqualTo(b.re);
    },

    negated: function() {
        return new ComplexDecimal(this.re.negated(), this.im.negated());
    },

    conjugate: function() {
        return new ComplexDecimal(this.re, this.im.negated());
    },

    plus: function(b) {
        b = ensure_decimal(b);
        return new ComplexDecimal(this.re.plus(b.re), this.im.plus(b.im));
    },

    minus: function(b) {
        b = ensure_decimal(b);
        return new ComplexDecimal(this.re.minus(b.re), this.im.minus(b.im));
    },
    times: function(b) {
        b = ensure_decimal(b);
        var re = this.re.times(b.re).minus(this.im.times(b.im));
        var im = this.re.times(b.im).plus(this.im.times(b.re));
        return new ComplexDecimal(re, im);
    },

    dividedBy: function(b) {
        b = ensure_decimal(b);
        if(b.isZero()) {
            return new ComplexDecimal(new Decimal(NaN), new Decimal(0));
        }
        var q = b.re.times(b.re).plus(b.im.times(b.im));
        var re = this.re.times(b.re).plus(this.im.times(b.im)).dividedBy(q);
        var im = this.im.times(b.re).minus(this.re.times(b.im)).dividedBy(q);
        return new ComplexDecimal(re, im);
    },

    pow: function(b) {
        b = ensure_decimal(b);
        if(this.isReal() && b.isReal()) {
            if(this.re.greaterThanOrEqualTo(0) || b.re.isInt()) {
                return new ComplexDecimal(this.re.pow(b.re), new Decimal(0));
            } else if(b.re.times(2).isInt()) {
                return new ComplexDecimal(new Decimal(0), this.re.negated().pow(b.re));
            }
        }
        var ss = this.re.times(this.re).plus(this.im.times(this.im));
        var arg1 = Decimal.atan2(this.im, this.re);
        var mag = ss.pow(b.re.dividedBy(2)).times(Decimal.exp(b.im.times(arg1).negated()));
        var arg = b.re.times(arg1).plus(b.im.times(Decimal.ln(ss)).dividedBy(2));
        return new ComplexDecimal(mag.times(arg.cos()), mag.times(arg.sin()));
    },

    squareRoot: function() {
        if(!this.isReal()) {
            var r = this.re.times(this.re).plus(this.im.times(this.im)).squareRoot();
            var re = r.plus(this.re).dividedBy(2).squareRoot();
            var im = (new Decimal(this.im.lessThan(0) ? -1 : 1)).times(r.minus(this.re).dividedBy(2).squareRoot());
            return new ComplexDecimal(re, im);
        }
        if(this.re.lessThan(0)) {
            return new ComplexDecimal(new Decimal(0), this.re.absoluteValue().squareRoot());
        } else {
            return new ComplexDecimal(this.re.squareRoot());
        }
    },

    reciprocal: function() {
        var denominator = this.re.pow(2).add(this.im.pow(2));
        return new ComplexDecimal(this.re.dividedBy(denominator), this.im.dividedBy(denominator));
    },

    absoluteValue: function() {
        return new ComplexDecimal(this.re.times(this.re).plus(this.im.times(this.im)).squareRoot());
    },

    argument: function() {
        return new ComplexDecimal(Decimal.atan2(this.im, this.re));
    },

    ln: function() {
        return new ComplexDecimal(this.absoluteValue().re.ln(), this.argument().re);
    },

    exp: function() {
        var r = this.re.exp();
        return new ComplexDecimal(r.times(Decimal.cos(this.im)), r.times(Decimal.sin(this.im)));
    },

    isInt: function() {
        return this.re.isInt() && this.im.isInt();
    },

    isNaN: function() {
        return this.re.isNaN() || this.im.isNaN();
    },

    isZero: function() {
        return this.re.isZero() && this.im.isZero();
    },

    isOne: function() {
        return this.im.isZero() && this.re.equals(new Decimal(1));
    },

    round: function() {
        return new ComplexDecimal(this.re.round(), this.im.round());
    },

    toDecimalPlaces: function(dp) {
        return new ComplexDecimal(this.re.toDecimalPlaces(dp), this.im.toDecimalPlaces(dp));
    },

    toFixed: function(dp) {
        var re = this.re.toFixed(dp);
        if(this.isReal()) {
            return re;
        } else {
            var symbol = this.im.isNegative() ? '-' : '+';
            var im = this.im.absoluteValue().toFixed(dp);
            return re + ' ' + symbol + ' ' + im + 'i';
        }
    },

    toNearest: function(n) {
        return new ComplexDecimal(this.re.toNearest(n), this.im.toNearest(n));
    },

    toPrecision: function(sf) {
        var re = this.re.toPrecision(sf);
        if(this.isReal()) {
            return re;
        } else {
            var symbol = this.im.isNegative() ? '-' : '+';
            var im = this.im.absoluteValue().toPrecision(sf);
            return re + ' ' + symbol + ' ' + im + 'i';
        }
    },

    toSignificantDigits: function(sf) {
        return new ComplexDecimal(this.re.toSignificantDigits(sf), this.im.toSignificantDigits(sf));
    }
}

ComplexDecimal.min = function(a, b) {
    if(!(a.isReal() && b.isReal())) {
        throw(new Numbas.Error('math.order complex numbers'));
    }
    return new ComplexDecimal(Decimal.min(a.re, b.re));
}
ComplexDecimal.max = function(a, b) {
    if(!(a.isReal() && b.isReal())) {
        throw(new Numbas.Error('math.order complex numbers'));
    }
    return new ComplexDecimal(Decimal.max(a.re, b.re));
}



/** A list of a vector's components.
 *
 * @typedef vector
 * @type {Array.<number>}
 */
/** Vector operations.
 *
 * These operations are very lax about the dimensions of vectors - they stick zeros in when pairs of vectors don't line up exactly.
 *
 * @namespace Numbas.vectormath
 */
var vectormath = Numbas.vectormath = {
    /** Negate a vector - negate each of its components.
     *
     * @param {vector} v
     * @returns {vector}
     */
    negate: function(v) {
        return v.map(function(x) {
            return negate(x);
        });
    },
    /** Add two vectors.
     *
     * @param {vector} a
     * @param {vector} b
     * @returns {vector}
     */
    add: function(a, b) {
        if(b.length > a.length) {
            var c = b;
            b = a;
            a = c;
        }
        return a.map(function(x, i) {
            return add(x, b[i] || 0)
        });
    },
    /** Subtract one vector from another.
     *
     * @param {vector} a
     * @param {vector} b
     * @returns {vector}
     */
    sub: function(a, b) {
        if(b.length > a.length) {
            return b.map(function(x, i) {
                return sub(a[i] || 0, x)
            });
        } else {
            return a.map(function(x, i) {
                return sub(x, b[i] || 0)
            });
        }
    },
    /** Multiply by a scalar.
     *
     * @param {number} k
     * @param {vector} v
     * @returns {vector}
     */
    mul: function(k, v) {
        return v.map(function(x) {
            return mul(k, x)
        });
    },
    /** Divide by a scalar.
     *
     * @param {vector} v
     * @param {number} k
     * @returns {vector}
     */
    div: function(v, k) {
        return v.map(function(x) {
            return div(x, k);
        });
    },
    /** Vector dot product - each argument can be a vector, or a matrix with one row or one column, which is converted to a vector.
     *
     * @param {vector|matrix} a
     * @param {vector|matrix} b
     * @returns {number}
     * @throws {Numbas.Error} "vectormaths.dot.matrix too big" if either of `a` or `b` is bigger than `1xN` or `Nx1`.
     */
    dot: function(a, b) {
        //check if A is a matrix object. If it's the right shape, we can use it anyway
        if('rows' in a) {
            if(a.rows == 1) {
                a = a[0];
            } else if(a.columns == 1) {
                a = a.map(function(x) {
                    return x[0]
                });
            } else {
                throw(new Numbas.Error('vectormath.dot.matrix too big'));
            }
        }
        //Same check for B
        if('rows' in b) {
            if(b.rows == 1) {
                b = b[0];
            } else if(b.columns == 1) {
                b = b.map(function(x) {
                    return x[0]
                });
            } else {
                throw(new Numbas.Error('vectormath.dot.matrix too big'));
            }
        }
        if(b.length > a.length) {
            var c = b;
            b = a;
            a = c;
        }
        return a.reduce(function(s, x, i) {
            return add(s, mul(x, b[i] || 0))
        }, 0);
    },
    /** Vector cross product - each argument can be a vector, or a matrix with one row, which is converted to a vector.
     *
     * @param {vector|matrix} a
     * @param {vector|matrix} b
     * @returns {vector}
     *
     * @throws {Numbas.Error} "vectormaths.cross.matrix too big" if either of `a` or `b` is bigger than `1xN` or `Nx1`.
     * @throws {Numbas.Error} "vectormath.cross.not 3d" if either of the vectors is not 3D.
     */
    cross: function(a, b) {
        //check if A is a matrix object. If it's the right shape, we can use it anyway
        if('rows' in a) {
            if(a.rows == 1) {
                a = a[0];
            } else if(a.columns == 1) {
                a = a.map(function(x) {
                    return x[0]
                });
            } else {
                throw(new Numbas.Error('vectormath.cross.matrix too big'));
            }
        }
        //Same check for B
        if('rows' in b) {
            if(b.rows == 1) {
                b = b[0];
            } else if(b.columns == 1) {
                b = b.map(function(x) {
                    return x[0]
                });
            } else {
                throw(new Numbas.Error('vectormath.cross.matrix too big'));
            }
        }
        if(a.length != 3 || b.length != 3) {
            throw(new Numbas.Error('vectormath.cross.not 3d'));
        }
        return [
                sub(mul(a[1], b[2]), mul(a[2], b[1])),
                sub(mul(a[2], b[0]), mul(a[0], b[2])),
                sub(mul(a[0], b[1]), mul(a[1], b[0]))
                ];
    },
    /** Length of a vector, squared.
     *
     * @param {vector} a
     * @returns {number}
     */
    abs_squared: function(a) {
        return a.reduce(function(s, x) {
            return s + mul(x, x);
        }, 0);
    },
    /** Length of a vector.
     *
     * @param {vector} a
     * @returns {number}
     */
    abs: function(a) {
        return Math.sqrt(a.reduce(function(s, x) {
            return s + mul(x, x);
        }, 0));
    },
    /** Angle between vectors a and b, in radians, or 0 if either vector has length 0.
     *
     * @param {vector} a
     * @param {vector} b
     * @returns {number}
     */
    angle: function(a, b) {
        var dot = vectormath.dot(a, b);
        var da = vectormath.abs_squared(a);
        var db = vectormath.abs_squared(b);
        if(da * db == 0) {
            return 0;
        }
        var d = Math.sqrt(da * db);
        return math.arccos(dot / d);
    },
    /** Are two vectors equal? True if each pair of corresponding components is equal.
     *
     * @param {vector} a
     * @param {vector} b
     * @returns {boolean}
     */
    eq: function(a, b) {
        if(b.length > a.length) {
            var c = b;
            b = a;
            a = c;
        }
        return a.reduce(function(s, x, i) {
            return s && eq(x, b[i] || 0)
        }, true);
    },
    /** Are two vectors unequal?
     *
     * @param {vector} a
     * @param {vector} b
     * @returns {boolean}
     * @see Numbas.vectormath.eq
     */
    neq: function(a, b) {
        return !vectormath.eq(a, b);
    },
    /** Multiply a vector on the left by a matrix.
     *
     * @param {matrix} m
     * @param {vector} v
     * @returns {vector}
     */
    matrixmul: function(m, v) {
        return m.map(function(row) {
            return row.reduce(function(s, x, i) {
                return add(s, mul(x, v[i] || 0));
            }, 0);
        });
    },
    /** Multiply a vector on the right by a matrix.
     * The vector is considered as a column vector.
     *
     * @param {vector} v
     * @param {matrix} m
     * @returns {vector}
     */
    vectormatrixmul: function(v, m) {
        var out = [];
        for(let i = 0;i < m.columns;i++) {
            out.push(v.reduce(function(s, x, j) {
                var c = j < m.rows ? (m[j][i] || 0) : 0; return add(s, mul(x, c));
            }, 0));
        }
        return out;
    },
    /** Apply given function to each element.
     *
     * @param {vector} v
     * @param {Function} fn
     * @returns {vector}
     */
    map: function(v, fn) {
        return v.map(fn);
    },
    /** Round each element to given number of decimal places.
     *
     * @param {vector} v
     * @param {number} dp - Number of decimal places.
     * @returns {vector}
     */
    precround: function(v, dp) {
        return vectormath.map(v, function(n) {
            return math.precround(n, dp);
        });
    },
    /** Round each element to given number of significant figures.
     *
     * @param {vector} v
     * @param {number} sf - Number of decimal places.
     * @returns {vector}
     */
    siground: function(v, sf) {
        return vectormath.map(v, function(n) {
            return math.siground(n, sf);
        });
    },
    /** Transpose of a vector.
     *
     * @param {vector} v
     * @returns {matrix}
     */
    transpose: function(v) {
        var matrix = [v.slice()];
        matrix.rows = 1;
        matrix.columns = v.length;
        return matrix;
    },
    /** Convert a vector to a 1-column matrix.
     *
     * @param {vector} v
     * @returns {matrix}
     */
    toMatrix: function(v) {
        var m = v.map(function(n) {
            return [n]
        });
        m.rows = m.length;
        m.columns = 1;
        return m;
    },

    /** Is every component of this vector zero?
     *
     * @param {vector} v
     * @returns {boolean}
     */
    is_zero: function(v) {
        return v.every(function(c) {
            return c == 0;
        });
    }
}
/** A two-dimensional matrix: an array of rows, each of which is an array of numbers.
 *
 * @typedef matrix
 * @type {Array.<Array.<number>>}
 * @property {number} rows - The number of rows in the matrix.
 * @property {number} columns - The number of columns in the matrix.
 */
/** Matrix operations.
 *
 * These operations are very lax about the dimensions of vectors - they stick zeros in when pairs of matrices don't line up exactly.
 *
 * @namespace Numbas.matrixmath
 */
var matrixmath = Numbas.matrixmath = {
    /** Negate a matrix - negate each of its elements .
     *
     * @param {matrix} m
     * @returns {matrix}
     */
    negate: function(m) {
        var matrix = [];
        for(let i = 0;i < m.rows;i++) {
            matrix.push(m[i].map(function(x) {
                return negate(x)
            }));
        }
        matrix.rows = m.rows;
        matrix.columns = m.columns;
        return matrix;
    },
    /** Add two matrices.
     *
     * @param {matrix} a
     * @param {matrix} b
     * @returns {matrix}
     */
    add: function(a, b) {
        var rows = Math.max(a.rows, b.rows);
        var columns = Math.max(a.columns, b.columns);
        var matrix = [];
        for(let i = 0;i < rows;i++) {
            var row = [];
            matrix.push(row);
            for(let j = 0;j < columns;j++) {
                row[j] = add(a[i][j] || 0, b[i][j] || 0);
            }
        }
        matrix.rows = rows;
        matrix.columns = columns;
        return matrix;
    },
    /** Subtract one matrix from another.
     *
     * @param {matrix} a
     * @param {matrix} b
     * @returns {matrix}
     */
    sub: function(a, b) {
        var rows = Math.max(a.rows, b.rows);
        var columns = Math.max(a.columns, b.columns);
        var matrix = [];
        for(let i = 0;i < rows;i++) {
            var row = [];
            matrix.push(row);
            for(let j = 0;j < columns;j++) {
                row[j] = sub(a[i][j] || 0, b[i][j] || 0);
            }
        }
        matrix.rows = rows;
        matrix.columns = columns;
        return matrix;
    },
    /** Matrix determinant. Only works up to 3x3 matrices.
     *
     * @param {matrix} m
     * @returns {number}
     * @throws {Numbas.Error} "matrixmath.abs.too big" if the matrix has more than 3 rows.
     */
    abs: function(m) {
        if(m.rows != m.columns) {
            throw(new Numbas.Error('matrixmath.abs.non-square'));
        }
        //abstraction failure!
        switch(m.rows) {
        case 1:
            return m[0][0];
        case 2:
            return sub(mul(m[0][0], m[1][1]), mul(m[0][1], m[1][0]));
        case 3:
            return add(sub(
                            mul(m[0][0], sub(mul(m[1][1], m[2][2]), mul(m[1][2], m[2][1]))),
                            mul(m[0][1], sub(mul(m[1][0], m[2][2]), mul(m[1][2], m[2][0])))
                        ),
                        mul(m[0][2], sub(mul(m[1][0], m[2][1]), mul(m[1][1], m[2][0])))
                    );
        default:
            throw(new Numbas.Error('matrixmath.abs.too big'));
        }
    },
    /** Multiply a matrix by a scalar.
     *
     * @param {number} k
     * @param {matrix} m
     * @returns {matrix}
     */
    scalarmul: function(k, m) {
        var out = m.map(function(row) {
            return row.map(function(x) {
                return mul(k, x);
            });
        });
        out.rows = m.rows;
        out.columns = m.columns;
        return out;
    },
    /** Divide a matrix by a scalar.
     *
     * @param {matrix} m
     * @param {number} k
     * @returns {matrix}
     */
    scalardiv: function(m, k) {
        var out = m.map(function(row) {
            return row.map(function(x) {
                return div(x, k);
            });
        });
        out.rows = m.rows;
        out.columns = m.columns;
        return out;
    },
    /** Multiply two matrices.
     *
     * @param {matrix} a
     * @param {matrix} b
     * @returns {matrix}
     * @throws {Numbas.Error} "matrixmath.mul.different sizes" if `a` doesn't have as many columns as `b` has rows.
     */
    mul: function(a, b) {
        if(a.columns != b.rows) {
            throw(new Numbas.Error('matrixmath.mul.different sizes'));
        }
        var out = [];
        out.rows = a.rows;
        out.columns = b.columns;
        for(let i = 0;i < a.rows;i++) {
            var row = [];
            out.push(row);
            for(let j = 0;j < b.columns;j++) {
                var s = 0;
                for(let k = 0;k < a.columns;k++) {
                    s = add(s, mul(a[i][k], b[k][j]));
                }
                row.push(s);
            }
        }
        return out;
    },
    /** Are two matrices equal? True if each pair of corresponding elements is equal.
     *
     * @param {matrix} a
     * @param {matrix} b
     * @returns {boolean}
     */
    eq: function(a, b) {
        var rows = Math.max(a.rows, b.rows);
        var columns = Math.max(a.columns, b.columns);
        for(let i = 0;i < rows;i++) {
            var rowA = a[i] || [];
            var rowB = b[i] || [];
            for(let j = 0;j < columns;j++) {
                if(!eq(rowA[j] || 0, rowB[j] || 0)) {
                    return false;
                }
            }
        }
        return true;
    },
    /** Are two matrices unequal?
     *
     * @param {matrix} a
     * @param {matrix} b
     * @returns {boolean}
     * @see Numbas.matrixmath.eq
     */
    neq: function(a, b) {
        return !matrixmath.eq(a, b);
    },
    /** Make an `NxN` identity matrix.
     *
     * @param {number} n
     * @returns {matrix}
     */
    id: function(n) {
        var out = [];
        out.rows = out.columns = n;
        for(let i = 0;i < n;i++) {
            var row = [];
            out.push(row);
            for(let j = 0;j < n;j++) {
                row.push(j == i ? 1 : 0);
            }
        }
        return out;
    },
    /** Matrix transpose.
     *
     * @param {matrix} m
     * @returns {matrix}
     */
    transpose: function(m) {
        var out = [];
        out.rows = m.columns;
        out.columns = m.rows;
        for(let i = 0;i < m.columns;i++) {
            var row = [];
            out.push(row);
            for(let j = 0;j < m.rows;j++) {
                row.push(m[j][i] || 0);
            }
        }
        return out;
    },

    /** Sum of every cell.
     *
     * @param {matrix} m
     * @returns {number}
     */
    sum_cells: function(m) {
        var t = 0;
        m.forEach(function(row) {
            row.forEach(function(cell) {
                t += cell;
            });
        });
        return t;
    },
    /** Returns number of row in a matrix.
     *
     * @param {matrix} m
     * @returns {number}
     */
    numrows: function(m) {
        return m.rows;
    },
    /** Returns number of columns in a matrix.
     *
     * @param {matrix} m
     * @returns {number}
     */
    numcolumns: function(m) {
        return m.columns;
    },
    /** Combine two matrices vertically.
     *
     * @param {matrix} m1
     * @param {matrix} m2
     * @returns {matrix}
     */
    combine_vertically: function(m1, m2) {
        var out = [];
        out.rows = m1.rows + m2.rows;
        out.columns = m1.columns > m2.columns ? m1.columns : m2.columns;
        for(let i = 0; i < out.rows; i++) {
            var row = [];
            out.push(row);
            for(let j = 0; j < out.columns; j++) {
                row.push(i < m1.rows && j < m1.columns ? m1[i][j]
                    : i >= m1.rows && j < m2.columns ? m2[i - m1.rows][j] : 0);
            }
        } return out;
    },
    /** Combine two matrices horizontally.
     *
     * @param {matrix} m1
     * @param {matrix} m2
     * @returns {matrix}
     */
    combine_horizontally: function(m1, m2) {
        var out = [];
        out.columns = m1.columns + m2.columns;
        out.rows = m1.rows > m2.rows ? m1.rows : m2.rows;
        for(let i = 0; i < out.rows; i++) {
            var row = [];
            out.push(row);
            for(let j = 0; j < out.columns; j++) {
                row.push(j < m1.columns && i < m1.rows ? m1[i][j]
                    : j >= m1.columns && i < m2.rows ? m2[i][j - m1.columns] : 0);
            }
        } return out;
    },
    /** Combine two matrices diagonally.
     *
     * @param {matrix} m1
     * @param {matrix} m2
     * @returns {matrix}
     */
    combine_diagonally: function(m1, m2) {
        var out = [];
        out.rows = m1.rows + m2.rows;
        out.columns = m1.columns + m2.columns;
        for(let i = 0; i < out.rows; i++) {
            var row = [];
            out.push(row);
            for(let j = 0; j < out.columns; j++) {
                row.push(i < m1.rows && j < m1.columns ? m1[i][j]
                    : i >= m1.rows && j >= m1.columns ? m2[i - m1.rows][j - m1.columns] : 0);
            }
        } return out;
    },

    /** Apply given function to each element.
     *
     * @param {matrix} m
     * @param {Function} fn
     * @returns {matrix}
     */
    map: function(m, fn) {
        var out = m.map(function(row) {
            return row.map(fn);
        });
        out.rows = m.rows;
        out.columns = m.columns;
        return out;
    },

    /** Round each element to given number of decimal places.
     *
     * @param {matrix} m
     * @param {number} dp - Number of decimal places.
     * @returns {matrix}
     */
    precround: function(m, dp) {
        return matrixmath.map(m, function(n) {
            return math.precround(n, dp);
        });
    },

    /** Round each element to given number of significant figures.
     *
     * @param {matrix} m
     * @param {number} sf - Number of decimal places.
     * @returns {matrix}
     */
    siground: function(m, sf) {
        return matrixmath.map(m, function(n) {
            return math.siground(n, sf);
        });
    },

    /** LU decomposition: decompose a square matrix m into a lower-triangular matrix L and upper-triangular matrix U, satisfying `m = L*U`.
     *
     * @param {matrix} m
     * @returns {Array.<matrix>}
     */
    lu_decomposition: function(m) {
        if(m.rows != m.columns) {
            throw(new Numbas.Error("matrixmath.not square"));
        }
        const n = m.rows;

        const L = m.map((row) => row.map((_) => 0));
        L.rows = L.columns = n;
        const U = m.map((row) => row.map((_) => 0));
        U.rows = U.columns = n;

        for(let i = 0; i < n; i++) {
            U[i][i] = 1;
        }

        for(let j = 0; j < n; j++) {
            for(let i = j; i < n; i++) {
                let sum = 0;
                for(let k = 0; k < j; k++) {
                    sum += L[i][k] * U[k][j];
                }
                L[i][j] = m[i][j] - sum;
            }

            for(let i = j; i < n; i++) {
                let sum = 0;
                for(let k = 0; k < j; k++) {
                    sum += L[j][k] * U[k][i];
                }
                if(L[j][j] == 0) {
                    throw(new Numbas.Error("matrixmath.not invertible"));
                }
                U[j][i] = (m[j][i] - sum) / L[j][j];
            }
        }

        return [L, U];
    },

    /** Convert a matrix of numbers to a matrix of Fractions.
     *
     * @param {matrix} matrix
     * @returns {Array.<Array.<Numbas.math.Fraction>>}
     */
    fraction_matrix: function(matrix) {
        var o = matrix.map(function(r) {
            return r.map(function(c) {
                return c instanceof Fraction ? c : new Fraction(c, 1)
            })
        });
        o.rows = matrix.rows;
        o.columns = matrix.columns;
        return o;
    },

    /** Convert a matrix of fractions to a matrix of numbers.
     *
     * @param {Array.<Array.<Numbas.math.Fraction>>} matrix
     * @returns {matrix}
     */
    unfraction_matrix: function(matrix) {
        var o = matrix.map(function(r) {
            return r.map(function(c) {
                return c.numerator / c.denominator
            })
        });
        o.rows = matrix.rows;
        o.columns = matrix.columns;
        return o;
    },

    /** Put a matrix in row-echelon form.
     *
     * @param {fraction_matrix} matrix
     * @returns {fraction_matrix}
     */
    row_echelon_form: function(matrix) {
        const rows = matrix.rows;
        const columns = matrix.columns;

        var current_row = 0;
        // for each column, there should be at most one row with a 1 in that column, and every other row should have 0 in that column
        for(let leader_column = 0;leader_column < columns;leader_column++) {
            // find the first row with a non-zero in that column
            let row;
            for(row = current_row;row < rows;row++) {
                if(!matrix[row][leader_column].is_zero()) {
                    break;
                }
            }
            // if we found a row with a non-zero in the leader column
            if(row < rows) {
                // swap that row with the <current_row>th one
                if(row != current_row) {
                    var tmp = matrix[row];
                    matrix[row] = matrix[current_row];
                    matrix[current_row] = tmp;
                }

                // multiply this row so the leader column has a 1 in it
                var leader = matrix[current_row][leader_column];
                if(!leader.is_one()) {
                    matrix[current_row] = matrix[current_row].map(function(c) {
                        return c.divide(leader)
                    });
                }

                // subtract multiples of this row from every other row so they all have a zero in this column
                var sub = function(a, b) {
                    return a.subtract(b);
                };
                var add = function(a, b) {
                    return a.add(b);
                };
                for(let row = current_row + 1;row < rows;row++) {
                    if(row != current_row && !matrix[row][leader_column].is_zero()) {
                        var scale = matrix[row][leader_column];
                        var op = sub;
                        if(scale.numerator < 0) {
                            scale = new Fraction(-scale.numerator, scale.denominator);
                            op = add;
                        }
                        matrix[row] = matrix[row].map(function(c, i) {
                            return op(c, matrix[current_row][i].multiply(scale));
                        });
                    }
                }
                current_row += 1;
            }
        }

        return matrix;
    },

    /** Put a matrix representing a system of equations in reduced row-echelon form.
     * Can:
     * - Swap two rows
     * - Multiply a row by a scalar
     * - Subtract a multiple of one row from another
     * As well as being in row-echelon form, the matrix has the property that the first non-zero entry in each row is also the only non-zero entry in its column.
     *
     * @param {fraction_matrix} matrix
     * @returns {fraction_matrix}
     */
    reduced_row_echelon_form: function(matrix) {
        matrix = matrixmath.row_echelon_form(matrix);

        var rows = matrix.length;
        var columns = matrix[0].length;
        matrix.rows = rows;
        matrix.columns = columns;

        var sub = function(a, b) {
            return a.subtract(b);
        };
        var add = function(a, b) {
            return a.add(b);
        };

        for(let row = 0;row < rows;row++) {
            let column;
            for(column = 0;column < columns && matrix[row][column].is_zero();column++) {}

            if(column == columns) {
                continue;
            }

            for(let vrow = 0;vrow < rows;vrow++) {
                if(vrow != row && !matrix[vrow][column].is_zero()) {
                    var scale = matrix[vrow][column];
                    if(!scale.is_zero()) {
                        var op = sub;
                        if(scale.numerator < 0) {
                            op = add;
                            scale = new Fraction(-scale.numerator, scale.denominator);
                        }
                        matrix[vrow] = matrix[vrow].map(function(c, i) {
                            return op(c, matrix[row][i].multiply(scale));
                        });
                    }
                }
            }
        }

        return matrix;
    },

    gauss_jordan_elimination: function(matrix) {
        return matrixmath.unfraction_matrix(matrixmath.reduced_row_echelon_form(matrixmath.fraction_matrix(matrix)));
    },

    /** Find the inverse of the given square matrix.
     *
     * @param {matrix} m
     * @returns {matrix}
     */
    inverse: function(m) {
        if(m.rows != m.columns) {
            throw(new Numbas.Error("matrixmath.not square"));
        }
        const n = m.rows;

        const adjoined = matrixmath.combine_horizontally(m, matrixmath.id(m.rows));
        const reduced = matrixmath.gauss_jordan_elimination(adjoined);
        const inverse = reduced.map((row) => row.slice(n));
        inverse.rows = n;
        inverse.columns = n;

        return inverse;
    }
}

/** A set of objects: no item occurs more than once.
 *
 * @typedef set
 * @type {Array}
 */
/** Set operations.
 *
 * @namespace Numbas.setmath
 */
var setmath = Numbas.setmath = {
    /** Does the set contain the given element?
     *
     * @param {set} set
     * @param {*} element
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {boolean}
     */
    contains: function(set, element, scope) {
        for(let i = 0, l = set.length;i < l;i++) {
            if(Numbas.util.eq(set[i], element, scope)) {
                return true;
            }
        }
        return false;
    },
    /** Union of two sets.
     *
     * @param {set} a
     * @param {set} b
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {set}
     */
    union: function(a, b, scope) {
        var out = a.slice();
        for(let i = 0, l = b.length;i < l;i++) {
            if(!setmath.contains(a, b[i], scope)) {
                out.push(b[i]);
            }
        }
        return out;
    },
    /** Intersection of two sets.
     *
     * @param {set} a
     * @param {set} b
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {set}
     */
    intersection: function(a, b, scope) {
        return a.filter(function(v) {
            return setmath.contains(b, v, scope);
        });
    },
    /** Are two sets equal? Yes if a,b and (a intersect b) all have the same length.
     *
     * @param {set} a
     * @param {set} b
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {boolean}
     */
    eq: function(a, b, scope) {
        return a.length == b.length && setmath.intersection(a, b, scope).length == a.length;
    },
    /** Set minus - remove b's elements from a.
     *
     * @param {set} a
     * @param {set} b
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {set}
     */
    minus: function(a, b, scope) {
        return a.filter(function(v) {
            return !setmath.contains(b, v, scope);
        });
    },
    /** Size of a set.
     *
     * @param {set} set
     * @returns {number}
     */
    size: function(set) {
        return set.length;
    }

}

});
;
Numbas.queueScript('parsel',[],function(module) {
    var exports = module.exports;

const TOKENS = {
    attribute: /\[\s*(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(\s(?<caseSensitive>[iIsS]))?\s*)?\]/gu,
    id: /#(?<name>[-\w\P{ASCII}]+)/gu,
    class: /\.(?<name>[-\w\P{ASCII}]+)/gu,
    comma: /\s*,\s*/g,
    combinator: /\s*[\s>+~]\s*/g,
    'pseudo-element': /::(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
    'pseudo-class': /:(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
    universal: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?\*/gu,
    type: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)/gu, // this must be last
};
const TRIM_TOKENS = new Set(['combinator', 'comma']);
const RECURSIVE_PSEUDO_CLASSES = new Set([
    'not',
    'is',
    'where',
    'has',
    'matches',
    '-moz-any',
    '-webkit-any',
    'nth-child',
    'nth-last-child',
]);
const nthChildRegExp = /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/;
const RECURSIVE_PSEUDO_CLASSES_ARGS = {
    'nth-child': nthChildRegExp,
    'nth-last-child': nthChildRegExp,
};
const getArgumentPatternByType = (type) => {
    switch (type) {
        case 'pseudo-element':
        case 'pseudo-class':
            return new RegExp(TOKENS[type].source.replace('(?<argument>¶*)', '(?<argument>.*)'), 'gu');
        default:
            return TOKENS[type];
    }
};
function gobbleParens(text, offset) {
    let nesting = 0;
    let result = '';
    for (; offset < text.length; offset++) {
        const char = text[offset];
        switch (char) {
            case '(':
                ++nesting;
                break;
            case ')':
                --nesting;
                break;
        }
        result += char;
        if (nesting === 0) {
            return result;
        }
    }
    return result;
}
function tokenizeBy(text, grammar = TOKENS) {
    if (!text) {
        return [];
    }
    const tokens = [text];
    for (const [type, pattern] of Object.entries(grammar)) {
        for (let i = 0; i < tokens.length; i++) {
            const token = tokens[i];
            if (typeof token !== 'string') {
                continue;
            }
            pattern.lastIndex = 0;
            const match = pattern.exec(token);
            if (!match) {
                continue;
            }
            const from = match.index - 1;
            const args = [];
            const content = match[0];
            const before = token.slice(0, from + 1);
            if (before) {
                args.push(before);
            }
            args.push({
                ...match.groups,
                type,
                content,
            });
            const after = token.slice(from + content.length + 1);
            if (after) {
                args.push(after);
            }
            tokens.splice(i, 1, ...args);
        }
    }
    let offset = 0;
    for (const token of tokens) {
        switch (typeof token) {
            case 'string':
                throw new Error(`Unexpected sequence ${token} found at index ${offset}`);
            case 'object':
                offset += token.content.length;
                token.pos = [offset - token.content.length, offset];
                if (TRIM_TOKENS.has(token.type)) {
                    token.content = token.content.trim() || ' ';
                }
                break;
        }
    }
    return tokens;
}
const STRING_PATTERN = /(['"])([^\\\n]+?)\1/g;
const ESCAPE_PATTERN = /\\./g;
function tokenize(selector, grammar = TOKENS) {
    // Prevent leading/trailing whitespaces from being interpreted as combinators
    selector = selector.trim();
    if (selector === '') {
        return [];
    }
    const replacements = [];
    // Replace escapes with placeholders.
    selector = selector.replace(ESCAPE_PATTERN, (value, offset) => {
        replacements.push({ value, offset });
        return '\uE000'.repeat(value.length);
    });
    // Replace strings with placeholders.
    selector = selector.replace(STRING_PATTERN, (value, quote, content, offset) => {
        replacements.push({ value, offset });
        return `${quote}${'\uE001'.repeat(content.length)}${quote}`;
    });
    // Replace parentheses with placeholders.
    {
        let pos = 0;
        let offset;
        while ((offset = selector.indexOf('(', pos)) > -1) {
            const value = gobbleParens(selector, offset);
            replacements.push({ value, offset });
            selector = `${selector.substring(0, offset)}(${'¶'.repeat(value.length - 2)})${selector.substring(offset + value.length)}`;
            pos = offset + value.length;
        }
    }
    // Now we have no nested structures and we can parse with regexes
    const tokens = tokenizeBy(selector, grammar);
    // Replace placeholders in reverse order.
    const changedTokens = new Set();
    for (const replacement of replacements.reverse()) {
        for (const token of tokens) {
            const { offset, value } = replacement;
            if (!(token.pos[0] <= offset &&
                offset + value.length <= token.pos[1])) {
                continue;
            }
            const { content } = token;
            const tokenOffset = offset - token.pos[0];
            token.content =
                content.slice(0, tokenOffset) +
                    value +
                    content.slice(tokenOffset + value.length);
            if (token.content !== content) {
                changedTokens.add(token);
            }
        }
    }
    // Update changed tokens.
    for (const token of changedTokens) {
        const pattern = getArgumentPatternByType(token.type);
        if (!pattern) {
            throw new Error(`Unknown token type: ${token.type}`);
        }
        pattern.lastIndex = 0;
        const match = pattern.exec(token.content);
        if (!match) {
            throw new Error(`Unable to parse content for ${token.type}: ${token.content}`);
        }
        Object.assign(token, match.groups);
    }
    return tokens;
}
/**
 *  Convert a flat list of tokens into a tree of complex & compound selectors
 */
function nestTokens(tokens, { list = true } = {}) {
    if (list && tokens.find((t) => t.type === 'comma')) {
        const selectors = [];
        const temp = [];
        for (let i = 0; i < tokens.length; i++) {
            if (tokens[i].type === 'comma') {
                if (temp.length === 0) {
                    throw new Error('Incorrect comma at ' + i);
                }
                selectors.push(nestTokens(temp, { list: false }));
                temp.length = 0;
            }
            else {
                temp.push(tokens[i]);
            }
        }
        if (temp.length === 0) {
            throw new Error('Trailing comma');
        }
        else {
            selectors.push(nestTokens(temp, { list: false }));
        }
        return { type: 'list', list: selectors };
    }
    for (let i = tokens.length - 1; i >= 0; i--) {
        let token = tokens[i];
        if (token.type === 'combinator') {
            let left = tokens.slice(0, i);
            let right = tokens.slice(i + 1);
            return {
                type: 'complex',
                combinator: token.content,
                left: nestTokens(left),
                right: nestTokens(right),
            };
        }
    }
    switch (tokens.length) {
        case 0:
            throw new Error('Could not build AST.');
        case 1:
            // If we're here, there are no combinators, so it's just a list.
            return tokens[0];
        default:
            return {
                type: 'compound',
                list: [...tokens], // clone to avoid pointers messing up the AST
            };
    }
}
/**
 * Traverse an AST in depth-first order
 */
function* flatten(node, 
/**
 * @internal
 */
parent) {
    switch (node.type) {
        case 'list':
            for (let child of node.list) {
                yield* flatten(child, node);
            }
            break;
        case 'complex':
            yield* flatten(node.left, node);
            yield* flatten(node.right, node);
            break;
        case 'compound':
            yield* node.list.map((token) => [token, node]);
            break;
        default:
            yield [node, parent];
    }
}
/**
 * Traverse an AST (or part thereof), in depth-first order
 */
function walk(node, visit, 
/**
 * @internal
 */
parent) {
    if (!node) {
        return;
    }
    for (const [token, ast] of flatten(node, parent)) {
        visit(token, ast);
    }
}
/**
 * Parse a CSS selector
 *
 * @param selector - The selector to parse
 * @param options.recursive - Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true.
 * @param options.list - Whether this can be a selector list (A, B, C etc). Defaults to true.
 */
function parse(selector, { recursive = true, list = true } = {}) {
    const tokens = tokenize(selector);
    if (!tokens) {
        return;
    }
    const ast = nestTokens(tokens, { list });
    if (!recursive) {
        return ast;
    }
    for (const [token] of flatten(ast)) {
        if (token.type !== 'pseudo-class' || !token.argument) {
            continue;
        }
        if (!RECURSIVE_PSEUDO_CLASSES.has(token.name)) {
            continue;
        }
        let argument = token.argument;
        const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[token.name];
        if (childArg) {
            const match = childArg.exec(argument);
            if (!match) {
                continue;
            }
            Object.assign(token, match.groups);
            argument = match.groups['subtree'];
        }
        if (!argument) {
            continue;
        }
        Object.assign(token, {
            subtree: parse(argument, {
                recursive: true,
                list: true,
            }),
        });
    }
    return ast;
}
/**
 * Converts the given list or (sub)tree to a string.
 */
function stringify(listOrNode) {
    let tokens;
    if (Array.isArray(listOrNode)) {
        tokens = listOrNode;
    }
    else {
        tokens = [...flatten(listOrNode)].map(([token]) => token);
    }
    return tokens.map(token => token.content).join('');
}
/**
 * To convert the specificity array to a number
 */
function specificityToNumber(specificity, base) {
    base = base || Math.max(...specificity) + 1;
    return (specificity[0] * (base << 1) + specificity[1] * base + specificity[2]);
}
/**
 * Calculate specificity of a selector.
 *
 * If the selector is a list, the max specificity is returned.
 */
function specificity(selector) {
    let ast = selector;
    if (typeof ast === 'string') {
        ast = parse(ast, { recursive: true });
    }
    if (!ast) {
        return [];
    }
    if (ast.type === 'list' && 'list' in ast) {
        let base = 10;
        const specificities = ast.list.map((ast) => {
            const sp = specificity(ast);
            base = Math.max(base, ...specificity(ast));
            return sp;
        });
        const numbers = specificities.map((ast) => specificityToNumber(ast, base));
        return specificities[numbers.indexOf(Math.max(...numbers))];
    }
    const ret = [0, 0, 0];
    for (const [token] of flatten(ast)) {
        switch (token.type) {
            case 'id':
                ret[0]++;
                break;
            case 'class':
            case 'attribute':
                ret[1]++;
                break;
            case 'pseudo-element':
            case 'type':
                ret[2]++;
                break;
            case 'pseudo-class':
                if (token.name === 'where') {
                    break;
                }
                if (!RECURSIVE_PSEUDO_CLASSES.has(token.name) ||
                    !token.subtree) {
                    ret[1]++;
                    break;
                }
                const sub = specificity(token.subtree);
                sub.forEach((s, i) => (ret[i] += s));
                // :nth-child() & :nth-last-child() add (0, 1, 0) to the specificity of their most complex selector
                if (token.name === 'nth-child' ||
                    token.name === 'nth-last-child') {
                    ret[1]++;
                }
        }
    }
    return ret;
}

    module.exports = {'parsel': { RECURSIVE_PSEUDO_CLASSES, RECURSIVE_PSEUDO_CLASSES_ARGS, TOKENS, TRIM_TOKENS, flatten, gobbleParens, parse, specificity, specificityToNumber, stringify, tokenize, tokenizeBy, walk }};
});
;
Numbas.queueScript('part-display', ['display-util', 'display-base', 'util', 'jme'], function() {
    var display = Numbas.display;
    var util = Numbas.util;

    /** Display methods for a generic question part.
     *
     * @name PartDisplay
     * @memberof Numbas.display
     * @class
     * @param {Numbas.parts.Part} p - The associated part object.
     */
    display.PartDisplay = function(p) {
        var pd = this;
        /** The associated part object.
         *
         * @member {Numbas.parts.Part} part
         * @memberof Numbas.display.PartDisplay
         */
        this.part = p;
        /** The display name of this part.
         *
         * @member {observable|string} name
         * @memberof Numbas.display.PartDisplay
         * @see Numbas.parts.Part#name
         */
        this.name = Knockout.observable('');
        /** Title text for this part's answer input.
         *
         * @member {observable.<string>} input_title
         * @memberof Numbas.display.PartDisplay
         */
        this.input_title = Knockout.computed(function() {
            return R('part.input title', {name: this.name()});
        }, this);

        this.feedback_title = Knockout.computed(function() {
            return R('part.feedback title', {name: this.name()});
        }, this);

        /** Should the name of this part be displayed?
         *
         * @member {observable|boolean} showName
         * @memberof Numbas.display.PartDisplay
         */
        this.showName = Knockout.computed(function() {
            return this.name() && !this.part.isGap && (this.part.question.partsMode == 'all' || this.revealed());
        }, this);

        /** The question this part belongs to.
         *
         * @member {Numbas.Question} question
         * @memberof Numbas.display.PartDisplay
         */
        this.question = p.question;

        /** Should this part be shown?
         *
         * @member {boolean} visible
         * @memberof Numbas.display.PartDisplay
         */
        this.visible = Knockout.computed(function() {
            switch(this.question.partsMode) {
                case 'all':
                    return true;
                case 'explore':
                    var part = this.part;
                    while(part.parentPart) {
                        part = part.parentPart;
                    }
                    return this.question.display.currentPart() == part.display || this.question.exam.display.mode() == 'review';
            }
        }, this);

        /** The student's current score.
         *
         * @see Numbas.parts.Part#score
         * @member {observable|number} score
         * @memberof Numbas.display.PartDisplay
         */
        this.score = Knockout.observable(p.score);
        /** The total marks available for this part.
         *
         * @see Numbas.parts.Part#marks
         * @member {observable|number} marks
         * @memberof Numbas.display.PartDisplay
         */
        this.marks = Knockout.observable(p.marks);
        /** Proportion of available marks awarded to the student - i.e. `score/marks`. Penalties will affect this instead of the raw score, because of things like the steps marking algorithm.
         *
         * @member {observable|number} credit
         * @memberof Numbas.display.PartDisplay
         */
        this.credit = Knockout.observable(p.credit);
        /** Does this part do any marking?
         *
         * @member {observable|boolean} doesMarking
         * @see Numbas.parts.Part#doesMarking
         * @memberof Numbas.display.PartDisplay
         */
        this.doesMarking = Knockout.observable(p.doesMarking);
        /** Has the student answered this part?
         *
         * @member {observable|boolean} answered
         * @memberof Numbas.display.PartDisplay
         */
        this.answered = Knockout.observable(p.answered);
        /** Has the student changed their answer since the last submission?
         *
         * @member {observable|boolean} isDirty
         * @memberof Numbas.display.PartDisplay
         */
        this.isDirty = Knockout.observable(false);

        this.isDirty.subscribe(function() {
            if(!pd.isDirty()) {
                return;
            }
            setTimeout(function() {
                pd.controls.auto_submit();
            }, 1000);
        });

        /** Is this part waiting for some pre-submit tasks to finish before submitting?
         *
         * @member {observable|boolean} waiting_for_pre_submit
         * @memberof Numbas.display.PartDisplay
         */
        this.waiting_for_pre_submit = Knockout.observable(false);

        var _warnings = Knockout.observableArray([]);

        /** Warnings based on the student's answer.
         *
         * @member {observable | Array.<{message: string}>} warnings
         * @memberof Numbas.display.PartDisplay
         */
        this.warnings = Knockout.computed({
            read: function() {
                return _warnings().filter(function(w) {
                    return util.isNonemptyHTML(w.message);
                });
            },
            write: _warnings
        });
        this.warnings.push = function() {
            return _warnings.push.apply(_warnings, arguments);
        }

        /** Does the part have any warnings to show?
         * Changes to false immediately.
         * Only changes to true after a delay if the part is dirty, but immediately if the part is not (i.e. it's just been submitted).
         *
         * @member {observable|boolean} hasWarnings
         */
        this.hasWarnings = Knockout.observable(false);
        var lastWarningReset;
        Knockout.computed(function() {
            if(this.warnings().length == 0) {
                this.hasWarnings(false);
                if(lastWarningReset) {
                    clearTimeout(lastWarningReset);
                    lastWarningReset = null;
                }
            }
        }, this);
        Knockout.computed(function() {
            if(this.warnings().length > 0) {
                if(lastWarningReset) {
                    clearTimeout(lastWarningReset);
                }
                if(this.isDirty()) {
                    lastWarningReset = setTimeout(function() {
                        pd.hasWarnings(true);
                    }, 500);
                } else {
                    pd.hasWarnings(true);
                }
            }
        }, this);

        var _warningsShown = Knockout.observable(false);
        /** Should the warning box be shown?
         *
         * @member {observable|boolean} warningsShown
         * @memberof Numbas.display.PartDisplay
         */
        this.warningsShown = Knockout.computed({
            read: function() {
                return this.hasWarnings() && (_warningsShown() || this.alwaysShowWarnings);
            },
            write: function(v) {
                return _warningsShown(v);
            }
        }, this);

        /** Position the warnings box.
         */
        function position_warnings() {
            if(!pd.html || !pd.warningsShown.peek()) {
                return;
            }
            var margin = 10;

            var warnings_box = pd.html.querySelector(':scope > .student.answer .warnings');
            var answer = pd.html.querySelector(':scope > .student.answer');
            var offsetTop = 0;
            var offsetLeft = 0;
            var el = answer;
            while(el.offsetParent && !el.classList.contains('question')) {
                offsetTop += el.offsetTop;
                offsetLeft += el.offsetLeft;
                el = el.offsetParent;
            }
            var answer_height = answer.getBoundingClientRect().height;
            var answer_width = answer.getBoundingClientRect().width;

            var wtop = offsetTop + (p.isGap ? 0 : answer_height);
            var wleft = (offsetLeft + (p.isGap ? margin + answer_width : 0));

            warnings_box.style.top = wtop + 'px';
            warnings_box.style.left = wleft + 'px';

            var box = warnings_box.getBoundingClientRect();
            const question_html = pd.part.question.display.html;
            var question_box = question_html.getBoundingClientRect();
            var docWidth = question_box.width;
            var dr = box.right - question_box.left - docWidth + margin;
            warnings_box.classList.remove('shifted-down');
            if(dr > 0) {
                wleft -= dr;
                if(p.isGap) {
                    wtop += answer_height;
                    warnings_box.classList.add('shifted-down');
                }
                warnings_box.style.left = wleft + 'px';
                warnings_box.style.top = wtop + 'px';
            }
            warnings_box.style.width = '';
            const ideal_width = parseFloat(window.getComputedStyle(warnings_box).width.replace('px', ''));
            var maxWidth = docWidth - 3 * margin;
            if(ideal_width > maxWidth) {
                warnings_box.style.width = maxWidth + 'px';
            }
        }


        /** Show the warnings.
         *
         * @member {Function} showWarnings
         * @function
         * @memberof Numbas.display.PartDisplay
         */
        this.showWarnings = function() {
            this.warningsShown(true);
            position_warnings();
        }

        setInterval(position_warnings, 200);

        /** Hide the warnings.
         *
         * @member {Function} hideWarnings
         * @function
         * @memberof Numbas.display.PartDisplay
         */
        this.hideWarnings = function() {
            this.warningsShown(false);
        }
        /** Feedback messages.
         *
         * @member {observable} feedbackMessages
         * @memberof Numbas.display.PartDisplay
         */
        this.feedbackMessages = Knockout.observableArray([]);

        /** Are there other parts in line with this one? (Used to decide whether to show the submit button and feedback text)
         * True if there's more than one part in the question, or this is a step.
         *
         * @member {observable|boolean} isNotOnlyPart
         * @memberof Numbas.display.PartDisplay
         */
        this.isNotOnlyPart = Knockout.computed(function() {
            return this.question.display.numParts() > 1 || this.part.isStep;
        }, this);

        var _feedbackShown = Knockout.observable(false);

        /** Is the box containing the feedback messages open?
         *
         * @member {observable.<boolean>} feedbackShown
         * @memberof Numbas.display.PartDisplay
         */
        this.feedbackShown = Knockout.computed({
            read: function() {
                return _feedbackShown() || !this.isNotOnlyPart()
            },

            write: function(v) {
                _feedbackShown(v);
            }
        }, this);

        /** Text for the button to toggle the display of the feedback messages.
         *
         * @member {observable|string} feedbackToggleText
         * @memberof Numbas.display.PartDisplay
         */
        this.feedbackToggleText = Knockout.pureComputed(function() {
            if(this.waiting_for_pre_submit()) {
                return R('part.waiting for pre submit');
            }
            return R('part.feedback');
        }, this);

        /** Have the steps ever been shown?
         *
         * @see Numbas.parts.Part#stepsShown
         * @member {observable|boolean} stepsShown
         * @memberof Numbas.display.PartDisplay
         */
        this.stepsShown = Knockout.observable(p.stepsShown);
        /** Are the steps currently open?
         *
         * @see Numbas.parts.Part#stepsOpen
         * @member {observable|boolean} stepsOpen
         * @memberof Numbas.display.PartDisplay
         */
        this.stepsOpen = Knockout.observable(p.stepsOpen);

        this.ended = this.question.exam.display.ended;

        /** Have the correct answers been revealed?
         *
         * @member {observable|boolean} revealed
         * @memberof Numbas.display.PartDisplay
         */
        this.revealed = Knockout.observable(false);
        /** Has this part been locked?
         *
         * @member {observable|boolean} locked
         * @memberof Numbas.display.PartDisplay
         */
        this.locked = Knockout.observable(false);
        /** Is this part disabled? True if revealed or locked.
         *
         * @member {observable|boolean} locked
         * @memberof Numbas.display.PartDisplay
         */
        this.disabled = Knockout.computed(function() {
            return this.revealed() || this.locked();
        }, this);
        /** Show the "submit part" button?
         *
         * @member {observable|boolean} showSubmitPart
         * @memberof Numbas.display.PartDisplay
         */
        this.showSubmitPart = Knockout.computed(function() {
            return this.doesMarking() && !this.disabled();
        }, this);
        /** Text to describe the state of the steps penalty.
         *
         * @member {observable|string} stepsPenaltyMessage
         * @memberof Numbas.display.PartDisplay
         */
        this.stepsPenaltyMessage = Knockout.computed(function() {
            if(this.stepsOpen()) {
                return R('question.hide steps no penalty');
            } else if(this.part.settings.stepsPenalty == 0 || this.revealed()) {
                return R('question.show steps no penalty');
            } else if(this.stepsShown()) {
                return R('question.show steps already penalised');
            } else {
                return R('question.show steps penalty', {count:this.part.settings.stepsPenalty});
            }
        }, this);
        /** Should the correct answer be shown? True if revealed and {@link Numbas.parts.Part#settings.showCorrectAnswer}) is true.
         *
         * @member {observable|boolean} showCorrectAnswer
         * @memberof Numbas.display.PartDisplay
         */
        this.showCorrectAnswer = Knockout.computed(function() {
            if(!pd.revealed()) {
                return false;
            }
            return Numbas.is_instructor || (p.doesMarking && p.settings.showCorrectAnswer && p.question.display.expectedAnswersRevealed());
        });

        var feedback_settings = Numbas.util.copyobj(p.question.exam.settings);

        feedback_settings.showFeedbackIcon = p.settings.showFeedbackIcon;
        if(p.parentPart && p.parentPart.type == 'gapfill' && p.parentPart.settings.sortAnswers) {
            feedback_settings.showFeedbackIcon = false;
            feedback_settings.showAnswerState = false;
        }

        this.previousPart = p.previousPart ? p.previousPart.display : null;

        /** Display of this parts's current score / answered status.
         *
         * @member {observable|Numbas.display.scoreFeedback} scoreFeedback
         * @memberof Numbas.display.PartDisplay
         */
        this.scoreFeedback = Numbas.display_util.showScoreFeedback(this, feedback_settings);

        /** Should feedback icons be shown for this part?
         *
         * @member {observable|boolean} showFeedbackIcon
         * @memberof Numbas.display.PartDisplay
         */
        this.showFeedbackIcon = Knockout.observable(feedback_settings.showFeedbackIcon);

        /** Show the marks feedback?
         *
         * @member {observable|boolean} showMarks
         * @memberof Numbas.display.PartDisplay
         */
        this.showMarks = Knockout.computed(function() {
            return this.scoreFeedback.message() && (this.isNotOnlyPart() || !(this.scoreFeedback.iconClass() == '' || this.scoreFeedback.iconClass() == 'invisible'));
        }, this);

        /** Should the box containing part marks and the submit and feedback buttons be shown?
         *
         * @member {observable|boolean} showFeedbackBox
         * @memberof Numbas.display.PartDisplay
         */
        this.showFeedbackBox = Knockout.computed(function() {
            return this.doesMarking();
        }, this);

        /** Should the feedback messages be shown?
         *
         * @member {observable|boolean} showFeedbackMessages
         * @memberof Numbas.display.PartDisplay
         */
        this.showFeedbackMessages = Numbas.display_util.resolve_feedback_setting(this, p.question.exam.settings.showPartFeedbackMessages);

        /** Has the feedback changed since it was last looked at?
         * @member {observable|boolean} showFeedbackMessages
         * @memberof Numbas.display.PartDisplay
         */
        this.changedFeedback = Knockout.observable(false);

        this.feedbackShown.subscribe(function(shown) {
            if(shown) {
                pd.changedFeedback(false);
            }
        });

        /**
         * Feedback messages that are shown to the student.
         * If all feedback should be shown, then returns the entire list. If not, then only messages relating to invalid input are shown.
         *
         * @member {observable.<Array>} shownFeedbackMessages
         * @memberof Numbas.display.PartDisplay
         */
        this.shownFeedbackMessages = Knockout.computed(function() {
            var messages = this.feedbackMessages();

            // If not showing part feedback messages, only show messages about invalid input.
            if(!this.showFeedbackMessages()) {
                messages = messages.filter(function(m) {
                    return m.credit_change == 'invalid';
                });
            }

            // If not showing the answer correctness, don't show messages that give positive or negative feedback.
            if(!this.scoreFeedback.showAnswerState()) {
                messages = messages.filter(function(m) {
                    return !(m.credit_change == 'positive' || m.credit_change == 'negative');
                });
            }

            // If showing the current score and this part is marked, add a message giving the total score.
            if(feedback_settings.showFeedbackIcon && this.marks() != 0 && this.scoreFeedback.showActualMark() && this.answered()) {
                messages.push({
                    credit_change: '',
                    message: '',
                    icon: '',
                    credit_message: R('part.marking.total score', {count:p.score}),
                    format: 'string'
                });
            }
            return messages;
        }, this);

        this.shownFeedbackMessages.subscribe(function(messages) {
            pd.changedFeedback(!pd.feedbackShown() && messages.length > 0);
        })

        /**
         * Does this part have any shown feedback messages?
         *
         * @member {observable.<boolean>} hasFeedbackMessages
         * @memberof Numbas.display.PartDisplay
         */
        this.hasFeedbackMessages = Knockout.pureComputed(function() {
            return this.shownFeedbackMessages().length > 0 || this.waiting_for_pre_submit();
        }, this);

        /** Options for the next part.
         *
         * @member {observable} nextParts
         * @memberof Numbas.display.PartDisplay
         */
        this.nextParts = Knockout.observableArray([]);
        this.updateNextParts();

        /** Should the list of next parts be shown?
         *
         * @member {observable.<boolean>} showNextParts
         * @memberof Numbas.display.PartDisplay
         */
        this.showNextParts = Knockout.computed(function() {
            if(this.part.question.partsMode != 'explore') {
                return false;
            }
            if(!(this.part.settings.suggestGoingBack || this.nextParts().length > 0)) {
                return false;
            }
            if(this.revealed()) {
                return false;
            }
            return true;
        }, this);

        /** Header for the menu of next parts.
         *
         * @member {observable.<string>} whatNextMessage
         * @memberof Numbas.display.PartDisplay
         */
        this.whatNextMessage = Knockout.computed(function() {
            return R(this.answered() ? "part.choose next part.answered" : "part.choose next part.unanswered");
        }, this);

        /** Is this part a dead end? True if answered or doesn't do marking, and there are no next parts.
         *
         * @member {observable.<boolean>} reachedDeadEnd
         * @memberof Numbas.display.PartDisplay
         */
        this.reachedDeadEnd = Knockout.computed(function() {
            return this.part.question.partsMode == 'explore' && (this.answered() || !this.doesMarking()) && !this.showNextParts() && !this.revealed();
        }, this);

        /** Is this part the current part in an explore mode question?
         *
         * @member {observable.<boolean>} isCurrentPart
         * @memberof Numbas.display.PartDisplay
         */
        this.isCurrentPart = Knockout.pureComputed(function() {
            return this == this.question.display.currentPart();
        }, this);

        /** Next parts that have been made.
         *
         * @member {observableArray.<Numbas.display.PartDisplay>} madeNextParts
         * @memberof Numbas.display.PartDisplay
         */

        this.madeNextParts = Knockout.computed(function() {
            var parts = this.nextParts().filter(function(np) {
                return np.made;
            }).map(function(np) {
                return np.instance;
            });
            return parts.sort(function(a, b) {
                return a.part.path < b.part.path ? -1 : a.part.path > b.part.path ? 1 : 0
            });
        }, this);

        /** Control functions.
         *
         * @member {object} controls
         * @memberof Numbas.display.PartDisplay
         * @property {Function} submit - Submit the student's answers for marking.
         * @property {Function} showSteps - Show the steps.
         * @property {Function} hideSteps - Hide the steps.
         */
        this.controls = {
            auto_submit: function() {
                if(!p.question.exam.settings.autoSubmit) {
                    return;
                }
                var clicking_inside = pd.clicking_inside;

                setTimeout(function() {
                    if(!pd.will_autoSubmit) {
                        return;
                    }
                    pd.will_autoSubmit = false;

                    if(p.type == 'gapfill') {
                        if(!p.gaps.every((g) => g.isDirty || g.answered)) {
                            return;
                        }
                    }
                    pd.controls.submit(!clicking_inside);
                }, 100);
            },
            submit: function(auto) {
                if(auto !== true) {
                    pd.feedbackShown(true);
                }
                var ps = p;
                while(ps.isGap) {
                    ps = ps.parentPart;
                }
                Numbas.controls.submitPart(ps);
            },
            showSteps: function() {
                p.showSteps();
            },
            hideSteps: function() {
                p.hideSteps();
            }
        }

        var autoSubmit_part = this.autoSubmit_part = p.parentPart && p.parentPart.type == 'gapfill' ? p.parentPart : p;
        this.will_autoSubmit = false;

        /** Event bindings.
         *
         * @member {object} inputEvents
         * @memberof Numbas.display.PartDisplay
         */
        this.inputEvents = {
            keyup: function(context, e) {
                if (e.key == 'Escape') {
                    pd.hideWarnings();
                } else {
                    return true;    // allow the event to be handled as normal
                }
            },
            keypress: function(context, e) {
                if (e.key == 'Enter') {
                    pd.controls.submit();
                } else {
                    return true;    // allow the event to be handled as normal
                }
            },
            blur: function() {
                pd.hideWarnings();

                if(pd.isDirty()) {
                    autoSubmit_part.display.will_autoSubmit = true;
                    autoSubmit_part.display.controls.auto_submit();
                }
            },
            focus: function() {
                autoSubmit_part.display.will_autoSubmit = false;
                pd.showWarnings();
            },
            click: function(context, e) {
                if(e.target.tagName.toLowerCase()=='input') {
                    e.target.focus();
                }
                return true;
            }
        };

        p.xml.setAttribute('jme-context-description', p.name);
        p.xml.setAttribute('path', p.path);
        p.xml.setAttribute('isgap', p.isGap);
        p.xml.setAttribute('isstep', p.isStep);

        /** A promise resolving to the part's HTML element.
         *
         * @see Numbas.display.makeHTMLFromXML
         * @type {Promise}
         * @memberof Numbas.display.PartDisplay
         */
        pd.html_promise = new Promise(function(resolve) {
            pd.resolve_html_promise = resolve;
        });

        /** Called when Knockout has finished binding the HTML for this part to the DOM.
         *
         * @memberof Numbas.display.PartDisplay
         */
        this.htmlBound = function() {
            p.signals.trigger('HTMLAttached');
        };
    }
    display.PartDisplay.prototype = /** @lends Numbas.display.PartDisplay.prototype */
    {
        /** Set this part's name.
         *
         * @param {string} name
         */
        setName: function(name) {
            this.name(name || this.part.name);
        },
        /** Show a warning message about this part.
         *
         * @param {string} warning
         * @memberof Numbas.display.PartDisplay
         */
        warning: function(warning) {
            this.warnings.push({message:warning + ''});
        },
        /** Set the list of warnings.
         *
         * @param {Array.<string>} warnings
         * @memberof Numbas.display.PartDisplay
         */
        setWarnings: function(warnings) {
            this.warnings(warnings.map(function(warning) {
                return {message: warning + ''}
            }));
        },
        /** Remove all previously displayed warnings.
         *
         * @memberof Numbas.display.PartDisplay
         */
        removeWarnings: function() {
            this.part.removeWarnings();
        },
        /** Called when the part is displayed (basically when question is changed).
         *
         * @see Numbas.display.QuestionDisplay.show
         * @memberof Numbas.display.PartDisplay
         */
        show: function() {
            this.showScore(this.part.answered, true);
        },
        /** Called when the correct answer to the question has changed (particularly when this part uses adaptive marking).
         * The displayed correct answer should update.
         *
         * @memberof Numbas.display.PartDisplay
         * @param {*} answer
         * @abstract
         */
        updateCorrectAnswer: function(answer) {},
        /**
         * Show/update the student's score and answer status on this part.
         *
         * @param {boolean} valid
         * @param {boolean} noUpdate
         * @memberof Numbas.display.PartDisplay
         */
        showScore: function(valid, noUpdate) {
            var p = this.part;
            this.score(p.score);
            this.marks(p.marks);
            this.credit(p.credit);
            if(!noUpdate) {
                this.scoreFeedback.update(true);
            }
            if(valid === undefined) {
                valid = this.part.answered;
            }
            this.answered(valid);
            if(this.part.markingFeedback.length) {
                if(!this.part.question.revealed) {
                    var messages = this.part.markingFeedback.filter(function(action) {
                        return util.isNonemptyHTML(action.message) || action.credit != 0;
                    }).map(function(action) {
                        var icons = {
                            'positive': 'icon-ok',
                            'negative': 'icon-remove',
                            'neutral': '',
                            'invalid': 'icon-exclamation-sign'
                        }
                        return {credit_change: action.credit_change, message: action.message, credit_message: action.credit_message, icon: icons[action.credit_change], format: action.format || 'string'};
                    });
                    this.feedbackMessages(messages);
                }
            } else {
                this.feedbackMessages([]);
            }
        },
        /** Called when 'show steps' button is pressed, or coming back to a part after steps shown.
         *
         * @memberof Numbas.display.PartDisplay
         */
        showSteps: function() {
            this.stepsShown(this.part.stepsShown);
            this.stepsOpen(this.part.stepsOpen);
            for(var i = 0;i < this.part.steps.length;i++) {
                this.part.steps[i].display.show();
            }
        },
        /** Hide the steps.
         *
         * @memberof Numbas.display.PartDisplay
         */
        hideSteps: function() {
            this.stepsOpen(this.part.stepsOpen);
        },
        /** Fill the student's last submitted answer into inputs.
         *
         * @abstract
         * @param {object} studentAnswer
         * @memberof Numbas.display.PartDisplay
         */
        restoreAnswer: function(studentAnswer) {
        },
        /** Show the correct answers to this part.
         *
         * @memberof Numbas.display.PartDisplay
         */
        revealAnswer: function() {
            this.revealed(true);
            this.removeWarnings();
            this.showScore();
        },

        /** Lock this part.
         *
         * @memberof Numbas.display.PartDisplay
         */
        lock: function() {
            this.locked(true);
        },

        event_handlers: {
            pointerdown: function(e) {
                this.clicking_inside = true;
                return true;
            },

            pointerup: function(e) {
                this.clicking_inside = false;
                return true;
            },

            focusin: function(e) {
                this.will_autoSubmit = false;
                return true;
            },

            focusout: function(e) {
                if(this.isDirty()) {
                    this.autoSubmit_part.display.will_autoSubmit = true;
                    this.autoSubmit_part.display.controls.auto_submit();
                }
                return true;
            },
        },

        /** Update the list of next parts.
         * Called when an instance of a next part is created or removed.
         *
         * @memberof Numbas.display.PartDisplay
         */
        updateNextParts: function() {
            var p = this.part;
            this.nextParts(p.availableNextParts().map(function(np) {
                var penaltyAmount = np.penalty ? np.penaltyAmount : 0;
                var label = np.label;
                if(!np.instance && np.showPenaltyHint && penaltyAmount != 0) {
                    label += ' ' + R('part.next part.penalty amount', {count:penaltyAmount});
                }
                return {
                    label: label,
                    made: np.instance !== null,
                    instance: np.instance !== null ? np.instance.display : null,
                    penaltyAmount: penaltyAmount,
                    lockAfterLeaving: np.lockAfterLeaving,
                    select: function() {
                        if(np.instance) {
                            p.question.setCurrentPart(np.instance)
                        } else {
                            var penaltyAmount = np.penalty ? np.penaltyAmount : 0;
                            if(penaltyAmount > 0) {
                                p.question.exam.display.root_element.showConfirm(
                                    R('part.next part.confirm penalty', {count: penaltyAmount}),
                                    function() {
                                        p.makeNextPart(np);
                                    }
                                );
                            } else {
                                p.makeNextPart(np);
                            }
                        }
                    }
                };
            }));
        },

        /** Initialise this part's display.
         *
         * @see Numbas.display.QuestionDisplay.init
         * @memberof Numbas.display.PartDisplay
         */
        init: function() {
            this.part.setDirty(false);
            for(var i = 0;i < this.part.steps.length;i++) {
                this.part.steps[i].display.init();
            }
        },
        /** Called when the exam ends.
         *
         * @memberof Numbas.display.PartDisplay
         */
        end: function() {
            this.restoreAnswer(this.part.studentAnswer);
        }
    };
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file {@link Numbas.parts}, {@link Numbas.partConstructors}, {@link Numbas.createPart} and the generic {@link Numbas.parts.Part} object */
Numbas.queueScript('part', ['base', 'jme', 'jme-variables', 'util', 'marking'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var math = Numbas.math;
var marking = Numbas.marking;

var SAVE_STAGED_ANSWER_FREQUENCY = 5000;

/** Definitions of custom part types.
 *
 * @name custom_part_types
 * @type {object}
 * @memberof Numbas
 */

/** A unique identifier for a {@link Numbas.parts.Part} object, of the form `qXpY[gZ|sZ]`. Numbering starts from zero, and the `gZ` bit is used only when the part is a gap, and `sZ` is used if it's a step.
 *
 * @typedef Numbas.parts.partpath
 * @type {string}
 */
/** Part type constructors.
 * These functions aren't called directly - they're the original part constructor objects before they're extended with the generic part methods, kept for reference so their methods can be reused by other parts.
 *
 * @see Numbas.partConstructors
 * @namespace Numbas.parts
 * @memberof Numbas
 */
Numbas.parts = {};
/** Associate part type names with their object constructors.
 * These constructors are called by {@link Numbas.createPart} - they should be finalised constructors with all the generic part methods implemented.
 * Most often, you do this by extending {@link Numbas.parts.Part}.
 *
 * @memberof Numbas
 */
var partConstructors = Numbas.partConstructors = {};
/** Create a question part based on an XML definition.
 *
 * @memberof Numbas
 * @param {number} index - The index of the part's definition.
 * @param {Element} xml
 * @param {Numbas.parts.partpath} [path]
 * @param {Numbas.Question} [question]
 * @param {Numbas.parts.Part} [parentPart]
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {Numbas.jme.Scope} [scope] - Scope in which the part should evaluate JME expressions. If not given, the question's scope or {@link Numbas.jme.builtinScope} are used.
 * @fires Numbas.Part#event:finaliseLoad
 * @returns {Numbas.parts.Part}
 * @throws {Numbas.Error} "part.missing type attribute" if the top node in `xml` doesn't have a "type" attribute.
 */
Numbas.createPartFromXML = function(index, xml, path, question, parentPart, store, scope) {
    var tryGetAttribute = Numbas.xml.tryGetAttribute;
    var type = tryGetAttribute(null, xml, '.', 'type', []);
    if(type == null) {
        throw(new Numbas.Error('part.missing type attribute', {part:util.nicePartName(path)}));
    }
    var part = createPart(index, type, path, question, parentPart, store, scope);
    try {
        part.loadFromXML(xml);
        part.finaliseLoad();
        part.signals.trigger('finaliseLoad');
        if(Numbas.display && part.question && part.question.display) {
            part.initDisplay();
        }
    } catch(e) {
        if(e.originalMessage == 'part.error') {
            throw(e);
        }
        part.error(e.message, {}, e);
    }
    return part;
}
/** Create a question part based on an XML definition.
 *
 * @memberof Numbas
 * @param {number} index - The index of the part's definition.
 * @param {object} data
 * @param {Numbas.parts.partpath} [path]
 * @param {Numbas.Question} [question]
 * @param {Numbas.parts.Part} [parentPart]
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {Numbas.jme.Scope} [scope] - Scope in which the part should evaluate JME expressions. If not given, the question's scope or {@link Numbas.jme.builtinScope} are used.
 * @fires Numbas.Part#event:finaliseLoad
 * @returns {Numbas.parts.Part}
 * @throws {Numbas.Error} "part.missing type attribute" if `data` doesn't have a "type" attribute.
 */
var createPartFromJSON = Numbas.createPartFromJSON = function(index, data, path, question, parentPart, store, scope) {
    if(!data.type) {
        throw(new Numbas.Error('part.missing type attribute', {part:util.nicePartName(path)}));
    }
    var part = createPart(index, data.type, path, question, parentPart, store, scope);
    try {
        part.loadFromJSON(data);
        part.finaliseLoad();
        part.signals.trigger('finaliseLoad');
        if(Numbas.display && part.question && part.question.display) {
            part.initDisplay();
        }
    } catch(e) {
        if(e.originalMessage == 'part.error') {
            throw(e);
        }
        part.error(e.message, {}, e);
    }
    return part;
}
/** Create a new question part.
 *
 * @see Numbas.partConstructors
 * @param {number} index - The index of the part's definition.
 * @param {string} type
 * @param {Numbas.parts.partpath} path
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {Numbas.jme.Scope} [scope] - Scope in which the part should evaluate JME expressions. If not given, the question's scope or {@link Numbas.jme.builtinScope} are used.
 * @returns {Numbas.parts.Part}
 * @throws {Numbas.Error} "part.unknown type" if the given part type is not in {@link Numbas.partConstructors}
 * @memberof Numbas
 */
var createPart = Numbas.createPart = function(index, type, path, question, parentPart, store, scope) {
    if(partConstructors[type]) {
        var cons = partConstructors[type];
        var part = new cons(index, path, question, parentPart, store);
        part.type = type;
        part.scope = part.makeScope(scope);
        return part;
    } else {
        throw(new Numbas.Error('part.unknown type', {part:util.nicePartName(path), type:type}));
    }
}

/** Base question part object.
 *
 * @class
 * @memberof Numbas.parts
 * @param {number} index - The index of the part's definition.
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @property {boolean} isStep - Is this part a step?
 * @property {boolean} isGap - Is this part a gap?
 * @property {string} full_path - A globally-unique path to this part, including the parent question's number.
 * @see Numbas.createPart
 */
var Part = Numbas.parts.Part = function(index, path, question, parentPart, store) {
    var p = this;
    p.signals = new Numbas.schedule.SignalBox();
    p.events = new Numbas.schedule.EventBox();
    this.index = index;
    this.store = store;
    //remember parent question object
    this.question = question;
    //remember parent part object, so scores can percolate up for steps/gaps
    this.parentPart = parentPart;
    //remember a path for this part, for stuff like marking and warnings
    this.path = path || 'p0';

    this.full_path = (this.question ? 'q' + this.question.number : '') + this.path;

    this.name = util.capitalise(util.nicePartName(path));

    this.label = '';

    if(this.question) {
        this.question.partDictionary[path] = this;
    }
    //initialise settings object
    this.settings = util.copyobj(Part.prototype.settings);

    //initialise gap and step arrays
    this.gaps = [];
    this.steps = [];
    this.alternatives = [];
    this.isStep = this.path.match(/s\d+$/) !== null;
    this.isGap = this.path.match(/g\d+$/) !== null;
    this.settings.errorCarriedForwardReplacements = [];
    this.errorCarriedForwardBackReferences = {};

    this.nextParts = [];

    this.pre_submit_cache = [];
    this.markingFeedback = [];
    this.finalised_result = {valid: false, credit: 0, states: []};
    this.warnings = [];
    this.scripts = {};

    this.save_staged_answer_debounce = Numbas.util.debounce(SAVE_STAGED_ANSWER_FREQUENCY);

    Object.defineProperty(this, "credit", {
        /** Proportion of available marks awarded to the student - i.e. `score/marks`. Penalties will affect this instead of the raw score, because of things like the steps marking algorithm.
         *
         * @type {number}
         * @returns {number}
         */
        get: function() {
            return this.creditFraction.toFloat();
        },
        set: function(credit) {
            this.creditFraction = math.Fraction.fromFloat(credit);
        }
    });
}
Part.prototype = /** @lends Numbas.parts.Part.prototype */ {
    /** Signals produced while loading this part.
     *
     * @type {Numbas.schedule.SignalBox}
     */
    signals: undefined,
    /** Storage engine.
     *
     * @type {Numbas.storage.BlankStorage}
     */
    store: undefined,
    /** XML defining this part.
     *
     * @type {Element}
     */
    xml: '',
    /** JSON defining this part.
     *
     * @type {object}
     */
    json: null,
    /** Load the part's settings from an XML `<part>` node.
     *
     * @param {Element} xml
     */
    loadFromXML: function(xml) {
        this.xml = xml;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        tryGetAttribute(this, this.xml, '.', ['type', 'marks', 'useCustomName', 'customName']);
        tryGetAttribute(this.settings, this.xml, '.', ['minimumMarks', 'enableMinimumMarks', 'stepsPenalty', 'showCorrectAnswer', 'showFeedbackIcon', 'exploreObjective', 'suggestGoingBack', 'useAlternativeFeedback'], []);
        //load steps
        var stepNodes = this.xml.selectNodes('steps/part');
        if(!this.question || !this.question.exam || this.question.exam.settings.allowSteps) {
            for(let i = 0; i < stepNodes.length; i++) {
                var step = Numbas.createPartFromXML(i, stepNodes[i], this.path + 's' + i, this.question, this, this.store);
                this.addStep(step, i);
            }
        } else {
            for(let i = 0; i < stepNodes.length; i++) {
                stepNodes[i].parentElement.removeChild(stepNodes[i]);
            }
        }
        var alternativeNodes = this.xml.selectNodes('alternatives/part');
        for(let i = 0; i < alternativeNodes.length; i++) {
            var alternative = Numbas.createPartFromXML(i, alternativeNodes[i], this.path + 'a' + i, this.question, this, this.store);
            this.addAlternative(alternative, i);
        }
        var alternativeFeedbackMessageNode = this.xml.selectSingleNode('alternativefeedbackmessage');
        if(alternativeFeedbackMessageNode) {
            this.alternativeFeedbackMessage = Numbas.xml.transform(Numbas.xml.templates.question, alternativeFeedbackMessageNode);
        }
        // set variable replacements
        var adaptiveMarkingNode = this.xml.selectSingleNode('adaptivemarking');
        tryGetAttribute(this.settings, this.xml, adaptiveMarkingNode, ['penalty', 'strategy'], ['adaptiveMarkingPenalty', 'variableReplacementStrategy']);
        var variableReplacementsNode = this.xml.selectSingleNode('adaptivemarking/variablereplacements');
        var replacementNodes = variableReplacementsNode.selectNodes('replace');
        for(let i = 0;i < replacementNodes.length;i++) {
            var n = replacementNodes[i];
            var vr = {}
            tryGetAttribute(vr, n, '.', ['variable', 'part', 'must_go_first']);
            this.addVariableReplacement(vr.variable, vr.part, vr.must_go_first);
        }

        var nextPartsNode = this.xml.selectSingleNode('nextparts');
        var nextPartNodes = nextPartsNode.selectNodes('nextpart');
        for(let i = 0;i < nextPartNodes.length;i++) {
            var nextPartNode = nextPartNodes[i];
            var np = new NextPart(this);
            np.loadFromXML(nextPartNode);
            this.nextParts.push(np);
        }

        // create the JME marking script for the part
        var markingScriptNode = this.xml.selectSingleNode('markingalgorithm');
        var markingScriptString = Numbas.xml.getTextContent(markingScriptNode).trim();
        var markingScript = {};
        tryGetAttribute(markingScript, this.xml, markingScriptNode, ['extend']);
        var extend_base = markingScript.extend;
        this.setMarkingScript(markingScriptString, extend_base);

        // custom JavaScript scripts
        var scriptNodes = this.xml.selectNodes('scripts/script');
        for(let i = 0;i < scriptNodes.length; i++) {
            var name = scriptNodes[i].getAttribute('name');
            var order = scriptNodes[i].getAttribute('order');
            var script = Numbas.xml.getTextContent(scriptNodes[i]);
            this.setScript(name, order, script);
        }
    },
    /** Load the part's settings from a JSON object.
     *
     * @param {object} data
     */
    loadFromJSON: function(data) {
        this.json = data;
        var p = this;
        var tryLoad = Numbas.json.tryLoad;
        var tryGet = Numbas.json.tryGet;
        tryLoad(data, ['marks', 'useCustomName', 'customName'], this);
        this.marks = parseFloat(this.marks);
        tryLoad(data, ['showCorrectAnswer', 'showFeedbackIcon', 'stepsPenalty', 'variableReplacementStrategy', 'adaptiveMarkingPenalty', 'exploreObjective', 'suggestGoingBack', 'useAlternativeFeedback'], this.settings);
        var variableReplacements = tryGet(data, 'variableReplacements');
        if(variableReplacements) {
            variableReplacements.map(function(vr) {
                p.addVariableReplacement(vr.variable, vr.part, vr.must_go_first);
            });
        }
        if('steps' in data) {
            data.steps.map(function(sd, i) {
                var s = createPartFromJSON(i, sd, p.path + 's' + i, p.question, p, p.store);
                p.addStep(s, i);
            });
        }
        var alternatives = tryGet(data, 'alternatives');
        if(alternatives) {
            alternatives.forEach(function(ad, i) {
                var alternative = Numbas.createPartFromJSON(i, ad, p.path + 'a' + i, p.question, p, p.store);
                p.addAlternative(alternative, i);
            });
        }
        tryLoad(data, 'alternativeFeedbackMessage', this);
        var marking = {};
        tryLoad(data, ['customMarkingAlgorithm', 'extendBaseMarkingAlgorithm'], marking);
        this.setMarkingScript(marking.customMarkingAlgorithm, marking.extendBaseMarkingAlgorithm);
        if('scripts' in data) {
            for(const [name, script] of Object.entries(data.scripts)) {
                this.setScript(name, script.order, script.script);
            }
        }
        var nextParts = tryGet(data, 'nextParts');
        if(nextParts) {
            nextParts.forEach(function(npdata) {
                var np = new NextPart(p);
                np.loadFromJSON(npdata);
                p.nextParts.push(np);
            });
        }
    },
    /** Perform any tidying up or processing that needs to happen once the part's definition has been loaded.
     */
    finaliseLoad: function() {
        this.marks = this.marks || 0;
        this.applyScripts();
        if(this.customConstructor) {
            this.customConstructor.apply(this);
        }
        var scope = this.getScope();
        this.nextParts.forEach(function(np) {
            if(np.penaltyAmountString != '') {
                np.penaltyAmount = np.penalty ? scope.evaluate(np.penaltyAmountString).value : 0;
            }
        });
    },

    /** All children of this part: all gaps and steps, but not alternatives.
     *
     * @returns {Array.<Numbas.jme.parts.Part>}
     */
    allChildren: function() {
        return this.gaps.concat(this.steps);
    },

    /** Initialise this part's display object.
     * Only called if the question this part belongs to has a display.
     */
    initDisplay: function() {
        this.display = new Numbas.display.PartDisplay(this);
    },
    /** Load saved data about this part from storage.
     * The part is not resubmitted - you must do this afterwards, once any steps or gaps have been resumed.
     *
     * @fires Numbas.Part#event:resume
     */
    resume: function() {
        this.resuming = true;
        var part = this;
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        this.answered = pobj.answered;
        this.stepsShown = pobj.stepsShown;
        this.stepsOpen = pobj.stepsOpen;
        this.resume_stagedAnswer = pobj.stagedAnswer;
        this.steps.forEach(function(s) {
            s.resume()
        });
        this.pre_submit_cache = pobj.pre_submit_cache;
        this.alternatives.forEach(function(alt, i) {
            var aobj = pobj.alternatives[i];
            if(!aobj) {
                return;
            }
            alt.pre_submit_cache = aobj.pre_submit_cache
        });

        this.display && this.display.updateNextParts();
        this.display && this.question && this.question.signals.on(['ready', 'HTMLAttached'], function() {
            part.display.restoreAnswer(part.resume_stagedAnswer !== undefined ? part.resume_stagedAnswer : part.studentAnswer);
        })
        this.signals.trigger('resume');
        this.resuming = false;
    },
    /** Add a step to this part.
     *
     * @param {Numbas.parts.Part} step
     * @param {number} index - Position of the step.
     * @fires Numbas.Part#event:addStep
     */
    addStep: function(step, index) {
        step.isStep = true;
        this.steps.splice(index, 0, step);
        this.stepsMarks += step.marks;
        this.events.trigger('addStep', step, index);
    },
    /** Add an alternative to this part.
     *
     * @param {Numbas.parts.Part} alternative
     * @param {number} index - Position of the alternative.
     * @fires Numbas.Part#event:addAlternative
     */
    addAlternative: function(alternative, index) {
        alternative.isAlternative = true;
        this.alternatives.splice(index, 0, alternative);
        this.events.trigger('addAlternative', alternative, index);
    },

    /** A definition of a variable replacement for adaptive marking.
     *
     * @typedef Numbas.parts.adaptive_variable_replacement_definition
     * @property {string} variable - The name of the variable to replace.
     * @property {string} part - The path of the part to use.
     * @property {boolean} must_go_first - Must the referred part be answered before this part can be marked?
     */

    /** Add a variable replacement for this part's adaptive marking.
     *
     * @param {string} variable - The name of the variable to replace.
     * @param {string} part - The path of the part to use.
     * @param {boolean} must_go_first - Must the referred part be answered before this part can be marked?
     * @fires Numbas.Part#event:addVariableReplacement
     */
    addVariableReplacement: function(variable, part, must_go_first) {
        if(part == this.path) {
            this.error("part.marking.adaptive variable replacement refers to self");
        }
        if(!part) {
            this.error("part.marking.adaptive variable replacement refers to nothing");
        }
        var vr = {
            variable: jme.normaliseName(variable, this.getScope()),
            part: part,
            must_go_first: must_go_first
        };
        this.settings.hasVariableReplacements = true;
        this.settings.errorCarriedForwardReplacements.push(vr);
        this.events.trigger('addVariableReplacement', variable, part);
    },
    /** The base marking script for this part.
     *
     * @abstract
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {},
    /** Set this part's JME marking script.
     *
     * @param {string} markingScriptString
     * @param {boolean} extend_base - Does this script extend the built-in script?
     */
    setMarkingScript: function(markingScriptString, extend_base) {
        if(!this.doesMarking) {
            return;
        }

        var p = this;

        var algo = this.baseMarkingScript();
        if(markingScriptString) {
            algo = new marking.MarkingScript(markingScriptString, extend_base ? algo : undefined, this.getScope());
        }
        this.markingScript = algo;

        // check that the required notes are present
        var requiredNotes = ['mark', 'interpreted_answer'];
        requiredNotes.forEach(function(name) {
            if(!(name in algo.notes)) {
                p.error("part.marking.missing required note", {note:name});
            }
        });
    },
    /** Set a custom JavaScript script.
     *
     * @param {string} name - The name of the method to override.
     * @param {string} order - When should the script run? `'instead'`, `'before'` or `'after'`.
     * @param {string} script - The source code of the script.
     * @see {Numbas.parts.Part#applyScripts}
     */
    setScript: function(name, order, script) {
        var p = this;

        if(name == 'mark') {
            // hack on a finalised_state for old marking scripts
            script = 'var res = (function(studentAnswer,scope) {' + script + '\n}).apply(this,arguments); \
this.answered = true; \
if(res) { \
    return res; \
} else {\
    res = { \
        states: {mark: this.markingFeedback.slice()}, \
        values: {interpreted_answer: Numbas.jme.wrapValue(arguments[0])}, \
        state_valid: {mark: true, interpreted_answer: true}, \
        state_errors: {}, \
        added_because_missing: true \
    }; \
    this.markingFeedback = []; \
    this.credit = 0; \
    return res; \
} \
';
            name = 'mark_answer';
        }
        var fn = new Function(['variables', 'question', 'part'], 'return (function(){try{' + script + '\n}catch(e){e = new Numbas.Error(\'part.script.error\',{path:this.name,script:this.name,message:e.message}); throw(e);}})');
        script = function() {
            return fn(
                p.question ? p.question.unwrappedVariables : {},
                p.question,
                p
            ).apply(this, arguments);
        }
        this.scripts[name] = {script, order};
    },
    /** The question this part belongs to.
     *
     * @type {Numbas.Question}
     */
    question: undefined,
    /** Reference to parent of this part, if this is a gap or a step.
     *
     * @type {Numbas.parts.Part}
     */
    parentPart: undefined,
    /** A question-wide unique 'address' for this part.
     *
     * @type {Numbas.parts.partpath}
     */
    path: '',
    /** A readable name for this part, to show to the student.
     * Change it with {@link Numbas.parts.Part#setName}.
     *
     * @type {string}
     */
    name: '',
    /** Should a custom name be used?
     *
     * @type {boolean}
     */
    useCustomName: false,
    /** Custom name for this part, or null if none.
     * Variables will be substituted into this string from the part's scope.
     *
     * @type {string}
     */
    customName: '',
    /** Assign a name to this part, and then assign names to its children.
     *
     * @param {number} index - The number of parts before this one that have names.
     * @param {number} siblings - The number of siblings this part has.
     * @returns {boolean} `true` if this part has a name that should increment the label counter.
     * @fires Numbas.Part#event:assignName
     */
    assignName: function(index, siblings) {
        if(this.useCustomName) {
            this.name = jme.contentsubvars(this.customName, this.getScope(), false);
        } else if(this.isGap) {
            this.name = util.capitalise(R('gap')) + ' ' + index;
        } else if(this.isStep && siblings > 0) {
            this.name = util.capitalise(R('step')) + ' ' + index;
        } else if(siblings == 0) {
            this.name = '';
        } else {
            this.name = util.letterOrdinal(index) + ')';
        }

        /** Assign names to the given child parts.
         *
         * @param {Array.<Numbas.parts.Part>} children
         */
        function assign_child_names(children) {
            if(!children) {
                return;
            }
            var i = 0;
            children.forEach(function(c) {
                var hasName = c.assignName(i, children.length - 1);
                i += hasName ? 1 : 0;
            });
        }

        assign_child_names(this.gaps);
        assign_child_names(this.steps);
        assign_child_names(this.alternatives);

        this.display && this.display.setName(this.name);
        this.events.trigger('assignName', index, siblings);
        return this.name != '';
    },
    /** This part's type, e.g. "jme", "numberentry", ...
     *
     * @type {string}
     */
    type: '',
    /** Maximum marks available for this part.
     *
     * @type {number}
     */
    marks: 0,
    /** Marks available for the steps, if any.
     *
     * @type {number}
     */
    stepsMarks: 0,
    /** Credit as a fraction. Used to avoid simple floating point errors.
     *
     * @type {Numbas.math.Fraction}
     */
    creditFraction: new math.Fraction(0, 1),
    /** Student's score on this part.
     *
     * @type {number}
     */
    score: 0,
    /** Messages explaining how marks were awarded.
     *
     * @type {Array.<Numbas.parts.feedbackmessage>}
     */
    markingFeedback: [],
    /** The result of the last marking run.
     *
     * @type {Numbas.marking.finalised_state}
     */
    finalised_result: {valid: false, credit: 0, states: []},
    /** Warnings shown next to the student's answer.
     *
     * @type {Array.<string>}
     */
    warnings: [],
    /** Has the student changed their answer since last submitting?
     *
     * @type {boolean}
     */
    isDirty: false,
    /** Student's answers as visible on the screen (not necessarily yet submitted).
     *
     * @type {Array.<string>}
     */
    stagedAnswer: undefined,

    /** Has this part been answered?
     *
     * @type {boolean}
     */
    answered: false,

    /** Child gapfill parts.
     *
     * @type {Numbas.parts.Part[]}
     */
    gaps: [],
    /** Child step parts.
     *
     * @type {Numbas.parts.Part[]}
     */
    steps: [],
    /** Child alternative parts.
     *
     * @type {Numbas.parts.Part[]}
     */
    alternatives: [],
    /** Feedback message shown if this part is used as an alternative.
     *
     * @type {string}
     */
    alternativeFeedbackMessage: '',
    /** Have the steps been show for this part?
     *
     * @type {boolean}
     */
    stepsShown: false,
    /** Is the steps display open?
     *
     * @type {boolean}
     */
    stepsOpen: false,
    /** True if this part should be resubmitted because another part it depended on has changed.
     *
     * @type {boolean}
     */
    shouldResubmit: false,
    /** Does this mark do any marking? False for information only parts.
     *
     * @type {boolean}
     */
    doesMarking: true,
    /** Has the answer to this part been revealed?
     *
     * @type {boolean}
     */
    revealed: false,
    /** Is this part locked? If false, the student can change and submit their answer.
     *
     * @type {boolean}
     */
    locked: false,
    /** Properties set when the part is generated.
     *
     * @type {object}
     * @property {number} stepsPenalty - Number of marks to deduct when the steps are shown.
     * @property {boolean} enableMinimumMarks - Is there a lower limit on the score the student can be awarded for this part?
     * @property {number} minimumMarks - Lower limit on the score the student can be awarded for this part.
     * @property {boolean} showCorrectAnswer - Show the correct answer on reveal?
     * @property {boolean} showFeedbackIcon - Show the tick/cross feedback symbol after this part is submitted?
     * @property {boolean} hasVariableReplacements - Does this part have any variable replacement rules?
     * @property {string} variableReplacementStrategy - `'originalfirst'` or `'alwaysreplace'`.
     * @property {string} exploreObjective - Name of the objective that this part's score counts towards.
     * @property {string} suggestGoingBack - In explore mode, suggest to the student to go back to the previous part after completing this one?
     * @property {number} adaptiveMarkingPenalty - Number of marks to deduct when adaptive marking is used.
     * @property {boolean} useAlternativeFeedback - Show all feedback from an alternative answer? If false, only the alternative feedback message is shown.
     * @property {Array.<Numbas.parts.adaptive_variable_replacement_definition>} errorCarriedForwardReplacements - Variable replacements to make during adaptive marking.
     */
    settings:
    {
        stepsPenalty: 0,
        enableMinimumMarks: true,
        minimumMarks: 0,
        showCorrectAnswer: true,
        showFeedbackIcon: true,
        hasVariableReplacements: false,
        variableReplacementStrategy: 'originalfirst',
        exploreObjective: null,
        suggestGoingBack: false,
        adaptiveMarkingPenalty: 0,
        useAlternativeFeedback: false,
        errorCarriedForwardReplacements: []
    },

    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @type {Numbas.marking.MarkingScript}
     */
    markingScript: null,

    /** Throw an error, with the part's identifier prepended to the message.
     *
     * @param {string} message
     * @param {object} args - Arguments for the error message.
     * @param {Error} [originalError] - If this is a re-thrown error, the original error object.
     * @fires Numbas.Part#event:error
     * @throws {Numbas.Error}
     */
    error: function(message, args, originalError) {
        if(originalError && originalError.originalMessages && originalError.originalMessages[0] == 'part.error') {
            throw(originalError);
        }
        var nmessage = R.apply(this, [message, args]);
        if(nmessage != message) {
            originalError = new Error(nmessage);
            originalError.originalMessages = [message].concat(originalError.originalMessages || []);
        }
        var niceName = this.name;
        this.events.trigger('error', message, args, originalError);
        throw(new Numbas.Error('part.error', {path: niceName, message: nmessage}, originalError));
    },
    /** The name of the input widget this part uses, if any.
     *
     * @returns {string}
     */
    input_widget: function() {
        return null;
    },
    /** Options for this part's input widget.
     *
     * @returns {object}
     */
    input_options: function() {
        return {};
    },
    applyScripts: function() {
        var part = this;
        this.originalScripts = {};

        /** Create a function which runs `script` (instead of the built-in script).
         *
         * @param {Function} script
         * @returns {Function}
         */
        function instead(script) {
            return function() {
                return script.apply(part, arguments);
            }
        }
        /** Create a function which runs `script` before `originalScript`.
         *
         * @param {Function} script
         * @param {Function} originalScript
         * @returns {Function}
         */
        function before(script, originalScript) {
            return function() {
                script.apply(part, arguments);
                return originalScript.apply(part, arguments);
            }
        }
        /** Create a function which runs `script` after `originalScript`.
         *
         * @param {Function} script
         * @param {Function} originalScript
         * @returns {Function}
         */
        function after(script, originalScript) {
            return function() {
                var original_result = originalScript.apply(part, arguments);
                if(original_result.waiting_for_pre_submit) {
                    return original_result;
                }
                var after_result = script.apply(part, arguments);
                if(!after_result) {
                    return original_result;
                }
                if(after_result.added_because_missing && after_result.states && after_result.states.mark) {
                    after_result.states.mark = original_result.states.mark.concat(after_result.states.mark);
                }
                return after_result;
            }
        }

        for(const [name, script_dict] of Object.entries(this.scripts)) {
            var order = script_dict.order;
            var script = script_dict.script;
            switch(name) {
                case 'constructor':
                    this.customConstructor = script;
                    break;
                default:
                    var originalScript = this[name];
                    switch(order) {
                        case 'instead':
                            this[name] = instead(script);
                            break;
                        case 'before':
                            this[name] = before(script, originalScript);
                            break;
                        case 'after':
                            this[name] = after(script, originalScript);
                            break;
                    }
            }
        }
    },
    /** Associated display object. It is not safe to assume this is always present - in the editor, parts have no display.
     *
     * @type {Numbas.display.PartDisplay}
     */
    display: undefined,
    /** Give the student a warning about this part.
     *
     * @param {string} warning
     * @fires Numbas.Part#event:giveWarning
     * @see Numbas.display.PartDisplay.warning
     */
    giveWarning: function(warning) {
        this.warnings.push(warning);
        this.display && this.display.warning(warning);
        this.events.trigger('giveWarning', warning);
    },
    /** Set the list of warnings.
     *
     * @param {Array.<string>} warnings
     * @see Numbas.display.PartDisplay.warning
     */
    setWarnings: function(warnings) {
        this.warnings = warnings;
        this.display && this.display.setWarnings(warnings);
    },
    /** Remove all warnings.
     *
     * @see Numbas.display.PartDisplay.warning
     */
    removeWarnings: function() {
        this.setWarnings([]);
    },

    /** The total marks available for this part, after applying adaptive marking and steps penalties.
     *
     * @returns {number}
     */
    availableMarks: function() {
        var marks = this.marks;
        if(this.adaptiveMarkingUsed) {
            marks -= this.settings.adaptiveMarkingPenalty;
        }
        var stepsPart = this.isGap ? this.parentPart : this;
        if(stepsPart.steps.length && stepsPart.stepsShown) {
            var stepsPenalty = stepsPart.settings.stepsPenalty;
            if(this.isGap && this.parentPart.marks > 0) {
                stepsPenalty *= this.marks / this.parentPart.marks;
            }
            marks -= stepsPenalty;
        }
        marks = Math.max(Math.min(this.marks, marks), 0);
        return marks;
    },

    /** Calculate the student's score based on their submitted answers.
     *
     * Calls the parent part's `calculateScore` method at the end.
     *
     * @fires Numbas.Part#event:calculateScore
     */
    calculateScore: function() {
        var marks = this.availableMarks();
        if(this.steps.length && this.stepsShown) {
            var oScore = this.score = marks * this.credit;     //score for main keypart
            var stepsScore = 0;
            var stepsMarks = 0;
            for(let i = 0; i < this.steps.length; i++) {
                stepsScore += this.steps[i].score;
                stepsMarks += this.steps[i].marks;
            }
            this.score += stepsScore;                        //add score from steps to total score
            this.score = Math.min(this.score, marks)    //if too many marks are awarded for steps, it's possible that getting all the steps right leads to a higher score than just getting the part right. Clip the score to avoid this.
            this.applyScoreLimits();
            if(stepsMarks != 0 && stepsScore != 0) {
                if(this.credit == 1) {
                    this.markingComment(R('part.marking.steps no matter'));
                } else {
                    var change = this.score - oScore;
                    if(this.submitting) {
                        this.markingComment(R('part.marking.steps change', {count:change}));
                    }
                }
            }
        } else {
            this.score = this.credit * marks;
            this.applyScoreLimits();
        }
        if(this.revealed) {
            this.score = 0;
        }
        if(this.parentPart && !this.parentPart.submitting) {
            this.parentPart.calculateScore();
        }
        this.events.trigger('calculateScore');
        this.display && this.display.showScore(this.answered);
    },

    /** Make sure the awarded score is between the minimum and maximum available.
     */
    applyScoreLimits: function() {
        var marks = this.availableMarks();
        if(this.settings.enableMinimumMarks && this.score < this.settings.minimumMarks) {
            this.score = this.settings.minimumMarks;
            this.creditFraction = marks != 0 ? math.Fraction.fromFloat(this.settings.minimumMarks, marks) : 0;
            this.markingComment(R('part.marking.minimum score applied', {score:this.settings.minimumMarks}));
        }
        if(this.score > marks) {
            this.finalised_result.states.push(Numbas.marking.feedback.sub_credit(this.credit - 1, R('part.marking.maximum score applied', {score:marks})));
            this.score = marks;
            this.creditFraction = math.Fraction.one;
            this.markingComment(R('part.marking.maximum score applied', {score:marks}));
        }
    },

    /** Update the stored answer from the student (called when the student changes their answer, but before submitting).
     *
     * @param {*} answer
     * @param {boolean} dontStore - Don't tell the storage that this is happening - use when loading from storage to avoid callback loops.
     * @fires Numbas.Part#event:storeAnswer
     * @see {Numbas.parts.Part.stagedAnswer}
     */
    storeAnswer: function(answer, dontStore) {
        var p = this;

        this.stagedAnswer = answer;
        this.setDirty(true);
        this.removeWarnings();
        this.display && this.display.showWarnings();

        if(!dontStore) {
            if(!this.question || !this.question.exam || !this.question.exam.loading) {
                this.store && this.save_staged_answer_debounce(function() {
                    p.store.storeStagedAnswer(p);
                })
            }
            this.events.trigger('storeAnswer', answer, dontStore);
        }
    },
    /** Call when the student changes their answer, or submits - update {@link Numbas.parts.Part.isDirty}.
     *
     * @param {boolean} dirty
     * @fires Numbas.Part#event:setDirty
     */
    setDirty: function(dirty) {
        this.isDirty = dirty;
        if(this.display) {
            this.display && this.display.isDirty(dirty);
            if(dirty && this.parentPart && !this.isStep && !this.parentPart.submitting) {
                this.parentPart.setDirty(true);
            }
            this.question && this.question.display && this.question.display.isDirty(this.question.isDirty());
        }
        this.events.trigger('setDirty', dirty);
    },
    /** Get a JME scope for this part.
     * If `this.question` is set, use the question's scope. Otherwise, use {@link Numbas.jme.builtinScope}.
     *
     * @returns {Numbas.jme.Scope}
     */
    getScope: function() {
        if(!this.scope) {
            this.scope = this.makeScope();
        }
        return this.scope;
    },

    /** Make the scope for this part.
     *
     * @param {Numbas.jme.Scope} [parentScope] - An optional parent scope. If not given, the following are tried: a parent part, the question this part belongs to, `Numbas.jme.builtinScope`.
     * @fires Numbas.Part#event:makeScope
     * @returns {Numbas.jme.Scope}
     */
    makeScope: function(parentScope) {
        if(!parentScope) {
            if(this.parentPart) {
                parentScope = this.parentPart.getScope();
            } else if(this.question) {
                parentScope = this.question.scope;
            } else {
                parentScope = new Numbas.jme.Scope(Numbas.jme.builtinScope);
            }
        }
        var scope = new Numbas.jme.Scope([parentScope]);
        scope.setVariable('part_path', new Numbas.jme.types.TString(this.path));
        scope.part = this;
        this.events && this.events.trigger('makeScope');
        return scope;
    },

    /** Mark this part, using adaptive marking when appropriate.
     *
     * @fires Numbas.Part#event:pre-markAdaptive
     * @fires Numbas.Part#event:post-markAdaptive
     * @returns {Numbas.parts.marking_results}
     */
    markAdaptive: function() {
        this.events.trigger('pre-markAdaptive');

        if(!this.doesMarking) {
            return undefined;
        }
        this.setStudentAnswer();

        // save existing feedback
        var existing_feedback = {
            warnings: this.warnings.slice(),
            markingFeedback: this.markingFeedback.slice()
        };

        var settings = this.isAlternative ? this.parentPart.settings : this.settings;

        var result;
        var try_replacement;
        var hasReplacements = this.getErrorCarriedForwardReplacements().length > 0;
        if(settings.variableReplacementStrategy == 'originalfirst' || !hasReplacements) {
            var result_original = this.markAgainstScope(this.getScope(), existing_feedback, '');
            if(result_original.waiting_for_pre_submit) {
                return result_original;
            }
            result = result_original;
            try_replacement = hasReplacements && (!result.answered || result.credit < 1);
        }
        if(settings.variableReplacementStrategy == 'alwaysreplace' && hasReplacements) {
            try_replacement = true;
        }
        if((!this.question || this.question.partsMode != 'explore') && try_replacement) {
            try {
                var scope = this.errorCarriedForwardScope();
                var result_replacement = this.markAgainstScope(scope, existing_feedback, 'adaptive ');
                if(result_replacement.waiting_for_pre_submit) {
                    return result_replacement;
                }
                if(!(result_original) || (result_replacement.answered && result_replacement.credit > result_original.credit)) {
                    result = result_replacement;
                    result.finalised_result.states.splice(0, 0, Numbas.marking.feedback.feedback(R('part.marking.used variable replacements')));
                    result.adaptiveMarkingUsed = true;
                }
            } catch(e) {
                if(e.originalMessage == 'part.marking.variable replacement part not answered') {
                    this.markingComment(e.message);
                    const errorFeedback = [
                        Numbas.marking.feedback.feedback(e.message)
                    ];
                    if(!result) {
                        result = {
                            warnings: [],
                            markingFeedback: errorFeedback,
                            finalised_result: {
                                valid: false,
                                credit: 0,
                                states: errorFeedback
                            },
                            values: {},
                            credit: 0,
                            script_result: {
                                state_errors: {
                                    mark: e
                                }
                            }
                        }
                    }
                    result.warnings.push(e.message);
                } else {
                    try{
                        this.error(e.message, {}, e);
                    } catch(pe) {
                        console.error(pe.message);
                        const errorFeedback = [
                            Numbas.marking.feedback.feedback(R('part.marking.error in adaptive marking', {message: e.message}))
                        ];
                        if(!result) {
                            result = {
                                warnings: [],
                                markingFeedback: errorFeedback,
                                finalised_result: {
                                    valid: false,
                                    credit: 0,
                                    states: errorFeedback
                                },
                                values: {},
                                credit: 0,
                                script_result: {
                                    state_errors: {
                                        mark: pe
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        this.events.trigger('post-markAdaptive');
        return result;
    },

    /** Wait for a promise to resolve before submitting.
     *
     * @param {Promise} promise
     * @fires Numbas.Part#event:waiting_for_pre_submit
     * @fires Numbas.Part#event:completed_pre_submit
     */
    wait_for_pre_submit: function(promise) {
        var p = this;
        this.waiting_for_pre_submit = promise;
        if(this.display) {
            this.display.waiting_for_pre_submit(true);
        }
        this.events.trigger('waiting_for_pre_submit');
        promise.then(function() {
            p.waiting_for_pre_submit = false;
            p.submit();
            if(p.display) {
                p.display.waiting_for_pre_submit(false);
            }
            p.events.trigger('completed_pre_submit');
        });
    },

    /** Submit the student's answers to this part - remove warnings. save answer, calculate marks, update scores.
     *
     * @fires Numbas.Part#event:pre-submit
     * @fires Numbas.Part#event:post-submit
     */
    submit: function() {
        this.events.trigger('pre-submit');
        var p = this;
        this.shouldResubmit = false;

        this.credit = 0;
        this.markingFeedback = [];
        this.finalised_result = {valid: false, credit: 0, states: []};

        if(this.waiting_for_pre_submit) {
            return;
        }

        if(this.question && this.question.partsMode == 'explore') {
            if(!this.resuming) {
                this.nextParts.forEach(function(np) {
                    if(np.instance !== null && np.usesStudentAnswer()) {
                        p.removeNextPart(np);
                    }
                });
            }
            if(this.settings.exploreObjective) {
                this.markingComment(
                    R('part.marking.counts towards objective', {objective: this.settings.exploreObjective})
                );
            }
        }

        this.submitting = true;
        if(this.parentPart && !this.parentPart.submitting) {
            this.parentPart.setDirty(true);
        }
        this.removeWarnings();
        if(this.hasStagedAnswer()) {
            this.setDirty(false);

            try {
                var result = this.markAdaptive();
            } catch(e) {
                this.error('part.marking.uncaught error', {message:e.message}, e);
            }
            if(!result) {
                this.setCredit(0, R('part.marking.no result after replacement'));
                this.answered = true;
            } else if(result.waiting_for_pre_submit) {
                this.wait_for_pre_submit(result.waiting_for_pre_submit);
                return;
            } else {
                this.setWarnings(result.warnings);
                this.markingFeedback = result.markingFeedback.slice();
                this.finalised_result = result.finalised_result;
                this.adaptiveMarkingUsed = result.adaptiveMarkingUsed;
                this.best_alternative = result.best_alternative;
                this.script_result = result.script_result;
                this.marking_values = result.values;
                this.credit = result.credit;
                this.answered = result.answered;
            }
        } else {
            this.submit_no_staged_answer();
            this.setCredit(0, R('part.marking.did not answer'));;
            this.answered = false;
        }
        if(this.stepsShown) {
            var steps_waiting_for_pre_submit = [];
            this.steps.forEach(function(step) {
                if(step.isDirty) {
                    step.submit();
                    if(step.waiting_for_pre_submit) {
                        steps_waiting_for_pre_submit.push(step.waiting_for_pre_submit);
                    }
                }
            });
            if(steps_waiting_for_pre_submit.length > 0) {
                this.wait_for_pre_submit(Promise.all(steps_waiting_for_pre_submit));
                return;
            }
        }
        var availableMarks = this.availableMarks();
        if(availableMarks < this.marks) {
            this.markingFeedback.splice(0, 0, {op: 'feedback', message: R('part.marking.maximum scaled down', {count: availableMarks})});
        }
        if(this.stepsShown) {
            this.markingFeedback.splice(0, 0, {op: 'feedback', message: R('part.marking.revealed steps')});
        }
        if(this.adaptiveMarkingUsed && this.settings.adaptiveMarkingPenalty > 0) {
            this.markingFeedback.splice(0, 0, {op: 'feedback', message: R('part.marking.used variable replacements')});
        }
        this.calculateScore();

        this.marking_result = {
            warnings: this.warnings.slice(),
            markingFeedback: this.markingFeedback.slice(),
            finalised_result: this.finalised_result,
            credit: this.credit,
            answered: this.answered
        };

        this.question && this.question.updateScore();
        if(this.answered) {
            if(this.display) {
                this.display.showScore(this.answered);
            }
        }
        if(this.display) {
            this.display.updateNextParts();
        }
        this.store && this.store.partAnswered(this);
        this.submitting = false;
        if(this.answered && this.question) {
            for(const path of Object.keys(this.errorCarriedForwardBackReferences)) {
                var p2 = this.question.getPart(path);
                if(p2.settings.variableReplacementStrategy == 'alwaysreplace') {
                    try {
                        var answer = p2.getCorrectAnswer(p2.errorCarriedForwardScope());
                        p2.display && p2.display.updateCorrectAnswer(answer);
                    } catch {
                    }
                }
                if(p2.answered) {
                    p2.pleaseResubmit();
                }
            }
        }
        this.events.trigger('post-submit');
    },

    /** Called when this part is submitted but it has no staged answer.
     */
    submit_no_staged_answer: function() {
        this.giveWarning(R('part.marking.not submitted'));
    },

    /** Has the student entered an answer to this part?
     *
     * @see Numbas.parts.Part#stagedAnswer
     * @returns {boolean}
     */
    hasStagedAnswer: function() {
        return !(this.stagedAnswer == undefined);
    },

    /** Called by another part when its marking means that the marking for this part might change (i.e., when this part replaces a variable with the answer from the other part).
     * Sets this part as dirty, and gives a warning explaining why the student must resubmit.
     */
    pleaseResubmit: function() {
        if(!this.shouldResubmit) {
            this.shouldResubmit = true;
            this.setDirty(true);
            this.giveWarning(R('part.marking.resubmit because of variable replacement'));
        }
    },

    /** @typedef {object} Numbas.parts.feedbackmessage
     * @property {string} op - The kind of feedback.
     * @see Numbas.parts.Part#setCredit Numbas.parts.Part#addCredit Numbas.parts.Part#multCredit Numbas.parts.Part#markingComment
     */

    /** @typedef {object} Numbas.parts.marking_results
     * A dictionary representing the results of marking a student's answer.
     *
     * @property {Array.<string>} warnings - Warning messages.
     * @property {Numbas.marking.finalised_state} finalised_result - A sequence of marking operations.
     * @property {Array.<Numbas.parts.feedbackmessage>} markingFeedback - Feedback messages to show to student, produced from `finalised_result`.
     * @property {Object<Numbas.jme.token>} values - The values of marking algorithm notes.
     * @property {number} credit - Proportion of the available marks to award to the student.
     * @property {boolean} answered - True if the student's answer could be marked. False if the answer was invalid - the student should change their answer and resubmit.
     */

    /** @typedef {object} Numbas.parts.alternative_result
     * A dictionary representing the result of marking the student's answer against a certain alternative version of the part and a given scope.
     *
     * @property {Numbas.marking.finalised_state} finalised_result - A sequence of marking operations.
     * @property {Object<Numbas.jme.token>} values - The values of marking algorithm notes.
     * @property {number} credit - Proportion of the available marks to award to the student.
     * @property {Numbas.marking.marking_script_result} script_result - The unprocessed result of the marking script.
     */

    /** @typedef {object} Numbas.parts.markAlternatives_result
     * A dictionary representing the results of the `markAlternatives` method.
     *
     * @property {Numbas.parts.alternative_result} result - The data produced by marking against the best alternative
     * @property {Numbas.parts.Part} best_alternative - The alternative which was used. Null if no alternative used.
     */

    /** Mark the student's answer against this part and its alternatives, and return the feedback corresponding to the alternative awarding the most credit.
     *
     * @param {Numbas.jme.Scope} scope - Scope in which to calculate the correct answer.
     * @param {Object<Array.<string>>} feedback - Dictionary of existing `warnings` and `markingFeedback` lists, to add to - copies of these are returned with any additional feedback appended.
     * @param {string} exec_path - A description of the path of execution, for caching pre-submit tasks.
     * @returns {Numbas.parts.markAlternatives_result}
     */
    markAlternatives: function(scope, feedback, exec_path) {
        var part = this;

        var alternatives_waiting = [];

        /** Mark against the given alternative.
         *
         * @param {Numbas.parts.Part} alt
         * @param {string} exec_path - A description of the path of execution, for caching pre-submit tasks.
         * @fires Numbas.Part#event:mark_alternative
         * @returns {Numbas.parts.alternative_result}
         */
        function mark_alternative(alt, exec_path) {
            part.events.trigger('mark_alternative', alt, exec_path);
            alt.restore_feedback(feedback);
            var values;
            var finalised_result = {states: [], valid: false, credit: 0};
            var script_result;
            try {
                var result = alt.mark(scope, exec_path);
                if(result.waiting_for_pre_submit) {
                    alternatives_waiting.push(result.waiting_for_pre_submit);
                    return result;
                }
                finalised_result = result.finalised_result;
                values = result.values;
                script_result = result.script_result
            } catch(e) {
                part.giveWarning(e.message);
                script_result = {
                    state_errors: {
                        mark: e
                    }
                };
            }
            return {finalised_result: finalised_result, values: values, credit: alt.credit, script_result: script_result};
        }

        var res = mark_alternative(this, exec_path);
        if(res.valid) {
            res.values['used_alternative'] = new Numbas.jme.types.TNothing()
            res.values['used_alternative_name'] = new Numbas.jme.types.TNothing();
        }

        if(this.alternatives.length) {
            var best_alternative = null;
            for(let i = 0;i < this.alternatives.length;i++) {
                var alt = this.alternatives[i];
                alt.stagedAnswer = this.stagedAnswer;
                alt.setStudentAnswer();
                var altres = mark_alternative(alt, exec_path + ' alternative ' + i + ' ');
                if(altres.waiting_for_pre_submit) {
                    continue;
                }
                if(!altres.finalised_result.valid) {
                    continue;
                }
                var scale = (this.marks == 0 ? 1 : alt.marks / this.marks);
                var scaled_credit = altres.credit * scale;
                if(altres.credit == 0) {
                    continue;
                }
                if(scaled_credit < res.credit) {
                    continue;
                }
                if(best_alternative && scaled_credit <= best_alternative.scaled_credit) {
                    continue;
                }
                altres.credit = scaled_credit;
                best_alternative = {
                    scale: scale,
                    scaled_credit: scaled_credit,
                    credit: altres.credit,
                    result: altres,
                    alternative: alt,
                    index: i
                }
            }
            if(best_alternative) {
                var alternative = best_alternative.alternative;
                res = best_alternative.result;
                var reason = best_alternative.scaled_credit == 1 ? 'correct' : best_alternative.scaled_credit == 0 ? 'incorrect' : '';
                var states = [
                    Numbas.marking.feedback.set_credit(best_alternative.scaled_credit, reason, alternative.alternativeFeedbackMessage)
                ];
                if(alternative.settings.useAlternativeFeedback) {
                    states = res.finalised_result.states.map(function(s) {
                        if(s.credit !== undefined) {
                            s.credit *= best_alternative.scale;
                        }
                        return s;
                    }).concat(states);
                }
                res.finalised_result = {
                    credit: best_alternative.scaled_credit,
                    states: states,
                    valid: true
                };
                this.restore_feedback(feedback);
                this.credit = 0;
                this.apply_feedback(res.finalised_result);
                this.warnings = best_alternative.alternative.warnings.slice();
                res.values['used_alternative'] = new Numbas.jme.types.TNum(best_alternative.index);
                res.values['used_alternative_name'] = new Numbas.jme.types.TString(alternative.name);
            }
        }

        if(alternatives_waiting.length > 0) {
            return {waiting_for_pre_submit: Promise.all(alternatives_waiting)};
        }

        if(res.valid) {
            res.script_result.states['used_alternative'] = [];
            res.script_result.states['used_alternative_name'] = [];
            res.script_result.state_valid['used_alternative'] = true;
            res.script_result.state_valid['used_alternative_name'] = true;
        }

        return {
            result: res,
            best_alternative: best_alternative ? best_alternative.alternative : null
        }
    },

    /** Mark the student's answer against the given scope.
     *
     * @param {Numbas.jme.Scope} scope - Scope in which to calculate the correct answer.
     * @param {Object<Array.<string>>} feedback - Dictionary of existing `warnings` and `markingFeedback` lists, to add to - copies of these are returned with any additional feedback appended.
     * @param {string} exec_path - A description of the path of execution, for caching pre-submit tasks.
     * @fires Numbas.Part#event:markAgainstScope
     * @returns {Numbas.parts.marking_results}
     */
    markAgainstScope: function(scope, feedback, exec_path) {
        this.events.trigger('markAgainstScope', scope, feedback, exec_path);
        var altres = this.markAlternatives(scope, feedback, exec_path);
        if(altres.waiting_for_pre_submit) {
            return altres;
        }
        var res = altres.result;
        if(res.script_result.state_errors.mark) {
            var message = res.script_result.state_errors.mark.message;
            this.markingComment(message);
            this.giveWarning(message);
        }

        return {
            warnings: this.warnings.slice(),
            markingFeedback: this.markingFeedback.slice(),
            best_alternative: altres.best_alternative,
            script_result: res.script_result,
            finalised_result: res.finalised_result,
            values: res.values,
            credit: this.credit,
            answered: this.answered
        }
    },

    /** Return the list of variable replacements to make for adaptive marking.
     * For alternatives, the parent part is used, otherwise this part is used.
     *
     * @returns {Array.<Numbas.parts.adaptive_variable_replacement_definition>}
     */
    getErrorCarriedForwardReplacements: function() {
        var replacements = this.settings.errorCarriedForwardReplacements;
        if(this.parentPart) {
            replacements = this.parentPart.getErrorCarriedForwardReplacements().concat(replacements);
        }
        return replacements;
    },

    /** Replace variables with student's answers to previous parts.
     *
     * @returns {Numbas.jme.Scope}
     */
    errorCarriedForwardScope: function() {
        // dictionary of variables to replace
        var replace = this.getErrorCarriedForwardReplacements();
        var replaced = [];
        if(!this.question) {
            return this.getScope();
        }
        // fill scope with new values of those variables
        var new_variables = {}
        for(let i = 0;i < replace.length;i++) {
            var vr = replace[i];
            var p2 = this.question.getPart(vr.part);
            if(p2.answered) {
                new_variables[vr.variable] = p2.studentAnswerAsJME();
                replaced.push(vr.variable);
            } else if(vr.must_go_first) {
                throw(new Numbas.Error("part.marking.variable replacement part not answered", {part: p2.name}));
            }
        }
        var scope = Numbas.jme.variables.remakeVariables(this.question.variablesTodo, new_variables, this.getScope());
        return scope;
    },
    /** Compute the correct answer, based on the given scope.
     * Anything to do with marking that depends on the scope should be in this method, and calling it with a new scope should update all the settings used by the marking algorithm.
     *
     * @param {Numbas.jme.Scope} scope
     * @abstract
     */
    getCorrectAnswer: function(scope) {},
    /** Save an answer entered by the student, for use in marking.
     *
     * @abstract
     */
    setStudentAnswer: function() {},
    /** Get the student's answer as it was entered as a JME data type, to be used in the marking script.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
    },
    /** Get the student's answer as a JME data type, to be used in error-carried-forward calculations.
     *
     * @returns {Numbas.jme.token}
     */
    studentAnswerAsJME: function() {
        return this.interpretedStudentAnswer;
    },

    /** @typedef {object} Numbas.parts.mark_result
     * A dictionary representing the results of marking a student's answer against a given scope, without considering alternatives.
     *
     * @property {Numbas.marking.finalised_state} finalised_result - A sequence of marking operations.
     * @property {Object<Numbas.jme.token>} values - The values of marking algorithm notes.
     * @property {Numbas.marking.marking_script_result} script_result - The unprocessed result of the marking script.
     */

    /** Function which marks the student's answer: run `this.settings.markingScript`, which sets the credit for the student's answer to a number between 0 and 1 and produces a list of feedback messages and warnings.
     * If the question has been answered in a way that can be marked, `this.answered` should be set to `true`.
     *
     * @see Numbas.parts.Part#markingScript
     * @see Numbas.parts.Part#answered
     * @param {Numbas.jme.Scope} scope
     * @param {string} exec_path - A description of the path of execution, for caching pre-submit tasks.
     * @fires Numbas.Part#event:pre-mark
     * @fires Numbas.Part#event:post-mark
     * @returns {Numbas.parts.mark_result}
     */
    mark: function(scope, exec_path) {
        this.events.trigger('pre-mark', scope, exec_path);
        var studentAnswer = this.rawStudentAnswerAsJME();
        var result;
        result = this.mark_answer(studentAnswer, scope, exec_path);
        if(result.waiting_for_pre_submit) {
            return result;
        }
        let finalised_result = {valid: false, credit: 0, states: []};
        if(!result.state_errors.mark) {
            finalised_result = marking.finalise_state(result.states.mark);
            this.credit = 0;
            this.apply_feedback(finalised_result);
            this.interpretedStudentAnswer = result.values['interpreted_answer'];
        }
        this.events.trigger('post-mark', result, finalised_result);
        return {finalised_result: finalised_result, values: result.values, script_result: result};
    },

    /** Restore a set of feedback messages.
     *
     * @param {Object<Array.<string>>} feedback - Dictionary of existing `warnings` and `markingFeedback` lists, to add to - copies of these are returned with any additional feedback appended.
     */
    restore_feedback: function(feedback) {
        if(feedback === undefined) {
            feedback = {
                warnings: [],
                markingFeedback: []
            }
        }
        this.setWarnings(feedback.warnings.slice());
        this.markingFeedback = feedback.markingFeedback.slice();
    },
    /** Apply a finalised list of feedback states to this part.
     *
     * @param {Numbas.marking.feedback_item[]} feedback
     * @see Numbas.marking.finalise_state
     */
    apply_feedback: function(feedback) {
        var valid = feedback.valid;
        var part = this;
        var end = false;
        var states = feedback.states.slice();
        var i = 0;
        var lifts = [];
        var scale = 1;
        while(i < states.length) {
            var state = states[i];
            var FeedbackOps = Numbas.marking.FeedbackOps;
            switch(state.op) {
                case FeedbackOps.SET_CREDIT:
                    part.setCredit(scale * state.credit, state.message, state.reason);
                    break;
                case FeedbackOps.MULTIPLY_CREDIT:
                    part.multCredit(state.factor, state.message);
                    break;
                case FeedbackOps.ADD_CREDIT:
                    part.addCredit(scale * state.credit, state.message);
                    break;
                case FeedbackOps.SUB_CREDIT:
                    part.subCredit(scale * state.credit, state.message);
                    break;
                case FeedbackOps.WARNING:
                    part.giveWarning(state.message);
                    break;
                case FeedbackOps.FEEDBACK:
                    part.markingComment(state.message, state.reason, state.format);
                    break;
                case FeedbackOps.END:
                    if(state.invalid) {
                        valid = false;
                    }
                    if(lifts.length) {
                        while(i + 1 < states.length && states[i + 1].op != "end_lift") {
                            i += 1;
                        }
                    } else {
                        end = true;
                    }
                    break;
                case "start_lift":
                    lifts.push({credit: this.credit, creditFraction: this.creditFraction, scale:scale});
                    this.credit = 0;
                    this.creditFraction = math.Fraction.zero;
                    scale = state.scale;
                    break;
                case 'end_lift':
                    var last_lift = lifts.pop();
                    var lift_credit = this.credit;
                    this.creditFraction = last_lift.creditFraction.add(math.Fraction.fromFloat(lift_credit));
                    scale = last_lift.scale;
                    break;
            }
            i += 1;
            if(end) {
                break;
            }
        }
        part.answered = valid;

        /** Add marks awarded/taken away messages to the end of each feedback item which changes awarded credit.
         */
        var t = 0;
        for(let i = 0;i < part.markingFeedback.length;i++) {
            var action = part.markingFeedback[i];
            var credit_change = 0;
            var change_desc;
            if(action.credit !== undefined) {
                var availableMarks = part.availableMarks();
                var change = action.credit * availableMarks;
                credit_change = action.credit;
                if(action.gap != undefined) {
                    const scale = availableMarks > 0 ? part.gaps[action.gap].availableMarks() / availableMarks : 0;
                    change *= scale;
                    credit_change *= part.marks > 0 ? scale : 1 / part.gaps.length;
                }
                var ot = t;
                t += change;
                change = t - ot;
                if(action.message === undefined) {
                    action.message = '';
                }
                if(change != 0) {
                    var marks = Math.abs(change);
                    if(change > 0) {
                        action.credit_message = R('feedback.you were awarded', {count:marks});
                    } else if(change < 0) {
                        action.credit_message = R('feedback.taken away', {count:marks});
                    }
                }
            }
            change_desc = credit_change > 0 ? 'positive' : credit_change < 0 ? 'negative' : 'neutral';
            switch(action.reason) {
                case 'correct':
                    change_desc = 'positive';
                    break;
                case 'incorrect':
                    change_desc = 'negative';
                    break;
                case 'invalid':
                    change_desc = 'invalid';
                    break;
            }
            action.credit_change = change_desc;
        }

    },

    /**
     * Get JME parameters to pass to the marking script.
     *
     * @param {Numbas.jme.token} studentAnswer - The student's answer to the part.
     * @param {Array.<Object<Numbas.jme.token>>} pre_submit_parameters
     * @param {string} exec_path
     * @returns {Object<Numbas.jme.token>}
     */
    marking_parameters: function(studentAnswer, pre_submit_parameters, exec_path) {
        studentAnswer = jme.makeSafe(studentAnswer);
        var obj = {
            path: jme.wrapValue(this.path),
            name: jme.wrapValue(this.name),
            question_definitions: jme.wrapValue(this.question ? this.question.local_definitions : {}),
            studentAnswer: studentAnswer,
            settings: jme.wrapValue(this.settings),
            marks: new jme.types.TNum(this.availableMarks()),
            partType: new jme.types.TString(this.type),
            exec_path: jme.wrapValue(exec_path),
            gaps: jme.wrapValue(this.gaps.map(function(g) {
                return g.marking_parameters(g.rawStudentAnswerAsJME(), [], exec_path)
            })),
            steps: jme.wrapValue(this.steps.map(function(s) {
                return s.marking_parameters(s.rawStudentAnswerAsJME(), [], exec_path)
            }))
        };
        pre_submit_parameters = pre_submit_parameters || [];
        if(pre_submit_parameters.length > 0) {
            var pre_submit = {};
            pre_submit_parameters.forEach(function(params) {
                for(const [k, v] of Object.entries(params)) {
                    pre_submit[k] = v;
                }
            });
            obj.pre_submit = new jme.types.TDict(pre_submit);
        }
        return obj;
    },

    /** Cached results of a pre-submit task.
     *
     * @typedef {object} Numbas.parts.pre_submit_cache_result
     * @param {string} exec_path - A description of the path of execution, for caching pre-submit tasks.
     * @property {Numbas.jme.token} studentAnswer - The answer that was marked.
     * @property {Array.<Object<Numbas.jme.token>>} results - The results of each task.
     */

    /**
     * Do all of the pre-submit tasks before marking an answer.
     * Results are cached by `exec_path` and `studentAnswer`.
     *
     * @param {Numbas.jme.token} studentAnswer
     * @param {Numbas.jme.Scope} scope
     * @param {string} exec_path
     * @fires Numbas.Part#event:do_pre_submit_tasks
     * @returns {object}
     */
    do_pre_submit_tasks: function(studentAnswer, scope, exec_path) {
        this.events.trigger('do_pre_submit_tasks');
        if(this.markingScript.notes.pre_submit === undefined) {
            return {parameters: []};
        }
        var p = this;
        var cache = this.pre_submit_cache.find(function(c) {
            return c.exec_path == exec_path && util.eq(studentAnswer, c.studentAnswer, scope);
        });
        if(cache) {
            return {parameters: cache.results};
        }
        var res = this.markingScript.evaluate_note('pre_submit', scope, this.marking_parameters(studentAnswer, [], exec_path));
        if(res.scope.state_errors.pre_submit) {
            throw(new Numbas.Error('part.marking.error in marking script', {message: res.scope.state_errors.pre_submit}));
        }
        res = jme.castToType(res.value, 'list');
        var promises = res.value.filter(function(v) {
            return jme.isType(v, 'promise');
        }).map(function(v) {
            return jme.castToType(v, 'promise').promise;
        });

        if(!promises.length) {
            return {};
        }

        var all_promises = Promise.all(promises);
        all_promises.then(function(results) {
            p.waiting_for_pre_submit = false;
            p.pre_submit_cache.push({
                exec_path: exec_path,
                studentAnswer: studentAnswer,
                results: results
            });
        });
        this.waiting_for_pre_submit = all_promises;
        return {
            waiting: all_promises
        }
    },

    /** Run the marking script against the given answer.
     * This does NOT apply the feedback and credit to the part object, it just returns it.
     *
     * @param {Numbas.jme.token} studentAnswer
     * @param {Numbas.jme.Scope} scope
     * @param {string} exec_path - A description of the path of execution, for caching pre-submit tasks.
     * @see Numbas.parts.Part#mark
     * @fires Numbas.Part#event:pre-mark_answer
     * @fires Numbas.Part#event:post-mark_answer
     * @returns {Numbas.marking.marking_script_result}
     */
    mark_answer: function(studentAnswer, scope, exec_path) {
        this.events.trigger('pre-mark_answer', studentAnswer, scope, exec_path);
        try {
            this.getCorrectAnswer(scope);
            var pre_submit_result = this.do_pre_submit_tasks(studentAnswer, scope, exec_path);
            if(pre_submit_result.waiting) {
                return {waiting_for_pre_submit: pre_submit_result.waiting};
            }
            var marking_parameters = this.marking_parameters(studentAnswer, pre_submit_result.parameters, exec_path);
            Object.keys(marking_parameters).forEach(function(name) {
                if(scope.getVariable(name) !== undefined) {
                    throw(new Numbas.Error("part.marking.parameter already in scope", {name: name}));
                }
            });
            var result = this.markingScript.evaluate(
                scope,
                marking_parameters
            );
        } catch(e) {
            throw(new Numbas.Error("part.marking.error in marking script", {message:e.message}, e));
        }
        this.events.trigger('post-mark_answer', result);
        return result;
    },
    /** Set the `credit` to an absolute value.
     *
     * @param {number} credit
     * @param {string} message - Message to show in feedback to explain this action.
     * @param {string} reason - Why was the credit set to this value? If given, either 'correct' or 'incorrect'.
     * @fires Numbas.Part#event:setCredit
     */
    setCredit: function(credit, message, reason) {
        var oCredit = this.creditFraction;
        this.creditFraction = math.Fraction.fromFloat(credit);
        if(this.settings.showFeedbackIcon) {
            this.markingFeedback.push({
                op: 'add_credit',
                credit: this.creditFraction.subtract(oCredit).toFloat(),
                message: message,
                reason: reason
            });
        }
        this.events.trigger('setCredit', credit, message, reason);
    },
    /** Add an absolute value to `credit`.
     *
     * @param {number} credit - Amount to add.
     * @param {string} message - Message to show in feedback to explain this action.
     * @fires Numbas.Part#event:addCredit
     */
    addCredit: function(credit, message) {
        var creditFraction = math.Fraction.fromFloat(credit);
        this.creditFraction = this.creditFraction.add(creditFraction);
        if(this.settings.showFeedbackIcon) {
            this.markingFeedback.push({
                op: 'add_credit',
                credit: credit,
                message: message
            });
        }
        this.events.trigger('addCredit', credit, message);
    },
    /** Subtract an absolute value from `credit`.
     *
     * @param {number} credit - Amount to subtract.
     * @param {string} message - Message to show in feedback to explain this action.
     * @fires Numbas.Part#event:subCredit
     */
    subCredit: function(credit, message) {
        var creditFraction = math.Fraction.fromFloat(credit);
        this.creditFraction = this.creditFraction.subtract(creditFraction);
        if(this.settings.showFeedbackIcon) {
            this.markingFeedback.push({
                op: 'sub_credit',
                credit: -credit,
                message: message
            });
        }
        this.events.trigger('subCredit', credit, message);
    },
    /** Multiply `credit` by the given amount - use to apply penalties.
     *
     * @param {number} factor
     * @param {string} message - Message to show in feedback to explain this action.
     * @fires Numbas.Part#event:multCredit
     */
    multCredit: function(factor, message) {
        var oCreditFraction = this.creditFraction;
        this.creditFraction = this.creditFraction.multiply(math.Fraction.fromFloat(factor));
        if(this.settings.showFeedbackIcon) {
            this.markingFeedback.push({
                op: 'multiply_credit',
                credit: this.creditFraction.subtract(oCreditFraction).toFloat(),
                factor: factor,
                message: message
            });
            this.events.trigger('multCredit', factor, message);
        }
    },
    /** Add a comment to the marking feedback.
     *
     * @param {string} message
     * @param {string} reason
     * @param {string} format - The format of the message: `"html"` or `"string"`.
     * @fires Numbas.Part#event:markingComment
     */
    markingComment: function(message, reason, format) {
        if(!this.settings.showFeedbackIcon && (reason == 'incorrect' || reason == 'correct')) {
            return;
        }
        this.markingFeedback.push({
            op: 'feedback',
            message: message,
            reason: reason,
            format: format || 'string'
        });
        this.events.trigger('markingComment', message, reason, format);
    },
    /** Show the steps, as a result of the student asking to show them.
     * If the answers have not been revealed, we should apply the steps penalty.
     *
     * @param {boolean} dontStore - Don't tell the storage that this is happening - use when loading from storage to avoid callback loops.
     * @fires Numbas.Part#event:showSteps
     */
    showSteps: function(dontStore) {
        this.openSteps();
        if(this.revealed) {
            return;
        }
        this.stepsShown = true;
        if(!this.revealed) {
            if(this.answered) {
                this.submit();
            } else {
                this.calculateScore();
                this.question && this.question.updateScore();
            }
        } else {
            this.calculateScore();
        }
        this.display && this.display.showSteps();
        if(!dontStore) {
            this.store && this.store.stepsShown(this);
        }
        this.events.trigger('showSteps', dontStore);
    },
    /** Open the steps, either because the student asked or the answers to the question are being revealed. This doesn't affect the steps penalty.
     *
     * @fires Numbas.Part#event:openSteps
     */
    openSteps: function() {
        this.stepsOpen = true;
        this.events.trigger('openSteps');
        this.display && this.display.showSteps();
    },
    /** Close the steps box. This doesn't affect the steps penalty.
     *
     * @fires Numbas.Part#event:hideSteps
     */
    hideSteps: function() {
        this.stepsOpen = false;
        this.events.trigger('hideSteps');
        this.display && this.display.hideSteps();
        this.store && this.store.stepsHidden(this);
    },

    /** Currently available next parts.
     *
     * @returns {Array.<Numbas.parts.NextPart>}
     */
    availableNextParts: function() {
        var extra = this.answered ? {variables: this.marking_values} : {};
        var scope = new jme.Scope([this.getScope(), extra]);
        scope.setVariable('credit', new jme.types.TNum(this.credit));
        scope.setVariable('answered', new jme.types.TBool(this.answered));
        return this.nextParts.filter(function(np) {
            if(np.instance) {
                return true;
            }
            var condition = np.availabilityCondition;
            if(condition == '') {
                return true;
            }
            try {
                var res = scope.evaluate(condition);
                return res.type == 'boolean' && res.value;
            } catch {
                return false;
            }
        });
    },

    /** Make an instance of the selected next part.
     *
     * @param {Numbas.parts.NextPart} np
     * @param {number} [index] - The position of the part in the question's parts list (added to the end if not given).
     * @fires Numbas.Part#event:makeNextPart
     */
    makeNextPart: function(np, index) {
        var p = this;
        var scope = this.getScope();

        var values = np.instanceVariables;
        if(np.instanceVariables === null) {
            values = np.instanceVariables = {};
            var replaceScope = new jme.Scope([scope, {variables: p.marking_values}]);
            replaceScope.setVariable('credit', new jme.types.TNum(this.credit));
            if(np.variableReplacements.length) {
                np.variableReplacements.forEach(function(vr) {
                    values[vr.variable] = replaceScope.evaluate(vr.definition + '');
                });
            }
        }

        np.instance = this.question.addExtraPart(np.index, scope, values, p, index);
        np.instance.useCustomName = true;
        np.instance.customName = np.label || '';
        np.instance.assignName();
        if(np.lockAfterLeaving) {
            this.lock();
        }
        if(this.display) {
            this.display.updateNextParts();
        }
        this.events.trigger('makeNextPart', np, index);
        if(index === undefined) {
            this.store && this.store.initPart(np.instance);
            this.question.updateScore();
        }
    },

    /** Remove the existing instance of the given next part.
     *
     * @param {Numbas.parts.NextPart} np
     * @fires Numbas.Part#event:removeNextPart
     */
    removeNextPart: function(np) {
        if(!np.instance) {
            return;
        }
        this.question.removePart(np.instance);
        np.instance.nextParts.forEach(function(np2) {
            np.instance.removeNextPart(np2);
        });
        np.instance = null;
        np.instanceVariables = null;
        if(this.display) {
            this.display.updateNextParts();
        }
        this.question.updateScore();
        this.events.trigger('removeNextPart', np);
    },

    /** Reveal the correct answer to this part.
     *
     * @param {boolean} dontStore - Don't tell the storage that this is happening - use when loading from storage to avoid callback loops.
     * @fires Numbas.Part#event:revealAnswer
     */
    revealAnswer: function(dontStore) {
        this.display && this.display.revealAnswer();
        this.revealed = true;
        this.setDirty(false);
        //this.setCredit(0);
        if(this.steps.length > 0) {
            this.openSteps();
            for(let i = 0; i < this.steps.length; i++) {
                this.steps[i].revealAnswer(dontStore);
            }
        }
        this.events.trigger('revealAnswer', dontStore);
    },

    /** Lock this part.
     *
     * @fires Numbas.Part#event:lock
     */
    lock: function() {
        this.locked = true;
        if(this.display) {
            this.display.lock();
        }
        this.events.trigger('lock');
    }
};

/** Definition of a 'next part' option following on from a part.
 *
 * @class
 * @memberof Numbas.parts
 * @param {Numbas.parts.Part} parentPart - The part this one follows on from.
 */
var NextPart = Numbas.parts.NextPart = function(parentPart) {
    this.parentPart = parentPart;

    this.variableReplacements = [];
}
NextPart.prototype = {
    /** List of variable replacements to make when creating this part.
     *
     * @type {Array.<object>}
     */
    variableReplacements: [],

    /** Values of replaced variables for this next part, once it's been created.
     *
     * @type {Object<Numbas.jme.token>}
     */
    instanceVariables: null,

    /** Reference to the instance of this next part, if it's been created.
     *
     * @type {Numbas.parts.Part}
     */
    instance: null,

    /** Name of the penalty to apply when this part is visited.
     *
     * @type {string}
     */
    penalty: null,

    /** Amount of penalty to apply when this part is visited.
     *
     * @type {number}
     */
    penaltyAmount: 0,

    /** Expression defining the amount of penalty to apply when this part is visited.
     *
     * @type {JME}
     */
    penaltyAmountString: '',

    /** Index of the definition of this part in the question's list of part definitions.
     *
     * @type {number}
     */
    index: null,

    /** Label for the button to select this next part.
     *
     * @type {string}
     */
    label: '',

    /** When should this next part be available to the student?
     *
     * @type {JME}
     */
    availabilityCondition: '',

    /** Perform any tidying up or processing that needs to happen once the definition has been loaded.
     */
    finaliseLoad: function() {
        this.label = Numbas.jme.contentsubvars(this.label, this.parentPart.getScope(), false);
    },

    /** Load the definition of this next part from XML.
     *
     * @param {Element} xml
     */
    loadFromXML: function(xml) {
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        tryGetAttribute(this, xml, '.', ['index', 'label', 'availabilityCondition', 'penalty', 'showPenaltyHint', 'lockAfterLeaving']);
        this.index = parseInt(this.index);
        tryGetAttribute(this, xml, '.', ['penaltyAmount'], ['penaltyAmountString']);
        this.penaltyAmountString += '';
        var replacementNodes = xml.selectNodes('variablereplacements/replacement');
        for(let j = 0;j < replacementNodes.length;j++) {
            var replacement = {};
            tryGetAttribute(replacement, replacementNodes[j], '.', ['variable', 'definition']);
            this.variableReplacements.push(replacement);
        }
        var otherPartNode = this.parentPart.question.xml.selectNodes('parts/part')[this.index];
        this.label = this.label || otherPartNode.getAttribute('customname');
        this.xml = otherPartNode;
        this.finaliseLoad();
    },

    /** Load the definition of this next part from JSON.
     *
     * @param {object} data
     */
    loadFromJSON: function(data) {
        var np = this;
        var tryLoad = Numbas.json.tryLoad;
        var tryGet = Numbas.json.tryGet;

        tryLoad(data, ['label', 'availabilityCondition', 'penalty', 'showPenaltyHint', 'lockAfterLeaving'], this);
        tryLoad(data, ['penaltyAmount', 'otherPart'], this, ['penaltyAmountString', 'index']);
        this.penaltyAmountString += '';
        var variableReplacements = tryGet(data, 'variableReplacements');
        if(variableReplacements) {
            variableReplacements.forEach(function(rd) {
                var replacement = {};
                tryLoad(rd, ['variable', 'definition'], replacement);
                np.variableReplacements.push(replacement);
            });
        }
        var otherPart = this.parentPart.question.json.parts[this.index];
        this.label = this.label || tryGet(otherPart, 'customName');
        this.json = data;
        this.finaliseLoad();
    },

    /** Do any of the variable replacements for this next part rely on information from the student's answer to the parent part?
     * Returns true if a variable replacement definition contains a variable name which is not a question variable - it must come from the marking algorithm.
     *
     * @returns {boolean}
     */
    usesStudentAnswer: function() {
        var np = this;
        var question_variables = this.parentPart.question.local_definitions.variables;
        return this.variableReplacements.some(function(vr) {
            var vars = jme.findvars(Numbas.jme.compile(vr.definition), [], np.parentPart.getScope());
            return vars.some(function(name) {
                return !question_variables.contains(name);
            });
        });
    }
};

});
;
Numbas.queueScript('display/parts/custom',['display-base','part-display','util','jme'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;

    /** An answer to a custom part type.
     *
     * @typedef Numbas.custom_part_answer
     * @property {boolean} valid - Is the answer valid?
     * @property {Object} value - The answer.
     * @property {boolean} empty - Is the answer empty?
     */

    /** Display code for a {@link Numbas.parts.CustomPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name CustomPartDisplay
     * @memberof Numbas.display
     */
    display.CustomPartDisplay = function() {
        var p = this.part;
        /** The type of input widget to use for this part.
         * @member {observable|string} input_widget
         * @memberof Numbas.display.CustomPartDisplay
         */
        this.input_widget = p.input_widget();
        /** Options for the input widget.
         * @member {observable|Object} input_options
         * @memberof Numbas.display.CustomPartDisplay
         */
        this.input_options = p.input_options();
        /** The student's current answer (not necessarily submitted)
         * @member {observable|Numbas.custom_part_answer} studentAnswer
         * @memberof Numbas.display.CustomPartDisplay
         */
        this.studentAnswer = Knockout.observable({valid: false, value: this.part.studentAnswer});
        this.correctAnswer = Knockout.observable({});
        this.updateCorrectAnswer(p.getCorrectAnswer(p.getScope()));
        Knockout.computed(function() {
            var answer = this.studentAnswer();

            p.removeWarnings();

            p.widget_warnings = answer.warnings;

            if(answer.warnings) {
                answer.warnings.forEach(function(warning){ p.giveWarning(warning); });
            }

            if(Numbas.util.objects_equal(answer.value, p.stagedAnswer) || !answer.valid && p.stagedAnswer===undefined) {
                return;
            }
            if(answer.valid) {
                p.storeAnswer(answer.value);
            } else {
                p.storeAnswer(undefined);
            }
        },this);
        this.alwaysShowWarnings = {radios: true, checkboxes: true, dropdown: true}[this.input_widget] || false;
    };
    display.CustomPartDisplay.prototype = {
        updateCorrectAnswer: function(answer) {
            this.correctAnswer({valid: true, value: answer});
        },
        restoreAnswer: function(studentAnswer) {
            this.studentAnswer({valid: studentAnswer!==undefined, value: studentAnswer});
        }
    };
    display.CustomPartDisplay = extend(display.PartDisplay,display.CustomPartDisplay,true);
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.CustomPart} constructor. */
Numbas.queueScript('parts/custom_part_type', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking', 'evaluate-settings'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var types = Numbas.jme.types;
var Part = Numbas.parts.Part;

/**
 * Register a custom input type.
 *
 * @param {string} name - The name of the input type.
 * @param {string} signature - The signature of the type of JME value that the input produces.
 * @param {Array} options_definition
 */
Numbas.parts.register_custom_part_input_type = function(name, signature, options_definition) {
    CustomPart.prototype.input_types[name] = function() {
        return signature;
    }
    CustomPart.prototype.custom_input_option_definitions[name] = options_definition;
}

/** Custom part - a part type defined in {@link Numbas.custom_part_types}.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var CustomPart = Numbas.parts.CustomPart = function(path, question, parentPart, store) {
    this.raw_settings = {};
    this.resolved_input_options = {};
}
CustomPart.prototype = /** @lends Numbas.parts.CustomPart.prototype */ {
    is_custom_part_type: true,

    getDefinition: function() {
        this.definition = Numbas.custom_part_types[this.type];
        return this.definition;
    },
    baseMarkingScript: function() {
        var definition = this.getDefinition();
        return new Numbas.marking.MarkingScript(definition.marking_script, null, this.getScope());
    },
    loadFromXML: function(xml) {
        var raw_settings = this.raw_settings;
        this.getDefinition();
        var settingNodes = xml.selectNodes('settings/setting');
        for(var i = 0;i < settingNodes.length;i++) {
            var settingNode = settingNodes[i];
            var name = settingNode.getAttribute('name');
            var value = settingNode.getAttribute('value');
            raw_settings[name] = JSON.parse(value);
        }
    },
    loadFromJSON: function(data) {
        var definition = this.getDefinition();
        var tryLoad = Numbas.json.tryLoad;
        var raw_settings = this.raw_settings;
        definition.settings.forEach(function(sdef) {
            tryLoad(data.settings, sdef.name, raw_settings);
        });
    },
    marking_parameters: function(studentAnswer, pre_submit_parameters) {
        var o = Part.prototype.marking_parameters.apply(this, arguments);
        o.input_options = jme.wrapValue(this.input_options());
        return o;
    },
    resume: function() {
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        this.stagedAnswer = pobj.studentAnswer;
    },

    evaluateSettings: function(scope) {
        var esettings = Numbas.evaluate_settings.evaluate_settings(this.definition, this.raw_settings, scope);
        for(var x in esettings) {
            this.settings[x] = esettings[x];
        }
    },

    finaliseLoad: function() {
        var p = this;
        var settings = this.settings;
        var scope = this.getScope();
        this.evaluateSettings(scope);
        var settings_scope = new jme.Scope([scope, {variables:{settings:new jme.types.TDict(settings)}}]);
        var raw_input_options = this.definition.input_options;
        ['correctAnswer', 'hint'].forEach(function(option) {
            if(raw_input_options[option] === undefined) {
                p.error('part.custom.input option missing', {option:option});
            }
        })
        /** Get the value of an input option by evaluating its definition.
         *
         * @param {string|object} option
         * @returns {*}
         */
        function evaluate_input_option(option) {
            var def = raw_input_options[option];
            var val;
            if(typeof(def) == 'string') {
                val = settings_scope.evaluate(def);
            } else {
                if(def.static) {
                    return def.value;
                } else {
                    val = settings_scope.evaluate(def.value);
                }
            }
            var generic_options = {
                'hint': 'string'
            }
            var type = generic_options[option] || p.input_option_types[p.definition.input_widget][option];
            if(!type) {
                return jme.unwrapValue(val);
            }
            var sig = jme.parse_signature(type);
            var m = sig([val]);
            if(!m) {
                throw(new Numbas.Error("part.custom.input option has wrong type", {option: option, shouldbe: type}));
            }
            var castval = jme.castToType(val, m[0]);
            return jme.unwrapValue(castval);
        }
        for(var option in raw_input_options) {
            if(option == 'correctAnswer') {
                continue;
            }
            try {
                p.resolved_input_options[option] = evaluate_input_option(option);
            } catch(e) {
                p.error('part.custom.error evaluating input option', {option:option, error:e.message}, e);
            }
        }
        this.input_signature = jme.parse_signature(this.get_input_type());
        try {
            var answer = this.getCorrectAnswer(this.getScope());
            p.resolved_input_options['correctAnswer'] = answer;
        } catch(e) {
            this.error(e.message, {}, e);
        }
    },
    initDisplay: function() {
        this.display = new Numbas.display.CustomPartDisplay(this);
    },
    getCorrectAnswer: function(scope) {
        this.evaluateSettings(scope);
        var correctAnswer = scope.evaluate(this.definition.input_options.correctAnswer, {settings: this.settings});
        var m = this.input_signature([correctAnswer]);
        if(!m) {
            throw(new Numbas.Error("part.custom.expected answer has wrong type", {shouldbe: this.get_input_type(), type: correctAnswer.type}));
        }
        this.correctAnswer = jme.castToType(correctAnswer, m[0]);
        switch(this.definition.input_widget) {
            case 'jme':
                return this.correctAnswer.tree;
            case 'checkboxes':
                return this.correctAnswer.value.map(function(c) {
                    return c.value;
                });
            case 'matrix':
                if(!this.resolved_input_options.parseCells) {
                    return jme.unwrapValue(this.correctAnswer);
                }
                return jme.unwrapValue(this.correctAnswer);
            default:
                return jme.unwrapValue(this.correctAnswer);
        }
    },
    setStudentAnswer: function() {
        this.studentAnswer = this.stagedAnswer;
    },
    input_widget: function() {
        return this.definition.input_widget;
    },
    input_options: function() {
        return this.resolved_input_options;
    },
    rawStudentAnswerAsJME: function() {
        if(this.studentAnswer === undefined) {
            return new types.TNothing();
        }
        return this.student_answer_jme_types[this.input_widget()](this.studentAnswer, this.input_options());
    },
    input_types: {
        string: function() {
            return 'string';
        },
        number: function() {
            return 'string';
        },
        jme: function() {
            return 'expression';
        },
        matrix: function() {
            return this.resolved_input_options.parseCells ? 'matrix' : 'list of list of string';
        },
        radios: function() {
            return 'number';
        },
        dropdown: function() {
            return 'number';
        },
        checkboxes: function() {
            return 'list of boolean';
        },
    },
    get_input_type: function() {
        return this.input_types[this.definition.input_widget].apply(this);
    },

    custom_input_option_definitions: {},

    input_option_types: {
        'string': {
            'allowEmpty': 'boolean'
        },
        'number': {
            'allowedNotationStyles': 'list of string',
            'allowFractions': 'boolean'
        },
        'jme': {
            'showPreview': 'boolean'
        },
        'matrix': {
            'allowedNotationStyles': 'list of string',
            'allowFractions': 'boolean',
            'parseCells': 'boolean',
            'allowResize': 'boolean',
            'numRows': 'number',
            'numColumns': 'number',
            'showBrackets': 'boolean',
            'rowHeaders': 'list of string',
            'columnHeaders': 'list of string'
        },
        'radios': {
            'choices': 'list of string'
        },
        'checkboxes': {
            'choices': 'list of string'
        },
        'dropdown': {
            'choices': 'list of string'
        }
    },
    student_answer_jme_types: {
        'string': function(answer) {
            return new types.TString(answer);
        },
        'number': function(answer) {
            return new types.TNum(answer);
        },
        'jme': function(answer) {
            return new types.TExpression(answer);
        },
        'matrix': function(answer, options) {
            if(options.parseCells) {
                return new types.TMatrix(answer);
            } else {
                return jme.wrapValue(answer);
            }
        },
        'radios': function(answer) {
            return new types.TNum(answer);
        },
        'checkboxes': function(answer) {
            return new types.TList(answer.map(function(ticked) {
                return new types.TBool(ticked)
            }));
        },
        'dropdown': function(answer) {
            return new types.TNum(answer);
        }
    },

    submit_no_staged_answer: function() {
        if(this.widget_warnings) {
            this.widget_warnings.forEach((warning) => this.giveWarning(warning));
        } else {
            this.giveWarning(R('part.marking.not submitted'));
        }
    }
};
['resume', 'finaliseLoad', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    CustomPart.prototype[method] = util.extend(Part.prototype[method], CustomPart.prototype[method]);
});
CustomPart = Numbas.parts.CustomPart = util.extend(Part, CustomPart);
});
;
Numbas.queueScript('display/parts/extension',['display-base','part-display','util'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    /** Display code for a {@link Numbas.parts.ExtensionPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name ExtensionPartDisplay
     * @memberof Numbas.display
     */
    display.ExtensionPartDisplay= function() {};
    display.ExtensionPartDisplay= extend(display.PartDisplay,display.ExtensionPartDisplay,true);
});;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.} object */
Numbas.queueScript('parts/extension', ['base', 'util', 'part'], function() {
var util = Numbas.util;
var Part = Numbas.parts.Part;
/** Extension part - validation and marking should be filled in by an extension, or custom javascript code belonging to the question.
 *
 * @class
 * @param {Element} xml
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var ExtensionPart = Numbas.parts.ExtensionPart = function(xml, path, question, parentPart, store) {
}
ExtensionPart.prototype = /** @lends Numbas.parts.ExtensionPart.prototype */ {
    loadFromXML: function() {},
    loadFromJSON: function() {},
    finaliseLoad: function() {},
    initDisplay: function() {
        this.display = new Numbas.display.ExtensionPartDisplay(this);
    },
    hasStagedAnswer: function() {
        return true;
    },
    doesMarking: true,
    /** Return suspend data for this part so it can be restored when resuming the exam - must be implemented by an extension or the question.
     *
     * @returns {object}
     */
    createSuspendData: function() {
        return {};
    },
    /** Get the suspend data created in a previous session for this part, if it exists.
     *
     * @returns {object}
     */
    loadSuspendData: function() {
        if(!this.store) {
            return undefined;
        }
        var pobj = this.store.loadExtensionPart(this);
        if(pobj) {
            return pobj.extension_data;
        }
        return undefined;
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the custom marking algorithm.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        return new Numbas.jme.types.TNothing();
    },

    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript('mark: nothing\n\ninterpreted_answer: nothing', null, this.getScope());
    },
};
['finaliseLoad', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    ExtensionPart.prototype[method] = util.extend(Part.prototype[method], ExtensionPart.prototype[method]);
});
Numbas.partConstructors['extension'] = util.extend(Part, ExtensionPart);
});
;
Numbas.queueScript('display/parts/gapfill',['display-base','part-display','util'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    /** Display code for a {@link Numbas.parts.GapFillPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name GapFillPartDisplay
     * @memberof Numbas.display
     */
    display.GapFillPartDisplay = function()
    {
        this.showCorrectAnswer = false;
    }
    display.GapFillPartDisplay.prototype =
    {
        show: function()
        {
            for(var i=0;i<this.part.gaps.length; i++)
                this.part.gaps[i].display.show();
        },
        restoreAnswer: function(studentAnswer)
        {
            if(!studentAnswer) {
                return;
            }
            for(var i=0;i<this.part.gaps.length; i++) {
                if(studentAnswer[i]!==undefined) {
                    this.part.gaps[i].display.restoreAnswer(studentAnswer[i]);
                }
            }
        },
        revealAnswer: function()
        {
        },
        init: function() {
            for(var i=0;i<this.part.gaps.length; i++)
                this.part.gaps[i].display.init();
        },
        end: function() {
            for(var i=0;i<this.part.gaps.length; i++)
                this.part.gaps[i].display.end();
        }
    };
    display.GapFillPartDisplay = extend(display.PartDisplay,display.GapFillPartDisplay,true);
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.GapFillPart} object */
Numbas.queueScript('parts/gapfill', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking_scripts'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var Part = Numbas.parts.Part;
/** Gap-fill part: text with multiple input areas, each of which is its own sub-part, known as a 'gap'.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var GapFillPart = Numbas.parts.GapFillPart = function(path, question, parentPart, store) {
    util.copyinto(GapFillPart.prototype.settings, this.settings);
}
GapFillPart.prototype = /** @lends Numbas.parts.GapFillPart.prototype */
{
    /** Properties set when the part is generated.
     *
     * Extends {@link Numbas.parts.Part#settings}
     *
     * @property {boolean} sortAnswers - Should the student's answers to the gaps be put in ascending order before marking?
     */
    settings: {
        sortAnswers: false
    },

    loadFromXML: function(xml) {
        var gapXML = xml.selectNodes('gaps/part');
        var settings = this.settings;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        this.marks = 0;
        tryGetAttribute(settings, xml, 'marking', ['sortanswers'], ['sortAnswers']);
        for(var i = 0 ; i < gapXML.length; i++) {
            var gap = Numbas.createPartFromXML(i, gapXML[i], this.path + 'g' + i, this.question, this, this.store);
            this.addGap(gap, i);
        }
    },
    loadFromJSON: function(data) {
        var p = this;
        var settings = this.settings;
        var tryLoad = Numbas.json.tryLoad;
        tryLoad(data, ['sortAnswers'], settings);
        if('gaps' in data) {
            data.gaps.forEach(function(gd, i) {
                var gap = Numbas.createPartFromJSON(i, gd, p.path + 'g' + i, p.question, p, p.store);
                p.addGap(gap, i)
            });
        }
    },
    finaliseLoad: function() {
        if(this.settings.sortAnswers && this.gaps.length) {
            var type = this.gaps[0].type;
            if(this.gaps.some(function(g) {
                return g.type != type;
            })) {
                this.settings.sortAnswers = false;
            }
        }
    },
    initDisplay: function() {
        this.display = new Numbas.display.GapFillPartDisplay(this);
    },

    /** The total marks available for this part, after applying adaptive marking and steps penalties.
     *
     * @returns {number}
     */
    availableMarks: function() {
        var marks = 0;
        for(var i = 0;i < this.gaps.length;i++) {
            marks += this.gaps[i].marks;
        }
        if(this.adaptiveMarkingUsed) {
            marks -= this.settings.adaptiveMarkingPenalty;
        }
        if(this.steps.length && this.stepsShown) {
            marks -= this.settings.stepsPenalty;
        }
        marks = Math.max(Math.min(this.marks, marks), 0);
        return marks;
    },


    /** Add a gap to this part.
     *
     * @param {Numbas.parts.Part} gap
     * @param {number} index - the position of the gap
     */
    addGap: function(gap, index) {
        gap.isGap = true;
        this.marks += gap.marks;
        this.gaps.splice(index, 0, gap);
    },
    resume: function() {
        var p = this;
        this.gaps.forEach(function(g) {
            g.resume();
            p.answered = p.answered || g.answered;
        });
    },
    /** Student's answers as visible on the screen (not necessarily yet submitted).
     *
     * @type {Array.<string>}
     */
    stagedAnswer: undefined,
    /** Has the student entered an answer to this part?
     *
     * @see Numbas.parts.Part#stagedAnswer
     * @returns {boolean}
     */
    hasStagedAnswer: function() {
        return this.gaps.some(function(g) {
            return g.hasStagedAnswer();
        });
    },
    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript(Numbas.raw_marking_scripts.gapfill, null, this.getScope());
    },
    /** Reveal the answers to all of the child gaps.
     *
     * @param {boolean} dontStore - don't tell the storage that this is happening - use when loading from storage to avoid callback loops
     * @augments Numbas.parts.Part#revealAnswer
     */
    revealAnswer: function(dontStore) {
        for(var i = 0; i < this.gaps.length; i++) {
            this.gaps[i].revealAnswer(dontStore);
        }
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the custom marking algorithm.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        if(this.gaps.some(function(g) {
            return g.rawStudentAnswerAsJME() === undefined;
        })) {
            return undefined;
        }
        return new Numbas.jme.types.TList(this.gaps.map(function(g) {
            return g.rawStudentAnswerAsJME()
        }));
    },
    storeAnswer: function(answer) {
        this.gaps.forEach(function(g, i) {
            g.storeAnswer(answer[i]);
        })
    },
    setStudentAnswer: function() {
        this.studentAnswer = this.gaps.map(function(g) {
            g.setStudentAnswer();
            return g.studentAnswer;
        });
    },
    /** Get the student's answer as a JME data type, to be used in error-carried-forward calculations.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    studentAnswerAsJME: function() {
        return new Numbas.jme.types.TList(this.gaps.map(function(g) {
            return g.studentAnswerAsJME()
        }));
    },

    getCorrectAnswer: function(scope) {
        return this.gaps.map(function(g) {
            return g.getCorrectAnswer(scope);
        });
    },

    marking_parameters: function(studentAnswer, pre_submit_parameters) {
        var p = this;
        var parameters = Part.prototype.marking_parameters.apply(this, arguments);
        var adaptive_order = [];

        /** Detect cyclic references in adaptive marking variable replacements.
         * Visit a gap, and raise an error if it's been visited before, i.e. there's a cycle in the graph of variable replacement dependencies.
         * Then, visit each of the gaps that this gap depends on for variable replacements.
         *
         * @param {Numbas.parts.Part} g - The gap being visited.
         * @param {Array.<Numbas.parts.Part>} path - The gaps that have already been visited.
         */
        function visit(g, path) {
            var i = p.gaps.indexOf(g);
            if(i < 0) {
                return;
            }
            path = path || [];
            var pi = path.indexOf(g);
            if(pi >= 0) {
                p.error('part.gapfill.cyclic adaptive marking', {name1: g.name, name2: path[pi + 1].name});
            }
            g.settings.errorCarriedForwardReplacements.forEach(function(vr) {
                visit(p.question.getPart(vr.part), path.concat([g]));
            })
            if(adaptive_order.indexOf(i) == -1) {
                adaptive_order.push(i);
            }
        }
        p.gaps.forEach(function(g) {
            visit(g);
        });
        parameters['gap_adaptive_order'] = jme.wrapValue(adaptive_order);
        return parameters;
    },

    lock: function() {
        this.gaps.forEach(function(g) {
            g.lock();
        });
    }
};
['loadFromXML', 'resume', 'finaliseLoad', 'loadFromJSON', 'storeAnswer', 'lock'].forEach(function(method) {
    GapFillPart.prototype[method] = util.extend(Part.prototype[method], GapFillPart.prototype[method]);
});
['revealAnswer'].forEach(function(method) {
    GapFillPart.prototype[method] = util.extend(GapFillPart.prototype[method], Part.prototype[method]);
});
Numbas.partConstructors['gapfill'] = util.extend(Part, GapFillPart);
});
;
Numbas.queueScript('display/parts/information',['display-base','part-display','util'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    /** Display code for a {@link Numbas.parts.InformationPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name InformationPartDisplay
     * @memberof Numbas.display
     */
    display.InformationPartDisplay = function() {};
    display.InformationPartDisplay = extend(display.PartDisplay,display.InformationPartDisplay,true);
});;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.} object */
Numbas.queueScript('parts/information', ['base', 'jme', 'jme-variables', 'util', 'part'], function() {
var util = Numbas.util;
var Part = Numbas.parts.Part;
/** Information only part - no input, no marking, just display some content to the student.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var InformationPart = Numbas.parts.InformationPart = function(path, question, parentPart, store) {
}
InformationPart.prototype = /** @lends Numbas.parts.InformationOnlyPart.prototype */ {
    assignName: function(index) {
        if(this.useCustomName) {
            Part.prototype.assignName.apply(this, arguments);
            return false;
        }
        return false;
    },

    loadFromXML: function() {
    },
    loadFromJSON: function() {
    },
    finaliseLoad: function() {
        this.answered = true;
        this.isDirty = false;
    },
    initDisplay: function() {
        this.display = new Numbas.display.InformationPartDisplay(this);
    },
    /** This part is always valid.
     *
     * @returns {boolean} true
     */
    validate: function() {
        this.answered = true;
        return true;
    },
    /** This part is never dirty.
     */
    setDirty: function() {
        this.isDirty = false;
    },
    hasStagedAnswer: function() {
        return true;
    },
    doesMarking: false
};
['finaliseLoad', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    InformationPart.prototype[method] = util.extend(Part.prototype[method], InformationPart.prototype[method]);
});
Numbas.partConstructors['information'] = util.extend(Part, InformationPart);
});
;
Numbas.queueScript('display/parts/jme',['display-base','part-display','util','jme-display','jme'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    var jme = Numbas.jme;
    /** Display code for a {@link Numbas.parts.JMEPart}
     * @constructor
     * @augments Numbas.display.PartDisplay
     * @name JMEPartDisplay
     * @memberof Numbas.display
     */
    display.JMEPartDisplay = function()
    {
        var p = this.part;
        /** The student's current answer (not necessarily submitted)
         * @member {observable|JME} studentAnswer
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.studentAnswer = Knockout.observable('');
        Knockout.computed(function() {
            p.storeAnswer(this.studentAnswer());
        },this);
        /** Should the LaTeX rendering of the student's answer be shown?
         * @member {boolean} showPreview
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.showPreview = p.settings.showPreview;
        /** The correct answer
         * @member {observable|JME} correctAnswer
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.correctAnswer = Knockout.observable('');
        /** The correct answer, in LaTeX form
         * @member {observable|TeX} correctAnswerLaTeX
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.correctAnswerLaTeX = Knockout.observable('');
        this.updateCorrectAnswer(p.getCorrectAnswer(p.getScope()));

        /** The student's answer, in LaTeX form
         * @member {observable|TeX} studentAnswerLaTeX
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.studentAnswerLaTeX = Knockout.computed(function() {
            var studentAnswer = this.studentAnswer();
            if(studentAnswer.trim()=='')
                return '';
            this.removeWarnings();
            try {
                var scope = p.getScope();
                var studentTree = scope.parser.compile(studentAnswer);
                var expand_settings = {
                    singleLetterVariables: p.settings.singleLetterVariables,
                    noUnknownFunctions: !p.settings.allowUnknownFunctions,
                    implicitFunctionComposition: p.settings.implicitFunctionComposition
                };
                studentTree = scope.expandJuxtapositions(studentTree, expand_settings);
                var tex = jme.display.texify(studentTree,{},scope);
                if(tex === undefined) {
                    throw(new Numbas.Error('display.part.jme.error making maths'));
                }
            }
            catch(e) {
                p.giveWarning(e.message);
                return '';
            }
            if(p.settings.checkVariableNames) {
                var usedvars = jme.findvars(studentTree,[],p.getScope());
                var failExpectedVariableNames = false;
                var correctTree = scope.parser.compile(this.correctAnswer());
                correctTree = scope.expandJuxtapositions(correctTree, expand_settings);
                var expectedVariableNames = jme.findvars(correctTree,[],p.getScope());
                var unexpectedVariableName;
                for(var i=0;i<usedvars.length;i++) {
                    if(!expectedVariableNames.contains(usedvars[i])) {
                        failExpectedVariableNames = true;
                        unexpectedVariableName = usedvars[i];
                        break;
                    }
                }
                if( failExpectedVariableNames ) {
                    var suggestedNames = unexpectedVariableName.split(jme.re.re_short_name);
                    if(suggestedNames.length>3) {
                        var suggestion = [];
                        for(var i=1;i<suggestedNames.length;i+=2) {
                            suggestion.push(suggestedNames[i]);
                        }
                        suggestion = suggestion.join('*');
                        p.giveWarning(R('part.jme.unexpected variable name suggestion',{name:unexpectedVariableName,suggestion:suggestion}));
                    }
                    else
                        p.giveWarning(R('part.jme.unexpected variable name', {name:unexpectedVariableName}));
                }
            }
            if(p.settings.mustMatchPattern && p.settings.mustMatchWarningTime=='input' || p.settings.mustMatchWarningTime == 'prevent') {
                var r = new Numbas.jme.rules.Rule(p.settings.mustMatchPattern, null, 'ac');
                var m = r.match(studentTree, p.getScope());
                if(!m) {
                    p.giveWarning(R('part.jme.must-match.warning', {message: p.settings.mustMatchMessage}));
                }
            }
            return tex;
        },this).extend({throttle:100});
        /** Does the input box have focus?
         * @member {observable|boolean} inputHasFocus
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.inputHasFocus = Knockout.observable(false);
        /** Give the input box focus
         * @member {function} focusInput
         * @method
         * @memberof Numbas.display.JMEPartDisplay
         */
        this.focusInput = function() {
            this.inputHasFocus(true);
        }
    }
    display.JMEPartDisplay.prototype =
    {
        updateCorrectAnswer: function(answer) {
            var p = this.part;
            var scope = p.getScope();
            this.correctAnswer(answer);

            var tree = jme.compile(answer);
            tree = scope.expandJuxtapositions(tree, {
                singleLetterVariables: p.settings.singleLetterVariables,
                noUnknownFunctions: !p.settings.allowUnknownFunctions,
                implicitFunctionComposition: p.settings.implicitFunctionComposition
            });
            var ruleset = jme.collectRuleset(p.settings.answerSimplificationString, scope.allRulesets());
            tree = jme.display.simplifyTree(
                tree,
                ruleset,
                scope
            );

            this.correctAnswerLaTeX(jme.display.texify(tree, ruleset.flags, scope));
        },
        restoreAnswer: function(studentAnswer) {
            this.studentAnswer(studentAnswer);
        }
    };
    display.JMEPartDisplay = extend(display.PartDisplay,display.JMEPartDisplay,true);
})
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.JMEPart} object */
Numbas.queueScript('parts/jme', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking_scripts'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var Part = Numbas.parts.Part;
/** Judged Mathematical Expression.
 *
 * Student enters a string representing a mathematical expression, eg. `x^2+x+1`, and it is compared with the correct answer by evaluating over a range of values.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var JMEPart = Numbas.parts.JMEPart = function(path, question, parentPart) {
    var settings = this.settings;
    util.copyinto(JMEPart.prototype.settings, settings);
    settings.valueGenerators = {};
    settings.mustHave = [];
    settings.notAllowed = [];
}
JMEPart.prototype = /** @lends Numbas.JMEPart.prototype */
{
    loadFromXML: function(xml) {
        var settings = this.settings;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        //parse correct answer from XML
        var answerNode = xml.selectSingleNode('answer/correctanswer');
        if(!answerNode) {
            this.error('part.jme.answer missing');
        }
        tryGetAttribute(settings, xml, 'answer/correctanswer', 'simplification', 'answerSimplificationString');
        settings.correctAnswerString = Numbas.xml.getTextContent(answerNode).trim();
        //get checking type, accuracy, checking range
        var parametersPath = 'answer';
        tryGetAttribute(settings, xml, parametersPath + '/checking', ['type', 'accuracy', 'failurerate'], ['checkingType', 'checkingAccuracy', 'failureRate']);
        tryGetAttribute(settings, xml, parametersPath + '/checking/range', ['start', 'end', 'points'], ['vsetRangeStart', 'vsetRangeEnd', 'vsetRangePoints']);

        var valueGeneratorsNode = xml.selectSingleNode('answer/checking/valuegenerators');
        if(valueGeneratorsNode) {
            var valueGenerators = valueGeneratorsNode.selectNodes('generator');
            for(let i = 0;i < valueGenerators.length;i++) {
                var generator = {};
                tryGetAttribute(generator, xml, valueGenerators[i], ['name', 'value']);
                this.addValueGenerator(generator.name, generator.value);
            }
        }

        //max length and min length
        let messageNode;
        tryGetAttribute(settings, xml, parametersPath + '/maxlength', ['length', 'partialcredit'], ['maxLength', 'maxLengthPC']);
        messageNode = xml.selectSingleNode('answer/maxlength/message');
        if(messageNode) {
            settings.maxLengthMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
            if(settings.maxLengthMessage.textContent == '') {
                settings.maxLengthMessage = R('part.jme.answer too long');
            }
        }
        tryGetAttribute(settings, xml, parametersPath + '/minlength', ['length', 'partialcredit'], ['minLength', 'minLengthPC']);
        messageNode = xml.selectSingleNode('answer/minlength/message');
        if(messageNode) {
            settings.minLengthMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
            if(settings.minLengthMessage.textContent == '') {
                settings.minLengthMessage = R('part.jme.answer too short');
            }
        }
        //get list of 'must have' strings
        var mustHaveNode = xml.selectSingleNode('answer/musthave');
        if(mustHaveNode) {
            var mustHaves = mustHaveNode.selectNodes('string');
            for(let i = 0; i < mustHaves.length; i++) {
                settings.mustHave.push(Numbas.xml.getTextContent(mustHaves[i]));
            }
            //partial credit for failing must-have test and whether to show strings which must be present to student when warning message displayed
            tryGetAttribute(settings, xml, mustHaveNode, ['partialcredit', 'showstrings'], ['mustHavePC', 'mustHaveShowStrings']);
            //warning message to display when a must-have is missing
            const messageNode = mustHaveNode.selectSingleNode('message');
            if(messageNode) {
                settings.mustHaveMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
            }
        }
        //get list of 'not allowed' strings
        var notAllowedNode = xml.selectSingleNode('answer/notallowed');
        if(notAllowedNode) {
            var notAlloweds = notAllowedNode.selectNodes('string');
            for(let i = 0; i < notAlloweds.length; i++) {
                settings.notAllowed.push(Numbas.xml.getTextContent(notAlloweds[i]));
            }
            //partial credit for failing not-allowed test
            tryGetAttribute(settings, xml, notAllowedNode, ['partialcredit', 'showstrings'], ['notAllowedPC', 'notAllowedShowStrings']);
            messageNode = notAllowedNode.selectSingleNode('message');
            if(messageNode) {
                settings.notAllowedMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
            }
        }
        //get pattern the student's answer must match
        var mustMatchNode = xml.selectSingleNode('answer/mustmatchpattern');
        if(mustMatchNode) {
            //partial credit for failing not-allowed test
            tryGetAttribute(settings, xml, mustMatchNode, ['pattern', 'partialCredit', 'nameToCompare', 'warningTime'], ['mustMatchPatternString', 'mustMatchPC', 'nameToCompare', 'mustMatchWarningTime']);
            const messageNode = mustMatchNode.selectSingleNode('message');
            if(messageNode) {
                var mustMatchMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
                if(util.isNonemptyHTML(mustMatchMessage)) {
                    settings.mustMatchMessage = mustMatchMessage;
                }
            }
        }

        tryGetAttribute(settings, xml, parametersPath, ['checkVariableNames', 'singleLetterVariables', 'allowUnknownFunctions', 'implicitFunctionComposition', 'showPreview', 'caseSensitive']);
    },
    loadFromJSON: function(data) {
        var p = this;
        var settings = this.settings;
        var tryLoad = Numbas.json.tryLoad;
        var tryGet = Numbas.json.tryGet;
        tryLoad(data, ['answer', 'answerSimplification'], settings, ['correctAnswerString', 'answerSimplificationString']);
        tryLoad(data, ['checkingType', 'checkingAccuracy', 'failureRate'], settings, ['checkingType', 'checkingAccuracy', 'failureRate']);
        tryLoad(data, ['vsetRangePoints'], settings);
        var vsetRange = tryGet(data, 'vsetRange');
        if(vsetRange) {
            settings.vsetRangeStart = util.parseNumber(vsetRange[0]);
            settings.vsetRangeEnd = util.parseNumber(vsetRange[1]);
        }
        tryLoad(data.maxlength, ['length', 'partialCredit', 'message'], settings, ['maxLength', 'maxLengthPC', 'maxLengthMessage']);
        tryLoad(data.minlength, ['length', 'partialCredit', 'message'], settings, ['minLength', 'minLengthPC', 'minLengthMessage']);
        tryLoad(data.musthave, ['strings', 'showStrings', 'partialCredit', 'message'], settings, ['mustHave', 'mustHaveShowStrings', 'mustHavePC', 'mustHaveMessage']);
        tryLoad(data.notallowed, ['strings', 'showStrings', 'partialCredit', 'message'], settings, ['notAllowed', 'notAllowedShowStrings', 'notAllowedPC', 'notAllowedMessage']);
        tryLoad(data.mustmatchpattern, ['pattern', 'partialCredit', 'message', 'nameToCompare', 'warningTime'], settings, ['mustMatchPatternString', 'mustMatchPC', 'mustMatchMessage', 'nameToCompare', 'mustMatchWarningTime']);
        settings.mustMatchPC /= 100;
        tryLoad(data, ['checkVariableNames', 'singleLetterVariables', 'allowUnknownFunctions', 'implicitFunctionComposition', 'showPreview', 'caseSensitive'], settings);
        var valuegenerators = tryGet(data, 'valuegenerators');
        if(valuegenerators) {
            valuegenerators.forEach(function(g) {
                p.addValueGenerator(g.name, g.value);
            });
        }
    },
    resume: function() {
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        this.stagedAnswer = pobj.studentAnswer;
    },
    finaliseLoad: function() {
        if(!this.settings.answerSimplificationString.trim()) {
            this.settings.answerSimplificationString = 'basic,unitFactor,unitPower,unitDenominator,zeroFactor,zeroTerm,zeroPower,collectNumbers,zeroBase,constantsFirst,sqrtProduct,sqrtDivision,sqrtSquare,otherNumbers';
        }
        this.stagedAnswer = '';
        this.getCorrectAnswer(this.getScope());
    },
    initDisplay: function() {
        this.display = new Numbas.display.JMEPartDisplay(this);
    },
    /** Student's last submitted answer.
     *
     * @type {string}
     */
    studentAnswer: '',
    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript(Numbas.raw_marking_scripts.jme, null, this.getScope());
    },
    /** Properties set when the part is generated.
     *
     * Extends {@link Numbas.parts.Part#settings}
     *
     * @property {JME} correctAnswerString - The definition of the correct answer, without variables substituted into it.
     * @property {string} correctAnswer - An expression representing the correct answer to the question. The student's answer should evaluate to the same value as this.
     * @property {string} answerSimplificationString - String from the XML defining which answer simplification rules to use
     * @property {Array.<string>} answerSimplification - Names of simplification rules (see {@link Numbas.jme.display.Rule}) to use on the correct answer
     * @property {string} checkingType - Method to compare answers. See {@link Numbas.jme.checkingFunctions}
     * @property {number} checkingAccuracy - Accuracy threshold for checking. Exact definition depends on the checking type.
     * @property {number} failureRate - Comparison failures allowed before we decide answers are different
     * @property {number} vsetRangeStart - Lower bound on range of points to pick values from for variables in the answer expression
     * @property {number} vsetRangeEnd - Upper bound on range of points to pick values from for variables in the answer expression
     * @property {number} vsetRangePoints - Number of points to compare answers on
     * @property {number} maxLength - Maximum length, in characters, of the student's answer. Note that the student's answer is cleaned up before checking length, so extra space or brackets aren't counted.
     * @property {number} maxLengthPC - Partial credit if the student's answer is too long.
     * @property {string} maxLengthMessage - Message to add to marking feedback if the student's answer is too long.
     * @property {number} minLength - Minimum length, in characters, of the student's answer. Note that the student's answer is cleaned up before checking length, so extra space or brackets aren't counted.
     * @property {number} minLengthPC - Partial credit if the student's answer is too short.
     * @property {string} minLengthMessage - Message to add to the marking feedback if the student's answer is too short.
     * @property {Array.<string>} mustHave - Strings which must be present in the student's answer.
     * @property {number} mustHavePC - Partial credit to award if any must-have string is missing.
     * @property {string} mustHaveMessage - Message to add to the marking feedback if the student's answer is missing a must-have string.
     * @property {boolean} mustHaveShowStrings - Tell the students which strings must be included in the marking feedback, if they're missing a must-have?
     * @property {Array.<string>} notAllowed - Strings which must not be present in the student's answer.
     * @property {number} notAllowedPC - Partial credit to award if any not-allowed string is present.
     * @property {string} notAllowedMessage - Message to add to the marking feedback if the student's answer contains a not-allowed string.
     * @property {boolean} notAllowedShowStrings - Tell the students which strings must not be included in the marking feedback, if they've used a not-allowed string?
     * @property {string} mustMatchPatternString - String defining the pattern the student's answer must match, before variables are substituted in.
     * @property {string} mustMatchPattern - A pattern that the student's answer must match.
     * @property {number} mustMatchPC - Partial credit to award if the student's answer does not match the pattern.
     * @property {string} mustMatchMessage - Message to add to the marking feedback if the student's answer does not match the pattern.
     * @property {string} nameToCompare - The name of a captured subexpression from the pattern match to compare with the corresponding captured part from the correct answer. If empty, the whole expressions are compared.
     * @property {string} mustMatchWarningTime - When to warn the student that their answer doesn't match the pattern. `input`: immediately, as they enter the answer. `submission`: only once they have submitted their answer.
     * @property {boolean} checkVariableNames - Check that the student has used the same variable names as the correct answer?
     * @property {boolean} singleLetterVariables - Force single letter variable names in the answer? Multi-letter variable names will be considered as implicit multiplication.
     * @property {boolean} allowUnknownFunctions - Allow the use of unknown functions in the answer? If false, application of unknown functions will be considered as multiplication instead.
     * @property {boolean} implicitFunctionComposition - Consider juxtaposition of function names as composition?
     * @property {boolean} caseSensitive - Should the answer expression be parsed as case-sensitive?
     */
    settings:
    {
        correctAnswerString: '',
        correctAnswer: '',
        answerSimplificationString: '',
        answerSimplification: ['basic', 'unitFactor', 'unitPower', 'unitDenominator', 'zeroFactor', 'zeroTerm', 'zeroPower', 'collectNumbers', 'zeroBase', 'constantsFirst', 'sqrtProduct', 'sqrtDivision', 'sqrtSquare', 'otherNumbers'],
        checkingType: 'RelDiff',
        checkingAccuracy: 0,
        failureRate: 1,
        vsetRangeStart: 0,
        vsetRangeEnd: 1,
        vsetRangePoints: 1,
        maxLength: 0,
        maxLengthPC: 0,
        maxLengthMessage: 'Your answer is too long',
        minLength: 0,
        minLengthPC: 0,
        minLengthMessage: 'Your answer is too short',
        mustHave: [],
        mustHavePC: 0,
        mustHaveMessage: '',
        mustHaveShowStrings: false,
        notAllowed: [],
        notAllowedPC: 0,
        notAllowedMessage: '',
        notAllowedShowStrings: false,
        mustMatchPattern: '',
        mustMatchPC: 0,
        mustMatchMessage: '',
        nameToCompare: '',
        checkVariableNames: false,
        mustMatchWarningTime: 'submission',
        singleLetterVariables: false,
        allowUnknownFunctions: true,
        implicitFunctionComposition: false,
        caseSensitive: false
    },
    /** The name of the input widget this part uses, if any.
     *
     * @returns {string}
     */
    input_widget: function() {
        return 'jme';
    },
    /** Options for this part's input widget.
     *
     * @returns {object}
     */
    input_options: function() {
        return {
            showPreview: this.settings.showPreview,
            returnString: true
        };
    },
    /** Compute the correct answer, based on the given scope.
     *
     * @param {Numbas.jme.Scope} scope
     * @returns {JME}
     */
    getCorrectAnswer: function(scope) {
        var settings = this.settings;
        var answerSimplification = Numbas.jme.collectRuleset(settings.answerSimplificationString, scope.allRulesets());
        var tree = jme.display.subvars(settings.correctAnswerString, scope);
        tree = scope.expandJuxtapositions(
            tree,
            {
                singleLetterVariables: settings.singleLetterVariables,
                noUnknownFunctions: !settings.allowUnknownFunctions,
                implicitFunctionComposition: settings.implicitFunctionComposition,
                normaliseSubscripts: true
            }
        );

        if(!tree && this.marks > 0) {
            this.error('part.jme.answer missing');
        }
        if(this.question) {
            scope = scope.unset(this.question.local_definitions);
        }
        var expr = jme.display.treeToJME(tree, {plaindecimal: true}, scope);
        settings.correctVariables = jme.findvars(jme.compile(expr), [], scope);
        settings.correctAnswer = jme.display.simplifyExpression(
            expr,
            answerSimplification,
            scope
        );
        settings.mustMatchPattern = jme.subvars(settings.mustMatchPatternString || '', scope);
        this.markingScope = new jme.Scope(this.getScope());
        this.markingScope.variables = {};
        return settings.correctAnswer;
    },
    /** Save a copy of the student's answer as entered on the page, for use in marking.
     */
    setStudentAnswer: function() {
        this.studentAnswer = this.stagedAnswer;
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the custom marking algorithm.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        return new Numbas.jme.types.TString(this.studentAnswer);
    },

    /** Add a value generator expression to the list in this part's settings.
     *
     * @param {string} name
     * @param {JME} expr
     */
    addValueGenerator: function(name, expr) {
        try {
            var expression = new jme.types.TExpression(expr);
            if(expression.tree) {
                this.settings.valueGenerators[name] = expression;
            }
        } catch(e) {
            this.error('part.jme.invalid value generator expression', {name: name, expr: expr, message: e.message}, e);
        }
    }
};
['resume', 'finaliseLoad', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    JMEPart.prototype[method] = util.extend(Part.prototype[method], JMEPart.prototype[method]);
});
Numbas.partConstructors['jme'] = util.extend(Part, JMEPart);
});
;
Numbas.queueScript('display/parts/matrix',['display-base','part-display','util','jme','jme-display'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    var util = Numbas.util;
    /** Display code for a {@link Numbas.parts.MatrixEntryPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name MatrixEntryPartDisplay
     * @memberof Numbas.display
     */
    display.MatrixEntryPartDisplay = function()
    {
        var p = this.part;
        /** The student's current (not necessarily submitted) answer
         * @member {observable|string} studentAnswer
         * @memberof Numbas.display.MatrixEntryPartDisplay
         */
        this.studentAnswer = Knockout.observable(p.studentAnswer);
        /** The correct answer
         * @member {observable|matrix} correctAnswer
         * @memberof Numbas.display.MatrixEntryPartDisplay
         */
        this.correctAnswer = Knockout.observable();
        this.updateCorrectAnswer(p.getCorrectAnswer(p.getScope()));
        this.correctAnswerRows = ko.computed(function() {
            return this.correctAnswer().rows;
        },this);
        this.correctAnswerColumns = ko.computed(function() {
            return this.correctAnswer().columns;
        },this);
        this.studentAnswerRows = Knockout.observable(p.settings.numRows || this.correctAnswerRows());
        this.studentAnswerColumns = Knockout.observable(p.settings.numColumns || this.correctAnswerColumns());
        this.allowResize = Knockout.observable(p.settings.allowResize);
        this.minColumns = Knockout.observable(p.settings.minColumns);
        this.maxColumns = Knockout.observable(p.settings.maxColumns);
        this.minRows = Knockout.observable(p.settings.minRows);
        this.maxRows = Knockout.observable(p.settings.maxRows);
        this.prefilledCells = Knockout.observable(p.settings.prefilledCells);
        Knockout.computed(function() {
            var oldRows, oldColumns, oldMatrix;
            if(p.stagedAnswer) {
                oldRows = p.stagedAnswer.rows;
                oldColumns = p.stagedAnswer.columns;
                oldMatrix = p.stagedAnswer;
            }
            var newRows = this.studentAnswerRows();
            var newColumns = this.studentAnswerColumns();
            var newMatrix = this.studentAnswer();
            if(newRows != oldRows || newColumns != oldColumns || !util.arraysEqual(oldMatrix,newMatrix)) {
                var m = this.studentAnswer();
                m.rows = this.studentAnswerRows();
                m.columns = this.studentAnswerColumns();
                p.storeAnswer(m);
            }
        },this);
    }
    display.MatrixEntryPartDisplay.prototype =
    {
        updateCorrectAnswer: function(answer) {
            this.correctAnswer(answer);
        },
        restoreAnswer: function(studentAnswer) {
            this.studentAnswerRows(studentAnswer.length || 1);
            this.studentAnswerColumns(studentAnswer.length ? studentAnswer[0].length : 1);
            this.studentAnswer(studentAnswer);
        }
    };
    display.MatrixEntryPartDisplay = extend(display.PartDisplay,display.MatrixEntryPartDisplay,true);
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.MatrixEntryPart} object */
Numbas.queueScript('parts/matrixentry', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking_scripts'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var math = Numbas.math;
var Part = Numbas.parts.Part;
/** Matrix entry part - student enters a matrix of numbers.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var MatrixEntryPart = Numbas.parts.MatrixEntryPart = function(path, question, parentPart, store) {
    var settings = this.settings;
    util.copyinto(MatrixEntryPart.prototype.settings, settings);
}
MatrixEntryPart.prototype = /** @lends Numbas.parts.MatrixEntryPart.prototype */
{
    loadFromXML: function(xml) {
        var settings = this.settings;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        tryGetAttribute(settings, xml, 'answer', ['correctanswer'], ['correctAnswerString'], {string:true});
        tryGetAttribute(settings, xml, 'answer',
            [
                'correctanswerfractions',
                'rows',
                'columns',
                'allowresize',
                'mincolumns',
                'maxcolumns',
                'minrows',
                'maxrows',
                'prefilledcells',
                'tolerance',
                'markpercell',
                'allowfractions'
            ],
            [
                'correctAnswerFractions',
                'numRowsString',
                'numColumnsString',
                'allowResize',
                'minColumnsString',
                'maxColumnsString',
                'minRowsString',
                'maxRowsString',
                'prefilledCellsString',
                'toleranceString',
                'markPerCell',
                'allowFractions'
            ]
        );
        tryGetAttribute(settings, xml, 'answer/precision', ['type', 'partialcredit', 'strict'], ['precisionType', 'precisionPC', 'strictPrecision']);
        tryGetAttribute(settings, xml, 'answer/precision', 'precision', 'precisionString', {'string':true});
        var messageNode = xml.selectSingleNode('answer/precision/message');
        if(messageNode) {
            settings.precisionMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
        }
    },
    loadFromJSON: function(data) {
        var settings = this.settings;
        var tryLoad = Numbas.json.tryLoad;
        tryLoad(data,
            [
                'correctAnswer',
                'correctAnswerFractions',
                'numRows',
                'numColumns',
                'allowResize',
                'minColumns',
                'maxColumns',
                'minRows',
                'maxRows',
                'prefilledCells',
                'tolerance',
                'markPerCell',
                'allowFractions'
            ],
            settings,
            [
                'correctAnswerString',
                'correctAnswerFractions',
                'numRowsString',
                'numColumnsString',
                'allowResize',
                'minColumnsString',
                'maxColumnsString',
                'minRowsString',
                'maxRowsString',
                'prefilledCellsString',
                'toleranceString',
                'markPerCell',
                'allowFractions'
            ]
        );
        tryLoad(data, ['precisionType', 'precision', 'precisionPartialCredit', 'precisionMessage', 'strictPrecision'], settings, ['precisionType', 'precisionString', 'precisionPC', 'precisionMessage', 'strictPrecision']);
        settings.precisionPC /= 100;
    },
    resume: function() {
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        if(pobj.studentAnswer !== undefined) {
            this.stagedAnswer = pobj.studentAnswer.matrix;
            this.stagedAnswer.rows = pobj.studentAnswer.rows;
            this.stagedAnswer.columns = pobj.studentAnswer.columns;
        }
    },
    finaliseLoad: function() {
        var p = this;
        var settings = this.settings;
        var scope = this.getScope();

        /** Evaluate a setting given as a JME expression.
         *
         * @param {JME} setting
         */
        function eval_setting(setting) {
            var expr = jme.subvars(settings[setting + 'String'] + '', scope);
            var value = scope.evaluate(expr);
            settings[setting] = value === null ? value : jme.unwrapValue(value);
        }
        ['numRows', 'numColumns', 'tolerance', 'prefilledCells'].map(eval_setting);
        if(settings.allowResize) {
            ['minColumns', 'maxColumns', 'minRows', 'maxRows'].map(eval_setting);
        }

        settings.tolerance = Math.max(settings.tolerance, 0.00000000001);
        if(settings.precisionType != 'none') {
            settings.allowFractions = false;
        }

        this.getCorrectAnswer(scope);

        var prefilled_fractions = settings.allowFractions && settings.correctAnswerFractions;
        if(settings.prefilledCellsString) {
            var prefilledCells = jme.castToType(scope.evaluate(jme.subvars(settings.prefilledCellsString + '', scope)), 'list');
            if(prefilledCells) {
                settings.prefilledCells = prefilledCells.value.map(function(row) {
                    row = jme.castToType(row, 'list');
                    return row.value.map(function(cell) {
                        if(jme.isType(cell, 'rational') && !prefilled_fractions) {
                            cell = jme.castToType(cell, 'decimal');
                        }
                        if(jme.isType(cell, 'string')) {
                            var s = jme.castToType(cell, 'string');
                            return s.value;
                        }
                        if(jme.isType(cell, 'number')) {
                            if(prefilled_fractions) {
                                var frac;
                                if(jme.isType(cell, 'rational')) {
                                    frac = jme.castToType(cell, 'rational').value;
                                } else if(jme.isType(cell, 'decimal')) {
                                    cell = jme.castToType(cell, 'decimal');
                                    frac = math.Fraction.fromDecimal(cell.value.re);
                                } else {
                                    var n = jme.castToType(cell, 'number');
                                    var approx = math.rationalApproximation(n.value.toNumber(), 35);
                                    frac = new math.Fraction(approx[0], approx[1]);
                                }
                                return frac.toString();
                            } else {
                                cell = jme.castToType(cell, 'number');
                                return math.niceRealNumber(cell.value, {precisionType: settings.precisionType, precision: settings.precision, style: settings.correctAnswerStyle});
                            }
                        }
                        p.error('part.matrix.invalid type in prefilled', {type: cell.type});
                    })
                });
            }
        }

        this.studentAnswer = [];
        for(var i = 0;i < this.settings.numRows;i++) {
            var row = [];
            for(var j = 0;j < this.settings.numColumns;j++) {
                row.push('');
            }
            this.studentAnswer.push(row);
        }
        if(!settings.allowResize && (settings.correctAnswer.rows != settings.numRows || settings.correctAnswer.columns != settings.numColumns)) {
            var correctSize = settings.correctAnswer.rows + '×' + settings.correctAnswer.columns;
            var answerSize = settings.numRows + '×' + settings.numColumns;
            throw(new Numbas.Error('part.matrix.size mismatch', {correct_dimensions:correctSize, input_dimensions:answerSize}));
        }
    },
    initDisplay: function() {
        this.display = new Numbas.display.MatrixEntryPartDisplay(this);
    },
    /** The student's last submitted answer.
     *
     * @type {matrix}
     */
    studentAnswer: null,
    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript(Numbas.raw_marking_scripts.matrixentry, null, this.getScope());
    },
    /** Properties set when part is generated.
     *
     * Extends {@link Numbas.parts.Part#settings}.
     *
     * @property {matrix} correctAnswer - The correct answer to the part.
     * @property {JME} numRows - Default number of rows in the student's answer.
     * @property {JME} numColumns - Default number of columns in the student's answer.
     * @property {boolean} allowResize - Allow the student to change the dimensions of their answer?
     * @property {JME} tolerance - Allowed margin of error in each cell (if student's answer is within +/- `tolerance` of the correct answer (after rounding to , mark it as correct.
     * @property {boolean} markPerCell - Should the student gain marks for each correct cell (true), or only if they get every cell right (false)?
     * @property {boolean} allowFractions - Can the student enter a fraction as their answer for a cell?
     * @property {string} precisionType - Type of precision restriction to apply: `none`, `dp` - decimal places, or `sigfig` - significant figures.
     * @property {number} precision - How many decimal places or significant figures to require.
     * @property {number} precisionPC - Partial credit to award if the answer is between `minvalue` and `maxvalue` but not given to the required precision.
     * @property {string} precisionMessage - Message to display in the marking feedback if their answer was not given to the required precision.
     * @property {boolean} strictPrecision - Must the student give exactly the required precision? If false, omitting trailing zeros is allowed.
     */
    settings: {
        correctAnswer: null,
        correctAnswerFractions: false,
        numRowsString: '3',
        numRows: 3,
        numColumnsString: '3',
        numColumns: 3,
        allowResize: true,
        toleranceString: '0',
        tolerance: 0,
        markPerCell: false,
        allowFractions: false,
        precisionType: 'none',    //'none', 'dp' or 'sigfig'
        precisionString: '0',
        precision: 0,
        precisionPC: 0,
        precisionMessage: R('You have not given your answer to the correct precision.'),
        strictPrecision: true,
        minRowsString: '0',
        maxRowsString: '0',
        minColumnsString: '0',
        maxColumnsString: '0',
        minRows: 0,
        maxRows: 0,
        minColumns: 0,
        maxColumns: 0,
        prefilledCellsString: '',
        prefilledCells: []
    },
    /** The name of the input widget this part uses, if any.
     *
     * @returns {string}
     */
    input_widget: function() {
        return 'matrix';
    },
    /** Options for this part's input widget.
     *
     * @returns {object}
     */
    input_options: function() {
        return {
            allowFractions: this.settings.allowFractions,
            allowedNotationStyles: ['plain', 'en', 'si-en'],
            allowResize: this.settings.allowResize,
            numRows: this.settings.numRows,
            numColumns: this.settings.numColumns,
            minColumns: this.settings.minColumns,
            maxColumns: this.settings.maxColumns,
            minRows: this.settings.minRows,
            maxRows: this.settings.maxRows,
            parseCells: false
        };
    },
    /** Compute the correct answer, based on the given scope.
     *
     * @param {Numbas.jme.Scope} scope
     * @returns {matrix}
     */
    getCorrectAnswer: function(scope) {
        var settings = this.settings;
        var correctAnswer = jme.subvars(settings.correctAnswerString, scope);
        correctAnswer = jme.evaluate(correctAnswer, scope);
        if(correctAnswer && correctAnswer.type == 'matrix') {
            settings.correctAnswer = correctAnswer.value;
        } else if(correctAnswer && correctAnswer.type == 'vector') {
            settings.correctAnswer = Numbas.vectormath.toMatrix(correctAnswer.value);
        } else {
            this.error('part.setting not present', {property:'correct answer'});
        }
        settings.precision = jme.subvars(settings.precisionString, scope);
        settings.precision = jme.evaluate(settings.precision, scope).value;

        var correctInput = settings.correctAnswer.map(function(row) {
            return row.map(function(c) {
                if(settings.allowFractions) {
                    var f = math.Fraction.fromFloat(c);
                    return f.toString();
                }
                return math.niceRealNumber(c, {precisionType: settings.precisionType, precision:settings.precision, style: settings.correctAnswerStyle});
            });
        });
        correctInput.rows = settings.correctAnswer.rows;
        correctInput.columns = settings.correctAnswer.columns;
        return correctInput;
    },
    /** Save a copy of the student's answer as entered on the page, for use in marking.
     */
    setStudentAnswer: function() {
        if(this.stagedAnswer !== undefined) {
            var m = this.stagedAnswer;
            this.studentAnswerRows = m.length;
            this.studentAnswerColumns = this.studentAnswerRows > 0 ? m[0].length : 0;
        } else {
            this.studentAnswerRows = 0;
            this.studentAnswerColumns = 0;
        }
        this.studentAnswer = this.stagedAnswer;
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the marking script.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        return jme.wrapValue(this.studentAnswer);
    }
};
['resume', 'finaliseLoad', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    MatrixEntryPart.prototype[method] = util.extend(Part.prototype[method], MatrixEntryPart.prototype[method]);
});
Numbas.partConstructors['matrix'] = util.extend(Part, MatrixEntryPart);
});
;
Numbas.queueScript('display/parts/multipleresponse',['display-base','part-display','util'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    var util = Numbas.util;
    /** Display code for a {@link Numbas.parts.MultipleResponsePart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name MultipleResponsePartDisplay
     * @memberof Numbas.display
     */
    display.MultipleResponsePartDisplay = function()
    {
        var pd = this;
        var p = this.part;

        function makeTicker(answer,choice) {
            var obs = Knockout.observable(p.ticks[answer][choice]);
            Knockout.computed(function() {
                p.storeTick({answer:answer, choice:choice, ticked:obs()});
            },p);
            return obs;
        }
        function makeRadioTicker(choice) {
            var obs = Knockout.observable(null);
            for(var i=0;i<p.numAnswers;i++) {
                if(p.ticks[i][choice])
                    obs(i);
            }
            Knockout.computed(function() {
                var answer = parseInt(obs());
                p.storeTick({answer:answer, choice:choice, ticked: true});
            },p);
            return obs;
        }
        function makeCheckboxTicker(answer,choice) {
            var obs = Knockout.observable(p.ticks[answer][choice]);
            Knockout.computed(function() {
                p.storeTick({answer:answer, choice:choice, ticked:obs()});
            });
            return obs;
        }
        function makeTickFeedback(answer,choice) {
            return Knockout.computed(function() {
                var ticks = pd.ticks;
                switch(p.settings.markingMethod) {
                    case 'sum ticked cells':
                        var checked = p.settings.displayType=='radiogroup' ? pd.ticks[choice]()==answer : pd.ticks[answer][choice];
                        return {
                            checked: checked, 
                            correct: checked && p.settings.matrix[answer][choice]>0
                        }
                    case 'score per matched cell':
                    case 'all-or-nothing':
                        return {
                            checked: pd.layout[answer][choice],
                            correct: pd.ticks[answer][choice]()==(p.settings.matrix[answer][choice]>0)
                        }
                }
            },pd);
        }
        this.layout = util.copyarray(p.layout);
        this.showCellAnswerState = Knockout.pureComputed(function() {
            if(p.question && p.question.exam) {
                if(!p.question.exam.display.expectedAnswersRevealed()) {
                    return false;
                }
            }
            return p.settings.showCellAnswerState;
        },this);
        this.displayColumns = Knockout.observable(p.settings.displayColumns);
        switch(p.type) {
        case '1_n_2':
            this.showBlankOption = Knockout.observable(p.settings.showBlankOption);
            /** Index of student's current answer choice (not necessarily submitted)
             * @member {observable|number} studentAnswer
             * @memberof Numbas.display.MultipleResponsePartDisplay
             */
            this.studentAnswer = Knockout.observable(null);
            for(var i=0;i<p.numAnswers;i++) {
                if(p.ticks[i][0])
                    this.studentAnswer(i);
            }
            var oldAnswer = null;
            Knockout.computed(function() {
                if(this.studentAnswer()==='' && oldAnswer!==null) {
                    oldAnswer = null;
                    p.storeTick({answer:null, choice: 0});
                }
                var i = parseInt(this.studentAnswer());
                if(i!==oldAnswer && !isNaN(i)) {
                    p.storeTick({answer:i, choice:0});
                    oldAnswer = i;
                }
            },this);
            /** Index of the answer which gains the most marks
             * @member {observable|number} correctAnswer
             * @memberof Numbas.display.MultipleResponsePartDisplay
             */
            this.correctAnswer = Knockout.observable();
            break;
        case 'm_n_2':
            /** For each choice, has the student selected it?
             *
             * For m_n_2 parts, this is a list of booleans. For m_n_x radiogroup parts, it's a list of indices. For m_n_x checkbox parts, it's a 2d array of booleans.
             * @member {observable|boolean[]|number[]|Array.<Array.<boolean>>} ticks
             * @memberof Numbas.display.MultipleResponsePartDisplay
             */
            this.ticks = [];
            /** For each choice, should it be selected to get the most marks?
             *
             * For m_n_2 parts, this is a list of booleans. For m_n_x radiogroup parts, it's a list of indices. For m_n_x checkbox parts, it's a 2d array of booleans.
             * @member {observable|boolean[]|number[]|Array.<Array.<boolean>>} ticks
             * @memberof Numbas.display.MultipleResponsePartDisplay
             */
            this.correctTicks = Knockout.observableArray([]);
            for(var i=0; i<p.numAnswers; i++) {
                this.ticks[i] = makeTicker(i,0);
            }
            if(p.settings.warningType!='none') {
                Knockout.computed(function() {
                    this.removeWarnings();
                    var ticked = 0;
                    this.ticks.map(function(tick) {
                        ticked += tick() ? 1 : 0;
                    });
                    if(ticked<p.settings.minAnswers || ticked>p.settings.maxAnswers) {
                        p.giveWarning(R('part.mcq.wrong number of choices'));
                    };
                },this);
            }
            break;
        case 'm_n_x':
            this.correctTicks = Knockout.observableArray([]);
            switch(p.settings.displayType) {
            case 'radiogroup':
                this.ticks = [];
                for(var i=0; i<p.numChoices; i++) {
                    this.ticks.push(makeRadioTicker(i));
                    var maxj=-1,max=0;
                    for(var j=0;j<p.numAnswers; j++) {
                        if(maxj==-1 || p.settings.matrix[j][i]>max) {
                            maxj = j;
                            max = p.settings.matrix[j][i];
                        }
                    }
                    this.correctTicks.push(maxj);
                }
                break;
            case 'checkbox':
                this.ticks = [];
                for(var i=0; i<p.numAnswers; i++) {
                    var row = [];
                    this.ticks.push(row);
                    var correctRow = [];
                    this.correctTicks.push(correctRow);
                    for(var j=0; j<p.numChoices; j++) {
                        row.push(makeCheckboxTicker(i,j));
                        correctRow.push(p.settings.matrix[i][j]>0);
                    }
                }
                if(p.settings.warningType!='none') {
                    Knockout.computed(function() {
                        this.removeWarnings();
                        var ticked = 0;
                        this.ticks.map(function(row) {
                            row.map(function(tick) {
                                ticked += tick() ? 1 : 0;
                            });
                        });
                        if(ticked<p.settings.minAnswers || ticked>p.settings.maxAnswers) {
                            p.giveWarning(R('part.mcq.wrong number of choices'));
                        };
                    },this);
                }
                break;
            }
            this.tickFeedback = Knockout.observableArray([]);
            for(var i=0; i<p.numAnswers; i++) {
                var feedbackRow = [];
                this.tickFeedback.push(feedbackRow);
                for(var j=0; j<p.numChoices; j++) {
                    feedbackRow.push(makeTickFeedback(i,j));
                }
            }
            break;
        }
        this.updateCorrectAnswer(p.getCorrectAnswer(p.getScope()));
    }
    display.MultipleResponsePartDisplay.prototype =
    {
        alwaysShowWarnings: true,
        updateCorrectAnswer: function(answer) {
            var p = this.part;
            switch(p.type) {
            case '1_n_2':
                var maxi;
                for(var i=0;i<p.numAnswers;i++) {
                    if(answer[i][0]) {
                        maxi = i;
                        break;
                    }
                }
                this.correctAnswer(maxi+'');
                break;
            case 'm_n_2':
                this.correctTicks(answer.map((function(x){ return x[0]; })));
                break;
            case 'm_n_x':
                switch(p.settings.displayType) {
                case 'radiogroup':
                    var ticks = [];
                    for(var i=0; i<p.numChoices; i++) {
                        for(var j=0;j<p.numAnswers;j++) {
                            if(answer[j][i]) {
                                ticks.push(j);
                                break;
                            }
                        }
                    }
                    this.correctTicks(ticks);
                    break;
                case 'checkbox':
                    this.correctTicks(answer);
                    break;
                }
            }
        },

        restoreAnswer: function(ticks) {
            ticks = ticks || this.part.ticks;
            var part = this.part;
            switch(part.type) {
            case '1_n_2':
                var ticked = false;
                for(var i=0;i<part.numAnswers; i++) {
                    if(ticks && ticks[i][0]) {
                        this.studentAnswer(i+'');
                        ticked = true;
                    }
                }
                if(!ticked) {
                    this.studentAnswer(null);
                }
                break;
            case 'm_n_2':
                for(var i=0; i<part.numAnswers; i++) {
                    this.ticks[i](ticks && ticks[i][0]);
                }
                break;
            case 'm_n_x':
                switch(part.settings.displayType) {
                case 'radiogroup':
                    for(var i=0; i<part.numAnswers; i++) {
                        for(var j=0; j<part.numChoices; j++) {
                            if(ticks && ticks[i][j]) {
                                this.ticks[j](i+'');
                            }
                        }
                    }
                    break;
                case 'checkbox':
                    for(var i=0; i<part.numAnswers; i++) {
                        for(var j=0; j<part.numChoices; j++) {
                            this.ticks[i][j](ticks && ticks[i][j]);
                        }
                    }
                    break;
                }
                break;
            }
        }
    };
    display.MultipleResponsePartDisplay = extend(display.PartDisplay,display.MultipleResponsePartDisplay,true);
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.MultipleResponsePart} object */
Numbas.queueScript('parts/multipleresponse', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking_scripts'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var math = Numbas.math;
var Part = Numbas.parts.Part;
/** Multiple choice part - either pick one from a list, pick several from a list, or match choices with answers (2d grid, either pick one from each row or tick several from each row)
 *
 * Types:
 * - `1_n_2`: pick one from a list. Represented as N answers, 1 choice
 * - `m_n_2`: pick several from a list. Represented as N answers, 1 choice
 * - `m_n_x`: match choices (rows) with answers (columns). Represented as N answers, X choices.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @augments Numbas.parts.Part
 * @memberof Numbas.parts
 */
var MultipleResponsePart = Numbas.parts.MultipleResponsePart = function(path, question, parentPart, store) {
    var settings = this.settings;
    util.copyinto(MultipleResponsePart.prototype.settings, settings);
}
MultipleResponsePart.prototype = /** @lends Numbas.parts.MultipleResponsePart.prototype */
{
    loadFromXML: function(xml) {
        var p = this;
        var settings = this.settings;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        var scope = this.getScope();
        //get number of answers and answer order setting
        if(this.type == '1_n_2' || this.type == 'm_n_2') {
            // the XML for these parts lists the options in the <choices> tag, but it makes more sense to list them as answers
            // so swap "answers" and "choices"
            // this all stems from an extremely bad design decision made very early on
            this.flipped = true;
        } else {
            this.flipped = false;
        }
        //work out marks available
        tryGetAttribute(settings, xml, '.', 'showCellAnswerState');
        tryGetAttribute(settings, xml, 'marking', 'method', 'markingMethod');
        tryGetAttribute(settings, xml, 'marking/maxmarks', 'enabled', 'maxMarksEnabled');
        if(this.type == '1_n_2') {
            settings.maxMarksEnabled = false;
        }
        if(settings.maxMarksEnabled) {
            tryGetAttribute(this, xml, 'marking/maxmarks', 'value', 'marks');
        } else {
            tryGetAttribute(this, xml, '.', 'marks');
        }
        //get minimum marks setting
        tryGetAttribute(settings, xml, 'marking/minmarks', 'enabled', 'minMarksEnabled');
        if(this.type == '1_n_2') {
            settings.minMarksEnabled = false;
        }
        if(settings.minMarksEnabled) {
            tryGetAttribute(settings, xml, 'marking/minmarks', 'value', 'minimumMarks');
        }
        //get restrictions on number of choices
        var choicesNode = xml.selectSingleNode('choices');
        if(!choicesNode) {
            this.error('part.mcq.choices missing');
        }
        tryGetAttribute(settings, null, choicesNode, ['minimumexpected', 'maximumexpected', 'shuffle', 'displayType', 'displayColumns', 'showBlankOption'], ['minAnswersString', 'maxAnswersString', 'shuffleChoices']);
        var choiceNodes = choicesNode.selectNodes('choice');
        var answersNode, answerNodes;
        if(this.type == '1_n_2' || this.type == 'm_n_2') {
            // the XML for these parts lists the options in the <choices> tag, but it makes more sense to list them as answers
            // so swap "answers" and "choices"
            // this all stems from an extremely bad design decision made very early on
            this.numAnswers = choiceNodes.length;
            this.numChoices = 1;
            answersNode = choicesNode;
            choicesNode = null;
        } else {
            this.numChoices = choiceNodes.length;
            answersNode = xml.selectSingleNode('answers');
            if(answersNode) {
                tryGetAttribute(settings, null, answersNode, 'shuffle', 'shuffleAnswers');
                answerNodes = answersNode.selectNodes('answer');
                this.numAnswers = answerNodes.length;
            }
        }
        var def;
        /** Load the definition of the choice or answer labels.
         *
         * @param {JME} def
         * @param {Numbas.jme.Scope} scope
         * @param {Element} topNode - Parent element of the list of labels
         * @param {string} nodeName - 'choice' or 'answer'.
         * @returns {number} - The number of items.
         */
        function loadDef(def, scope, topNode, nodeName) {
            var values = jme.evaluate(def, scope);
            if(!jme.isType(values, 'list')) {
                p.error('part.mcq.options def not a list', {properties: nodeName});
            }
            var numValues = jme.castToType(values, 'list').value.length;
            values.value.map(function(value) {
                var node = xml.ownerDocument.createElement(nodeName);
                var content = xml.ownerDocument.createElement('content');
                var span = xml.ownerDocument.createElement('span');
                content.appendChild(span);
                node.appendChild(content);
                topNode.appendChild(node);
                /** Load a string representing the text of a label into the `span` element for this label.
                 *
                 * @param {string} str
                 */
                function load_string(str) {
                    var d = document.createElement('d');
                    d.innerHTML = str;
                    var newNode;
                    try {
                        newNode = xml.ownerDocument.importNode(d, true);
                    } catch {
                        d = Numbas.xml.dp.parseFromString('<d>' + str.replace(/&(?!amp;)/g, '&amp;') + '</d>', 'text/xml').documentElement;
                        newNode = xml.ownerDocument.importNode(d, true);
                    }
                    while(newNode.childNodes.length) {
                        span.appendChild(newNode.childNodes[0]);
                    }
                }
                if(jme.isType(value, 'string')) {
                    load_string(jme.castToType(value, 'string').value);
                } else if(jme.isType(value, 'number')) {
                    load_string(Numbas.math.niceRealNumber(jme.castToType(value, 'string')));
                } else if(jme.isType(value, 'html')) {
                    var selection = jme.castToType(value, 'html').value;
                    for(let i = 0; i < selection.length; i++) {
                        try {
                            span.appendChild(xml.ownerDocument.importNode(selection[i], true));
                        } catch {
                            var d = Numbas.xml.dp.parseFromString('<d>' + selection[i].outerHTML + '</d>', 'text/xml').documentElement;
                            var newNode = xml.ownerDocument.importNode(d, true);
                            while(newNode.childNodes.length) {
                                span.appendChild(newNode.childNodes[0]);
                            }
                        }
                    }
                } else {
                    span.appendChild(xml.ownerDocument.createTextNode(value));
                }
            });
            return numValues;
        }
        if(def = answersNode.getAttribute('def')) {
            settings.answersDef = def;
            var nodeName = this.flipped ? 'choice' : 'answer';
            loadDef(settings.answersDef, scope, answersNode, nodeName);
            answerNodes = answersNode.selectNodes(nodeName);
            this.numAnswers = answerNodes.length;
        }
        if(choicesNode && (def = choicesNode.getAttribute('def'))) {
            settings.choicesDef = def;
            loadDef(settings.choicesDef, scope, choicesNode, 'choice');
            choiceNodes = choicesNode.selectNodes('choice');
            this.numChoices = choiceNodes.length;
        }
        //get warning type and message for wrong number of choices
        var warningNode = xml.selectSingleNode('marking/warning');
        if(warningNode) {
            tryGetAttribute(settings, null, warningNode, 'type', 'warningType');
        }
        if(this.type == 'm_n_x') {
            var layoutNode = xml.selectSingleNode('layout');
            tryGetAttribute(settings, null, layoutNode, ['type', 'expression'], ['layoutType', 'layoutExpression']);
        }
        //fill marks matrix
        var markingMatrixNode = xml.selectSingleNode('marking/matrix');
        var markingMatrixString = markingMatrixNode.getAttribute('def');
        var useMarkingString = settings.answersDef || settings.choicesDef || (typeof markingMatrixString == "string");
        if(useMarkingString) {
            settings.markingMatrixString = markingMatrixString;
            if(!settings.markingMatrixString) {
                this.error('part.mcq.marking matrix string empty')
            }
        } else {
            var matrixNodes = xml.selectNodes('marking/matrix/mark');
            var markingMatrixArray = settings.markingMatrixArray = [];
            for(let i = 0; i < this.numAnswers; i++) {
                markingMatrixArray.push([]);
            }
            for(let i = 0; i < matrixNodes.length; i++) {
                const cell = {value: ""};
                tryGetAttribute(cell, null, matrixNodes[i], ['answerIndex', 'choiceIndex', 'value']);
                if(this.flipped) {
                    // possible answers are recorded as choices in the multiple choice types.
                    // switch the indices round, so we don't have to worry about this again
                    cell.answerIndex = cell.choiceIndex;
                    cell.choiceIndex = 0;
                }
                markingMatrixArray[cell.answerIndex][cell.choiceIndex] = cell.value;
            }
        }
        var distractors = [];
        for(let i = 0; i < this.numAnswers; i++) {
            var row = [];
            for(let j = 0;j < this.numChoices;j++) {
                row.push('');
            }
            distractors.push(row);
        }
        var distractorNodes = xml.selectNodes('marking/distractors/distractor');
        for(let i = 0; i < distractorNodes.length; i++) {
            const cell = {message: ""};
            tryGetAttribute(cell, null, distractorNodes[i], ['answerIndex', 'choiceIndex']);
            var elem = document.createElement('div');
            elem.innerHTML = Numbas.xml.transform(Numbas.xml.templates.question, distractorNodes[i]);
            elem = Numbas.jme.variables.DOMcontentsubvars(elem, scope);
            cell.message = elem.innerHTML;
            if(this.type == '1_n_2' || this.type == 'm_n_2') {
                // possible answers are recorded as choices in the multiple choice types.
                // switch the indices round, so we don't have to worry about this again
                cell.answerIndex = cell.choiceIndex;
                cell.choiceIndex = 0;
            }
            distractors[cell.answerIndex][cell.choiceIndex] = util.isNonemptyHTML(cell.message) ? cell.message : '';
        }
        settings.distractors = distractors;
    },
    loadFromJSON: function(data) {
        var settings = this.settings;
        var tryLoad = Numbas.json.tryLoad;
        var scope = this.getScope();
        //get number of answers and answer order setting
        if(this.type == '1_n_2' || this.type == 'm_n_2') {
            this.flipped = true;
        } else {
            this.flipped = false;
        }
        if(this.type != '1_n_2') {
            tryLoad(data, ['maxMarks'], this, ['marks']);
        }
        tryLoad(data, ['minMarks', 'markingMethod'], settings, ['minimumMarks', 'markingMethod']);
        tryLoad(data, ['minAnswers', 'maxAnswers', 'shuffleChoices', 'shuffleAnswers', 'displayType', 'displayColumns', 'showBlankOption'], settings, ['minAnswersString', 'maxAnswersString', 'shuffleChoices', 'shuffleAnswers', 'displayType', 'displayColumns', 'showBlankOption']);
        tryLoad(data, ['warningType'], settings);
        tryLoad(data.layout, ['type', 'expression'], settings, ['layoutType', 'layoutExpression']);
        if('choices' in data) {
            if(typeof(data.choices) == 'string') {
                var choices = jme.evaluate(data.choices, scope);
                if(!choices || !jme.isType(choices, 'list')) {
                    this.error('part.mcq.options def not a list', {properties: 'choice'});
                }
                settings.choices = jme.unwrapValue(jme.castToType(choices, 'list'));
            } else {
                settings.choices = data.choices;
            }
            this.numChoices = settings.choices.length;
        }
        if('answers' in data) {
            if(typeof(data.answers) == 'string') {
                var answers = jme.evaluate(data.answers, scope);
                if(!answers || !jme.isType(answers, 'list')) {
                    this.error('part.mcq.options def not a list', {properties: 'answer'});
                }
                settings.answers = jme.unwrapValue(jme.castToType(answers, 'list'));
            } else {
                settings.answers = data.answers;
            }
            this.numAnswers = settings.answers.length;
        }
        if(this.flipped) {
            this.numAnswers = 1;
        }
        if(typeof(data.matrix) == 'string') {
            settings.markingMatrixString = data.matrix;
        } else {
            settings.markingMatrixArray = data.matrix.map(function(row) {
                return typeof(row) == 'object' ? row : [row]
            });
            if(!this.flipped) {
                var m = settings.markingMatrixArray;
                m.rows = this.numChoices;
                m.columns = this.numAnswers;
                settings.markingMatrixArray = Numbas.matrixmath.transpose(settings.markingMatrixArray);
            }
        }
        if(this.flipped) {
            this.numAnswers = this.numChoices;
            this.numChoices = 1;
            this.answers = this.choices;
            this.choices = null;
        }
        tryLoad(data, ['distractors'], settings);
        if(settings.distractors && (this.type == '1_n_2' || this.type == 'm_n_2')) {
            settings.distractors = settings.distractors.map(function(d) {
                return [d]
            });
        }
        if(!settings.distractors) {
            settings.distractors = [];
            for(let i = 0;i < this.numAnswers; i++) {
                var row = [];
                for(let j = 0;j < this.numChoices; j++) {
                    row.push('');
                }
                settings.distractors.push(row);
            }
        }
    },
    resume: function() {
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        if(this.type == 'm_n_x') {
            this.shuffleChoices = pobj.shuffleChoices;
        } else {
            this.shuffleChoices = [0];
        }
        this.shuffleAnswers = pobj.shuffleAnswers;
        this.ticks = pobj.studentAnswer;
        this.stagedAnswer = [];
        for(let i = 0; i < this.numAnswers; i++) {
            this.stagedAnswer.push([]);
            for(var j = 0; j < this.numChoices; j++) {
                this.stagedAnswer[i].push(pobj.studentAnswer[i][j] || false);
            }
        }
    },
    finaliseLoad: function() {
        var settings = this.settings;
        var scope = this.getScope();
        if(this.type == 'm_n_2') {
            settings.displayType = 'checkbox';
        }
        if(settings.displayType == 'radiogroup') {
            settings.markingMethod = 'sum ticked cells';
        }
        //get number of answers and answer order setting
        if(this.type == '1_n_2' || this.type == 'm_n_2') {
            settings.shuffleAnswers = settings.shuffleChoices;
            settings.shuffleChoices = false;
        }
        this.shuffleChoices = [];
        if(settings.shuffleChoices) {
            this.shuffleChoices = math.deal(this.numChoices);
        } else {
            this.shuffleChoices = math.range(this.numChoices);
        }
        this.shuffleAnswers = [];
        if(settings.shuffleAnswers) {
            this.shuffleAnswers = math.deal(this.numAnswers);
        } else {
            this.shuffleAnswers = math.range(this.numAnswers);
        }
        this.marks = util.parseNumber(this.marks) || 0;
        settings.minimumMarks = util.parseNumber(settings.minimumMarks) || 0;
        var minAnswers = jme.subvars(settings.minAnswersString, scope);
        minAnswers = jme.evaluate(minAnswers, scope);
        try {
            settings.minAnswers = jme.castToType(minAnswers, 'number').value;
        } catch {
            this.error('part.setting not present', {property: 'minimum answers'});
        }
        var maxAnswers = jme.subvars(settings.maxAnswersString, scope);
        maxAnswers = jme.evaluate(maxAnswers, scope);
        try {
            settings.maxAnswers = jme.castToType(maxAnswers, 'number').value;
        } catch {
            this.error('part.setting not present', {property: 'maximum answers'});
        }
        // fill layout matrix
        var layout = this.layout = [];
        if(this.type == 'm_n_x') {
            let layoutFunction;
            if(settings.layoutType == 'expression') {
                // expression can either give a 2d array (list of lists) or a matrix
                // note that the list goes [row][column], unlike all the other properties of this part object, which go [column][row], i.e. they're indexed by answer then choice
                // it's easier for question authors to go [row][column] because that's how they're displayed, but it's too late to change the internals of the part to match that now
                // I have only myself to thank for this - CP
                var layoutMatrix = jme.unwrapValue(jme.evaluate(settings.layoutExpression, scope));
                layoutFunction = function(row, column) {
                    return layoutMatrix[row][column];
                };
            } else {
                layoutFunction = MultipleResponsePart.layoutTypes[settings.layoutType];
            }
            for(let i = 0;i < this.numAnswers;i++) {
                const row = [];
                for(let j = 0;j < this.numChoices;j++) {
                    row.push(layoutFunction(j, i));
                }
                layout.push(row);
            }
        } else {
            for(let i = 0;i < this.numAnswers;i++) {
                const row = [];
                for(let j = 0;j < this.numChoices;j++) {
                    row.push(true);
                }
                layout.push(row);
            }
        }
        if(this.type == '1_n_2') {
            settings.maxAnswers = 1;
        } else if(settings.maxAnswers == 0) {
            settings.maxAnswers = this.numAnswers * this.numChoices;
        }
        this.getCorrectAnswer(scope);
        if(this.marks == 0) {    //if marks not set explicitly
            var matrix = this.settings.matrix;
            var flat = [];
            switch(this.type) {
            case '1_n_2':
                for(let i = 0;i < matrix.length;i++) {
                    flat.push(matrix[i][0]);
                }
                break;
            case 'm_n_2':
                for(let i = 0;i < matrix.length;i++) {
                    flat.push(matrix[i][0]);
                }
                break;
            case 'm_n_x':
                if(settings.displayType == 'radiogroup') {
                    for(let i = 0;i < this.numChoices;i++) {
                        const row = [];
                        for(let j = 0;j < this.numAnswers;j++) {
                            row.push(matrix[j][i]);
                        }
                        row.sort(function(a, b) {
                            return a > b ? 1 : a < b ? -1 : 0
                        });
                        flat.push(row[row.length - 1]);
                    }
                } else {
                    for(let i = 0;i < matrix.length;i++) {
                        flat = flat.concat(matrix[i]);
                    }
                }
                break;
            }
            flat.sort(function(a, b) {
                return a > b ? 1 : a < b ? -1 : 0
            });
            for(let i = flat.length - 1; i >= 0 && flat.length - 1 - i < settings.maxAnswers && flat[i] > 0;i--) {
                this.marks += flat[i];
            }
        }
        //ticks array - which answers/choices are selected?
        this.ticks = [];
        this.stagedAnswer = [];
        for(let i = 0; i < this.numAnswers; i++) {
            this.ticks.push([]);
            this.stagedAnswer.push([]);
            for(var j = 0; j < this.numChoices; j++) {
                this.ticks[i].push(false);
                this.stagedAnswer[i].push(false);
            }
        }
    },
    initDisplay: function() {
        this.display = new Numbas.display.MultipleResponsePartDisplay(this);
    },
    /** Student's last submitted answer/choice selections.
     *
     * @type {Array.<Array.<boolean>>}
     */
    ticks: [],
    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript(Numbas.raw_marking_scripts.multipleresponse, null, this.getScope());
    },
    /** Number of choices - used by `m_n_x` parts.
     *
     * @type {number}
     */
    numChoices: 0,
    /** Number of answers.
     *
     * @type {number}
     */
    numAnswers: 0,
    /** Have choice and answers been swapped (because of the weird settings for 1_n_2 and m_n_2 parts)?
     *
     * @type {boolean}
     */
    flipped: false,
    /** Properties set when the part is generated.
     * Extends {@link Numbas.parts.Part#settings}.
     *
     * @property {string} markingMethod - The marking method to use for "choose several" or "match choices with answers" parts - one of `sum ticked cells`, `score per matched cell` or `all-or-nothing`.
     * @property {boolean} maxMarksEnabled - Is there a maximum number of marks the student can get?
     * @property {string} minAnswersString - Minimum number of responses the student must select, without variables substituted in.
     * @property {string} maxAnswersString - Maximum number of responses the student must select, without variables substituted in.
     * @property {number} minAnswers - Minimum number of responses the student must select. Generated from `minAnswersString`.
     * @property {number} maxAnswers - Maximum number of responses the student must select. Generated from `maxAnswersString`.
     * @property {string} shuffleChoices - Should the order of choices be randomised?
     * @property {string} shuffleAnswers - Should the order of answers be randomised?
     * @property {Array.<Array.<number>>} matrix - Marks for each answer/choice pair. Arranged as `matrix[answer][choice]`.
     * @property {string} displayType - How to display the response selectors. Can be `radiogroup`, `checkbox` or `dropdownlist`.
     * @property {number} displayColumns - How many columns to use to display the choices.
     * @property {string} warningType - What to do if the student picks the wrong number of responses? Either `none` (do nothing), `prevent` (don't let the student submit), or `warn` (show a warning but let them submit).
     * @property {string} layoutType - The kind of layout to use. See {@link Numbas.parts.MultipleResponsePart.layoutTypes}.
     * @property {JME} layoutExpression - Expression giving a 2d array or matrix describing the layout when `layoutType` is `'expression'`.
     */
    settings:
    {
        markingMethod: 'sum ticked cells',
        maxMarksEnabled: false,        //is there a maximum number of marks the student can get?
        minAnswersString: '0',                //minimum number of responses student must select
        maxAnswersString: '0',                //maximum ditto
        minAnswers: 0,                //minimum number of responses student must select
        maxAnswers: 0,                //maximum ditto
        shuffleChoices: false,
        shuffleAnswers: false,
        matrix: [],                    //marks matrix
        displayType: 'radiogroup',            //how to display the responses? can be: radiogroup, dropdownlist, buttonimage, checkbox, choicecontent
        warningType: 'none',                //what to do if wrong number of responses
        layoutType: 'all',
        layoutExpression: '',
    },
    /** The name of the input widget this part uses, if any.
     *
     * @returns {string}
     */
    input_widget: function() {
        switch(this.type) {
            case '1_n_2':
                if(this.settings.displayType == 'dropdownlist') {
                    return 'dropdown';
                } else {
                    return 'radios';
                }
            case 'm_n_2':
                return 'checkboxes';
            case 'm_n_x':
                return 'm_n_x';
        }
    },
    /** Options for this part's input widget.
     *
     * @returns {object}
     */
    input_options: function() {
        return {
            choices: this.settings.choices,
            answers: this.settings.answers,
            displayType: this.settings.displayType,
            layout: this.layout,
            answerAsArray: true
        };
    },
    /** Compute the correct answer, based on the given scope - a matrix filled with 1 for choices that should be selected, and 0 otherwise.
     *
     * @param {Numbas.jme.Scope} scope
     * @returns {matrix}
     */
    getCorrectAnswer: function(scope) {
        var settings = this.settings;
        var matrix = [];
        if(settings.markingMatrixString) {
            matrix = jme.evaluate(settings.markingMatrixString, scope);
            var sig = Numbas.jme.signature;
            var m;
            if(m = sig.type('matrix')([matrix])) {
                matrix = jme.castToType(matrix, m[0]).value;
            } else if(m = sig.listof(sig.type('number'))([matrix])) {
                matrix = jme.castToType(matrix, m[0]).value.map(function(e) {
                    return [e.value];
                });
                matrix.rows = matrix.length;
                matrix.columns = matrix[0].length;
            } else if(m = sig.listof(sig.listof(sig.type('number')))([matrix])) {
                matrix = jme.castToType(matrix, m[0]).value.map(function(row) {
                    return row.value.map(function(e) {
                        return e.value;
                    });
                });
                matrix.rows = matrix.length;
                matrix.columns = matrix[0].length;
            } else {
                this.error('part.mcq.matrix not a list');
            }
            if(this.flipped) {
                matrix = Numbas.matrixmath.transpose(matrix);
            }
            if(matrix.length != this.numChoices) {
                this.error('part.mcq.matrix wrong size');
            }
            // take into account shuffling;
            for(let i = 0;i < this.numChoices;i++) {
                if(matrix[i].length != this.numAnswers) {
                    this.error('part.mcq.matrix wrong size');
                }
            }
            matrix = Numbas.matrixmath.transpose(matrix);
        } else {
            for(let i = 0;i < this.numAnswers;i++) {
                const row = [];
                matrix.push(row);
                for(let j = 0;j < this.numChoices;j++) {
                    var value = settings.markingMatrixArray[i][j];
                    if(util.isFloat(value)) {
                        value = parseFloat(value);
                    } else {
                        if(value == '') {
                          this.error('part.mcq.matrix cell empty', {part:this.path, row:i, column:j});
                        }
                        try {
                          value = jme.castToType(jme.evaluate(value, scope), 'number').value;
                        } catch(e) {
                          this.error('part.mcq.matrix jme error', {part:this.path, row:i, column:j, error:e.message});
                        }
                        if(!util.isFloat(value)) {
                          this.error('part.mcq.matrix not a number', {part:this.path, row:i, column:j});
                        }
                        value = parseFloat(value);
                    }
                    row[j] = value;
                }
            }
        }
        for(let i = 0;i < matrix.length;i++) {
            var l = matrix[i].length;
            for(let j = 0;j < l;j++) {
                if(!this.layout[i][j]) {
                    matrix[i][j] = 0;
                }
            }
        }
        let max, maxi;
        switch(this.type) {
        case '1_n_2':
            max = 0;
            maxi = null;
            for(let i = 0;i < this.numAnswers;i++) {
                if(matrix[i][0] > max || maxi === null) {
                    max = matrix[i][0];
                    maxi = i;
                }
            }
            var best = [];
            for(let i = 0;i < this.numAnswers;i++) {
                best.push([i == maxi]);
            }
            settings.maxMatrix = best;
            break;
        case 'm_n_2':
                settings.maxMatrix = matrix.map(function(r) {
                    return [r[0] > 0];
                });
            break;
        case 'm_n_x':
            switch(this.settings.displayType) {
                case 'radiogroup':
                    var correctTicks = [];
                    for(let i = 0; i < this.numChoices; i++) {
                        let maxj = -1;
                        let max = 0;
                        for(let j = 0;j < this.numAnswers; j++) {
                            if(maxj == -1 || matrix[j][i] > max) {
                                maxj = j;
                                max = matrix[j][i];
                            }
                        }
                        correctTicks.push(maxj);
                    }
                    settings.maxMatrix = matrix.map(function(r, j) {
                        return r.map(function(c, i) {
                            return j == correctTicks[i];
                        })
                    });
                    break;
                case 'checkbox':
                    settings.maxMatrix = matrix.map(function(r) {
                        return r.map(function(c) {
                            return c > 0;
                        })
                    });
                    break;
            }
            break;
        }
        settings.matrix = matrix;
        return settings.maxMatrix;
    },
    /** Store the student's choices.
     *
     * @param {object} answer - Object with properties `answer` and `choice`, giving the index of the chosen item.
     */
    storeTick: function(answer) {
        //get choice and answer
        //in MR1_n_2 and MRm_n_2 parts, only the choiceindex matters
        var answerIndex = answer.answer;
        var choiceIndex = answer.choice;
        switch(this.settings.displayType) {
            case 'radiogroup':                            //for radiogroup parts, only one answer can be selected.
            case 'dropdownlist':
                for(let i = 0; i < this.numAnswers; i++) {
                    this.stagedAnswer[i][choiceIndex] = i === answerIndex;
                }
                break;
            default:
                this.stagedAnswer[answerIndex][choiceIndex] = answer.ticked;
        }
        this.storeAnswer(this.stagedAnswer);
    },
    /** Save a copy of the student's answer as entered on the page, for use in marking.
     */
    setStudentAnswer: function() {
        this.ticks = this.stagedAnswer === undefined ? this.ticks.map((row) => row.map(() => false)) : util.copyarray(this.stagedAnswer, true);
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the custom marking algorithm.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        return Numbas.jme.wrapValue(this.ticks);
    },
    /** Get the student's answer as a JME data type, to be used in error-carried-forward calculations.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    studentAnswerAsJME: function() {
        const o = [];
        switch(this.type) {
            case '1_n_2':
                for(let i = 0;i < this.numAnswers;i++) {
                    if(this.ticks[i][0]) {
                        return new jme.types.TNum(i);
                    }
                }
                break;
            case 'm_n_2':
                for(let i = 0;i < this.numAnswers;i++) {
                    o.push(new jme.types.TBool(this.ticks[i][0]));
                }
                return new jme.types.TList(o);
            case 'm_n_x':
                switch(this.settings.displayType) {
                    case 'radiogroup':
                        for(let choice = 0;choice < this.numChoices;choice++) {
                            for(let answer = 0;answer < this.numAnswers;answer++) {
                                if(this.ticks[choice][answer]) {
                                    o.push(new jme.types.TNum(answer));
                                    break;
                                }
                            }
                        }
                        return new jme.types.TList(o);
                    case 'checkbox':
                        return Numbas.jme.wrapValue(this.ticks);
                }
        }
    },
    /** Reveal the correct answers, and any distractor messages for the student's choices.
     * Extends {@link Numbas.parts.Part.revealAnswer}.
     */
    revealAnswer: function() {
        var row, message;
        for(let i = 0;i < this.numAnswers;i++) {
            for(let j = 0;j < this.numChoices;j++) {
                if((row = this.settings.distractors[i]) && (message = row[j])) {
                    this.markingComment(message);
                }
            }
        }
    },

    marking_parameters: function(studentAnswer, pre_submit_parameters) {
        var obj = Part.prototype.marking_parameters.apply(this, arguments);
        obj.shuffleChoices = jme.wrapValue(this.shuffleChoices);
        obj.shuffleAnswers = jme.wrapValue(this.shuffleAnswers);
        obj.layout = jme.wrapValue(this.layout);
        return obj;
    }
};
['resume', 'finaliseLoad', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    MultipleResponsePart.prototype[method] = util.extend(Part.prototype[method], MultipleResponsePart.prototype[method]);
});
['revealAnswer'].forEach(function(method) {
    MultipleResponsePart.prototype[method] = util.extend(MultipleResponsePart.prototype[method], Part.prototype[method]);
});

/** Layouts for multiple response types.
 *
 * @type {Object<Function>}
 */
Numbas.parts.MultipleResponsePart.layoutTypes = {
    all: function(row, column) {
        return true;
    },
    lowertriangle: function(row, column) {
        return row >= column;
    },
    strictlowertriangle: function(row, column) {
        return row > column;
    },
    uppertriangle: function(row, column) {
        return row <= column;
    },
    strictuppertriangle: function(row, column) {
        return row < column;
    }
};
Numbas.partConstructors['1_n_2'] = util.extend(Part, MultipleResponsePart);
Numbas.partConstructors['m_n_2'] = util.extend(Part, MultipleResponsePart);
Numbas.partConstructors['m_n_x'] = util.extend(Part, MultipleResponsePart);
});
;
Numbas.queueScript('display/parts/numberentry',['display-base','part-display','util'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    var util = Numbas.util;
    /** Display code for a {@link Numbas.parts.NumberEntryPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name NumberEntryPartDisplay
     * @memberof Numbas.display
     */
    display.NumberEntryPartDisplay = function()
    {
        var p = this.part;
        /** The student's current (not necessarily submitted) answer
         * @member {observable|string} studentAnswer
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.studentAnswer = Knockout.observable(p.studentAnswer);
        /** The correct answer
         * @member {observable|number} correctAnswer
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.correctAnswer = Knockout.observable('');
        this.updateCorrectAnswer(p.getCorrectAnswer(p.getScope()));
        Knockout.computed(function() {
            p.storeAnswer(this.studentAnswer());
        },this);
        /** Cleaned-up version of student answer (remove commas and trim whitespace)
         *
         * Also check for validity and give warnings
         * @member {observable|string} cleanStudentAnswer
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.cleanStudentAnswer = Knockout.computed(function() {
            var studentAnswer = p.cleanAnswer(this.studentAnswer());
            this.removeWarnings();
            if(studentAnswer=='')
                return '';
            if(p.settings.integerAnswer) {
                var dp = Numbas.math.countDP(studentAnswer);
                if(dp>0)
                    p.giveWarning(R('part.numberentry.answer not integer'));
            }
            if(!util.isNumber(studentAnswer,p.settings.allowFractions,p.settings.notationStyles,true)) {
                p.giveWarning(R(p.settings.allowFractions ? 'part.numberentry.answer not integer or decimal or fraction' : 'part.numberentry.answer not integer or decimal'));
                return '';
            }
            var n = util.parseNumber(studentAnswer,p.settings.allowFractions,p.settings.notationStyles,true);
            return n+'';
        },this);
        /** Does the input box have focus?
         * @member {observable|boolean} inputHasFocus
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.inputHasFocus = Knockout.observable(false);
        /** Give the input box focus
         * @member {function} focusInput
         * @method
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.focusInput = function() {
            this.inputHasFocus(true);
        }
        /** Some text describing how the student should enter their answer
         * @member {observable|string} inputHint
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.inputHint = Knockout.computed(function() {
            if(this.part.settings.precisionType=='none') {
                if(this.part.settings.mustBeReduced) {
                    return R('part.numberentry.give your answer as a reduced fraction');
                } else if(this.part.settings.allowFractions) {
                    return R('part.numberentry.write your answer as a fraction');
                } else {
                    return '';
                }
            } else {
                var precision = this.part.settings.precision;
                var precisionType = R('part.numberentry.precision type.'+this.part.settings.precisionType,{count:precision});
                if (precision === 0) {
                  return R('part.numberentry.give your answer to precision_0',{count: precision,precisionType: precisionType});
                } else {
                  return R('part.numberentry.give your answer to precision',{count: precision,precisionType: precisionType});
                }
            }
        },this);
        /** Show the input hint?
         * @member {observable|string} showInputHint
         * @memberof Numbas.display.NumberEntryPartDisplay
         */
        this.showInputHint = Knockout.computed(function() {
            if(!this.inputHint()) {
                return false;
            }
            if(this.part.settings.precisionType=='none') {
                return this.part.settings.showFractionHint;
            } else {
                return this.part.settings.showPrecisionHint;
            }
        },this);
    }
    display.NumberEntryPartDisplay.prototype =
    {
        updateCorrectAnswer: function(answer) {
            this.correctAnswer(answer);
        },
        restoreAnswer: function(studentAnswer) {
            this.studentAnswer(studentAnswer);
        }
    };
    display.NumberEntryPartDisplay = extend(display.PartDisplay,display.NumberEntryPartDisplay,true);
});
;
/*
Copyright 2011-15 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.parts.NumberEntryPart} object */
Numbas.queueScript('parts/numberentry', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking_scripts'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var math = Numbas.math;
var Part = Numbas.parts.Part;
/** Number entry part - student's answer must be within given range, and written to required precision.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var NumberEntryPart = Numbas.parts.NumberEntryPart = function(path, question, parentPart, store) {
    var settings = this.settings;
    util.copyinto(NumberEntryPart.prototype.settings, settings);
}
NumberEntryPart.prototype = /** @lends Numbas.parts.NumberEntryPart.prototype */
{
    loadFromXML: function(xml) {
        var settings = this.settings;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        tryGetAttribute(settings, xml, 'answer', ['minvalue', 'maxvalue'], ['minvalueString', 'maxvalueString'], {string:true});
        tryGetAttribute(settings, xml, 'answer', ['correctanswerfraction', 'correctanswerstyle', 'allowfractions', 'showfractionhint', 'displayanswer'], ['correctAnswerFraction', 'correctAnswerStyle', 'allowFractions', 'showFractionHint', 'displayAnswerString']);
        tryGetAttribute(settings, xml, 'answer', ['mustbereduced', 'mustbereducedpc'], ['mustBeReduced', 'mustBeReducedPC']);
        var answerNode = xml.selectSingleNode('answer');
        var notationStyles = answerNode.getAttribute('notationstyles');
        if(notationStyles) {
            settings.notationStyles = notationStyles.split(',');
        }
        tryGetAttribute(settings, xml, 'answer/precision', ['type', 'partialcredit', 'strict', 'showprecisionhint'], ['precisionType', 'precisionPC', 'strictPrecision', 'showPrecisionHint']);
        tryGetAttribute(settings, xml, 'answer/precision', 'precision', 'precisionString', {'string':true});
        var messageNode = xml.selectSingleNode('answer/precision/message');
        if(messageNode) {
            settings.precisionMessage = Numbas.xml.transform(Numbas.xml.templates.question, messageNode);
        }
    },
    loadFromJSON: function(data) {
        var settings = this.settings;
        var tryLoad = Numbas.json.tryLoad;
        if('answer' in data) {
            settings.minvalueString = settings.maxvalueString = data.answer + '';
        }
        tryLoad(data, ['minValue', 'maxValue'], settings, ['minvalueString', 'maxvalueString']);
        tryLoad(data, ['correctAnswerFraction', 'correctAnswerStyle', 'allowFractions'], settings);
        tryLoad(data, ['mustBeReduced', 'mustBeReducedPC'], settings);
        settings.mustBeReducedPC /= 100;
        tryLoad(data, ['notationStyles'], settings);
        tryLoad(data, ['precisionPartialCredit', 'strictPrecision', 'showPrecisionHint', 'showFractionHint', 'precision', 'precisionType', 'precisionMessage'], settings, ['precisionPC', 'strictPrecision', 'showPrecisionHint', 'showFractionHint', 'precisionString', 'precisionType', 'precisionMessage']);
        settings.precisionPC /= 100;
    },
    finaliseLoad: function() {
        var settings = this.settings;
        if(settings.precisionType != 'none') {
            settings.allowFractions = false;
        }
        try {
            this.getCorrectAnswer(this.getScope());
        } catch(e) {
            this.error(e.message, {}, e);
        }
        this.stagedAnswer = '';
    },
    initDisplay: function() {
        this.display = new Numbas.display.NumberEntryPartDisplay(this);
    },
    resume: function() {
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        this.stagedAnswer = pobj.studentAnswer + '';
    },
    /** The student's last submitted answer */
    studentAnswer: '',
    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript(Numbas.raw_marking_scripts.numberentry, null, this.getScope());
    },
    /** Properties set when the part is generated
     * Extends {@link Numbas.parts.Part#settings}
     *
     * @property {number} minvalueString - Definition of minimum value, before variables are substituted in.
     * @property {number} minvalue - Minimum value marked correct.
     * @property {number} maxvalueString - Definition of maximum value, before variables are substituted in.
     * @property {number} maxvalue - Maximum value marked correct.
     * @property {number} correctAnswerFraction - Display the correct answer as a fraction?
     * @property {boolean} allowFractions - Can the student enter a fraction as their answer?
     * @property {Array.<string>} notationStyles - Styles of notation to allow, other than `<digits>.<digits>`. See {@link Numbas.util.re_decimal}.
     * @property {string} displayAnswerString - The definition of the display answer, without variables substituted in.
     * @property {number} displayAnswer - Representative correct answer to display when revealing answers.
     * @property {string} precisionType - Type of precision restriction to apply: `none`, `dp` - decimal places, or `sigfig` - significant figures.
     * @property {number} precisionString - Definition of precision setting, before variables are substituted in.
     * @property {boolean} strictPrecision - Must the student give exactly the required precision? If false, omitting trailing zeros is allowed.
     * @property {number} precision - How many decimal places or significant figures to require.
     * @property {number} precisionPC - Partial credit to award if the answer is between `minvalue` and `maxvalue` but not given to the required precision.
     * @property {string} precisionMessage - Message to display in the marking feedback if their answer was not given to the required precision.
     * @property {boolean} mustBeReduced - Should the student enter a fraction in lowest terms.
     * @property {number} mustBeReducedPC - Partial credit to award if the answer is not a reduced fraction.
     * @property {boolean} showPrecisionHint - Show a hint about the required precision next to the input?
     * @property {boolean} showFractionHint - Show a hint that the answer should be a fraction next to the input?
     */
    settings:
    {
        minvalueString: '0',
        maxvalueString: '0',
        minvalue: 0,
        maxvalue: 0,
        correctAnswerFraction: false,
        allowFractions: false,
        notationStyles: ['plain', 'en', 'si-en'],
        displayAnswer: 0,
        precisionType: 'none',
        precisionString: '0',
        strictPrecision: false,
        precision: 0,
        precisionPC: 0,
        mustBeReduced: false,
        mustBeReducedPC: 0,
        precisionMessage: R('You have not given your answer to the correct precision.'),
        showPrecisionHint: true,
        showFractionHint: true
    },
    /** The name of the input widget this part uses, if any.
     *
     * @returns {string}
     */
    input_widget: function() {
        return 'string';
    },
    /** Options for this part's input widget.
     *
     * @returns {object}
     */
    input_options: function() {
        return {
            allowFractions: this.settings.allowFractions,
            allowedNotationStyles: this.settings.notationStyles
        };
    },
    /** Compute the correct answer, based on the given scope.
     *
     * @param {Numbas.jme.Scope} scope
     * @returns {string}
     */
    getCorrectAnswer: function(scope) {
        var settings = this.settings;
        var precision = jme.subvars(settings.precisionString, scope);
        settings.precision = scope.evaluate(precision).value;
        if(settings.precisionType == 'sigfig' && settings.precision <= 0) {
            throw(new Numbas.Error('part.numberentry.zero sig fig'));
        }
        if(settings.precisionType == 'dp' && settings.precision < 0) {
            throw(new Numbas.Error('part.numberentry.negative decimal places'));
        }

        var minvalue = jme.subvars(settings.minvalueString, scope);
        minvalue = scope.evaluate(minvalue);
        var ominvalue = minvalue;
        if(!minvalue) {
            this.error('part.setting not present', {property:R('minimum value')});
        }
        var maxvalue = jme.subvars(settings.maxvalueString, scope);
        maxvalue = scope.evaluate(maxvalue);
        var omaxvalue = maxvalue;
        if(!maxvalue) {
            this.error('part.setting not present', {property:R('maximum value')});
        }

        var dmin = jme.castToType(minvalue, 'decimal').value;
        var dmax = jme.castToType(maxvalue, 'decimal').value;
        if(dmax.lessThan(dmin)) {
            var tmp = dmin;
            dmin = dmax;
            dmax = tmp;
            tmp = minvalue;
            minvalue = maxvalue;
            maxvalue = tmp;
        }

        var isNumber = ominvalue.type == 'number' || omaxvalue.type == 'number';

        if(minvalue.type == 'number' && isFinite(minvalue.value)) {
            const size = Math.floor(Math.log10(Math.abs(minvalue.value)));
            minvalue = new jme.types.TNum(minvalue.value - Math.pow(10, size - 12));
            minvalue.precisionType = 'dp';
            minvalue.precision = 12 - size;
        }
        minvalue = jme.castToType(minvalue, 'decimal').value;
        settings.minvalue = minvalue;
        if(maxvalue.type == 'number' && isFinite(maxvalue.value)) {
            const size = Math.floor(Math.log10(Math.abs(maxvalue.value)));
            maxvalue = new jme.types.TNum(maxvalue.value + Math.pow(10, size - 12));
            maxvalue.precisionType = 'dp';
            maxvalue.precision = 12 - size;
        }
        maxvalue = jme.castToType(maxvalue, 'decimal').value;
        settings.maxvalue = maxvalue;


        var displayAnswer;
        if(settings.displayAnswerString) {
            displayAnswer = scope.evaluate(jme.subvars(settings.displayAnswerString + '', scope));
            if(settings.allowFractions && settings.correctAnswerFraction && jme.isType(displayAnswer, 'rational')) {
                displayAnswer = jme.unwrapValue(jme.castToType(displayAnswer, 'rational'));
                settings.displayAnswer = displayAnswer.toString();
            } else if(jme.isType(displayAnswer, 'decimal')) {
                displayAnswer = jme.unwrapValue(jme.castToType(displayAnswer, 'decimal'));
                settings.displayAnswer = math.niceNumber(displayAnswer.toNumber(), {precisionType: settings.precisionType, precision:settings.precision, style: settings.correctAnswerStyle});
            } else if(jme.isType(displayAnswer, 'number')) {
                displayAnswer = jme.unwrapValue(jme.castToType(displayAnswer, 'number'));
                settings.displayAnswer = math.niceNumber(displayAnswer, {precisionType: settings.precisionType, precision:settings.precision, style: settings.correctAnswerStyle});
            } else if(jme.isType(displayAnswer, 'string')) {
                settings.displayAnswer = jme.unwrapValue(jme.castToType(displayAnswer, 'string'));
            } else {
                this.error('part.numberentry.display answer wrong type', {want_type: 'string', got_type: displayAnswer.type});
            }
        } else {
            if(minvalue.re.isFinite()) {
                if(maxvalue.re.isFinite()) {
                    displayAnswer = minvalue.plus(maxvalue).dividedBy(2);
                } else {
                    displayAnswer = minvalue;
                }
            } else {
                if(maxvalue.re.isFinite()) {
                    displayAnswer = maxvalue;
                } else if(maxvalue.equals(minvalue)) {
                    displayAnswer = maxvalue;
                } else {
                    displayAnswer = new math.ComplexDecimal(new Decimal(0));
                }
            }
            if(settings.allowFractions && settings.correctAnswerFraction) {
                var frac;
                if(isNumber) {
                    var approx = math.rationalApproximation(displayAnswer.re.toNumber(), 35);
                    frac = new math.Fraction(approx[0], approx[1]);
                } else {
                    frac = math.Fraction.fromDecimal(displayAnswer.re);
                }
                settings.displayAnswer = frac.toString();
            } else {
                settings.displayAnswer = math.niceNumber(displayAnswer.toNumber(), {precisionType: settings.precisionType, precision:settings.precision, style: settings.correctAnswerStyle});
            }
        }
        return settings.displayAnswer;
    },
    /** Tidy up the student's answer - at the moment, just remove space.
     * You could override this to do more substantial filtering of the student's answer.
     *
     * @param {string} answer
     * @returns {string}
     */
    cleanAnswer: function(answer) {
        if(answer === undefined) {
            answer = '';
        }
        answer = answer.toString().trim();
        return answer;
    },
    /** Save a copy of the student's answer as entered on the page, for use in marking.
     */
    setStudentAnswer: function() {
        this.studentAnswer = this.cleanAnswer(this.stagedAnswer);
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the custom marking algorithm.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        return new Numbas.jme.types.TString(this.studentAnswer);
    }
};
['loadFromXML', 'loadFromJSON', 'resume', 'finaliseLoad'].forEach(function(method) {
    NumberEntryPart.prototype[method] = util.extend(Part.prototype[method], NumberEntryPart.prototype[method]);
});
Numbas.partConstructors['numberentry'] = util.extend(Part, NumberEntryPart);
});
;
Numbas.queueScript('display/parts/patternmatch',['display-base','part-display','util'],function() {
    var display = Numbas.display;
    var extend = Numbas.util.extend;
    /** Display code for a {@link Numbas.parts.PatternMatchPart}
     * @augments Numbas.display.PartDisplay
     * @constructor
     * @name PatternMatchPartDisplay
     * @memberof Numbas.display
     */
    display.PatternMatchPartDisplay = function()
    {
        var p = this.part;
        /** The student's current answer (not necessarily submitted)
         * @member {observable|string} studentAnswer
         * @memberof Numbas.display.PatternMatchPartDisplay
         */
        this.studentAnswer = Knockout.observable(this.part.studentAnswer);
        /** The correct answer regular expression
         * @member {observable|RegExp} correctAnswer
         * @memberof Numbas.display.PatternMatchPartDisplay
         */
        this.correctAnswer = Knockout.observable('');
        /** A representative correct answer to display when answers are revealed
         * @member {observable|string} displayAnswer
         * @memberof Numbas.display.PatternMatchPartDisplay
         */
        this.displayAnswer = Knockout.observable();

        this.updateCorrectAnswer(p.getCorrectAnswer(p.getScope()));

        Knockout.computed(function() {
            p.storeAnswer(this.studentAnswer());
        },this);
    }
    display.PatternMatchPartDisplay.prototype =
    {
        updateCorrectAnswer: function(answer) {
            this.correctAnswer(this.part.settings.correctAnswer);
            this.displayAnswer(answer);
        },
        restoreAnswer: function(studentAnswer) {
            this.studentAnswer(studentAnswer);
        }
    };
    display.PatternMatchPartDisplay = extend(display.PartDisplay,display.PatternMatchPartDisplay,true);
});
;
/*
Copyright 2011-15 Newcastle University
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
   http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/** @file The {@link Numbas.parts.PatternMatchPart} object */
Numbas.queueScript('parts/patternmatch', ['base', 'jme', 'jme-variables', 'util', 'part', 'marking_scripts'], function() {
var util = Numbas.util;
var jme = Numbas.jme;
var Part = Numbas.parts.Part;
/** Text-entry part - student's answer must match the given regular expression.
 *
 * @class
 * @param {Numbas.parts.partpath} [path='p0']
 * @param {Numbas.Question} question
 * @param {Numbas.parts.Part} parentPart
 * @param {Numbas.storage.BlankStorage} [store]
 * @memberof Numbas.parts
 * @augments Numbas.parts.Part
 */
var PatternMatchPart = Numbas.parts.PatternMatchPart = function(path, question, parentPart, store) {
    var settings = this.settings;
    util.copyinto(PatternMatchPart.prototype.settings, settings);
}
PatternMatchPart.prototype = /** @lends Numbas.PatternMatchPart.prototype */ {
    loadFromXML: function(xml) {
        var settings = this.settings;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        settings.correctAnswerString = Numbas.xml.getTextContent(xml.selectSingleNode('correctanswer')).trim();
        tryGetAttribute(settings, xml, 'correctanswer', ['mode', 'allowEmpty'], ['matchMode', 'allowEmpty']);
        var displayAnswerNode = xml.selectSingleNode('displayanswer');
        if(!displayAnswerNode) {
            this.error('part.patternmatch.display answer missing');
        }
        settings.displayAnswerString = Numbas.xml.getTextContent(displayAnswerNode).trim();
        tryGetAttribute(settings, xml, 'case', ['sensitive', 'partialCredit'], 'caseSensitive');
    },
    loadFromJSON: function(data) {
        var settings = this.settings;
        var tryLoad = Numbas.json.tryLoad;
        tryLoad(data, ['answer', 'displayAnswer'], settings, ['correctAnswerString', 'displayAnswerString']);
        tryLoad(data, ['caseSensitive', 'partialCredit', 'matchMode', 'allowEmpty'], settings);
        settings.partialCredit /= 100;
    },
    finaliseLoad: function() {
        this.getCorrectAnswer(this.getScope());
    },
    initDisplay: function() {
        this.display = new Numbas.display.PatternMatchPartDisplay(this);
    },
    resume: function() {
        if(!this.store) {
            return;
        }
        var pobj = this.store.loadPart(this);
        this.stagedAnswer = pobj.studentAnswer;
    },
    /** The student's last submitted answer.
     *
     * @type {string}
     */
    studentAnswer: '',
    /** The script to mark this part - assign credit, and give messages and feedback.
     *
     * @returns {Numbas.marking.MarkingScript}
     */
    baseMarkingScript: function() {
        return new Numbas.marking.MarkingScript(Numbas.raw_marking_scripts.patternmatch, null, this.getScope());
    },
    /** Properties set when the part is generated.
     * Extends {@link Numbas.parts.Part#settings}.
     *
     * @property {string} correctAnswerString - The definition of the correct answer, without variables substituted in.
     * @property {RegExp} correctAnswer - Regular expression pattern to match correct answers.
     * @property {string} displayAnswerString - The definition of the display answer, without variables substituted in.
     * @property {string} displayAnswer - A representative correct answer to display when answers are revealed.
     * @property {boolean} caseSensitive - Does case matter?
     * @property {boolean} allowEmpty - May the student submit an empty answer?
     * @property {number} partialCredit - Partial credit to award if the student's answer matches, apart from case, and `caseSensitive` is `true`.
     * @property {string} matchMode - Either "regex", for a regular expression, or "exact", for an exact match.
     */
    settings: {
        correctAnswerString: '.*',
        correctAnswer: /.*/,
        displayAnswerString: '',
        displayAnswer: '',
        caseSensitive: false,
        allowEmpty: false,
        partialCredit: 0,
        matchMode: 'regex'
    },
    /** The name of the input widget this part uses, if any.
     *
     * @returns {string}
     */
    input_widget: function() {
        return 'string';
    },
    /** Options for this part's input widget.
     *
     * @returns {object}
     */
    input_options: function() {
        return {
            allowEmpty: false
        }
    },
    /** Compute the correct answer, based on the given scope.
     *
     * @param {Numbas.jme.Scope} scope
     * @returns {string}
     */
    getCorrectAnswer: function(scope) {
        var settings = this.settings;
        settings.correctAnswer = jme.subvars(settings.correctAnswerString, scope, true);
        switch(this.settings.matchMode) {
            case 'regex':
                settings.correctAnswer = '^' + settings.correctAnswer + '$';
                settings.displayAnswer = jme.subvars(settings.displayAnswerString, scope, true);
                break;
            case 'exact':
                settings.displayAnswer = settings.correctAnswer;
                break;
        }
        return settings.displayAnswer;
    },
    /** Save a copy of the student's answer as entered on the page, for use in marking.
     */
    setStudentAnswer: function() {
        this.studentAnswer = this.stagedAnswer;
    },
    /** Get the student's answer as it was entered as a JME data type, to be used in the custom marking algorithm.
     *
     * @abstract
     * @returns {Numbas.jme.token}
     */
    rawStudentAnswerAsJME: function() {
        return new Numbas.jme.types.TString(this.studentAnswer);
    },
};
['finaliseLoad', 'resume', 'loadFromXML', 'loadFromJSON'].forEach(function(method) {
    PatternMatchPart.prototype[method] = util.extend(Part.prototype[method], PatternMatchPart.prototype[method]);
});
Numbas.partConstructors['patternmatch'] = util.extend(Part, PatternMatchPart);
});
;
Numbas.queueScript('question-display', ['display-util', 'display-base', 'jme-variables', 'xml', 'schedule', 'jme', 'util'], function() {
    var display = Numbas.display;
    /** Display properties of a question object.
     *
     * @name QuestionDisplay
     * @memberof Numbas.display
     * @class
     * @param {Numbas.Question} q - the associated question object
     */
    display.QuestionDisplay = function(q) {
        var qd = this;
        this.question = q;
        var exam = q.exam;

        this.contextDescription = R('question.header', {number:q.number + 1});

        /** Does this question have non-empty statement text?
         *
         * @member {observable|string} hasStatement
         * @memberof Numbas.display.QuestionDisplay
         */
        this.hasStatement = Knockout.observable(Numbas.util.isNonemptyHTML(q.statement));
        /** Does this question have non-empty advice text?
         *
         * @member {observable|string} hasAdvice
         * @memberof Numbas.display.QuestionDisplay
         */
        this.hasAdvice = Knockout.observable(Numbas.util.isNonemptyHTML(q.advice));
        /** Has the advice been shown?
         *
         * @member {observable|boolean} adviceDisplayed
         * @memberof Numbas.display.QuestionDisplay
         */
        this.adviceDisplayed = Knockout.observable(false);
        /** Get the {@link Numbas.display.PartDisplay} object for the given path.
         *
         * @param {Numbas.parts.partpath} path
         * @returns {Numbas.display.PartDisplay}
         * @function getPart
         * @memberof Numbas.display.QuestionDisplay
         */
        this.getPart = function(path) {
            return q.getPart(path).display;
        }
        /** The name to display for this question - in default locale, it's "Question {N}".
         *
         * @member {observable|string} displayName
         * @memberof Numbas.display.QuestionDisplay
         */
        this.displayName = Knockout.observable(q.exam.settings.navigateMode == 'sequence' && q.customName.trim() == '' ? R('question.header', {'number':q.displayNumber + 1}) : q.name);
        /** Has the student looked at this question?
         *
         * @see Numbas.Question#visited
         * @member {observable|boolean} visited
         * @memberof Numbas.display.QuestionDisplay
         */
        this.visited = Knockout.observable(q.visited);

        this.isCurrentQuestion = Knockout.computed(function() {
            return exam.display.currentQuestionNumber() == q.number;
        }, this);

        /** Is this question visible in the list?
         *
         * @member {observable|boolean} visible
         * @memberof Numbas.display.QuestionDisplay
         */
        this.visible = Knockout.computed(function() {
            var q = this.question;
            var currentQuestionNumber = exam.display.currentQuestionNumber();
            return (
                   exam.display.mode() == 'review'
                || q.number == currentQuestionNumber
                || exam.settings.navigateBrowse                                                 // is browse navigation enabled?
                || this.visited()                            // if not, we can still move backwards to questions already seen if reverse navigation is enabled
                || (currentQuestionNumber !== null && q.number > currentQuestionNumber && exam.display.questions()[q.number - 1].visited())    // or you can always move to the next question
            )
        }, this);

        /** Display objects for all parts in this question.
         *
         * @member {observable.<Array.<Numbas.display.PartDisplay>>} parts
         * @memberof Numbas.display.QuestionDisplay
         */
        this.parts = Knockout.observableArray(this.question.parts.map(function(p) {
            return p.display;
        }));

        /** Text for the "submit all answers" button.
         *
         * @member {observable|string} submitMessage
         * @memberof Numbas.display.QuestionDisplay
         */
        this.submitMessage = Knockout.pureComputed(function() {
            if(this.parts().every(function(pd) {
                return pd.answered() && !pd.isDirty();
            })) {
                return R(q.parts.length <= 1 ? 'question.answer saved' : 'question.all answers saved');
            } else {
                return R(q.parts.length <= 1 ? 'control.submit answer' : 'control.submit all parts');
            }
        }, this);

        /** The first part in the question.
         *
         * @member {observable.<Numbas.display.PartDisplay>} firstPart
         * @memberof Numbas.display.QuestionDisplay
         */
        this.firstPart = Knockout.computed(function() {
            return this.parts()[0];
        }, this);

        /** Number of parts in this question.
         *
         * @member {observable|number} numParts
         * @memberof Numbas.display.QuestionDisplay
         */
        this.numParts = Knockout.observable(q.parts.length);

        /** The currently visible part, in explore mode.
         *
         * @member {observable|Numbas.display.PartDisplay} currentPart
         * @memberof Numbas.display.QuestionDisplay
         */
        this.currentPart = Knockout.observable(null);

        this.currentPart.subscribe(function(pd) {
            pd.html_promise.then(function(html) {
                html.scrollIntoView();
                Numbas.display_util.force_focus(html);
                document.querySelector('main.mainDisplay')?.scrollTo(0, 0);
            });
        }, this);

        /** The part that created the current part.
         *
         * @member {observable|Numbas.display.PartDisplay} previousPart
         * @memberof Numbas.display.QuestionDisplay
         */
        this.previousPart = Knockout.computed(function() {
            var p = this.currentPart();
            if(!(p && p.part.previousPart)) {
                return null;
            }
            return p.part.previousPart.display;
        }, this);

        /** Set the current part to the previous part, if it's defined.
         *
         * @see Numbas.display.QuestionDisplay.currentPart
         */
        this.goToPreviousPart = function() {
            var p = qd.previousPart();
            if(p) {
                q.setCurrentPart(p.part);
            }
        };

        /** Set the current part.
         *
         * @param {Numbas.display.PartDisplay} pd
         */
        this.setCurrentPart = function(pd) {
            q.setCurrentPart(pd.part);
        }

        /** Student's current score.
         *
         * @see Numbas.Question#score
         * @member {observable|number} score
         * @memberof Numbas.display.QuestionDisplay
         */
        this.score = Knockout.observable(q.score);
        /** Total marks available for this question.
         *
         * @see Numbas.Question#marks
         * @member {observable|number} marks
         * @memberof Numbas.display.QuestionDisplay
         */
        this.marks = Knockout.observable(q.marks);
        /** Proportion of available marks awarded to the student.
         *
         * @member {observable|number} credit
         * @memberof Numbas.display.QuestionDisplay
         */
        this.credit = Knockout.computed(function() {
            return this.score() / this.marks();
        }, this);
        /** Does this question do any marking?
         *
         * @member {observable|boolean} doesMarking
         * @memberof Numbas.display.QuestionDisplay
         */
        this.doesMarking = Knockout.computed(function() {
            return this.marks() > 0
        }, this);
        /** Has this question been answered?
         *
         * @see Numbas.Question#answered
         * @member {observable|boolean} answered
         * @memberof Numbas.display.QuestionDisplay
         */
        this.answered = Knockout.observable(q.answered);
        /** Has this question been locked?
         *
         * @see Numbas.Question#locked
         * @member {observable|boolean} locked
         * @memberof Numbas.display.QuestionDisplay
         */
        this.locked = Knockout.observable(q.locked);

        this.ended = exam.display.ended;

        /** Have the correct answers been revealed?
         *
         * @see Numbas.Question#revealed
         * @member {observable|boolean} revealed
         * @memberof Numbas.display.QuestionDisplay
         */
        this.revealed = Knockout.observable(q.revealed);

        /** Should expected answers to parts be shown?
         */
        this.expectedAnswersRevealed = Knockout.pureComputed(function() {
            if(!this.revealed()) {
                return false;
            }
            return exam.settings.revealExpectedAnswers == 'inreview';
        }, this);

        /** Have any of this question's parts been answered?
         *
         * @member {observable|boolean} anyAnswered
         * @memberof Numbas.display.QuestionDisplay
         */
        this.anyAnswered = Knockout.observable(false);
        /** Has the student changed any of their answers since submitting?
         *
         * @member {observable|boolean} isDirty
         * @memberof Numbas.display.QuestionDisplay
         */
        this.isDirty = Knockout.observable(false);
        /** Is the student able to reveal the correct answers?
         *
         * @member {observable|boolean} canReveal
         * @memberof Numbas.display.QuestionDisplay
         */
        this.canReveal = Knockout.computed(function() {
            return exam.settings.allowRevealAnswer && !this.revealed();
        }, this);
        /** Score feedback string.
         *
         * @member {{update: Function, message: observable|string}} scoreFeedback
         * @memberof Numbas.display.QuestionDisplay
         */
        this.scoreFeedback = Numbas.display_util.showScoreFeedback(this, q.exam.settings);

        /** Explore mode objectives.
         *
         * @member {Array.<object>} objectives
         * @memberof Numbas.display.QuestionDisplay
         */
        this.objectives = q.objectives.map(function(o) {
            var od = {
                objective: o,
                name: o.name,
                marks: Knockout.observable(o.limit),
                score: Knockout.observable(o.score),
                doesMarking: Knockout.observable(true),
                revealed: qd.revealed,
                ended: qd.ended,
                answered: Knockout.observable(false)
            }
            od.credit = Knockout.computed(function() {
                return od.score() / od.marks();
            });
            od.visible = Knockout.computed(function() {
                return q.objectiveVisibility == 'always' || od.answered() || od.revealed();
            }, this);
            od.feedback = Numbas.display_util.showScoreFeedback(od, q.exam.settings);
            return od;
        });
        /** Explore mode penalties.
         *
         * @member {Array.<object>} penalties
         * @memberof Numbas.display.QuestionDisplay
         */
        this.penalties = q.penalties.map(function(p) {
            var pd = {
                penalty: p,
                name: p.name,
                limit: p.limit,
                score: Knockout.observable(p.score),
                revealed: qd.revealed,
                applied: Knockout.observable(false),
            };
            pd.visible = Knockout.computed(function() {
                return q.penaltyVisibility == 'always' || pd.applied() || pd.revealed();
            })
            pd.scoreDisplay = Knockout.computed(function() {
                return Numbas.math.niceNumber(pd.score());
            });
            return pd;
        });

        /** Should the score breakdown table be shown?
         *
         * @member {Observable.<boolean>} showScoreBreakdown
         * @memberof Numbas.display.QuestionDisplay
         */
        this.showScoreBreakdown = Knockout.computed(function() {
            return q.partsMode == 'explore' && q.objectives.length > 0;
        }, this);

        /** Show the tree of parts for navigation?
         *
         * @member {Observable.<boolean>} showPartsTree
         * @memberof Numbas.display.QuestionDisplay
         */
        this.showPartsTree = Knockout.computed(function() {
            return q.partsMode == 'explore';
        }, this);

        /** Show this question in review mode.
         *
         * @member {Function} review
         * @function
         * @memberof Numbas.display.QuestionDisplay
         */
        this.review = function() {
            exam.reviewQuestion(q.number);
        }

        /** CSS classes for this question's HTML element.
         *
         * @member {Observable.<object>} css_classes
         * @memberof Numbas.display.QuestionDisplay
         */
        this.css_classes = Knockout.computed(function() {
            var css = {};
            css['partsmode-' + q.partsMode] = true;
            return css;
        }, this);

        /** Called when Knockout has finished binding all the HTML for this question to the DOM.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        this.htmlBound = function() {
            q.signals.on(['partsGenerated'], function() {
                q.signals.trigger('HTMLAttached');
            });
        }
        /** A promise resolving to the question's HTML element.
         *
         * @see Numbas.display.makeHTMLFromXML
         * @type {Promise}
         * @memberof Numbas.display.QuestionDisplay
         */
        this.html_promise = new Promise(function(resolve) {
            qd.resolve_html_promise = resolve;
        });
        this.html_promise.then(function(html) {
            q.signals.trigger('mainHTMLAttached');
        });
    }
    display.QuestionDisplay.prototype = /** @lends Numbas.display.QuestionDisplay.prototype */
    {
        /** The associated question object.
         *
         * @type {Numbas.Question}
         * @memberof Numbas.display.QuestionDisplay
         */
        question: undefined,            //reference back to the main question object

        /** HTML representing the question.
         *
         * @type {Element}
         * @memberof Numbas.display.QuestionDisplay
         */
        html: '',                        //HTML for displaying question

        /** Make the HTML to display the question.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        makeHTML: function() {
            var q = this.question;
            var qd = this;

            var promise = display.makeHTMLFromXML(
                q.xml,
                Numbas.xml.templates.question,
                q.scope,
                qd.contextDescription,
                q.exam.display.root_element
            );

            promise.then(function(html) {
                qd.html = html;

                qd.resolve_html_promise(html);

                qd.css = document.createElement('style');
                qd.css.setAttribute('type', 'text/css');
                const css = `@layer question {\n${q.preamble.css}\n}`;
                qd.css.appendChild(document.createTextNode(css));

                document.body.append(qd.css);

                qd.html.append(qd.css);
            });
        },

        /** Update the list of parts.
         */
        updateParts: function() {
            this.parts(this.question.parts.map(function(p) {
                return p.display;
            }));
            this.marks(this.question.marks);
        },

        /** Add a new part to the display.
         *
         * @param {Numbas.parts.Part} p
         */
        addPart: function(p) {
            var qd = this;
            this.updateParts();
            this.question.signals.on('mainHTMLAttached', function() {
                var promise = display.makeHTMLFromXML(
                    p.xml,
                    Numbas.xml.templates.part,
                    p.getScope(),
                    qd.contextDescription + ' ' + (p.display.name() || p.name),
                    p.question.exam.display.root_element
                );

                /** Register the rendered HTML with the part, resolving the part's html promise.
                 *
                 * @param {Numbas.parts.Part} p
                 * @param {Element} html
                 */
                function add_html_to_part(p, html) {
                    if(p.display) {
                        p.display.html = html;
                        p.display.resolve_html_promise(html);
                    }
                    p.allChildren().forEach(function(cp) {
                        add_html_to_part(cp, html.querySelector('.part[data-part-path="' + cp.path + '"]'));
                    });
                }
                promise.then(function(html) {
                    add_html_to_part(p, html);
                });
            });
        },

        /** Remove a part from the display.
         *
         * @param {Numbas.parts.Part} p
         */
        removePart: function(p) {
            this.updateParts();
            this.question.signals.on('mainHTMLAttached', function() {
                p.display.html_promise.then(function(html) {
                    html.remove();
                });
            });
        },

        /** Show the question.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        show: function() {
            var q = this.question;
            var exam = q.exam;
            this.visited(q.visited);
            //update the question menu - highlight this question, etc.
            exam.display.updateQuestionMenu();
            switch(exam.mode) {
            case 'normal':
                break;
            case 'review':
                break;
            }
            //show parts
            for(var i = 0;i < q.parts.length;i++) {
                q.parts[i].display.show();
            }
            //display advice if appropriate
            this.showAdvice();
            //show correct answers if appropriate
            this.revealAnswer();
            //display score if appropriate
            this.showScore(true);
            //scroll back to top of page
            Numbas.display.control_focus && scroll(0, 0);
            // make mathjax process the question text (render the maths)
            Numbas.display.typeset(this.html);

            // move focus to the top of this question's HTML
            Numbas.display_util.force_focus(this.html);
        },
        /** Called when the student leaves the question.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        leave: function() {
        },
        /**
         * Show this question's advice.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        showAdvice: function() {
            this.adviceDisplayed(this.question.adviceDisplayed);
        },
        /** Reveal the answers to this question.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        revealAnswer: function() {
            this.revealed(this.question.revealed);
            if(!this.question.revealed) {
                return;
            }
            Numbas.display.control_focus && scroll(0, 0);
        },
        /**
         * Display question score and answer state.
         *
         * @param {boolean} noUpdate
         * @memberof Numbas.display.QuestionDisplay
         */
        showScore: function(noUpdate) {
            var q = this.question;
            this.score(q.score);
            this.marks(q.marks);
            this.answered(q.answered);
            if(!noUpdate) {
                this.scoreFeedback.update(true);
            }
            var anyAnswered = false;
            for(var i = 0;i < q.parts.length;i++) {
                anyAnswered = anyAnswered || (q.parts[i].doesMarking && q.parts[i].answered);
            }
            this.anyAnswered(anyAnswered);
            this.objectives.forEach(function(o) {
                o.score(o.objective.score);
                o.answered(o.objective.answered);
                o.feedback.update(true);
            });
            this.penalties.forEach(function(p) {
                p.score(-p.penalty.score);
                p.applied(p.penalty.applied);
            });
        },
        /** Initialise this question's display.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        init: function() {
            var q = this.question;
            for(var i = 0;i < q.parts.length;i++) {
                q.parts[i].display.init();
            }
            this.numParts(q.parts.length);
        },
        /** Called when the attempt is resumed.
         *
         * @see Numbas.Question#resume
         */
        resume: function() {
            var q = this.question;
            this.adviceDisplayed(q.adviceDisplayed);
            this.answered(q.answered);
            this.locked(q.locked);
            this.revealed(q.revealed);
            this.visited(q.visited);
        },
        /** Called when the exam ends.
         *
         * @memberof Numbas.display.QuestionDisplay
         */
        end: function() {
            var q = this.question;
            this.locked(true);
            q.allParts().forEach(function(part) {
                part.display.end();
            });
            Numbas.display.typeset(this.html);
        }
    };
})
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file The {@link Numbas.Question} object */
Numbas.queueScript('standard_parts', ['parts/jme', 'parts/patternmatch', 'parts/numberentry', 'parts/matrixentry', 'parts/multipleresponse', 'parts/gapfill', 'parts/information', 'parts/extension', 'parts/custom_part_type'], function() {});
Numbas.queueScript('question', ['base', 'schedule', 'jme', 'jme-variables', 'util', 'part', 'standard_parts'], function() {
var jme = Numbas.jme;
/** Create a {@link Numbas.Question} object from an XML definition.
 *
 * @memberof Numbas
 * @param {Element} xml
 * @param {number} number - The number of the question in the exam.
 * @param {Numbas.Exam} [exam] - The exam this question belongs to.
 * @param {Numbas.QuestionGroup} [group] - The group this question belongs to.
 * @param {Numbas.jme.Scope} [scope] - The global JME scope.
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {boolean} loading - Is this question being resumed?
 * @returns {Numbas.Question}
 */
Numbas.createQuestionFromXML = function(xml, number, exam, group, scope, store, loading) {
    try {
        var q = new Question(number, exam, group, scope, store);
        q.loadFromXML(xml);
        q.finaliseLoad(loading);
    } catch(e) {
        throw(new Numbas.Error('question.error creating question', {number: number + 1, message: e.message}));
    }
    return q;
}
/** Create a {@link Numbas.Question} object from a JSON object.
 *
 * @memberof Numbas
 * @param {object} data
 * @param {number} number - The number of the question in the exam.
 * @param {Numbas.Exam} [exam] - The exam this question belongs to.
 * @param {Numbas.QuestionGroup} [group] - The group this question belongs to.
 * @param {Numbas.jme.Scope} [scope] - The global JME scope.
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 * @param {boolean} loading - Is this question being resumed?
 * @returns {Numbas.Question}
 */
Numbas.createQuestionFromJSON = function(data, number, exam, group, scope, store, loading) {
    try {
        var q = new Question(number, exam, group, scope, store);
        q.loadFromJSON(data);
        q.finaliseLoad(loading);
    } catch(e) {
        throw(new Numbas.Error('question.error creating question', {number: number + 1, message: e.message}, e));
    }
    return q;
}
/** Keeps track of all info to do with an instance of a single question.
 *
 * @class
 * @memberof Numbas
 * @param {number} number - The index of this question in the exam (starting at 0).
 * @param {Numbas.Exam} [exam] - The parent exam.
 * @param {Numbas.QuestionGroup} [group] - The group this question belongs to.
 * @param {Numbas.jme.Scope} [gscope=Numbas.jme.builtinScope] - The global JME scope.
 * @param {Numbas.storage.BlankStorage} [store] - The storage engine to use.
 */
var Question = Numbas.Question = function(number, exam, group, gscope, store) {
    var q = this;
    q.store = store;
    q.signals = new Numbas.schedule.SignalBox();
    q.signals.on('partsGenerated', function() {
        q.setErrorCarriedForwardBackReferences();
    })
    q.events = new Numbas.schedule.EventBox();
    q.exam = exam;
    q.tags = [];
    q.group = group;
    q.number = number;
    q.path = `q${this.number}`;
    gscope = gscope || (exam && exam.scope) || Numbas.jme.builtinScope;
    q.scope = new jme.Scope(gscope);
    q.scope.question = q;
    q.preamble = {
        'js': '',
        'css': ''
    };
    q.functionsTodo = [];
    q.variableDefinitions = [];
    q.variablesTodo = {};
    q.rulesets = {};
    q.variablesTest = {
        condition: '',
        maxRuns: 10
    };
    q.parts = [];
    q.partDictionary = {};
    q.extraPartOrder = [];
    q.objectives = [];
    q.penalties = [];
    q.extensions = [];
}

/** The question preamble has been loaded but not run yet- this happens before any variables, functions, rulesets or parts are generated.
 *
 * @event Numbas.Question#preambleLoaded
 * @see Numbas.Question#event:preambleRun
 */
/** The question preamble has been run.
 *
 * @event Numbas.Question#preambleRun
 */
/** The question's function definitions have been loaded, but the corresponding {@link Numbas.jme.funcObj} objects have not been added to the scope yet.
 *
 * @event Numbas.Question#functionsLoaded
 * @see Numbas.Question#event:functionsMade
 */
/** The question's functions have been made and added to the question's scope.
 *
 * @event Numbas.Question#functionsMade
 */
/** The question's ruleset  definitions have been loaded, but the {@link Numbas.jme.rules.Ruleset} objects have not been added to the scope yet.
 *
 * @event Numbas.Question#rulesetsLoaded
 * @see Numbas.Question#event:rulesetsMade
 */
/** The question's rulesets have been made and added to the question's scope.
 *
 * @event Numbas.Question#rulesetsMade
 */
/** Trigger this when you're ready to evaluate the question's variables. In an exam context, the {@link Numbas.Exam} object triggers this event.
 * If the question has been created standalone, this event must be triggered in order for the question to finish loading.
 *
 * @event Numbas.Question#generateVariables
 */
/** The variable definitions have been loaded, but their values have not been generated yet.
 *
 * @event Numbas.Question#variableDefinitionsLoaded
 * @see Numbas.Question#event:variablesSet
 * @see Numbas.Question#event:variablesGenerated
 */
/** The parts of the question have been generated.
 * If resuming an attempt, the parts have not yet been restored to the saved state.
 *
 * @event Numbas.Question#partsGenerated
 * @see Numbas.Question#event:partsResumed
 */
/** Triggered when resuming a saved attempt: the question's parts have been restored to the saved state.
 *
 * @event Numbas.Question#partsResumed
 */
/** The custom constant definitions have been loaded.
 *
 * @event Numbas.Question#constantsLoaded
 */
/** The custom constants have been evaluated and added to the scope
 *
 * @event Numbas.Question#constantsMade
 */
/** The variables have been evaluated, but {@link Numbas.Question.unwrappedVariables} has not been set yet.
 *
 * @event Numbas.Question#variablesSet
 */
/** The variables have been generated and added to the scope, and are ready to use.
 *
 * @event Numbas.Question#variablesGenerated
 */
/** The question advice has been shown to the student.
 *
 * @event Numbas.Question#adviceDisplayed
 */
/** The question is fully loaded and ready to use.
 *
 * @event Numbas.Question#ready
 */
/** The question's HTML has been generated and attached to the page.
 *
 * @event Numbas.Question#mainHTMLAttached
 */
/** The entire question, including each part's HTML, has been generated and attached to the page.
 *
 * @event Numbas.Question#HTMLAttached
 */

Question.prototype = /** @lends Numbas.Question.prototype */
{
    /** How should parts be shown?
     *
     * - `all` - All available parts are generated straight away.
     * - `explore` - Parts are only generated when required.
     *
     * @type {string}
     */
    partsMode: 'all',

    /** Maximum available marks in explore mode.
     *
     * @type {number}
     */
    maxMarks: 0,

    /** When should information about objectives be shown to the student? ``'always'`` or ``'when-active'``.
     *
     * @type {string}
     */
    objectiveVisibility: 'always',

    /** When should information about penalties be shown to the student? ``'always'`` or ``'when-active'``.
     *
     * @type {string}
     */
    penaltyVisibility: 'always',

    /** In explore mode, the part that the student is currently looking at.
     *
     * @type {Numbas.parts.Part}
     */
    currentPart: null,

    /** Signals produced while loading this question.
     *
     * @type {Numbas.schedule.SignalBox}
     */
    signals: undefined,

    /** Storage engine.
     *
     * @type {Numbas.storage.BlankStorage}
     */
    store: undefined,

    /** Throw an error, with the question's identifier prepended to the message.
     *
     * @param {string} message
     * @param {object} args - Arguments for the error message.
     * @param {Error} [originalError] - If this is a re-thrown error, the original error object.
     * @fires Numbas.Question#event:error
     * @throws {Numbas.Error}
     */
    error: function(message, args, originalError) {
        if(originalError && originalError.originalMessages && originalError.originalMessages[0] == 'question.error') {
            throw(originalError);
        }
        var nmessage = R.apply(this, [message, args]);
        if(nmessage != message) {
            originalError = new Error(nmessage);
            originalError.originalMessages = [message].concat(originalError.originalMessages || []);
        }
        this.events.trigger('error', message, args, originalError);
        throw(new Numbas.Error('question.error', {number: this.number + 1, message: nmessage}, originalError));
    },

    /** Load the question's settings from an XML <question> node.
     *
     * @param {Element} xml
     * @fires Numbas.Question#preambleLoaded
     * @fires Numbas.Question#constantsLoaded
     * @fires Numbas.Question#functionsLoaded
     * @fires Numbas.Question#rulesetsLoaded
     * @fires Numbas.Question#variableDefinitionsLoaded
     * @fires Numbas.Question#partsGenerated
     * @listens Numbas.Question#variablesGenerated
     */
    loadFromXML: function(xml) {
        var q = this;
        var tryGetAttribute = Numbas.xml.tryGetAttribute;
        q.xml = xml;
        q.originalXML = q.xml;

        tryGetAttribute(q, q.xml, '.', ['name', 'customName', 'partsMode', 'maxMarks', 'objectiveVisibility', 'penaltyVisibility']);
        q.hasCustomName = q.customName.trim() != '';
        if(q.hasCustomName) {
            q.name = q.customName.trim();
        }

        var statementNode = q.xml.selectSingleNode('statement');
        q.statement = Numbas.xml.serializeMessage(statementNode);
        var adviceNode = q.xml.selectSingleNode('advice');
        q.advice = Numbas.xml.serializeMessage(adviceNode);

        var preambleNodes = q.xml.selectNodes('preambles/preamble');
        for(let i = 0; i < preambleNodes.length; i++) {
            var lang = preambleNodes[i].getAttribute('language');
            q.preamble[lang] = Numbas.xml.getTextContent(preambleNodes[i]);
        }
        q.signals.trigger('preambleLoaded');

        var extensionNodes = q.xml.selectNodes('extensions/extension');
        extensionNodes.forEach(function(node) {
            q.useExtension(node.textContent);
        });

        var part_defs = Array.from(q.xml.selectNodes('parts//part'));
        if(q.partsMode == 'explore' && part_defs.length == 0) {
            throw(new Numbas.Error('question.explore.no parts defined'));
        }

        // Activate extensions needed by part types in this question.
        part_defs.forEach(function(p) {
            var type = tryGetAttribute(null, p, '.', 'type', []);
            var cpt = Numbas.custom_part_types[type];
            if(!cpt) {
                return;
            }
            cpt.extensions.forEach(function(extension) {
                q.useExtension(extension)
            });
        });

        q.addExtensionScopes();

        q.constantsTodo = {
            builtin: [],
            custom: []
        }

        var builtinConstantNodes = q.xml.selectNodes('constants/builtin/constant');
        for(let i = 0;i < builtinConstantNodes.length;i++) {
            const node = builtinConstantNodes[i];
            const data = {};
            tryGetAttribute(data, node, '.', ['name', 'enable']);
            q.constantsTodo.builtin.push(data);
        }
        var customConstantNodes = q.xml.selectNodes('constants/custom/constant');
        for(let i = 0;i < customConstantNodes.length;i++) {
            const node = customConstantNodes[i];
            const data = {};
            tryGetAttribute(data, node, '.', ['name', 'value', 'tex']);
            q.constantsTodo.custom.push(data);
        }
        q.signals.trigger('constantsLoaded');

        q.functionsTodo = Numbas.xml.loadFunctions(q.xml, q.scope);
        q.signals.trigger('functionsLoaded');

        var tagNodes = q.xml.selectNodes('tags/tag');
        for(let i = 0; i < tagNodes.length; i++) {
            this.tags.push(tagNodes[i].textContent);
        }

        //make rulesets
        var rulesetNodes = q.xml.selectNodes('rulesets/set');
        for(let i = 0; i < rulesetNodes.length; i++) {
            var name = rulesetNodes[i].getAttribute('name');
            var set = [];
            //get new rule definitions
            var defNodes = rulesetNodes[i].selectNodes('ruledef');
            for(var j = 0; j < defNodes.length; j++) {
                var pattern = defNodes[j].getAttribute('pattern');
                var result = defNodes[j].getAttribute('result');
                var conditions = [];
                var conditionNodes = defNodes[j].selectNodes('conditions/condition');
                for(let k = 0; k < conditionNodes.length; k++) {
                    conditions.push(Numbas.xml.getTextContent(conditionNodes[k]));
                }
                var rule = new Numbas.jme.display.Rule(pattern, conditions, result);
                set.push(rule);
            }
            //get included sets
            var includeNodes = rulesetNodes[i].selectNodes('include');
            for(let j = 0; j < includeNodes.length; j++) {
                set.push(includeNodes[j].getAttribute('name'));
            }
            q.rulesets[name] = set;
        }
        q.signals.trigger('rulesetsLoaded');

        var objectiveNodes = q.xml.selectNodes('objectives/scorebin');
        for(let i = 0; i < objectiveNodes.length; i++) {
            var objective = {
                name: '',
                limit: 0,
                score: 0,
                answered: false
            };
            tryGetAttribute(objective, objectiveNodes[i], '.', ['name', 'limit']);
            q.objectives.push(objective);
        }

        var penaltyNodes = q.xml.selectNodes('penalties/scorebin');
        for(let i = 0; i < penaltyNodes.length; i++) {
            var penalty = {
                name: '',
                limit: 0,
                score: 0,
                applied: false
            };
            tryGetAttribute(penalty, penaltyNodes[i], '.', ['name', 'limit']);
            q.penalties.push(penalty);
        }

        q.variableDefinitions = Numbas.xml.loadVariables(q.xml, q.scope);
        tryGetAttribute(q.variablesTest, q.xml, 'variables', ['condition', 'maxRuns'], []);
        q.signals.trigger('variableDefinitionsLoaded');
        q.signals.on('variablesGenerated', function() {
            q.xml = q.originalXML.cloneNode(true);    //get a fresh copy of the original XML, to sub variables into
            q.xml.setAttribute('number', q.number);
        });
        q.signals.on(['variablesGenerated', 'rulesetsMade'], function() {
            var partNodes = q.xml.selectNodes('parts/part');
            switch(q.partsMode) {
                case 'all':
                    //load parts
                    for(let j = 0; j < partNodes.length; j++) {
                        var part = Numbas.createPartFromXML(j, partNodes[j], 'p' + j, q, null, q.store);
                        q.addPart(part, j);
                    }
                    break;
                case 'explore':
                    q.addExtraPart(0);
                    break;
            }
            q.signals.trigger('partsGenerated');
        });
    },

    /** Create a part whose definition is at the given index in the question's definition, using the given scope, and add it to this question.
     * The question's variables are remade using the given dictionary of changed variables.
     *
     * @param {number} def_index - The index of the part's definition in the question's list of part definitions.
     * @param {Numbas.jme.Scope} scope
     * @param {Object<Numbas.jme.token>} variables
     * @param {Numbas.parts.Part} [previousPart] - The part that this part follows on from.
     * @param {number} [index] - The position of the part in the parts list (added to the end if not given).
     * @fires Numbas.Question#event:addExtraPart
     * @returns {Numbas.parts.Part}
     */
    addExtraPart: function(def_index, scope, variables, previousPart, index) {
        var p;

        this.extraPartOrder.push(def_index);
        scope = scope || this.scope;
        variables = variables || {};
        var pscope = Numbas.jme.variables.remakeVariables(this.variablesTodo, variables, scope);

        if(this.xml) {
            p = this.createExtraPartFromXML(def_index, pscope);
        } else {
            p = this.createExtraPartFromJSON(def_index, pscope);
        }
        index = index !== undefined ? index : this.parts.length;
        this.addPart(p, index);
        p.assignName(index, this.parts.length - 1);
        p.previousPart = previousPart;
        this.setCurrentPart(p);
        this.updateScore();
        this.events.trigger('addExtraPart', p);
        return p;
    },

    /** Create an extra part with the given XML definition, using the given scope.
     *
     * @param {number} xml_index - The index of the part's definition in the XML.
     * @param {Numbas.jme.Scope} scope
     * @returns {Numbas.parts.Part}
     */
    createExtraPartFromXML: function(xml_index, scope) {
        var xml = this.xml.selectNodes('parts/part')[xml_index].cloneNode(true);
        this.xml.selectSingleNode('parts').appendChild(xml);
        var j = this.parts.length;
        var p = Numbas.createPartFromXML(xml_index, xml, 'p' + j, this, null, this.store, scope);
        return p;
    },

    /** Set the currently displayed part.
     *
     * @param {Numbas.parts.Part} part
     * @fires Numbas.Question#event:setCurrentPart
     */
    setCurrentPart: function(part) {
        this.currentPart = part;
        this.display && this.display.currentPart(part.display);
        this.events.trigger('setCurrentPart', part);
    },

    /** Load the question's settings from a JSON object.
     *
     * @param {object} data
     * @fires Numbas.Question#preambleLoaded
     * @fires Numbas.Question#functionsLoaded
     * @fires Numbas.Question#rulesetsLoaded
     * @fires Numbas.Question#variableDefinitionsLoaded
     * @fires Numbas.Question#partsGenerated
     * @listens Numbas.Question#variablesGenerated
     */
    loadFromJSON: function(data) {
        this.json = data;
        var q = this;
        var tryLoad = Numbas.json.tryLoad;
        var tryGet = Numbas.json.tryGet;
        tryLoad(data, ['name', 'customName', 'partsMode', 'maxMarks', 'objectiveVisibility', 'penaltyVisibility', 'statement', 'advice'], q);


        var tags = tryGet(data, 'tags');
        if(tags) {
            q.tags = tags.slice();
        }

        var extensions = tryGet(data, 'extensions');
        if(extensions) {
            extensions.forEach(function(extension) {
                q.useExtension(extension);
            });
        }

        /**
         * Get the extensions used by custom part types.
         *
         * @param {object} pdata - A part definition.
         */
        function get_part_extensions(pdata) {
            var type = pdata.type;
            var cpt = Numbas.custom_part_types && Numbas.custom_part_types[type];
            if(!cpt) {
                return;
            }
            cpt.extensions.forEach(function(extension) {
                q.useExtension(extension)
            });
            if(pdata.gaps) {
                pdata.gaps.forEach(get_part_extensions);
            }
            if(pdata.steps) {
                pdata.steps.forEach(get_part_extensions);
            }
        }
        var parts = tryGet(data, 'parts');
        if(parts) {
            parts.forEach(get_part_extensions);
        }

        q.addExtensionScopes();

        var preambles = tryGet(data, 'preamble');
        if(preambles) {
            Object.keys(preambles).forEach(function(key) {
                q.preamble[key] = preambles[key];
            });
        }
        q.signals.trigger('preambleLoaded');

        q.constantsTodo = {
            builtin: [],
            custom: []
        };
        var builtin_constants = tryGet(data, 'builtin_constants') || [];
        if(builtin_constants) {
            q.constantsTodo.builtin = Object.entries(builtin_constants).map(function(d) {
                return {name: d[0], enable: d[1]};
            });
        }
        q.constantsTodo.custom = tryGet(data, 'constants') || [];
        q.signals.trigger('constantsLoaded');

        var functions = tryGet(data, 'functions');
        if(functions) {
            q.functionsTodo = Object.keys(functions).map(function(name) {
                var fd = functions[name];
                return {
                    name: name,
                    definition: fd.definition,
                    language: fd.language,
                    outtype: fd.type,
                    parameters: fd.parameters.map(function(p) {
                        return {
                            name:p[0],
                            type: p[1]
                        }
                    })
                };
            });
        }
        q.signals.trigger('functionsLoaded');
        var rulesets = tryGet(data, 'rulesets');
        if(rulesets) {
            Object.keys(rulesets).forEach(function(name) {
                q.rulesets[name] = rulesets[name];
            });
        }
        q.signals.trigger('rulesetsLoaded');

        var objectives = tryGet(data, 'objectives');
        if(objectives) {
            objectives.forEach(function(od) {
                var objective = {
                    name: '',
                    limit: 0,
                    score: 0,
                    answered: false
                };
                tryLoad(od, ['name', 'limit'], objective);
                q.objectives.push(objective);
            });
        }
        var penalties = tryGet(data, 'penalties');
        if(penalties) {
            penalties.forEach(function(pd) {
                var penalty = {
                    name: '',
                    limit: 0,
                    score: 0,
                    applied: false
                };
                tryLoad(pd, ['name', 'limit'], penalty);
                q.penalties.push(penalty);
            });
        }

        q.variableDefinitions = [];
        var variables = tryGet(data, 'variables');
        if(variables) {
            q.variableDefinitions = Object.values(variables);
        }
        var variablesTest = tryGet(data, 'variablesTest');
        if(variablesTest) {
            tryLoad(variablesTest, ['condition', 'maxRuns'], q.variablesTest);
        }
        q.signals.trigger('variableDefinitionsLoaded');
        q.signals.on(['variablesGenerated', 'rulesetsMade'], function() {
            var parts = tryGet(data, 'parts');
            if(parts) {
                switch(q.partsMode) {
                    case 'all':
                        parts.forEach(function(pd, i) {
                            var p = Numbas.createPartFromJSON(i, pd, 'p' + i, q, null, q.store);
                            q.addPart(p, i);
                        });
                        break;
                    case 'explore':
                        q.addExtraPart(0);
                        break;
                }
            }
            q.signals.trigger('partsGenerated');
        });
    },

    /** Record that this question uses the given extension.
     *
     * @param {string} extension
     */
    useExtension: function(extension) {
        if(this.extensions.contains(extension)) {
            return;
        }
        this.extensions.push(extension);
    },

    /** Extend this question's scope with scopes from any extensions used.
     */
    addExtensionScopes: function() {
        var scope = this.scope;
        for(const extension of this.extensions) {
            if(!Numbas.extensions[extension]) {
                throw(new Numbas.Error("question.required extension not available", {extension: extension}));
            }
            if(Numbas.extensions[extension] && ('scope' in Numbas.extensions[extension])) {
                scope = new Numbas.jme.Scope([scope, Numbas.extensions[extension].scope]);
            }
        }
        this.scope = scope;
    },

    /** Create a part with the given JSON definition, using the given scope, and add it to this question.
     * The question's variables are remade using the given dictionary of changed variables.
     *
     * @param {number} json_index - The index of the part's definition in the JSON.
     * @param {Numbas.jme.Scope} scope
     * @param {Object<Numbas.jme.token>} variables
     * @param {Numbas.parts.Part} [previousPart] - The part that this part follows on from.
     * @param {number} [index] - The position of the part in the parts list (added to the end if not given).
     * @returns {Numbas.parts.Part}
     */
    createExtraPartFromJSON: function(json_index, scope, variables, previousPart, index) {
        var data = this.json.parts[json_index];
        var p = Numbas.createPartFromJSON(json_index, data, 'p' + this.parts.length, this, null, this.store, scope);
        return p;
    },

    /** Set back references for adaptive marking: for each part, maintain a list of other parts which use that part in adaptive marking.
     */
    setErrorCarriedForwardBackReferences: function() {
        var q = this;
        this.allParts().forEach(function(p) {
            p.settings.errorCarriedForwardReplacements.forEach(function(r) {
                var p2 = q.getPart(r.part);
                p2.errorCarriedForwardBackReferences[p.path] = true;
            });
        });
    },

    /** A list of all parts in the question which can be answered by the student: top-level parts, gaps and steps.
     * Doesn't include alternative versions of parts.
     *
     * @returns {Array.<Numbas.parts.Part>}
     */
    allParts: function() {
        return this.parts.reduce(function(out, p) {
            return out.concat([p], p.gaps, p.steps);
        }, []);
    },

    /** Add a part to the question.
     *
     * @param {Numbas.parts.Part} part
     * @param {number} index
     * @fires Numbas.Question#event:addPart
     */
    addPart: function(part, index) {
        this.parts.splice(index, 0, part);
        this.display && this.display.addPart(part);
        this.updateScore();
        this.events.trigger('addPart', part, index);
    },

    /** Remove a part from the question.
     *
     * @param {Numbas.parts.Part} part
     * @fires Numbas.Question#event:removePart
     */
    removePart: function(part) {
        this.parts = this.parts.filter(function(p2) {
            return p2 != part;
        });
        this.display && this.display.removePart(part);
        this.updateScore();
        if(this.partsMode == 'explore' && this.currentPart == part) {
            if(part.previousPart) {
                this.setCurrentPart(part.previousPart);
            } else {
                this.setCurrentPart(this.parts[0]);
            }
        }
        this.events.trigger('removePart', part);
    },

    /** Perform any tidying up or processing that needs to happen once the question's definition has been loaded.
     *
     * @param {boolean} loading - Is this question being resumed?
     *
     * @fires Numbas.Question#functionsMade
     * @fires Numbas.Question#constantsMade
     * @fires Numbas.Question#rulesetsMade
     * @fires Numbas.Question#variablesSet
     * @fires Numbas.Question#variablesGenerated
     * @fires Numbas.Question#ready
     * @fires Numbas.Question#variablesTodoMade
     * @listens Numbas.Question#preambleLoaded
     * @listens Numbas.Question#functionsLoaded
     * @listens Numbas.Question#rulesetsLoaded
     * @listens Numbas.Question#generateVariables
     * @listens Numbas.Question#constantsMade
     * @listens Numbas.Question#functionsMade
     * @listens Numbas.Question#rulesetsMade
     * @listens Numbas.Question#variableDefinitionsLoaded
     * @listens Numbas.Question#variablesSet
     * @listens Numbas.Question#variablesGenerated
     * @listens Numbas.Question#variablesTodoMade
     * @listens Numbas.Question#partsGenerated
     * @listens Numbas.Question#ready
     * @listens Numbas.Question#HTMLAttached
     */
    finaliseLoad: function(loading) {
        var q = this;

        q.displayNumber = q.exam ? q.exam.questionList.filter(function(q2) {
            return q2.number < q.number && !q2.hasCustomName;
        }).length : 0;

        q.signals.on('preambleLoaded', function() {
            q.runPreamble();
            if(q.partsMode == 'explore') {
                if(q.maxMarks == 0) {
                    q.objectives.forEach(function(o) {
                        q.maxMarks += o.limit;
                    });
                }
            }
        });
        q.signals.on(['preambleRun', 'constantsLoaded'], function() {
            var enabled_constants = {};
            q.constantsTodo.builtin.forEach(function(c) {
                c.name.split(',').forEach(function(name) {
                    enabled_constants[name] = c.enable;
                });
            });
            Numbas.jme.variables.makeConstants(Numbas.jme.builtin_constants, q.scope, enabled_constants);
            Numbas.jme.variables.makeConstants(q.constantsTodo.custom, q.scope);
            q.signals.trigger('constantsMade');
        });
        q.signals.on(['preambleRun', 'functionsLoaded'], function() {
            var functions = Numbas.jme.variables.makeFunctions(q.functionsTodo, q.scope, {question:q});
            q.scope = new jme.Scope([q.scope, {functions: functions}]);
            q.signals.trigger('functionsMade');
        });
        q.signals.on(['preambleRun', 'rulesetsLoaded'], function() {
            Numbas.jme.variables.makeRulesets(q.rulesets, q.scope);
            q.signals.trigger('rulesetsMade');
        });
        q.signals.on(['variableDefinitionsLoaded', 'functionsMade', 'rulesetsMade', 'constantsMade'], function() {
            var todo = q.variablesTodo = {};
            var seen_names = {}
            q.variableDefinitions.forEach(function(def) {
                var name = jme.normaliseName(def.name.trim());
                var names = jme.variables.splitVariableNames(name);
                names.forEach(function(n) {
                    if(seen_names[n]) {
                        q.error("jme.variables.duplicate definition", {name:n});
                    }
                    seen_names[n] = true;
                });
                var definition = def.definition.toString().trim();
                if(name == '') {
                    if(definition == '') {
                        return;
                    }
                    q.error('jme.variables.empty name');
                }
                if(definition == '') {
                    q.error('jme.variables.empty definition', {name:name});
                }
                try {
                    var tree = Numbas.jme.compile(definition);
                } catch {
                    q.error('variable.error in variable definition', {name:name});
                }
                var vars = Numbas.jme.findvars(tree, [], q.scope);
                todo[name] = {
                    tree: tree,
                    vars: vars
                };
            });
            q.signals.trigger('variablesTodoMade')
        });
        q.signals.on(['generateVariables', 'functionsMade', 'rulesetsMade', 'constantsMade', 'variablesTodoMade'], function() {
            var conditionSatisfied = false;
            var condition = jme.compile(q.variablesTest.condition);
            var runs = 0;
            var scope;
            var maxRuns = q.variablesTest.maxRuns;
            if(isNaN(maxRuns) || maxRuns < 1) {
                maxRuns = 1;
            }
            maxRuns = Math.min(1000000, maxRuns);
            while(runs < maxRuns && !conditionSatisfied) {
                runs += 1;
                scope = new jme.Scope([q.scope]);
                scope.setVariable('variable_generation_run_number', new jme.types.TNum(runs));
                var result = jme.variables.makeVariables(q.variablesTodo, scope, condition);
                conditionSatisfied = result.conditionSatisfied;
            }
            if(!conditionSatisfied) {
                q.error('jme.variables.question took too many runs to generate variables');
            } else {
                q.scope = scope;
            }
            q.signals.trigger('variablesSet');
        });
        q.signals.on('variablesSet', function() {
            q.scope = new jme.Scope([q.scope]);
            q.scope.flatten();
            q.local_definitions = {
                variables: q.variableDefinitions.map(function(d) {
                    return d.name;
                }).filter(function(n) {
                    return n.trim();
                }),
                functions: Object.keys(q.functionsTodo),
                rulesets: Object.keys(q.rulesets)
            };
            q.unwrappedVariables = {};
            var all_variables = q.scope.allVariables()
            for(const [name, v] of Object.entries(all_variables)) {
                q.unwrappedVariables[name] = Numbas.jme.unwrapValue(v);
            }
            q.signals.trigger('variablesGenerated');
        });
        q.signals.on('variablesGenerated', function() {
            q.name = jme.contentsubvars(q.name, q.scope);
        });
        if(Numbas.display && q.exam && q.exam.display) {
            q.display = new Numbas.display.QuestionDisplay(q);
        }
        q.signals.on('partsGenerated', function() {
            var i = 0;
            q.parts.forEach(function(p) {
                var hasName = p.assignName(i, q.parts.length - 1);
                i += hasName ? 1 : 0;
            });
        });
        q.signals.on(['variablesGenerated', 'partsGenerated'], function() {
            //initialise display - get question HTML, make menu item, etc.
            q.display && q.display.makeHTML();
        });
        q.signals.on(['variablesGenerated', 'partsGenerated'], function() {
            q.signals.trigger('finalisedLoad');
        });
        if(!loading) {
            q.signals.on('finalisedLoad', function() {
                q.signals.trigger('ready');
            });
        }
        q.signals.on('ready', function() {
            q.updateScore();
        });
        q.signals.on(['ready', 'HTMLAttached'], function() {
            q.display && q.display.showScore();
        });
    },

    /** Get this question's scope object.
     *
     * @returns {Numbas.jme.Scope}
     */
    getScope: function() {
        return this.scope;
    },

    /** Generate this question's variables.
     *
     * @fires Numbas.Question#generateVariables
     */
    generateVariables: function() {
        this.signals.trigger('generateVariables');
    },
    /** Load saved data about this question from storage.
     *
     * @fires Numbas.Question#variablesSet
     * @fires Numbas.Question#partsResumed
     * @listens Numbas.Question#partsGenerated
     * @listens Numbas.Question#ready
     */
    resume: function() {
        if(!this.store) {
            return;
        }
        var q = this;

        // check the suspend data was for this question - if the test is updated and the question set changes, this won't be the case!
        q.signals.on(['constantsMade'], function() {
            var qobj = q.store.loadQuestion(q);
            for(const [k, v] of Object.entries(qobj.variables)) {
                q.scope.setVariable(k, v);
            }
            q.generateVariables();
            q.signals.on(['variablesSet', 'partsGenerated'], function() {
                q.parts.forEach(function(part) {
                    part.resume();
                });
                if(q.partsMode == 'explore') {
                    qobj.parts.slice(1).forEach(function(pobj, qindex) {
                        var index = pobj.index;
                        var previousPart = q.getPart(pobj.previousPart);
                        var ppobj = q.store.loadPart(previousPart);
                        var i = 0;
                        for(;i < previousPart.nextParts.length;i++) {
                            if(previousPart.nextParts[i].index == index) {
                                break;
                            }
                        }
                        var np = previousPart.nextParts[i];
                        var npobj = ppobj.nextParts[i];
                        np.instanceVariables = q.store.loadVariables(npobj.variableReplacements, previousPart.getScope());
                        previousPart.makeNextPart(np, qindex + 1);
                        np.instance.resume();
                    });
                }

                const part_submit_promises = {};
                q.allParts().forEach(function(p) {
                    var obj = {};
                    obj.promise = new Promise(function(resolve, reject) {
                        obj.resolve = resolve;
                    });
                    part_submit_promises[p.path] = obj;
                });
                /** Submit a given part, setting its `resume` property so it doesn't save to storage.
                 *
                 * @param {Numbas.parts.Part} part
                 */
                const promises_to_wait_for = [];
                q.promises_to_wait_for = promises_to_wait_for;

                /** Submit the previously submitted answer for this part, restoring its feedback.
                 *
                 * @param {Numbas.parts.Part} part
                 */
                function submit_part(part) {
                    const {promise, resolve} = part_submit_promises[part.path];
                    promises_to_wait_for.push(promise);
                    part.resuming = true;

                    const replacement_promises = part.settings.errorCarriedForwardReplacements.map((vr) => part_submit_promises[vr.part].promise);
                    Promise.all(replacement_promises).then(function() {
                        if(part.answered) {
                            part.submit();
                        }
                        if(part.resume_stagedAnswer !== undefined) {
                            part.stagedAnswer = part.resume_stagedAnswer;
                        }
                        part.resuming = false;
                        if(part.waiting_for_pre_submit) {
                            part.waiting_for_pre_submit.then(function() {
                                resolve();
                            });
                        } else {
                            resolve();
                        }
                    });
                    if(part.gaps) {
                        promise.then(function() {
                            part.gaps.forEach(function(g) {
                                part_submit_promises[g.path].resolve();
                            });
                        });
                    }
                }

                q.signals.on('finalisedLoad', function() {
                    q.parts.forEach(function(part) {
                        part.steps.forEach(submit_part);
                        submit_part(part);
                    });

                    Promise.all(promises_to_wait_for).then(function() {
                        q.signals.trigger('partsResumed');
                    });
                });

                q.signals.on('partsResumed', function() {
                    q.signals.trigger('ready');
                });
            });
            q.signals.on('partsResumed', function() {
                q.adviceDisplayed = qobj.adviceDisplayed;
                q.answered = qobj.answered;
                q.revealed = qobj.revealed;
                q.submitted = qobj.submitted;
                q.visited = qobj.visited;
                q.score = qobj.score;
                if(q.revealed) {
                    q.revealAnswer(true);
                } else if(q.adviceDisplayed) {
                    q.getAdvice(true);
                }
                q.display && q.display.resume();
                q.updateScore();
                if(q.partsMode == 'explore') {
                    q.setCurrentPart(q.getPart(qobj.currentPart));
                }
                q.signals.trigger('resume');
            });
        });
    },
    /** XML definition of this question.
     *
     * @type {Element}
     */
    xml: null,
    /** Position of this question in the exam.
     *
     * @type {number}
     */
    number: -1,
    /** The question's name.
     *
     * @type {string}
     */
    name: '',
    /** The question's statement text.
     *
     * @type {string}
     */
    statement: '',
    /** The question's advice text.
     *
     * @type {string}
     */
    advice: '',
    /** The JME scope for this question. Contains variables, functions and rulesets defined in this question.
     *
     * @type {Numbas.jme.Scope}
     */
    scope: null,
    /** Maximum marks available for this question.
     *
     * @type {number}
     */
    marks: 0,
    /** Student's score on this question.
     *
     * @type {number}
     */
    score: 0,
    /** Has this question been seen by the student? For determining if you can jump back to this question, when {@link Numbas.Question.navigateBrowse} is disabled.
     *
     * @type {boolean}
     */
    visited: false,
    /** Has this question been answered satisfactorily?
     *
     * @type {boolean}
     */
    answered: false,
    /** Number of times this question has been submitted.
     *
     * @type {number}
     */
    submitted: 0,
    /** Has the advice been displayed?
     *
     * @type {boolean}
     */
    adviceDisplayed: false,
    /** Has this question been locked?
     *
     * @type {boolean}
     */
    locked: false,
    /** Have the correct answers been revealed?
     *
     * @type {boolean}
     */
    revealed: false,
    /** Parts belonging to this question, in the order they're displayed.
     *
     * @type {Numbas.parts.Part}
     */
    parts: [],
    /** Dictionary mapping part addresses (of the form `qXpY[gZ]`) to {@link Numbas.parts.Part} objects.
     *
     * @type {Object<Numbas.parts.Part>}
     */
    partDictionary: {},
    /** The indices in the definition of the extra parts that have been added to this question.
     *
     * @type {Array.<number>}
     */
    extraPartOrder: [],
    /** Associated display object.
     *
     * @type {Numbas.display.QuestionDisplay}
     */
    display: undefined,
    /** Callbacks to run when various events happen.
     *
     * @property {Array.<Function>} HTMLAttached - Run when the question's HTML has been attached to the page.
     * @property {Array.<Function>} variablesGenerated - Run when the question's variables have been generated.
     * @type {Object<Array.<Function>>}
     */
    callbacks: {
    },
    /** Leave this question - called when moving to another question, or showing an info page.
     *
     * @fires Numbas.Question#event:leave
     * @see Numbas.display.QuestionDisplay.leave
     */
    leave: function() {
        this.display && this.display.leave();
        this.events.trigger('leave');
    },
    /** Execute the question's JavaScript preamble - should happen as soon as the configuration has been loaded from XML, before variables are generated.
     *
     * @fires Numbas.Question#preambleRun
     * @returns {Promise} - Resolves once the preamble has been run.
     */
    runPreamble: function() {
        var jfn = new Function(['question'], this.preamble.js);
        var res;
        try {
            res = jfn(this);
            return Promise.resolve(res).then(() => {
                this.signals.trigger('preambleRun');
            }).catch((e) => {
                try {
                    this.error('question.preamble.error', {message: e.message});
                } catch(e) {
                    Numbas.schedule.halt(e);
                }
            });
        } catch(e) {
            var errorName = e.name == 'SyntaxError' ? 'question.preamble.syntax error' : 'question.preamble.error';
            this.error(errorName, {message: e.message});
        }
    },
    /** Get the part object corresponding to a path.
     *
     * @param {Numbas.parts.partpath} path
     * @returns {Numbas.parts.Part}
     */
    getPart: function(path) {
        var p = this.partDictionary[path];
        if(!p) {
            this.error("question.no such part", {path:path});
        }
        return p;
    },

    /** Get the explore mode objective with the given name.
     *
     * @param {string} name
     * @returns {object}
     */
    getObjective: function(name) {
        return this.objectives.find(function(o) {
            return o.name == name;
        });
    },

    /** Get the explore mode penalty with the given name.
     *
     * @param {string} name
     * @returns {object}
     */
    getPenalty: function(name) {
        return this.penalties.find(function(p) {
            return p.name == name;
        });
    },

    /** Show the question's advice.
     *
     * @param {boolean} dontStore - Don't tell the storage that the advice has been shown - use when loading from storage!
     * @fires Numbas.Question#adviceDisplayed
     */
    getAdvice: function(dontStore) {
        if(!Numbas.is_instructor && this.exam && this.exam.settings.revealAdvice == 'never') {
            return;
        }
        this.adviceDisplayed = true;
        this.display && this.display.showAdvice(true);
        if(this.store && !dontStore) {
            this.store.adviceDisplayed(this);
        }
        this.signals.trigger('adviceDisplayed', dontStore);
    },

    /** Lock this question - the student can no longer change their answers.
     *
     * @fires Numbas.Question#event:locked
     */
    lock: function() {
        this.locked = true;
        this.allParts().forEach(function(part) {
            part.lock();
        });
        this.display && this.display.end();
        this.events.trigger('locked');
    },
    /** Reveal the correct answers to the student.
     *
     * @param {boolean} dontStore - Don't tell the storage that the advice has been shown - use when loading from storage!
     * @fires Numbas.Question#revealed
     */
    revealAnswer: function(dontStore) {
        this.lock();
        this.revealed = true;
        //display advice if allowed
        this.getAdvice(dontStore);
        //part-specific reveal code. Might want to do some logging in future?
        for(let i = 0; i < this.parts.length; i++) {
            this.parts[i].revealAnswer(dontStore);
        }
        if(this.display) {
            //display revealed answers
            this.display.revealAnswer();
            this.display.showScore();
        }
        if(this.store && !dontStore) {
            this.store.answerRevealed(this);
        }
        this.exam && this.exam.updateScore();
        this.signals.trigger('revealed', dontStore);
    },
    /** Validate the student's answers to the question. True if all parts are either answered or have no marks available.
     *
     * @returns {boolean}
     */
    validate: function() {
        switch(this.partsMode) {
            case 'all':
                var success = true;
                for(let i = 0; i < this.parts.length; i++) {
                    success = success && (this.parts[i].answered || this.parts[i].marks == 0);
                }
                return success;
            case 'explore':
                var numAnswered = 0;
                var numMarked = 0;
                this.parts.forEach(function(p) {
                    if(p.doesMarking) {
                        numMarked += 1;
                        if(p.answered) {
                            numAnswered += 1;
                        }
                    }
                });
                return numMarked > 0 && numAnswered == numMarked;
        }
    },
    /** Has anything been changed since the last submission? If any part has `isDirty` set to true, return true.
     *
     * @returns {boolean}
     */
    isDirty: function() {
        if(this.revealed) {
            return false;
        }
        for(let i = 0;i < this.parts.length; i++) {
            if(this.parts[i].isDirty) {
                return true;
            }
        }
        return false;
    },
    /** Show a warning and return true if the question is dirty.
     *
     * @see Numbas.Question#isDirty
     * @fires Numbas.Question#event:leavingDirtyQuestion
     * @returns {boolean}
     */
    leavingDirtyQuestion: function() {
        if(this.answered && this.isDirty()) {
            this.exam?.display && this.exam.display.root_element.showAlert(R('question.unsubmitted changes', {count:this.parts.length}));
            this.events.trigger('leavingDirtyQuestion');
            return true;
        }
        return false;
    },
    /** Calculate the student's total score for this question - adds up all part scores.
     *
     * @fires Numbas.Question#event:calculateScore
     */
    calculateScore: function() {
        var q = this;
        var score = 0;
        var marks = 0;

        switch(this.partsMode) {
            case 'all':
                for(let i = 0; i < this.parts.length; i++) {
                    var part = this.parts[i];
                    score += part.score;
                    marks += part.marks;
                }
                break;
            case 'explore':
                marks = this.maxMarks;
                this.objectives.forEach(function(o) {
                    o.score = 0;
                    o.answered = false;
                });
                this.penalties.forEach(function(p) {
                    p.score = 0;
                    p.applied = false;
                });
                this.allParts().forEach(function(part) {
                    part.nextParts.forEach(function(np) {
                        if(np.instance) {
                            var penalty = q.getPenalty(np.penalty);
                            if(penalty) {
                                penalty.score += np.penaltyAmount;
                                penalty.applied = true;
                            }
                        }
                    });

                    var objective = q.getObjective(part.settings.exploreObjective);
                    if(!objective) {
                        return;
                    }
                    objective.score += part.score;
                    objective.answered = objective.answered || part.answered;

                });
                this.objectives.forEach(function(o) {
                    o.score = Math.min(o.limit, o.score);
                    score += o.score;
                });
                this.penalties.forEach(function(p) {
                    p.score = Math.min(p.limit, p.score);
                    score -= p.score;
                });
                score = Math.min(this.maxMarks, Math.max(0, score));
                break;
        }

        this.score = score;
        this.marks = marks;
        this.answered = this.validate();
        this.events.trigger('calculateScore');
    },
    /** Submit every part in the question.
     *
     * @fires Numbas.Question#event:pre-submit
     * @fires Numbas.Question#event:post-submit
     */
    submit: function() {
        this.events.trigger('pre-submit');
        //submit every part
        for(let i = 0; i < this.parts.length; i++) {
            this.parts[i].submit();
        }
        //validate every part
        //displays warning messages if appropriate,
        //and returns false if any part is not completed sufficiently
        this.answered = this.validate();
        //keep track of how many times question successfully submitted
        if(this.answered) {
            this.submitted += 1;
        }
        this.updateScore();
        this.store && this.store.questionSubmitted(this);
        this.events.trigger('post-submit');
    },
    /**
     * Recalculate the student's score, update the display, and notify storage.
     *
     * @fires Numbas.Question#event:updateScore
     */
    updateScore: function() {
        //calculate score
        this.calculateScore();
        //update total exam score
        this.exam && this.exam.updateScore();
        //display score - ticks and crosses etc.
        this.display && this.display.showScore();
        //notify storage
        this.store && this.store.saveQuestion(this);
        this.events.trigger('updateScore');
    },
    /** Add a callback function to run when the question's HTML is attached to the page.
     *
     * @param {Function} fn
     * @deprecated Use {@link Numbas.Question#signals} instead.
     * @listens Numbas.Question#HTMLAttached
     */
    onHTMLAttached: function(fn) {
        this.signals.on('HTMLAttached', fn);
    },
    /** Add a callback function to run when the question's variables are generated (but before the HTML is attached).
     *
     * @param {Function} fn
     * @deprecated Use {@link Numbas.Question#signals} instead.
     * @listens Numbas.Question#variablesGenerated
     */
    onVariablesGenerated: function(fn) {
        this.signals.on('variablesGenerated', fn);
    }
};
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Provides {@link Numbas.schedule} */
Numbas.queueScript('schedule', ['base'], function() {

/** Schedule functions to be called. The scheduler can put tiny timeouts in between function calls so the browser doesn't become unresponsive. It also updates the loading bar.
 *
 * @namespace Numbas.schedule
 */
var schedule = Numbas.schedule = /** @lends Numbas.schedule */ {
    /** Functions to call.
     *
     * @type {Function[]}
     */
    calls: [],
    /** Bits of queue that have been picked up while a task performs sub-tasks.
     *
     * @type {Array.<Array.<Function>>}
     */
    lifts: [],
    /** Number of tasks completed.
     *
     * @type {number}
     */
    completed: 0,
    /** Total number of tasks ever scheduled.
     *
     * @type {number}
     */
    total: 0,
    /** All signal box objects.
     *
     * @type {Array.<Numbas.schedule.SignalBox>}
     */
    signalboxes: [],
    /** Should the scheduler stop running tasks?
     * Don't use this directly - use {@link Numbas.schedule.halt}.
     *
     * @type {boolean}
     */
    halted:false,
    /** Reset the scheduler: remove all callbacks and signal boxes.
     */
    reset: function() {
        schedule.calls = [];
        schedule.lifts = [];
        schedule.completed = 0;
        schedule.total = 0;
        schedule.signalboxes = [];
        Numbas.signals = new Numbas.schedule.SignalBox();
    },
    /** Error which caused the scheduler to halt.
     *
     * @type {Error}
     */
    halt_error: null,
    /** Prevent the scheduler from running any more tasks, and save the error message which caused this.
     *
     * @param {Error} error
     * @see Numbas.schedule.halted
     * @see Numbas.schedule.halt_error
     */
    halt: function(error) {
        Numbas.display && Numbas.display.die(error);
        schedule.halted = true;
        schedule.halt_error = error;
        schedule.signalboxes.forEach(function(sb) {
            sb.halt(error);
        });
    },
    /** Allow the scheduler to run tasks again.
     * Unhalts all signalboxes too.
     */
    unhalt: function() {
        schedule.halted = false;
        schedule.signalboxes.forEach(function(sb) {
            sb.unhalt();
        });
    },
    /** @typedef {object} Numbas.schedule.task_object
     * @property {Function} task - The function to execute.
     * @property {Function} error - A callback, used if an error is raised.
     */
    /** Add a task to the queue.
     *
     * @param {Function|Numbas.schedule.task_object} fn - The function to run, or a dictionary `{task: fn, error: fn}`, where `error` is a callback if an error is caused.
     * @param {object} that - What `this` should be when the function is called.
     */
    add: function(fn, that) {
        if(schedule.halted) {
            return;
        }
        var args = [];
        var l = arguments.length;
        for(var i = 2;i < l;i++) {
            args[i - 2] = arguments[i];
        }
        if(typeof(fn) == 'function') {
            fn = {task: fn};
        }
        var task = function() {
            try {
                fn.task.apply(that, args);
            } catch(e) {
                if(fn.error) {
                    fn.error(e);
                } else {
                    throw(e);
                }
            }
        };
        schedule.calls.push(task);
        setTimeout(schedule.pop, 0);
        schedule.total++;
    },
    /** Pop the first task off the queue and run it.
     *
     * If there's an error, the scheduler halts and shows the error.
     */
    pop: function() {
        var calls = schedule.calls;
        if(!calls.length || schedule.halted) {
            return;
        }
        var task = calls.shift();
        schedule.lift();
        try {
            task();
        } catch(e) {
            schedule.halt(e);
        }
        schedule.drop();
        schedule.completed++;
        Numbas.display && Numbas.display.showLoadProgress();
    },
    /** Pick up the current queue and put stuff in front. Called before running a task, so it can queue things which must be done before the rest of the queue is called. */
    lift: function() {
        schedule.lifts.push(schedule.calls);
        schedule.calls = new Array();
    },
    /** Put the last lifted queue back on the end of the real queue. */
    drop: function() {
        schedule.calls = schedule.calls.concat(schedule.lifts.pop());
    },
};

/** Coordinates Promises corresponding to different stages in the loading process.
 *
 * @class
 * @memberof Numbas.schedule
 */
var SignalBox = schedule.SignalBox = function() {
    this.callbacks = {};
    this.generic_listeners = [];
    schedule.signalboxes.push(this);
}
SignalBox.prototype = { /** @lends Numbas.schedule.SignalBox.prototype */
    /** @typedef Numbas.schedule.callback
     * @type {object}
     * @property {Promise} promise - A promise that will resolve when this signal is triggered.
     * @property {Function} resolve - The promise's `resolve` function.
     * @property {Function} reject - The promise's `reject` function.
     * @property {boolean} resolved - Has the promise been resolved?
     */

    /** Dictionary of registered callbacks.
     *
     * @type {Object<Numbas.schedule.callback>}
     * @private
     */
    callbacks: {},

    /** Callback functions which will be called when any signal is triggered.
     *
     * @type {Array<Numbas.schedule.callback>}
     * @private
     */
    generic_listeners: [],

    /** Get a callback object for the event with the given name.
     * If the callback hasn't been accessed before, it's created.
     *
     * @param {string} name
     * @returns {Numbas.schedule.callback}
     */
    getCallback: function(name) {
        if(this.callbacks[name]) {
            return this.callbacks[name];
        }
        var deferred = this.callbacks[name] = {};
        deferred.promise = new Promise(function(resolve, reject) {
            deferred.resolve = resolve;
            deferred.reject = reject;
        });
        deferred.promise.catch(function(e) {
            deferred.reject(e);
        });
        return deferred;
    },

    /** Once the given event(s) have resolved, run the given callback function. Returns a Promise, so can be used without a callback.
     *
     * @param {string|Array.<string>} events - The name of an event, or a list of event names.
     * @param {Function} [fn] - A callback function to run.
     * @returns {Promise} Resolves when all of the events have resolved, or rejects if the signal box is in an error state.
     */
    on: function(events, fn) {
        var sb = this;
        if(sb.error) {
            return Promise.reject(sb.error);
        }
        if(typeof(events) == 'string') {
            events = [events];
        }
        var promises = [];
        events.map(function(name) {
            var callback = sb.getCallback(name);
            promises.push(callback.promise);
            return callback;
        });
        var promise = Promise.all(promises);
        if(fn) {
            promise = promise.then(function() {
                return new Promise(function(resolve, reject) {
                    try {
                        if(schedule.halted) {
                            reject(schedule.halt_error)
                        }
                        var result = fn();
                        resolve(result);
                    } catch(e) {
                        reject(e);
                    }
                });
            });
            promise.catch(function(e) {
                sb.halt(e);
            });
        }
        return promise;
    },

    /**
     * Register a callback function which will be called whenever any signal has resolved.
     * The callback is called with the name of the triggered signal.
     *
     * @param {Function} fn
     */
    on_any: function(fn) {
        this.generic_listeners.push(fn);
    },

    /** Halt this signal box because of an error: reject all outstanding promises.
     *
     * @param {Error} error - The error that caused the signal box to halt.
     */
    halt: function(error) {
        this.error = error;
        for(var x in this.callbacks) {
            this.callbacks[x].reject(error);
        }
    },

    /** Unhalt this signal box: allow promises to be made again.
     */
    unhalt: function() {
        this.error = null;
    },

    /** Notify the signal box that the event with the given name has happened.
     *
     * @param {string} name
     */
    trigger: function(name) {
        var callback = this.getCallback(name);
        if(this.error) {
            callback.reject(this.error);
        }
        callback.resolved = true;
        callback.resolve();

        this.generic_listeners.forEach((fn) => {
            fn(name);
        });
    }
}

/** Coordinates callbacks to run whenever named events happen.
 *
 * @class
 * @memberof Numbas.schedule
 */
var EventBox = schedule.EventBox = function() {
    this.events = {};
}
EventBox.prototype = {
    getEvent: function(name) {
        if(this.events[name]) {
            return this.events[name];
        }
        var ev = this.events[name] = {
            listeners: []
        }
        this.setEventPromise(ev);
        return ev;
    },

    setEventPromise: function(ev) {
        ev.next = new Promise(function(resolve, reject) {
            ev.next_resolve = resolve;
        });
    },

    /** Register a callback function which is called every time the event is triggered.
     *
     * @param {string} name
     * @param {Function} callback
     */
    on: function(name, callback) {
        var ev = this.getEvent(name);
        ev.listeners.push(callback);
    },

    /** Returns a promise which is resolved the next time the event is triggered.
     *
     * @param {string} name
     * @returns {Promise}
     */
    once: function(name) {
        var ev = this.getEvent(name);
        return ev.next;
    },

    trigger: function(name) {
        var ev = this.getEvent(name);
        var args = Array.from(arguments).slice(1);
        ev.listeners.forEach(function(callback) {
            callback.apply(this, args);
        });
        this.getEvent('').listeners.forEach(function(callback) {
            callback.apply(this, [name, ...args]);
        });
        ev.next_resolve(...arguments);
        this.setEventPromise(ev);
    }
}
/** Signals produced by the Numbas runtime.
 *
 * @type {Numbas.schedule.SignalBox}
 * @memberof Numbas
 */
schedule.reset();

/** Manages a queue of tasks.
 *
 * @memberof Numbas
 */
class Scheduler {
    num_jobs = 0;

    completed_jobs = 0;

    constructor() {
        this.events = new EventBox();
        this.last = Promise.resolve();
    }

    /** Add a task to the queue.
     *
     * @param {Function} fn
     */
    job(fn) {
        this.num_jobs += 1;
        const i = this.num_jobs;
        this.events.trigger('add job', i);
        this.last = this.last.then(fn).catch((error) => {
            Numbas.display && Numbas.display.die(error);
            console.error(error);
        });

        this.last = this.last.then(async () => {
            await (new Promise((resolve) => {
                setTimeout(resolve, 1);
            }));
        })

        this.last.then(() => {
            this.completed_jobs += 1;
            this.events.trigger('finish job', i);
        });
    }
}

Numbas.Scheduler = Scheduler;

});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Provides a storage API {@link Numbas.storage.SCORMStorage} which interfaces with SCORM */
Numbas.queueScript('scorm-storage', ['base', 'util', 'SCORM_API_wrapper', 'storage', 'jme-display'], function() {
var scorm = Numbas.storage.scorm = {};
/** SCORM storage object - controls saving and loading of data from the LMS.
 *
 * @class
 * @memberof Numbas.storage
 */
class SCORMStorage extends Numbas.storage.Storage {
    constructor() {
        super();

        if(pipwerks.SCORM.init()) {
           Numbas.storage.lmsConnected = true;
        } else {
            var errorCode = pipwerks.SCORM.debug.getCode();
            if(errorCode) {
                throw(new Numbas.Error(R('scorm.error initialising', {message: pipwerks.SCORM.debug.getInfo(errorCode)})));
            }
            //if the pretend LMS extension is loaded, we can start that up
            if(Numbas.storage.PretendLMS) {
                if(!Numbas.storage.lms) {
                    Numbas.storage.lms = new Numbas.storage.PretendLMS();
                }
                window.API_1484_11 = Numbas.storage.lms.API;
                pipwerks.SCORM.API.handle = window.API_1484_11;
                pipwerks.SCORM.API.isFound = true;
                pipwerks.SCORM.version = '2004';
                pipwerks.SCORM.init();
            } else {
            //otherwise return a blank storage object which does nothing
                return new Numbas.storage.Storage();
            }
        }
        this.getEntry();
        //get all question-objective indices
        this.questionIndices = {};
        var numObjectives = parseInt(this.get('cmi.objectives._count'), 10);
        for(let i = 0;i < numObjectives;i++) {
            const id = this.get('cmi.objectives.' + i + '.id');
            this.questionIndices[id] = i;
        }
        //get part-interaction indices
        this.partIndices = {};
        var numInteractions = parseInt(this.get('cmi.interactions._count'), 10);
        for(let i = 0;i < numInteractions;i++) {
            const id = this.get('cmi.interactions.' + i + '.id');
            this.partIndices[id] = i;
        }
        Numbas.is_instructor = this.get('numbas.user_role') == 'instructor';
    }

    /** Mode the session started in:
     *
     * - `ab-initio` - starting a new attempt;
     * - `resume` - loaded attempt in progress.
     */
    mode = 'ab-initio';

    /** Indicates whether a true SCORM connection to an LMS exists.
     *
     * @type {boolean}
     */
    lmsConnected = true;

    /** Reference to the {@link Numbas.Exam} object for the current exam.
     *
     * @type {Numbas.Exam}
     */
    exam = undefined;

    /** Dictionary mapping question ids (of the form `qN`) to `cmi.objective` indices.
     *
     * @type {Object<number>}
     */
    questionIndices = {};

    /** Dictionary mapping {@link Numbas.parts.partpath} ids to `cmi.interaction` indices.
     *
     * @type {Object<number>}
     */
    partIndices = {};

    /** The last `cmi.suspend_data` object.
     *
     * @type {Numbas.storage.exam_suspend_data}
     */
    suspendData = undefined;

    /** Save SCORM data - call the SCORM commit method to make sure the data model is saved to the server. */
    save() {
        var exam = this.exam;
        /** Try to save. Display a "saving" message, then call `SCORM.save()`. If it succeeds, hide the message, else wait and try again.
         */
        function trySave() {
            exam.display && exam.display.saving(true);
            var saved = pipwerks.SCORM.save();
            if(!saved) {
                exam.display.root_element.showAlert(R('scorm.failed save'), function() {
                    setTimeout(trySave, 1);
                });
            } else {
                exam.display && exam.display.saving(false);
            }
        }
        trySave();
    }

    /** Set a SCORM data model element.
     *
     * @param {string} key - Element name. This is prepended with `cmi.`.
     * @param {string} value - Element value.
     * @returns {boolean} - Did the call succeed?
     */
    set(key, value) {
        var val = pipwerks.SCORM.set(key, value);
        return val;
    }

    /** Get a SCORM data model element.
     *
     * @param {string} key - Element name. This is prepended with `cmi.`.
     * @returns {string} - The value of the element.
     */
    get(key) {
        var val = pipwerks.SCORM.get(key);
        return val;
    }

    /** Make an id string corresponding to a question, of the form `qN`, where `N` is the question's number.
     *
     * @param {Numbas.Question} question
     * @returns {string}
     */
    getQuestionId(question) {
        return 'q' + question.number;
    }

    /** Make an id string corresponding to a part, of the form `qNpXgYsZ`.
     *
     * @param {Numbas.parts.Part} part
     * @returns {string}
     */
    getPartId(part) {
        return this.getQuestionId(part.question) + part.path;
    }

    /** Load student's name and ID.
     */
    get_student_name() {
        if(this.exam) {
            this.exam.student_name = this.get('cmi.learner_name');
            this.exam.student_id = this.get('cmi.learner_id');
        }
    }

    /** Get the initial seed value.
     *
     * @returns {string}
     */
    get_initial_seed() {
        return this.get('numbas.initial_seed');
    }

    listen_messages() {
        var sc = this;
        this.receive_window_message = function(ev) {
            var data = ev.data;
            try {
                var change = data['numbas change'];
                switch(change) {
                    case 'exam duration extension':
                        sc.exam.updateDurationExtension();
                        break;
                }
            } catch {
            }
        }
        window.addEventListener('message', this.receive_window_message);
    }

    /** Initialise the SCORM data model and this storage object.
     *
     * @param {Numbas.Exam} exam
     */
    init(exam) {
        this.exam = exam;
        this.listen_messages();
        this.get_student_name();
        this.set('cmi.completion_status', 'incomplete');
        this.set('cmi.exit', 'suspend');
        this.set('cmi.progress_measure', 0);
        this.set('cmi.session_time', 'PT0H0M0S');
        this.set('cmi.success_status', 'unknown');
        this.set('cmi.score.scaled', 0);
        this.set('cmi.score.raw', 0);
        this.set('cmi.score.min', 0);
        this.questionIndices = {};
        this.partIndices = {};
    }

    init_questions() {
        for(let i = 0; i < this.exam.settings.numQuestions; i++) {
            this.initQuestion(this.exam.questionList[i]);
        }
        this.setSuspendData();
        this.set('cmi.score.max', this.exam.mark);
    }

    /** Initialise a question - make an objective for it, and initialise all its parts.
     *
     * @param {Numbas.Question} q
     */
    initQuestion(q) {
        var id = this.getQuestionId(q);
        if(this.questionIndices[id] === undefined) {
            var index = this.get('cmi.objectives._count');
            this.questionIndices[id] = index;
        }
        var prepath = 'cmi.objectives.' + this.questionIndices[id] + '.';
        this.set(prepath + 'id', id);
        this.set(prepath + 'score.min', 0);
        this.set(prepath + 'score.max', q.marks);
        this.set(prepath + 'score.raw', q.score || 0);
        this.set(prepath + 'success_status', 'unknown');
        this.set(prepath + 'completion_status', 'not attempted');
        this.set(prepath + 'progress_measure', 0);
        this.set(prepath + 'description', q.name);
        for(let i = 0; i < q.parts.length;i++) {
            this.initPart(q.parts[i]);
        }
    }

    /**
     * Initialise a part - make an interaction for it, and set up correct responses.
     *
     * @param {Numbas.parts.Part} p
     */
    initPart(p) {
        var id = this.getPartId(p);
        if(this.partIndices[id] === undefined) {
            var index = this.get('cmi.interactions._count');
            this.partIndices[id] = index;
        }
        var prepath = this.partPath(p);
        this.set(prepath + 'id', id);
        this.set(prepath + 'objectives.0.id', this.getQuestionId(p.question));
        this.set(prepath + 'weighting', p.marks);
        this.set(prepath + 'result', 0);
        this.set(prepath + 'description', p.type);
        var typeStorage = this.getPartStorage(p);
        if(typeStorage) {
            this.set(prepath + 'type', typeStorage.interaction_type(p));
            var correct_answer = typeStorage.correct_answer(p);
            if(correct_answer !== undefined) {
                this.set(prepath + 'correct_responses.0.pattern', correct_answer);
            }
        }
        if(p.type == 'gapfill') {
            for(let i = 0;i < p.gaps.length;i++) {
                this.initPart(p.gaps[i]);
            }
        }
        for(let i = 0;i < p.steps.length;i++) {
            this.initPart(p.steps[i]);
        }
    }

    /** Save the exam suspend data using the `cmi.suspend_data` string.
     */
    setSuspendData() {
        var eobj = this.examSuspendData();
        if(eobj !== undefined) {
            var estr = JSON.stringify(eobj);
            if(estr != this.get('cmi.suspend_data')) {
                this.set('cmi.suspend_data', estr);
            }
        }
        this.setSessionTime();
        this.suspendData = eobj;
    }

    /** Get the suspend data from the SCORM data model.
     *
     * @returns {Numbas.storage.exam_suspend_data}
     */
    getSuspendData() {
        try {
            if(!this.suspendData) {
                var suspend_data = this.get('cmi.suspend_data');
                if(suspend_data.length) {
                    this.suspendData = JSON.parse(suspend_data);
                }
            }
            if(!this.suspendData) {
                throw(new Numbas.Error('scorm.no exam suspend data'));
            }
        } catch(e) {
            throw(new Numbas.Error('scorm.error loading suspend data', {message: e.message}));
        }
        return this.suspendData;
    }

    /** Get an externally-set extension to the exam duration.
     *
     * @returns {object}
     */
    getDurationExtension() {
        var duration_extension = this.get('numbas.duration_extension.amount');
        var duration_extension_units = this.get('numbas.duration_extension.units');
        var disable_duration = this.get('numbas.disable_duration') == 'true';
        return {
            disabled: disable_duration,
            amount: duration_extension,
            units: duration_extension_units
        }
    }

    /** Get suspended exam info.
     *
     * @param {Numbas.Exam} exam
     * @returns {Numbas.storage.exam_suspend_data}
     */
    load(exam) {
        this.exam = exam;
        this.listen_messages();
        this.get_student_name();
        var eobj = this.getSuspendData();
        this.set('cmi.exit', 'suspend');
        var currentQuestion = this.get('cmi.location');
        if(currentQuestion.length) {
            currentQuestion = parseInt(currentQuestion, 10);
        } else {
            currentQuestion = undefined;
        }
        var score = parseInt(this.get('cmi.score.raw'), 10);
        return {
            timeRemaining: eobj.timeRemaining || 0,
            timeSpent: eobj.timeSpent || 0,
            duration: eobj.duration || 0,
            questionSubsets: eobj.questionSubsets,
            questionGroupOrder: eobj.questionGroupOrder,
            start: eobj.start,
            stop: eobj.stop,
            randomSeed: eobj.randomSeed,
            score: score,
            currentQuestion: currentQuestion,
            diagnostic: eobj.diagnostic,
            questions: eobj.questions
        };
    }

    /** Get suspended info for a question.
     *
     * @param {Numbas.Question} question
     * @returns {Numbas.storage.question_suspend_data}
     */
    loadQuestion(question) {
        try {
            var eobj = this.getSuspendData();
            var qobj = eobj.questions[question.number];
            if(!qobj) {
                throw(new Numbas.Error('scorm.no question suspend data'));
            }
            var id = this.getQuestionId(question);
            var index = this.questionIndices[id];
            var variables = this.loadVariables(qobj.variables, question.scope);
            return {
                name: qobj.name,
                score: parseInt(this.get('cmi.objectives.' + index + '.score.raw') || 0, 10),
                visited: qobj.visited,
                answered: qobj.answered,
                submitted: qobj.submitted,
                adviceDisplayed: qobj.adviceDisplayed,
                revealed: qobj.revealed,
                variables: variables,
                currentPart: qobj.currentPart,
                parts: qobj.parts
            };
        } catch(e) {
            throw(new Numbas.Error('scorm.error loading question', {'number':question.number, message:e.message}));
        }
    }

    /** Get suspended info for a part.
     *
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadPart(part) {
        try {
            var eobj = this.getSuspendData();
            var pobj = eobj.questions[part.question.number];
            var re = /(p|g|s)(\d+)/g;
            var m;
            while(m = re.exec(part.path)) {
                var i = parseInt(m[2]);
                switch(m[1]) {
                    case 'p':
                        pobj = pobj.parts[i];
                        break;
                    case 'g':
                        pobj = pobj.gaps[i];
                        break;
                    case 's':
                        pobj = pobj.steps[i];
                        break;
                }
            }
            if(!pobj) {
                throw(new Numbas.Error('scorm.no part suspend data'));
            }
            pobj = Numbas.util.copyobj(pobj);
            var prepath = this.partPath(part);
            var sc = this;
            /** Get a SCORM element for this part's interaction.
             *
             * @param {string} key
             * @returns {string}
             */
            function get(key) {
                return sc.get(prepath + key);
            };
            pobj.answer = get('learner_response');
            var typeStorage = this.getPartStorage(part);
            if(typeStorage) {
                var studentAnswer = typeStorage.load(part, pobj);
                if(studentAnswer !== undefined) {
                    pobj.studentAnswer = studentAnswer;
                }
            }
            var scope = part.getScope();
            /**
             * Load cached pre-submit task results.
             *
             * @param {object} cd
             * @returns {Numbas.parts.pre_submit_cache_result}
             */
            function load_pre_submit_cache(cd) {
                var studentAnswer = scope.evaluate(cd.studentAnswer);
                var results = cd.results.map(function(rd) {
                    var o = {};
                    for(const [k, v] of Object.entries(rd)) {
                        o[k] = scope.evaluate(v);
                    }
                    return o;
                });
                return {
                    exec_path: cd.exec_path,
                    studentAnswer: studentAnswer,
                    results: results
                }
            }
            if(Numbas.load_pre_submit_cache !== false) {
                pobj.pre_submit_cache = (pobj.pre_submit_cache || []).map(load_pre_submit_cache);
            } else {
                pobj.pre_submit_cache = []
            }
            pobj.alternatives = (pobj.alternatives || []).map(function(aobj) {
                return {
                    pre_submit_cache: (aobj.pre_submit_cache || []).map(load_pre_submit_cache)
                };
            });
            pobj.stagedAnswer = undefined;
            var stagedAnswerString = get('staged_answer');
            if(stagedAnswerString != '') {
                try {
                    pobj.stagedAnswer = JSON.parse(stagedAnswerString);
                } catch {
                }
            }
            return pobj;
        } catch(e) {
            throw(new Numbas.Error('scorm.error loading part', {part:part.name, message:e.message}));
        }
    }

    /** Record duration of the current session.
     */
    setSessionTime() {
        var timeSpent = this.exam.timeSpent;
        var seconds = Math.floor(timeSpent % 60);
        var minutes = Math.floor(timeSpent / 60) % 60;
        var hours = Math.floor(timeSpent / 60 / 60);

        var sessionTime = 'PT' + hours + 'H' + minutes + 'M' + seconds + 'S';
        this.set('cmi.session_time', sessionTime);
    }

    /** Call this when the exam is started (when {@link Numbas.Exam#begin} runs, not when the page loads). */
    start() {
        this.set('cmi.completion_status', 'incomplete');
    }

    /** Call this when the exam is paused.
     *
     * @see Numbas.Exam#pause
     */
    pause() {
        this.setSuspendData();
    }

    /** Call this when the exam is resumed.
     *
     * @see Numbas.Exam#resume
     */
    resume() {}

    /** Call this when the exam ends.
     *
     * @see Numbas.Exam#end
     */
    end() {
        this.setSessionTime();
        this.setSuspendData();
        this.set('cmi.success_status', this.exam.passed ? 'passed' : 'failed');
        this.set('cmi.completion_status', 'completed');
        pipwerks.SCORM.quit();
    }

    /** Get the student's ID.
     *
     * @returns {string}
     */
    getStudentID() {
        var id = this.get('cmi.learner_id');
        return id || null;
    }

    /** Get entry state: `ab-initio`, or `resume`.
     *
     * @returns {string}
     */
    getEntry() {
        return this.get('cmi.entry');
    }

    /** Get viewing mode:
     *
     * - `browse` - see exam info, not questions;
     * - `normal` - sit exam;
     * - `review` - look at completed exam.
     *
     * @returns {string}
     */
    getMode() {
        return this.get('cmi.mode');
    }

    /** Is review mode allowed?
     *
     * @returns {boolean}
     */
    reviewModeAllowed() {
        var allowed = this.get('numbas.review_allowed');
        return allowed !== 'false';
    }

    /** Call this when the student moves to a different question.
     *
     * @param {Numbas.Question} question
     */
    changeQuestion(question) {
        this.set('cmi.location', question.number);    //set bookmark
        this.setSuspendData();    //because currentQuestion.visited has changed
    }

    /** The 'interactions.N.' prefix for the given part's datamodel elements.
     *
     * @param {Numbas.parts.Part} part
     * @returns {string}
     */
    partPath(part) {
        var id = this.getPartId(part);
        var index = this.partIndices[id];
        if(index !== undefined) {
            return 'cmi.interactions.' + index + '.';
        }
        return undefined;
    }

    /** Call this when a part is answered.
     *
     * @param {Numbas.parts.Part} part
     */
    partAnswered(part) {
        this.storeStagedAnswer(part);
        var prepath = this.partPath(part);
        this.set(prepath + 'result', part.score);
        if(part.answered) {
            var typeStorage = this.getPartStorage(part);
            if(typeStorage) {
                var answer = typeStorage.student_answer(part, this);
                if(answer !== undefined) {
                    this.set(prepath + 'learner_response', answer + '');
                }
            }
        } else {
            this.set(prepath + 'learner_response', '');
        }
        this.setSuspendData();
    }

    /** Save the staged answer for a part.
     * Note: this is not part of the SCORM standard, so can't rely on this being saved.
     *
     * @param {Numbas.parts.Part} part
     */
    storeStagedAnswer(part) {
        var prepath = this.partPath(part);
        if(prepath === undefined) {
            return;
        }
        this.set(prepath + 'staged_answer', JSON.stringify(part.stagedAnswer));
    }

    /** Save exam-level details.
     *
     * @param {Numbas.Exam} exam
     */
    saveExam(exam) {
        if(exam.loading) {
            return;
        }
        //update total exam score and so on
        this.set('cmi.score.raw', exam.score);
        this.set('cmi.score.scaled', (exam.mark > 0 ? exam.score / exam.mark : 0) || 0);
    }

    /** Save details about a question - save score and success status.
     *
     * @param {Numbas.Question} question
     */
    saveQuestion(question) {
        if(question.exam.loading) {
            return;
        }
        var id = this.getQuestionId(question);
        if(!(id in this.questionIndices)) {
            return;
        }
        var index = this.questionIndices[id];
        var prepath = 'cmi.objectives.' + index + '.';
        this.set(prepath + 'score.raw', question.score);
        this.set(prepath + 'score.scaled', (question.marks > 0 ? question.score / question.marks : 0) || 0);
        this.set(prepath + 'success_status', question.score == question.marks ? 'passed' : 'failed');
        this.set(prepath + 'completion_status', question.answered ? 'completed' : 'incomplete');
        this.setSuspendData();
    }

    /** Record that a question has been submitted.
     *
     * @param {Numbas.Question} question
     */
    questionSubmitted(question) {
        this.save();
    }

    /** Record that the student displayed question advice.
     *
     * @param {Numbas.Question} question
     */
    adviceDisplayed(question) {
        this.setSuspendData();
    }

    /** Record that the student revealed the answers to a question.
     *
     * @param {Numbas.Question} question
     */
    answerRevealed(question) {
        this.setSuspendData();
        this.save();
    }

    /** Record that the student showed the steps for a part.
     *
     * @param {Numbas.parts.Part} part
     */
    stepsShown(part) {
        this.setSuspendData();
        this.save();
    }

    /** Record that the student hid the steps for a part.
     *
     * @param {Numbas.parts.Part} part
     */
    stepsHidden(part) {
        this.setSuspendData();
        this.save();
    }
};

scorm.SCORMStorage = SCORMStorage;

});
;
﻿Numbas.queueScript('seedrandom',[],function(module) {
// seedrandom.js version 2.0.
// Author: David Bau 4/2/2011
//
// Defines a method Math.seedrandom() that, when called, substitutes
// an explicitly seeded RC4-based algorithm for Math.random().  Also
// supports automatic seeding from local or network sources of entropy.
//
// Usage:
//
//   <script src=http://davidbau.com/encode/seedrandom-min.js></script>
//
//   Math.seedrandom('yipee'); Sets Math.random to a function that is
//                             initialized using the given explicit seed.
//
//   Math.seedrandom();        Sets Math.random to a function that is
//                             seeded using the current time, dom state,
//                             and other accumulated local entropy.
//                             The generated seed string is returned.
//
//   Math.seedrandom('yowza', true);
//                             Seeds using the given explicit seed mixed
//                             together with accumulated entropy.
//
//   <script src="http://bit.ly/srandom-512"></script>
//                             Seeds using physical random bits downloaded
//                             from random.org.
//
//   <script src="https://jsonlib.appspot.com/urandom?callback=Math.seedrandom">
//   </script>                 Seeds using urandom bits from call.jsonlib.com,
//                             which is faster than random.org.
//
// Examples:
//
//   Math.seedrandom("hello");            // Use "hello" as the seed.
//   document.write(Math.random());       // Always 0.5463663768140734
//   document.write(Math.random());       // Always 0.43973793770592234
//   var rng1 = Math.random;              // Remember the current prng.
//
//   var autoseed = Math.seedrandom();    // New prng with an automatic seed.
//   document.write(Math.random());       // Pretty much unpredictable.
//
//   Math.random = rng1;                  // Continue "hello" prng sequence.
//   document.write(Math.random());       // Always 0.554769432473455
//
//   Math.seedrandom(autoseed);           // Restart at the previous seed.
//   document.write(Math.random());       // Repeat the 'unpredictable' value.
//
// Notes:
//
// Each time seedrandom('arg') is called, entropy from the passed seed
// is accumulated in a pool to help generate future seeds for the
// zero-argument form of Math.seedrandom, so entropy can be injected over
// time by calling seedrandom with explicit data repeatedly.
//
// On speed - This javascript implementation of Math.random() is about
// 3-10x slower than the built-in Math.random() because it is not native
// code, but this is typically fast enough anyway.  Seeding is more expensive,
// especially if you use auto-seeding.  Some details (timings on Chrome 4):
//
// Our Math.random()            - avg less than 0.002 milliseconds per call
// seedrandom('explicit')       - avg less than 0.5 milliseconds per call
// seedrandom('explicit', true) - avg less than 2 milliseconds per call
// seedrandom()                 - avg about 38 milliseconds per call
//
// LICENSE (BSD):
//
// Copyright 2010 David Bau, all rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   1. Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//
//   2. Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//
//   3. Neither the name of this module nor the names of its contributors may
//      be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
/**
 * All code is in an anonymous closure to keep the global namespace clean.
 *
 * @param {number=} overflow
 * @param {number=} startdenom
 */
(function (pool, math, width, chunks, significance, overflow, startdenom) {
//
// seedrandom()
// This is the seedrandom function described above.
//
math['seedrandom'] = function seedrandom(seed, use_entropy) {
  var key = [];
  var arc4;
  // Flatten the seed string or build one from local entropy if needed.
  seed = mixkey(flatten(
    use_entropy ? [seed, pool] :
    arguments.length ? seed :
    [new Date().getTime(), pool, window], 3), key);
  // Use the seed to initialize an ARC4 generator.
  arc4 = new ARC4(key);
  // Mix the randomness into accumulated entropy.
  mixkey(arc4.S, pool);
  // Override Math.random
  // This function returns a random double in [0, 1) that contains
  // randomness in every bit of the mantissa of the IEEE 754 value.
  math['random'] = function random() {  // Closure to return a random double:
    var n = arc4.g(chunks);             // Start with a numerator n < 2 ^ 48
    var d = startdenom;                 //   and denominator d = 2 ^ 48.
    var x = 0;                          //   and no 'extra last byte'.
    while (n < significance) {          // Fill up all significant digits by
      n = (n + x) * width;              //   shifting numerator and
      d *= width;                       //   denominator and generating a
      x = arc4.g(1);                    //   new least-significant-byte.
    }
    while (n >= overflow) {             // To avoid rounding up, before adding
      n /= 2;                           //   last byte, shift everything
      d /= 2;                           //   right using integer math until
      x >>>= 1;                         //   we have exactly the desired bits.
    }
    return (n + x) / d;                 // Form the number within [0, 1).
  };
  // Return the seed that was used
  return seed;
};
//
// ARC4
//
// An ARC4 implementation.  The constructor takes a key in the form of
// an array of at most (width) integers that should be 0 <= x < (width).
//
// The g(count) method returns a pseudorandom integer that concatenates
// the next (count) outputs from ARC4.  Its return value is a number x
// that is in the range 0 <= x < (width ^ count).
//
/** @constructor */
function ARC4(key) {
  var t, u, me = this, keylen = key.length;
  var i = 0, j = me.i = me.j = me.m = 0;
  me.S = [];
  me.c = [];
  // The empty key [] is treated as [0].
  if (!keylen) { key = [keylen++]; }
  // Set up S using the standard key scheduling algorithm.
  while (i < width) { me.S[i] = i++; }
  for (i = 0; i < width; i++) {
    t = me.S[i];
    j = lowbits(j + t + key[i % keylen]);
    u = me.S[j];
    me.S[i] = u;
    me.S[j] = t;
  }
  // The "g" method returns the next (count) outputs as one number.
  me.g = function getnext(count) {
    var s = me.S;
    var i = lowbits(me.i + 1); var t = s[i];
    var j = lowbits(me.j + t); var u = s[j];
    s[i] = u;
    s[j] = t;
    var r = s[lowbits(t + u)];
    while (--count) {
      i = lowbits(i + 1); t = s[i];
      j = lowbits(j + t); u = s[j];
      s[i] = u;
      s[j] = t;
      r = r * width + s[lowbits(t + u)];
    }
    me.i = i;
    me.j = j;
    return r;
  };
  // For robust unpredictability discard an initial batch of values.
  // See http://www.rsa.com/rsalabs/node.asp?id=2009
  me.g(width);
}
//
// flatten()
// Converts an object tree to nested arrays of strings.
//
/** @param {Object=} result
  * @param {string=} prop
  * @param {string=} typ */
function flatten(obj, depth, result, prop, typ) {
  result = [];
  typ = typeof(obj);
  if (depth && typ == 'object') {
    for (prop in obj) {
      if (prop.indexOf('S') < 5) {    // Avoid FF3 bug (local/sessionStorage)
        try { result.push(flatten(obj[prop], depth - 1)); } catch (e) {}
      }
    }
  }
  return (result.length ? result : obj + (typ != 'string' ? '\0' : ''));
}
//
// mixkey()
// Mixes a string seed into a key that is an array of integers, and
// returns a shortened string seed that is equivalent to the result key.
//
/** @param {number=} smear
  * @param {number=} j */
function mixkey(seed, key, smear, j) {
  seed += '';                         // Ensure the seed is a string
  smear = 0;
  for (j = 0; j < seed.length; j++) {
    key[lowbits(j)] =
      lowbits((smear ^= key[lowbits(j)] * 19) + seed.charCodeAt(j));
  }
  seed = '';
  for (j in key) { seed += String.fromCharCode(key[j]); }
  return seed;
}
//
// lowbits()
// A quick "n mod width" for width a power of 2.
//
function lowbits(n) { return n & (width - 1); }
//
// The following constants are related to IEEE 754 limits.
//
startdenom = math.pow(width, chunks);
significance = math.pow(2, significance);
overflow = significance * 2;
//
// When seedrandom.js is loaded, we immediately mix a few bits
// from the built-in RNG into the entropy pool.  Because we do
// not want to intefere with determinstic PRNG state later,
// seedrandom will not call math.random on its own again after
// initialization.
//
mixkey(math.random(), pool);
// End anonymous scope, and pass initial values.
})(
  [],   // pool: entropy pool starts empty
  Math, // math: package containing random, pow, and seedrandom
  256,  // width: each RC4 output is 0 <= x < 256
  6,    // chunks: at least six RC4 outputs for each double
  52    // significance: there are 52 significant digits in a double
);
});;
﻿/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Start the exam */
// 'base' gives the third-party libraries on which Numbas depends
Numbas.queueScript('base', ['localisation', 'seedrandom', 'knockout'], function() {
});
Numbas.queueScript('start-exam', ['base', 'util', 'exam', 'settings', 'exam-to-xml'], function() {
    /** The current exam.
     *
     * @name exam
     * @memberof Numbas
     * @type {Numbas.Exam}
     */

    /**
     * @typedef Numbas.load_exam_options
     * @type {object}
     * @property {string} exam_url - A URL to load the exam definition from.
     * @property {string} exam_source - A string containing the exam definition.
     */

    /**
     * Load an exam definition from the given source or data, load any required extensions, and then initialise the exam.
     *
     * @param {Numbas.load_exam_options} options
     *
     * @returns {{exam_data: {custom_part_types: Array.<{short_name: string}>, extensions: Array.<string>}, exam: Promise.<Numbas.Exam>}}
     */
    Numbas.load_exam = async function(options) {
        let source;

        if(options.exam_url) {
            const res = await fetch(options.exam_url);
            if(!res.ok) {
                Numbas.schedule.halt(new Numbas.Error('exam.error loading exam definition', {text: res.statusText}));
            }
            source = await res.text();

        } else if(options.exam_source) {
            source = options.exam_source;
        } else {
            throw(new Numbas.Error('exam.no exam definition'));
        }

        const encoded_json = source.replace(/^\/\/.*$/m, '');

        const exam_data = JSON.parse(encoded_json);
        exam_data.extensions = exam_data.extensions || [];
        exam_data.custom_part_types = exam_data.custom_part_types || [];

        const custom_part_types = Object.fromEntries(exam_data.custom_part_types.map((cpt) => [cpt.short_name, cpt]));
        Numbas.custom_part_types = Object.assign(Numbas.custom_part_types || {}, custom_part_types);

        const examXML = Numbas.exam_to_xml(exam_data).selectSingleNode('/exam');

        const deps = exam_data.extensions.map((extension) => `extensions/${extension}/${extension}.js`);

        const exam = Numbas.awaitScripts(deps).then(() => {
            const storages = {
                scorm: Numbas.storage.scorm.SCORMStorage
            };

            const storage_constructor = storages[options.storage] || Numbas.storage.Storage;
            const store = new storage_constructor();

            Numbas.init_extensions();

            return Numbas.init_exam(examXML, store, options.element);
        });

        return {exam_data, exam};
    }

    /**
     * Initialise the exam:
     *
     * - Connect to the LMS, which might have saved student answers
     * - Load the exam XML and the XSL templates
     * - create and initialise the exam object
     * - display the frontpage
     *
     * This function is called when all the other scripts have been loaded and executed.
     * It uses the scheduling system to make sure the browser isn't locked up when the exam is being initialised.
     *
     * @memberof Numbas
     * @fires Numbas.signals#exam_ready
     * @fires Numbas.signals#Numbas_initialised
     * @function
     *
     * @param {Element} examXML - The XML definition of the exam.
     * @param {Numbas.storage.Storage} store - Attempt data storage controller.
     * @param {Element} [element] - The root `<numbas-exam>` element for this exam's display.
     *
     * @returns {Promise.<Numbas.Exam>}
     */
    Numbas.init_exam = async function(examXML, store, element) {
        await Numbas.init_promise;

        const scheduler = new Numbas.Scheduler();
        if(element) {
            scheduler.events.on('add job', () => element.showLoadProgress(scheduler));
            scheduler.events.on('finish job', () => element.showLoadProgress(scheduler));
        }
        var job = scheduler.job.bind(scheduler);

        return new Promise((resolve) => {
            job(function() {
                var external_seed = store.get_initial_seed();
                var seed = external_seed || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
                Math.seedrandom(seed);
                var exam = Numbas.createExamFromXML(examXML, store, element, scheduler);
                exam.seed = seed;

                var entry = store.getEntry();
                if(store.getMode() == 'review') {
                    entry = 'review';
                }
                exam.entry = entry;

                exam.signals.on('exam ready').catch((error) => {
                    Numbas.display && Numbas.display.die(error);
                });

                switch(entry) {
                    case '':
                    case 'ab-initio':
                        job(() => exam.init());
                        exam.signals.on('ready', function() {
                            Numbas.signals.trigger('exam ready');
                            element && job(() => element.init(exam));
                            job(function() {
                                if(exam.settings.showFrontPage && exam.display) {
                                    exam.display.showInfoPage('frontpage');
                                } else {
                                    exam.begin();
                                }
                            });
                        })
                        break;
                    case 'resume':
                    case 'review':
                        job(() => exam.load());
                        exam.signals.on('ready', function() {
                            Numbas.signals.trigger('exam ready');
                            element && job(() => element.init(exam));
                            job(function() {
                                if(entry == 'review') {
                                    job(() => exam.end(false));
                                } else if(exam.currentQuestion !== undefined) {
                                    job(() => exam.display && exam.display.showInfoPage('resumed'));
                                } else {
                                    job(() => exam.display && exam.display.showInfoPage('frontpage'));
                                }
                            });
                        });
                        break;
                }
                resolve(exam);

                Numbas.signals.trigger('Numbas initialised');
            });
        });
    }

    Numbas.init_extensions = function() {
        for(var name in Numbas.custom_part_types) {
            Numbas.partConstructors[name] = Numbas.parts.CustomPart;
        };

        for(var x in Numbas.extensions) {
            Numbas.activateExtension(x);
        }
    }

    const numbas_init = Promise.withResolvers();
    Numbas.init_promise = numbas_init.promise;

    Numbas.util.document_ready(function() {
        Numbas.locale.init();

        var job = Numbas.schedule.add;
        job(Numbas.xml.loadXMLDocs);
        job(Numbas.diagnostic.load_scripts);
        Numbas.display && job(Numbas.display.init, Numbas.display);
        job(() => numbas_init.resolve());
    });
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
Numbas.queueScript('storage', ['base'], function() {
/** @namespace Numbas.storage */
/** @typedef exam_suspend_data
 * @memberof Numbas.storage
 * @property {number} timeRemaining - Seconds until the end of the exam ({@link Numbas.Exam#timeRemaining})
 * @property {number} duration - Length of the exam, in seconds ({@link Numbas.Exam#settings})
 * @property {Array.<Array.<number>>} questionSubsets - The sets of questions in each question group ({@link Numbas.Exam#question_groups})
 * @property {Date} start - The time the exam was started ({@link Numbas.Exam#start})
 * @property {number} score - The student's current score ({@link Numbas.exam#score})
 * @property {number} currentQuestion - The index of the current question ({@link Numbas.Exam#currentQuestionNumber})
 */
/** @typedef question_suspend_data
 * @memberof Numbas.storage
 * @property {string} name - The name of the question ({@link Numbas.Question#name})
 * @property {number} score - The student's score for this question ({@link Numbas.Question#score})
 * @property {boolean} visited - Has the student visited this question yet? ({@link Numbas.Question#visited})
 * @property {boolean} answered - Has the student answered this question? ({@link Numbas.Question#answered})
 * @property {boolean} adviceDisplayed - Has the advice been displayed? ({@link Numbas.Question#adviceDisplayed})
 * @property {boolean} revealed - Have the correct answers been revealed? ({@link Numbas.Question#revealed})
 * @property {Object<JME>} variables - A dictionary of the values of the question variables. ({@link Numbas.Question#scope})
 * @see Numbas.storage.SCORMStorage#loadQuestion
 */
/** @typedef part_suspend_data
 * @memberof Numbas.storage
 * @property {string} answer - student's answer to the part, as encoded for saving
 * @property {boolean} answered - has the student answered this part? ({@link Numbas.parts.Part#answered})
 * @property {boolean} stepsShown - have the steps been shown? ({@link Numbas.parts.Part#stepsShown})
 * @property {boolean} stepsOpen - are the steps currently visible? ({@link Numbas.parts.Part#stepsOpen})
 * @property {Array.<Numbas.storage.part_suspend_data>} gaps - data for gaps, if this is a gapfill part
 * @property {Array.<Numbas.storage.part_suspend_data>} steps - data for steps, if this part has steps
 * @property {string} studentAnswer - student's answer, for {@link Numbas.parts.JMEPart}, {@link Numbas.parts.NumberEntryPart} or {@link Numbas.parts.PatternMatchPart} parts
 * @property {Array.<number>} shuffleChoices - order of choices, if this is a {@link Numbas.parts.MultipleResponsePart}
 * @property {Array.<number>} shuffleAnswers - order of answers, if this is a {@link Numbas.parts.MultipleResponsePart}
 * @property {Array.<Array.<number>>} ticks - student's choices, for {@link Numbas.parts.MultipleResponsePart} parts
 */

var storage = Numbas.storage = {
    stores: []
};

/** A blank storage object which does nothing.
 *
 * Any real storage object needs to implement all of this object's methods.
 *
 * @memberof Numbas.storage
 * @augments Numbas.storage.Storage
 * @class
 */
class Storage {
    /** Initialise the SCORM data model and this storage object.
     *
     * @param {Numbas.Exam} exam
     */
    init(exam) {
        this.exam = exam;
    }

    init_questions() {
    }

    /** Initialise a question.
     *
     * @param {Numbas.Question} q
     * @abstract
     */
    initQuestion(q) {}

    /**
     * Initialise a part.
     *
     * @param {Numbas.parts.Part} p
     * @abstract
     */
    initPart(p) {}

    /** Get an externally-set extension to the exam duration.
     *
     * @abstract
     * @returns {object}
     */
    getDurationExtension() {
    }

    /** Get suspended exam info.
     *
     * @abstract
     * @param {Numbas.Exam} exam
     * @returns {Numbas.storage.exam_suspend_data}
     */
    load(exam) {}

    /** Save SCORM data - call the SCORM commit method to make sure the data model is saved to the server/backing store.
     *
     * @abstract
     */
    save() {
    }

    /** Load student's name and ID.
     *
     * @abstract
     */
    get_student_name() {}

    /** Get the initial seed value.
     *
     * @abstract
     * @returns {string}
     */
    get_initial_seed() {}

    /**
     * Get suspended info for a question.
     *
     * @abstract
     * @param {Numbas.Question} question
     * @returns {Numbas.storage.question_suspend_data}
     */
    loadQuestion(question) {}

    /** Get suspended info for a part.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadPart(part) {}

    /** Load a {@link Numbas.parts.JMEPart}.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadJMEPart(part) {}

    /** Load a {@link Numbas.parts.PatternMatchPart}.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadPatternMatchPart(part) {}

    /** Load a {@link Numbas.parts.NumberEntryPart}.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadNumberEntryPart(part) {}

    /** Load a {@link Numbas.parts.MatrixEntryPart}.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadMatrixEntryPart(part) {}

    /** Load a {@link Numbas.parts.MultipleResponsePart}.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadMultipleResponsePart(part) {}

    /** Load a {@link Numbas.parts.ExtensionPart}.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     */
    loadExtensionPart(part) {}

    /** Load a dictionary of JME variables.
     *
     * @param {Object<JME>} vobj
     * @param {Numbas.jme.Scope} scope
     * @returns {Object<Numbas.jme.token>}
     */
    loadVariables(vobj, scope) {
        var variables = {};
        for(const [snames, v_def] of Object.entries(vobj)) {
            const v = scope.evaluate(v_def);
            var names = snames.split(',');
            if(names.length > 1) {
                names.forEach(function(name, i) {
                    variables[name] = scope.evaluate('$multi[' + i + ']', {'$multi':v});
                });
            } else {
                variables[snames] = v;
            }
        }
        return variables;
    }


    /** Call this when the exam is started (when {@link Numbas.Exam#begin} runs, not when the page loads).
     *
     * @abstract
     */
    start() {}

    /** Call this when the exam is paused ({@link Numbas.Exam#pause}).
     *
     * @abstract
     */
    pause() {}

    /** Call this when the exam is resumed ({@link Numbas.Exam#resume}).
     *
     * @abstract
     */
    resume() {}

    /** Call this when the exam ends ({@link Numbas.Exam#end}).
     *
     * @abstract
     */
    end() {}

    /** Get the student's ID.
     *
     * @abstract
     * @returns {string}
     */
    getStudentID() {
        return '';
    }

    /** Get entry state: `ab-initio`, or `resume`.
     *
     * @abstract
     * @returns {string}
     */
    getEntry() {
        return 'ab-initio';
    }

    /** Get viewing mode:
     *
     * - `browse` - see exam info, not questions;
     * - `normal` - sit exam;
     * - `review` - look at completed exam.
     *
     * @abstract
     * @returns {string}
     */
    getMode() {}

    /** Is review mode allowed?
     *
     * @returns {boolean}
     */
    reviewModeAllowed() {}

    /** Call this when the student moves to a different question.
     *
     * @abstract
     * @param {Numbas.Question} question
     */
    changeQuestion(question) {}

    /** Call this when a part is answered.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     */
    partAnswered(part) {}

    /** Save the staged answer for a part.
     * Note: this is not part of the SCORM standard, so can't rely on this being saved.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     */
    storeStagedAnswer(part) {}

    /** Save exam-level details.
     *
     * @abstract
     * @param {Numbas.Exam} exam
     */
    saveExam(exam) {}

    /* Save details about a question - save score and success status.
     *
     * @abstract
     * @param {Numbas.Question} question
     */
    saveQuestion(question) {}

    /** Record that a question has been submitted.
     *
     * @abstract
     * @param {Numbas.Question} question
     */
    questionSubmitted(question) {}

    /** Rcord that the student displayed question advice.
     *
     * @abstract
     * @param {Numbas.Question} question
     */
    adviceDisplayed(question) {}

    /** Record that the student revealed the answers to a question.
     *
     * @abstract
     * @param {Numbas.Question} question
     */
    answerRevealed(question) {}

    /** Record that the student showed the steps for a part.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     */
    stepsShown(part) {}

    /** Record that the student hid the steps for a part.
     *
     * @abstract
     * @param {Numbas.parts.Part} part
     */
    stepsHidden(part) {}

    /** Suspend data for the exam - all the other stuff that doesn't fit into the standard SCORM data model.
     *
     * @returns {object}
     */
    examSuspendData() {
        var exam = this.exam;
        if(exam.loading) {
            return undefined;
        }
        var eobj = {
            timeRemaining: exam.timeRemaining || 0,
            timeSpent: exam.timeSpent || 0,
            duration: exam.settings.duration || 0,
            questionSubsets: exam.question_groups.map(function(g) {
                return g.questionSubset
            }),
            questionGroupOrder: exam.questionGroupOrder,
            start: exam.start - 0,
            stop: exam.stop ? exam.stop - 0 : null,
            randomSeed: exam.seed,
            student_name: exam.student_name,
            score:  exam.score,
            max_score:  exam.mark,
        };
        if(exam.settings.navigateMode == 'diagnostic') {
            eobj.diagnostic = this.diagnosticSuspendData();
        }
        eobj.questions = [];
        for(let i = 0;i < exam.questionList.length;i++) {
            eobj.questions.push(this.questionSuspendData(exam.questionList[i]));
        }

        return eobj;
    }

    /** Create suspend data to do with diagnostic mode.
     *
     * @returns {object}
     */
    diagnosticSuspendData() {
        var exam = this.exam;
        var dobj = {};
        dobj.state = Numbas.jme.display.treeToJME({tok:exam.diagnostic_controller.state});
        return dobj;
    }

    /** Create suspend data object for a dictionary of JME variables.
     *
     * @param {Object<Numbas.jme.token>} variables
     * @param {Numbas.jme.Scope} scope
     * @returns {Object<JME>}
     * @see Numbas.storage.SCORMStorage#setSuspendData
     */
    variablesSuspendData(variables, scope) {
        var vobj = {};
        for(const [name, v] of Object.entries(variables)) {
            vobj[name] = Numbas.jme.display.treeToJME({tok: v}, {nicenumber:false, wrapexpressions: true, store_precision: true}, scope);
        }
        return vobj;
    }

    /** Create suspend data object for a question.
     *
     * @param {Numbas.Question} question
     * @returns {Numbas.storage.question_suspend_data}
     * @see Numbas.storage.SCORMStorage#setSuspendData
     */
    questionSuspendData(question) {
        var qobj = {
            name: question.name,
            number_in_group: question.number_in_group,
            group: question.group.number,
            visited: question.visited,
            answered: question.answered,
            submitted: question.submitted,
            adviceDisplayed: question.adviceDisplayed,
            revealed: question.revealed,
            score: question.score,
            max_score: question.marks
        };

        var scope = question.getScope();

        if(question.partsMode == 'explore') {
            qobj.currentPart = question.currentPart.path;
        }

        var variables = {};
        question.local_definitions.variables.forEach(function(names) {
            names = Numbas.jme.normaliseName(names, scope);
            if(!question.variablesTodo[names] || Numbas.jme.isDeterministic(question.variablesTodo[names].tree, scope)) {
                return;
            }
            names.split(',').forEach(function(name) {
                name = name.trim();
                var value = question.scope.getVariable(name);
                variables[name] = value;
            });
        });
        qobj.variables = this.variablesSuspendData(variables, scope);

        qobj.parts = [];
        for(let i = 0;i < question.parts.length;i++) {
            qobj.parts.push(this.partSuspendData(question.parts[i]));
        }

        return qobj;
    }

    /** Create suspend data object for a part.
     *
     * @param {Numbas.parts.Part} part
     * @returns {Numbas.storage.part_suspend_data}
     * @see Numbas.storage.SCORMStorage#setSuspendData
     */
    partSuspendData(part) {
        var name_bits = [part.name];
        var par = part.parentPart;
        while(par) {
            name_bits.splice(0, 0, par.name);
            par = par.parentPart;
        }
        name_bits.splice(0, 0, part.question.name);
        var name = name_bits.join(' ');

        var scope = part.getScope();
        /**
         * Produce the suspend data for cached pre-submit task results.
         *
         * @param {Numbas.parts.pre_submit_cache_result} c
         * @returns {object}
         */
        function pre_submit_cache_suspendData(c) {
            var obj = {
                exec_path: c.exec_path,
                studentAnswer: Numbas.jme.display.treeToJME({tok: c.studentAnswer}, scope),
                results: c.results.map(function(r) {
                    var o = {};
                    for(const [k, v] of Object.entries(r)) {
                        o[k] = Numbas.jme.display.treeToJME({tok: v}, scope);
                    }
                    return o;
                })
            };
            return obj;
        }

        var pobj = {
            answered: part.answered,
            stepsShown: part.stepsShown,
            stepsOpen: part.stepsOpen,
            name: name,
            index: part.index,
            previousPart: part.previousPart ? part.previousPart.path : null,
            pre_submit_cache: part.pre_submit_cache.map(pre_submit_cache_suspendData),
            alternatives: part.alternatives.map(function(alt) {
                return {
                    pre_submit_cache: alt.pre_submit_cache.map(pre_submit_cache_suspendData)
                };
            }),
            score: part.score,
            max_score: part.marks,
        };
        var typeStorage = this.getPartStorage(part);
        if(typeStorage) {
            var data = typeStorage.suspend_data(part, this);
            if(data) {
                pobj = Numbas.util.extend_object(pobj, data);
            }
            pobj.student_answer = typeStorage.student_answer(part);
            pobj.correct_answer = typeStorage.correct_answer(part);
        }
        pobj.steps = [];
        for(let i = 0;i < part.steps.length;i++) {
            pobj.steps.push(this.partSuspendData(part.steps[i]));
        }
        pobj.nextParts = [];
        for(let i = 0;i < part.nextParts.length;i++) {
            var np = part.nextParts[i];
            pobj.nextParts.push({
                instance: np.instance ? np.instance.path : null,
                variableReplacements: np.instanceVariables ? this.variablesSuspendData(np.instanceVariables, part.getScope()) : null,
                index: np.instance ? np.instance.index : null
            });
        }
        return pobj;
    }

    /** Get the relevant part storage methods for the given part.
     *
     * @param {Numbas.parts.Part} p
     * @returns {Numbas.storage.partTypeStorage}
     */
    getPartStorage(p) {
        if(p.is_custom_part_type) {
            return storage.partTypeStorage['custom'];
        } else {
            return storage.partTypeStorage[p.type];
        }
    }
};

/** @typedef {object} Numbas.storage.partTypeStorage
 * @property {Function} interaction_type - `(part)`
 * @property {Function} correct_answer - `(part)`
 * @property {Function} student_answer - `(part)`
 * @property {Function} suspend_data - `(part)`
 * @property {Function} load - `(part,data)`
 */

storage.partTypeStorage = {
    'information': {
        interaction_type: function() {
            return 'other';
        },
        correct_answer: function() {},
        student_answer: function() {},
        suspend_data: function() {},
        load: function() {}
    },
    'extension': {
        interaction_type: function() {
            return 'other';
        },
        correct_answer: function() {},
        student_answer: function() {},
        suspend_data: function(part) {
            return {extension_data: part.createSuspendData()};
        },
        load: function() {}
    },
    '1_n_2': {
        interaction_type: function() {
            return 'choice';
        },
        correct_answer: function(part) {
            for(let i = 0;i < part.numAnswers;i++) {
                if(part.settings.maxMatrix[i][0]) {
                    return i + '';
                }
            }
        },
        student_answer: function(part) {
            for(let i = 0;i < part.numAnswers;i++) {
                if(part.ticks[i][0]) {
                    return i + '';
                }
            }
        },
        suspend_data: function(part) {
            return {shuffleAnswers: Numbas.math.inverse(part.shuffleAnswers)};
        },
        load: function(part, data) {
            var ticks = [];
            var tick = parseInt(data.answer, 10);
            for(let i = 0;i < part.numAnswers;i++) {
                ticks.push([i == tick]);
            }
            return ticks;
        }
    },
    'm_n_2': {
        interaction_type: function(part) {
            return 'choice';
        },
        correct_answer: function(part) {
            var good_choices = [];
            for(let i = 0;i < part.numAnswers;i++) {
                if(part.settings.maxMatrix[i][0]) {
                    good_choices.push(i);
                }
            }
            return good_choices.join('[,]');
        },
        student_answer: function(part) {
            var choices = [];
            for(let i = 0;i < part.numAnswers;i++) {
                if(part.ticks[i][0]) {
                    choices.push(i);
                }
            }
            return choices.join('[,]');
        },
        suspend_data: function(part) {
            return {shuffleAnswers: Numbas.math.inverse(part.shuffleAnswers)};
        },
        load: function(part, data) {
            var ticks = [];
            for(let i = 0;i < part.numAnswers;i++) {
                ticks.push([false]);
            }
            data.answer.split('[,]').forEach(function(tickstr) {
                var tick = parseInt(tickstr, 10);
                if(!isNaN(tick)) {
                    ticks[tick][0] = true;
                }
            });
            return ticks;
        }
    },
    'm_n_x': {
        interaction_type: function(part) {
            return 'matching';
        },
        correct_answer: function(part) {
            var good_choices = [];
            for(let i = 0;i < part.settings.maxMatrix.length;i++) {
                for(let j = 0;j < part.settings.maxMatrix[i].length;j++) {
                    if(part.settings.maxMatrix[i][j]) {
                        good_choices.push(i + '[.]' + j);
                    }
                }
            }
            return good_choices.join('[,]');
        },
        student_answer: function(part) {
            var choices = [];
            for(let i = 0;i < part.numAnswers;i++) {
                for(var j = 0;j < part.numChoices;j++) {
                    if(part.ticks[i][j]) {
                        choices.push(i + '[.]' + j);
                    }
                }
            }
            return choices.join('[,]');
        },
        suspend_data: function(part) {
            return {
                shuffleAnswers: Numbas.math.inverse(part.shuffleAnswers),
                shuffleChoices: Numbas.math.inverse(part.shuffleChoices)
            };
        },
        load: function(part, data) {
            var ticks = [];
            for(let i = 0;i < part.numAnswers;i++) {
                var row = [];
                ticks.push(row);
                for(let j = 0;j < part.numChoices;j++) {
                    row.push(false);
                }
            }
            var tick_re = /(\d+)\[\.\](\d+)/;
            var bits = data.answer.split('[,]');
            for(let i = 0;i < bits.length;i++) {
                var m = bits[i].match(tick_re);
                if(m) {
                    var x = parseInt(m[1], 10);
                    var y = parseInt(m[2], 10);
                    ticks[x][y] = true;
                }
            }
            return ticks;
        }
    },
    'numberentry': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return Numbas.math.niceRealNumber(part.settings.minvalue) + '[:]' + Numbas.math.niceRealNumber(part.settings.maxvalue);
        },
        student_answer: function(part) {
            return part.studentAnswer;
        },
        suspend_data: function() {},
        load: function(part, data) {
            return data.answer || '';
        }
    },
    'matrix': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return '{case_matters=false}' + JSON.stringify(part.settings.correctAnswer);
        },
        student_answer: function(part) {
            return JSON.stringify({
                rows: part.studentAnswerRows,
                columns: part.studentAnswerColumns,
                matrix: part.studentAnswer
            });
        },
        suspend_data: function() {},
        load: function(part, data) {
            if(data.answer) {
                return JSON.parse(data.answer);
            }
        }
    },
    'patternmatch': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return '{case_matters=' + part.settings.caseSensitive + '}' + part.settings.correctAnswer;
        },
        student_answer: function(part) {
            return part.studentAnswer;
        },
        suspend_data: function() {},
        load: function(part, data) {
            return data.answer || '';
        }
    },
    'jme': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return '{case_matters=false}' + part.settings.correctAnswer;
        },
        student_answer: function(part) {
            return part.studentAnswer;
        },
        suspend_data: function() {},
        load: function(part, data) {
            return data.answer || '';
        }
    },
    'gapfill': {
        interaction_type: function(part) {
            return 'other';
        },
        correct_answer: function(part) {},
        student_answer: function(part) {},
        suspend_data: function(part, store) {
            var gapSuspendData = part.gaps.map(function(gap) {
                return store.partSuspendData(gap);
            });
            return {gaps: gapSuspendData};
        },
        load: function(part) {}
    },
    'custom': {
        interaction_type: function(part) {
            var widget = part.input_widget();
            var widget_storage = storage.inputWidgetStorage[widget];
            if(widget_storage) {
                return widget_storage.interaction_type(part);
            } else {
                return 'other';
            }
        },
        correct_answer: function(part) {
            var widget = part.input_widget();
            var widget_storage = storage.inputWidgetStorage[widget];
            if(widget_storage) {
                return widget_storage.correct_answer(part);
            }
        },
        student_answer: function(part) {
            var widget = part.input_widget();
            var widget_storage = storage.inputWidgetStorage[widget];
            if(widget_storage) {
                return widget_storage.student_answer(part);
            }
        },
        suspend_data: function() {},
        load: function(part, data) {
            var widget = part.input_widget();
            var widget_storage = storage.inputWidgetStorage[widget];
            if(widget_storage) {
                return widget_storage.load(part, data);
            }
      }
    }
};

/** @typedef inputWidgetStorage
 * @memberof Numbas.storage
 * @property {Function} interaction_type - Return the SCORM interaction type identifier for the given part.
 * @property {Function} correct_answer - Return a JSON-serialisable object representing the correct answer for the given part.
 * @property {Function} student_answer - Return a JSON-serialisable object representing the student's answer to the given part.
 * @property {Function} load - Given arguments `part` and `data`, load the student's answer to the given part from the suspend data.
 */

/** @type {Object<inputWidgetStorage>}
 * @memberof Numbas.storage
 */
storage.inputWidgetStorage = {
    'string': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return part.input_options().correctAnswer;
        },
        student_answer: function(part) {
            return part.studentAnswer;
        },
        load: function(part, data) {
            return data.answer;
        }
    },
    'number': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return Numbas.math.niceRealNumber(part.input_options().correctAnswer);
        },
        student_answer: function(part) {
            return part.studentAnswer !== undefined ? Numbas.math.niceRealNumber(part.studentAnswer) : '';
        },
        load: function(part, data) {
            return Numbas.util.parseNumber(data.answer, part.input_options().allowFractions, part.input_options().allowedNotationStyles);
        }
    },
    'jme': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return Numbas.jme.display.treeToJME(part.input_options().correctAnswer, {}, part.getScope());
        },
        student_answer: function(part) {
            return Numbas.jme.display.treeToJME(part.studentAnswer, {}, part.getScope());
        },
        load: function(part, data) {
            return Numbas.jme.compile(data.answer);
        }
    },
    'matrix': {
        interaction_type: function(part) {
            return 'fill-in';
        },
        correct_answer: function(part) {
            return JSON.stringify(part.input_options().correctAnswer);
        },
        student_answer: function(part) {
            return JSON.stringify(part.studentAnswer);
        },
        load: function(part, data) {
            try {
                var m = JSON.parse(data.answer);
                m.rows = m.length;
                m.columns = m.length > 0 ? m[0].length : 0;
                return m;
            } catch {
                return undefined;
            }
        }
    },
    'radios': {
        interaction_type: function(part) {
            return 'choice';
        },
        correct_answer: function(part) {
            return part.input_options().correctAnswer + '';
        },
        student_answer: function(part) {
            return part.studentAnswer + '';
        },
        load: function(part, data) {
            return parseInt(data.answer, 10);
        }
    },
    'checkboxes': {
        interaction_type: function(part) {
            return 'choice';
        },
        correct_answer: function(part) {
            var good_choices = [];
            part.input_options().correctAnswer.forEach(function(c, i) {
                if(c) {
                    good_choices.push(i);
                }
            });
            return good_choices.join('[,]');
        },
        student_answer: function(part) {
            var ticked = [];
            if(part.studentAnswer) {
                part.studentAnswer.forEach(function(c, i) {
                    if(c) {
                        ticked.push(i);
                    }
                });
            }
            return ticked.join('[,]');
        },
        load: function(part, data) {
            var ticked = part.input_options().choices.map(function(c) {
                return false;
            });
            data.answer.split('[,]').forEach(function(c) {
                var i = parseInt(c, 10); ticked[i] = true;
            });
            return ticked;
        }
    },
    'dropdown': {
        interaction_type: function(part) {
            return 'choice';
        },
        correct_answer: function(part) {
            return part.input_options().correctAnswer + '';
        },
        student_answer: function(part) {
            return part.studentAnswer + '';
        },
        load: function(part, data) {
            return parseInt(data.answer, 10);
        }
    }
}

Numbas.storage.Storage = Storage;

});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file A few functions to do with time and date, and also performance timing. Provides {@link Numbas.timing}. */

/** A duration of time.
 * @typedef {object} duration
 * @property {number} seconds - Number of seconds.
 * @property {number} minutes - Number of minutes.
 * @property {number} hours - Number of hours.
 * @property {number} days - Number of days.
 */

Numbas.queueScript('timing', ['base'], function() {
/** @namespace Numbas.timing */
var timing = Numbas.timing = /** @lends Numbas.timing */ {
    /** Get the current date as a string in the user's locale.
     *
     * @returns {string}
     */
    displayDate: function() {
        return (new Date()).toLocaleDateString();
    },

    /** Convert a number of seconds to an object with seconds, minutes, hours and days.
     *
     * @param {number} time
     * @returns {duration}
     */
    secsToUnits: function(time) {
        time = Math.floor(time);
        const seconds = time % 60;
        time = (time - seconds) / 60;
        const minutes = time % 60;
        time = (time - minutes) / 60;
        const hours = time % 24;
        time = (time - hours) / 24;
        const days = time;

        return {seconds, minutes, hours, days};
    },

    /** Convert a number of seconds to a string in `HH:MM:SS` format.
     *
     * @param {number} time
     * @returns {string}
     */
    secsToDisplayTime: function(time) {
        if(time < 0) {
            return '-' + Numbas.timing.secsToDisplayTime(-time);
        }

        const {seconds, minutes, hours} = timing.secsToUnits(time);

        /** Interpolate numbers into a string template, padding each number to two characters.
         *
         * @param {Array.<string>} text
         * @param {Array.<number>} numbers
         * @returns {string}
         */
        function padded(text, ...numbers) {
            let out = text[0];
            for(let i = 0;i < text.length - 1;i++) {
                out += numbers[i].toString().padStart(2, '0') + text[i + 1];
            }
            return out;
        }

        return padded`${hours}:${minutes}:${seconds}`;
    },

    /** Convert a number of seconds to an ISO8601 duration string in the format `PdDThHmMsS`
     *
     * @param {number} time
     * @returns {string}
     */
    secsToMachineDuration: function(time) {
        const {seconds, minutes, hours, days} = timing.secsToUnits(time);

        return `P${days}DT${hours}H${minutes}M${seconds}S`;
    },

    /** A queue of timers.
     *
     * @type {Date[]}
     */
    timers: [],
    /** Timing messages - how long did each timer take?
     *
     * @type {Array.<string>}
     */
    messages: [],

    /** Start a new timer.
     *
     * @see {Numbas.timing.timers}
     */
    start: function() {
        timing.timers.push(new Date());
    },
    /** End the top timer on the queue.
     *
     * @param {string} label - A description of the timer.
     */
    end: function(label) {
        var s = '';
        for(let i = 0;i < timing.timers.length;i++) {
            s += '   ';
        }
        s += (new Date()) - timing.timers.pop();
        s += ' ' + label;
        timing.messages.push(s);
        if(!timing.timers.length) {
            timing.show();
        }
    },
    /** Show all timing messages through {@link Numbas.debug}.
     */
    show: function() {
        for(const [k, v] of Object.entries(timing.accs)) {
            Numbas.debug(v.total + ' ' + k, true);
        }
        timing.accs = {};
        for(let i = 0;i < timing.messages.length;i++) {
            Numbas.debug(timing.messages[i], true);
        }
        timing.messages = [];
    },
    /** Stress test a function by running it a lot of times and seeing how long it takes.
     *
     * @param {Function} f
     * @param {number} times
     */
    stress: function(f, times) {
        timing.start();
        for(let i = 0;i < times;i++) {
            f();
        }
        timing.end();
    },
    /** Timing accumulators.
     *
     * @see Numbas.timing.startacc
     */
    accs: {},
    /** Accumulators are for counting time spent in functions which don't take long to evaluate, but are called repeatedly.
     *
     * Call this with the function's name when you start the function, and {@link Numbas.timing.endacc} with the same name just before returning a value.
     *
     * It copes with recursion automatically, so you don't need to worry about double counting.
     *
     * @param {string} name
     */
    startacc: function(name) {
        if(timing.accs[name] == undefined) {
            timing.accs[name] = {
                total: 0,
                go: 0
            }
        }
        var acc = timing.accs[name];
        acc.go += 1;
        if(acc.go > 1) {
            return;
        }
        acc.start = new Date();
    },
    /** Stop accumulating runtime for a function.
     *
     * @param {string} name
     * @see Numbas.timing.startacc
     */
    endacc: function(name) {
        var acc = timing.accs[name];
        if(!acc) {
            throw(new Numbas.Error('timing.no accumulator', {name:name}));
        }
        acc.go -= 1;
        if(acc.go == 0) {
            var end = new Date();
            acc.total += (end - acc.start);
        }
    }
};
});
;

// Created using https://github.com/numbas/unicode-math-normalization
Numbas.queueScript('unicode-mappings', [], function() {
    Numbas.unicode_mappings = {"greek": {"\u0391": "Alpha", "\u0392": "Beta", "\u03a7": "Chi", "\u0394": "Delta", "\u0395": "Epsilon", "\u0397": "Eta", "\u0393": "Gamma", "\u0370": "Heta", "\u0399": "Iota", "\u039a": "Kappa", "\u039b": "Lambda", "\u039c": "Mu", "\u039d": "Nu", "\u03a9": "Omega", "\u039f": "Omicron", "\u03a6": "Phi", "\u03a0": "Pi", "\u03a8": "Psi", "\u03a1": "Rho", "\u03fa": "San", "\u03f7": "Sho", "\u03a3": "Sigma", "\u03a4": "Tau", "\u0398": "Theta", "\u03a5": "Upsilon", "\u039e": "Xi", "\u037f": "Yot", "\u0396": "Zeta", "\u1d26": "Gamma", "\u1d27": "Lambda", "\uab65": "Omega", "\u1d28": "Pi", "\u1d2a": "Psi", "\u1d29": "Rho", "\u03b1": "alpha", "\u03b2": "beta", "\u03c7": "chi", "\u03b4": "delta", "\u03dd": "digamma", "\u03b5": "epsilon", "\u03b7": "eta", "\u03c2": "sigma", "\u03b3": "gamma", "\u0371": "heta", "\u03b9": "iota", "\u03ba": "kappa", "\u03df": "koppa", "\u03bb": "lambda", "\u03bc": "mu", "\u03bd": "nu", "\u03c9": "omega", "\u03bf": "omicron", "\u03c6": "phi", "\u03c0": "pi", "\u03c8": "psi", "\u03c1": "rho", "\u03e1": "sampi", "\u03fb": "san", "\u03f8": "sho", "\u03c3": "sigma", "\u03db": "stigma", "\u03c4": "tau", "\u03b8": "theta", "\u03c5": "upsilon", "\u03be": "xi", "\u03b6": "zeta"}, "subscripts": {"\u1d62": "i", "\u1d63": "r", "\u1d64": "u", "\u1d65": "v", "\u1d66": "\u03b2", "\u1d67": "\u03b3", "\u1d68": "\u03c1", "\u1d69": "\u03c6", "\u1d6a": "\u03c7", "\u2080": "0", "\u2081": "1", "\u2082": "2", "\u2083": "3", "\u2084": "4", "\u2085": "5", "\u2086": "6", "\u2087": "7", "\u2088": "8", "\u2089": "9", "\u208a": "+", "\u208b": "-", "\u208c": "=", "\u208d": "(", "\u208e": ")", "\u2090": "a", "\u2091": "e", "\u2092": "o", "\u2093": "x", "\u2095": "h", "\u2096": "k", "\u2097": "l", "\u2098": "m", "\u2099": "n", "\u209a": "p", "\u209b": "s", "\u209c": "t", "\u2c7c": "j"}, "superscripts": {"\u00b2": "2", "\u00b3": "3", "\u00b9": "1", "\u2070": "0", "\u2071": "i", "\u2074": "4", "\u2075": "5", "\u2076": "6", "\u2077": "7", "\u2078": "8", "\u2079": "9", "\u207a": "+", "\u207b": "-", "\u207c": "=", "\u207d": "(", "\u207e": ")", "\u207f": "n"}, "letters": {"\ud835\udc00": ["A", ["BOLD"]], "\ud835\udc01": ["B", ["BOLD"]], "\ud835\udc02": ["C", ["BOLD"]], "\ud835\udc03": ["D", ["BOLD"]], "\ud835\udc04": ["E", ["BOLD"]], "\ud835\udc05": ["F", ["BOLD"]], "\ud835\udc06": ["G", ["BOLD"]], "\ud835\udc07": ["H", ["BOLD"]], "\ud835\udc08": ["I", ["BOLD"]], "\ud835\udc09": ["J", ["BOLD"]], "\ud835\udc0a": ["K", ["BOLD"]], "\ud835\udc0b": ["L", ["BOLD"]], "\ud835\udc0c": ["M", ["BOLD"]], "\ud835\udc0d": ["N", ["BOLD"]], "\ud835\udc0e": ["O", ["BOLD"]], "\ud835\udc0f": ["P", ["BOLD"]], "\ud835\udc10": ["Q", ["BOLD"]], "\ud835\udc11": ["R", ["BOLD"]], "\ud835\udc12": ["S", ["BOLD"]], "\ud835\udc13": ["T", ["BOLD"]], "\ud835\udc14": ["U", ["BOLD"]], "\ud835\udc15": ["V", ["BOLD"]], "\ud835\udc16": ["W", ["BOLD"]], "\ud835\udc17": ["X", ["BOLD"]], "\ud835\udc18": ["Y", ["BOLD"]], "\ud835\udc19": ["Z", ["BOLD"]], "\ud835\udc1a": ["a", ["BOLD"]], "\ud835\udc1b": ["b", ["BOLD"]], "\ud835\udc1c": ["c", ["BOLD"]], "\ud835\udc1d": ["d", ["BOLD"]], "\ud835\udc1e": ["e", ["BOLD"]], "\ud835\udc1f": ["f", ["BOLD"]], "\ud835\udc20": ["g", ["BOLD"]], "\ud835\udc21": ["h", ["BOLD"]], "\ud835\udc22": ["i", ["BOLD"]], "\ud835\udc23": ["j", ["BOLD"]], "\ud835\udc24": ["k", ["BOLD"]], "\ud835\udc25": ["l", ["BOLD"]], "\ud835\udc26": ["m", ["BOLD"]], "\ud835\udc27": ["n", ["BOLD"]], "\ud835\udc28": ["o", ["BOLD"]], "\ud835\udc29": ["p", ["BOLD"]], "\ud835\udc2a": ["q", ["BOLD"]], "\ud835\udc2b": ["r", ["BOLD"]], "\ud835\udc2c": ["s", ["BOLD"]], "\ud835\udc2d": ["t", ["BOLD"]], "\ud835\udc2e": ["u", ["BOLD"]], "\ud835\udc2f": ["v", ["BOLD"]], "\ud835\udc30": ["w", ["BOLD"]], "\ud835\udc31": ["x", ["BOLD"]], "\ud835\udc32": ["y", ["BOLD"]], "\ud835\udc33": ["z", ["BOLD"]], "\ud835\udc34": ["A", ["ITALIC"]], "\ud835\udc35": ["B", ["ITALIC"]], "\ud835\udc36": ["C", ["ITALIC"]], "\ud835\udc37": ["D", ["ITALIC"]], "\ud835\udc38": ["E", ["ITALIC"]], "\ud835\udc39": ["F", ["ITALIC"]], "\ud835\udc3a": ["G", ["ITALIC"]], "\ud835\udc3b": ["H", ["ITALIC"]], "\ud835\udc3c": ["I", ["ITALIC"]], "\ud835\udc3d": ["J", ["ITALIC"]], "\ud835\udc3e": ["K", ["ITALIC"]], "\ud835\udc3f": ["L", ["ITALIC"]], "\ud835\udc40": ["M", ["ITALIC"]], "\ud835\udc41": ["N", ["ITALIC"]], "\ud835\udc42": ["O", ["ITALIC"]], "\ud835\udc43": ["P", ["ITALIC"]], "\ud835\udc44": ["Q", ["ITALIC"]], "\ud835\udc45": ["R", ["ITALIC"]], "\ud835\udc46": ["S", ["ITALIC"]], "\ud835\udc47": ["T", ["ITALIC"]], "\ud835\udc48": ["U", ["ITALIC"]], "\ud835\udc49": ["V", ["ITALIC"]], "\ud835\udc4a": ["W", ["ITALIC"]], "\ud835\udc4b": ["X", ["ITALIC"]], "\ud835\udc4c": ["Y", ["ITALIC"]], "\ud835\udc4d": ["Z", ["ITALIC"]], "\ud835\udc4e": ["a", ["ITALIC"]], "\ud835\udc4f": ["b", ["ITALIC"]], "\ud835\udc50": ["c", ["ITALIC"]], "\ud835\udc51": ["d", ["ITALIC"]], "\ud835\udc52": ["e", ["ITALIC"]], "\ud835\udc53": ["f", ["ITALIC"]], "\ud835\udc54": ["g", ["ITALIC"]], "\ud835\udc56": ["i", ["ITALIC"]], "\ud835\udc57": ["j", ["ITALIC"]], "\ud835\udc58": ["k", ["ITALIC"]], "\ud835\udc59": ["l", ["ITALIC"]], "\ud835\udc5a": ["m", ["ITALIC"]], "\ud835\udc5b": ["n", ["ITALIC"]], "\ud835\udc5c": ["o", ["ITALIC"]], "\ud835\udc5d": ["p", ["ITALIC"]], "\ud835\udc5e": ["q", ["ITALIC"]], "\ud835\udc5f": ["r", ["ITALIC"]], "\ud835\udc60": ["s", ["ITALIC"]], "\ud835\udc61": ["t", ["ITALIC"]], "\ud835\udc62": ["u", ["ITALIC"]], "\ud835\udc63": ["v", ["ITALIC"]], "\ud835\udc64": ["w", ["ITALIC"]], "\ud835\udc65": ["x", ["ITALIC"]], "\ud835\udc66": ["y", ["ITALIC"]], "\ud835\udc67": ["z", ["ITALIC"]], "\ud835\udc68": ["A", ["BOLD", "ITALIC"]], "\ud835\udc69": ["B", ["BOLD", "ITALIC"]], "\ud835\udc6a": ["C", ["BOLD", "ITALIC"]], "\ud835\udc6b": ["D", ["BOLD", "ITALIC"]], "\ud835\udc6c": ["E", ["BOLD", "ITALIC"]], "\ud835\udc6d": ["F", ["BOLD", "ITALIC"]], "\ud835\udc6e": ["G", ["BOLD", "ITALIC"]], "\ud835\udc6f": ["H", ["BOLD", "ITALIC"]], "\ud835\udc70": ["I", ["BOLD", "ITALIC"]], "\ud835\udc71": ["J", ["BOLD", "ITALIC"]], "\ud835\udc72": ["K", ["BOLD", "ITALIC"]], "\ud835\udc73": ["L", ["BOLD", "ITALIC"]], "\ud835\udc74": ["M", ["BOLD", "ITALIC"]], "\ud835\udc75": ["N", ["BOLD", "ITALIC"]], "\ud835\udc76": ["O", ["BOLD", "ITALIC"]], "\ud835\udc77": ["P", ["BOLD", "ITALIC"]], "\ud835\udc78": ["Q", ["BOLD", "ITALIC"]], "\ud835\udc79": ["R", ["BOLD", "ITALIC"]], "\ud835\udc7a": ["S", ["BOLD", "ITALIC"]], "\ud835\udc7b": ["T", ["BOLD", "ITALIC"]], "\ud835\udc7c": ["U", ["BOLD", "ITALIC"]], "\ud835\udc7d": ["V", ["BOLD", "ITALIC"]], "\ud835\udc7e": ["W", ["BOLD", "ITALIC"]], "\ud835\udc7f": ["X", ["BOLD", "ITALIC"]], "\ud835\udc80": ["Y", ["BOLD", "ITALIC"]], "\ud835\udc81": ["Z", ["BOLD", "ITALIC"]], "\ud835\udc82": ["a", ["BOLD", "ITALIC"]], "\ud835\udc83": ["b", ["BOLD", "ITALIC"]], "\ud835\udc84": ["c", ["BOLD", "ITALIC"]], "\ud835\udc85": ["d", ["BOLD", "ITALIC"]], "\ud835\udc86": ["e", ["BOLD", "ITALIC"]], "\ud835\udc87": ["f", ["BOLD", "ITALIC"]], "\ud835\udc88": ["g", ["BOLD", "ITALIC"]], "\ud835\udc89": ["h", ["BOLD", "ITALIC"]], "\ud835\udc8a": ["i", ["BOLD", "ITALIC"]], "\ud835\udc8b": ["j", ["BOLD", "ITALIC"]], "\ud835\udc8c": ["k", ["BOLD", "ITALIC"]], "\ud835\udc8d": ["l", ["BOLD", "ITALIC"]], "\ud835\udc8e": ["m", ["BOLD", "ITALIC"]], "\ud835\udc8f": ["n", ["BOLD", "ITALIC"]], "\ud835\udc90": ["o", ["BOLD", "ITALIC"]], "\ud835\udc91": ["p", ["BOLD", "ITALIC"]], "\ud835\udc92": ["q", ["BOLD", "ITALIC"]], "\ud835\udc93": ["r", ["BOLD", "ITALIC"]], "\ud835\udc94": ["s", ["BOLD", "ITALIC"]], "\ud835\udc95": ["t", ["BOLD", "ITALIC"]], "\ud835\udc96": ["u", ["BOLD", "ITALIC"]], "\ud835\udc97": ["v", ["BOLD", "ITALIC"]], "\ud835\udc98": ["w", ["BOLD", "ITALIC"]], "\ud835\udc99": ["x", ["BOLD", "ITALIC"]], "\ud835\udc9a": ["y", ["BOLD", "ITALIC"]], "\ud835\udc9b": ["z", ["BOLD", "ITALIC"]], "\ud835\udc9c": ["A", ["SCRIPT"]], "\ud835\udc9e": ["C", ["SCRIPT"]], "\ud835\udc9f": ["D", ["SCRIPT"]], "\ud835\udca2": ["G", ["SCRIPT"]], "\ud835\udca5": ["J", ["SCRIPT"]], "\ud835\udca6": ["K", ["SCRIPT"]], "\ud835\udca9": ["N", ["SCRIPT"]], "\ud835\udcaa": ["O", ["SCRIPT"]], "\ud835\udcab": ["P", ["SCRIPT"]], "\ud835\udcac": ["Q", ["SCRIPT"]], "\ud835\udcae": ["S", ["SCRIPT"]], "\ud835\udcaf": ["T", ["SCRIPT"]], "\ud835\udcb0": ["U", ["SCRIPT"]], "\ud835\udcb1": ["V", ["SCRIPT"]], "\ud835\udcb2": ["W", ["SCRIPT"]], "\ud835\udcb3": ["X", ["SCRIPT"]], "\ud835\udcb4": ["Y", ["SCRIPT"]], "\ud835\udcb5": ["Z", ["SCRIPT"]], "\ud835\udcb6": ["a", ["SCRIPT"]], "\ud835\udcb7": ["b", ["SCRIPT"]], "\ud835\udcb8": ["c", ["SCRIPT"]], "\ud835\udcb9": ["d", ["SCRIPT"]], "\ud835\udcbb": ["f", ["SCRIPT"]], "\ud835\udcbd": ["h", ["SCRIPT"]], "\ud835\udcbe": ["i", ["SCRIPT"]], "\ud835\udcbf": ["j", ["SCRIPT"]], "\ud835\udcc0": ["k", ["SCRIPT"]], "\ud835\udcc1": ["l", ["SCRIPT"]], "\ud835\udcc2": ["m", ["SCRIPT"]], "\ud835\udcc3": ["n", ["SCRIPT"]], "\ud835\udcc5": ["p", ["SCRIPT"]], "\ud835\udcc6": ["q", ["SCRIPT"]], "\ud835\udcc7": ["r", ["SCRIPT"]], "\ud835\udcc8": ["s", ["SCRIPT"]], "\ud835\udcc9": ["t", ["SCRIPT"]], "\ud835\udcca": ["u", ["SCRIPT"]], "\ud835\udccb": ["v", ["SCRIPT"]], "\ud835\udccc": ["w", ["SCRIPT"]], "\ud835\udccd": ["x", ["SCRIPT"]], "\ud835\udcce": ["y", ["SCRIPT"]], "\ud835\udccf": ["z", ["SCRIPT"]], "\ud835\udcd0": ["A", ["BOLD", "SCRIPT"]], "\ud835\udcd1": ["B", ["BOLD", "SCRIPT"]], "\ud835\udcd2": ["C", ["BOLD", "SCRIPT"]], "\ud835\udcd3": ["D", ["BOLD", "SCRIPT"]], "\ud835\udcd4": ["E", ["BOLD", "SCRIPT"]], "\ud835\udcd5": ["F", ["BOLD", "SCRIPT"]], "\ud835\udcd6": ["G", ["BOLD", "SCRIPT"]], "\ud835\udcd7": ["H", ["BOLD", "SCRIPT"]], "\ud835\udcd8": ["I", ["BOLD", "SCRIPT"]], "\ud835\udcd9": ["J", ["BOLD", "SCRIPT"]], "\ud835\udcda": ["K", ["BOLD", "SCRIPT"]], "\ud835\udcdb": ["L", ["BOLD", "SCRIPT"]], "\ud835\udcdc": ["M", ["BOLD", "SCRIPT"]], "\ud835\udcdd": ["N", ["BOLD", "SCRIPT"]], "\ud835\udcde": ["O", ["BOLD", "SCRIPT"]], "\ud835\udcdf": ["P", ["BOLD", "SCRIPT"]], "\ud835\udce0": ["Q", ["BOLD", "SCRIPT"]], "\ud835\udce1": ["R", ["BOLD", "SCRIPT"]], "\ud835\udce2": ["S", ["BOLD", "SCRIPT"]], "\ud835\udce3": ["T", ["BOLD", "SCRIPT"]], "\ud835\udce4": ["U", ["BOLD", "SCRIPT"]], "\ud835\udce5": ["V", ["BOLD", "SCRIPT"]], "\ud835\udce6": ["W", ["BOLD", "SCRIPT"]], "\ud835\udce7": ["X", ["BOLD", "SCRIPT"]], "\ud835\udce8": ["Y", ["BOLD", "SCRIPT"]], "\ud835\udce9": ["Z", ["BOLD", "SCRIPT"]], "\ud835\udcea": ["a", ["BOLD", "SCRIPT"]], "\ud835\udceb": ["b", ["BOLD", "SCRIPT"]], "\ud835\udcec": ["c", ["BOLD", "SCRIPT"]], "\ud835\udced": ["d", ["BOLD", "SCRIPT"]], "\ud835\udcee": ["e", ["BOLD", "SCRIPT"]], "\ud835\udcef": ["f", ["BOLD", "SCRIPT"]], "\ud835\udcf0": ["g", ["BOLD", "SCRIPT"]], "\ud835\udcf1": ["h", ["BOLD", "SCRIPT"]], "\ud835\udcf2": ["i", ["BOLD", "SCRIPT"]], "\ud835\udcf3": ["j", ["BOLD", "SCRIPT"]], "\ud835\udcf4": ["k", ["BOLD", "SCRIPT"]], "\ud835\udcf5": ["l", ["BOLD", "SCRIPT"]], "\ud835\udcf6": ["m", ["BOLD", "SCRIPT"]], "\ud835\udcf7": ["n", ["BOLD", "SCRIPT"]], "\ud835\udcf8": ["o", ["BOLD", "SCRIPT"]], "\ud835\udcf9": ["p", ["BOLD", "SCRIPT"]], "\ud835\udcfa": ["q", ["BOLD", "SCRIPT"]], "\ud835\udcfb": ["r", ["BOLD", "SCRIPT"]], "\ud835\udcfc": ["s", ["BOLD", "SCRIPT"]], "\ud835\udcfd": ["t", ["BOLD", "SCRIPT"]], "\ud835\udcfe": ["u", ["BOLD", "SCRIPT"]], "\ud835\udcff": ["v", ["BOLD", "SCRIPT"]], "\ud835\udd00": ["w", ["BOLD", "SCRIPT"]], "\ud835\udd01": ["x", ["BOLD", "SCRIPT"]], "\ud835\udd02": ["y", ["BOLD", "SCRIPT"]], "\ud835\udd03": ["z", ["BOLD", "SCRIPT"]], "\ud835\udd04": ["A", ["FRAKTUR"]], "\ud835\udd05": ["B", ["FRAKTUR"]], "\ud835\udd07": ["D", ["FRAKTUR"]], "\ud835\udd08": ["E", ["FRAKTUR"]], "\ud835\udd09": ["F", ["FRAKTUR"]], "\ud835\udd0a": ["G", ["FRAKTUR"]], "\ud835\udd0d": ["J", ["FRAKTUR"]], "\ud835\udd0e": ["K", ["FRAKTUR"]], "\ud835\udd0f": ["L", ["FRAKTUR"]], "\ud835\udd10": ["M", ["FRAKTUR"]], "\ud835\udd11": ["N", ["FRAKTUR"]], "\ud835\udd12": ["O", ["FRAKTUR"]], "\ud835\udd13": ["P", ["FRAKTUR"]], "\ud835\udd14": ["Q", ["FRAKTUR"]], "\ud835\udd16": ["S", ["FRAKTUR"]], "\ud835\udd17": ["T", ["FRAKTUR"]], "\ud835\udd18": ["U", ["FRAKTUR"]], "\ud835\udd19": ["V", ["FRAKTUR"]], "\ud835\udd1a": ["W", ["FRAKTUR"]], "\ud835\udd1b": ["X", ["FRAKTUR"]], "\ud835\udd1c": ["Y", ["FRAKTUR"]], "\ud835\udd1e": ["a", ["FRAKTUR"]], "\ud835\udd1f": ["b", ["FRAKTUR"]], "\ud835\udd20": ["c", ["FRAKTUR"]], "\ud835\udd21": ["d", ["FRAKTUR"]], "\ud835\udd22": ["e", ["FRAKTUR"]], "\ud835\udd23": ["f", ["FRAKTUR"]], "\ud835\udd24": ["g", ["FRAKTUR"]], "\ud835\udd25": ["h", ["FRAKTUR"]], "\ud835\udd26": ["i", ["FRAKTUR"]], "\ud835\udd27": ["j", ["FRAKTUR"]], "\ud835\udd28": ["k", ["FRAKTUR"]], "\ud835\udd29": ["l", ["FRAKTUR"]], "\ud835\udd2a": ["m", ["FRAKTUR"]], "\ud835\udd2b": ["n", ["FRAKTUR"]], "\ud835\udd2c": ["o", ["FRAKTUR"]], "\ud835\udd2d": ["p", ["FRAKTUR"]], "\ud835\udd2e": ["q", ["FRAKTUR"]], "\ud835\udd2f": ["r", ["FRAKTUR"]], "\ud835\udd30": ["s", ["FRAKTUR"]], "\ud835\udd31": ["t", ["FRAKTUR"]], "\ud835\udd32": ["u", ["FRAKTUR"]], "\ud835\udd33": ["v", ["FRAKTUR"]], "\ud835\udd34": ["w", ["FRAKTUR"]], "\ud835\udd35": ["x", ["FRAKTUR"]], "\ud835\udd36": ["y", ["FRAKTUR"]], "\ud835\udd37": ["z", ["FRAKTUR"]], "\ud835\udd38": ["A", ["DOUBLE-STRUCK"]], "\ud835\udd39": ["B", ["DOUBLE-STRUCK"]], "\ud835\udd3b": ["D", ["DOUBLE-STRUCK"]], "\ud835\udd3c": ["E", ["DOUBLE-STRUCK"]], "\ud835\udd3d": ["F", ["DOUBLE-STRUCK"]], "\ud835\udd3e": ["G", ["DOUBLE-STRUCK"]], "\ud835\udd40": ["I", ["DOUBLE-STRUCK"]], "\ud835\udd41": ["J", ["DOUBLE-STRUCK"]], "\ud835\udd42": ["K", ["DOUBLE-STRUCK"]], "\ud835\udd43": ["L", ["DOUBLE-STRUCK"]], "\ud835\udd44": ["M", ["DOUBLE-STRUCK"]], "\ud835\udd46": ["O", ["DOUBLE-STRUCK"]], "\ud835\udd4a": ["S", ["DOUBLE-STRUCK"]], "\ud835\udd4b": ["T", ["DOUBLE-STRUCK"]], "\ud835\udd4c": ["U", ["DOUBLE-STRUCK"]], "\ud835\udd4d": ["V", ["DOUBLE-STRUCK"]], "\ud835\udd4e": ["W", ["DOUBLE-STRUCK"]], "\ud835\udd4f": ["X", ["DOUBLE-STRUCK"]], "\ud835\udd50": ["Y", ["DOUBLE-STRUCK"]], "\ud835\udd52": ["a", ["DOUBLE-STRUCK"]], "\ud835\udd53": ["b", ["DOUBLE-STRUCK"]], "\ud835\udd54": ["c", ["DOUBLE-STRUCK"]], "\ud835\udd55": ["d", ["DOUBLE-STRUCK"]], "\ud835\udd56": ["e", ["DOUBLE-STRUCK"]], "\ud835\udd57": ["f", ["DOUBLE-STRUCK"]], "\ud835\udd58": ["g", ["DOUBLE-STRUCK"]], "\ud835\udd59": ["h", ["DOUBLE-STRUCK"]], "\ud835\udd5a": ["i", ["DOUBLE-STRUCK"]], "\ud835\udd5b": ["j", ["DOUBLE-STRUCK"]], "\ud835\udd5c": ["k", ["DOUBLE-STRUCK"]], "\ud835\udd5d": ["l", ["DOUBLE-STRUCK"]], "\ud835\udd5e": ["m", ["DOUBLE-STRUCK"]], "\ud835\udd5f": ["n", ["DOUBLE-STRUCK"]], "\ud835\udd60": ["o", ["DOUBLE-STRUCK"]], "\ud835\udd61": ["p", ["DOUBLE-STRUCK"]], "\ud835\udd62": ["q", ["DOUBLE-STRUCK"]], "\ud835\udd63": ["r", ["DOUBLE-STRUCK"]], "\ud835\udd64": ["s", ["DOUBLE-STRUCK"]], "\ud835\udd65": ["t", ["DOUBLE-STRUCK"]], "\ud835\udd66": ["u", ["DOUBLE-STRUCK"]], "\ud835\udd67": ["v", ["DOUBLE-STRUCK"]], "\ud835\udd68": ["w", ["DOUBLE-STRUCK"]], "\ud835\udd69": ["x", ["DOUBLE-STRUCK"]], "\ud835\udd6a": ["y", ["DOUBLE-STRUCK"]], "\ud835\udd6b": ["z", ["DOUBLE-STRUCK"]], "\ud835\udd6c": ["A", ["BOLD", "FRAKTUR"]], "\ud835\udd6d": ["B", ["BOLD", "FRAKTUR"]], "\ud835\udd6e": ["C", ["BOLD", "FRAKTUR"]], "\ud835\udd6f": ["D", ["BOLD", "FRAKTUR"]], "\ud835\udd70": ["E", ["BOLD", "FRAKTUR"]], "\ud835\udd71": ["F", ["BOLD", "FRAKTUR"]], "\ud835\udd72": ["G", ["BOLD", "FRAKTUR"]], "\ud835\udd73": ["H", ["BOLD", "FRAKTUR"]], "\ud835\udd74": ["I", ["BOLD", "FRAKTUR"]], "\ud835\udd75": ["J", ["BOLD", "FRAKTUR"]], "\ud835\udd76": ["K", ["BOLD", "FRAKTUR"]], "\ud835\udd77": ["L", ["BOLD", "FRAKTUR"]], "\ud835\udd78": ["M", ["BOLD", "FRAKTUR"]], "\ud835\udd79": ["N", ["BOLD", "FRAKTUR"]], "\ud835\udd7a": ["O", ["BOLD", "FRAKTUR"]], "\ud835\udd7b": ["P", ["BOLD", "FRAKTUR"]], "\ud835\udd7c": ["Q", ["BOLD", "FRAKTUR"]], "\ud835\udd7d": ["R", ["BOLD", "FRAKTUR"]], "\ud835\udd7e": ["S", ["BOLD", "FRAKTUR"]], "\ud835\udd7f": ["T", ["BOLD", "FRAKTUR"]], "\ud835\udd80": ["U", ["BOLD", "FRAKTUR"]], "\ud835\udd81": ["V", ["BOLD", "FRAKTUR"]], "\ud835\udd82": ["W", ["BOLD", "FRAKTUR"]], "\ud835\udd83": ["X", ["BOLD", "FRAKTUR"]], "\ud835\udd84": ["Y", ["BOLD", "FRAKTUR"]], "\ud835\udd85": ["Z", ["BOLD", "FRAKTUR"]], "\ud835\udd86": ["a", ["BOLD", "FRAKTUR"]], "\ud835\udd87": ["b", ["BOLD", "FRAKTUR"]], "\ud835\udd88": ["c", ["BOLD", "FRAKTUR"]], "\ud835\udd89": ["d", ["BOLD", "FRAKTUR"]], "\ud835\udd8a": ["e", ["BOLD", "FRAKTUR"]], "\ud835\udd8b": ["f", ["BOLD", "FRAKTUR"]], "\ud835\udd8c": ["g", ["BOLD", "FRAKTUR"]], "\ud835\udd8d": ["h", ["BOLD", "FRAKTUR"]], "\ud835\udd8e": ["i", ["BOLD", "FRAKTUR"]], "\ud835\udd8f": ["j", ["BOLD", "FRAKTUR"]], "\ud835\udd90": ["k", ["BOLD", "FRAKTUR"]], "\ud835\udd91": ["l", ["BOLD", "FRAKTUR"]], "\ud835\udd92": ["m", ["BOLD", "FRAKTUR"]], "\ud835\udd93": ["n", ["BOLD", "FRAKTUR"]], "\ud835\udd94": ["o", ["BOLD", "FRAKTUR"]], "\ud835\udd95": ["p", ["BOLD", "FRAKTUR"]], "\ud835\udd96": ["q", ["BOLD", "FRAKTUR"]], "\ud835\udd97": ["r", ["BOLD", "FRAKTUR"]], "\ud835\udd98": ["s", ["BOLD", "FRAKTUR"]], "\ud835\udd99": ["t", ["BOLD", "FRAKTUR"]], "\ud835\udd9a": ["u", ["BOLD", "FRAKTUR"]], "\ud835\udd9b": ["v", ["BOLD", "FRAKTUR"]], "\ud835\udd9c": ["w", ["BOLD", "FRAKTUR"]], "\ud835\udd9d": ["x", ["BOLD", "FRAKTUR"]], "\ud835\udd9e": ["y", ["BOLD", "FRAKTUR"]], "\ud835\udd9f": ["z", ["BOLD", "FRAKTUR"]], "\ud835\udda0": ["A", ["SANS-SERIF"]], "\ud835\udda1": ["B", ["SANS-SERIF"]], "\ud835\udda2": ["C", ["SANS-SERIF"]], "\ud835\udda3": ["D", ["SANS-SERIF"]], "\ud835\udda4": ["E", ["SANS-SERIF"]], "\ud835\udda5": ["F", ["SANS-SERIF"]], "\ud835\udda6": ["G", ["SANS-SERIF"]], "\ud835\udda7": ["H", ["SANS-SERIF"]], "\ud835\udda8": ["I", ["SANS-SERIF"]], "\ud835\udda9": ["J", ["SANS-SERIF"]], "\ud835\uddaa": ["K", ["SANS-SERIF"]], "\ud835\uddab": ["L", ["SANS-SERIF"]], "\ud835\uddac": ["M", ["SANS-SERIF"]], "\ud835\uddad": ["N", ["SANS-SERIF"]], "\ud835\uddae": ["O", ["SANS-SERIF"]], "\ud835\uddaf": ["P", ["SANS-SERIF"]], "\ud835\uddb0": ["Q", ["SANS-SERIF"]], "\ud835\uddb1": ["R", ["SANS-SERIF"]], "\ud835\uddb2": ["S", ["SANS-SERIF"]], "\ud835\uddb3": ["T", ["SANS-SERIF"]], "\ud835\uddb4": ["U", ["SANS-SERIF"]], "\ud835\uddb5": ["V", ["SANS-SERIF"]], "\ud835\uddb6": ["W", ["SANS-SERIF"]], "\ud835\uddb7": ["X", ["SANS-SERIF"]], "\ud835\uddb8": ["Y", ["SANS-SERIF"]], "\ud835\uddb9": ["Z", ["SANS-SERIF"]], "\ud835\uddba": ["a", ["SANS-SERIF"]], "\ud835\uddbb": ["b", ["SANS-SERIF"]], "\ud835\uddbc": ["c", ["SANS-SERIF"]], "\ud835\uddbd": ["d", ["SANS-SERIF"]], "\ud835\uddbe": ["e", ["SANS-SERIF"]], "\ud835\uddbf": ["f", ["SANS-SERIF"]], "\ud835\uddc0": ["g", ["SANS-SERIF"]], "\ud835\uddc1": ["h", ["SANS-SERIF"]], "\ud835\uddc2": ["i", ["SANS-SERIF"]], "\ud835\uddc3": ["j", ["SANS-SERIF"]], "\ud835\uddc4": ["k", ["SANS-SERIF"]], "\ud835\uddc5": ["l", ["SANS-SERIF"]], "\ud835\uddc6": ["m", ["SANS-SERIF"]], "\ud835\uddc7": ["n", ["SANS-SERIF"]], "\ud835\uddc8": ["o", ["SANS-SERIF"]], "\ud835\uddc9": ["p", ["SANS-SERIF"]], "\ud835\uddca": ["q", ["SANS-SERIF"]], "\ud835\uddcb": ["r", ["SANS-SERIF"]], "\ud835\uddcc": ["s", ["SANS-SERIF"]], "\ud835\uddcd": ["t", ["SANS-SERIF"]], "\ud835\uddce": ["u", ["SANS-SERIF"]], "\ud835\uddcf": ["v", ["SANS-SERIF"]], "\ud835\uddd0": ["w", ["SANS-SERIF"]], "\ud835\uddd1": ["x", ["SANS-SERIF"]], "\ud835\uddd2": ["y", ["SANS-SERIF"]], "\ud835\uddd3": ["z", ["SANS-SERIF"]], "\ud835\uddd4": ["A", ["SANS-SERIF", "BOLD"]], "\ud835\uddd5": ["B", ["SANS-SERIF", "BOLD"]], "\ud835\uddd6": ["C", ["SANS-SERIF", "BOLD"]], "\ud835\uddd7": ["D", ["SANS-SERIF", "BOLD"]], "\ud835\uddd8": ["E", ["SANS-SERIF", "BOLD"]], "\ud835\uddd9": ["F", ["SANS-SERIF", "BOLD"]], "\ud835\uddda": ["G", ["SANS-SERIF", "BOLD"]], "\ud835\udddb": ["H", ["SANS-SERIF", "BOLD"]], "\ud835\udddc": ["I", ["SANS-SERIF", "BOLD"]], "\ud835\udddd": ["J", ["SANS-SERIF", "BOLD"]], "\ud835\uddde": ["K", ["SANS-SERIF", "BOLD"]], "\ud835\udddf": ["L", ["SANS-SERIF", "BOLD"]], "\ud835\udde0": ["M", ["SANS-SERIF", "BOLD"]], "\ud835\udde1": ["N", ["SANS-SERIF", "BOLD"]], "\ud835\udde2": ["O", ["SANS-SERIF", "BOLD"]], "\ud835\udde3": ["P", ["SANS-SERIF", "BOLD"]], "\ud835\udde4": ["Q", ["SANS-SERIF", "BOLD"]], "\ud835\udde5": ["R", ["SANS-SERIF", "BOLD"]], "\ud835\udde6": ["S", ["SANS-SERIF", "BOLD"]], "\ud835\udde7": ["T", ["SANS-SERIF", "BOLD"]], "\ud835\udde8": ["U", ["SANS-SERIF", "BOLD"]], "\ud835\udde9": ["V", ["SANS-SERIF", "BOLD"]], "\ud835\uddea": ["W", ["SANS-SERIF", "BOLD"]], "\ud835\uddeb": ["X", ["SANS-SERIF", "BOLD"]], "\ud835\uddec": ["Y", ["SANS-SERIF", "BOLD"]], "\ud835\udded": ["Z", ["SANS-SERIF", "BOLD"]], "\ud835\uddee": ["a", ["SANS-SERIF", "BOLD"]], "\ud835\uddef": ["b", ["SANS-SERIF", "BOLD"]], "\ud835\uddf0": ["c", ["SANS-SERIF", "BOLD"]], "\ud835\uddf1": ["d", ["SANS-SERIF", "BOLD"]], "\ud835\uddf2": ["e", ["SANS-SERIF", "BOLD"]], "\ud835\uddf3": ["f", ["SANS-SERIF", "BOLD"]], "\ud835\uddf4": ["g", ["SANS-SERIF", "BOLD"]], "\ud835\uddf5": ["h", ["SANS-SERIF", "BOLD"]], "\ud835\uddf6": ["i", ["SANS-SERIF", "BOLD"]], "\ud835\uddf7": ["j", ["SANS-SERIF", "BOLD"]], "\ud835\uddf8": ["k", ["SANS-SERIF", "BOLD"]], "\ud835\uddf9": ["l", ["SANS-SERIF", "BOLD"]], "\ud835\uddfa": ["m", ["SANS-SERIF", "BOLD"]], "\ud835\uddfb": ["n", ["SANS-SERIF", "BOLD"]], "\ud835\uddfc": ["o", ["SANS-SERIF", "BOLD"]], "\ud835\uddfd": ["p", ["SANS-SERIF", "BOLD"]], "\ud835\uddfe": ["q", ["SANS-SERIF", "BOLD"]], "\ud835\uddff": ["r", ["SANS-SERIF", "BOLD"]], "\ud835\ude00": ["s", ["SANS-SERIF", "BOLD"]], "\ud835\ude01": ["t", ["SANS-SERIF", "BOLD"]], "\ud835\ude02": ["u", ["SANS-SERIF", "BOLD"]], "\ud835\ude03": ["v", ["SANS-SERIF", "BOLD"]], "\ud835\ude04": ["w", ["SANS-SERIF", "BOLD"]], "\ud835\ude05": ["x", ["SANS-SERIF", "BOLD"]], "\ud835\ude06": ["y", ["SANS-SERIF", "BOLD"]], "\ud835\ude07": ["z", ["SANS-SERIF", "BOLD"]], "\ud835\ude08": ["A", ["SANS-SERIF", "ITALIC"]], "\ud835\ude09": ["B", ["SANS-SERIF", "ITALIC"]], "\ud835\ude0a": ["C", ["SANS-SERIF", "ITALIC"]], "\ud835\ude0b": ["D", ["SANS-SERIF", "ITALIC"]], "\ud835\ude0c": ["E", ["SANS-SERIF", "ITALIC"]], "\ud835\ude0d": ["F", ["SANS-SERIF", "ITALIC"]], "\ud835\ude0e": ["G", ["SANS-SERIF", "ITALIC"]], "\ud835\ude0f": ["H", ["SANS-SERIF", "ITALIC"]], "\ud835\ude10": ["I", ["SANS-SERIF", "ITALIC"]], "\ud835\ude11": ["J", ["SANS-SERIF", "ITALIC"]], "\ud835\ude12": ["K", ["SANS-SERIF", "ITALIC"]], "\ud835\ude13": ["L", ["SANS-SERIF", "ITALIC"]], "\ud835\ude14": ["M", ["SANS-SERIF", "ITALIC"]], "\ud835\ude15": ["N", ["SANS-SERIF", "ITALIC"]], "\ud835\ude16": ["O", ["SANS-SERIF", "ITALIC"]], "\ud835\ude17": ["P", ["SANS-SERIF", "ITALIC"]], "\ud835\ude18": ["Q", ["SANS-SERIF", "ITALIC"]], "\ud835\ude19": ["R", ["SANS-SERIF", "ITALIC"]], "\ud835\ude1a": ["S", ["SANS-SERIF", "ITALIC"]], "\ud835\ude1b": ["T", ["SANS-SERIF", "ITALIC"]], "\ud835\ude1c": ["U", ["SANS-SERIF", "ITALIC"]], "\ud835\ude1d": ["V", ["SANS-SERIF", "ITALIC"]], "\ud835\ude1e": ["W", ["SANS-SERIF", "ITALIC"]], "\ud835\ude1f": ["X", ["SANS-SERIF", "ITALIC"]], "\ud835\ude20": ["Y", ["SANS-SERIF", "ITALIC"]], "\ud835\ude21": ["Z", ["SANS-SERIF", "ITALIC"]], "\ud835\ude22": ["a", ["SANS-SERIF", "ITALIC"]], "\ud835\ude23": ["b", ["SANS-SERIF", "ITALIC"]], "\ud835\ude24": ["c", ["SANS-SERIF", "ITALIC"]], "\ud835\ude25": ["d", ["SANS-SERIF", "ITALIC"]], "\ud835\ude26": ["e", ["SANS-SERIF", "ITALIC"]], "\ud835\ude27": ["f", ["SANS-SERIF", "ITALIC"]], "\ud835\ude28": ["g", ["SANS-SERIF", "ITALIC"]], "\ud835\ude29": ["h", ["SANS-SERIF", "ITALIC"]], "\ud835\ude2a": ["i", ["SANS-SERIF", "ITALIC"]], "\ud835\ude2b": ["j", ["SANS-SERIF", "ITALIC"]], "\ud835\ude2c": ["k", ["SANS-SERIF", "ITALIC"]], "\ud835\ude2d": ["l", ["SANS-SERIF", "ITALIC"]], "\ud835\ude2e": ["m", ["SANS-SERIF", "ITALIC"]], "\ud835\ude2f": ["n", ["SANS-SERIF", "ITALIC"]], "\ud835\ude30": ["o", ["SANS-SERIF", "ITALIC"]], "\ud835\ude31": ["p", ["SANS-SERIF", "ITALIC"]], "\ud835\ude32": ["q", ["SANS-SERIF", "ITALIC"]], "\ud835\ude33": ["r", ["SANS-SERIF", "ITALIC"]], "\ud835\ude34": ["s", ["SANS-SERIF", "ITALIC"]], "\ud835\ude35": ["t", ["SANS-SERIF", "ITALIC"]], "\ud835\ude36": ["u", ["SANS-SERIF", "ITALIC"]], "\ud835\ude37": ["v", ["SANS-SERIF", "ITALIC"]], "\ud835\ude38": ["w", ["SANS-SERIF", "ITALIC"]], "\ud835\ude39": ["x", ["SANS-SERIF", "ITALIC"]], "\ud835\ude3a": ["y", ["SANS-SERIF", "ITALIC"]], "\ud835\ude3b": ["z", ["SANS-SERIF", "ITALIC"]], "\ud835\ude3c": ["A", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude3d": ["B", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude3e": ["C", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude3f": ["D", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude40": ["E", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude41": ["F", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude42": ["G", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude43": ["H", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude44": ["I", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude45": ["J", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude46": ["K", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude47": ["L", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude48": ["M", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude49": ["N", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude4a": ["O", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude4b": ["P", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude4c": ["Q", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude4d": ["R", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude4e": ["S", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude4f": ["T", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude50": ["U", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude51": ["V", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude52": ["W", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude53": ["X", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude54": ["Y", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude55": ["Z", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude56": ["a", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude57": ["b", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude58": ["c", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude59": ["d", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude5a": ["e", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude5b": ["f", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude5c": ["g", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude5d": ["h", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude5e": ["i", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude5f": ["j", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude60": ["k", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude61": ["l", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude62": ["m", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude63": ["n", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude64": ["o", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude65": ["p", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude66": ["q", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude67": ["r", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude68": ["s", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude69": ["t", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude6a": ["u", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude6b": ["v", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude6c": ["w", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude6d": ["x", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude6e": ["y", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude6f": ["z", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\ude70": ["A", ["MONOSPACE"]], "\ud835\ude71": ["B", ["MONOSPACE"]], "\ud835\ude72": ["C", ["MONOSPACE"]], "\ud835\ude73": ["D", ["MONOSPACE"]], "\ud835\ude74": ["E", ["MONOSPACE"]], "\ud835\ude75": ["F", ["MONOSPACE"]], "\ud835\ude76": ["G", ["MONOSPACE"]], "\ud835\ude77": ["H", ["MONOSPACE"]], "\ud835\ude78": ["I", ["MONOSPACE"]], "\ud835\ude79": ["J", ["MONOSPACE"]], "\ud835\ude7a": ["K", ["MONOSPACE"]], "\ud835\ude7b": ["L", ["MONOSPACE"]], "\ud835\ude7c": ["M", ["MONOSPACE"]], "\ud835\ude7d": ["N", ["MONOSPACE"]], "\ud835\ude7e": ["O", ["MONOSPACE"]], "\ud835\ude7f": ["P", ["MONOSPACE"]], "\ud835\ude80": ["Q", ["MONOSPACE"]], "\ud835\ude81": ["R", ["MONOSPACE"]], "\ud835\ude82": ["S", ["MONOSPACE"]], "\ud835\ude83": ["T", ["MONOSPACE"]], "\ud835\ude84": ["U", ["MONOSPACE"]], "\ud835\ude85": ["V", ["MONOSPACE"]], "\ud835\ude86": ["W", ["MONOSPACE"]], "\ud835\ude87": ["X", ["MONOSPACE"]], "\ud835\ude88": ["Y", ["MONOSPACE"]], "\ud835\ude89": ["Z", ["MONOSPACE"]], "\ud835\ude8a": ["a", ["MONOSPACE"]], "\ud835\ude8b": ["b", ["MONOSPACE"]], "\ud835\ude8c": ["c", ["MONOSPACE"]], "\ud835\ude8d": ["d", ["MONOSPACE"]], "\ud835\ude8e": ["e", ["MONOSPACE"]], "\ud835\ude8f": ["f", ["MONOSPACE"]], "\ud835\ude90": ["g", ["MONOSPACE"]], "\ud835\ude91": ["h", ["MONOSPACE"]], "\ud835\ude92": ["i", ["MONOSPACE"]], "\ud835\ude93": ["j", ["MONOSPACE"]], "\ud835\ude94": ["k", ["MONOSPACE"]], "\ud835\ude95": ["l", ["MONOSPACE"]], "\ud835\ude96": ["m", ["MONOSPACE"]], "\ud835\ude97": ["n", ["MONOSPACE"]], "\ud835\ude98": ["o", ["MONOSPACE"]], "\ud835\ude99": ["p", ["MONOSPACE"]], "\ud835\ude9a": ["q", ["MONOSPACE"]], "\ud835\ude9b": ["r", ["MONOSPACE"]], "\ud835\ude9c": ["s", ["MONOSPACE"]], "\ud835\ude9d": ["t", ["MONOSPACE"]], "\ud835\ude9e": ["u", ["MONOSPACE"]], "\ud835\ude9f": ["v", ["MONOSPACE"]], "\ud835\udea0": ["w", ["MONOSPACE"]], "\ud835\udea1": ["x", ["MONOSPACE"]], "\ud835\udea2": ["y", ["MONOSPACE"]], "\ud835\udea3": ["z", ["MONOSPACE"]], "\ud835\udea4": ["\u0131", ["ITALIC"]], "\ud835\udea5": ["\u0237", ["ITALIC"]], "\ud835\udea8": ["\u0391", ["BOLD"]], "\ud835\udea9": ["\u0392", ["BOLD"]], "\ud835\udeaa": ["\u0393", ["BOLD"]], "\ud835\udeab": ["\u0394", ["BOLD"]], "\ud835\udeac": ["\u0395", ["BOLD"]], "\ud835\udead": ["\u0396", ["BOLD"]], "\ud835\udeae": ["\u0397", ["BOLD"]], "\ud835\udeaf": ["\u0398", ["BOLD"]], "\ud835\udeb0": ["\u0399", ["BOLD"]], "\ud835\udeb1": ["\u039a", ["BOLD"]], "\ud835\udeb2": ["\u039b", ["BOLD"]], "\ud835\udeb3": ["\u039c", ["BOLD"]], "\ud835\udeb4": ["\u039d", ["BOLD"]], "\ud835\udeb5": ["\u039e", ["BOLD"]], "\ud835\udeb6": ["\u039f", ["BOLD"]], "\ud835\udeb7": ["\u03a0", ["BOLD"]], "\ud835\udeb8": ["\u03a1", ["BOLD"]], "\ud835\udeb9": ["\u0398", ["BOLD"]], "\ud835\udeba": ["\u03a3", ["BOLD"]], "\ud835\udebb": ["\u03a4", ["BOLD"]], "\ud835\udebc": ["\u03a5", ["BOLD"]], "\ud835\udebd": ["\u03a6", ["BOLD"]], "\ud835\udebe": ["\u03a7", ["BOLD"]], "\ud835\udebf": ["\u03a8", ["BOLD"]], "\ud835\udec0": ["\u03a9", ["BOLD"]], "\ud835\udec2": ["\u03b1", ["BOLD"]], "\ud835\udec3": ["\u03b2", ["BOLD"]], "\ud835\udec4": ["\u03b3", ["BOLD"]], "\ud835\udec5": ["\u03b4", ["BOLD"]], "\ud835\udec6": ["\u03b5", ["BOLD"]], "\ud835\udec7": ["\u03b6", ["BOLD"]], "\ud835\udec8": ["\u03b7", ["BOLD"]], "\ud835\udec9": ["\u03b8", ["BOLD"]], "\ud835\udeca": ["\u03b9", ["BOLD"]], "\ud835\udecb": ["\u03ba", ["BOLD"]], "\ud835\udecc": ["\u03bb", ["BOLD"]], "\ud835\udecd": ["\u03bc", ["BOLD"]], "\ud835\udece": ["\u03bd", ["BOLD"]], "\ud835\udecf": ["\u03be", ["BOLD"]], "\ud835\uded0": ["\u03bf", ["BOLD"]], "\ud835\uded1": ["\u03c0", ["BOLD"]], "\ud835\uded2": ["\u03c1", ["BOLD"]], "\ud835\uded3": ["\u03c2", ["BOLD"]], "\ud835\uded4": ["\u03c3", ["BOLD"]], "\ud835\uded5": ["\u03c4", ["BOLD"]], "\ud835\uded6": ["\u03c5", ["BOLD"]], "\ud835\uded7": ["\u03c6", ["BOLD"]], "\ud835\uded8": ["\u03c7", ["BOLD"]], "\ud835\uded9": ["\u03c8", ["BOLD"]], "\ud835\udeda": ["\u03c9", ["BOLD"]], "\ud835\udedc": ["\u03b5", ["BOLD"]], "\ud835\udedd": ["\u03b8", ["BOLD"]], "\ud835\udede": ["\u03ba", ["BOLD"]], "\ud835\udedf": ["\u03c6", ["BOLD"]], "\ud835\udee0": ["\u03c1", ["BOLD"]], "\ud835\udee1": ["\u03c0", ["BOLD"]], "\ud835\udee2": ["\u0391", ["ITALIC"]], "\ud835\udee3": ["\u0392", ["ITALIC"]], "\ud835\udee4": ["\u0393", ["ITALIC"]], "\ud835\udee5": ["\u0394", ["ITALIC"]], "\ud835\udee6": ["\u0395", ["ITALIC"]], "\ud835\udee7": ["\u0396", ["ITALIC"]], "\ud835\udee8": ["\u0397", ["ITALIC"]], "\ud835\udee9": ["\u0398", ["ITALIC"]], "\ud835\udeea": ["\u0399", ["ITALIC"]], "\ud835\udeeb": ["\u039a", ["ITALIC"]], "\ud835\udeec": ["\u039b", ["ITALIC"]], "\ud835\udeed": ["\u039c", ["ITALIC"]], "\ud835\udeee": ["\u039d", ["ITALIC"]], "\ud835\udeef": ["\u039e", ["ITALIC"]], "\ud835\udef0": ["\u039f", ["ITALIC"]], "\ud835\udef1": ["\u03a0", ["ITALIC"]], "\ud835\udef2": ["\u03a1", ["ITALIC"]], "\ud835\udef3": ["\u0398", ["ITALIC"]], "\ud835\udef4": ["\u03a3", ["ITALIC"]], "\ud835\udef5": ["\u03a4", ["ITALIC"]], "\ud835\udef6": ["\u03a5", ["ITALIC"]], "\ud835\udef7": ["\u03a6", ["ITALIC"]], "\ud835\udef8": ["\u03a7", ["ITALIC"]], "\ud835\udef9": ["\u03a8", ["ITALIC"]], "\ud835\udefa": ["\u03a9", ["ITALIC"]], "\ud835\udefc": ["\u03b1", ["ITALIC"]], "\ud835\udefd": ["\u03b2", ["ITALIC"]], "\ud835\udefe": ["\u03b3", ["ITALIC"]], "\ud835\udeff": ["\u03b4", ["ITALIC"]], "\ud835\udf00": ["\u03b5", ["ITALIC"]], "\ud835\udf01": ["\u03b6", ["ITALIC"]], "\ud835\udf02": ["\u03b7", ["ITALIC"]], "\ud835\udf03": ["\u03b8", ["ITALIC"]], "\ud835\udf04": ["\u03b9", ["ITALIC"]], "\ud835\udf05": ["\u03ba", ["ITALIC"]], "\ud835\udf06": ["\u03bb", ["ITALIC"]], "\ud835\udf07": ["\u03bc", ["ITALIC"]], "\ud835\udf08": ["\u03bd", ["ITALIC"]], "\ud835\udf09": ["\u03be", ["ITALIC"]], "\ud835\udf0a": ["\u03bf", ["ITALIC"]], "\ud835\udf0b": ["\u03c0", ["ITALIC"]], "\ud835\udf0c": ["\u03c1", ["ITALIC"]], "\ud835\udf0d": ["\u03c2", ["ITALIC"]], "\ud835\udf0e": ["\u03c3", ["ITALIC"]], "\ud835\udf0f": ["\u03c4", ["ITALIC"]], "\ud835\udf10": ["\u03c5", ["ITALIC"]], "\ud835\udf11": ["\u03c6", ["ITALIC"]], "\ud835\udf12": ["\u03c7", ["ITALIC"]], "\ud835\udf13": ["\u03c8", ["ITALIC"]], "\ud835\udf14": ["\u03c9", ["ITALIC"]], "\ud835\udf16": ["\u03b5", ["ITALIC"]], "\ud835\udf17": ["\u03b8", ["ITALIC"]], "\ud835\udf18": ["\u03ba", ["ITALIC"]], "\ud835\udf19": ["\u03c6", ["ITALIC"]], "\ud835\udf1a": ["\u03c1", ["ITALIC"]], "\ud835\udf1b": ["\u03c0", ["ITALIC"]], "\ud835\udf1c": ["\u0391", ["BOLD", "ITALIC"]], "\ud835\udf1d": ["\u0392", ["BOLD", "ITALIC"]], "\ud835\udf1e": ["\u0393", ["BOLD", "ITALIC"]], "\ud835\udf1f": ["\u0394", ["BOLD", "ITALIC"]], "\ud835\udf20": ["\u0395", ["BOLD", "ITALIC"]], "\ud835\udf21": ["\u0396", ["BOLD", "ITALIC"]], "\ud835\udf22": ["\u0397", ["BOLD", "ITALIC"]], "\ud835\udf23": ["\u0398", ["BOLD", "ITALIC"]], "\ud835\udf24": ["\u0399", ["BOLD", "ITALIC"]], "\ud835\udf25": ["\u039a", ["BOLD", "ITALIC"]], "\ud835\udf26": ["\u039b", ["BOLD", "ITALIC"]], "\ud835\udf27": ["\u039c", ["BOLD", "ITALIC"]], "\ud835\udf28": ["\u039d", ["BOLD", "ITALIC"]], "\ud835\udf29": ["\u039e", ["BOLD", "ITALIC"]], "\ud835\udf2a": ["\u039f", ["BOLD", "ITALIC"]], "\ud835\udf2b": ["\u03a0", ["BOLD", "ITALIC"]], "\ud835\udf2c": ["\u03a1", ["BOLD", "ITALIC"]], "\ud835\udf2d": ["\u0398", ["BOLD", "ITALIC"]], "\ud835\udf2e": ["\u03a3", ["BOLD", "ITALIC"]], "\ud835\udf2f": ["\u03a4", ["BOLD", "ITALIC"]], "\ud835\udf30": ["\u03a5", ["BOLD", "ITALIC"]], "\ud835\udf31": ["\u03a6", ["BOLD", "ITALIC"]], "\ud835\udf32": ["\u03a7", ["BOLD", "ITALIC"]], "\ud835\udf33": ["\u03a8", ["BOLD", "ITALIC"]], "\ud835\udf34": ["\u03a9", ["BOLD", "ITALIC"]], "\ud835\udf36": ["\u03b1", ["BOLD", "ITALIC"]], "\ud835\udf37": ["\u03b2", ["BOLD", "ITALIC"]], "\ud835\udf38": ["\u03b3", ["BOLD", "ITALIC"]], "\ud835\udf39": ["\u03b4", ["BOLD", "ITALIC"]], "\ud835\udf3a": ["\u03b5", ["BOLD", "ITALIC"]], "\ud835\udf3b": ["\u03b6", ["BOLD", "ITALIC"]], "\ud835\udf3c": ["\u03b7", ["BOLD", "ITALIC"]], "\ud835\udf3d": ["\u03b8", ["BOLD", "ITALIC"]], "\ud835\udf3e": ["\u03b9", ["BOLD", "ITALIC"]], "\ud835\udf3f": ["\u03ba", ["BOLD", "ITALIC"]], "\ud835\udf40": ["\u03bb", ["BOLD", "ITALIC"]], "\ud835\udf41": ["\u03bc", ["BOLD", "ITALIC"]], "\ud835\udf42": ["\u03bd", ["BOLD", "ITALIC"]], "\ud835\udf43": ["\u03be", ["BOLD", "ITALIC"]], "\ud835\udf44": ["\u03bf", ["BOLD", "ITALIC"]], "\ud835\udf45": ["\u03c0", ["BOLD", "ITALIC"]], "\ud835\udf46": ["\u03c1", ["BOLD", "ITALIC"]], "\ud835\udf47": ["\u03c2", ["BOLD", "ITALIC"]], "\ud835\udf48": ["\u03c3", ["BOLD", "ITALIC"]], "\ud835\udf49": ["\u03c4", ["BOLD", "ITALIC"]], "\ud835\udf4a": ["\u03c5", ["BOLD", "ITALIC"]], "\ud835\udf4b": ["\u03c6", ["BOLD", "ITALIC"]], "\ud835\udf4c": ["\u03c7", ["BOLD", "ITALIC"]], "\ud835\udf4d": ["\u03c8", ["BOLD", "ITALIC"]], "\ud835\udf4e": ["\u03c9", ["BOLD", "ITALIC"]], "\ud835\udf50": ["\u03b5", ["BOLD", "ITALIC"]], "\ud835\udf51": ["\u03b8", ["BOLD", "ITALIC"]], "\ud835\udf52": ["\u03ba", ["BOLD", "ITALIC"]], "\ud835\udf53": ["\u03c6", ["BOLD", "ITALIC"]], "\ud835\udf54": ["\u03c1", ["BOLD", "ITALIC"]], "\ud835\udf55": ["\u03c0", ["BOLD", "ITALIC"]], "\ud835\udf56": ["\u0391", ["SANS-SERIF", "BOLD"]], "\ud835\udf57": ["\u0392", ["SANS-SERIF", "BOLD"]], "\ud835\udf58": ["\u0393", ["SANS-SERIF", "BOLD"]], "\ud835\udf59": ["\u0394", ["SANS-SERIF", "BOLD"]], "\ud835\udf5a": ["\u0395", ["SANS-SERIF", "BOLD"]], "\ud835\udf5b": ["\u0396", ["SANS-SERIF", "BOLD"]], "\ud835\udf5c": ["\u0397", ["SANS-SERIF", "BOLD"]], "\ud835\udf5d": ["\u0398", ["SANS-SERIF", "BOLD"]], "\ud835\udf5e": ["\u0399", ["SANS-SERIF", "BOLD"]], "\ud835\udf5f": ["\u039a", ["SANS-SERIF", "BOLD"]], "\ud835\udf60": ["\u039b", ["SANS-SERIF", "BOLD"]], "\ud835\udf61": ["\u039c", ["SANS-SERIF", "BOLD"]], "\ud835\udf62": ["\u039d", ["SANS-SERIF", "BOLD"]], "\ud835\udf63": ["\u039e", ["SANS-SERIF", "BOLD"]], "\ud835\udf64": ["\u039f", ["SANS-SERIF", "BOLD"]], "\ud835\udf65": ["\u03a0", ["SANS-SERIF", "BOLD"]], "\ud835\udf66": ["\u03a1", ["SANS-SERIF", "BOLD"]], "\ud835\udf67": ["\u0398", ["SANS-SERIF", "BOLD"]], "\ud835\udf68": ["\u03a3", ["SANS-SERIF", "BOLD"]], "\ud835\udf69": ["\u03a4", ["SANS-SERIF", "BOLD"]], "\ud835\udf6a": ["\u03a5", ["SANS-SERIF", "BOLD"]], "\ud835\udf6b": ["\u03a6", ["SANS-SERIF", "BOLD"]], "\ud835\udf6c": ["\u03a7", ["SANS-SERIF", "BOLD"]], "\ud835\udf6d": ["\u03a8", ["SANS-SERIF", "BOLD"]], "\ud835\udf6e": ["\u03a9", ["SANS-SERIF", "BOLD"]], "\ud835\udf70": ["\u03b1", ["SANS-SERIF", "BOLD"]], "\ud835\udf71": ["\u03b2", ["SANS-SERIF", "BOLD"]], "\ud835\udf72": ["\u03b3", ["SANS-SERIF", "BOLD"]], "\ud835\udf73": ["\u03b4", ["SANS-SERIF", "BOLD"]], "\ud835\udf74": ["\u03b5", ["SANS-SERIF", "BOLD"]], "\ud835\udf75": ["\u03b6", ["SANS-SERIF", "BOLD"]], "\ud835\udf76": ["\u03b7", ["SANS-SERIF", "BOLD"]], "\ud835\udf77": ["\u03b8", ["SANS-SERIF", "BOLD"]], "\ud835\udf78": ["\u03b9", ["SANS-SERIF", "BOLD"]], "\ud835\udf79": ["\u03ba", ["SANS-SERIF", "BOLD"]], "\ud835\udf7a": ["\u03bb", ["SANS-SERIF", "BOLD"]], "\ud835\udf7b": ["\u03bc", ["SANS-SERIF", "BOLD"]], "\ud835\udf7c": ["\u03bd", ["SANS-SERIF", "BOLD"]], "\ud835\udf7d": ["\u03be", ["SANS-SERIF", "BOLD"]], "\ud835\udf7e": ["\u03bf", ["SANS-SERIF", "BOLD"]], "\ud835\udf7f": ["\u03c0", ["SANS-SERIF", "BOLD"]], "\ud835\udf80": ["\u03c1", ["SANS-SERIF", "BOLD"]], "\ud835\udf81": ["\u03c2", ["SANS-SERIF", "BOLD"]], "\ud835\udf82": ["\u03c3", ["SANS-SERIF", "BOLD"]], "\ud835\udf83": ["\u03c4", ["SANS-SERIF", "BOLD"]], "\ud835\udf84": ["\u03c5", ["SANS-SERIF", "BOLD"]], "\ud835\udf85": ["\u03c6", ["SANS-SERIF", "BOLD"]], "\ud835\udf86": ["\u03c7", ["SANS-SERIF", "BOLD"]], "\ud835\udf87": ["\u03c8", ["SANS-SERIF", "BOLD"]], "\ud835\udf88": ["\u03c9", ["SANS-SERIF", "BOLD"]], "\ud835\udf8a": ["\u03b5", ["SANS-SERIF", "BOLD"]], "\ud835\udf8b": ["\u03b8", ["SANS-SERIF", "BOLD"]], "\ud835\udf8c": ["\u03ba", ["SANS-SERIF", "BOLD"]], "\ud835\udf8d": ["\u03c6", ["SANS-SERIF", "BOLD"]], "\ud835\udf8e": ["\u03c1", ["SANS-SERIF", "BOLD"]], "\ud835\udf8f": ["\u03c0", ["SANS-SERIF", "BOLD"]], "\ud835\udf90": ["\u0391", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf91": ["\u0392", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf92": ["\u0393", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf93": ["\u0394", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf94": ["\u0395", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf95": ["\u0396", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf96": ["\u0397", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf97": ["\u0398", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf98": ["\u0399", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf99": ["\u039a", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf9a": ["\u039b", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf9b": ["\u039c", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf9c": ["\u039d", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf9d": ["\u039e", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf9e": ["\u039f", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udf9f": ["\u03a0", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa0": ["\u03a1", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa1": ["\u0398", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa2": ["\u03a3", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa3": ["\u03a4", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa4": ["\u03a5", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa5": ["\u03a6", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa6": ["\u03a7", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa7": ["\u03a8", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfa8": ["\u03a9", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfaa": ["\u03b1", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfab": ["\u03b2", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfac": ["\u03b3", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfad": ["\u03b4", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfae": ["\u03b5", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfaf": ["\u03b6", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb0": ["\u03b7", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb1": ["\u03b8", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb2": ["\u03b9", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb3": ["\u03ba", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb4": ["\u03bb", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb5": ["\u03bc", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb6": ["\u03bd", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb7": ["\u03be", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb8": ["\u03bf", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfb9": ["\u03c0", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfba": ["\u03c1", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfbb": ["\u03c2", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfbc": ["\u03c3", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfbd": ["\u03c4", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfbe": ["\u03c5", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfbf": ["\u03c6", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc0": ["\u03c7", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc1": ["\u03c8", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc2": ["\u03c9", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc4": ["\u03b5", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc5": ["\u03b8", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc6": ["\u03ba", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc7": ["\u03c6", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc8": ["\u03c1", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc9": ["\u03c0", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfca": ["\u03dc", ["BOLD"]], "\ud835\udfcb": ["\u03dd", ["BOLD"]], "\ud83b\ude00": ["\u0627", []], "\ud83b\ude01": ["\u0628", []], "\ud83b\ude02": ["\u062c", []], "\ud83b\ude03": ["\u062f", []], "\ud83b\ude05": ["\u0648", []], "\ud83b\ude06": ["\u0632", []], "\ud83b\ude07": ["\u062d", []], "\ud83b\ude08": ["\u0637", []], "\ud83b\ude09": ["\u064a", []], "\ud83b\ude0a": ["\u0643", []], "\ud83b\ude0b": ["\u0644", []], "\ud83b\ude0c": ["\u0645", []], "\ud83b\ude0d": ["\u0646", []], "\ud83b\ude0e": ["\u0633", []], "\ud83b\ude0f": ["\u0639", []], "\ud83b\ude10": ["\u0641", []], "\ud83b\ude11": ["\u0635", []], "\ud83b\ude12": ["\u0642", []], "\ud83b\ude13": ["\u0631", []], "\ud83b\ude14": ["\u0634", []], "\ud83b\ude15": ["\u062a", []], "\ud83b\ude16": ["\u062b", []], "\ud83b\ude17": ["\u062e", []], "\ud83b\ude18": ["\u0630", []], "\ud83b\ude19": ["\u0636", []], "\ud83b\ude1a": ["\u0638", []], "\ud83b\ude1b": ["\u063a", []], "\ud83b\ude1c": ["\u066e", ["DOTLESS"]], "\ud83b\ude1d": ["\u06ba", ["DOTLESS"]], "\ud83b\ude1e": ["\u06a1", ["DOTLESS"]], "\ud83b\ude1f": ["\u066f", ["DOTLESS"]], "\ud83b\ude21": ["\u0628", ["INITIAL"]], "\ud83b\ude22": ["\u062c", ["INITIAL"]], "\ud83b\ude24": ["\u0647", ["INITIAL"]], "\ud83b\ude27": ["\u062d", ["INITIAL"]], "\ud83b\ude29": ["\u064a", ["INITIAL"]], "\ud83b\ude2a": ["\u0643", ["INITIAL"]], "\ud83b\ude2b": ["\u0644", ["INITIAL"]], "\ud83b\ude2c": ["\u0645", ["INITIAL"]], "\ud83b\ude2d": ["\u0646", ["INITIAL"]], "\ud83b\ude2e": ["\u0633", ["INITIAL"]], "\ud83b\ude2f": ["\u0639", ["INITIAL"]], "\ud83b\ude30": ["\u0641", ["INITIAL"]], "\ud83b\ude31": ["\u0635", ["INITIAL"]], "\ud83b\ude32": ["\u0642", ["INITIAL"]], "\ud83b\ude34": ["\u0634", ["INITIAL"]], "\ud83b\ude35": ["\u062a", ["INITIAL"]], "\ud83b\ude36": ["\u062b", ["INITIAL"]], "\ud83b\ude37": ["\u062e", ["INITIAL"]], "\ud83b\ude39": ["\u0636", ["INITIAL"]], "\ud83b\ude3b": ["\u063a", ["INITIAL"]], "\ud83b\ude42": ["\u062c", ["TAILED"]], "\ud83b\ude47": ["\u062d", ["TAILED"]], "\ud83b\ude49": ["\u064a", ["TAILED"]], "\ud83b\ude4b": ["\u0644", ["TAILED"]], "\ud83b\ude4d": ["\u0646", ["TAILED"]], "\ud83b\ude4e": ["\u0633", ["TAILED"]], "\ud83b\ude4f": ["\u0639", ["TAILED"]], "\ud83b\ude51": ["\u0635", ["TAILED"]], "\ud83b\ude52": ["\u0642", ["TAILED"]], "\ud83b\ude54": ["\u0634", ["TAILED"]], "\ud83b\ude57": ["\u062e", ["TAILED"]], "\ud83b\ude59": ["\u0636", ["TAILED"]], "\ud83b\ude5b": ["\u063a", ["TAILED"]], "\ud83b\ude5d": ["\u06ba", ["TAILED", "DOTLESS"]], "\ud83b\ude5f": ["\u066f", ["TAILED", "DOTLESS"]], "\ud83b\ude61": ["\u0628", ["STRETCHED"]], "\ud83b\ude62": ["\u062c", ["STRETCHED"]], "\ud83b\ude64": ["\u0647", ["STRETCHED"]], "\ud83b\ude67": ["\u062d", ["STRETCHED"]], "\ud83b\ude68": ["\u0637", ["STRETCHED"]], "\ud83b\ude69": ["\u064a", ["STRETCHED"]], "\ud83b\ude6a": ["\u0643", ["STRETCHED"]], "\ud83b\ude6c": ["\u0645", ["STRETCHED"]], "\ud83b\ude6d": ["\u0646", ["STRETCHED"]], "\ud83b\ude6e": ["\u0633", ["STRETCHED"]], "\ud83b\ude6f": ["\u0639", ["STRETCHED"]], "\ud83b\ude70": ["\u0641", ["STRETCHED"]], "\ud83b\ude71": ["\u0635", ["STRETCHED"]], "\ud83b\ude72": ["\u0642", ["STRETCHED"]], "\ud83b\ude74": ["\u0634", ["STRETCHED"]], "\ud83b\ude75": ["\u062a", ["STRETCHED"]], "\ud83b\ude76": ["\u062b", ["STRETCHED"]], "\ud83b\ude77": ["\u062e", ["STRETCHED"]], "\ud83b\ude79": ["\u0636", ["STRETCHED"]], "\ud83b\ude7a": ["\u0638", ["STRETCHED"]], "\ud83b\ude7b": ["\u063a", ["STRETCHED"]], "\ud83b\ude7c": ["\u066e", ["STRETCHED", "DOTLESS"]], "\ud83b\ude7e": ["\u06a1", ["STRETCHED", "DOTLESS"]], "\ud83b\ude80": ["\u0627", ["LOOPED"]], "\ud83b\ude81": ["\u0628", ["LOOPED"]], "\ud83b\ude82": ["\u062c", ["LOOPED"]], "\ud83b\ude83": ["\u062f", ["LOOPED"]], "\ud83b\ude84": ["\u0647", ["LOOPED"]], "\ud83b\ude85": ["\u0648", ["LOOPED"]], "\ud83b\ude86": ["\u0632", ["LOOPED"]], "\ud83b\ude87": ["\u062d", ["LOOPED"]], "\ud83b\ude88": ["\u0637", ["LOOPED"]], "\ud83b\ude89": ["\u064a", ["LOOPED"]], "\ud83b\ude8b": ["\u0644", ["LOOPED"]], "\ud83b\ude8c": ["\u0645", ["LOOPED"]], "\ud83b\ude8d": ["\u0646", ["LOOPED"]], "\ud83b\ude8e": ["\u0633", ["LOOPED"]], "\ud83b\ude8f": ["\u0639", ["LOOPED"]], "\ud83b\ude90": ["\u0641", ["LOOPED"]], "\ud83b\ude91": ["\u0635", ["LOOPED"]], "\ud83b\ude92": ["\u0642", ["LOOPED"]], "\ud83b\ude93": ["\u0631", ["LOOPED"]], "\ud83b\ude94": ["\u0634", ["LOOPED"]], "\ud83b\ude95": ["\u062a", ["LOOPED"]], "\ud83b\ude96": ["\u062b", ["LOOPED"]], "\ud83b\ude97": ["\u062e", ["LOOPED"]], "\ud83b\ude98": ["\u0630", ["LOOPED"]], "\ud83b\ude99": ["\u0636", ["LOOPED"]], "\ud83b\ude9a": ["\u0638", ["LOOPED"]], "\ud83b\ude9b": ["\u063a", ["LOOPED"]], "\ud83b\udea1": ["\u0628", ["DOUBLE-STRUCK"]], "\ud83b\udea2": ["\u062c", ["DOUBLE-STRUCK"]], "\ud83b\udea3": ["\u062f", ["DOUBLE-STRUCK"]], "\ud83b\udea5": ["\u0648", ["DOUBLE-STRUCK"]], "\ud83b\udea6": ["\u0632", ["DOUBLE-STRUCK"]], "\ud83b\udea7": ["\u062d", ["DOUBLE-STRUCK"]], "\ud83b\udea8": ["\u0637", ["DOUBLE-STRUCK"]], "\ud83b\udea9": ["\u064a", ["DOUBLE-STRUCK"]], "\ud83b\udeab": ["\u0644", ["DOUBLE-STRUCK"]], "\ud83b\udeac": ["\u0645", ["DOUBLE-STRUCK"]], "\ud83b\udead": ["\u0646", ["DOUBLE-STRUCK"]], "\ud83b\udeae": ["\u0633", ["DOUBLE-STRUCK"]], "\ud83b\udeaf": ["\u0639", ["DOUBLE-STRUCK"]], "\ud83b\udeb0": ["\u0641", ["DOUBLE-STRUCK"]], "\ud83b\udeb1": ["\u0635", ["DOUBLE-STRUCK"]], "\ud83b\udeb2": ["\u0642", ["DOUBLE-STRUCK"]], "\ud83b\udeb3": ["\u0631", ["DOUBLE-STRUCK"]], "\ud83b\udeb4": ["\u0634", ["DOUBLE-STRUCK"]], "\ud83b\udeb5": ["\u062a", ["DOUBLE-STRUCK"]], "\ud83b\udeb6": ["\u062b", ["DOUBLE-STRUCK"]], "\ud83b\udeb7": ["\u062e", ["DOUBLE-STRUCK"]], "\ud83b\udeb8": ["\u0630", ["DOUBLE-STRUCK"]], "\ud83b\udeb9": ["\u0636", ["DOUBLE-STRUCK"]], "\ud83b\udeba": ["\u0638", ["DOUBLE-STRUCK"]], "\ud83b\udebb": ["\u063a", ["DOUBLE-STRUCK"]], "\u00b5": ["\u03bc", []], "\u2107": ["E", []], "\u210e": ["h", []], "\u210f": ["hbar", []], "\u2126": ["omega", []], "\u2127": ["ohm", []], "\u212b": ["A", ["RING"]], "\u2102": ["C", ["DOUBLE-STRUCK"]], "\u210d": ["H", ["DOUBLE-STRUCK"]], "\u2115": ["N", ["DOUBLE-STRUCK"]], "\u2119": ["P", ["DOUBLE-STRUCK"]], "\u211a": ["Q", ["DOUBLE-STRUCK"]], "\u211d": ["R", ["DOUBLE-STRUCK"]], "\u2124": ["Z", ["DOUBLE-STRUCK"]], "\u213c": ["pi", ["DOUBLE-STRUCK"]], "\u213d": ["gamma", ["DOUBLE-STRUCK"]], "\u213e": ["gamma", ["DOUBLE-STRUCK"]], "\u213f": ["Pi", []], "\u2145": ["D", ["DOUBLE-STRUCK", "ITALIC"]], "\u2146": ["d", ["DOUBLE-STRUCK", "ITALIC"]], "\u2147": ["e", ["DOUBLE-STRUCK", "ITALIC"]], "\u2148": ["i", ["DOUBLE-STRUCK", "ITALIC"]], "\u2149": ["j", ["DOUBLE-STRUCK", "ITALIC"]], "\u210a": ["g", ["SCRIPT"]], "\u210b": ["H", ["SCRIPT"]], "\u2110": ["I", ["SCRIPT"]], "\u2112": ["L", ["SCRIPT"]], "\u2113": ["l", ["SCRIPT"]], "\u211b": ["R", ["SCRIPT"]], "\u212c": ["B", ["SCRIPT"]], "\u212f": ["e", ["SCRIPT"]], "\u2130": ["E", ["SCRIPT"]], "\u2131": ["F", ["SCRIPT"]], "\u2133": ["M", ["SCRIPT"]], "\u2134": ["o", ["SCRIPT"]], "\u210c": ["H", ["BLACK-LETTER"]], "\u2111": ["I", ["BLACK-LETTER"]], "\u211c": ["R", ["BLACK-LETTER"]], "\u2128": ["Z", ["BLACK-LETTER"]], "\u212d": ["C", ["BLACK-LETTER"]], "\u2135": ["alef", []], "\u2136": ["bet", []], "\u2137": ["gimel", []], "\u2138": ["dalet", []], "\u221e": ["infinity", []], "\u2205": ["emptyset", []], "\u29b0": ["emptyset", []]}, "symbols": {"~": ["~", []], "\u00ac": ["not", []], "\u00d7": ["*", []], "\u00f7": ["/", []], "\u2208": ["in", []], "\u2213": ["pm", []], "\u2227": ["and", []], "\u2228": ["or", []], "\u00b1": ["pm", []], "\u0606": ["cube_root", []], "\u0607": ["fourth_root", []], "\u2044": ["/", []], "\u2052": ["-", []], "\u208b": ["-", []], "\u2118": ["P", ["SCRIPT"]], "\u2140": ["sum", ["DOUBLE-STRUCK"]], "\u2200": ["forall", []], "\u2201": ["complement", []], "\u2202": ["pdiff", []], "\u2203": ["exists", []], "\u2204": ["not exists", []], "\u2206": ["increment", []], "\u2207": ["nabla", []], "\u2209": ["not in", []], "\u220a": ["in", []], "\u220b": ["contains", []], "\u220c": ["not contains", []], "\u220d": ["contains", []], "\u220f": ["product", []], "\u2210": ["coproduct", []], "\u2211": ["sum", []], "\u2212": ["-", []], "\u2214": ["+", ["DOT"]], "\u2215": ["/", []], "\u2216": ["setminus", []], "\u2217": ["*", []], "\u2218": ["circ", []], "\u2219": ["cdot", []], "\u221a": ["sqrt", []], "\u221b": ["cube_root", []], "\u221c": ["fourth_root", []], "\u221d": ["propto", []], "\u221f": ["right_angle", []], "\u2220": ["angle", []], "\u2223": ["divides", []], "\u2224": ["not divides", []], "\u2225": ["parallel_to", []], "\u2226": ["not parallel_to", []], "\u2229": ["intersection", []], "\u222a": ["union", []], "\u222b": ["integral", []], "\u222e": ["contour_integral", []], "\u222f": ["surface_integral", []], "\u2230": ["volume_integral", []], "\u2231": ["clockwise_integral", []], "\u2232": ["clockwise_contour_integral", []], "\u2233": ["anticlockwise_contour_integral", []], "\u2234": ["therefore", []], "\u2235": ["because", []], "\u2236": [":", []], "\u2237": ["::", []], "\u2238": ["-", ["DOT"]], "\u223c": ["~", []], "\u223d": ["~", []], "\u2240": ["wreath", []], "\u2241": ["not ~", []], "\u2245": ["approx", []], "\u2260": ["<>", []], "\u2261": ["identical", []], "\u2262": ["not identical", []], "\u227a": ["prec", []], "\u227b": ["succ", []], "\u227c": ["prec_eq", []], "\u227d": ["succ_eq", []], "\u2280": ["not prec", []], "\u2281": ["not succ", []], "\u2282": ["subset", []], "\u2283": ["superset", []], "\u2284": ["not subset", []], "\u2285": ["not superset", []], "\u2286": ["subset_eq", []], "\u2287": ["superset_eq", []], "\u2288": ["not subset_eq", []], "\u2289": ["not superset_eq", []], "\u228a": ["subset_neq", []], "\u228b": ["superset_neq", []], "\u2293": ["cap", ["SQUARE"]], "\u2294": ["cup", ["SQUARE"]], "\u2295": ["+", ["CIRCLE"]], "\u2296": ["-", ["CIRCLE"]], "\u2297": ["*", ["CIRCLE"]], "\u2298": ["/", ["CIRCLE"]], "\u2299": ["cdot", ["CIRCLE"]], "\u229a": ["circ", ["CIRCLE"]], "\u229b": ["*", ["CIRCLE"]], "\u229c": ["=", ["CIRCLE"]], "\u229d": ["-", ["CIRCLE"]], "\u229e": ["+", ["SQUARE"]], "\u229f": ["-", ["SQUARE"]], "\u22a0": ["*", ["SQUARE"]], "\u22a1": ["cdot", ["SQUARE"]], "\u22b0": ["prec", ["RELATION"]], "\u22b1": ["prec", ["RELATION"]], "\u22b2": ["normal_subgroup", []], "\u22b3": ["contains_normal_subgroup", []], "\u22b4": ["normal_subgroup_eq", []], "\u22b5": ["contains_normal_subgroup_eq", []], "\u22bb": ["xor", []], "\u22bc": ["nand", []], "\u22bd": ["nor", []], "\u22be": ["right_angle ARC", []], "\u22bf": ["right_triangle", []], "\u22c0": ["and", []], "\u22c1": ["or", []], "\u22c2": ["intersection", []], "\u22c3": ["union", []], "\u22c4": ["diamond", []], "\u22c5": ["cdot", []], "\u22c6": ["star", []], "\u22c7": ["divide_times", []], "\u22c8": ["bowtie", []], "\u22ce": ["or", ["CURLY"]], "\u22cf": ["and", ["CURLY"]], "\u22d0": ["subset", ["DOUBLE"]], "\u22d1": ["superset", ["DOUBLE"]], "\u22d2": ["intersection", ["DOUBLE"]], "\u22d3": ["union", ["DOUBLE"]], "\u2264": ["<=", []], "\u2265": [">=", []], "\u22d6": ["<", ["DOT"]], "\u22d7": [">", ["DOT"]], "\u22dc": ["<=", []], "\u22dd": [">=", []], "\u22de": ["prec_eq", []], "\u22df": ["succ_eq", []], "\u22e0": ["not prec_eq", []], "\u22e1": ["not succ_eq", []], "\u22ea": ["not normal_subgroup", []], "\u22eb": ["not contains_normal_subgroup", []], "\u22ec": ["not normal_subgroup_eq", []], "\u22ed": ["not contains_normal_subgroup_eq", []], "\u27c0": ["angle", ["THREE-DIMENSIONAL"]], "\u27c2": ["perpendicular", []], "\u27c3": ["subset", ["OPEN"]], "\u27c4": ["superset", ["OPEN"]], "\u27c7": ["or", ["DOT"]], "\u27cc": ["/", []], "\u27ce": ["and", ["SQUARE"]], "\u27cf": ["or", ["SQUARE"]], "\u27d1": ["and", ["DOT"]], "\u27d2": ["in", ["UPWARDS"]], "\u27f9": ["implies", []], "\u27fa": ["iff", []], "\u299c": ["right_angle", ["SQUARE"]], "\u299d": ["right_angle", ["DOT"]], "\u299e": ["angle", []], "\u299f": ["angle", ["ACUTE"]], "\u29b6": ["|", ["CIRCLE"]], "\u29b7": ["parallel_to", ["CIRCLE"]], "\u29b8": ["/", ["CIRCLE", "REVERSE"]], "\u29b9": ["perpendicular", ["CIRCLE"]], "\u29c0": ["<", ["CIRCLE"]], "\u29c1": [">", ["CIRCLE"]], "\u29c6": ["*", ["SQUARE"]], "\u29c7": ["circ", ["SQUARE"]], "\u29fa": ["+", ["DOUBLE"]], "\u29fb": ["+", ["TRIPLE"]], "\u2a00": ["dot", ["CIRCLE"]], "\u2a01": ["+", ["CIRCLE"]], "\u2a02": ["*", ["CIRCLE"]], "\u2a05": ["intersection", []], "\u2a06": ["union", ["SQUARE"]], "\u2a07": ["and", ["DOUBLE"]], "\u2a08": ["or", ["DOUBLE"]], "\u2a09": ["*", []], "\u2a1d": ["join", []], "\u2a2f": ["cross", []], "\u2a33": ["smash", []], "\u2a51": ["and", ["DOT"]], "\u2a52": ["or", ["DOT"]], "\u2a53": ["and", ["DOUBLE"]], "\u2a54": ["or", ["DOUBLE"]], "\u2a57": ["or", ["SLOPING"]], "\u2a58": ["and", ["SLOPING"]], "\u2a66": ["=", ["DOT"]], "\u2a67": ["identical", ["DOT"]], "\u2a6a": ["~", ["DOT"]], "\u2a6d": ["congruent", ["DOT"]], "\u2aaa": ["<", []], "\u2aab": [">", []], "\u2aac": ["<=", []], "\u2aad": [">=", []], "\u2abb": ["prec", ["DOUBLE"]], "\u2abc": ["succ", ["DOUBLE"]], "\u2abd": ["subset", ["DOT"]], "\u2abe": ["superset", ["DOT"]], "\u2acf": ["subset", ["CLOSED"]], "\u2ad0": ["superset", ["CLOSED"]], "\u2ad1": ["subset_eq", ["CLOSED"]], "\u2ad2": ["superset_eq", ["CLOSED"]], "\u2ad9": ["in", ["DOWNWARDS"]], "\uff5e": ["~", []], "\ud835\udec1": ["nabla", ["BOLD"]], "\ud835\udedb": ["pdiff", ["BOLD"]], "\ud835\udefb": ["nabla", ["ITALIC"]], "\ud835\udf15": ["pdiff", ["ITALIC"]], "\ud835\udf35": ["nabla", ["BOLD", "ITALIC"]], "\ud835\udf4f": ["pdiff", ["BOLD", "ITALIC"]], "\ud835\udf6f": ["nabla", ["SANS-SERIF", "BOLD"]], "\ud835\udf89": ["pdiff", ["SANS-SERIF", "BOLD"]], "\ud835\udfa9": ["nabla", ["SANS-SERIF", "BOLD", "ITALIC"]], "\ud835\udfc3": ["pdiff", ["SANS-SERIF", "BOLD", "ITALIC"]], "\u02c6": ["^", []], "\u00b7": ["*", []], "\u0387": ["*", []], "\u055a": ["'", []], "\u055c": ["!", []], "\u055d": [",", []], "\u055e": ["?", []], "\u060c": [",", []], "\u060d": [",", []], "\u061b": [";", []], "\u061f": ["?", []], "\u066a": ["%", []], "\u066d": ["*", []], "\u0704": [":", []], "\u0705": [":", []], "\u0706": [":", []], "\u0707": [":", []], "\u0708": [":", []], "\u0709": [":", []], "\u07f8": [",", []], "\u07f9": ["!", []], "\u1363": [",", []], "\u1364": [";", []], "\u1365": [":", []], "\u1366": [":", []], "\u1367": ["?", []], "\u16eb": ["*", []], "\u16ec": [":", []], "\u16ed": ["+", []], "\u1802": [",", []], "\u1804": [":", []], "\u1808": [",", []], "\u180a": ["-", []], "\u1944": ["!", []], "\u1945": ["?", []], "\u1c3d": ["*", []], "\u2022": ["*", []], "\u2027": ["*", []], "\u2032": ["'", []], "\u2035": ["'", []], "\u2043": ["-", []], "\u204e": ["*", []], "\u204f": [";", []], "\u2e31": ["*", []], "\u2e32": [",", []], "\u2e33": ["*", []], "\u2e34": [",", []], "\u2e35": [";", []], "\ua60d": [",", []], "\ua60f": ["?", []], "\ua6f4": [":", []], "\ua6f5": [",", []], "\ua6f6": [";", []], "\ua6f7": ["?", []], "\ufe11": [",", []], "\uff65": ["*", []], "\ud802\udd1f": ["*", []]}, "punctuation": {"\u00b7": ["*", []], "\u0387": ["*", []], "\u055a": ["'", []], "\u055c": ["!", []], "\u055d": [",", []], "\u055e": ["?", []], "\u060c": [",", []], "\u060d": [",", []], "\u061b": [";", []], "\u061f": ["?", []], "\u066a": ["%", []], "\u066d": ["*", []], "\u0704": [":", []], "\u0705": [":", []], "\u0706": [":", []], "\u0707": [":", []], "\u0708": [":", []], "\u0709": [":", []], "\u07f8": [",", []], "\u07f9": ["!", []], "\u1363": [",", []], "\u1364": [";", []], "\u1365": [":", []], "\u1366": [":", []], "\u1367": ["?", []], "\u16eb": ["*", []], "\u16ec": [":", []], "\u16ed": ["+", []], "\u1802": [",", []], "\u1804": [":", []], "\u1808": [",", []], "\u180a": ["-", []], "\u1944": ["!", []], "\u1945": ["?", []], "\u1c3d": ["*", []], "\u2022": ["*", []], "\u2027": ["*", []], "\u2032": ["'", []], "\u2035": ["'", []], "\u2043": ["-", []], "\u204e": ["*", []], "\u204f": [";", []], "\u2e31": ["*", []], "\u2e32": [",", []], "\u2e33": ["*", []], "\u2e34": [",", []], "\u2e35": [";", []], "\ua60d": [",", []], "\ua60f": ["?", []], "\ua6f4": [":", []], "\ua6f5": [",", []], "\ua6f6": [";", []], "\ua6f7": ["?", []], "\ufe11": [",", []], "\uff65": ["*", []], "\ud802\udd1f": ["*", []]}, "brackets": {"\u2768": ["(", []], "\u276a": ["(", []], "\u2774": ["{", []], "\u27e6": ["[", []], "\u27ee": ["(", []], "\u2983": ["{", []], "\u2985": ["(", []], "\u2e28": ["(", []], "\u301a": ["[", []], "\ufd3f": ["]", []], "\uff5f": ["(", []], "\u2769": [")", []], "\u276b": [")", []], "\u2775": ["}", []], "\u27e7": ["]", []], "\u27ef": [")", []], "\u2984": ["}", []], "\u2986": [")", []], "\u2e29": [")", []], "\u301b": ["]", []], "\ufd3e": ["(", []], "\uff60": [")", []]}}
});
;
/*
Copyright 2011-14 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Convenience functions, extensions to javascript built-ins, etc. Provides {@link Numbas.util}. Includes es5-shim.js */
Numbas.queueScript('util', ['base', 'math', 'parsel'], function() {
/** @namespace Numbas.util */
var util = Numbas.util = /** @lends Numbas.util */ {
    /** Run the given function when the document is ready.
     *
     * @param {Function} fn
     */
    document_ready: function(fn) {
        if(document.readyState == 'complete') {
            setTimeout(fn, 1);
        } else {
            document.addEventListener('readystatechange', function(e) {
                if(document.readyState == 'complete') {
                    setTimeout(fn, 1);
                }
            });
        }
    },

    /** Derive type B from A (class inheritance, really)
     *
     * B's prototype supercedes A's.
     *
     * @param {Function} a - the constructor for the parent class
     * @param {Function} b - a constructor to be called after `a`'s constructor is done.
     * @param {boolean} extendMethods - if `true`, the methods of the new type are constructed so that the method from type A is applied, then the method from type B. Nothing is returned.
     * @returns {Function} a constructor for the derived class
     */
    extend: function(a, b, extendMethods) {
        var c = function() {
            a.apply(this, arguments);
            return b.apply(this, arguments);
        };
        var x;
        for(x in a.prototype) {
            c.prototype[x] = a.prototype[x];
        }
        for(x in b.prototype) {
            c.prototype[x] = b.prototype[x];
        }
        if(extendMethods) {
            for(x in a.prototype) {
                if(typeof(a.prototype[x]) == 'function' && b.prototype[x]) {
                    c.prototype[x] = Numbas.util.extend(a.prototype[x], b.prototype[x]);
                }
            }
        }
        return c;
    },
    /** Extend `destination` with all the properties from subsequent arguments.
     * `undefined` values are not copied over.
     * Replacement for jQuery.extend. Modified from https://stackoverflow.com/a/11197343
     * Object.assign doesn't behave the same way - it copies over `undefined`.
     *
     * @param {object} destination
     * @returns {object}
     */
    extend_object: function(destination) {
        for(let i = 1; i < arguments.length; i++) {
            for(const key in arguments[i]) {
                if(Object.hasOwn(arguments[i], key) && arguments[i][key] !== undefined) {
                    destination[key] = arguments[i][key];
                }
            }
        }
        return destination;
    },
    /** Extend `destination` with all the properties from subsequent arguments, and recursively extend objects that both properties have under the same key.
     *
     * @param {object} destination
     * @returns {object}
     */
    deep_extend_object: function(destination) {
        for(let i = 1; i < arguments.length; i++) {
            const arg = arguments[i];
            for(const key of Object.keys(arg)) {
                if(arg[key] === undefined) {
                    continue;
                }
                if(typeof arg[key] === 'object' && typeof destination[key] === 'object') {
                    util.deep_extend_object(destination[key], arg[key]);
                } else {
                    destination[key] = arg[key];
                }
            }
        }
        return destination;
    },
    /** Clone an array, with array elements copied too.
     * Array.splice() will create a copy of an array, but the elements are the same objects, which can cause fruity bugs.
     * This function clones the array elements as well, so there should be no side-effects when operating on the cloned array.
     *
     * @param {Array} arr
     * @param {boolean} deep - if true, do a deep copy of each element
     * @see Numbas.util.copyobj
     * @returns {Array}
     */
    copyarray: function(arr, deep) {
        arr = arr.slice();
        if(deep) {
            for(let i = 0;i < arr.length;i++) {
                arr[i] = util.copyobj(arr[i], deep);
            }
        }
        return arr;
    },
    /** Clone an object.
     *
     * @param {object} obj
     * @param {boolean} deep - if true, each property is cloned as well (recursively) so there should be no side-effects when operating on the cloned object.
     * @returns {object}
     */
    copyobj: function(obj, deep) {
        switch(typeof(obj)) {
        case 'object':
                if(obj === null) {
                    return obj;
                }
            if(obj.length !== undefined) {
                return util.copyarray(obj, deep);
            } else {
                var newobj = {};
                for(const x in obj) {
                    if(deep) {
                        newobj[x] = util.copyobj(obj[x], deep);
                    } else {
                        newobj[x] = obj[x];
                    }
                }
                return newobj;
            }
        default:
            return obj;
        }
    },
    /** Shallow copy an object into an already existing object - add all `src`'s properties to `dest`.
     *
     * @param {object} src
     * @param {object} dest
     */
    copyinto: function(src, dest) {
        for(const x in src) {
            if(dest[x] === undefined) {
                dest[x] = src[x]
            }
        }
    },
    /** Generic equality test on {@link Numbas.jme.token}s.
     *
     * @param {Numbas.jme.token} a
     * @param {Numbas.jme.token} b
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @see Numbas.util.equalityTests
     * @returns {boolean}
     */
    eq: function(a, b, scope) {
        if(a.type != b.type) {
            var type = Numbas.jme.findCompatibleType(a.type, b.type);
            if(type) {
                a = Numbas.jme.castToType(a, type);
                b = Numbas.jme.castToType(b, type);
            } else {
                return false;
            }
        }
        if(a.type in util.equalityTests) {
            return util.equalityTests[a.type](a, b, scope);
        } else {
            throw(new Numbas.Error('util.equality not defined for type', {type:a.type}));
        }
    },

    /** Functions to decide if two tokens of the same type are equal.
     * Dictionary mapping token type name to function.
     *
     * @see Numbas.util.eq
     */
    equalityTests: {
        'boolean': function(a, b) {
            return a.value == b.value;
        },
        'dict': function(a, b, scope) {
            var seen = {};
            for(const x in a.value) {
                seen[x] = true;
                if(b.value[x] === undefined || !util.eq(a.value[x], b.value[x], scope)) {
                    return false;
                }
            }
            for(const x in a.value) {
                if(seen[x]) {
                    continue;
                }
                if(!util.eq(a.value[x], b.value[x], scope)) {
                    return false;
                }
            }
            return true;
        },
        'expression': function(a, b, scope) {
            return Numbas.jme.treesSame(a.tree, b.tree, scope);
        },
        'function': function(a, b) {
            return a.name == b.name;
        },
        'html': function(a, b) {
            return a.value[0] && b.value[0] && a.value[0].outerHTML == b.value[0].outerHTML;
        },
        'keypair': function(a, b) {
            return a.key == b.key;
        },
        'list': function(a, b, scope) {
            if(!a.value || !b.value) {
                return !a.value && !b.value;
            }
            return a.value.length == b.value.length && a.value.filter(function(ae, i) {
                return !util.eq(ae, b.value[i], scope)
            }).length == 0;
        },
        'matrix': function(a, b) {
            return Numbas.matrixmath.eq(a.value, b.value);
        },
        'name': function(a, b, scope) {
            return Numbas.jme.normaliseName(a.name, scope) == Numbas.jme.normaliseName(b.name, scope);
        },
        'nothing': function(a, b) {
            return true;
        },
        'number': function(a, b) {
            return Numbas.math.eq(a.value, b.value);
        },
        'integer': function(a, b) {
            return Numbas.math.eq(a.value, b.value);
        },
        'rational': function(a, b) {
            return a.value.equals(b.value);
        },
        'decimal': function(a, b) {
            return a.value.equals(b.value);
        },
        'op': function(a, b) {
            return a.name == b.name;
        },
        'range': function(a, b) {
            return a.value[0] == b.value[0] && a.value[1] == b.value[1] && a.value[2] == b.value[2];
        },
        'set': function(a, b, scope) {
            return Numbas.setmath.eq(a.value, b.value, scope);
        },
        'string': function(a, b) {
            return a.value == b.value;
        },
        'vector': function(a, b) {
            return Numbas.vectormath.eq(a.value, b.value);
        }
    },
    /** Generic inequality test on {@link Numbas.jme.token}s.
     *
     * @param {Numbas.jme.token} a
     * @param {Numbas.jme.token} b
     * @param {Numbas.jme.Scope} scope - The scope to use for normalising names.
     * @returns {boolean}
     * @see Numbas.util.eq
     */
    neq: function(a, b, scope) {
        return !util.eq(a, b, scope);
    },

    /** Are the given objects equal?
     * False if they're of different types.
     * If they're both arrays, uses {@link Numbas.util.arraysEqual}.
     * If they're both objects, true if every key in `b` is also in `a`, and `a[k]` is equal to `b[k]` for every `k` in `a`.
     * Otherwise, uses JavaScript's equality test.
     *
     * @param {*} a
     * @param {*} b
     * @returns {boolean}
     */
    objects_equal: function(a, b) {
        if(a === b) {
            return true;
        }
        if(typeof(a) != typeof(b)) {
            return false;
        }
        if(typeof(a) == 'object') {
            if(a === null || b === null) {
                return a === b;
            }
            if(Array.isArray(a) && Array.isArray(b)) {
                return util.arraysEqual(a, b);
            } else {
                return Object.keys(a).every(function(k) {
                    return util.objects_equal(a[k], b[k])
                }) && Object.keys(b).every(function(k) {
                    return Object.hasOwn(a, k);
                });
            }
        }
        return a == b;
    },
    /** Are two arrays equal? True if their elements are all equal.
     *
     * @param {Array} a
     * @param {Array} b
     * @returns {boolean}
     */
    arraysEqual: function(a, b) {
        if(!Array.isArray(a) || !Array.isArray(b)) {
            return false;
        }
        if(a.length != b.length) {
            return false;
        }
        var l = a.length;
        for(let i = 0;i < l;i++) {
            if(Array.isArray(a[i])) {
                if(!Array.isArray(b[i])) {
                    return false;
                } else if(!util.arraysEqual(a[i], b[i])) {
                    return false;
                }
            } else {
                if(!util.objects_equal(a[i], b[i])) {
                    return false;
                }
            }
        }
        return true;
    },
    /** Filter out values in `exclude` from `list`.
     *
     * @param {Numbas.jme.types.TList} list
     * @param {Numbas.jme.types.TList} exclude
     * @param {Numbas.jme.Scope} scope - The scope to use for establishing equality of tokens.
     * @returns {Array}
     */
    except: function(list, exclude, scope) {
        return list.filter(function(l) {
            for(let i = 0;i < exclude.length;i++) {
                if(util.eq(l, exclude[i], scope)) {
                    return false;
                }
            }
            return true;
        });
    },
    /** Return a copy of the input list with duplicates removed.
     *
     * @param {Array} list
     * @param {Numbas.jme.Scope} scope - The scope to use for establishing equality of tokens.
     * @returns {Array}
     * @see Numbas.util.eq
     */
    distinct: function(list, scope) {
        if(list.length == 0) {
            return [];
        }
        var out = [list[0]];
        for(let i = 1;i < list.length;i++) {
            var got = false;
            for(let j = 0;j < out.length;j++) {
                if(util.eq(list[i], out[j], scope)) {
                    got = true;
                    break;
                }
            }
            if(!got) {
                out.push(list[i]);
            }
        }
        return out;
    },
    /** Is value in the list?
     *
     * @param {Array} list
     * @param {Numbas.jme.token} value
     * @param {Numbas.jme.Scope} scope - The scope to use for establishing equality of tokens.
     * @returns {boolean}
     */
    contains: function(list, value, scope) {
        for(let i = 0;i < list.length;i++) {
            if(util.eq(value, list[i], scope)) {
                return true;
            }
        }
        return false;
    },
    /** Test if parameter is an integer.
     *
     * @param {object} i
     * @returns {boolean}
     */
    isInt: function(i) {
        return parseInt(i, 10) == i;
    },
    /** Test if parameter is a float.
     *
     * @param {object} f
     * @returns {boolean}
     */
    isFloat: function(f) {
        return parseFloat(f) == f;
    },
    /** Test if parameter is a fraction.
     *
     * @param {string} s
     * @returns {boolean}
     */
    isFraction: function(s) {
        s = s.toString().trim();
        return util.re_fraction.test(s);
    },
    /** Is `n`a number? i.e. `!isNaN(n)`, or is `n` "infinity", or if `allowFractions` is true, is `n` a fraction?
     *
     * If `styles` is given, try to put the number in standard form if it matches any of the given styles.
     *
     * @param {number|string} n
     * @param {boolean} allowFractions
     * @param {string|Array.<string>} styles - Styles of notation to allow.
     * @param {boolean} strictStyle - If false or not given, strings which do not match any of the allowed styles but are valid JavaScript number literals will be allowed. If true, these strings will return false.
     * @see Numbas.util.cleanNumber
     * @returns {boolean}
     */
    isNumber: function(n, allowFractions, styles, strictStyle) {
        if(n === undefined || n === null) {
            return false;
        }
        if(allowFractions && util.re_fraction.test(n)) {
            return true;
        }
        n = util.cleanNumber(n, styles, strictStyle);
        if(!isNaN(n)) {
            return true;
        }
        if(/-?infinity/i.test(n)) {
            return true;
        } else {
            return false;
        }
    },
    /** Wrap a list index so `-1` maps to `length-1`.
     *
     * @param {number} n
     * @param {number} size
     * @returns {number}
     */
    wrapListIndex: function(n, size) {
        if(n < 0) {
            n += size;
        }
        return n;
    },
    /** Test if parameter is a boolean - that is: a boolean literal, or any of the strings 'false','true','yes','no', case-insensitive.
     *
     * @param {object} b
     * @returns {boolean}
     */
    isBool: function(b) {
        if(b == null) {
            return false;
        }
        if(typeof(b) == 'boolean') {
            return true;
        }
        b = b.toString().toLowerCase();
        return b == 'false' || b == 'true' || b == 'yes' || b == 'no';
    },
    /** Parse a string as HTML, and return true only if it contains non-whitespace text.
     *
     * @param {string} html
     * @returns {boolean}
     */
    isNonemptyHTML: function(html) {
        if(html === undefined || html === null) {
            return false;
        }
        if(window.document) {
            var d = document.createElement('div');
            d.innerHTML = html;
            return d.textContent.trim().length > 0 || d.querySelector('img,iframe,object');
        } else {
            return html.replace(/<\/?[^>]*>/g, '').trim() != '';
        }
    },
    /** Parse parameter as a boolean. The boolean value `true` and the strings 'true' and 'yes' are parsed as the value `true`, everything else is `false`.
     *
     * @param {object} b
     * @returns {boolean}
     */
    parseBool: function(b) {
        if(!b) {
            return false;
        }
        b = b.toString().toLowerCase();
        return(b == 'true' || b == 'yes');
    },
    /** Regular expression recognising a fraction.
     *
     * @type {RegExp}
     */
    re_fraction: /^\s*(-?)\s*(\d+)\s*\/\s*(-?)\s*(\d+)\s*/,

    /**
     * Create a function `(integer,decimal) -> string` which formats a number according to the given punctuation.
     *
     * @param {string} thousands - The string used to separate powers of 1000.
     * @param {string} decimal_mark - The decimal mark character.
     * @param {boolean} [separate_decimal=false] - Should the `thousands` separator be used to separate negative powers of 1000 (that is, groups of 3 digits after the decimal point)?
     * @returns {Function}
     */
    standardNumberFormatter: function(thousands, decimal_mark, separate_decimal) {
        return function(integer, decimal) {
            var s = util.separateThousands(integer, thousands);
            if(decimal) {
                var o = '';
                if(separate_decimal) {
                    for(let i = 0;i < decimal.length;i += 3) {
                        o += (o ? thousands : '') + decimal.slice(i, i + 3);
                    }
                } else {
                    o = decimal;
                }
                s += decimal_mark + o;
            }
            return s;
        }
    },

    /** Try to match a string representing a number in any of the given styles at the start of the given string, and return both the matched text and a JavaScript number literal equivalent.
     *
     * @param {string} s - The string potentially representing a number.
     * @param {string|string[]} styles - Styles of notation to allow, e.g. `['en','si-en']`.
     * @param {boolean} [strictStyle] - If false or not given, strings which do not match any of the allowed styles but are valid JavaScript number literals will be allowed. If true, these strings will return 'NaN'.
     * @param {boolean} [mustMatchAll] - If true, then the string must contain only the matched number.
     * @returns {object|null} - `{matched, cleaned}` or `null`
     *
     * @see Numbas.util.numberNotationStyles
     */
    matchNotationStyle: function(s, styles, strictStyle, mustMatchAll) {
        var pos = 0;
        s = s.toString();
        var match_neg = /^\s*(-)?\s*/.exec(s);
        var minus = match_neg[1] || '';
        pos += match_neg[0].length;

        var matched = false;
        var cleaned = s;
        var bestpos = pos;
        if(styles !== undefined) {
            if(typeof styles == 'string') {
                styles = [styles];
            }
            for(let i = 0, l = styles.length;i < l;i++) {
                var style = util.numberNotationStyles[styles[i]];
                if(!style) {
                    continue;
                }
                var re = style.re;
                var m;
                if(re && (m = re.exec(s.slice(pos))) && (!mustMatchAll || s.slice(pos + m[0].length).trim() == '')) {
                    matched = true;
                    var mcleaned;
                    var mpos = pos + m[0].length;
                    if(style.clean) {
                        mcleaned = minus + style.clean(m);
                    } else {
                        var integer = m[1].replace(/\D/g, '');
                        if(m[2]) {
                            var decimal = m[2].replace(/\D/g, '');
                            mcleaned = minus + integer + '.' + decimal
                        } else {
                            mcleaned = minus + integer;
                        }
                        mpos = pos + m[0].length;
                    }
                    if(mpos > bestpos) {
                        bestpos = mpos;
                        cleaned = mcleaned;
                    }
                }
            }
        }
        pos = bestpos;
        if(strictStyle && !matched) {
            cleaned = 'NaN';
        }
        return {
            matched: matched ? s.slice(0, pos) : '',
            cleaned: cleaned
        }
    },

    /** Clean a string potentially representing a number.
     * Remove space, and then try to identify a notation style.
     *
     * If `styles` is given, `s` will be tested against the given styles. If it matches, the string will be rewritten using the matched integer and decimal parts, with punctuation removed and the decimal point changed to a dot.
     *
     * @param {string} s - The string potentially representing a number.
     * @param {string|string[]} styles - Styles of notation to allow, e.g. `['en','si-en']`.
     * @param {boolean} [strictStyle] - If false or not given, strings which do not match any of the allowed styles but are valid JavaScript number literals will be allowed. If true, these strings will return 'NaN'.
     * @returns {string}
     *
     * @see Numbas.util.numberNotationStyles
     */
    cleanNumber: function(s, styles, strictStyle) {
        var result = util.matchNotationStyle(s, styles, strictStyle, true);
        return result.cleaned;
    },

    /** Format a string representing a number given in "plain" notation: an optional minus sign followed by digits, and optionally a dot and more digits.
     *
     * @param {string} s - The string representing a number.
     * @param {string} style_name - The style of notation to use.
     * @param {string} [syntax="plain"] - The syntax to use, either "plain" for plain text, or "latex", for LaTeX.
     * @returns {string}
     */
    formatNumberNotation: function(s, style_name, syntax) {
        var match_neg = /^(-)?(.*)/.exec(s);
        var minus = match_neg[1] || '';
        var bits = match_neg[2].split('.');
        var integer = bits[0];
        var decimal = bits[1];
        var style = util.numberNotationStyles[style_name];
        syntax = syntax || 'plain';
        if(!style.format[syntax]) {
            throw(new Error('util.formatNumberNotation.unrecognised syntax', {syntax: syntax}));
        }
        var formatted = style.format[syntax](integer, decimal);
        return minus + formatted;
    },

    /** Parse a number - either as a `Decimal`, or parse a fraction.
     *
     * @param {string} s
     * @param {boolean} allowFractions - Are fractions of the form `a/b` (`a` and `b` integers without punctuation) allowed?
     * @param {string|string[]} styles - Styles of notation to allow.
     * @param {boolean} strictStyle - If false or not given, strings which do not match any of the allowed styles but are valid JavaScript number literals will be allowed. If true, these strings will return NaN.
     * @see Numbas.util.cleanNumber
     * @returns {Decimal}
     */
    parseDecimal: function(s, allowFractions, styles, strictStyle) {
        var cleaned_s = util.cleanNumber(s, styles, strictStyle);
        var m;
        if(util.isFloat(cleaned_s)) {
            return new Decimal(cleaned_s);
        } else if(s.toLowerCase() == 'infinity') {
            return new Decimal(Infinity);
        } else if(s.toLowerCase() == '-infinity') {
            return new Decimal(-Infinity);
        } else if(allowFractions && (m = util.parseFraction(s, true))) {
            return new Decimal(m.numerator).dividedBy(new Decimal(m.denominator));
        } else {
            return new Decimal(NaN);
        }
    },
    /** Parse a number - either parseFloat, or parse a fraction.
     *
     * @param {string} s
     * @param {boolean} allowFractions - Are fractions of the form `a/b` (`a` and `b` integers without punctuation) allowed?
     * @param {string|string[]} styles - Styles of notation to allow.
     * @param {boolean} strictStyle - If false or not given, strings which do not match any of the allowed styles but are valid JavaScript number literals will be allowed. If true, these strings will return NaN.
     * @see Numbas.util.cleanNumber
     * @returns {number}
     */
    parseNumber: function(s, allowFractions, styles, strictStyle) {
        var cleaned_s = util.cleanNumber(s, styles, strictStyle);
        var m;
        if(util.isFloat(cleaned_s)) {
            return parseFloat(cleaned_s);
        } else if(s.toLowerCase() == 'infinity') {
            return Infinity;
        } else if(s.toLowerCase() == '-infinity') {
            return -Infinity;
        } else if(allowFractions && (m = util.parseFraction(s, true))) {
            return m.numerator / m.denominator;
        } else {
            return NaN;
        }
    },

    /**
     * Parse an integer in the given base.
     * Unlike javascript's built-in `parseInt`, this returns `NaN` if an invalid character is present in the string.
     * The digits are the numerals 0 to 9, then the letters of the English alphabet.
     *
     * @param {string} s - a representation of a number.
     * @param {number} base - the base of the number's representation.
     * @returns {number}
     */
    parseInt: function(s, base) {
        s = s.toLowerCase();
        var alphabet = 'abcdefghijklmnopqrstuvwxyz';
        var digits = '0123456789';
        var acceptable_digits = (digits + alphabet).slice(0, base);
        if(!s.match(new RegExp('^[' + acceptable_digits + ']*$'))) {
            return NaN;
        }
        return parseInt(s, base);
    },

    /** A fraction.
     *
     * @typedef {object} fraction
     * @property {number} numerator - The number on top.
     * @property {number} denominator - The number on the bottom.
     */
    /** Parse a string representing an integer or fraction.
     *
     * @param {string} s
     * @param {boolean} [mustMatchAll] - If true, then the string must contain only the matched number.
     * @see Numbas.util.re_fraction
     * @returns {fraction}
     */
    parseFraction: function(s, mustMatchAll) {
        if(util.isInt(s)) {
            return {numerator:parseInt(s), denominator:1};
        }
        var m = util.re_fraction.exec(s);
        if(!m || (mustMatchAll && m[0] != s)) {
            return;
        }
        var n = parseInt(m[2]);
        n = (!!m[1] ^ !!m[3]) ? -n : n;
        var d = parseInt(m[4]);
        return {numerator:n, denominator:d};
    },

    /** Transform the given string to one containing only letters, digits and hyphens.
     * @param {string} str
     * @returns {string}
     */
    slugify: function(str) {
        if (str === undefined) {
            return '';
        }
        return (str + '').replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-]/g, '').replace(/-+/g, '-');

    },

    /** Pad string `s` on the left with a character `p` until it is `n` characters long.
     *
     * @param {string} s
     * @param {number} n
     * @param {string} p
     * @returns {string}
     */
    lpad: function(s, n, p) {
        s = s.toString();
        p = (p + '').slice(0, 1);
        while(s.length < n) {
            s = p + s;
        }
        return s;
    },
    /** Pad string `s` on the right with a character `p` until it is `n` characters long.
     *
     * @param {string} s
     * @param {number} n
     * @param {string} p
     * @returns {string}
     */
    rpad: function(s, n, p) {
        s = s.toString();
        p = (p + '').slice(0, 1);
        while(s.length < n) {
            s = s + p;
        }
        return s;
    },
    /** Replace occurences of `%s` with the extra arguments of the function.
     *
     * @example
     * formatString('hello %s %s','Mr.','Perfect')
     * // 'hello Mr. Perfect'
     * @param {string} str
     * @param {...string} value - String to substitute.
     * @returns {string}
     */
    formatString: function(str, value) {
        for(let i = 1;i < arguments.length;i++) {
            str = str.replace(/%s/, arguments[i]);
        }
        return str;
    },
    /** String representation of a time, in the format HH:MM:SS.
     *
     * @param {Date} t
     * @returns {string}
     */
    formatTime: function(t) {
        var h = t.getHours();
        var m = t.getMinutes();
        var s = t.getSeconds();
        var lpad = util.lpad;
        return t.toDateString() + ' ' + lpad(h, 2, '0') + ':' + lpad(m, 2, '0') + ':' + lpad(s, 2, '0');
    },
    /** Format an amount of currency.
     *
     * @example
     * currency(5.3,'£','p')
     * // £5.30
     * @param {number} n
     * @param {string} prefix - Symbol to use in front of currency if `abs(n) >= 1`.
     * @param {string} suffix - Symbol to use after currency if `abs(n) <= 1`.
     * @returns {string}
     */
    currency: function(n, prefix, suffix) {
        if(n < 0) {
            return '-' + util.currency(-n, prefix, suffix);
        } else if(n == 0) {
            return prefix + '0';
        }
        // convert n to a whole number of pence, as a string
        var s = Numbas.math.niceRealNumber(100 * n, {precisionType:'dp', precision:0});
        if(n >= 0.995) {
            if(n % 1 < 0.005) {
                return prefix + Numbas.math.niceRealNumber(Math.floor(n));
            } else if(n % 1 >= 0.995) {
                return prefix + Numbas.math.niceRealNumber(Math.ceil(n));
            }
            s = s.replace(/(..)$/, '.$1');   // put a dot before the last two digits, representing the pence
            return prefix + s
        } else {
            return s + suffix;
        }
    },

    /** Write a number with every three digits separated by the given separator character.
     *
     * @example
     * separateThousands(1234567.1234,',')
     * // '1,234,567.1234'
     * @param {number} n
     * @param {string} separator
     * @returns {string}
     */
    separateThousands: function(n, separator) {
        var s = n;
        if(typeof n == 'number') {
            if(n < 0) {
                return '-' + util.separateThousands(-n, separator);
            }
            s = Numbas.math.niceRealNumber(n);
        }
        var bits = s.split('.');
        var whole = bits[0];
        var frac = bits[1];
        var over = whole.length % 3;
        var out = whole.slice(0, over);
        var i = over;
        while(i < whole.length) {
            out += (out ? separator : '') + whole.slice(i, i + 3);
            i += 3;
        }
        if(frac > 0) {
            out += '.' + (frac + '');
        }
        return out;
    },
    /** Get rid of the % on the end of percentages and parse as float, then divide by 100.
     *
     * @example
     * unPercent('50%')
     * // 0.5
     * @example
     * unPercent('50')
     * // 0.5
     * @param {string} s
     * @returns {number}
     */
    unPercent: function(s) {
        return (util.parseNumber(s.replace(/%/, '')) / 100);
    },
    /** Pluralise a word.
     *
     * If `n` is not unity, return `plural`, else return `singular`.
     *
     * @param {number} n
     * @param {string} singular - String to return if `n` is +1 or -1.
     * @param {string} plural - String to returns if `n` is not +1 or -1.
     * @returns {string}
     */
    pluralise: function(n, singular, plural) {
        n = Numbas.math.precround(n, 10);
        if(n == -1 || n == 1) {
            return singular;
        } else {
            return plural;
        }
    },
    /** Make the first letter in the string a capital.
     *
     * @param {string} str
     * @returns {string}
     */
    capitalise: function(str) {
        return str.replace(/^[a-z]/, function(c) {
            return c.toUpperCase()
        });
    },
    /** Split a string up according to brackets.
     *
     * Strips out nested brackets.
     *
     * @example
     * splitbrackets('a{{b}}c','{','}')
     * // ['a','b','c']
     * @param {string} str - String to split.
     * @param {string} lb - Left bracket string.
     * @param {string} rb - Right bracket string.
     * @param {string} [nestlb] - String to replace nested left brackets with.
     * @param {string} [nestrb] - String to repalce nested right brackets with.
     * @returns {Array.<string>} - Alternating strings in brackets and strings outside: odd-numbered indices are inside brackets.
     */
    splitbrackets: function(str, lb, rb, nestlb, nestrb) {
        var length = str.length;
        nestlb = nestlb || '';
        nestrb = nestrb || '';
        var bits = [];
        var start = 0;
        var depth = 0;
        var m;
        for(let i = 0;i < length;i++) {
            if(str.charAt(i) == '\\') {
                i += 1;
                continue;
            }
            // if cursor is at a left bracket
            if(str.slice(i, i + lb.length) == lb) {
                bits.push({kind:'str', str:str.slice(start, i)});
                bits.push({kind:'lb'});
                i += lb.length - 1;
                start = i + 1;
                depth += 1;
            } else if(str.slice(i, i + rb.length) == rb) {
                bits.push({kind:'str', str:str.slice(start, i)});
                bits.push({kind:'rb'});
                i += rb.length - 1;
                start = i + 1;
                depth -= 1;
            } else if(depth > 0 && (m = re_jme_string.exec(str.slice(i)))) {
                bits.push({kind:'str', str: str.slice(start, i)});
                bits.push({kind:'jme_str', str: m[0]});
                i += m[0].length - 1;
                start = i + 1;
            }
        }
        if(start < str.length) {
            bits.push({kind:'str', str:str.slice(start)});
        }

        depth = 0;
        var out = [];
        var s = '';
        var s_plain = '';
        var s_unclosed = '';
        for(let i = 0;i < bits.length;i++) {
            switch(bits[i].kind) {
                case 'jme_str':
                    s += bits[i].str;
                    break;
                case 'str':
                    s += bits[i].str;
                    s_unclosed += bits[i].str;
                    break;
                case 'lb':
                    s_unclosed += lb;
                    if(depth == 0) {
                        s_plain = s;
                        s = '';
                    } else {
                        s += nestlb;
                    }
                    depth += 1;
                    break;
                case 'rb':
                    if(depth == 0) {
                        s += rb;
                        s_unclosed += rb;
                    } else {
                        depth -= 1;
                        if(depth > 0) {
                            s += nestrb;
                        } else {
                            out.push(s_plain);
                            out.push(s);
                            s = '';
                            s_unclosed = '';
                        }
                    }
                    break;
            }
        }
        if(s_unclosed.length) {
            out.push(s_unclosed);
        }
        return out;
    },

    /** Because XML doesn't like having ampersands hanging about, replace them with escape codes.
     *
     * @param {string} str - XML string.
     * @returns {string}
     */
    escapeHTML: function(str) {
        return str
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;")
        ;
    },
    /** Create a comparison function which sorts objects by a particular property.
     *
     * @param {Array.<string>|string} props - Name of the property (or list of names of properties) to sort by.
     * @returns {Function}
     */
    sortBy: function(props) {
        if(typeof props == 'string') {
            props = [props];
        }
        var l = props.length;
        return function(a, b) {
            for(let i = 0;i < l;i++) {
                var prop = props[i];
                if(a[prop] > b[prop]) {
                    return 1;
                } else if(a[prop] < b[prop]) {
                    return -1;
                }
            }
            return 0;
        }
    },
    /** Hash a string into a string of digits.
     *
     * From {@link http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/}.
     *
     * @param {string} str
     * @returns {string}
     */
    hashCode: function(str) {
        var hash = 0;
        var i;
        var c;
        if (str.length == 0) {
            return hash;
        }
        for (i = 0; i < str.length; i++) {
            c = str.charCodeAt(i);
            hash = ((hash << 5) - hash) + c;
        }
        if(hash < 0) {
            return '0' + (-hash);
        } else {
            return '1' + hash;
        }
    },
    /** Cartesian product of one or more lists.
     *
     * @param {Array} lists - list of arrays
     * @returns {Array}
     */
    product: function(lists) {
        if(!Array.isArray(lists)) {
            throw(new Numbas.Error("util.product.non list"));
        }
        var indexes = lists.map(function() {
            return 0
        });
        var zero = false;
        var nonArray = false;
        var lengths = lists.map(function(l) {
            if(!Array.isArray(l)) {
                nonArray = true;
            }
            if(l.length == 0) {
                zero = true;
            }
            return l.length
        });
        if(nonArray) {
            throw(new Numbas.Error("util.product.non list"));
        }
        if(zero) {
            return [];
        }
        var end = lists.length - 1;
        var out = [];
        while(indexes[0] != lengths[0]) {
            out.push(indexes.map(function(i, n) {
                return lists[n][i]
            }));
            var k = end;
            indexes[k] += 1;
            while(k > 0 && indexes[k] == lengths[k]) {
                indexes[k] = 0;
                k -= 1;
                indexes[k] += 1;
            }
        }
        return out;
    },

    /** Cartesian product of list, repeated `n` times.
     *
     * @param {Array} l
     * @param {number} n
     * @returns {Array}
     */
    cartesian_power: function(l, n) {
        var o = [[]];
        for(let i = 0;i < n;i++) {
            var no = [];
            o.forEach(function(ol) {
                l.forEach(function(x) {
                    var nl = ol.slice();
                    nl.push(x);
                    no.push(nl);
                })
            });
            o = no;
        }
        return o;
    },

    /** Zip lists together: given lists `[a,b,c,...]`, `[x,y,z,...]`, return `[[a,x],[b,y],[c,z], ...]`.
     *
     * @param {Array} lists - list of arrays
     * @returns {Array}
     */
    zip: function(lists) {
        var out = [];
        if(lists.length == 0) {
            return out;
        }
        let i = 0;
        while(true) {
            var z = [];
            for(let j = 0; j < lists.length; j++) {
                if(i < lists[j].length) {
                    z.push(lists[j][i]);
                } else {
                    return out;
                }
            }
            out.push(z);
            i += 1;
        }
    },

    /** All combinations of `r` items from given array, without replacement.
     *
     * @param {Array} list
     * @param {number} r
     * @returns {Array.<Array>}
     */
    combinations: function(list, r) {
        var indexes = [];
        for(let i = 0;i < r;i++) {
            indexes.push(i);
        }
        var length = list.length;
        var end = r - 1;
        var out = [];
        var steps = 0;
        while(steps < 1000 && indexes[0] < length + 1 - r) {
            steps += 1;
            out.push(indexes.map(function(i) {
                return list[i];
            }));
            indexes[end] += 1;
            if(indexes[end] == length) {
                var k = end;
                while(k >= 0 && indexes[k] == length + 1 - r + k) {
                    k -= 1;
                    indexes[k] += 1;
                }
                for(k = k + 1;k < r;k++) {
                    indexes[k] = indexes[k - 1] + 1;
                }
            }
        }
        return out;
    },
    /** All combinations of `r` items from given array, with replacement.
     *
     * @param {Array} list
     * @param {number} r
     * @returns {Array.<Array>}
     */
    combinations_with_replacement: function(list, r) {
        var indexes = [];
        for(let i = 0;i < r;i++) {
            indexes.push(0);
        }
        var length = list.length;
        var end = r - 1;
        var out = [];
        while(indexes[0] < length) {
            out.push(indexes.map(function(i) {
                return list[i];
            }));
            indexes[end] += 1;
            if(indexes[end] == length) {
                var k = end;
                while(k >= 0 && indexes[k] == length) {
                    k -= 1;
                    indexes[k] += 1;
                }
                for(k = k + 1;k < r;k++) {
                    indexes[k] = indexes[k - 1];
                }
            }
        }
        return out;
    },
    /** All permutations of all choices of `r` elements from list.
     *
     * Inspired by the algorithm in Python's itertools library.
     *
     * @param {Array} list - Elements to choose and permute.
     * @param {number} r - Number of elements to choose.
     * @returns {Array.<Array>}
     */
    permutations: function(list, r) {
        var n = list.length;
        if(r === undefined) {
            r = n;
        }
        if(r > n) {
            throw(new Numbas.Error('util.permutations.r bigger than n'));
        }
        var indices = [];
        var cycles = [];
        for(let i = 0;i < n;i++) {
            indices.push(i);
        }
        for(let i = n;i >= n - r + 1;i--) {
            cycles.push(i);
        }
        var out = [indices.slice(0, r).map(function(v) {
            return list[v]
        })];
        while(n) {
            let i;
            for(i = r - 1;i >= 0;i--) {
                cycles[i] -= 1
                if(cycles[i] == 0) {
                    indices.push(indices.splice(i, 1)[0]);
                    cycles[i] = n - i
                } else {
                    var j = cycles[i];
                    var t = indices[i];
                    indices[i] = indices[n - j];
                    indices[n - j] = t;
                    out.push(indices.slice(0, r).map(function(v) {
                        return list[v]
                    }));
                    break;
                }
            }
            if(i == -1) {
                break;
            }
        }
        return out;
    },
    /** Get the letter format of an ordinal.
     * e.g. the Nth element in the sequence a,b,c,...z,aa,ab,..,az,ba,...
     *
     * @param {number} n
     * @returns {string}
     */
    letterOrdinal: function(n) {
        var alphabet = 'abcdefghijklmnopqrstuvwxyz';
        var b = alphabet.length;
        if(n == 0) {
            return alphabet[0];
        }
        var s = '';
        while(n > 0) {
            if(s) {
                n -= 1;
            }
            var m = n % b;
            s = alphabet[m] + s;
            n = (n - m) / b;
        }
        return s;
    },
    /** Get a human-sensible name of a part, given its path.
     *
     * @param {string} path
     * @returns {string}
     */
    nicePartName: function(path) {
        var re_path = /^p(\d+)(?:s(\d+))?(?:g(\d+))?(?:a(\d+))?$/;
        var m = re_path.exec(path);
        var s = R('part') + ' ' + util.letterOrdinal(m[1]);
        if(m[2]) {
            s += ' ' + R('step') + ' ' + m[2];
        }
        if(m[3]) {
            s += ' ' + R('gap') + ' ' + m[3];
        }
        if(m[4]) {
            s += ' ' + R('alternative') + ' ' + m[4];
        }
        return s;
    },

    /** Debounce a function: run it no more than every `frequency` milliseconds.
     *
     * @param {number} frequency - Minimum gap between runs of the callback, in milliseconds.
     * @returns {Function} Call with a callback that you want to run.
     */
    debounce: function(frequency) {
        var last_run = 0;
        var cb;
        var timeout;
        /** If it's at least `frequency` milliseconds since the last run, run the callback, else wait and try again.
         */
        function go() {
            var t = new Date();
            if(t - frequency < last_run) {
                if(timeout) {
                    clearTimeout(timeout);
                }
                timeout = setTimeout(go, frequency + 1 - (t - last_run));
            } else {
                last_run = t;
                cb();
            }
        }
        return function(fn) {
            cb = fn;
            go();
        }
    },

    /** Encode the contents of an ArrayBuffer in base64.
     *
     * @param {ArrayBuffer} arrayBuffer
     * @returns {string}
     */
    b64encode: function(arrayBuffer) {
        return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)))
    },

    /** Decode a base64 string to an ArrayBuffer.
     *
     * @param {string} encoded
     * @returns {ArrayBuffer}
     */
    b64decode: function(encoded) {
        const byteString = atob(encoded);
        const bytes = new Uint8Array(byteString.length);
        for (let i = 0; i < byteString.length; i++) {
            bytes[i] = byteString.charCodeAt(i);
        }
        return bytes.buffer;
    },

    /**
     * Compare two strings, ignoring differences in case.
     * Does not ignore differences in accent, even where
     * base characters are identical.
     *
     * @param {string} a - reference string
     * @param {string} b - comparison string
     * @returns {boolean}
     */
    caselessCompare: function(a, b) {
        return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0
    },

    /** Prefix every selector in the given CSS stylesheet with the given selector.
     *
     * @param {Element} style
     * @param {string} prefix - A CSS selector.
     */
    prefix_css_selectors: function(style, prefix) {
        const sheet = style.sheet;
        const prefix_tokens = parsel.tokenize(prefix);
        const space_token = {"type": "combinator", "content": " "};
        prefix_tokens.push(space_token);

        /**
         * Visit a rule or query block.
         * If it's a style rule, prefix its selector.
         * If it's a group, visit every rule inside it.
         *
         * @param {CSSRule} rule
         */
        function visit_rule(rule) {
            if(rule instanceof CSSStyleRule) {
                const tokens = parsel.tokenize(rule.selectorText);
                tokens.splice(0, 0, ...prefix_tokens);
                for(let i = 0;i < tokens.length;i++) {
                    if(tokens[i].type == 'comma') {
                        tokens.splice(i + 1, 0, space_token, ...prefix_tokens);
                        i += prefix_tokens.length + 1;
                    }
                }
                rule.selectorText = parsel.stringify(tokens);
            }

            if(rule.cssRules) {
                for(const r of rule.cssRules) {
                    visit_rule(r);
                }
            }
        }

        for(const rule of sheet.cssRules) {
            visit_rule(rule);
        }

        style.textContent = Array.from(style.sheet.cssRules).map(function(r) {
            return r.cssText;
        }).join('\n');
    }
};

/**
 * A regular expression matching JME string tokens
 *
 * @type {string}
 */
var re_jme_string = util.re_jme_string = /^("""|'''|['"])((?:[^\1\\]|\\.)*?)\1/;

/** Different styles of writing a decimal.
 *
 * Objects of the form `{re,format}`, where `re` is a regex recognising numbers in this style, and `format(integer,decimal)` renders the number in this style.
 *
 * Each regex matches the integer part in group 1, and the decimal part in group 2 - it should be safe to remove all non-digit characters in these and preserve meaning.
 *
 * @see {@link https://en.wikipedia.org/wiki/Decimal_mark#Examples_of_use|Examples of decimal mark use on Wikipedia}
 * @memberof Numbas.util
 */
util.numberNotationStyles = {
    // Plain English style - no thousands separator, dot for decimal point
    'plain': {
        re: /^([0-9]+)(\x2E[0-9]+)?/,
        format: {
            plain: function(integer, decimal) {
                if(decimal) {
                    return integer + '.' + decimal;
                } else {
                    return integer;
                }
            },
            latex: function(integer, decimal) {
                if(decimal) {
                    return integer + '.' + decimal;
                } else {
                    return integer;
                }
            }
        }
    },
    // English style - commas separate thousands, dot for decimal point
    'en': {
        re: /^(\d{1,3}(?:,\d{3})*)(\x2E\d+)?/,
        format: {
            plain: util.standardNumberFormatter(',', '.'),
            latex: util.standardNumberFormatter('{,}', '.')
        }
    },
    // English SI style - spaces separate thousands, dot for decimal point
    'si-en': {
        re: /^(\d{1,3}(?: +\d{3})*)(\x2E(?:\d{3} )*\d{1,3})?/,
        format: {
            plain: util.standardNumberFormatter(' ', '.', true),
            latex: util.standardNumberFormatter('\\,', '.', true)
        }
    },
    // French SI style - spaces separate thousands, comma for decimal point
    'si-fr': {
        re: /^(\d{1,3}(?: +\d{3})*)(,(?:\d{3} )*\d{1,3})?/,
        format: {
            plain: util.standardNumberFormatter(' ', ',', true),
            latex: util.standardNumberFormatter('\\,', '{,}', true)
        }
    },
    // Continental European style - dots separate thousands, comma for decimal point
    'eu': {
        re: /^(\d{1,3}(?:\x2E\d{3})*)(,\d+)?/,
        format: {
            plain: util.standardNumberFormatter('.', ','),
            latex: util.standardNumberFormatter('.\\,', '{,}')
        }
    },
    // Plain French style - no thousands separator, comma for decimal point
    'plain-eu': {
        re: /^([0-9]+)(,[0-9]+)?/,
        format: {
            plain: function(integer, decimal) {
                if(decimal) {
                    return integer + ',' + decimal;
                } else {
                    return integer;
                }
            },
            latex: function(integer, decimal) {
                if(decimal) {
                    return integer + '{,}' + decimal;
                } else {
                    return integer;
                }
            }
        }
    },
    // Swiss style - apostrophes separate thousands, dot for decimal point
    'ch': {
        re: /^(\d{1,3}(?:'\d{3})*)(\x2E\d+)?/,
        format: {
            plain: util.standardNumberFormatter('\'', '.'),
            latex: util.standardNumberFormatter('\'', '.')
        }
    },
    // Indian style - commas separate groups, dot for decimal point. The rightmost group is three digits, other groups are two digits.
    'in': {
        re: /^((?:\d{1,2}(?:,\d{2})*,\d{3})|\d{1,3})(\x2E\d+)?/,
        format: {
            plain: function(integer, decimal) {
                integer = integer + '';
                if(integer.length > 3) {
                    var over = (integer.length - 3) % 2
                    var out = integer.slice(0, over);
                    var i = over;
                    while(i < integer.length - 3) {
                        out += (out ? ',' : '') + integer.slice(i, i + 2);
                        i += 2;
                    }
                    integer = out + ',' + integer.slice(i);
                }
                if(decimal) {
                    return integer + '.' + decimal;
                } else {
                    return integer;
                }
            },
            latex: function(integer, decimal) {
                integer = integer + '';
                if(integer.length > 3) {
                    var over = (integer.length - 3) % 2
                    var out = integer.slice(0, over);
                    var i = over;
                    while(i < integer.length - 3) {
                        out += (out ? '{,}' : '') + integer.slice(i, i + 2);
                        i += 2;
                    }
                    integer = out + '{,}' + integer.slice(i);
                }
                if(decimal) {
                    return integer + '.' + decimal;
                } else {
                    return integer;
                }
            }
        }
    },
    // Significand-exponent ("scientific") style
    'scientific': {
        re: /^(\d[ \d]*)(\x2E\d[ \d]*)?\s*[eE]\s*([-+]?\d[ \d]*)/,
        clean: function(m) {
            return Numbas.math.unscientific(m[0]);
        },
        format: {
            plain: function(integer, decimal) {
                return Numbas.math.niceRealNumber(parseFloat(integer + '.' + decimal), {style:'scientific'});
            },
            latex: function(integer, decimal) {
                return Numbas.math.niceRealNumber(parseFloat(integer + '.' + decimal), {style:'scientific', syntax: 'latex'});
            }
        }
    }
}
var endDelimiters = {
    '$': /[^\\]\$/,
    '\\(': /[^\\]\\\)/,
    '$$': /[^\\]\$\$/,
    '\\[': /[^\\]\\\]/
}
var re_startMaths = /(^|[^\\])(?:\$\$|\$)|\\\(|\\\[|\\begin\{(\w+)\}/;
/** Split a string up by TeX delimiters (`$`, `\[`, `\]`)
 *
 * `bits.re_end` stores the delimiter if the returned array has unfinished maths at the end.
 *
 * @param {string} txt - String to split up.
 * @param {RegExp} re_end - If tex is split across several strings (e.g. text nodes with <br> in the middle), this can be used to give the end delimiter for unfinished maths.
 * @returns {Array.<string>} bits - Stuff outside TeX, left delimiter, TeX, right delimiter, stuff outside TeX, ...
 * @example
 * contentsplitbrackets('hello $x+y$ and \[this\] etc')
 * // ['hello ','$','x+y','$',' and ','\[','this','\]']
 * @memberof Numbas.util
 * @function
 */
util.contentsplitbrackets = function(txt, re_end) {
    if(txt === undefined) {
        return [''];
    }
    var m;
    var startDelimiter = '';
    var endDelimiter = '';
    var startText = '';
    var start = '';
    var end = '';
    var startChop, endChop;
    var bits = [];
    while(txt.length) {
        if(!re_end) {
            m = re_startMaths.exec(txt);
            if(!m) {     // if no maths delimiters, we're done
                bits.push(txt);
                txt = '';
                break;
            }
            startDelimiter = m[0];
            start = m.index;
            startChop = start + startDelimiter.length;
            startText = txt.slice(0, start);
            if(m[1]) {
                startText += m[1];
                startDelimiter = startDelimiter.slice(m[1].length);
            }
            txt = txt.slice(startChop);
            if(startDelimiter.match(/^\\begin/m)) {    //if this is an environment, construct a regexp to find the corresponding \end{} command.
                var environment = m[1];
                re_end = new RegExp('[^\\\\]\\\\end\\{' + environment + '\\}');    // don't ask if this copes with nested environments
            } else if(startDelimiter.match(/^(?:.|[\r\n])\$/m)) {
                re_end = endDelimiters[startDelimiter.slice(1)];
            } else {
                re_end = endDelimiters[startDelimiter];    // get the corresponding end delimiter for the matched start delimiter
            }
        }
        m = re_end.exec(txt);
        if(!m) {    // if no ending delimiter, the text contains no valid maths
            bits.push(startText, startDelimiter, txt);
            bits.re_end = re_end;
            txt = '';
            break;
        }
        endDelimiter = m[0].slice(1);
        end = m.index + 1;    // the end delimiter regexp has a "not a backslash" character at the start because JS regexps don't do negative lookbehind
        endChop = end + endDelimiter.length;
        var math = txt.slice(0, end);
        txt = txt.slice(endChop);
        bits.push(startText, startDelimiter, math, endDelimiter);
        re_end = null;
    }
    return bits;
}
//Because indexOf not supported in IE
if(!Array.indexOf) {
    Array.prototype.indexOf = function(obj) {
        for(let i = 0; i < this.length; i++) {
            if(this[i] == obj) {
                return i;
            }
        }
        return -1;
    };
}
//nice short 'string contains' function
if(!String.prototype.contains) {
    String.prototype.contains = function(it) {
        return this.indexOf(it) != -1;
    };
}
if(!Array.prototype.contains) {
    Array.prototype.contains = function(it) {
        return this.indexOf(it) != -1;
    };
}
//merge one array into another, only adding elements which aren't already present
if(!Array.prototype.merge) {
    Array.prototype.merge = function(arr, sortfn) {
        if(this.length == 0) {
            return arr.slice();
        }
        var out = this.concat(arr);
        if(sortfn) {
            out.sort(sortfn);
        } else {
            out.sort();
        }
        if(sortfn) {
            for(let i = 1; i < out.length;) {
                if(sortfn(out[i - 1], out[i]) == 0) {    //duplicate elements, so remove latest
                    out.splice(i, 1);
                } else {
                    i++;
                }
            }
        } else {
            for(let i = 1;i < out.length;) {
                if(out[i - 1] == out[i]) {
                    out.splice(i, 1);
                } else {
                    i++;
                }
            }
        }
        return out;
    };
}

(function() {
    var reduce = Function.bind.call(Function.call, Array.prototype.reduce);
    var isEnumerable = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable);
    var concat = Function.bind.call(Function.call, Array.prototype.concat);
    var keys = Reflect.ownKeys;

    if (!Object.values) {
        Object.values = function values(O) {
            return reduce(keys(O), function(v, k) {
                return concat(v, typeof k === 'string' && isEnumerable(O, k) ? [O[k]] : [])
            }, []);
        };
    }

    if (!Object.entries) {
        Object.entries = function entries(O) {
            return reduce(keys(O), function(e, k) {
                return concat(e, typeof k === 'string' && isEnumerable(O, k) ? [[k, O[k]]] : [])
            }, []);
        };
    }
})();

if (!Date.prototype.toISOString) {
  (function() {

    /** Pad a number to two digits if necessary.
     * @param {number} number
     * @returns {string}
     */
    function pad(number) {
      if (number < 10) {
        return '0' + number;
      }
      return number.toString();
    }

    Date.prototype.toISOString = function() {
      return this.getUTCFullYear() +
        '-' + pad(this.getUTCMonth() + 1) +
        '-' + pad(this.getUTCDate()) +
        'T' + pad(this.getUTCHours()) +
        ':' + pad(this.getUTCMinutes()) +
        ':' + pad(this.getUTCSeconds()) +
        '.' + (this.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) +
        'Z';
    };

  }());
}

});
;
/*
Copyright 2011-25 Newcastle University
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
       http://www.apache.org/licenses/LICENSE-2.0
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/** @file Stuff to do with loading XML, and getting data out of XML. Provides {@link Numbas.xml}. */
Numbas.queueScript('xml', ['base', 'jme'], function() {

/** Raw XML of the exam definition.
 *
 * @name rawxml
 * @memberof Numbas
 * @type {Object<string>}
 */

/** XML for the current exam.
 *
 * @name examXML
 * @memberof Numbas.xml
 * @type {XMLDocument}
 */

/** XSLT stylesheets.
 *
 * @name templates
 * @memberof Numbas.xml
 * @type {Object<XMLDocument>}
 */


if(window.XMLDocument) {
/**
 * Extends the XMLDocument to emulate IE's selectNodes.
 *
 * @param {string} xpath_selector - The XPath expression to use.
 * @param {Node} contextNode - The top node to match against.
 * @returns {Array.<Node>} - The nodes matching the XPath expression.
 */
window.XMLDocument.prototype.selectNodes = function(xpath_selector, contextNode) {
    var oResult = this.evaluate(
        xpath_selector,
        contextNode || this,
        this.documentElement,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
    );
    var nodeList = new Array(oResult.snapshotLength);
    for(var i = 0; i < nodeList.length; i++) {
        nodeList[i] = oResult.snapshotItem(i);
    }
    return nodeList;
}

/**
 * Extends the XMLDocument to emulate IE's `selectSingleNode`.
 *
 * @param {string} xpath_selector - The XPath expression to use.
 * @param {Node} contextNode - this is for internal use only by the same method when called on Elements.
 * @returns {Node} - The first node matching the XPath expression.
 */
window.XMLDocument.prototype.selectSingleNode = function(xpath_selector, contextNode) {
    return this.evaluate(
        xpath_selector,
        contextNode || this,
        this.documentElement,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
    ).singleNodeValue;
};


/**
 * Extends the Element to emulate IE's selectNodes.
 * @param {string} xpath_selector - The XPath expression to use.
 * @returns {Array.<Node>} - The result of the XPath search.
 */
window.Element.prototype.selectNodes = function(xpath_selector) {
    return this.ownerDocument.selectNodes(xpath_selector, this);
};


/**
 * Extends the Element to emulate IE's `selectSingleNode`.
 *
 * @param {string} xpath_selector - The XPath expression to use.
 * @returns {Node} - The first node matching the XPath expression.
 */
window.Element.prototype.selectSingleNode = function(xpath_selector) {
    return this.ownerDocument.selectSingleNode(xpath_selector, this);
};
}

/** @namespace Numbas.xml */
var xml = Numbas.xml = {
    /** DOM parser to use to parse XML.
     *
     * @type {DOMParser}
     * @private
     */
    dp: window.DOMParser ? new window.DOMParser() : null,
    /** Load in all the XSLT/XML documents from {@link Numbas.rawxml}. */
    loadXMLDocs: function() {
        var templates = xml.templates = {};
        for(var x in Numbas.rawxml.templates) {
            templates[x] = xml.loadXML(Numbas.rawxml.templates[x]);
        }
    },
    /** Load in a single XML document.
     *
     * @param {string} xmlstring
     * @returns {XMLDocument}
     */
    loadXML: function(xmlstring) {
        //parse the XML document
        const parser = new DOMParser();
        var doc = parser.parseFromString(xmlstring, 'text/xml');
        //check for errors
        const errorNode = doc.querySelector("parsererror");
        if (errorNode) {
            throw(new Numbas.Error('xml.could not load', {message: Numbas.util.escapeHTML(errorNode.textContent)}));
        }
        //convert all the attribute names to lower case
        var es = doc.selectNodes('descendant::*');
        for(var i = 0; i < es.length; i++) {
            var e = es[i];
            var attrs = [];
            var j = 0;
            for(j = 0; j < e.attributes.length; j++) {
                attrs.push(e.attributes[j].name);
            }
            for(j = 0; j < attrs.length; j++) {
                var name = attrs[j];
                if(name != name.toLowerCase()) {
                    var value = e.getAttribute(name);
                    e.removeAttribute(name);
                    e.setAttribute(name.toLowerCase(), value);
                }
            }
        }
        return doc;
    },
    /** Load user-defined functions from an XML node.
     *
     * @param {Element} xml
     * @returns {Numbas.jme.variables.func_data[]}
     */
    loadFunctions: function(xml) {
        var tmpFunctions = [];
        //work out functions
        var functionNodes = xml.selectNodes('functions/function');
        if(!functionNodes) {
            return {};
        }
        //first pass: get function names and types
        for(var i = 0; i < functionNodes.length; i++) {
            var name = functionNodes[i].getAttribute('name').toLowerCase();
            var definition = functionNodes[i].getAttribute('definition');
            var language = functionNodes[i].getAttribute('language');
            var outtype = functionNodes[i].getAttribute('outtype').toLowerCase();
            var parameterNodes = functionNodes[i].selectNodes('parameters/parameter');
            var parameters = [];
            for(var j = 0; j < parameterNodes.length; j++) {
                parameters.push({
                    name: parameterNodes[j].getAttribute('name'),
                    type: parameterNodes[j].getAttribute('type').toLowerCase()
                });
            }
            tmpFunctions.push({
                name: name,
                definition: definition,
                language: language,
                outtype: outtype,
                parameters: parameters
            });
        }
        return tmpFunctions;
    },
    /** Load variable definitions from an XML node.
     *
     * @param {Element} xml
     * @param {Numbas.jme.Scope} scope - Scope to compile relative to.
     * @returns {Numbas.jme.variables.variable_data_dict[]}
     */
    loadVariables: function(xml, scope) {
        var variableNodes = xml.selectNodes('variables/variable');    //get variable definitions out of XML
        if(!variableNodes) {
            return {};
        }
        //evaluate variables - work out dependency structure, then evaluate from definitions in correct order
        var definitions = [];
        for(var i = 0; i < variableNodes.length; i++) {
            var name = variableNodes[i].getAttribute('name');
            var definition = Numbas.xml.getTextContent(variableNodes[i].selectSingleNode('value'));
            definitions.push({
                name: name,
                definition: definition
            });
        }
        return definitions;
    },
    /** Lots of the time we have a message stored inside content/html/.. structure.
     *
     * This pulls the message out and serializes it so it can be inserted easily with jQuery.
     *
     * @param {Element} node
     * @returns {string}
     */
    serializeMessage: function(node) {
        return new XMLSerializer().serializeToString(node.selectSingleNode('content'));
    },
    /** Get all the text belonging to an element.
     *
     * @param {Element} elem
     * @returns {string}
     */
    getTextContent: function(elem) {
        return elem.textContent;
    },
    /** Set the text content of an element.
     *
     * @param {Element} elem
     * @param {string} text
     */
    setTextContent: function(elem, text) {
        if(elem.textContent !== undefined) {
            elem.textContent = text;
        } else {
            elem.text = text;
        }
    },
    /** @typedef {object} Numbas.xml.tryGetAttribute_options
     * @property {boolean} string - Always return the attribute as a string.
     */
    /** Try to get attributes from an XML node, and use them to fill in an object's properties if they're present. If `obj` is null, then the loaded value is just returned.
     *
     * @param {object} obj - Object to fill up.
     * @param {Element} xmlroot - Root XML element.
     * @param {Element|string} elem - Either an XML node to get attributes from, or an XPath query to get the element from `xmlroot`.
     * @param {string[]} names - Names of attributes to load.
     * @param {string[]} [altnames] - Names of object properties to associate with attribute names. If undefined, the attribute name is used.
     * @param {Numbas.xml.tryGetAttribute_options} options
     * @returns {object} - The last attribute loaded.
     */
    tryGetAttribute: function(obj, xmlroot, elem, names, altnames, options) {
        if(!options) {
            options = {};
        }
        if(typeof(elem) == 'string') {    //instead of passing in an XML node to use, can give an XPath query, and we try to get that from xmlroot
            elem = xmlroot.selectSingleNode(elem);
        }
        if(!elem) {
            return false;
        }
        if(typeof(names) == 'string') {
            names = [names];
        }
        if(!altnames) {
            altnames = [];
        } else if(typeof(altnames) == 'string') {
            altnames = [altnames];
        }
        for(var i = 0;i < names.length;i++) {
            var value = elem.getAttribute(names[i].toLowerCase());    //try to get attribute from node
            if(value !== null) {
                //establish which field of target object we're filling in
                var name = altnames[i] ? altnames[i] : names[i];
                if(options.string) {
                //if this property is already defined in the target object, cast the loaded value to the same type as the existing value
                } else if(obj !== null && obj[name] !== undefined) {
                    if(value.length > 0) {
                        if(typeof(obj[name]) == 'number') {
                            if(Numbas.util.isNumber(value, true)) {
                                value = Numbas.util.parseNumber(value, true);
                            } else if(Numbas.util.isFloat(Numbas.util.unPercent(value))) {
                                value = Numbas.util.unPercent(value);
                            } else {
                                throw(new Numbas.Error('xml.property not number', {name:name, value:value, element:elem}));
                            }
                        } else if(typeof(obj[name]) == 'boolean') {
                            if(Numbas.util.isBool(value)) {
                                value = Numbas.util.parseBool(value);
                            } else {
                                throw(new Numbas.Error('xml.property not boolean', {name:name, value:value, element:elem}));
                            }
                        }
                        //otherwise must be a string, so leave it alone
                    }
                } else {
                    //automatically convert to a number or a boolean if possible
                    if(Numbas.util.isFloat(value)) {
                        value = parseFloat(value);
                    } else if(Numbas.util.isBool(value)) {
                        value = Numbas.util.parseBool(value);
                    }
                }
                if(obj) {
                    obj[name] = value;
                }
            }
        }
        return value;
    },
    /** Replace every `<localise>` tag with its contents, run through localisation, i.e. get localised strings.
     *
     * @param {Element} template
     * @returns {Element}
     */
    localise: function(template) {
        for(const localise of template.querySelectorAll('localise')) {
            const d = document.createElement('container');
            d.innerHTML = R(localise.textContent);
            localise.replaceWith(...d.childNodes);
        }
        return template;
    },
    /** Transform an XML node using the given XSL template, returning a string representation of the transformed XML.
     *
     * @param {Element} template
     * @param {Element} xml
     * @returns {string}
     */
    transform: function(template, xml) {
        const container = xml.ownerDocument.createElement('container');
        xml = xml.cloneNode(true);
        container.append(xml);

        const processor = new XSLTProcessor();
        processor.importStylesheet(template);
        const doc = processor.transformToDocument(container);
        const serializer = new XMLSerializer();

        const string = serializer.serializeToString(doc);

        return string;
    },
    /** Is the given node empty? True if it has no children.
     *
     * @param {Element} node
     * @returns {boolean}
     */
    isEmpty: function(node) {
        return node.childNodes.length == 0;
    },


    pretty_print: function(node, indent = '') {
        if(node.nodeType != node.ELEMENT_NODE) {
            return;
        }

        const attrs = Array.from(node.attributes).map(({name, value}) => `${name}="${value}"`);

        const children = Array.from(node.children).map((c) => xml.pretty_print(c, indent + '  '));
        const nodeName = node.nodeName.toLowerCase();
        return `${indent}<${nodeName} ${attrs.join(' ')}>${children.length ? '\n' + children.join('\n') + '\n' + indent : ''}</${nodeName}>`
    },
};
});
;
Numbas.queueScript('settings',[],function() {
    Numbas.version = 9.1;

    Numbas.rawxml = {"templates": {"question": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\nCopyright 2011-16 Newcastle University\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n       http://www.apache.org/licenses/LICENSE-2.0\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n-->\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n    <xsl:output method=\"html\" version=\"5.0\" encoding=\"UTF-8\" standalone=\"yes\" indent=\"yes\" media-type=\"text/html\" omit-xml-declaration=\"yes\"/>\n    <xsl:strip-space elements=\"p\"/>\n    <xsl:template match=\"question\">\n        \n        <article class=\"main-content question print-visible\" data-bind=\"visible: isCurrentQuestion, css: css_classes, descendantsComplete: htmlBound, attr: {{'aria-label': displayName, id: 'question-'+question.path}}\">\n        \n            <form autocomplete=\"off\">\n                <span style=\"display:none\">\\( \\begingroup \\)</span>\n                <header>\n                    <h2 data-bind=\"latex: displayName, attr: {{id: question.path+'-header'}}\" class=\"question-header\"></h2>\n                    <nav class=\"explore-nav navbar\" data-bind=\"if: showPartsTree, visible: showPartsTree, attr: {{'aria-labelledby': question.path+'-breadcrumbs'}}\">\n                        <menu class=\"pager\">\n                            <li class=\"previous\">\n                                <button type=\"button\" class=\"btn sm info\" data-bind=\"click: goToPreviousPart, css: {{'visibility-hidden': !previousPart()}}\"><span class=\"sr-only\" data-localise=\"question.back to previous part\"></span>\u2190</button>\n                            </li>\n                            <li>\n                                <div class=\"part\" data-bind=\"latex: currentPart().name\"></div>\n                            </li>\n                            <li>\n                                <details class=\"parts-tree\">\n                                    <summary class=\"part-progress\" data-bind=\"attr: {{id: question.path+'-breadcrumbs'}}\"><localise>question.progress</localise></summary>\n                                    <div class=\"part\" data-bind=\"treeView: firstPart\" role=\"tree\">\n                                        <div data-bind=\"jmescope: part.getScope()\">\n                                            <a href=\"#\" role=\"treeitem\" class=\"name\" data-bind=\"latex: name, click: $parent.setCurrentPart, attr: {{'tabindex': isCurrentPart() ? 0 : -1, 'aria-current': isCurrentPart() ? 'step' : false}}\"></a>\n                                        </div>\n                                        <menu data-bind=\"foreach: madeNextParts\" role=\"group\">\n                                            <li role=\"none\">\n                                                <div class=\"part\" data-bind=\"treeNode: $data\"></div>\n                                            </li>\n                                        </menu>\n                                    </div>\n                                </details>\n                            </li>\n                        </menu>\n                    </nav>\n                </header>\n                <xsl:apply-templates />\n                <span style=\"display: none\">\\( \\endgroup \\)</span>\n            </form>\n\n            \n<!-- bottom nav bar - prv/nxt, plus submit/advice/reveal buttons -->\n<nav class=\"question-nav navbar\" data-bind=\"jmescope: question.scope,attr: {{'aria-label': R('question.nav.label')}}\">\n    <p class=\"marks\" data-bind=\"visible: !showScoreBreakdown()\">\n        <span class=\"score\" data-bind=\"html: scoreFeedback.message\"></span>\n        <span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n        <span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n    </p>\n\n    <table class=\"explore-breakdown\" data-bind=\"visible: showScoreBreakdown()\">\n        <tbody>\n            <xsl:comment> ko foreach: objectives </xsl:comment>\n            <xsl:comment> ko if: visible </xsl:comment>\n            <tr>\n                <td class=\"name\" data-bind=\"latex: name\"></td>\n                <td class=\"message\"><span data-bind=\"text: feedback.plainMessage\"></span></td>\n                <td>\n                    <span class=\"feedback-icon\" data-bind=\"css: feedback.iconClass, attr: feedback.iconAttr\"></span>\n                </td>\n            </tr>\n            <xsl:comment> /ko </xsl:comment>\n            <xsl:comment> /ko </xsl:comment>\n\n            <xsl:comment> ko foreach: penalties </xsl:comment>\n            <xsl:comment> ko if: visible </xsl:comment>\n            <tr>\n                <td class=\"name\" data-bind=\"latex: name\"></td>\n                <td class=\"message\"><span data-bind=\"text: scoreDisplay\"></span></td>\n                <td></td>\n            </tr>\n            <xsl:comment> /ko </xsl:comment>\n            <xsl:comment> /ko </xsl:comment>\n        </tbody>\n        <tfoot>\n            <tr class=\"total\">\n                <th><localise>control.total</localise></th>\n                <td>\n                    <span class=\"score\" data-bind=\"html: scoreFeedback.plainMessage\"></span>\n                </td>\n                <td>\n                    <span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\"></span>\n                </td>\n            </tr>\n        </tfoot>\n    </table>\n\n    <button type=\"button\" class=\"btn nextQuestionBtn\" data-bind=\"visible: $root.exam().mode()=='normal' &amp;&amp; $root.exam().exam.settings.navigateMode=='diagnostic', click: $parent.advance, attr: {{disabled: !$parent.canAdvance()}}\"><localise>control.move to next question</localise></button>\n    <button class=\"btn regenBtn\" data-bind=\"visible: $root.exam().mode()=='normal' &amp;&amp; $root.exam().exam.settings.allowRegen, click: () => Numbas.controls.regenQuestion($root.exam().exam)\"><localise>control.regen</localise></button>\n    <button class=\"btn revealBtn\" data-bind=\"visible: question.parts.length &amp;&amp; canReveal(), click: () => Numbas.controls.revealAnswer($root.exam().exam)\"><localise>control.reveal</localise></button>\n</nav>\n\n        </article>\n    </xsl:template>\n    <xsl:template match=\"properties|feedbacksettings|preview|notes|variables|preprocessing|preambles\" />\n    <xsl:template match=\"content\">\n        <xsl:apply-templates select=\"*\" mode=\"content\" />\n    </xsl:template>\n    <xsl:template match=\"@*|node()\" mode=\"content\">\n        <xsl:copy>\n            <xsl:apply-templates select=\"@*|node()\" mode=\"content\" />\n        </xsl:copy>\n    </xsl:template>\n    <xsl:template match=\"parts\">\n        <div class=\"parts\" data-bind=\"foreach: parts\">\n            <div class=\"part-wrapper\" data-bind=\"promise: html_promise, descendantsComplete: htmlBound\"></div>\n        </div>\n    </xsl:template>\n    <xsl:template match=\"part\">\n    </xsl:template>\n    <xsl:template match=\"tags\">\n    </xsl:template>\n    <xsl:template match=\"extensions\">\n    </xsl:template>\n    \n<xsl:template match=\"statement\">\n    <div data-bind=\"visible: hasStatement\">\n        <div class=\"statement content-area\" localise-data-jme-context-description=\"question.statement\">\n            <xsl:apply-templates />\n        </div>\n        <hr/>\n    </div>\n</xsl:template>\n\n    <xsl:template match=\"advice\">\n    \n    <div class=\"adviceContainer\" data-bind=\"visible: hasAdvice() &amp;&amp; adviceDisplayed()\" localise-data-jme-context-description=\"question.advice\">\n    \n        <h3><localise>question.advice</localise></h3>\n        <span class=\"adviceDisplay content-area\">\n            <xsl:apply-templates />\n        </span>\n    </div>\n</xsl:template>\n</xsl:stylesheet>", "part": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\nCopyright 2011-16 Newcastle University\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n       http://www.apache.org/licenses/LICENSE-2.0\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n-->\n<xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n    <xsl:output method=\"html\" version=\"5.0\" encoding=\"UTF-8\" standalone=\"yes\" indent=\"yes\" media-type=\"text/html\" omit-xml-declaration=\"yes\"/>\n    <xsl:strip-space elements=\"p\"/>\n    <xsl:template match=\"content\">\n        <xsl:apply-templates select=\"*\" mode=\"content\" />\n    </xsl:template>\n\n    <xsl:template match=\"@*|node()\" mode=\"content\">\n        <xsl:copy>\n            <xsl:apply-templates select=\"@*|node()\" mode=\"content\" />\n        </xsl:copy>\n    </xsl:template>\n\n    <xsl:template match=\"no-paragraph\">\n        <xsl:apply-templates select=\"*\" mode=\"no-paragraph\" />\n    </xsl:template>\n\n    <!-- the `no-paragraph` mode strips block-level tags which would be invalid, such as inside a <span> or <option> tag. -->\n\n    <xsl:template match=\"content\" mode=\"no-paragraph\">\n        <xsl:apply-templates select=\"*\" mode=\"no-paragraph\" />\n    </xsl:template>\n\n    <!-- block-level tags; list from https://www.w3.org/TR/html4/sgml/dtd.html#block -->\n    <xsl:template match=\"p|div|h1|h2|h3|h4|h5|h6|pre|dl|blockquote|form|hr|table|fieldset|address\" mode=\"no-paragraph\">\n        <xsl:apply-templates select=\"@*|node()\" mode=\"no-paragraph\" />\n    </xsl:template>\n\n    <xsl:template match=\"@*|node()\" mode=\"no-paragraph\">\n        <xsl:copy>\n            <xsl:apply-templates select=\"@*|node()\" mode=\"no-paragraph\" />\n        </xsl:copy>\n    </xsl:template>\n\n    \n<xsl:template match=\"steps\">\n    <div class=\"steps well\" data-bind=\"visible: stepsOpen\">\n        <xsl:apply-templates select=\"part\"/>\n    </div>\n    <div class=\"stepsBtn\">\n        <button class=\"btn primary\" data-bind=\"visible: !stepsOpen(), click: controls.showSteps\"><localise>question.show steps</localise></button>\n        <button class=\"btn primary\" data-bind=\"visible: stepsOpen(), click: controls.hideSteps\"><localise>question.hide steps</localise></button>\n        <span class=\"help-block hint penaltyMessage\">(<span data-bind=\"html: stepsPenaltyMessage\"></span>)</span>\n    </div>\n</xsl:template>\n\n    \n<xsl:template match=\"prompt\">\n    <span class=\"prompt content-area\" localise-data-jme-context-description=\"part.prompt\">\n        <xsl:apply-templates />\n    </span>\n</xsl:template>\n\n    \n<xsl:template match=\"part\">\n    <xsl:variable name=\"inline\">\n        <xsl:choose>\n            <xsl:when test=\"@isgap='true' and @type='1_n_2' and choices/@displaytype='dropdownlist'\"><xsl:text>true</xsl:text></xsl:when>\n            <xsl:when test=\"@isgap='true' and not (choices)\"><xsl:text>true</xsl:text></xsl:when>\n            <xsl:otherwise><xsl:text>false</xsl:text></xsl:otherwise>\n        </xsl:choose>\n    </xsl:variable>\n    <xsl:variable name=\"tag\">\n        <xsl:choose>\n            <xsl:when test=\"$inline='true'\">span</xsl:when>\n            <xsl:otherwise>section</xsl:otherwise>\n        </xsl:choose>\n    </xsl:variable>\n    <xsl:variable name=\"block\">\n        <xsl:choose>\n        <xsl:when test=\"@type='m_n_2' or @type='m_n_x'\"><xsl:text> block</xsl:text></xsl:when>\n            <xsl:when test=\"@type='1_n_2' and @displaytype='radiogroup'\"><xsl:text> block</xsl:text></xsl:when>\n        </xsl:choose>\n    </xsl:variable>\n    <xsl:element name=\"{$tag}\">\n        <xsl:attribute name=\"class\">part type-<xsl:value-of select=\"@type\"/> <xsl:value-of select=\"$block\"/><xsl:if test=\"parent::steps\"> step</xsl:if><xsl:if test=\"parent::gaps\"> gap</xsl:if></xsl:attribute>\n        <xsl:attribute name=\"data-bind\">with: question.display.getPart('<xsl:value-of select=\"@path\" />'), visible: question.display.getPart('<xsl:value-of select=\"@path\" />').visible, css: {dirty: question.display.getPart('<xsl:value-of select=\"@path\" />').isDirty, 'has-name': question.display.getPart('<xsl:value-of select=\"@path\" />').showName(), answered: answered(), dirty: isDirty(), 'has-feedback-messages': hasFeedbackMessages()}, event: event_handlers</xsl:attribute>\n        <xsl:attribute name=\"data-part-path\"><xsl:value-of select=\"@path\" /></xsl:attribute>\n        <xsl:attribute name=\"data-jme-context-description\"><xsl:value-of select=\"@jme-context-description\" /></xsl:attribute>\n        <xsl:if test=\"$inline='false'\"><h3 class=\"partheader\" data-bind=\"visible: showName(), latex: name\"></h3></xsl:if>\n        <xsl:if test=\"not(ancestor::gaps)\">\n            <xsl:apply-templates select=\"prompt\" />\n        </xsl:if>\n        <xsl:if test=\"count(steps/part)>0\">\n            <xsl:apply-templates select=\"steps\"/>\n        </xsl:if>\n        <span class=\"student answer\">\n            <xsl:attribute name=\"data-bind\">css: {answered: scoreFeedback.answered, 'has-warnings': hasWarnings}, attr: {\"feedback-state\": scoreFeedback.state}</xsl:attribute>\n            <xsl:apply-templates select=\".\" mode=\"typespecific\"/>\n            <span class=\"warnings alert alert-warning\" aria-live=\"assertive\" role=\"alert\" data-bind=\"visible: warningsShown, css: {{shown: warningsShown}}, attr: {{id: part.full_path+'-warnings'}}\">\n                <xsl:comment>ko foreach: warnings</xsl:comment>\n                <span class=\"warning\" data-bind=\"latex: message\"></span>\n                <xsl:comment>/ko</xsl:comment>\n            </span>\n        </span>\n        <span class=\"correct answer alert info\" data-bind=\"visible: showCorrectAnswer, typeset: showCorrectAnswer\">\n            <xsl:apply-templates select=\".\" mode=\"correctanswer\"/>\n        </span>\n        <xsl:if test=\"not(ancestor::gaps)\">\n            <div class=\"submit-and-feedback\" data-bind=\"visible: doesMarking, css: {{changed: changedFeedback()}}\">\n                <button class=\"btn primary submitPart\" data-bind=\"visible: showSubmitPart, click: controls.submit, text: isDirty() || !scoreFeedback.answered() ? R('question.submit part') : R('question.answer saved')\"><localise>question.submit part</localise></button>\n                <p class=\"waiting-for-pre-submit\" data-bind=\"visible: waiting_for_pre_submit\"><localise>part.waiting for pre submit</localise></p>\n                <div class=\"partFeedback\" data-bind=\"visible: showFeedbackBox()\">\n                    <div class=\"marks\" data-bind=\"visible: showMarks()\">\n                        <span class=\"score\" data-bind=\"html: scoreFeedback.message\"></span>\n                        <span class=\"feedback-icon\" data-bind=\"visible: scoreFeedback.iconClass, css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n                        <span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n                    </div>\n                </div>\n                <details class=\"feedbackMessages alert info\" role=\"log\" aria-live=\"polite\" data-bind=\"open: feedbackShown, css: {{changed: changedFeedback()}}\" localise-data-jme-context-description=\"part.feedback\">\n                    <summary data-bind=\"visible: isNotOnlyPart\">\n                        <p class=\"sr-only\" data-bind=\"visible: isNotOnlyPart, text: feedback_title\"></p>\n                        <span data-bind=\"text: feedbackToggleText\"></span><span class=\"sr-only\">.</span>\n                        <span class=\"sr-only\" data-bind=\"visible: changedFeedback\"><localise>part.there is new feedback</localise></span>\n                    </summary>\n                    <p class=\"out-of-date-message\" data-bind=\"visible: isDirty\"><localise>part.feedback out of date</localise></p>\n                    <ol data-bind=\"visible: shownFeedbackMessages().length, foreach: shownFeedbackMessages\">\n                        <li class=\"feedbackMessage\" data-bind=\"attr: {{'data-credit-change': credit_change}}\">\n                            <span data-bind=\"visible: $parent.showFeedbackIcon, css: 'feedback-icon '+icon\" aria-hidden=\"true\"></span> \n\n                            <span class=\"message\">\n                                <xsl:comment>ko if: format=='html'</xsl:comment>\n                                    <span data-bind=\"dom: message\"></span>\n                                <xsl:comment>/ko</xsl:comment>\n\n                                <xsl:comment>ko if: format=='string'</xsl:comment>\n                                    <span data-bind=\"latex: message\"></span>\n                                <xsl:comment>/ko</xsl:comment>\n\n                                <xsl:comment>ko if: $parent.scoreFeedback.showActualMark() &amp;&amp; credit_message</xsl:comment>\n                                    <xsl:text> </xsl:text>\n                                    <span data-bind=\"dom: credit_message\"></span>\n                                <xsl:comment>/ko</xsl:comment>\n                            </span>\n                        </li>\n                    </ol>\n                </details>\n            </div>\n            <div class=\"next-parts\" data-bind=\"visible: showNextParts\">\n                <p>\n                    <span class=\"what-next\" data-bind=\"text: whatNextMessage\"></span>\n                </p>\n                <button class=\"btn link\" type=\"button\" data-bind=\"visible: part.settings.suggestGoingBack, click: question.display.goToPreviousPart\">\u293a <localise>question.back to previous part</localise></button>\n                <menu data-bind=\"foreach: nextParts\">\n                    <li class=\"next-part\">\n                        <button class=\"btn primary next-part-option\" type=\"button\" data-bind=\"click: select, css: {{made: made}}, disable: $parent.isDirty\">\n                            <span data-bind=\"latex: label\"></span>\n                            <span class=\"hint\" data-bind=\"visible: lockAfterLeaving\"> <localise>part.choose next part.will be locked</localise></span>\n                        </button>\n                    </li>\n                </menu>\n            </div>\n            <div class=\"dead-end\" data-bind=\"visible: reachedDeadEnd\">\n                <p><localise>part.reached dead end</localise></p>\n            </div>\n        </xsl:if>\n    </xsl:element>\n</xsl:template>\n<xsl:template match=\"part\" mode=\"typespecific\">\n    <localise>question.unsupported part type</localise> <xsl:text> </xsl:text> <xsl:value-of select=\"@type\"/>\n</xsl:template>\n<xsl:template match=\"part\" mode=\"correctanswer\">\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='1_n_2']\" mode=\"typespecific\">\n    <xsl:apply-templates select=\"choices\" mode=\"one\"/>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='1_n_2']\" mode=\"correctanswer\">\n    <xsl:apply-templates select=\"choices\" mode=\"correctanswer\"/>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='m_n_2']\" mode=\"typespecific\">\n    <xsl:apply-templates select=\"choices\" mode=\"one\"/>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='m_n_2']\" mode=\"correctanswer\">\n    <xsl:apply-templates select=\"choices\" mode=\"correctanswer\"/>\n</xsl:template>\n\n    \n<xsl:template match=\"choices\" mode=\"one\">\n    <xsl:variable name=\"displaytype\"><xsl:value-of select=\"@displaytype\"/></xsl:variable>\n    <span localise-data-jme-context-description=\"part.mcq.choices\">\n    <xsl:choose>\n        <xsl:when test=\"@displaytype='radiogroup'\">\n            <fieldset data-bind=\"part_aria_validity: hasWarnings, part: $data, attr: {{id: part.full_path+'-input'}}\">\n                <legend data-bind=\"text: input_title\" class=\"sr-only\"></legend>\n                <menu class=\"multiplechoice radiogroup\" data-bind=\"reorder_list: {{order: part.shuffleAnswers}}, css: {{'show-cell-answer-state': showCellAnswerState, 'columns': displayColumns}}\">\n                    <xsl:variable name=\"cols\" select=\"@displaycolumns\"/>\n                    <xsl:if test=\"$cols>0\"> \n                        <xsl:attribute name=\"style\">display: inline-grid; grid-template-columns: repeat(<xsl:number value=\"$cols\"/>,auto);</xsl:attribute>\n                    </xsl:if>\n                    <xsl:apply-templates select=\"choice\" mode=\"radiogroup\"/>\n                </menu>\n            </fieldset>\n        </xsl:when>\n        <xsl:when test=\"@displaytype='checkbox'\">\n            <fieldset data-bind=\"part_aria_validity: hasWarnings, part: $data, attr: {{id: part.full_path+'-input'}}\">\n                <legend data-bind=\"text: input_title\" class=\"sr-only\"></legend>\n                <menu class=\"multiplechoice checkbox\" data-bind=\"reorder_list: {{order: part.shuffleAnswers}}, css: {{'show-cell-answer-state': showCellAnswerState, 'columns': displayColumns}}\">\n                    <xsl:variable name=\"cols\" select=\"@displaycolumns\"/>\n                    <xsl:if test=\"$cols>0\"> \n                        <xsl:attribute name=\"style\">display: inline-grid; grid-template-columns: repeat(<xsl:number value=\"$cols\"/>,auto);</xsl:attribute>\n                    </xsl:if>\n                    <xsl:apply-templates select=\"choice\" mode=\"checkbox\"/>\n                </menu>\n            </fieldset>\n        </xsl:when>\n        <xsl:when test=\"@displaytype='dropdownlist'\">\n            <select class=\"multiplechoice dropdownlist screen-only\" data-bind=\"event: inputEvents, value: studentAnswer, disable: disabled, reorder_list: {{order: part.shuffleAnswers}}, css: {{'show-cell-answer-state': showCellAnswerState}}, attr: {{title: input_title, id: part.full_path+'-input'}}, part_aria_validity: hasWarnings, part: $data\">\n                <xsl:comment>ko if: showBlankOption</xsl:comment><option value=\"\"></option><xsl:comment>/ko</xsl:comment>\n                <xsl:apply-templates select=\"choice\" mode=\"dropdownlist-screen\"/>\n            </select>\n            <span class=\"multiplechoice dropdownlist print-only\" data-bind=\"value: studentAnswer, reorder_list: {{order: part.shuffleAnswers}}, css: {{'show-cell-answer-state': showCellAnswerState}}, attr: {{title: input_title, id: part.full_path+'-input'}}, part_aria_validity: hasWarnings, part: $data\">\n                <xsl:apply-templates select=\"choice\" mode=\"dropdownlist-print\"/>\n            </span>\n        </xsl:when>\n    </xsl:choose>\n    </span>\n</xsl:template>\n\n<xsl:template match=\"choices\" mode=\"correctanswer\">\n    <xsl:variable name=\"displaytype\"><xsl:value-of select=\"@displaytype\"/></xsl:variable>\n    <span>\n    <xsl:choose>\n        <xsl:when test=\"@displaytype='radiogroup'\">\n            <fieldset data-bind=\"attr: {{id: part.full_path+'-expected-input'}}\">\n                <legend><localise>part.correct answer</localise></legend>\n                <menu class=\"multiplechoice radiogroup\" data-bind=\"reorder_list: {{order: part.shuffleAnswers}}\">\n                    <xsl:apply-templates select=\"choice\" mode=\"radiogroup-correctanswer\"/>\n                </menu>\n            </fieldset>\n        </xsl:when>\n        <xsl:when test=\"@displaytype='checkbox'\">\n            <fieldset data-bind=\"attr: {{id: part.full_path+'-expected-input'}}\">\n                <legend><localise>part.correct answer</localise></legend>\n                <menu class=\"multiplechoice checkbox\" data-bind=\"reorder_list: {{order: part.shuffleAnswers}}\">\n                    <xsl:apply-templates select=\"choice\" mode=\"checkbox-correctanswer\"/>\n                </menu>\n            </fieldset>\n        </xsl:when>\n        <xsl:when test=\"@displaytype='dropdownlist'\">\n            <label>\n                <localise>part.correct answer</localise>\n                <select class=\"multiplechoice screen-only\" data-bind=\"value: correctAnswer, reorder_list: {{order: part.shuffleAnswers}}, attr: {{id: part.full_path+'-expected-input'}}\" disabled=\"true\">\n                    <xsl:apply-templates select=\"choice\" mode=\"dropdownlist-correctanswer-screen\"/>\n                </select>\n                <span class=\"multiplechoice dropdownlist print-only\" data-bind=\"value: correctAnswer, reorder_list: {{order: part.shuffleAnswers}}, attr: {{id: part.full_path+'-expected-input'}}\" disabled=\"true\">\n                    <xsl:apply-templates select=\"choice\" mode=\"dropdownlist-correctanswer-print\"/>\n                </span>\n            </label>\n        </xsl:when>\n    </xsl:choose>\n    </span>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"radiogroup\">\n    <xsl:variable name=\"path\">\n        <xsl:apply-templates select=\"../..\" mode=\"path\"/>\n    </xsl:variable>\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <li>\n        <xsl:attribute name=\"data-bind\">css: {checked: studentAnswer()==<xsl:value-of select=\"$choicenum\"/>, correct: studentAnswer()==<xsl:value-of select=\"$choicenum\"/> &amp;&amp; correctAnswer()==<xsl:value-of select=\"$choicenum\"/>}</xsl:attribute>\n        <label>\n            <input type=\"radio\" class=\"choice\" data-bind=\"event: inputEvents, checked: studentAnswer, disable: disabled, attr: {{name: part.path+'-choice'}}\" value=\"{$choicenum}\"/>\n            <xsl:apply-templates select=\"content\"/>\n        </label>\n    </li>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"radiogroup-correctanswer\">\n    <xsl:variable name=\"path\">\n        <xsl:apply-templates select=\"../..\" mode=\"path\"/>\n    </xsl:variable>\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <li>\n        <label>\n            <input type=\"radio\" class=\"choice\" data-bind=\"checked: correctAnswer()+'', attr: {{name: part.path+'-correctanswer'}}\" disabled=\"true\" value=\"{$choicenum}\"/>\n            <xsl:apply-templates select=\"content\"/>\n        </label>\n    </li>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"checkbox\">\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <li>\n        <xsl:attribute name=\"data-bind\">css: {checked: ticks[<xsl:value-of select=\"$choicenum\"/>], correct: ticks[<xsl:value-of select=\"$choicenum\"/>] &amp;&amp; correctTicks()[<xsl:value-of select=\"$choicenum\"/>]}</xsl:attribute>\n        <label>\n            <input type=\"checkbox\" class=\"choice\" data-bind=\"event: inputEvents, checked: ticks[{$choicenum}], disable: disabled, attr: {{name: part.path+'-choice'}}\" />\n            <xsl:apply-templates select=\"content\"/>\n        </label>\n    </li>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"checkbox-correctanswer\">\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <li>\n        <label>\n            <input type=\"checkbox\" class=\"choice\" name=\"choice\" data-bind=\"checked: correctTicks()[{$choicenum}], attr: {{name: part.path+'-correctanswer'}}\" disabled=\"true\" />\n            <xsl:apply-templates select=\"content\"/>\n        </label>\n    </li>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"dropdownlist-screen\">\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <option value=\"{$choicenum}\">\n        <xsl:apply-templates select=\"content\" mode=\"no-paragraph\" />\n    </option>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"dropdownlist-correctanswer-screen\">\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <option value=\"{$choicenum}\">\n        <xsl:apply-templates select=\"content\" mode=\"no-paragraph\" />\n    </option>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"dropdownlist-print\">\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <span class=\"dropdownlist-option\" value=\"{$choicenum}\">\n        <xsl:attribute name=\"data-bind\">css: {'checked': studentAnswer()===\"<xsl:value-of select=\"$choicenum\"/>\"}</xsl:attribute>\n        <xsl:apply-templates select=\"content\" mode=\"no-paragraph\"/>\n    </span>\n</xsl:template>\n\n<xsl:template match=\"choice\" mode=\"dropdownlist-correctanswer-print\">\n    <xsl:variable name=\"choicenum\"><xsl:value-of select=\"count(preceding-sibling::choice)\"/></xsl:variable>\n    <span class=\"dropdownlist-option\" value=\"{$choicenum}\">\n        <xsl:attribute name=\"data-bind\">css: {'checked': correctAnswer()===\"<xsl:value-of select=\"$choicenum\"/>\"}</xsl:attribute>\n        <xsl:apply-templates select=\"content\" mode=\"no-paragraph\"/>\n    </span>\n</xsl:template>\n\n<xsl:template match=\"distractor\">\n    <span><xsl:apply-templates /></span>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='m_n_x']\" mode=\"typespecific\">\n    <xsl:variable name=\"displaytype\" select=\"choices/@displaytype\"/>\n    <form autocomplete=\"off\">\n        <fieldset data-bind=\"part_aria_validity: hasWarnings, part: $data, attr: {{id: part.full_path+'-input'}}\">\n            <legend data-bind=\"text: input_title\" class=\"sr-only\"></legend>\n            <table class=\"choices-grid\" data-bind=\"reorder_table: {{rows: part.shuffleChoices, columns: part.shuffleAnswers, leaders: 1}}, css: {{'show-cell-answer-state': showCellAnswerState}}\">\n                <thead localise-data-jme-context-description=\"part.mcq.answers\">\n                    <xsl:variable name=\"blank_cells\">\n                        <xsl:choose>\n                            <xsl:when test=\"string(choices/header)\">2</xsl:when>\n                            <xsl:otherwise>1</xsl:otherwise>\n                        </xsl:choose>\n                    </xsl:variable>\n                    <xsl:if test=\"string(answers/header)\">\n                        <tr data-shuffle=\"no\">\n                            <xsl:variable name=\"numanswers\" select=\"count(answers/answer)\"></xsl:variable>\n                            <td colspan=\"{$blank_cells}\" />\n                            <td class=\"answer-heading\" colspan=\"{$numanswers}\">\n                                <xsl:apply-templates select=\"answers/header\" />\n                            </td>\n                        </tr>\n                    </xsl:if>\n                    <tr>\n                        <td colspan=\"{$blank_cells}\" />\n                        <xsl:for-each select=\"answers/answer\">\n                            <xsl:variable name=\"answernum\" select=\"count(preceding-sibling::answer)\"/>\n                            <th data-bind=\"attr: {{id: part.full_path+'-answer-{$answernum}'}}\"><xsl:apply-templates select=\"content\"/></th>\n                        </xsl:for-each>\n                    </tr>\n                </thead>\n                <tbody>\n                    <xsl:for-each select=\"choices/choice\">\n                        <xsl:apply-templates select=\".\" mode=\"m_n_x\">\n                            <xsl:with-param name=\"displaytype\" select=\"$displaytype\"/>\n                        </xsl:apply-templates>\n                    </xsl:for-each>\n                </tbody>\n            </table>\n        </fieldset>\n    </form>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='m_n_x']\" mode=\"correctanswer\">\n    <xsl:variable name=\"displaytype\" select=\"choices/@displaytype\"/>\n    <form autocomplete=\"off\">\n        <fieldset data-bind=\"attr: {{id: part.full_path+'-correct-input'}}\">\n            <legend><localise>part.correct answer</localise></legend>\n            <table class=\"choices-grid\" data-bind=\"reorder_table: {{rows: part.shuffleChoices, columns: part.shuffleAnswers, leaders: 1}}\">\n                <thead>\n                    <xsl:if test=\"string(answers/header)\">\n                        <tr data-shuffle=\"no\">\n                            <xsl:variable name=\"numanswers\" select=\"count(answers/answer)\"></xsl:variable>\n                            <td colspan=\"2\" />\n                            <td class=\"answer-heading\" colspan=\"{$numanswers}\">\n                                <xsl:apply-templates select=\"answers/header\" />\n                            </td>\n                        </tr>\n                    </xsl:if>\n                    <tr>\n                        <td colspan=\"2\" />\n                        <xsl:for-each select=\"answers/answer\">\n                            <xsl:variable name=\"answernum\" select=\"count(preceding-sibling::answer)\"/>\n                            <th data-bind=\"attr: {{id: part.full_path+'-expected-answer-{$answernum}'}}\"><xsl:apply-templates select=\"content\"/></th>\n                        </xsl:for-each>\n                    </tr>\n                </thead>\n                <tbody>\n                    <xsl:for-each select=\"choices/choice\">\n                        <xsl:apply-templates select=\".\" mode=\"m_n_x-correctanswer\">\n                            <xsl:with-param name=\"displaytype\" select=\"$displaytype\"/>\n                        </xsl:apply-templates>\n                    </xsl:for-each>\n                </tbody>\n            </table>\n        </fieldset>\n    </form>\n</xsl:template>\n<xsl:template match=\"choice\" mode=\"m_n_x\">\n    <xsl:param name=\"displaytype\"/>\n    <xsl:variable name=\"path\">\n        <xsl:apply-templates select=\"../..\" mode=\"path\"/>\n    </xsl:variable>\n    <xsl:variable name=\"answers\" select=\"../../answers\"/>\n    <xsl:variable name=\"choicenum\" select=\"count(preceding-sibling::choice)\"/>\n    <tr>\n        <xsl:if test=\"$choicenum = 0 and string(../header)\">\n            <xsl:variable name=\"numchoices\" select=\"count(../choice)\"></xsl:variable>\n            <td class=\"choice-heading\" data-shuffle=\"no\" rowspan=\"{$numchoices}\">\n                <xsl:apply-templates select=\"../header\" />\n            </td>\n        </xsl:if>\n        <th class=\"choice\" data-bind=\"attr: {{id: part.full_path+'-choice-{$choicenum}'}}\"><xsl:apply-templates select=\"content\"/></th>\n        <xsl:for-each select=\"$answers/answer\">\n            <xsl:variable name=\"answernum\" select=\"count(preceding-sibling::answer)\"/>\n            <td class=\"option\">\n                <xsl:attribute name=\"data-bind\">css: tickFeedback()[<xsl:value-of select=\"$answernum\"/>][<xsl:value-of select=\"$choicenum\"/>]</xsl:attribute>\n                <label>\n                <xsl:choose>\n                    <xsl:when test=\"$displaytype='checkbox'\">\n                        <input type=\"checkbox\" class=\"choice\" data-bind=\"event: inputEvents, checked: ticks[{$answernum}][{$choicenum}], disable: disabled, visible: layout[{$answernum}][{$choicenum}], attr: {{name: part.full_path+'-choice-{$choicenum}', 'aria-labelledby': part.full_path+'-choice-{$choicenum} '+part.full_path+'-answer-{$answernum}'}}\" />\n                    </xsl:when>\n                    <xsl:when test=\"$displaytype='radiogroup'\">\n                        <input type=\"radio\" class=\"choice\" data-bind=\"event: inputEvents, checked: ticks[{$choicenum}], disable: disabled, visible: layout[{$answernum}][{$choicenum}], attr: {{name: part.path+'-choice-'+{$choicenum}, 'aria-labelledby': part.full_path+'-choice-{$choicenum} '+part.full_path+'-answer-{$answernum}'}}\" value=\"{$answernum}\"/>\n                    </xsl:when>\n                </xsl:choose>\n                </label>\n            </td>\n        </xsl:for-each>\n    </tr>\n</xsl:template>\n<xsl:template match=\"choice\" mode=\"m_n_x-correctanswer\">\n    <xsl:param name=\"displaytype\"/>\n    <xsl:variable name=\"path\">\n        <xsl:apply-templates select=\"../..\" mode=\"path\"/>\n    </xsl:variable>\n    <xsl:variable name=\"answers\" select=\"../../answers\"/>\n    <xsl:variable name=\"choicenum\" select=\"count(preceding-sibling::choice)\"/>\n    <tr>\n        <xsl:if test=\"$choicenum = 0\">\n            <xsl:variable name=\"numchoices\" select=\"count(../choice)\"></xsl:variable>\n            <td class=\"choice-heading\" data-shuffle=\"no\" rowspan=\"{$numchoices}\">\n                <xsl:apply-templates select=\"../header\" />\n            </td>\n        </xsl:if>\n        <th class=\"choice\" data-bind=\"attr: {{id: part.full_path+'-expected-choice-{$choicenum}'}}\"><xsl:apply-templates select=\"content\"/></th>\n        <xsl:for-each select=\"$answers/answer\">\n            <xsl:variable name=\"answernum\" select=\"count(preceding-sibling::answer)\"/>\n            <td class=\"option\">\n                <xsl:choose>\n                    <xsl:when test=\"$displaytype='checkbox'\">\n                        <input type=\"checkbox\" class=\"choice\" data-bind=\"checked: correctTicks()[{$answernum}][{$choicenum}], visible: layout[{$answernum}][{$choicenum}], disable: true, attr: {{name: part.path+'-choice-{$choicenum}-correctanswer', 'aria-labelledby': part.full_path+'-expected-choice-{$choicenum} '+part.full_path+'-expected-answer-{$answernum}'}}\" disabled=\"true\"/>\n                    </xsl:when>\n                    <xsl:when test=\"$displaytype='radiogroup'\">\n                        <input type=\"radio\" class=\"choice\" data-bind=\"checked: correctTicks()[{$choicenum}]+'', visible: layout[{$answernum}][{$choicenum}], disable: true, attr: {{name: part.path+'-choice-'+{$choicenum}+'-correctanswer', 'aria-labelledby': part.full_path+'-expected-choice-{$choicenum} '+part.full_path+'-expected-answer-{$answernum}'}}\" disabled=\"true\" value=\"{$answernum}\"/>\n                    </xsl:when>\n                </xsl:choose>\n            </td>\n        </xsl:for-each>\n    </tr>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='patternmatch']\" mode=\"typespecific\">\n    <xsl:if test=\"count(steps/part)>0\"><localise>part.with steps answer prompt</localise></xsl:if>\n    <input type=\"text\" autocapitalize=\"off\" inputmode=\"text\" spellcheck=\"false\" class=\"patternmatch\" size=\"12.5\" data-bind=\"event: inputEvents, textInput: studentAnswer, autosize: true, disable: disabled, attr: {{title: input_title, id: part.full_path+'-input'}}, part_aria_validity: hasWarnings, part: $data\"/>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='patternmatch']\" mode=\"correctanswer\">\n    <label>\n        <localise>part.correct answer</localise>\n        <input type=\"text\" autocapitalize=\"off\" inputmode=\"text\" spellcheck=\"false\" disabled=\"true\" class=\"patternmatch\" data-bind=\"value: displayAnswer, autosize: true, attr: {{id: part.full_path+'-expected-input'}}\"/>\n    </label>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='gapfill']\" mode=\"typespecific\">\n</xsl:template>\n<xsl:template match=\"part[@type='gapfill']\" mode=\"correctanswer\">\n</xsl:template>\n<xsl:template match=\"gapfill\" mode=\"content\">\n    <xsl:variable name=\"n\"><xsl:value-of select=\"@reference\"/></xsl:variable>\n    <xsl:apply-templates select=\"ancestor::part[1]/gaps/part[$n+1]\" />\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='jme']\" mode=\"typespecific\">\n    <xsl:if test=\"count(steps/part)>0\"><localise>part.with steps answer prompt</localise></xsl:if>\n    <input type=\"text\" autocapitalize=\"off\" inputmode=\"text\" spellcheck=\"false\" class=\"jme\" data-bind=\"event: inputEvents, textInput: studentAnswer, autosize: true, disable: disabled, attr: {{title: input_title, id: part.full_path+'-input'}}, part_aria_validity: hasWarnings, part: $data\"/>\n    <span class=\"jme-preview\" data-bind=\"visible: showPreview &amp;&amp; studentAnswerLaTeX()\">\n        <span class=\"sr-only\"><localise>jme.interpreted as</localise></span>\n        <output aria-live=\"polite\" data-bind=\"attr: {{for: part.full_path+'-input'}}, maths: showPreview ? '\\\\displaystyle{{'+studentAnswerLaTeX()+'}}' : '', click: focusInput\"></output>\n    </span>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='jme']\" mode=\"correctanswer\">\n    <label>\n        <localise>part.correct answer</localise>\n        <input type=\"text\" autocapitalize=\"off\" inputmode=\"text\" spellcheck=\"false\" disabled=\"true\" class=\"jme\" data-bind=\"value: correctAnswer, autosize: true, attr: {{id: part.full_path+'-expected-input'}}\"/>\n        <span class=\"jme-preview\">\n            <span class=\"sr-only\"><localise>jme.interpreted as</localise></span>\n            <output aria-live=\"polite\" data-bind=\"attr: {{for: part.full_path+'-correct-input'}}, maths: '\\\\displaystyle{{'+correctAnswerLaTeX()+'}}'\"></output>\n        </span>\n    </label>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='numberentry']\" mode=\"typespecific\">\n    <xsl:if test=\"count(steps/part)>0\"><localise>part.with steps answer prompt</localise></xsl:if>\n    <input type=\"text\" autocapitalize=\"off\" inputmode=\"text\" spellcheck=\"false\" step=\"{answer/inputstep/@value}\" class=\"numberentry\" data-bind=\"event: inputEvents, textInput: studentAnswer, autosize: true, disable: disabled, css: {{'has-error': warningsShown}}, attr: {{title: input_title, id: part.full_path+'-input'}}, part_aria_validity: hasWarnings, part: $data\"/>\n    <span class=\"help-block hint precision-hint\" data-bind=\"visible: showInputHint, html: inputHint\"></span>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='numberentry']\" mode=\"correctanswer\">\n    <label>\n        <localise>part.correct answer</localise>\n        <input type=\"text\" autocapitalize=\"off\" inputmode=\"text\" spellcheck=\"false\" disabled=\"true\" class=\"jme\" data-bind=\"value: correctAnswer, autosize: true, attr: {{id: part.full_path+'-expected-input'}}\"/>\n    </label>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='matrix']\" mode=\"typespecific\">\n    <xsl:if test=\"count(steps/part)>0\"><localise>part.with steps answer prompt</localise></xsl:if>\n    <matrix-input data-bind=\"attr: {{id: part.full_path+'-input'}}\" params=\"rows: studentAnswerRows, columns: studentAnswerColumns, prefilledCells: prefilledCells, value: studentAnswer, allowResize: allowResize, minColumns: minColumns, maxColumns: maxColumns, minRows: minRows, maxRows: maxRows, disable: disabled, events: inputEvents, title: input_title\"></matrix-input>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@type='matrix']\" mode=\"correctanswer\">\n    <label>\n        <localise>part.correct answer</localise>\n        <matrix-input data-bind=\"attr: {{id: part.full_path+'-expected-input'}}\" params=\"rows: correctAnswerRows, columns: correctAnswerColumns, prefilledCells: prefilledCells, value: correctAnswer, allowResize: false, disable: true\"></matrix-input>\n    </label>\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='information']\" mode=\"typespecific\">\n</xsl:template>\n<xsl:template match=\"part[@type='information']\" mode=\"correctanswer\">\n</xsl:template>\n\n    \n<xsl:template match=\"part[@type='extension']\" mode=\"typespecific\">\n</xsl:template>\n<xsl:template match=\"part[@type='extension']\" mode=\"correctanswer\">\n</xsl:template>\n\n    \n<xsl:template match=\"part[@custom='true']\" mode=\"typespecific\">\n    <xsl:if test=\"count(steps/part)>0\"><localise>part.with steps answer prompt</localise></xsl:if>\n    <span data-bind=\"event: inputEvents, component: {{name: 'answer-widget', params: {{answer: studentAnswer, widget: input_widget, widget_options: input_options, part: part, disable: disabled, events: part.display.inputEvents, title: input_title, id: part.full_path}}}}\"></span>\n    <span class=\"help-block hint\" data-bind=\"visible: input_options.hint, html: input_options.hint, typeset: input_options.hint\"></span>\n\n    \n<span class=\"feedback-icon\" data-bind=\"css: scoreFeedback.iconClass, attr: scoreFeedback.iconAttr\" aria-hidden=\"true\"></span>\n<span class=\"sr-only\" data-bind=\"text: scoreFeedback.iconAttr().title\"></span>\n\n\n</xsl:template>\n<xsl:template match=\"part[@custom='true']\" mode=\"correctanswer\">\n    <label>\n        <localise>part.correct answer</localise>\n        <span data-bind=\"component: {{name: 'answer-widget', params: {{answer: correctAnswer, widget: input_widget, widget_options: input_options, part: part, disable: true, id: part.full_path+'-expected'}}}}\"></span>\n    </label>\n</xsl:template>\n\n</xsl:stylesheet>"}};
});
