Appearance
Overview
Parsing formdown
Forms are defined via PEG.js syntax.
parser-syntax.pegjs
js
{
var result = {
definition: null,
sections: [],
fields: {},
placeholders: [],
conditions: {},
//typePositions: [],
log: ""
}
var nextConditionId=0;
addSection(0, 0, {offset:0, level:0, title:'', type:'ROOT', directives:[]});
function addSection(tokenOffset, tokenLength, section){
if(currentSection() && section.level > currentSection().level+1){
var virtualSection = {
offset: section.offset,
level: section.level-1,
title: '',
type: 'VIRTUAL',
directives: []
}
addSection(tokenOffset, 0, virtualSection);
}
// close previous section:
if(section.type !== "ROOT"){
var previousSection = currentSection();
previousSection.length = tokenOffset - previousSection.offset;
}
var newSection = {
offset: tokenOffset,
offsetExcludeDirectives: section.offset,
offsetExcludeTitle: tokenOffset+tokenLength,
length: null,
lengthIncludeSubsections: null,
level: section.level,
title: section.title,
type: section.type,
style: {},
tags: {},
placeholders: [],
paragraphs: []
};
applyDirectives(newSection, section.directives);
newSection.index = result.sections.length;
result.sections.push(newSection);
addNextParagraph();
}
function currentSection(){
return result.sections[result.sections.length-1];
}
function addNextParagraph(){
var newParagraph = {
offset: null,
length: null,
hasText: false,
hasPlaceholders: false,
placeholders: [],
style: {}
};
currentSection().paragraphs.push(newParagraph);
}
function completeCurrentParagraph(tokenOffset, tokenLength, offsetExcludeDirectives, directives){
if(currentParagraph() && !currentParagraph().length){
currentParagraph().offset = tokenOffset;
currentParagraph().offsetExcludeDirectives = offsetExcludeDirectives;
currentParagraph().length = tokenLength;
applyDirectives(currentParagraph(), directives)
}
}
function currentParagraph(){
if(currentSection() && currentSection().paragraphs.length > 0)
return currentSection().paragraphs[currentSection().paragraphs.length-1];
else
return undefined;
}
function getField(fieldName){
if(!result.fields[fieldName]){
var newField = {
name: fieldName,
cardinality: null,
defaultValue: null,
description: null,
filter: null,
required: false,
type: null,
subType: null
};
result.fields[fieldName] = newField;
addTypeDefinitionToField(fieldName, {});
}
return result.fields[fieldName];
}
function addPlaceholder(tokenOffset, tokenLength, directives, fieldName, label){
var field = null;
if(fieldName) {
// var newPosition = {
// offset: tokenOffset,
// length: tokenLength,
// label: label,
// control: null,
// style: {}
// }
field = getField(fieldName);
//applyDirectives(newPosition, directives);
//field.positions.push(newPosition);
}
var newPlaceholder = {
field: field,
offset: tokenOffset,
length: tokenLength,
label: label,
control: null,
style: {}
}
if(directives)
applyDirectives(newPlaceholder, directives);
newPlaceholder.index = result.placeholders.length;
result.placeholders.push(newPlaceholder);
currentSection().placeholders.push(newPlaceholder);
currentParagraph().placeholders.push(newPlaceholder);
currentParagraph().hasPlaceholders = true;
}
function applyDirectives(item, directives){
directives.forEach(function(directive){
// convert convenience negations of readonly and hide
if((directive.type === "STYLE") && (directive.condition)){
if(directive.params[0] === "show"){
directive.params[0] = "hidden";
directive.condition = "!(" + directive.condition + ")";
}
else if(directive.params[0] === "editable"){
directive.params[0] = "readonly";
directive.condition = "!(" + directive.condition + ")";
}
}
// hide is now also allowed as an alias for hidden
if((directive.type === "STYLE") && (directive.params[0] === "hide")){
directive.params[0] === "hidden"
}
var condition;
if(directive.condition){
var cid = 'cond' + nextConditionId++;
condition = {
id: cid,
expression: directive.condition
}
result.conditions[cid] = directive.condition;
}
if(directive.type === "CONTROL"){
var newControl = {
namespace: directive.controlNamespace,
name: directive.controlName,
fullName: directive.controlFullName,
parameters: {}
};
if(condition)
newControl.condition = condition;
directive.params.forEach(function(param){
var newParam = {
value: param.value
}
if(param.field)
newParam.field = param.field;
newControl.parameters[param.name] = newParam;
});
item.control = newControl;
}
else if(directive.type === "STYLE"){
if(!item.style){
item.style = {};
}
directive.params.forEach(function(param){
var propertyName = getPropertyNameForStyle(item.style, param)
item.style[propertyName] = { value: param.value ?? true };
if(condition)
item.style[propertyName].condition = condition;
});
} else if (directive.type === 'TAGS') {
const tags = item.tags ?? {}
item.tags = { ...tags, ...directive.tags };
}
});
}
function getPropertyNameForStyle(item, param){
let counter = 1;
let paramName = param.name ?? param;
var propertyName = paramName;
while(item.hasOwnProperty(propertyName)){
propertyName = paramName + '-' + counter++;
}
return propertyName;
}
function addTypeDefinition(tokenOffset, tokenLength, fieldName, typeDefinition){
addTypeDefinitionToField(fieldName, typeDefinition);
var newTypePosition = {
offset: tokenOffset,
length: tokenLength
};
result.placeholders.push(newTypePosition)
currentSection().placeholders.push(newTypePosition);
}
function addTypeDefinitionToField(fieldName, typeDefinition){
var field = getField(fieldName);
if( !field.typeDefinition
|| (field.typeDefinition.typeReference && typeDefinition.typeReference && (field.typeDefinition.typeReference != typeDefinition.typeReference))
|| ((field.typeDefinition.cardinality||{}).min != (typeDefinition.cardinality||{}).min)
|| ((field.typeDefinition.cardinality||{}).max != (typeDefinition.cardinality||{}).max)
|| (field.typeDefinition.required != typeDefinition.required ))
{
// completely reset the typeDefinition if the typeReference or cardinality differs from
// former specification
// TODO: deep-check tags array
field.typeDefinition = {}
}
if(typeDefinition.typeReference){
field.typeDefinition.typeReference = typeDefinition.typeReference;
if(typeDefinition.typeReference === 'QUICKSYNTAXTYPE'){
field.typeDefinition.quickSyntaxText = typeDefinition.quickSyntaxText;
}
}
field.typeDefinition.help = typeDefinition.help
field.typeDefinition.required = typeDefinition.required
field.typeDefinition.cardinality = typeDefinition.cardinality;
field.tags = typeDefinition.tags || {};
for (let tag of ['required','optional']) {
if (typeof field.tags[tag] === 'string') {
field.tags[tag] = {
expression: field.tags[tag]
}
}
}
if(typeDefinition.defaultDirectives){
if(!field.typeDefinition.defaultDirectives){
field.typeDefinition.defaultDirectives = {};
}
if(typeDefinition.defaultDirectives.control){
field.typeDefinition.defaultDirectives.control = typeDefinition.defaultDirectives.control;
}
if(typeDefinition.defaultDirectives.style){
if(!field.typeDefinition.defaultDirectives.style){
field.typeDefinition.defaultDirectives.style = {};
}
for(var styleName in typeDefinition.defaultDirectives.style){
field.typeDefinition.defaultDirectives.style[styleName] = typeDefinition.defaultDirectives.style[styleName];
}
}
//field.typeDefinition.defaultDirectives = typeDefinition.defaultDirectives;
}
if(typeDefinition.settingsText){
if(!field.typeDefinition.settingsText){
field.typeDefinition.settingsText = {};
}
// override settings on a per-label basis
for(var label in typeDefinition.settingsText){
field.typeDefinition.settingsText[label] = typeDefinition.settingsText[label].trim();
}
}
}
function completeSections(){
var allSections = result.sections;
//
// gather subsections...
//
for(var i=0; i<allSections.length; i++){
var section = allSections[i];
section.directSubsections = [];
section.allSubsections = [];
var inSection = false;
for(var j=0; j<allSections.length; j++){
var s = allSections[j];
if(s === section)
inSection = true;
else if(inSection && s.level <= section.level)
inSection = false;
if(inSection && (s.level === section.level+1)){
section.directSubsections.push(s);
s.parentSectionIndex = section.index
}
if(inSection && (s.level > section.level))
section.allSubsections.push(s);
}
//
// remove empty paragraphs...
//
var lastParagraph = section.paragraphs[section.paragraphs.length-1];
if(!lastParagraph.offset && !lastParagraph.length)
section.paragraphs.pop();
//
// set section lengths
//
if(i+1 < allSections.length){
section.length = allSections[i+1].offset - section.offset;
}
else{
section.length = result.definition.length - section.offset;
}
}
//
// set section lengths including subsections
// (based on previously set section lengths)
//
for(var i=0; i<allSections.length; i++){
var section = allSections[i];
if(section.allSubsections.length > 0){
var lastSubSection = section.allSubsections[section.allSubsections.length-1];
section.lengthIncludeSubsections = lastSubSection.offset - section.offset + lastSubSection.length;
}
else{
section.lengthIncludeSubsections = section.length;
}
}
}
function applyDefaultDirectives(){
result.placeholders.forEach(function(placeholder){
if(placeholder.field && placeholder.field.typeDefinition && placeholder.field.typeDefinition.defaultDirectives)
var defaultDirectives = placeholder.field.typeDefinition.defaultDirectives;
else
return;
if(!placeholder.control && defaultDirectives.control){
placeholder.control = defaultDirectives.control;
}
if(defaultDirectives.style){
if(!placeholder.style){
placeholder.style = defaultDirectives.style;
}
else{
for(var styleName in defaultDirectives.style){
if(!placeholder.style[styleName]){
placeholder.style[styleName] = defaultDirectives.style[styleName];
}
}
}
}
});
}
function log(text){
result.log += text + "\n";
}
}
/*------------------
FORM
------------------*/
Start
= formDefinition:_FormDefinition
{
result.definition = text();
completeSections();
applyDefaultDirectives();
return result;
}
_FormDefinition
= rootDirectives:_RootSectionDirectives _DefinitionItem+
{
applyDirectives(result.sections[0], rootDirectives);
}
/ _DefinitionItem+
_RootSectionDirectives
= directives:SectionDirectives EOL EmptyLine { return directives }
_DefinitionItem
= ss:_StyledSection
{
addSection(location().start.offset, text().length, ss);
}
/ td:_StyledTypeDefinition
{
addTypeDefinition(td.offset, td.length, td.fieldName, td.spec);
}
/ _StyledMarkupBlock
/ EmptyLine
/*------------------
SECTIONS
------------------*/
_StyledSection
= directives:SectionDirectives LineTerminatorSequence section:Section
{
section.directives = directives;
return section;
}
/ section:Section
{
section.directives = [];
return section;
}
Section
= Section1
/ Section2
/ SectionX
Section1
= chars:LineCharacter+ LineTerminator "===" LineCharacter* LineTerminator
{
return {
level: 1,
title: chars.join('').trim(),
type: "SETEXT",
offset: location().start.offset,
length: text().length
};
}
Section2
= chars:LineCharacter+ LineTerminator "---" LineCharacter* LineTerminator
{
return {
level: 2,
title: chars.join('').trim(),
type: "SETEXT",
offset: location().start.offset,
length: text().length
};
}
SectionX
= pounds:("#"+) title:SectionXTitle? EOL
{
return {
level: pounds.length,
title: title || '',
type: "ATX",
offset: location().start.offset,
length: text().length
};
}
SectionXTitle
= WhiteSpace+ chars:LineCharacter+
{
return chars.join('').trim();
}
/*---------------
Markup
---------------*/
_StyledMarkupBlock
= fp:FieldPosition EOL
{
//special case: when a block consists of just one field, then the directives
//apply to the field, not the block
addPlaceholder(location().start.offset, text().length, fp.directives, fp.field.name, fp.field.label);
completeCurrentParagraph(location().start.offset, text().length, location().start.offset, []);
addNextParagraph();
}
/ directives:SectionDirectives EOL block:_MarkupBlock
{
completeCurrentParagraph(location().start.offset, text().length, block.offset, directives);
addNextParagraph();
}
/ block:_MarkupBlock
{
completeCurrentParagraph(location().start.offset, text().length, location().start.offset, []);
addNextParagraph();
}
_MarkupBlock
= (!TypeDefinition _MarkupSpan (!EOB EOL)?)+
{
return {
offset: location().start.offset,
length: text().length
}
}
_MarkupSpan
= fp:FieldPosition
{
addPlaceholder(location().start.offset, text().length, fp.directives, fp.field.name, fp.field.label)
}
/ sc:StandaloneControl
{
addPlaceholder(sc.offset, sc.length, [sc.controlDirective])
}
/ TextSpan
{
if(text().trim().length > 0){
currentParagraph().hasText = true;
}
}
TextSpan
= (!FieldPosition !StandaloneControl LineCharacter)+ { return text(); }
/*--------------------
Standalone Control
--------------------*/
StandaloneControl
= "{" __ cd:ControlDirective __ "}" &EOL &EmptyLine
{
return {
offset: location().start.offset,
length: text().length,
controlDirective: cd
}
}
/*---------------
FIELDS
---------------*/
FieldPosition
= directives:FieldDirectives EOL? field:Field
{
return {
directives:directives,
field:field
}
}
/ field:Field
{
return {
directives:[],
field:field
}
}
Field
= "[" label:FieldLabel "][" name:FieldName "]"
{
return {
name: name,
label: label
}
}
FieldLabel
= (!"[" !"]" LineCharacter)*
{
return text();
}
FieldName
= firstChar:[a-zA-Z_] otherChars:[a-zA-Z0-9_]*
{
return text();
}
/*---------------
DIRECTIVES
---------------*/
FieldDirectives
= md:FieldMultiDirective EOL? ds:FieldDirectives { return md.concat(ds); }
/ FieldMultiDirective
FieldMultiDirective
= "{" __ d:ConditionalFieldDirective ds:(__ "|" __ d2:ConditionalFieldDirective { return d2 })* __ "}" {
return [d].concat(ds);
}
ConditionalFieldDirective
= d:FieldDirective WhiteSpace+ c:Condition
{
d.condition = c;
return d;
}
/ FieldDirective
SectionDirectives
= md:SectionMultiDirective EOL? ds:SectionDirectives { return md.concat(ds); }
/ SectionMultiDirective
SectionMultiDirective
= "{" __ d:ConditionalSectionDirective ds:(__ "|" __ d2:ConditionalSectionDirective { return d2 })* __ "}" { return [d].concat(ds); }
ConditionalSectionDirective
= d:SectionDirective WhiteSpace+ c:Condition
{
d.condition = c;
return d;
}
/ SectionDirective
Condition = "if" WhiteSpace+ c:(("||" / (!"}" !"|" LineCharacter))+{ return text() }) {
return c.trim();
}
SectionDirective
= StylingDirective
/ WidthDirective
/ TagsDirective
FieldDirective
= StylingDirective
/ WidthDirective
/ ControlDirective
WidthDirective
= UnsignedNumber __ UnitOfWidth?
{
return {
type: "STYLE",
params: [{name:"width", value:text()}]
}
}
StylingDirective
= !Condition p1:StylingParameter ps:(WhiteSpace+ p:StylingParameter { return p})*
{
return {
type: "STYLE",
params: [p1].concat(ps)
}
}
StylingParameter = styling:( "readonly" / "editable" / "hidden" / "hide" / "show" ) &(WhiteSpace/"}"/"|") { return styling }
ControlDirective
= !Condition namespace:ControlNamespace? control:ControlName params:(WhiteSpace+ ps:(Parameter/FlagParameter) { return ps}) *
{
return {
type: "CONTROL",
controlNamespace: namespace || "",
controlName: control,
controlFullName: namespace? namespace+":"+control : control,
params: params
}
}
TagsDirective = firstTag:Tag additionalTags:(WhiteSpace+ tags:Tag { return tags } )*
{
const tags = {}
for (const tag of [firstTag].concat(additionalTags)) {
tags[tag.name] = tag.value ?? true
}
return {
type: "TAGS",
tags
}
}
ControlNamespace
= namespace:IdentifierAllowingDashesAndHyphens ":" { return namespace }
TypeSettingsSectionParam
= characters:[a-z0-9_\-]i+
{
return characters.join('');
}
ControlName
= IdentifierAllowingDashesAndHyphens
Parameter
= i:ParamName __ "=" __ fr:FieldReferencingParameterValue { return { name:i, value:fr.value, field:fr.field}; }
/ i:ParamName __ "=" __ v:(DoubleQuotedParamValue/SingleQuotedParamValue/ParamValue) { return { name:i, value:v }; }
FlagParameter
= i:ParamName { return {name:i, value:true}; }
ParamName
= !("if" WhiteSpace) i:IdentifierAllowingDashesAndHyphens { return i }
IdentifierAllowingDashes
= initialCharacter:[a-z_]i otherCharacters:[a-z0-9_\-]i* { return initialCharacter+otherCharacters.join(''); }
IdentifierAllowingDashesAndHyphens
= IdentifierAllowingDashes / "-"
QuickTags
= "<" __ firstTag: Tag additionalTags:(WhiteSpace+ f:Tag{ return f})* __ ">"
{
var tags = [firstTag].concat(additionalTags)
var result = {}
for (var tag of tags) {
result[tag.name] = tag.value == null ? true : tag.value
}
return result
}
Tag = t:(TagWithValue/TagFlag)
{
return {
name: t.name,
value: t.value
}
}
TagWithValue
= i:TagName "=" v:(DoubleQuotedParamValue/SingleQuotedParamValue/ParamValue) { return { name:i, value:v }; }
TagFlag
= i:TagName { return {name:i, value:true}; }
TagName
= !("if" WhiteSpace) i:IdentifierAllowingDashesAndColons { return i }
IdentifierAllowingDashesAndColons
= initialCharacter:[a-z_]i otherCharacters:[a-z0-9:_\-]i* { return initialCharacter+otherCharacters.join(''); }
UnitOfWidth
= "em"/"px"/"rem"/"*"/"col"/"%"
ParamValue
= chars:[^ \t\n\r\'\"|}=\>]+ { return chars.join(''); }
//DoubleQuotedParamValue
// = "\"" chars:[^\n\r\"]* "\"" { return chars.join(''); }
DoubleQuotedParamValue
= "\"" chars:( "\\\"" / [^\n\r\"] )* "\""
{
return chars.join('').replace(/\\"/g, '"');
}
SingleQuotedParamValue
= "'" chars:( "\\'"/ [^\n\r\'] )* "'" {
return chars.join('').replace(/\\'/g, "'");
}
// = "\'" chars:[^\n\r\']* "\'" { return chars.join(''); }
FieldReferencingParameterValue
= "[" f:FieldName "]" { return {value: text(), field: f} }
/*------------------
TYPES
------------------*/
_StyledTypeDefinition
= typeDef:TypeDefinition
/ directives:FieldDirectives typeDef:TypeDefinition
{
typeDef.spec.defaultDirectives = {};
applyDirectives(typeDef.spec.defaultDirectives, directives);
return typeDef;
}
/ directives:FieldDirectives EOL typeDef:TypeDefinition
{
typeDef.spec.defaultDirectives = {};
applyDirectives(typeDef.spec.defaultDirectives, directives);
return typeDef;
}
TypeDefinition
= "[" fieldName:FieldName "]" __ ":" __ spec:TypeSpec
{
return {
offset: location().start.offset,
length: text().length,
fieldName: fieldName,
spec: spec
};
}
// TODO: change to general more extensible soloution for short-syntaxes
TypeSpec
// = typeref:TypeReference? cardinality:(__ c:Cardinality { return c})? formula:(__ f:Formula { return f})? tags:(__ v:QuickTags { return v})? help:(__ h:QuickHelp { return h})? EOL settings:TypeSettings ?
= typeref:TypeReference? __ cardinality:Cardinality? __ tags:QuickTags? __ formula:Formula? __ help:QuickHelp? EOL settings:TypeSettings?
{
var result = {
required: tags && tags.required === true,
cardinality: cardinality,
tags: tags || {},
settingsText: settings || {}
}
if (formula) {
result.settingsText.formula = formula;
}
if (help && !result.help) {
result.help = help;
}
if (!typeref) {
//result.typeReference = "string";
}
else if(typeref.quickSyntax) {
result.typeReference = 'QUICKSYNTAXTYPE';
result.quickSyntaxText = typeref.quickSyntax;
}
else {
result.typeReference = typeref.toLowerCase()
}
return result;
}
/ EOL settings: TypeSettings
{
var result = {
settingsText: settings
}
return result;
}
TypeReference
= "Text"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Password"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Number"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Select"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Switch"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Date"i & ("(" / WhiteSpace / EOL) { return text() }
/ "DateTime"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Mail"i & ("(" / WhiteSpace / EOL) { return text() }
/ "DayOfYear"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Time"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Duration"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Ref"i & ("(" / WhiteSpace / EOL) { return text() }
/ "File"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Image"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Address"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Currency"i & ("(" / WhiteSpace / EOL) { return text() }
/ "Geolocation"i & ("(" / WhiteSpace / EOL) { return text() }
/ TypeReferenceQuickSyntax
TypeReferenceQuickSyntax
= (!"(" !"\"" !"=" !"<" LineCharacter)+
{
return {
quickSyntax: text().trim()
}
}
Cardinality
= "(" __ min:(UnsignedNumber/"n"i)? __ "-" __ max:(UnsignedNumber/"n"i) __ ")"
{
var result = {
max: max.toLowerCase()
}
if(min)
result.min = min.toLowerCase();
return result;
}
/ "(" __ min:(UnsignedNumber/"n"i) __ "-" __ max:(UnsignedNumber/"n"i)? __ ")"
{
var result = {
min: min.toLowerCase()
}
if(max)
result.max = max.toLowerCase();
return result;
}
/ "(" __ minmax:(UnsignedNumber/"n"i) __ ")"
{
return {
min: minmax.toLowerCase(),
max: minmax.toLowerCase()
}
}
Formula
= "=" __ chars:([^\n\r\\""] / "\\\"")* { return chars.join('').trim() }
QuickHelp
= "\"" chars:[^\n\r\"]* "\"" { return chars.join('').trim(); }
TypeSettings = allSettings:TypeSettingSets
{
var result = {};
allSettings.forEach(function(setting){
result[setting.label] = setting.text;
});
return result;
}
TypeSettingSets
= first:LabeledTypeSettingsBlock others:(EmptyLine+ settings:LabeledTypeSettingsBlock{ return settings}) *
{
return [first].concat(others)
}
/ first:DefaultTypeSettingsBlock others:(EmptyLine+ settings:LabeledTypeSettingsBlock{ return settings}) *
{
return [first].concat(others)
}
DefaultTypeSettingsBlock = lines:TypeSettingsLine+
{
return {
label: '$default',
text: lines.join('\n')
}
}
LabeledTypeSettingsBlock
= label:TypeSettingsLabel lines:TypeSettingsLineOrEmptyLine+
{
return {
label: label,
text: lines.join('\n').replace(/\s*$/, '')
}
}
TypeSettingsLineOrEmptyLine
= !TypeSettingsLabel EmptyLine* !TypeSettingsLabel line:TypeSettingsLine { return line ; }
TypeSettingsLine
= Indent chars:LineCharacter+ EOL
{
var line = chars.join('').trim();
return line;
}
TypeSettingsLabel
= Indent label:IdentifierAllowingDashes __ "(" __ param:TypeSettingsSectionParam? __ ")" __ ":" EOL Indent "---" "-"*
{
if (param) {
return label + "(" + param + ")";
} else {
return label;
}
}
/ Indent label:IdentifierAllowingDashes __ ":" EOL Indent "---" "-"* EOL &Indent
{
return label;
}
/*----------------*/
SignedNumber
= sign:"-"? digits:[0-9.,]+ { return text() }
UnsignedNumber
= digits:[0-9.,]+ { return text() }
EmptyLine
= chars:WhiteSpace* LineTerminatorSequence & { return (chars.join('').trim().length == 0) }
/ LineTerminatorSequence
DoubleEmptyLine = EmptyLine EmptyLine
EndOfIndent = EmptyLine EmptyLine+ !WhiteSpace
/ EmptyLine+ !.
TextLine
= chars:LineCharacter+ LineTerminatorSequence
{
var line = chars.join('');
return line;
}
__
= WhiteSpace*
EOB "End of Block (empty lines)"
= EOL EOL+
EOL "End of Line (return)"
= __ LineTerminatorSequence
EOF
= !.
Indent
= "\t"
/ " "
WhiteSpace "whitespace"
= "\t"
/ " "
LineTerminator
= [\n\r]
LineTerminatorSequence "end of line"
= "\n"
/ "\r\n"
/ "\r"
LineCharacter
= !LineTerminator char:.
{
return char
}
Note: The formdown parsing rules are provided by the
@aeppic/forms-parser
module.
class Form
Aeppic is based on Forms. The Form
class handles parsing the form document definition, building the form controller and form functions.
It uses the parsed form info to expose a structured view of the form via it's rootSection
property. Which returns a FormSection
(Related to, but not identical to the raw ParsedFormSection
).
The class is exposed whenever working with EditableDocuments via the form
property:
ts
const myDocument = await Aeppic.edit('....')
myDocument.form.name
Using Tags on fields
When handling e.g optional fields they
Each tag where interpretations might interfere (e.g shared forms etc) should be namespaced with a unique preceding prefix followed by a colon. E.g [myField]: <my-company:my-tag>
.
Reserved tags
The following tags are reserved by aeppic:
- required
- optional
- sensitive
- encrypt
Note: The
EditableDocument
exposes the same functions by forwarding the call to it's form.
It is possible to query by name or value with string or regexp.
isTaggedField
js
const isTagged = form.isTaggedField('myField', ['tag1', 'tag2'], { operation: 'or' })
// isTagged === true
const isTagged2 = form.isTaggedField('myField', ['tag1', { name: 'tag2' }], { operation: 'and' })
// isTagged2 === false
const isTagged3 = form.isTaggedField('myField', ['tag-x', { name: /^tag/ }], { operation: 'or' })
// isTagged3 === true
md
[My Field][myField]
[myField]: <tag1>
getTaggedFields
js
const taggedFields2 = form.getTaggedFields([{ name: 'tag1', value: /^a/ }, 'unused-tag'], { operation: 'or' })
// taggedFields2 is ['myField2']
md
[My Field][myField]
[myField]: number <tag1=a-value>
getTaggedSections
This function recursively goes through all sections from root down and finds sections where all fields match the requested requirements.
Sections with fields that directly have placeholders to fields which match are returned (or rather their indices are). Sections with no fields at all wont be included (they don't match) Sections with with subsections only get returned when all their subsections are matching the criteria above. Independent of whether it itself has fields that match or not.
js
const taggedSections = form.getTaggedSections(editable, ['optional'])
// taggedSections is [1]
md
[My Field][myField]
# A Section
[My field 2][myField2]
[myField2]: <optional>
Handling undefined fields
Undefined fields are allowed when the form is marked to allowUndefinedFields.
A field is defined as undefined when the field's value is equal to undefined. The functions
form.getUndefinedFields(document)
, form.getUndefinedSections(document)
, form.isUndefinedField(document, fieldName)
thus both take a document as input. They are also exposed by the editable document itself (without the parameter of course).
Otherwise they behave identical to the function to search for tagged fields above, with the difference being the match criteria of a placeholder/field being the value of the field being undefined
.
Note: The
EditableDocument
exposes the same functions, but without the document parameter using itself instead.
Types
Details
ts
export type ParsedTagWithExpression = {
expression: string
}
export type ParsedTag = ParsedTagWithExpression | true | string
export type ParsedTags = {
[key: string]: ParsedTag
}
export type ParsedFieldGeneric = {
name: string
cardinality?: {
min?: number,
max?: number
},
description?: string
filter?: any
required: boolean
tags?: ParsedTags
settings?: any | {
help?: {
text: string,
html: string
},
helpExtended?: {
text: string,
html: string
},
required?: {
text: string,
html: string,
}
}
defaultValue: any
}
export type ParsedStringSelectField = ParsedFieldGeneric & {
type: 'string'
subType: 'select'
options: {
value: string
display: string
}
}
export type ParsedNumberSelectField = ParsedFieldGeneric & {
type: 'number'
subType: 'select'
options: {
value: number
display: string
}
}
export type ParsedStringField = ParsedFieldGeneric & {
type: 'string'
subType: 'text'|'password'|'date'|'datetime'|'time'|'dayofyear'|'mail'
defaultValue: ''
}
export type ParsedNumberField = ParsedFieldGeneric & {
type: 'number'
subType: 'number'
defaultValue: 0
}
export type ParsedBooleanField = ParsedFieldGeneric & {
type: 'boolean'
subType: 'switch'
defaultValue: false
}
export type ParsedObjectField = ParsedFieldGeneric & {
type: 'object'
}
export type ParsedObjectRefField = ParsedObjectField & {
subType: 'ref'
defaultValue: {
id: string
v: string
text: string
}
}
export type ParsedFileFieldDefaultValue = {
name: string
size: number
type: string
sha1: string
dataUrl: string
thumbnailUrl: string
iconUrl: string
fileInfo: {
created: number
modified: number
read: number
imported: number
}
}
export type ParsedObjectFileField = ParsedObjectField & {
subType: 'file'
defaultValue: ParsedFileFieldDefaultValue
}
export type ParsedObjectImageField = ParsedObjectField & {
subType: 'image'
mediaInfo: {
width: number
height: number
}
}
export type ParsedGeoLocationField = ParsedObjectField & {
subType: 'geolocation'
defaultValue: {
lat: number
lon: number
}
}
export type ParsedObjectAddressField = ParsedObjectField & {
subType: 'address'
defaultValue: {
street: string
streetNumber: string
postalCode: string
city: string
state: string
country: string
supplement: string
}
}
export type ParsedDurationField = ParsedObjectField & {
subType: 'duration'
defaultValue: {
days: number
hours: number
milliseconds: number
minutes: number
months: number
quarters: number
seconds: number
weeks: number
years: number
}
}
export type ParsedCurrencyField = ParsedObjectField & {
subType: 'currency'
defaultValue: {
amount: number
currency: string
precision: number
}
}
export type ParsedField = ParsedStringField | ParsedNumberField | ParsedBooleanField | ParsedObjectRefField | ParsedObjectFileField | ParsedObjectImageField | ParsedObjectAddressField | ParsedGeoLocationField | ParsedDurationField | ParsedCurrencyField | ParsedStringSelectField | ParsedNumberSelectField
export type ParsedControlAtPlaceholder = {
fullName: string,
namespace: string,
name: string,
parameters?: ControlParameters
}
export type ParsedPlaceholder = {
index: number
offset: number
length: number
label: string
control: ParsedControlAtPlaceholder,
style: StyleParameters
field: ParsedField
}
export type ParsedFormSection = {
offset: number
offsetExcludeDirectives: number
offsetExcludeTitle: number
length: number
lengthIncludeSubsections: number
level: number
title: string
type: 'ROOT'|'ATX'
style: object
placeholders: ParsedPlaceholder[]
paragraphs: unknown[]
index: number
directSubsections: ParsedFormSection[]
allSubsections: ParsedFormSection[]
}
export type ParsedFormInfo = {
definition: string
sections: ParsedFormSection[]
fields: {
[name: string]: ParsedField
},
default: {
[name: string]: any
},
conditions: any[]
placeholders: ParsedPlaceholder[]
}
export function parse(formDown: string): ParsedFormInfo
export function markdown(markdown: string): string
Examples of Parsed Info
js
info = {
conditions: [...],
default: { ... },
definition: "...<formdown>...",
fields: [...],
log: (...),
placeholders: (...),
schema: (...),
sections: [],
}
Field
json
{
cardinality: (...),
defaultValue: (...),
description: (...),
filter: (...),
name: (...),
required: (...),
settings: (...),
subType: (...),
tags: (...),
type: (...),
}
Section
js
rootSection = {
"offset": 0,
"offsetExcludeDirectives": 0,
"offsetExcludeTitle": 0,
"length": 0,
"lengthIncludeSubsections": 9054,
"level": 0,
"title": "",
"type": "ROOT",
"style": {},
"placeholders": [],
"paragraphs": [],
"index": 0,
"directSubsections": [subSection]
}
js
subSection = {
"offset": 0,
"offsetExcludeDirectives": 0,
"offsetExcludeTitle": 13,
"length": 2070,
"lengthIncludeSubsections": 3958,
"level": 1,
"title": "Allgemein",
"type": "ATX",
"style": {},
"placeholders": [...],
}
Paragraph
js
paragraph = {
"offset": 0,
"length": 19,
"hasText": true,
"hasPlaceholders": true,
"placeholders": [
{
"field": {
"name": "field",
"cardinality": null,
"defaultValue": "",
"description": null,
"filter": null,
"type": "string",
"subType": "text",
"tags": {},
"settings": {
"help": {
"text": "",
"html": ""
}
}
},
"offset": 5,
"length": 14,
"label": "field",
"control": null,
"style": {},
"index": 0
}
],
"style": {},
"offsetExcludeDirectives": 0
}
Placeholder
js
placeholder = {
"field": (...),
"offset": 13,
"length": 36,
"label": "Bezeichnung der Verarbeitung",
"control": null,
"style": {},
"index": 0
}
Control
js
placeholderControl = {
"namespace": "",
"name": "ref",
"fullName": "ref",
"parameters": {
"hide-path": {
"value": "true"
},
"ancestor-search-limit": {
"value": "2"
},
"handle-selected": {
"value": "false"
}
}
}
Field
js
field = {
"name": "zulaessig",
"cardinality": {
"max": "n",
"min": "0"
},
"defaultValue": {
"text": "",
"id": "",
"v": ""
},
"description": null,
"filter": [
{
"f.id": "'e86d03d7-1711-4138-b4ee-990efbd1a988'"
}
],
"required": null,
"type": "object",
"subType": "ref",
"tags": {},
"formRef": "e86d03d7-1711-4138-b4ee-990efbd1a988",
"settings": {
"help": {
"text": "",
"html": ""
}
},
"joinedFilter": "f.id:'e86d03d7-1711-4138-b4ee-990efbd1a988'"
}