fluid-form
Version:
Fluid form generator
309 lines (282 loc) • 8.32 kB
JavaScript
'use strict';
var dslap = require('dslap');
module.exports = fluidFormParserService;
/*@ngInject*/
function fluidFormParserService() {
return formParser;
}
/*
* Field specifier syntax: [name*repeat:format?condition]
*
* `repeat*` is optional.
* `?condition` is optional.
*
* [name:text] text
* [name:int] int
* [name:red|blue|yellow] choice
* [name:date] date
*
* Expression may contain words, groups, and field specifications.
*
* Conditional:
* {if condition: <expression>}
*
* Dependency (like $if groups without the nesting)
* {when condition:} <expression>
*
* Echo (field value echoed as text):
* {=name}
*/
var letter = '\\u00BF-\\u1FFF\\u2C00-\\uD7FF\\w';
var punctuation = '-+.,:;!?&()\'"';
var letterPunc = punctuation + letter;
var special = '[{}\\[\\]\\s]';
var notSpecial = '[^{}\\[\\]\\s]';
var rxChoice = /[^|\]]+(\|[^|\]]+)+/;
var rxWord = new RegExp('[' + letter + '][' + letterPunc + ']*');
var rxPunctuation = new RegExp('[' + punctuation + ']');
function formLanguage() {
return dslap.util.languageBuilder({
$root: 'expression',
expression: ['field', 'group', 'word', 'newline', 'punctuation', 'submit'],
whitespace: { entity: ' ', samePostgroups: true },
newline: { entity: '\n', samePostgroups: true },
punctuation: { entity: rxPunctuation, samePostgroups: true },
word: { entity: rxWord, samePostgroups: true },
field: { start: '[', end: ']', subgroups: ['fieldName'], samePostgroups: true },
group: { start: '{', end: '}', subgroups: ['when', 'if', 'echo'], samePostgroups: true },
when: { entity: 'when', postgroups: ['whenCondition'] },
'if': { entity: 'if', postgroups: ['ifCondition'] },
whenCondition: { entity: /[^:}]+/, postgroups: ['whenConditionEnd'] },
ifCondition : { entity: /[^:]+/, postgroups: ['ifConditionEnd'] },
whenConditionEnd: { entity: ':', postgroups: [] },
ifConditionEnd : { entity: ':', postgroups: ['expression'] },
echo: { entity: '=', postgroups: ['echoExpression'] },
echoExpression: { entity: /[^}]+/, postgroups: [] },
fieldName: { entity: /\w+/, postgroups: ['fieldNameSpecDelim', 'fieldNameRepeatDelim'] },
fieldNameRepeatDelim: { entity: '*', postgroups: ['fieldRepeat'] },
fieldRepeat: { entity: /[^:]+/, postgroups: ['fieldNameSpecDelim'] },
fieldNameSpecDelim: { entity: ':', postgroups: ['fieldSpec'] },
fieldSpec: ['fieldText', 'fieldInt', 'fieldDate', 'fieldChoice'],
fieldText: { entity: 'text' },
fieldInt: { entity: 'int' },
fieldChoice: { entity: rxChoice },
fieldDate: { entity: 'date' },
submit: { start: '[', end: ']', subgroups: ['submit2'], samePostgroups: true },
submit2: { start: '[', end: ']', subgroups: ['submitLabel'] },
submitLabel: { entity: /[^\]]+/ },
}, {
parseMarkers: true,
extra: {
aether: 'whitespace'
}
});
}
function formLanguageOpts() {
return {
backtrack: true,
originalStrings: true
};
}
function formParser(formSpec) {
/* Parse tree */
var tree = dslap.parsers.recursive(formSpec, formLanguage(), formLanguageOpts());
/* Stack */
var conditions = [];
/* Output list */
var clauses = [];
/* Yo mum is so fat she can flatten the parse tree to a token list in O(1) time just by sitting on it */
parseGroup(tree);
return clauses;
function parseGroup(groupNode, start) {
var nodes = groupNode.groups;
var i = start || 0;
for (; i < nodes.length; i++) {
var term = nodes[i];
var str = term.content;
var clause;
if (term.type === 'whitespace') {
continue;
} else if (term.type === 'newline') {
clause = new Newline();
} else if (term.type === 'word') {
clause = new Phrase(str);
} else if (term.type === 'punctuation') {
clause = new Punctuation(str);
} else if (term.type === 'group') {
var first = term.groups.length ? term.groups[0] : { type: null };
var second = first ? term.groups[1] : null;
if (first.type === 'when') {
conditions.push(second.content);
continue;
} else if (first.type === 'if') {
conditions.push(second.content);
parseGroup(term, 3);
conditions.pop();
continue;
} else if (first.type === 'echo') {
clause = new Echo(second.content);
} else {
parseGroup(term);
}
} else if (term.type === 'field') {
clause = parseField(term);
} else if (term.type === 'submit') {
clause = new SubmitButton(term.groups[0].groups[0].content);
}
if (!clause) {
throw new Error('Parser error :(');
}
clause.conditions = [].slice.apply(conditions);
clauses.push(clause);
}
}
function parseField(fieldNode) {
var nodes = fieldNode.groups;
var i = 0;
var field;
var name;
var repeat = 1;
/* fieldName */
name = nodes[i].content;
i++;
/* fieldNameRepeat{Delim,} */
if (nodes[i].type === 'fieldNameRepeatDelim') {
repeat = nodes[i+1].content;
i += 2;
}
/* fieldNameSpecDelim */
i++;
var m;
/* fieldText */
if (nodes[i].type === 'fieldText') {
field = new TextField();
i++;
}
/* fieldInt{Low,Sep,High} */
else if (nodes[i].type === 'fieldInt') {
field = new IntField();
i++;
}
/* fieldDate{,Format,RangeDelim,Start,Sep,End} */
else if (nodes[i].type === 'fieldDate') {
field = new DateField();
i++;
}
/* fieldChoice [fieldChoiceDelim fieldChoice]... */
else if (nodes[i].type === 'fieldChoice') {
var opts = nodes[i].content.split('|');
field = new ChoiceField(opts);
i++;
}
/* Oops */
else {
throw new Error('Parser bug :( failed to parse ' + fieldNode.content);
}
if (!field) {
throw new Error('Unknown field type: ' + nodes[i].type);
}
if (i < nodes.length) {
throw new Error('Invalid field specification: ' + fieldNode.content);
}
field.name = name;
field.label = field.label || name;
field.repeat = repeat;
return field;
}
}
function Clause(type) {
this.type = type;
this.visible = false;
this.label = '';
this.conditions = null;
this.repeat = 1;
}
function Phrase(str) {
Clause.call(this, 'phrase');
this.isPhrase = true;
this.label = str;
}
Phrase.prototype = new Clause();
function Punctuation(str) {
Clause.call(this, 'phrase');
this.isPunctuation = true;
this.label = str;
}
Punctuation.prototype = new Clause();
function Newline() {
Clause.call(this, 'newline');
this.isNewline = true;
}
Newline.prototype = new Clause();
function Echo(expr) {
Clause.call(this, 'echo');
this.isEcho = true;
this.label = expr;
}
Echo.prototype = new Clause();
function Field(type) {
Clause.call(this, type);
this.isField = true;
this.value = '';
}
Field.prototype = new Clause();
function TextField() {
Field.call(this, 'text');
}
TextField.prototype = new Field();
function ChoiceField(choices) {
Field.call(this, 'choice');
var def = null;
for (var i = 0; i < choices.length; i++) {
if (/\*$/.test(choices[i])) {
def = choices[i] = choices[i].substr(0, choices[i].length - 1);
break;
}
}
this.choices = choices;
this.value = def;
}
ChoiceField.prototype = new Field();
function IntField() {
Field.call(this, 'int');
}
IntField.prototype = new Field();
function DateField() {
Field.call(this, 'date');
}
DateField.prototype = new Field();
function SubmitButton(str) {
Clause.call(this, 'submit');
this.label = str;
}
SubmitButton.prototype = new Clause();
function test() {
var tests = [
'I\'m plain text!',
'text field [name:text]',
'repeated text field [name*count:text]',
'choice field [name:yes|no]',
'choice field [name:red|blue|green]',
'int field [name:int]',
'date field [name:date]',
'date [alpha:date] lol potato [n:int] lemon [gamma*n:date] and [name:text] or [choosy:red|green|blue] lol',
'{if condition:expression}',
'{if condition: expression}',
'{when condition:} expression',
'{when condition: } expression',
'{when condition} expression',
'{=obj.prop[idx].func()}',
'I, [name:text] {when set(name)} am [sex:male|female] and my favourite {if sex==="male":football team} {if sex==="female":rugby team} is [team:text]. {when set(team)} Go {=team}!',
'[[submit button]]'
];
tests.forEach(function (test) {
console.log('\x1b[1m' + test + '\x1b[0m');
var res = formParser.bind(dummyParse)(test);
console.log(JSON.stringify(res, null, 4));
});
function dummyParse() { return function () {}; }
}
if (typeof window === 'undefined') {
test();
}