Home Reference Source Test Repository

src/IceCap.js

import cheerio from 'cheerio';

/**
 * IceCap provides programmable HTML templating with `cheerio`.
 *
 * @example
 * import IceCap from 'typhonjs-ice-cap';
 *
 * const ice = new IceCap('<p data-ice="name"></p>');
 * ice.text('name', 'Alice');
 * console.log(ice.html); // <p data-ice="name">Alice</p>
 */
export default class IceCap
{
   static get MODE_APPEND() { return 'append'; }

   static get MODE_WRITE() { return 'write'; }

   static get MODE_REMOVE() { return 'remove'; }

   static get MODE_PREPEND() { return 'prepend'; }

   static get CALLBACK_TEXT() { return 'text'; }

   static get CALLBACK_LOAD() { return 'html'; }

   static set debug(v) { this._debug = v; }

   /**
    * Create instance with HTML template.
    *
    * @param {!string}     html -
    *
    * @param {{ autoClose: boolean, autoDrop: boolean}} options -
    *
    * @param {EventProxy}  eventbus -
    */
   constructor(html, options = { autoClose: true, autoDrop: true }, eventbus = void 0)
   {
      if (!html)
      {
         throw new Error('html must be specified.');
      }

      if (typeof html === 'string')
      {
         this._$root = cheerio.load(html).root();
      }
      else if (html.find)
      {
         this._$root = html;
      }

      this._options = options;

      this._eventbus = eventbus;
   }

   attr(id, key, value, mode = IceCap.MODE_APPEND)
   {
      const nodes = this._nodes(id);
      let transformedValue;

      if (value === null || value === undefined) { value = ''; }

      for (const node of nodes.iterator)
      {
         const currentValue = node.attr(key) || '';

         switch (mode)
         {
            case IceCap.MODE_WRITE:
               transformedValue = value;
               break;

            case IceCap.MODE_APPEND:
               transformedValue = currentValue + value;
               break;

            case IceCap.MODE_REMOVE:
               transformedValue = currentValue.replace(new RegExp(value, 'g'), '');
               break;

            case IceCap.MODE_PREPEND:
               transformedValue = value + currentValue;
               break;

            default:
               throw Error(`unknown mode. mode = "${mode}"`);
         }

         node.attr(key, transformedValue);
      }

      return this;
   }

   get autoClose()
   {
      return this._options.autoClose;
   }

   set autoClose(val)
   {
      this._options.autoClose = val;
   }

   set autoDrop(val)
   {
      this._options.autoDrop = val;
   }

   get autoDrop()
   {
      return this._options.autoDrop;
   }

   close()
   {
      if (!this._$root) { return this; }

      this._html = this._takeHTML();
      this._$root = null;

      return this;
   }

   drop(id, isDrop = true)
   {
      if (!isDrop) { return; }

      const nodes = this._nodes(id);

      nodes.remove();

      return this;
   }

   _filter(nodes)
   {
      const results = [];

      for (let cntr = 0; cntr < nodes.length; cntr++)
      {
         const node = nodes.eq(cntr);
         const length = node.parents('[data-ice-loaded]').length;

         if (length === 0) { results.push(node[0]); }
      }

      const $result = cheerio(results);

      Object.defineProperty($result, 'iterator',
      {
         get: function()
         {
            const nodes = [];

            for (let cntr = 0; cntr < this.length; cntr++) { nodes.push(this.eq(cntr)); }

            return nodes;
         }
      });

      return $result;
   }

   get html()
   {
      if (!this._$root) { return this._html; }

      this._html = this._takeHTML();

      if (this._options.autoClose) { this.close(); }

      return this._html;
   }

   load(id, ice, mode = IceCap.MODE_APPEND)
   {
      let html = '';

      if (ice instanceof IceCap)
      {
         html = ice.html;
      }
      else if (ice)
      {
         html = ice.toString();
      }

      const nodes = this._nodes(id);

      if (this._options.autoDrop && !html)
      {
         nodes.remove();

         return;
      }

      nodes.attr('data-ice-loaded', '1');

      let transformedValue;

      for (const node of nodes.iterator)
      {
         const currentValue = node.html() || '';

         switch (mode)
         {
            case IceCap.MODE_WRITE:
               node.text('');
               transformedValue = html;
               break;

            case IceCap.MODE_APPEND:
               transformedValue = currentValue + html;
               break;

            case IceCap.MODE_REMOVE:
               transformedValue = currentValue.replace(new RegExp(html, 'g'), '');
               break;

            case IceCap.MODE_PREPEND:
               transformedValue = html + currentValue;
               break;

            default:
               throw Error(`unknown mode. mode = "${mode}"`);
         }

         node.html(transformedValue);
      }

      return this;
   }

   loop(id, values, callback)
   {
      if (!Array.isArray(values))
      {
         throw new Error(`values must be array. values = "${values}"`);
      }

      if (['function', 'string'].indexOf(typeof callback) === -1)
      {
         throw new Error(`callback must be function. callback = "${callback}"`);
      }

      if (typeof callback === 'string')
      {
         switch (callback)
         {
            case IceCap.CALLBACK_TEXT:
               callback = (i, value, ice) => { ice.text(id, value); };
               break;

            case IceCap.CALLBACK_LOAD:
               callback = (i, value, ice) => { ice.load(id, value); };
               break;

            default:
               throw Error(`unknown callback. callback = "${callback}"`);
         }
      }

      const nodes = this._nodes(id);

      if (values.length === 0)
      {
         nodes.remove();

         return;
      }

      for (const node of nodes.iterator)
      {
         const results = [];

         for (let j = 0; j < values.length; j++)
         {
            const parent = cheerio.load('<div/>').root();
            const clonedNode = node.clone();
            const textNode = cheerio.load('\n').root();

            parent.append(clonedNode);
            results.push(clonedNode[0]);
            results.push(textNode[0]);

            const ice = new IceCap(parent);

            callback(j, values[j], ice);
         }

         if (node.parent().length)
         {
            node.parent().append(results);
         }
         else
         {
            this._$root.append(results);
         }

         node.remove();
      }

      return this;
   }

   into(id, value, callback)
   {
      const nodes = this._nodes(id);

      if (value === '' || value === null || value === undefined)
      {
         nodes.remove();

         return;
      }
      else if (Array.isArray(value))
      {
         if (value.length === 0)
         {
            nodes.remove();

            return;
         }
      }

      if (typeof callback !== 'function')
      {
         throw new Error(`callback must be function. callback = "${callback}"`);
      }

      for (const node of nodes.iterator)
      {
         callback(value, new IceCap(node));
      }

      return this;
   }

   _nodes(id)
   {
      if (!this._$root) { throw new Error('can not operation after close.'); }
      if (!id) { throw new Error('id must be specified.'); }

      const $nodes = this._$root.find(`[data-ice="${id}"]`);

      const filtered = this._filter($nodes);

      if (filtered.length === 0 && IceCap._debug && this._eventbus)
      {
         this._eventbus.trigger('log:debug', `node not found. id = ${id}`);
      }

      return filtered;
   }

   _takeHTML()
   {
      const loadedNodes = this._$root.find('[data-ice-loaded]').removeAttr('data-ice-loaded');

      const html = this._$root.html();

      loadedNodes.attr('data-ice-loaded', 1);

      return html;
   }

   /**
    * Apply value to DOM that is specified with id.
    *
    * @param {!string} id -
    * @param {string} value -
    * @param {string} [mode=IceCap.MODE_APPEND] -
    *
    * @return {IceCap} self instance.
    */
   text(id, value, mode = IceCap.MODE_APPEND)
   {
      const nodes = this._nodes(id);

      if (this._options.autoDrop && !value)
      {
         nodes.remove();

         return;
      }

      if (value === null || value === undefined) { value = ''; }

      let transformedValue;

      for (const node of nodes.iterator)
      {
         const currentValue = node.text() || '';

         switch (mode)
         {
            case IceCap.MODE_WRITE:
               transformedValue = value;
               break;

            case IceCap.MODE_APPEND:
               transformedValue = currentValue + value;
               break;

            case IceCap.MODE_REMOVE:
               transformedValue = currentValue.replace(new RegExp(value, 'g'), '');
               break;

            case IceCap.MODE_PREPEND:
               transformedValue = value + currentValue;
               break;

            default:
               throw Error(`unknown mode. mode = "${mode}"`);
         }

         node.text(transformedValue);
      }

      return this;
   }
}

/**
 * Wires up Logger on the plugin eventbus.
 *
 * @param {PluginEvent} ev - The plugin event.
 *
 * @see https://www.npmjs.com/package/typhonjs-plugin-manager
 *
 * @ignore
 */
export function onPluginLoad(ev)
{
   const eventbus = ev.eventbus;

   let eventPrepend = '';

   const options = ev.pluginOptions;

   // Apply any plugin options.
   if (typeof options === 'object')
   {
      // If `eventPrepend` is defined then it is prepended before all event bindings.
      if (typeof options.eventPrepend === 'string') { eventPrepend = `${options.eventPrepend}:`; }
   }

   eventbus.on(`${eventPrepend}ice:cap:create`, (html, options = { autoClose: true, autoDrop: true },
    targetEventbus = eventbus) =>
   {
      return new IceCap(html, options, targetEventbus);
   });

   eventbus.on(`${eventPrepend}ice:cap:set:debug`, (debug) => { IceCap.debug = debug; });
}