const snakecase = require('snakecase-keys');
const tags = require('./tag');
const UlTag = tags.UlTag,
SectionTag = tags.SectionTag,
FormTag = tags.FormTag,
LiTag = tags.LiTag,
ATag = tags.ATag,
HeaderTag = tags.HeaderTag,
FooterTag = tags.FooterTag,
InputTag = tags.InputTag;
/**
* @typedef {object} Form
* @property {('form')} type
* @property {string|undefined} header header value
* @property {string|undefined} footer footer value
* @property {FormMeta} meta
* @property {('get'|'post'|'put'|'delete')} method
* @property {string} path
* @property {FormItemContent | FormItemMenu} body form body object
*/
/**
* @typedef {object} FormMeta
* @property {boolean|undefined} completionStatusShow
* @property {boolean|undefined} completionStatusInHeader
* @property {boolean|undefined} confirmationNeeded
*/
/**
* @typedef {object} FormItemContent
* @property {('string'|'date'|'datetime')} type
* @property {string} name
* @property {string} description
* @property {string|undefined} header
* @property {string|undefined} footer
*/
/**
* @typedef {Object} FormItemMenu
* @property {('form-menu')} type
* @property {Array<FormItemMenuItem>} body
* @property {string} name
* @property {string|undefined} header
* @property {string|undefined} footer
* @property {FormItemMenuMeta|undefined} meta
*/
/**
* @typedef {Object} FormItemMenuMeta
* @property {boolean} autoSelect
* @property {boolean} multiSelect
* @property {boolean} numbered
*/
/**
* @typedef {object} FormItemMenuItem
* @property {('option'|'content')} type
* @property {string} description
* @property {string|undefined} value
*/
/**
* @typedef {Object} Menu
* @property {('menu')} type
* @property {Array<MenuItem>} body
* @property {string|undefined} header
* @property {string|undefined} footer
* @property {MenuMeta|undefined} meta
*/
/**
* @typedef {Object} MenuMeta
* @property {boolean} autoSelect
*/
/**
* @typedef {object} MenuItem
* @property {('option'|'content')} type indicating menu option or plain content
* @property {string} description
* @property {string|undefined} path For menu options only. Path to be used for HTTP callback (added to base path configured in app's settings in developer portal)
* @property {('get'|'post'|'put'|'delete'|undefined)} method=get For menu options only. HTTP method that should be used when redirecting after successful menu option submission
*/
/**
* @typedef {Object} Response
* @property {('form'|'menu')} contentType
* @property {Form | Menu} content
*/
/**
* Instantiates a new Form
* @param {Array<FormItemContent|FormItemMenu>} body
* @param {('GET'|'POST'|'PUT'|'DELETE')} method='POST'
* @param {string} path
* @param {string|undefined} header
* @param {string|undefined} footer
* @param {FormMeta|undefined} meta
* @constructor
*/
function Form(body, method, path, header, footer, meta) {
if (!body || !path) {
throw Error('(body, path) are mandatory');
}
this.type = 'form';
this.body = body;
this.method = method || 'POST';
this.path = path;
this.header = header || null;
this.footer = footer || null;
this.meta = meta || null;
}
/**
* Creates a Form from a FormTag
* @param {FormTag} formTag
* @returns {Form}
*/
Form.fromTag = function (formTag) {
let body = [];
formTag.children.forEach(function (sectionTag) {
let childType = FormItemContent;
for (let i = 0; i < sectionTag.children.length; i++) {
if (sectionTag.children[i] instanceof UlTag) {
childType = FormItemMenu;
break;
}
}
body.push(childType.fromTag(sectionTag));
});
return new Form(
body,
formTag.attrs.method,
formTag.attrs.action,
formTag.attrs.header,
formTag.attrs.footer,
new FormMeta(
formTag.attrs.completionStatusShow,
formTag.attrs.completionStatusInHeader,
formTag.attrs.confirmationNeeded
)
);
};
/**
* Instantiates a new FormMeta
* @param {boolean|undefined} completionStatusShow
* @param {boolean|undefined} completionStatusInHeader
* @param {boolean|undefined} confirmationNeeded
* @constructor
*/
function FormMeta(completionStatusShow, completionStatusInHeader, confirmationNeeded) {
if (typeof completionStatusShow === 'boolean') {
this.completionStatusShow = completionStatusShow;
} else {
this.completionStatusShow = completionStatusShow !== undefined;
}
if (typeof completionStatusInHeader === 'boolean') {
this.completionStatusInHeader = completionStatusInHeader;
} else {
this.completionStatusInHeader = completionStatusInHeader !== undefined;
}
if (typeof confirmationNeeded === 'boolean') {
this.confirmationNeeded = confirmationNeeded;
} else {
this.confirmationNeeded = confirmationNeeded !== undefined;
}
}
/**
* Instantiates a new FormItemContent
* @param {('string'|'date'|'datetime')} type
* @param {string} name
* @param {string} description
* @param {string|undefined} header
* @param {string|undefined} footer
* @constructor
*/
function FormItemContent(type, name, description, header, footer) {
this.type = type;
this.name = name;
this.description = description;
this.header = header || null;
this.footer = footer || null;
}
/**
* Creates a FormItemContent from a SectionTag
* @param {SectionTag} sectionTag
* @returns {FormItemContent}
*/
FormItemContent.fromTag = function (sectionTag) {
let type,
header,
footer;
sectionTag.children.forEach(function (child) {
if (child instanceof InputTag) {
type = child.attrs.type;
}
});
if (!type) {
throw Error('When <section> plays the role of a form item content, ' +
'it must contain a <input/>')
}
// Translate the InputTag type to FormItemContent type
switch (type) {
case 'text':
type = 'string';
break;
case 'date':
case 'datetime':
// These are the same
break;
default:
throw Error(`<input/> type "#{type}" is not supported`);
}
if (sectionTag.children[0] instanceof HeaderTag) {
header = sectionTag.children[0].toString();
}
if (sectionTag.children[sectionTag.children.length - 1] instanceof FooterTag) {
footer = sectionTag.children[sectionTag.children.length - 1].toString();
}
return new FormItemContent(
type,
sectionTag.attrs.name,
sectionTag.toString(true, true),
header || sectionTag.attrs.header,
footer || sectionTag.attrs.footer,
);
};
/**
* Instantiates a new FormItemMenu
* @param {Array<FormItemMenuItem>} body
* @param {string} name
* @param {string|undefined} header
* @param {string|undefined} footer
* @param {FormItemMenuMeta|undefined} meta
* @constructor
*/
function FormItemMenu(body, name, header, footer, meta) {
this.type = 'form-menu';
this.body = body;
this.name = name;
this.header = header || null;
this.footer = footer || null;
this.meta = meta || null;
}
/**
* Creates a FormItemMenu from a SectionTag
* @param {SectionTag} sectionTag
* @returns {FormItemMenu}
*/
FormItemMenu.fromTag = function (sectionTag) {
let body = [],
header,
footer;
sectionTag.children.forEach(function (child) {
if (child instanceof UlTag) {
child.children.forEach(function (liTag) {
body.push(FormItemMenuItem.fromTag(liTag));
})
} else if (child instanceof HeaderTag) {
header = child.toString();
} else if (child instanceof FooterTag) {
footer = child.toString();
} else {
body.push(FormItemMenuItem.fromTag(child));
}
});
// Discard all the menu items evaluated to false (eg: those with no description)
body = body.filter(function (menuItem) {
return menuItem;
});
return new FormItemMenu(
body,
sectionTag.attrs.name,
header || sectionTag.attrs.header,
footer || sectionTag.attrs.footer,
new FormItemMenuMeta(
sectionTag.attrs.autoSelect,
sectionTag.attrs.multiSelect,
sectionTag.attrs.numbered,
)
);
};
/**
* Instantiates a new FormItemMenuMeta
* @param {boolean} autoSelect
* @param {boolean} multiSelect
* @param {boolean} numbered
* @constructor
*/
function FormItemMenuMeta(autoSelect, multiSelect, numbered) {
this.autoSelect = autoSelect;
this.multiSelect = multiSelect;
this.numbered = numbered;
}
/**
* Instantiates a new FormItemMenuItem
* @param {('option'|'content')} type
* @param {string} description
* @param {string|undefined} value
* @constructor
*/
function FormItemMenuItem(type, description, value) {
this.type = type;
this.description = description;
this.value = value || null;
}
/**
* Creates a FormItemMenuItem from a SectionTag's child
* @param tag
* @returns {FormItemMenuItem}
*/
FormItemMenuItem.fromTag = function (tag) {
let description,
type = 'content',
value;
if (typeof tag === 'string') {
description = tag;
} else {
description = tag.toString();
}
if (!description) {
// Ignore the menu items without text
return undefined;
}
if (tag instanceof LiTag && tag.attrs.value) {
type = 'option';
value = tag.attrs.value;
}
return new FormItemMenuItem(type, description, value);
};
/**
* Instantiates a new Menu
* @param {Array<MenuItem>} body
* @param {string|undefined} header
* @param {string|undefined} footer
* @param {MenuMeta} meta
* @constructor
*/
function Menu(body, header, footer, meta) {
this.type = "menu";
this.body = body;
this.header = header || null;
this.footer = footer || null;
this.meta = meta || null;
}
/**
* Creates a Menu from a SectionTag
* @param {SectionTag} sectionTag
* @returns {Menu}
*/
Menu.fromTag = function (sectionTag) {
let body = [],
header,
footer;
sectionTag.children.forEach(function (child) {
if (child instanceof UlTag) {
child.children.forEach(function (liTag) {
body.push(MenuItem.fromTag(liTag));
});
} else if (child instanceof HeaderTag) {
header = child.toString();
} else if (child instanceof FooterTag) {
footer = child.toString();
} else {
body.push(MenuItem.fromTag(child));
}
});
// Discard all the menu items evaluated to false (eg: those with no description)
body = body.filter(function (menuItem) {
return menuItem;
});
return new Menu(
body,
header || sectionTag.attrs.header,
footer || sectionTag.attrs.footer,
new MenuMeta(sectionTag.attrs.autoSelect)
);
};
/**
* Instantiates a new MenuMeta
* @param {boolean} autoSelect
* @constructor
*/
function MenuMeta(autoSelect) {
this.autoSelect = autoSelect;
}
/**
* Instantiates a new MenuItem
* @param {('option'|'content')} type
* @param {string} description
* @param {('GET'|'POST'|'PUT'|'DELETE'|undefined)} method
* @param {string|undefined} path
* @constructor
*/
function MenuItem(type, description, method, path) {
this.type = type;
this.description = description;
this.method = method || null;
this.path = path || null;
}
/**
* Creates a MenuItem from a SectionTag's child
* @param {LiTag|BrTag|PTag|LabelTag|InputTag|string} tag
* @returns {MenuItem}
*/
MenuItem.fromTag = function (tag) {
let description,
method,
path,
type = 'content';
if (typeof tag === 'string') {
description = tag;
} else {
description = tag.toString();
}
if (!description) {
// Ignore the menu items without text
return undefined;
}
if (tag instanceof LiTag && tag.children[0] instanceof ATag) {
const aTag = tag.children[0];
method = aTag.attrs.method;
path = aTag.attrs.href;
type = 'option';
}
return new MenuItem(type, description, method, path);
};
/**
* Instantiates a Response object
* @param {Form|Menu} content
* @constructor
*/
function Response(content) {
if (!content) {
throw Error('content is mandatory');
}
let contentType;
if (content instanceof Form) {
contentType = 'form';
} else if (content instanceof Menu) {
contentType = 'menu';
} else {
throw Error(`Cannot create Response from ${content.constructor}`)
}
this.contentType = contentType;
this.content = content;
}
/**
* Creates a Response from a FormTag or SectionTag
* @param {FormTag|SectionTag} tag
* @returns {Response}
*/
Response.fromTag = function (tag) {
if (tag instanceof FormTag) {
return new Response(Form.fromTag(tag));
} else if (tag instanceof SectionTag) {
return new Response(Menu.fromTag(tag));
} else {
throw Error(`Cannot create response from ${tag.tagName} tag`)
}
};
Response.prototype.toJSON = function () {
return snakecase(this);
};
exports.Form = Form;
exports.Response = Response;
exports.Menu = Menu;
exports.MenuItem = MenuItem;
exports.FormItemMenu = FormItemMenu;
exports.FormItemMenuItem = FormItemMenuItem;
exports.FormItemContent = FormItemContent;
exports.FormMeta = FormMeta;
exports.MenuMeta = MenuMeta;
exports.FormItemMenuMeta = FormItemMenuMeta;
exports.parser = require('./parser');
exports.tags = require('./tag');
exports.config = require('./config');