matthewmatician/xml-flow

View on GitHub
lib/helper.js

Summary

Maintainability
A
0 mins
Test Coverage
/*
  This code was derived from lodash.js (the _.escape function)
  https://github.com/lodash/lodash
 */
const reUnescapedHtml = /[&<>"]/g;
const reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
const htmlEscapes = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;'
};

const escape = string => {
  const result = String(string);
  return (result && reHasUnescapedHtml.test(result))
    ? result.replace(reUnescapedHtml, chr => htmlEscapes[chr])
    : result;
};
/* end of derivation from lodash */

const isCongruent = (a, b) => (
  (typeof a === 'string' && typeof b === 'string')
  || (a && a[0].$name === b.$name)
);

const pushTogether = (a, b) => (typeof a === 'string' ? a + b : a.concat(b));

const condenseArray = items => items.reduce(
  (result, element) => {
    const lastPosition = result.length - 1;
    const last = result[lastPosition];
    if (isCongruent(last, element)) result[lastPosition] = pushTogether(last, element);
    else result.push(typeof element === 'string' ? element : [ element ]);
    return result;
  }, []
);

const disassembleNode = (node, dropName, unwrap) => {
  let $attrs = null;
  const stripped = {};
  Object.keys(node).forEach(key => {
    const val = isSomething(node[key]) ? node[key] : null;
    if (!val) return;
    switch (key) {
      case '$name':
        if (!dropName && val) stripped.$name = val;
        break;

      case '$attrs':
        $attrs = val;
        break;

      case '$markup':
        stripped.$markup = simplifyAll(val);
        break;

      default:
        if (val) stripped[key] = unwrap(val);
    }
  });
  return { stripped, $attrs };
};

const unwrapSingleArrays = x => (Array.isArray(x) && x.length === 1 ? x[0] : x);

const tryMergingAttrs = ($attrs, stripped) => {
  if (!isSomething(stripped)) return $attrs;
  if (!$attrs) return stripped;
  const keys = Object.keys(stripped);
  if (keys.length === 1 && stripped.$name) return Object.assign(stripped, $attrs);
  return Object.assign(stripped, { $attrs });
};

const tryGuttingNode = node => {
  if (!node) return undefined;
  const keys = Object.keys(node);
  const [ firstKey ] = keys;
  if (
    keys.length === 1
    && firstKey !== '$name'
    && firstKey !== 'id'
  ) return node[keys[0]];
  return node;
};

const simplifyNode = (node, dropName, keepArrays) => {
  const unwrap = keepArrays ? x => x : unwrapSingleArrays;
  const unwrapped = unwrap(node);
  if (!isSomething(unwrapped)) return null;
  if (
    typeof unwrapped !== 'object'
    || Array.isArray(unwrapped)
  ) return unwrapped;
  const { $attrs, stripped } = disassembleNode(unwrapped, dropName, unwrap);
  const merged = tryMergingAttrs($attrs, stripped);
  return tryGuttingNode(merged);
};

const shouldObjectifyMarkup = items => {
  let currentTag;
  const foundTags = {};
  let shouldObjectify = true;

  items.every(item => {
    if (typeof item === 'string') {
      currentTag = '$text';
    } else {
      currentTag = item[0].$name;
    }

    if (foundTags[currentTag]) {
      shouldObjectify = false;
      return false;
    }
    foundTags[currentTag] = true;
    return true;
  });
  return shouldObjectify;
};

/**
 * Try to concatenate things into one array (arrays, strings, whatever).
 * Be smart about the inputs, so as to create a simple array.
 * @param  {any}   [a=[]]                         String, array, whatever
 * @param  {any}   [b=[]]                         String, array, whatever
 * @param  {boolean} [preserveSingleArrays=false] Whether to unwrap a result of length = 1
 * @return {Array|any}                            The mooshing of the inputs
 */
const moosh = (a = [], b = [], preserveSingleArrays = false) => {
  const arrayA = Array.isArray(a) ? a : [ a ];
  const arrayB = Array.isArray(b) ? b : [ b ];
  const mooshed = arrayA.concat(arrayB);
  return ((mooshed.length === 1 && !preserveSingleArrays) ? mooshed[0] : mooshed);
};

const simplifyAll = (input, dropName) => {
  const output = [];
  if (input !== null && typeof input === 'object' && input.constructor === Array) {
    input.forEach(item => {
      output.push(simplifyNode(item, dropName));
    });
    return output;
  }
  return simplifyNode(input, dropName);
};

const objectifyMarkup = (node, keepArrays) => {
  let key;
  const output = {};

  Object.keys(node).forEach(nodeKey => {
    if (nodeKey !== '$markup') {
      output[nodeKey] = node[nodeKey];
    }
  });

  if (node.$markup) {
    node.$markup.forEach(item => {
      if (typeof item === 'string') {
        output.$text = moosh(output.$text, item, keepArrays);
      } else if (typeof item === 'object') {
        if (item.constructor === Array) {
          key = item[0].$name;
        } else {
          key = item.$name;
        }
        output[key] = moosh(output[key], simplifyAll(item, true), keepArrays);
      }
    });
  }
  return output;
};

const isSomething = value => (
  value != null
  && (value.length == null || value.length !== 0)
  && (typeof value !== 'object' || Object.keys(value).length !== 0)
);

module.exports = {
  condenseArray,
  escape,
  isSomething,
  moosh,
  objectifyMarkup,
  shouldObjectifyMarkup,
  simplifyNode
};