Source: Parser.js

Parser.js

/**
 * Marklang Parser
 * @example
 * const parser = new markus.Parser();
 * await parser.parseMarkfile('mark.view');
 * > [{type="elementNode", element: 'app', props: {...}, presets: [...]}]
 * @class
 * @memberof markus
 * @param [loadType=ajax] {string} Method of loading markfile
 */
export default class Parser {
  constructor(loadType='ajax') {
    /**
     * Method of loading markfile.
     * @member {string}
     */
    this.loadType = loadType;
  }

  /**
   * Parse markfile to AST presets
   * @param filepath {string} file path to markfile
   * @returns {Promise} Return promise with ast presets
   *
   * @example
   * await parser.parseMarkfile(['./mark.view');
   * > [{type="elementNode", element: 'app', props: {...}, presets: [...]}]
   */
  parseMarkfile(filepath) {
    return new Promise((resolve) => {
      this.imports([filepath]).then((data) => {
        let entry = data[0].data;
        let imports = this.getImports(entry);

        this.imports(imports).then((files) => {
          for(let i = 0; i < files.length; i++) {
            if(this.getImports(files[i].data).length) {
              throw Error('Imports are possible only in the entry file.');
            }
            entry = entry.replace('import ' + files[i].path, files[i].data);
          }

          let presets = this.parsePresets(entry.split('\n'));
          resolve(this.generateTree(presets));
        });
      });
    });
  }

  /**
   * Loaded markfiles from array pathes
   * @param pathes {string[]} Patches to markfiles
   * @returns {Promise}
   *
   * @example
   * await parser.imports(['./mark.view', './resources.mark']);
   * > [{name: 'mark.view', path: './mark.view', data: '...'}, {...}]
   */
  imports(pathes) {
    let files = [];
    for(let i = 0; i < pathes.length; i++) {
      if(this.loadType === 'ajax') {
        files.push(fetch(pathes[i])
          .then((res) => {
            if(res.status === 404) {
              throw Error('Markus module "' + pathes[i] + '" is not found');
            }
            return res.text();
          }).then((data) => {
            return {name: pathes[i].split('/').slice(-1)[0], path: pathes[i], data};
          })
        );
      }
    }
    return Promise.all(files);
  }

  /**
   * Generates AST from presets based on type and depth properties of a preset.
   * @param presets {Preset[]} List presets
   * @returns {Array} AST presets
   *
   * @example
   * parser.generateTree([
   *   {type: 'elementNode', depth: 0, element: 'app'},
   *   {type: 'elementNode', depth: 1, element: 'text'},
   *   {type: 'propNode', depth: 2, name: 'text', value=''}
   *   {type: 'valueNode', depth: 3, value='TEXT NODE'}
   * ])
   *
   * > {type: 'elementNode', depth: 0, element: 'app', presets: [{
   *     type: 'elementNode', depth: 1, element: 'text', props: {
   *       text: 'TEXT NODE'
   *     }}
   *   }]
   * }
   */
  generateTree(presets) {
    let tree = [];
    for(let i = presets.length-1; i >= 0; i--) {
      if(presets[i].depth !== 0) {
        for(let j = i-1; j >= 0; j--) {
          if(presets[j].depth < presets[i].depth) {
            let parent = presets[j];
            let child = presets[i];

            if(child.type === 'valueNode') {
              if(parent.type === 'elementNode') {
                parent.value = child.value + (parent.value ? '\n' + parent.value : '');
              }
              else if(parent.type === 'propNode') {
                if(parent.value === true) {
                  parent.value = '';
                }
                parent.value = child.value + (parent.value ? '\n' + parent.value : '');
              }
              else {
                throw Error('valueNode cannot be a child of a ' + parent.type);
              }
            }
            else if(child.type === 'propNode') {
              if(parent.type === 'propNode') {
                if(typeof parent.value === 'string') {
                  parent.directory = parent.value;
                  parent.value = {[parent.value]: {[child.name]: child.value}};
                }
                else if(parent.directory) {
                  Object.assign(parent.value[parent.directory], {[child.name]: child.value});
                }
                else if(typeof parent.value === 'object' && parent.value != null) {
                  Object.assign(parent.value, {[child.name]: child.value});
                }
                else {
                  parent.value = {value: parent.value, [child.name]: child.value};
                }
              }
              else if(parent.type === 'elementNode') {
                if(Array.isArray(parent.props[child.name])) {
                  parent.props[child.name].push(child.value);
                }
                else if(typeof parent.props[child.name] === 'object') {
                  Object.assign(parent.props[child.name], child.value);
                }
                else if(parent.props[child.name]) {
                  parent.props[child.name] = [parent.props[child.name], child.value];
                }
                else {
                  parent.props[child.name] = child.value;
                }
              }
              else {
                throw Error('propNode cannot be a child of a ' + parent.type);
              }
            }
            else if(child.type === 'elementNode') {
              if(parent.type === 'elementNode') {
                parent.presets.unshift(child);
              }
              else {
                throw Error('elementNode cannot be a child of a ' + parent.type);
              }
            }
            break;
          }
        }
      }
      else {
        tree.push(presets[i]);
      }
    }
    return tree;
  }

  /**
   * Parse marklang lines array to presets. Calls parser.parsePreset (line [i])
   * @param lines {string[]} Strings with marklang markup
   * @returns {Preset[]}
  */
  parsePresets(lines) {
    let presets = [];
    for(let i = 0; i < lines.length; i++) {
      let preset = this.parsePreset(lines[i]);
      if(preset != null) {
        presets.push(preset);
      }
    }
    return presets;
  }

  /**
   * Parse marklang string to preset
   * @param line {string} String with marklang markup
   * @returns {Preset}
   *
   * @example
   * parser.parsePreset('    text.tag#id(obj.visible = yes) | VALUE')
   * > {
   *   type: 'elementNode',
   *   depth: 2
   *   element: 'text',
   *   id: 'id',
   *   tags: ['tag'],
   *   props: {obj: {visible: true}},
   *   value: 'VALUE'
   * }
   *
   * parser.parsePreset('    | VALUE TEXT')
   * > {
   *   depth: 2,
   *   type: 'valueNode',
   *   value: 'VALUE TEXT'
   * }
   *
   * parser.parsePreset('  @prop .3324')
   * > {
   *   depth: 1,
   *   type: 'propNode',
   *   typeAttr: 'prop',
   *   name: 'prop',
   *   value: 0.3324
   * }
   *
   * parser.parsePreset('  @move(10, 30)')
   * > {
   *   depth: 1,
   *   type: 'propNode',
   *   typeAttr: 'method',
   *   name: 'move',
   *   args: [10, 30]
   * }
   */
  parsePreset(line) {
    line = this.removeComment(line);

    let type = 'elementNode';
    let depth = this.getDepth(line);

    // if line is attr node
    let attr = this.getAttr(line);
    if(attr) {
      return {type: 'propNode', depth, name: attr[1], value: attr[2], typeAttr: attr[0]};
    }

    // else line is element, empty or value node
    let element = this.getElement(line);
    let tags = this.getTags(line);
    let value = this.getValue(line);
    let id = this.getId(line);
    let props = [];

    // if element is undefined, then line is block or value node
    if(element == null) {
      if(tags.length || id) {
        element = '';
      }
      else if(value) {
        type = 'valueNode';
      }
      else {
        return;
      }
    }

    // if line is elementNode, then parse props
    if(type !== 'valueNode') {
      props = this.getInlineAttrs(line);
    }

    return {type, element, value, props, tags, id, depth, presets: []};
  }


  /**
   * Convert value from props with support js types
   * @param value {string}
   * @return {any} Parsed value
   *
   * @example
   * parser.parseValue('no'|'off'|'false');
   * > false
   * parser.parseValue('yes'|'on'|'true');
   * > true
   * parser.parseValue('.5'|'+34'|'-34'|'3.32432');
   * > Number
   * parser.parseValue('anystring');
   * > String
   */
  parseValue(value) {
    if(value === 'on' || value === 'yes' || value === 'true') {
      return true;
    }
    else if(value === 'off' || value === 'no' || value === 'false') {
      return false;
    }
    else if(/^[-\.\+]?[0-9]+\.?([0-9]+)?$/.test(value)) {
      return Number(value);
    }
    return value;
  }


  /**
   * Removes comment from markline
   * @param line {string} line with marklang markup
   * @returns {string} markline without comment
   *
   * @example
   * removeComment('elm.tag // some comment')
   * > 'elm.tag'
   */
  removeComment(line) {
    return line.replace(/\/\/.+/, '');
  }

  /**
   * Finds all import requests in the file from the `import path` construction
   * @param data {string[]} lines array with marklang markup
   * @returns {string[]} imports data
   *
   * @example
   * parser.getImports([
   *   'import resources.mark',
   *   'app(w=1920, h=900)',
   *   ' import scenes.mark'
   * ].join('\n'));
   * > ['resources.mark', 'styles.mark']
   */
  getImports(data) {
    return (data.match(/import .+/g) || []).map((v) => v.split(' ')[1]);
  }

  /**
   * Getes comment from markline
   * @param line {string} line with marklang markup
   * @returns {string} Comment from markline
   *
   * @example
   * getComment('elm.tag // some comment')
   * > // some comment
   */
  getComment(line) {
    return (line.match(/\/\/.+/)  || [''])[0];
  }

  /**
   * Finds the depth of the entry element. Calculated by the number of tabs at the beginning of the line.
   * @param line {string} line with marklang markup
   * @returns {number} depth
   *
   * @example
   * parser.getDepth("\t\t\t\t")
   * > 4
   */
  getDepth(line) {
    return (line.match(/^[\t ]+/) || [''])[0].length/2;
  }

  /**
   * Parse query selector to object
   * @param query {string} Query selector in marklang markup
   * @returns {Object} Query selector in object
   *
   * @example
   * parser.parseQuery('element.tag.tag2#id');
   * > {element: 'element', tags: ['tag', 'tag2'], id: 'id'}
   */
  parseQuery(query) {
    let tags = (query.match(/\.\w+/g) || []).map((tag) => tag.slice(1));
    let id = (query.match(/\#\w+/) || [''])[0].slice(1);
    let element = (query.match(/^\w+/) || [])[0];
    return {element, id, tags};
  }

  /**
   * Get element name
   * @param line {string} line with marklang markup
   * @returns {string} element name from markline
   *
   * @example
   * parser.getElement("sprite.tag#id(prop=data)")
   * > "sprite"
   */
  getElement(line) {
    return (line.match(/^[\t ]*(\w+)/) || [])[1];
  }

  /**
   * Extracts all element tags from .tag_name construction
   * @param line {string} line with marklang markup
   * @returns {string[]} element tags from markline
   *
   * @example
   * parser.getTags("el.tag1#id.tag2.tag3")
   * > ["tag1", "tag2", "tag3"]
   */
  getTags(line) {
    return (line.replace(/\(.+\)/, '').match(/\.\w+/g) || []).map((tag) => tag.slice(1));
  }

  /**
   * Extracts the element id from the `#id_name` construction
   * @param line {string} line with marklang markup
   * @returns {string} element id from markline
   *
   * @example
   * parser.getId("el.tag1#cat.tag2.tag3")
   * > "cat"
   */
  getId(line) {
    return (line.replace(/\(.+\)/, '').match(/#\w+/) || [''])[0].slice(1);
  }

  /**
   * Extract text value from the `| .+` construction
   * @param line {string} line with marklang markup
   * @returns {string} element value from markline
   *
   * @example
   * parser.getValue("| SOME VALUE ");
   * > "SOME VALUE "
   */
  getValue(line) {
    return (line.match(/\| *(.+)/) || [])[1] || '';
  }

  /**
   * Retrieves an element property from a `@prop value` and `$prop value` structure
   * @param line {string} line with marklang markup
   * @returns {string[]} attr from markline. [attrType, attrNane, attrValue]
   *
   * @example
   * parser.getAttr("@prop on")
   * > ['propNode', "prop", true]
   *
   * parser.getAttr("@func(on, 20, text)")
   * > ['propNode', "attr", [true, 20, 'text']]
   */
  getAttr(line) {
    let func = line.match(/^[\t ]*\@(\w+)\((.+)?\)/);
    if(func) {
      let args = func[2] != null ? func[2].split(/,\s+/).map(v => this.parseValue(v)) : [];
      return ['method', func[1], function() {
        this[func[1]].apply(this, args);
      }];
    }

    let prop = line.match(/^[\t ]*\@(\w+)(\s(.+))?/);
    if(prop) {
      return ['prop', prop[1], prop[3] != null ? this.parseValue(prop[3]) : true];
    }
  }

  /**
   * Retrieves all element properties from the structure (prop = data, ...)
   * @param line {string} line with marklang markup
   * @returns {Object} element attrs from markline
   *
   * @example
   * parser.getInlineAttrs("el(texture=cat.png, font= Bebas Neue, visible = off, some.point.x = .4)")
   * > {texture: "cat.png", font="Bebas Neue", visible: false, some: {point: {x: .4}}}
   */
  getInlineAttrs(line) {
    let res = {};
    let find = line.match(/\((.+)\)/g);
    if(find == null) {
      return {};
    }

    let props = find[0].split(/,\s+/);
    for(let key in props) {
      let prop = props[key].replace('(', '').replace(')', '').split('=');
      let keys = prop[0].split('.');
      let _prop = res;
      for(let i = 0; i < keys.length; i++) {
        if(keys[i+1]) {
          _prop = _prop[keys[i]] = {};
        }
        else {
          _prop[keys[i]] = prop[1] != null ? this.parseValue(prop[1]) : true;
        }
      }
    }
    return res;
  }
}