(function() {
/**
* XML Helper class.
*/
Help4.widget.help.XmlHelper = class {
/**
* @param {Document} entityXml
* @param {string} projectId
* @returns {Help4.widget.help.project.SEN.EntityTxt|null}
*/
static toEntityTxt(entityXml, projectId) {
/** @type {Element|null} */
const project = _getElements('project', entityXml, true);
if (!project) return null;
/** @type {Help4.widget.help.project.SEN.EntityTxt} */
const entityTxt = _getAttributes(project);
entityTxt.assets = [];
entityTxt.tclass = 'project';
entityTxt.uid = projectId;
entityTxt.hidden = Help4.clampBoolean(entityTxt.hidden);
return entityTxt;
}
/**
* @param {Document} projectDpr
* @param {Help4.widget.help.project.SEN.EntityTxt} entityTxt
* @returns {Help4.widget.help.project.SEN.LessonJs}
*/
static toLessonJs(projectDpr, entityTxt) {
/** @type {Element|null} */
const audioFormat = _getElements('AudioFormat', projectDpr, true);
let videoFormat = '.wav', isMP3 = false, sps = 0;
if (audioFormat) {
videoFormat = audioFormat.getAttribute('Encoding') === 'Linear' ? '.wav' : '.mp3';
isMP3 = videoFormat === '.mp3';
sps = Number(audioFormat.getAttribute('SamplesPerSecond')) || 0;
}
/** @type {Element|null} */
const author = _getElements('author', projectDpr, true);
/** @type {Element|null} */
const version = _getElements('modified_version', projectDpr, true) ||
_getElements('version', projectDpr, true);
/** @type {Element|null} */
const globalParams = _getElements('GlobalParams', projectDpr, true);
/** @type {Help4.widget.help.project.SEN.LessonJsTourstop[]} */
const tourstops = [];
/** @type {Help4.widget.help.project.SEN.LessonJs} */
const json = {
user_header: {
audio_ext: videoFormat,
author: author ? _getElementText(author) : '',
comment: entityTxt.description,
language: entityTxt.language,
mastery_score_percent: entityTxt.mastery_score_percent,
max_score_percent: entityTxt.max_score_percent,
shelftype: entityTxt.macroset,
title: entityTxt.caption,
version: version ? _getElementText(version) : ''
},
global_params: globalParams ? _getAttributes(globalParams) : {},
tourstops
};
/** @type {NodeList} */
const takes = _getElements('Take', projectDpr);
for (const [index, /** @type {Element} */ take] of Help4.arrayEntries(takes)) {
/** @type {Element|null} */
const macroTrack = _getElements('MacroTrack', take, true);
if (!macroTrack) continue;
/** @type {Element|null} */
let data = _getElements('data', take, true);
if (!data) continue;
/** @type {Object} */
data = _getAttributes(data);
const {audio_duration_ms, callable, jump, caption: title, name: uid, show} = data;
/** @type {number} */
const smp = Number(macroTrack.getAttribute('samples')) || 0;
/** @type {number} */
const adm = Number(audio_duration_ms) || 0;
/** @type {Help4.widget.help.project.SEN.LessonJsMacroArray} */
const macros = _getMacros(macroTrack, sps);
/** @type {Help4.widget.help.project.SEN.LessonJsTourstop} */
const tourstop = {
audio: '',
audio_duration: isMP3 ? (adm + 100) : adm,
callable: callable === '1',
duration: ~~(smp * 1000 / sps),
index,
jumpable: jump === '1',
macros,
title,
uid,
visible: show === '1'
};
if (tourstop.audio_duration) tourstop.audio = uid;
tourstops.push(tourstop);
}
return json;
}
}
/**
* @memberof Help4.widget.help.XmlHelper
* @private
* @param {Element} macroTrack
* @param {number} sps
* @returns {Help4.widget.help.project.SEN.LessonJsMacroArray}
*/
function _getMacros(macroTrack, sps) {
/** @type {Help4.widget.help.project.SEN.LessonJsMacroArray} */
const macros = [];
const {/** @type {Help4.widget.help.HeadersXml} */ HeadersXml} = Help4.widget.help;
/** @type {Element} */
for (const macro of macroTrack.children) {
const first = macro.nodeType === 1 && macro.firstElementChild; // Init or MacroTemplate
if (!first) continue;
const name = first.getAttribute('template_name');
/** @type {Help4.widget.help.HeadersXmlMacroDefinition} */
const paramset = HeadersXml[name];
/** @type {Help4.widget.help.project.SEN.LessonJsMacro} */
const attributes = _getAttributes(first, {paramset, includeSubTree: true});
const pos = Number(macro.getAttribute('pos')) || 0;
attributes.time = Math.floor(pos * 1000 / sps);
attributes.duration = paramset.Duration;
attributes.macro_template = attributes.template_name;
delete attributes.template_name;
macros.push(attributes);
}
return macros;
}
/**
* @memberof Help4.widget.help.XmlHelper
* @private
* @param {Element} element
* @returns {string}
*/
function _getElementText(element) {
const text = element.innerHTML || element.nodeTypedValue || '';
if (!text) {
// needed for Edge browser
const child = element.firstChild;
return child?.wholeText?.trim() || '';
}
return text;
}
/**
* @memberof Help4.widget.help.XmlHelper
* @private
* @param {Element} element
* @param {Object} [options = {}]
* @param {Help4.widget.help.HeadersXmlMacroDefinition} [options.paramset]
* @param {boolean} [options.includeSubTree = false]
* @returns {Object}
*/
function _getAttributes(element, {paramset, includeSubTree = false} = {}) {
const result = {};
/**
* @param {string} nodeName
* @param {*} nodeValue
*/
const handleResult = (nodeName, nodeValue) => {
if (paramset) {
const {NoExport, ParamSet} = paramset;
const {type} = ParamSet[nodeName] || {};
if (NoExport.indexOf(nodeName) < 0 && type) {
result[nodeName] = _clampTypedValue(type, nodeValue);
}
} else {
result[nodeName] = nodeValue;
}
}
/** @type {Attr} */
for (const attribute of element.attributes) {
const {nodeName, nodeValue} = attribute;
handleResult(nodeName, nodeValue);
}
if (includeSubTree) {
/** @type {Element} */
for (const child of element.children) {
const attributes = _getAttributes(child);
handleResult(child.nodeName, attributes);
}
}
return result;
}
/**
* @memberof Help4.widget.help.XmlHelper
* @private
* @param {string} type
* @param {*} value
* @returns {*}
*/
function _clampTypedValue(type, value) {
switch (type) {
case 'POSITION': {
const left = Number(value?.x) || 0;
const top = Number(value?.y) || 0;
return {left, top};
}
case 'POSSIZE': {
const left = Number(value?.x) || 0;
const top = Number(value?.y) || 0;
const width = Number(value?.cx) || 100;
const height = Number(value?.cy) || 100;
return {left, top, width, height};
}
case 'SIZE':
// type mismatch in Headers.xml; SIZE is sometimes string and sometimes object
if (typeof value === 'string') {
let [width, height] = value?.split(',') || [];
width = Number(width) || 0;
height = Number(height) || 0;
return {width, height};
} else {
const [x, y] = value?.hasOwnProperty('x')
? ['x', 'y']
: ['width', 'height'];
const width = Number(value?.[x]) || 0;
const height = Number(value?.[y]) || 0;
return {width, height};
}
case 'BOOL_0_1':
case 'BOOL_YES_NO':
case 'BOOL_TRUE_FALSE':
// not always accurate within project.dpr
return value == 1 || value === 'yes' || value === 'true';
}
return value;
}
/**
* @memberof Help4.widget.help.XmlHelper
* @private
* @param {string} nodeName
* @param {Element|Document} xml
* @param {boolean} [first = false]
* @returns {Element|NodeList|null}
*/
function _getElements(nodeName, xml, first = false) {
return first
? xml.querySelector(nodeName)
: xml.querySelectorAll(nodeName);
}
})();