Skip to content

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'"
}