| 382 | const questionElements = new Set([..._.keys(coreElementsCache), ..._.keys(context.course_elements)]); |
| 383 | |
| 384 | const visitNode = async (node) => { |
| 385 | if (node.tagName && questionElements.has(node.tagName)) { |
| 386 | const elementName = node.tagName; |
| 387 | const elementFile = module.exports.getElementController(elementName, context); |
| 388 | if (phase === 'render' && !_.includes(renderedElementNames, elementName)) { |
| 389 | renderedElementNames.push(elementName); |
| 390 | } |
| 391 | // We need to wrap it in another node, since only child nodes |
| 392 | // are serialized |
| 393 | const serializedNode = parse5.serialize({ |
| 394 | childNodes: [node], |
| 395 | }); |
| 396 | let ret_val, consoleLog; |
| 397 | try { |
| 398 | [ret_val, consoleLog] = await module.exports.elementFunction(pc, phase, elementName, serializedNode, data, context); |
| 399 | } catch (e) { |
| 400 | const courseIssue = new Error(`${elementFile}: Error calling ${phase}(): ${e.toString()}`); |
| 401 | courseIssue.data = e.data; |
| 402 | courseIssue.fatal = true; |
| 403 | // We'll catch this and add it to the course issues list |
| 404 | throw courseIssue; |
| 405 | } |
| 406 | if (_.isString(consoleLog) && consoleLog.length > 0) { |
| 407 | const courseIssue = new Error(`${elementFile}: output logged on console during ${phase}()`); |
| 408 | courseIssue.data = { outputBoth: consoleLog }; |
| 409 | courseIssue.fatal = false; |
| 410 | courseIssues.push(courseIssue); |
| 411 | } |
| 412 | if (phase == 'render') { |
| 413 | if (!_.isString(ret_val)) { |
| 414 | const courseIssue = new Error(`${elementFile}: Error calling ${phase}(): return value is not a string`); |
| 415 | courseIssue.data = { ret_val }; |
| 416 | courseIssue.fatal = true; |
| 417 | throw courseIssue; |
| 418 | } |
| 419 | node = parse5.parseFragment(ret_val); |
| 420 | } else if (phase == 'file') { |
| 421 | // Convert ret_val from base64 back to buffer (this always works, |
| 422 | // whether or not ret_val is valid base64) |
| 423 | const buf = Buffer.from(ret_val, 'base64'); |
| 424 | // If the buffer has non-zero length... |
| 425 | if (buf.length > 0) { |
| 426 | if (fileData.length > 0) { |
| 427 | // If fileData already has non-zero length, throw an error |
| 428 | const courseIssue = new Error(`${elementFile}: Error calling ${phase}(): attempting to overwrite non-empty fileData`); |
| 429 | courseIssue.fatal = true; |
| 430 | throw courseIssue; |
| 431 | } else { |
| 432 | // If not, replace fileData with buffer |
| 433 | fileData = buf; |
| 434 | } |
| 435 | } |
| 436 | } else { |
| 437 | // the following line is safe because we can't be in multiple copies of this function simultaneously |
| 438 | data = ret_val; // eslint-disable-line require-atomic-updates |
| 439 | const checkErr = module.exports.checkData(data, origData, phase); |
| 440 | if (checkErr) { |
| 441 | const courseIssue = new Error(`${elementFile}: Invalid state after ${phase}(): ${checkErr}`); |