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;
/**
Instantiates a new Form
@class Form
@classdesc A Form object as defined in the JSON schema
@param {object} props - Properties to initialize the Form with
@param {Array<FormItem>} props.body - Sets {@link Form#body}
@param {('GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD'|'OPTIONS'|'TRACE')} props.method='POST' - Sets {@link Form#method}
@param {string} props.path - Sets {@link Form#path}
@param {string} [props.header] - Sets {@link Form#header}
@param {string} [props.footer] - Sets {@link Form#footer}
@param {FormMeta} [props.meta] - Sets {@link Form#meta}
*/
function Form(props) {
if (!props.body || !props.path) {
throw Error('(body, path) are mandatory');
}
/**
This is the Form's type
@name Form#type
@type {string}
@default "form"
@readonly
*/
this.type = 'form';
/**
This is the Form's body
@name Form#body
@type {Array<FormItem>}
*/
this.body = props.body;
/**
This is the Form's method
@name Form#method
@type {string}
@default "POST"
*/
this.method = props.method || 'POST';
/**
This is the Form's path
@name Form#path
@type {string}
*/
this.path = props.path;
/**
This is the Form's generic header. It is used in those `FormItem`s
which don't define a header.
@name Form#header
@type {string}
*/
this.header = props.header || null;
/**
This is the Form's footer. It is inherited by those `FormItem`s
which don't define a footer.
@name Form#footer
@type {string}
*/
this.footer = props.footer || null;
/**
This is the Form's meta. It contains the form configuration
@name Form#meta
@type {FormMeta}
*/
this.meta = props.meta || null;
}
/**
* Creates a Form from a FormTag
* @param {FormTag} formTag
* @returns {Form}
*/
Form.fromTag = function (formTag) {
let body = [];
for (const sectionTag of formTag.children) {
body.push(FormItem.fromTag(sectionTag));
}
return new Form({
body: body,
method: formTag.attrs.method,
path: formTag.attrs.action,
header: formTag.attrs.header,
footer: formTag.attrs.footer,
meta: new FormMeta({
completionStatusShow: formTag.attrs.completionStatusShow,
completionStatusInHeader: formTag.attrs.completionStatusInHeader,
confirmationNeeded: formTag.attrs.confirmationNeeded
})
});
};
/**
Instantiates a new FormMeta
@class FormMeta
@classdesc A FormMeta object as defined in the JSON schema
@param {object} props - Properties to initialize the FormMeta with
@param {boolean} [props.completionStatusShow] - Sets {@link FormMeta#completionStatusShow}
@param {boolean} [props.completionStatusInHeader] - Sets {@link FormMeta#completionStatusInHeader}
@param {boolean} [props.confirmationNeeded] - Sets {@link FormMeta#confirmationNeeded}
*/
function FormMeta(props) {
/**
Whether to show or not the completion status in each {@link FormItem}.
@name FormMeta#completionStatusShow
@type {boolean}
@default false
*/
this.completionStatusShow = props.completionStatusShow;
/**
Whether to show or not the completion status in header. It has effect
only if {@link FormMeta#completionStatusShow} is `true`.
@name FormMeta#completionStatusInHeader
@type {boolean}
@default false
*/
this.completionStatusInHeader = props.completionStatusInHeader;
/**
Whether to show an extra step at the end of the {@link Form} to visualize
and check the form responses.
@name FormMeta#confirmationNeeded
@type {boolean}
@default false
*/
this.confirmationNeeded = props.confirmationNeeded;
}
/**
* Instantiates a new FormItem
*
* @class FormItem
* @classdesc A FormItem object as defined in the JSON schema
*
* @param {object} props - Properties to initialize the form item with
* @param {('string'|'date'|'datetime'|'int'|'float'|'hidden'|'form-menu'|
* 'email'|'url'|'location')} props.type - Sets {@link FormItem#type}
* @param {string} props.name - Sets {@link FormItem#name}
* @param {string} props.description - Sets {@link FormItem#description}
* @param {string} [props.header] - Sets {@link FormItem#header}
* @param {string} [props.footer] - Sets {@link FormItem#footer}
* @param {Array<MenuItemFormItem>} [props.body] - Sets {@link FormItem#body}
* @param {string} [props.value] - Sets {@link FormItem#value}
* @param {string} [props.chunkingFooter] - Sets {@link FormItem#chunkingFooter}
* @param {string} [props.confirmationLabel] - Sets {@link FormItem#confirmationLabel}
* @param {number} [props.minLength] - Sets {@link FormItem#minLength}. It must be integer.
* @param {string} [props.minLengthError] - Sets {@link FormItem#minLengthError}
* @param {number} [props.maxLength] - Sets {@link FormItem#maxLength}. It must be integer.
* @param {string} [props.maxLengthError] - Sets {@link FormItem#maxLengthError}
* @param {number} [props.minValue] - Sets {@link FormItem#minValue}
* @param {string} [props.minValueError] - Sets {@link FormItem#minValueError}
* @param {number} [props.maxValue] - Sets {@link FormItem#maxValue}
* @param {string} [props.maxValueError] - Sets {@link FormItem#maxValueError}
* @param {MenuFormItemMeta} [props.meta] - Sets {@link FormItem#meta}
* @param {string} [props.method] - Sets {@link FormItem#method}
* @param {boolean} [props.required=false] - Sets {@link FormItem#required}
* @param {boolean} [props.statusExclude=false] - Sets {@link FormItem#statusExclude}
* @param {boolean} [props.statusPrepend=false] - Sets {@link FormItem#statusPrepend}
* @param {string} [props.url] - Sets {@link FormItem#url}
* @param {string} [props.validateTypeError] - Sets {@link FormItem#validateTypeError}
* @param {string} [props.validateTypeErrorFooter] - Sets {@link FormItem#validateTypeErrorFooter}
* @param {string} [props.validateUrl] - Sets {@link FormItem#validateUrl}
* @constructor
*/
function FormItem(props) {
/**
This is the FormItem's type
@name FormItem#type
@type {string}
*/
this.type = props.type;
const supportedTypes = [
'date', 'datetime', 'email', 'form-menu', 'float', 'hidden', 'int',
'location', 'string', 'url'
];
if (supportedTypes.indexOf(this.type) === -1) {
throw Error(`FormItem type="${this.type}" is not supported. Supported types: ${supportedTypes}`);
}
/**
This is the FormItem's name. Each form item name must be unique within the same form.
@name FormItem#name
@type {string}
*/
this.name = props.name;
/**
This is the FormItem's displayed text.
@name FormItem#description
@type {string}
*/
this.description = props.description || ""; // description is required so it cannot be null
/**
This is the FormItem's header. If defined, it overrides {@link Form#header}.
@name FormItem#header
@type {string}
*/
this.header = props.header || null;
/**
This is the FormItem's footer. If defined, it overrides {@link Form#footer}.
@name FormItem#footer
@type {string}
*/
this.footer = props.footer || null;
/**
This is the FormItem's body.
@name FormItem#body
@type {Array<MenuItemFormItem>}
*/
this.body = props.body || null;
/**
`value` must be set only if {@link FormItem#type} is `hidden`.
@name FormItem#value
@type {string}
*/
this.value = props.value || null;
if (this.value == null) {
if (this.type === 'hidden') {
throw Error('value is required when type="hidden"');
}
}
/**
This is the FormItem's chunking footer.
@name FormItem#chunkingFooter
@type {string}
*/
this.chunkingFooter = props.chunkingFooter || null;
/**
This is the FormItem's confirmation label.
@name FormItem#confirmationLabel
@type {string}
*/
this.confirmationLabel = props.confirmationLabel || null;
/**
This defines the minimum length of the input if {@link FormItem#type}
is `string`. It must be an integer.
@name FormItem#minLength
@type {number}
*/
this.minLength = props.minLength || null;
/**
This is the error for {@link FormItem#minLength}.
@name FormItem#minLengthError
@type {string}
*/
this.minLengthError = props.minLengthError || null;
/**
This defines the maximum length of the input if {@link FormItem#type}
is `string`. It must be an integer.
@name FormItem#maxLength
@type {number}
*/
this.maxLength = props.maxLength || null;
/**
This is the error for {@link FormItem#maxLength}.
@name FormItem#maxLengthError
@type {string}
*/
this.maxLengthError = props.maxLengthError || null;
/**
This defines the minimum value of the input if {@link FormItem#type}
is `int` or `float`.
@name FormItem#minValue
@type {number}
*/
this.minValue = props.minValue || null;
/**
This is the error for {@link FormItem#minValue}.
@name FormItem#minValueError
@type {string}
*/
this.minValueError = props.minValueError || null;
/**
This defines the maximum value of the input if {@link FormItem#type}
is `int` or `float`.
@name FormItem#maxValue
@type {number}
*/
this.maxValue = props.maxValue || null;
/**
This is the error for {@link FormItem#maxValue}.
@name FormItem#minValueError
@type {string}
*/
this.maxValueError = props.maxValueError || null;
/**
This must be defined if {@link FormItem#type} is `form-item`.
@name FormItem#meta
@type {MenuFormItemMeta}
*/
this.meta = props.meta || null;
/**
This is the FormItem's method.
@name FormItem#method
@type {string}
*/
this.method = props.method || null;
/**
Whether the form item is required to be answered or not.
@name FormItem#required
@type {boolean}
@default false
*/
this.required = props.required || false;
/**
Whether the form item's status should be excluded or not.
@name FormItem#statusExclude
@type {boolean}
@default false
*/
this.statusExclude = props.statusExclude || false;
/**
Whether the form item's status should be prepended or not.
@name FormItem#statusPrepend
@type {boolean}
@default false
*/
this.statusPrepend = props.statusPrepend || false;
/**
This is the FormItem's url.
@name FormItem#url
@type {string}
*/
this.url = props.url || null;
/**
This is the FormItem's validation type error.
@name FormItem#validateTypeError
@type {string}
*/
this.validateTypeError = props.validateTypeError || null;
/**
This is the FormItem's validation type error footer.
@name FormItem#validateTypeErrorFooter
@type {string}
*/
this.validateTypeErrorFooter = props.validateTypeErrorFooter || null;
/**
This is the FormItem's validation url.
@name FormItem#validateUrl
@type {string}
*/
this.validateUrl = props.validateUrl || null;
}
/**
* Creates a FormItem from a SectionTag
* @param {SectionTag} sectionTag
* @returns {FormItem}
*/
FormItem.fromTag = function (sectionTag) {
let header,
footer,
body = [],
value,
minValue,
minValueError,
minLength,
minLengthError,
maxValue,
maxValueError,
maxLength,
maxLengthError,
formItemType,
description;
for (const child of sectionTag.children) {
if (child instanceof InputTag) {
const inputType = child.attrs.type;
if (inputType === 'number') {
if (child.attrs.step === 1) {
formItemType = 'int';
} else {
formItemType = 'float';
}
} else if (inputType === 'hidden') {
value = child.attrs.value;
formItemType = 'hidden';
if (value === undefined) {
throw Error('value attribute is required for input type="hidden"');
}
} else {
switch (inputType) {
case 'text':
formItemType = 'string';
break;
case 'date':
formItemType = 'date';
break;
case 'datetime':
formItemType = 'datetime';
break;
case 'url':
formItemType = 'url';
break;
case 'email':
formItemType = 'email';
break;
case 'location':
formItemType = 'location';
break;
default:
throw Error(`<input/> type "${inputType}" is not supported`);
}
}
minValue = child.attrs.min;
minValueError = child.attrs.minError;
minLength = child.attrs.minlength;
minLengthError = child.attrs.minlengthError;
maxValue = child.attrs.max;
maxValueError = child.attrs.maxError;
maxLength = child.attrs.maxlength;
maxLengthError = child.attrs.maxlengthError;
description = sectionTag.toString(true, true);
break; // ignore other <input> tags if exist
}
if (child instanceof UlTag) {
formItemType = 'form-menu';
let menuItemFormItem;
// reiterate through the section's children to render them in body
for (const child2 of sectionTag.children) {
if (child2 instanceof UlTag) {
for (const li of child2.children) {
menuItemFormItem = MenuItemFormItem.fromTag(li);
if (menuItemFormItem !== undefined) {
body.push(menuItemFormItem);
}
}
} else if (child2 instanceof HeaderTag) {
} else if (child2 instanceof FooterTag) {
} else {
menuItemFormItem = MenuItemFormItem.fromTag(child2);
if (menuItemFormItem !== undefined) {
body.push(menuItemFormItem);
}
}
}
break;
}
}
if (!formItemType) {
throw Error('When <section> plays the role of a form item, ' +
'it must contain a <input/> or <ul></ul>'
)
}
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 FormItem({
type: formItemType,
name: sectionTag.attrs.name,
description: description,
header: header || sectionTag.attrs.header,
footer: footer || sectionTag.attrs.footer,
body: body.length === 0 ? undefined : body,
value: value,
chunkingFooter: sectionTag.attrs.chunkingFooter,
confirmationLabel: sectionTag.attrs.confirmationLabel,
minLength: minLength,
minLengthError: minLengthError,
maxLength: maxLength,
maxLengthError: maxLengthError,
minValue: minValue,
minValueError: minValueError,
maxValue: maxValue,
maxValueError: maxValueError,
meta: new MenuFormItemMeta({
autoSelect: sectionTag.attrs.autoSelect,
multiSelect: sectionTag.attrs.multiSelect,
numbered: sectionTag.attrs.numbered
}),
method: sectionTag.attrs.method,
required: sectionTag.attrs.required,
statusExclude: sectionTag.attrs.statusExclude,
statusPrepend: sectionTag.attrs.statusPrepend,
url: sectionTag.attrs.url,
validateTypeError: sectionTag.attrs.validateTypeError,
validateTypeErrorFooter: sectionTag.attrs.validateTypeErrorFooter,
validateUrl: sectionTag.attrs.validateUrl
});
}
;
/**
Instantiates a new MenuFormItemMeta
@class MenuFormItemMeta
@classdesc A MenuFormItemMeta object as defined in the JSON schema
@param {object} props - Properties to initialize the MenuFormItemMeta object with
@param {boolean} [props.autoSelect=false] - Sets {@link MenuFormItemMeta#autoSelect}
@param {boolean} [props.multiSelect=false] - Sets {@link MenuFormItemMeta#multiSelect}
@param {boolean} [props.numbered=false] - Sets {@link MenuFormItemMeta#numbered}
*/
function MenuFormItemMeta(props) {
/**
Will be automatically selected if set to true and in case of a single
option in the menu.
@name MenuFormItemMeta#autoSelect
@type {boolean}
@default false
*/
this.autoSelect = props.autoSelect || false;
/**
It allows multiple options to be selected.
@name MenuFormItemMeta#multiSelect
@type {boolean}
@default false
*/
this.multiSelect = props.multiSelect || false;
/**
Display numbers instead of letter option markers.
@name MenuFormItemMeta#numbered
@type {boolean}
@default false
*/
this.numbered = props.numbered || false;
}
/**
* Instantiates a new MenuItemFormItem
*
* @class MenuItemFormItem
* @classdesc A MenuItemFormItem object as defined in the JSON schema. It
* represents an item in a form's menu.
*
* @param {object} props - Properties to initialize the MenuItemFormItem with
* @param {string} props.description - Sets {@link MenuItemFormItem#description}
* @param {string} [props.textSearch] - Sets {@link MenuItemFormItem#textSearch}
* @param {string} [props.value] - Sets {@link MenuItemFormItem#value}
*/
function MenuItemFormItem(props) {
/**
The type of a menu item inside a form, either `"option"` or `"content"`.
@name MenuItemFormItem#type
@type {string}
@readonly
*/
this.type = 'content';
if (props.value !== undefined) {
this.type = 'option';
}
/**
The description of this MenuItemFormItem.
@name MenuItemFormItem#description
@type {string}
*/
this.description = (props.description === undefined) ? null : props.description;
/**
The value of this MenuItemFormItem, used in form serialization.
@name MenuItemFormItem#value
@type {string}
*/
this.value = props.value || null;
/**
Field to add more context for searching in options.
@name MenuItemFormItem#textSearch
@type {string}
*/
this.textSearch = props.textSearch || null;
}
/**
* Creates a MenuItemFormItem from a SectionTag's child
* @param tag
* @returns {MenuItemFormItem}
*/
MenuItemFormItem.fromTag = function (tag) {
let description,
textSearch,
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) {
value = tag.attrs.value;
textSearch = tag.attrs.textSearch;
}
return new MenuItemFormItem({
description: description,
value: value,
textSearch: textSearch
});
};
/**
* Instantiates a new Menu
*
* @class Menu
* @classdesc A Menu object as defined in the JSON schema. It represents
* a top level component that permits displaying a navigable menu or a plain text.
*
* @param {object} props - Properties to initialize the menu with
* @param {Array<MenuItem>} props.body - Sets {@link Menu#body}
* @param {string} [props.header] - Sets {@link Menu#header}
* @param {string} [props.footer] - Sets {@link Menu#footer}
* @param {MenuMeta} [props.meta] - Sets {@link Menu#meta}
*/
function Menu(props) {
/**
The type of the Menu object is always "menu".
@name Menu#type
@type {string}
@readonly
*/
this.type = "menu";
/**
The body/content of the menu.
@name Menu#body
@type {Array<MenuItem>}
@default "menu"
*/
this.body = props.body;
/**
The header of the menu.
@name Menu#header
@type {string}
*/
this.header = props.header || null;
/**
The footer of the menu.
@name Menu#footer
@type {string}
*/
this.footer = props.footer || null;
/**
Configuration fields for menu.
@name Menu#meta
@type {MenuMeta}
*/
this.meta = props.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: body,
header: header || sectionTag.attrs.header,
footer: footer || sectionTag.attrs.footer,
meta: new MenuMeta({
autoSelect: sectionTag.attrs.autoSelect
})
});
};
/**
Instantiates a new MenuMeta
@class MenuMeta
@classdesc A MenuMeta object as defined in the JSON schema. It contains
configuration fields for {@link Menu}.
@param {object} props - Properties to initialize the menu meta with
@param {boolean} [props.autoSelect=false] - Sets {@link MenuMeta.autoSelect}
*/
function MenuMeta(props) {
/**
If the Menu has only one option, it is automatically selected, without
asking the user for selection.
@name MenuMeta#autoSelect
@type {boolean}
@default false
*/
this.autoSelect = props.autoSelect || false;
}
/**
Instantiates a new MenuItem
@class MenuItem
@classdesc A MenuItem object as defined in the JSON schema. It represents an item
in a menu. Depending on its type, a menu item can be either an option
(type=option) or an option separator (type=content).
@param {object} props - Properties to initialize the menu item with.
@param {string} props.description - Sets {@link MenuItem#description}
@param {string} [props.textSearch] - Sets {@link MenuItem#textSearch}
@param {('GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD'|'OPTIONS'|'TRACE')} [props.method] - Sets {@link MenuItem#method}
@param {string} [props.path] - Sets {@link MenuItem#path}
*/
function MenuItem(props) {
/**
The type of the menu item (`content` or `option`).
@name MenuItem#type
@type {string}
@readonly
*/
this.type = 'content';
if (props.path !== undefined) {
this.type = 'option';
}
/**
The displayed text of a menu item.
@name MenuItem#description
@type {string}
*/
this.description = props.description;
/**
Field to add more context for searching in options.
@name MenuItem#textSearch
@type {string}
*/
this.textSearch = props.textSearch || null;
/**
The HTTP method called when the menu item is selected.
@name MenuItem#method
@type {string}
*/
this.method = props.method || null;
/**
The path called when the menu item is selected.
@name MenuItem#path
@type {string}
*/
this.path = props.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,
textSearch,
path;
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;
textSearch = tag.attrs.textSearch;
}
return new MenuItem({
description: description,
textSearch: textSearch,
method: method,
path: path
});
};
/**
* Instantiates a Response object
*
* @class Response
* @classdesc A Response object as defined in the JSON schema. It can be
* built only from a top level object (Menu, Form).
*
* @param {Form|Menu} content - A {@link Menu} or a {@link Form} to
* initialize the response with.
*/
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}`)
}
/**
The type of the content of the response, either `"form"` or `"menu"`.
@name Response#contentType
@type {string}
*/
this.contentType = contentType;
/**
The content of the response, either a {@link Form} or a {@link Menu}.
@name Response#content
@type {Form|Menu}
*/
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.MenuItemFormItem = MenuItemFormItem;
exports.FormItem = FormItem;
exports.FormMeta = FormMeta;
exports.MenuMeta = MenuMeta;
exports.MenuFormItemMeta = MenuFormItemMeta;
exports.parser = require('./parser');
exports.tags = require('./tag');
exports.config = require('./config');