Merge origin/master

This commit is contained in:
Edi
2019-07-23 08:27:24 +02:00
parent 62830e92a6
commit f416b0574a
44 changed files with 82250 additions and 386 deletions
+7
View File
@@ -0,0 +1,7 @@
Copyright (c) 2012-2018 Joshfire and contributors
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.
+112
View File
@@ -0,0 +1,112 @@
JSON Form
=========
![MIT license](https://img.shields.io/badge/License-MIT-blue.svg?longCache=true)
![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?longCache=true)
![Maintained](https://img.shields.io/badge/Maintained-yes-brightgreen.svg?longCache=true)
![Release](https://img.shields.io/github/release/jsonform/jsonform.svg)
![NPM: released](https://img.shields.io/npm/v/jsonform.svg)
The JSON Form library is a JavaScript client-side library that takes a
structured data model defined using [JSON Schema](http://json-schema.org/) as
input and returns a [Bootstrap 3](https://getbootstrap.com/docs/3.3/)-friendly
HTML form that matches the schema.
The generated HTML form includes client-side validation logic that provides direct inline feedback to the user upon form submission (provided a JSON Schema validator is available). If values are valid, the JSON Form library uses submitted values to create the JavaScript data structure that matches the data model.
The layout of the generated HTML form may be entirely fine-tuned through
a simple declarative mechanism.
Getting started
---------------
The example below creates a form that asks for the user's name and age. The user's name is a required field, while the age is optional.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Getting started with JSON Form</title>
<link rel="stylesheet" style="text/css" href="deps/opt/bootstrap.css" />
</head>
<body>
<h1>Getting started with JSON Form</h1>
<form></form>
<div id="res" class="alert"></div>
<script type="text/javascript" src="deps/jquery.min.js"></script>
<script type="text/javascript" src="deps/underscore.js"></script>
<script type="text/javascript" src="deps/opt/jsv.js"></script>
<script type="text/javascript" src="lib/jsonform.js"></script>
<script type="text/javascript">
$('form').jsonForm({
schema: {
name: {
type: 'string',
title: 'Name',
required: true
},
age: {
type: 'number',
title: 'Age'
}
},
onSubmit: function (errors, values) {
if (errors) {
$('#res').html('<p>I beg your pardon?</p>');
}
else {
$('#res').html('<p>Hello ' + values.name + '.' +
(values.age ? '<br/>You are ' + values.age + '.' : '') +
'</p>');
}
}
});
</script>
</body>
</html>
```
Loading this page in a browser renders a form with two input fields and a submit button. The ```onSubmit``` function is called upon form submission. If you press "Submit" without entering values or if the age you enter is not a number, error messages appear next to the input fields.
NB: Paths in this example are relative to the root of the JSON Form project.
Documentation
-------------
You can do much more with the JSON Form library. You may define a more complex data model that includes arrays and objects for instance, or you may control the layout of the form to include fieldsets, expandable sections or tabs. For more information, check the [reference documentation for JSON Form](http://github.com/joshfire/jsonform/wiki).
Playground
----------
If you're more of the acting type than of the reading type, the [JSON Form Playground](https://jsonform.github.io/jsonform/playground/index.html) is a simple JSON Form editor that lets you try out and extend all the examples in the doc.
Dependencies
------------
At a minimum, the JSON Form library depends on:
- [jQuery](http://jquery.com/)
- The [Underscore.js](http://underscorejs.org/) utility belt
The JSON Form library may require further libraries, depending on the features you need for the forms you need to render. In particular:
- [ACE](http://ace.ajax.org/) is needed to render rich text input fields. The [deps/opt/ace](https://github.com/joshfire/jsonform/tree/master/deps/opt/ace) folder contains a minimal set of files from ACE to render a JSON input field. Beware that the code of `ace.js` needs to be encapsulated in `(function(require,define,requirejs) {...})(undefined,undefined,undefined);` before it may be used within JSON Form.
- [Bootstrap](https://getbootstrap.com/docs/3.3/) v3.3 or above is more or less needed (unless you enjoy ugly forms, that is) if you don't provide your own styles. JSON Form only needs the ```bootstrap.css``` file.
- The [JSON Schema Validator](https://github.com/garycourt/JSV) is used to detect and report validation errors upon form submission. The [deps/opt](https://github.com/joshfire/jsonform/tree/master/deps/opt) folder contains a "build" of the JSON Schema Validator for use in JSON Form.
- [Bootstrap Dropdowns](https://github.com/twbs/bootstrap/blob/master/javascript.html) v2.0.3 or above is needed for ```imageselect``` fields.
- [jQuery UI Sortable](http://jqueryui.com/demos/sortable/) v1.8.20 or above is required for drag-and-drop support within arrays and tabarrays. Note the plugin itself depends on jQuery IU Core, jQuery UI Mouse, and jQuery UI Widget.
- [wysihtml5](http://jhollingworth.github.com/bootstrap-wysihtml5/) is required if the form uses ```wysihtml5``` textarea fields.
- [Spectrum](http://bgrins.github.com/spectrum/) is required if the form uses `color` fields.
All of these libraries are in the [deps](https://github.com/joshfire/jsonform/tree/master/deps) folder for now, although you might want to check their respective Web site for more recent versions.
NB: JSON Form also uses ```JSON.parse``` and ```JSON.stringify``` which is normally already natively supported by all modern browsers.
License
-------
The JSON Form library is licensed under the [MIT license](https://raw.github.com/joshfire/jsonform/master/LICENSE).
All the libraries that JSON Form may depend on are licensed under the MIT license, except for the JSON Schema Validator, licensed under the BSD 3 Clause license and the ACE editor licensed under the Mozilla tri-license (MPL/GPL/LGPL).
+331
View File
@@ -0,0 +1,331 @@
/**
* @fileoverview The JSON Form "defaults" library exposes a setDefaultValues
* method that extends the object passed as argument so that it includes
* values for all required fields of the JSON schema it is to follow that
* define a default value.
*
* The library is called to complete the configuration settings of a template in
* the Factory and to complete datasource settings.
*
* The library is useless if the settings have already been validated against the
* schema using the JSON schema validator (typically, provided the validator is
* loaded, submitting the form created from the schema will raise an error when
* required properties are missing).
*
* Note the library does not validate the created object, it merely sets missing
* values to the default values specified in the schema. All other values may
* be invalid.
*
* Nota Bene:
* - in data-joshfire, the runtime/nodejs/lib/jsonform-defaults.js file is a
* symbolic link to the jsonform submodule in deps/jsonform
* - in platform-joshfire, the server/public/js/libs/jsonform-defaults.js file
* is a symbolic link to the jsonform submodule in deps/jsonform
*/
(function () {
// Establish the root object:
// that's "window" in the browser, "global" in node.js
var root = this;
/**
* Sets default values, ensuring that fields defined as "required" in the
* schema appear in the object. If missing, the hierarchy that leads to
* a required key is automatically created.
*
* @function
* @param {Object} obj The object to complete with default values according
* to the schema
* @param {Object} schema The JSON schema that the object follows
* @param {boolean} includeOptionalValues Include default values for fields
* that are not "required"
* @param {boolean} skipFieldsWithoutDefaultValue Set flag not to include a
* generated empty default value for fields marked as "required" in the
* schema but that do not define a default value.
* @return {Object} The completed object (same instance as obj)
*/
var setDefaultValues = function (obj, schema, includeOptionalValues, skipFieldsWithoutDefaultValue) {
if (!obj || !schema) return obj;
if (!schema.properties) {
schema = { properties: schema };
}
// Inner function that parses the schema recursively to build a flat
// list of defaults
var defaults = {};
var extractDefaultValues = function (schemaItem, path) {
var properties = null;
var child = null;
if (!schemaItem || (schemaItem !== Object(schemaItem))) return null;
if (schemaItem.required) {
// Item is required
if (schemaItem['default']) {
// Item defines a default value, let's use it,
// no need to continue in that case, we have the default value
// for the whole subtree starting at schemaItem.
defaults[path] = schemaItem['default'];
return;
}
else if (skipFieldsWithoutDefaultValue) {
// Required but no default value and caller explicitly asked not
// include such fields in the returned object.
}
else if ((schemaItem.type === 'object') || schemaItem.properties) {
// Item is a required object
defaults[path] = {};
}
else if ((schemaItem.type === 'array') || schemaItem.items) {
// Item is a required array
defaults[path] = [];
}
else if (schemaItem.type === 'string') {
defaults[path] = '';
}
else if ((schemaItem.type === 'number') || (schemaItem.type === 'integer')) {
defaults[path] = 0;
}
else if (schemaItem.type === 'boolean') {
defaults[path] = false;
}
else {
// Unknown type, use an empty object by default
defaults[path] = {};
}
}
else if (schemaItem['default'] && includeOptionalValues) {
// Item is not required but defines a default value and the
// include optional values flag is set, so let's use it.
// No need to continue in that case, we have the default value
// for the whole subtree starting at schemaItem.
defaults[path] = schemaItem['default'];
return;
}
// Parse schema item's properties recursively
properties = schemaItem.properties;
if (properties) {
for (var key in properties) {
if (properties.hasOwnProperty(key)) {
extractDefaultValues(properties[key], path + '.' + key);
}
}
}
// Parse schema item's children recursively
if (schemaItem.items) {
// Items may be a single item or an array composed of only one item
child = schemaItem.items;
if (_isArray(child)) {
child = child[0];
}
extractDefaultValues(child, path + '[]');
}
};
// Build a flat list of default values
extractDefaultValues(schema, '');
// Ensure the object's default values are correctly set
for (var key in defaults) {
if (defaults.hasOwnProperty(key)) {
setObjKey(obj, key, defaults[key]);
}
}
};
/**
* Retrieves the default value for the given key in the schema
*
* Levels in the path are separated by a dot. Array items are marked
* with []. For instance:
* foo.bar[].baz
*
* @function
* @param {Object} schema The schema to parse
* @param {String} key The path to the key whose default value we're
* looking for. each level is separated by a dot, and array items are
* flagged with [x].
* @return {Object} The default value, null if not found.
*/
var getSchemaKeyDefaultValue = function(schema,key) {
var schemaKey = key
.replace(/\./g, '.properties.')
.replace(/\[.*\](\.|$)/g, '.items$1');
var schemaDef = getObjKey(schema, schemaKey);
if (schemaDef) return schemaDef['default'];
return null;
};
/**
* Retrieves the key identified by a path selector in the structured object.
*
* Levels in the path are separated by a dot. Array items are marked
* with []. For instance:
* foo.bar[].baz
*
* @function
* @param {Object} obj The object to parse
* @param {String} key The path to the key whose default value we're
* looking for. each level is separated by a dot, and array items are
* flagged with [x].
* @return {Object} The key definition, null if not found.
*/
var getObjKey = function (obj, key) {
var innerobj = obj;
var keyparts = key.split('.');
var subkey = null;
var arrayMatch = null;
var reArraySingle = /\[([0-9]*)\](?:\.|$)/;
for (var i = 0; i < keyparts.length; i++) {
if (typeof innerobj !== 'object') return null;
subkey = keyparts[i];
arrayMatch = subkey.match(reArraySingle);
if (arrayMatch) {
// Subkey is part of an array
subkey = subkey.replace(reArraySingle, '');
if (!_isArray(innerobj[subkey])) {
return null;
}
innerobj = innerobj[subkey][parseInt(arrayMatch[1], 10)];
}
else {
innerobj = innerobj[subkey];
}
}
return innerobj;
};
/**
* Sets the key identified by a path selector to the given value.
*
* Levels in the path are separated by a dot. Array items are marked
* with []. For instance:
* foo.bar[].baz
*
* The hierarchy is automatically created if it does not exist yet.
*
* Default values are added to all array items. Array items are not
* automatically created if they do not exist (in particular, the
* minItems constraint is not enforced)
*
* @function
* @param {Object} obj The object to build
* @param {String} key The path to the key to set where each level
* is separated by a dot, and array items are flagged with [x].
* @param {Object} value The value to set, may be of any type.
*/
var setObjKey = function (obj, key, value) {
var keyparts = key.split('.');
// Recursive version of setObjKey
var recSetObjKey = function (obj, keyparts, value) {
var arrayMatch = null;
var reArray = /\[([0-9]*)\]$/;
var subkey = keyparts.shift();
var idx = 0;
if (keyparts.length > 0) {
// Not the end yet, build the hierarchy
arrayMatch = subkey.match(reArray);
if (arrayMatch) {
// Subkey is part of an array, check all existing array items
// TODO: review that! Only create the right item!!!
subkey = subkey.replace(reArray, '');
if (!_isArray(obj[subkey])) {
obj[subkey] = [];
}
obj = obj[subkey];
if (arrayMatch[1] !== '') {
idx = parseInt(arrayMatch[1], 10);
if (!obj[idx]) {
obj[idx] = {};
}
recSetObjKey(obj[idx], keyparts, value);
}
else {
for (var k = 0; k < obj.length; k++) {
recSetObjKey(obj[k], keyparts, value);
}
}
return;
}
else {
// "Normal" subkey
if (typeof obj[subkey] !== 'object') {
obj[subkey] = {};
}
obj = obj[subkey];
recSetObjKey(obj, keyparts, value);
}
}
else {
// Last key, time to set the value, unless already defined
arrayMatch = subkey.match(reArray);
if (arrayMatch) {
subkey = subkey.replace(reArray, '');
if (!_isArray(obj[subkey])) {
obj[subkey] = [];
}
idx = parseInt(arrayMatch[1], 10);
if (!obj[subkey][idx]) {
obj[subkey][idx] = value;
}
}
else if (!obj[subkey]) {
obj[subkey] = value;
}
}
};
// Skip first item if empty (key starts with a '.')
if (!keyparts[0]) {
keyparts.shift();
}
recSetObjKey(obj, keyparts, value);
};
// Taken from Underscore.js (not included to save bytes)
var _isArray = Array.isArray || function (obj) {
return Object.prototype.toString.call(obj) == '[object Array]';
};
// Export the code as:
// 1. an AMD module (the "define" method exists in that case), or
// 2. a node.js module ("module.exports" is defined in that case), or
// 3. a global JSONForm object (using "root")
if (typeof define !== 'undefined') {
// AMD module
define([], function () {
return {
setDefaultValues: setDefaultValues,
setObjKey: setObjKey,
getSchemaKeyDefaultValue: getSchemaKeyDefaultValue
};
});
}
else if ((typeof module !== 'undefined') && module.exports) {
// Node.js module
module.exports = {
setDefaultValues: setDefaultValues,
setObjKey: setObjKey,
getSchemaKeyDefaultValue: getSchemaKeyDefaultValue
};
}
else {
// Export the function to the global context, using a "string" for
// Google Closure Compiler "advanced" mode
// (not sure why it's needed, done by Underscore)
root['JSONForm'] = root['JSONForm'] || {};
root['JSONForm'].setDefaultValues = setDefaultValues;
root['JSONForm'].setObjKey = setObjKey;
root['JSONForm'].getSchemaKeyDefaultValue = getSchemaKeyDefaultValue;
}
})();
+320
View File
@@ -0,0 +1,320 @@
/**
* @fileoverview The JSON Form "split" library exposes a "split" method
* that can be used to divide a JSON Form object into two disjoint
* JSON Form objects:
* - the first one includes the schema and layout of the form that
* contains the list of keys given as parameters as well as keys that
* cannot be separated from them (typically because they belong to the
* same array in the layout)
* - the second one includes the schema and layout of a form that does not
* contain the list of keys given as parameters.
*
* The options parameter lets one be explicit about whether it wants to include
* fields that are tightly coupled with the provided list of keys or not.
*/
/*global exports, _*/
(function (serverside, global, _) {
if (serverside && !_) {
_ = require('underscore');
}
/**
* Splits a JSON Form object into two autonomous JSON Form objects,
* one that includes the provided list of schema keys as well as keys
* that are tightly coupled to these keys, and the other that does not
* include these keys.
*
* The function operates on the "schema", "form", and "value" properties
* of the initial JSON Form object. It copies over the other properties
* to the resulting JSON Form objects.
*
* Note that the split function does not support "*" form definitions. The
* "form" property must be set in the provided in the provided JSON Form
* object.
*
* @function
* @param {Object} jsonform JSON Form object with a "schema" and "form"
* @param {Array(String)} keys Schema keys used to split the form. Each
* key must reference a schema key at the first level in the schema
* (in other words, the keys cannot include "." or "[]")
* @param {Object} options Split options. Set the "excludeCoupledKeys" flag
* not to include keys that are tightly coupled with the ones provided in
* the included part of the JSON Form object.
* @return {Object} An object with an "included" property whose value is
* the JSON Form object that includes the keys and an "excluded" property
* whose value is the JSON Form object that does not contain any of the
* keys. These objects may be empty.
*/
var split = function (jsonform, keys, options) {
options = options || {};
keys = keys || [];
if (!jsonform || !jsonform.form) {
return {
included: {},
excluded: {}
};
}
if (_.isString(keys)) {
keys = [keys];
}
// Prepare the object that will be returned
var result = {
included: {
schema: {
properties: {}
},
form: []
},
excluded: {
schema: {
properties: {}
},
form: []
}
};
// Copy over properties such as "value" or "tpldata" that do not need
// to be split (note both forms will reference the same structures)
_.each(jsonform, function (value, key) {
if ((key !== 'schema') && (key !== 'form')) {
result.included[key] = value;
result.excluded[key] = value;
}
});
/**
* Helper function that parses the given field and returns true if
* it references one of the keys to include directly. Note the function
* does not parse the potential children of the field and will thus
* return false even if the field actually references a key to include
* indirectly.
*
* @function
* @param {Object} formField The form field to parse
* @return {boolean} true when the field references one of the keys to
* include, false when not
*/
var formFieldReferencesKey = function (formField) {
var referencedKey = _.isString(formField) ?
formField :
formField.key;
if (!referencedKey) {
return false;
}
return _.include(keys, referencedKey) ||
!!_.find(keys, function (key) {
return (referencedKey.indexOf(key + '.') === 0) ||
(referencedKey.indexOf(key + '[]') === 0);
});
};
/**
* Helper function that parses the given field and returns true if
* it references a key that is not in the list of keys to include.
* Note the function does not parse the potential children of the field
* and will thus return false even if the field actually references a key
* to include indirectly.
*
* @function
* @param {Object} formField The form field to parse
* @return {boolean} true when the field references one of the keys to
* include, false when not
*/
var formFieldReferencesOtherKey = function (formField) {
var referencedKey = _.isString(formField) ?
formField :
formField.key;
if (!referencedKey) {
return false;
}
return !_.include(keys, referencedKey) &&
!_.find(keys, function (key) {
return (referencedKey.indexOf(key + '.') === 0) ||
(referencedKey.indexOf(key + '[]') === 0);
});
};
/**
* Helper function that parses the given field and returns true if
* it references one of the keys to include somehow (either directly
* or through one of its descendants).
*
* @function
* @param {Object} formField The form field to parse
* @return {boolean} true when the field references one of the keys to
* include, false when not
*/
var includeFormField = function (formField) {
return formFieldReferencesKey(formField) ||
formField.items && !!_.some(formField.items, function (item) {
return includeFormField(item);
});
};
/**
* Helper function that parses the given field and returns true if
* it references a key that is not one of the keys to include somehow
* (either directly or through one of its descendants).
*
* @function
* @param {Object} formField The form field to parse
* @return {boolean} true when the field references one of the keys to
* include, false when not
*/
var excludeFormField = function (formField) {
return formFieldReferencesOtherKey(formField) ||
formField.items && !!_.some(formField.items, function (item) {
return excludeFormField(item);
});
};
/**
* Converts the provided form field for inclusion in the included/excluded
* portion of the result. The function returns null if the field should not
* appear in the relevant part.
*
* Note the function is recursive.
*
* @function
* @param {Object} formField The form field to convert
* @param {string} splitPart The targeted result part, one of "included",
* "excluded", or "all". The "all" string is used in recursions to force
* the inclusion of the field even if it does not reference one of the
* provided keys.
* @param {Object} parentField Pointer to the form field parent. This
* parameter is used in recursions to preserve direct children of a
* "selectfieldset".
* @return {Object} The converted field.
*/
var convertFormField = function (formField, splitPart, parentField) {
var convertedField = null;
var keepField = formField.root ||
(splitPart === 'all') ||
(parentField && parentField.key &&
(parentField.type === 'selectfieldset')) ||
(formField.type && formField.type === 'help');
if (!keepField) {
keepField = (splitPart === 'included') && includeFormField(formField);
}
if (!keepField) {
keepField = (splitPart === 'excluded') && excludeFormField(formField);
if (keepField && !options.excludeCoupledKeys) {
keepField = !includeFormField(formField);
}
}
if (!keepField) {
return null;
}
var childPart = splitPart;
if ((childPart === 'included') &&
!options.excludeCoupledKeys &&
!formField.root) {
childPart = 'all';
}
// Make a shallow copy of the field since we will preserve all of its
// properties (save perhaps "items")
convertedField = _.clone(formField);
// Recurse through the descendants of the field
if (convertedField.items) {
convertedField.items = _.map(convertedField.items, function (field) {
return convertFormField(field, childPart, convertedField);
});
convertedField.items = _.compact(convertedField.items);
}
return convertedField;
};
/**
* Helper function that checks the given schema key definition
* and returns true when the definition is referenced in the given
* form field definition
*
* @function
* @param {Object} formField The form field to check
* @param {string} schemaKey The key to search in the form field
* @return {boolean} true if the form field references the key somehow,
* false otherwise.
*/
var includeSchemaKey = function (formField, schemaKey) {
if (!formField) return false;
if (!schemaKey) return false;
if (_.isString(formField)) {
// Direct reference to a key in the schema
return (formField === schemaKey) ||
(formField.indexOf(schemaKey + '.') === 0) ||
(formField.indexOf(schemaKey + '[]') === 0);
}
if (formField.key) {
if ((formField.key === schemaKey) ||
(formField.key.indexOf(schemaKey + '.') === 0) ||
(formField.key.indexOf(schemaKey + '[]') === 0)
) {
return true;
}
}
return !!_.some(formField.items, function (item) {
return includeSchemaKey(item, schemaKey);
});
};
// Prepare the included/excluded forms
var converted = null;
converted = convertFormField({
items: jsonform.form,
root: true
}, 'included');
if (converted) {
result.included.form = converted.items;
}
converted = convertFormField({
items: jsonform.form,
root: true
}, 'excluded');
if (converted) {
result.excluded.form = converted.items;
}
// Split the schema into two schemas.
// (note that the "excluded" JSON Form object may contain keys that
// are never referenced in the initial JSON Form layout. That's normal)
var schemaProperties = jsonform.schema;
if (schemaProperties.properties) {
schemaProperties = schemaProperties.properties;
}
_.each(schemaProperties, function (schemaDefinition, schemaKey) {
if (_.some(result.included.form, function (formField) {
return includeSchemaKey(formField, schemaKey);
})) {
result.included.schema.properties[schemaKey] = schemaDefinition;
}
else {
result.excluded.schema.properties[schemaKey] = schemaDefinition;
}
});
return result;
};
global.JSONForm = global.JSONForm || {};
global.JSONForm.split = split;
})((typeof exports !== 'undefined'),
((typeof exports !== 'undefined') ? exports : window),
((typeof _ !== 'undefined') ? _ : null));
+3712
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "jsonform",
"version": "2.1.4",
"main": "./lib/jsonform.js",
"user": "jsonform",
"repository": "https://github.com/jsonform/jsonform",
"description": "Client-side JavaScript library that generates HTML forms from structured data models expressed using a JSON schema, possibly completed by a form layout description.",
"keywords": [
"JSON",
"schema",
"HTML5",
"form"
],
"maintainers": [
{
"name": "Joshfire",
"email": "contact@joshfire.com",
"web": "http://www.joshfire.com"
},
{
"name": "tchapi",
"email": "regbasket+github@gmail.com",
"web": "https://github.com/tchapi"
}
],
"license": "MIT"
}