UNPKG

fluid-form

Version:
309 lines (282 loc) 8.32 kB
'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(); }