/* eslint-disable */

(function()
{
   'use strict';

   var circleRadius = 6;
   var minScaleExtent = 0.2, maxScaleExtent = 5;
   var graph, layout, zoom, nodes, links;
   var selectedContextNode, selectedDragNode;
   var linkedByIndex = {};
   var graphWidth, graphHeight;
   var data;

   var appOptions =
   {
      currentLevel: 0,
      currentScope: 'all', // Stores package / source scope: main, dev, all
      maxDepthSticky: true,
      showFullNames: false,
      showTableView: false
   };

   // Stores menu options that have a toggle state.
   var appMenuToggleOptions = ['maxDepthSticky', 'showFullNames', 'showTableView'];

   // Gathers data from template filled functions.
   var dataPackageMap =
   {
      'all': getPackageDataAll(),
      'dev': getPackageDataDev(),
      'main': getPackageDataMain()
   };

   // Provides a cache of recycled SVG elements.
   var svgElementMap =
   {
      circle: [],
      g: [],
      path: [],
      text: []
   };

   function cerialize(text)
   {
      var svgxml = (new XMLSerializer()).serializeToString(d3.select('svg').node());
      console.log('!!! serialize data -- text: ' + text);
      console.log('!!! svg: ' + svgxml);
      if (data)
      {
         console.log('!!! data.links: ' + JSON.stringify(data.links));
         console.log('!!! data.nodes: ' + JSON.stringify(data.nodes));
      }
   }

   /**
    * Bootstraps the setup of the graph.
    */
   function bootstrap()
   {
      // Hook up controllers
      $('.control-zoom button').on('click', onControlZoomClicked);
      $('.control-level input').on('change', onControlLevelChanged);
      $('.control-deps input').on('click', onControlDepsClicked);
      $('.control-links input').on('click', onControlLinksClicked);
      $('.control-menu li').on('click', onControlMenuClicked);

      $('#contextpopup li[data-action]').on('click', onNodeContextMenuClick);

      // Gather current level and scope from template values.
      appOptions.currentLevel = parseInt($('.control-level input').val());
      appOptions.currentScope = $('.control-deps input:radio[name=dep]:checked').val();

      zoom = d3.behavior.zoom();

      zoom.scaleExtent([minScaleExtent, maxScaleExtent]);
      zoom.on('zoom', onZoomChanged);

      graphWidth = window.innerWidth;
      graphHeight = window.innerHeight;

      d3.select(window).on('resize', onResize);

      // Setup layout
      layout = d3.layout.force()
       .gravity(.05)
       .charge(-300)
       .linkDistance(80)
       .size([graphWidth, graphHeight])
       .on('tick', onTick);

      // Setup drag callbacks for nodes. If a node is clicked and dragged it becomes fixed.
      layout.drag()
       .on('dragstart', function(targetNode)
       {
          d3.event.sourceEvent.stopPropagation();
          d3.select(this).classed('dragging', true).classed('fixed', targetNode.fixed = true);
          detectAllNodesFixed();
       })
       .on('drag', function(targetNode)
       {
          d3.select(this).attr('cx', targetNode.x = d3.event.x).attr('cy', targetNode.y = d3.event.y);
       })
       .on('dragend', function()
       {
          d3.select(this).classed('dragging', false);
       });

      // Setup graph
      d3.select('.graph')
       .append('svg')
       .attr('transform', 'rotate(0)')
       .on('contextmenu', function() { d3.event.preventDefault(); })
       .attr('pointer-events', 'all')
       .call(zoom).on('dblclick.zoom', null);

      // Markers Def
      d3.select('.graph svg')
       .append('defs').selectAll('marker')
       .data(['regular'])
       .enter().append('marker')
       .attr('id', String)
       .attr('viewBox', '0 -5 10 10')
       .attr('refX', 15)
       .attr('refY', -1.5)
       .attr('markerWidth', 6)
       .attr('markerHeight', 6)
       .attr('orient', 'auto')
       .append('path')
       .attr('d', 'M0,-5L10,0L0,5');

      // Top level SVGGElement for dragging / centering graph
      graph = d3.select('.graph svg')
       .append(getSVG('g'))
       .attr('width', graphWidth)
       .attr('height', graphHeight)
       .attr('transform', 'translate(' + zoom.translate() + ')' + ' scale(' + zoom.scale() + ')');

      // Initialize tablesorter
      $('#nodeTable').tablesorter(
      {
         widgets: ['stickyHeaders'],
         widgetOptions:
         {
            stickyHeaders_filteredToTop: true,
            stickyHeaders_cloneId : '-sticky',
            stickyHeaders_attachTo : '.control-table-inner'
         },
         sortMultiSortKey: ''
      });

      updateAll();

      // Center graph w/ zoom fit w/ 1 second transition applied after 4 seconds delay for debounce.
      centerGraph(zoomFit, 1000, 4000);
   }

   /**
    * Centers the graph. All parameters can be either a number or a function evaluated for a number result.
    *
    * `newScale` and `duration` are evaluated in the centering anonymous function allowing values to be determined
    * after debounce.
    *
    * @param {number|function}   newScale - New scale value for graph.
    * @param {number|function}   duration - Duration of centering transition.
    * @param {number|function}   delay - Delay for debounce before centering occurs.
    */
   function centerGraph(newScale, duration, delay)
   {
      if (typeof delay === 'function') { delay = delay.call(this); }

      delay = typeof delay === 'number' ? delay : 0;

      setTimeout(function()
      {
         if (typeof newScale === 'function') { newScale = newScale.call(this); }
         if (typeof duration === 'function') { duration = duration.call(this); }

         newScale = typeof newScale === 'number' ? newScale : zoom.scale();
         duration = typeof duration === 'number' ? duration : 200;

         if (typeof newScale !== 'number') { throw new TypeError("centerGraph error: 'newScale' is not a 'number'."); }
         if (typeof duration !== 'number') { throw new TypeError("centerGraph error: 'duration' is not a 'number'."); }

         var bounds = graph.node().getBBox();

         var centerSVGX = (graphWidth * newScale / 2);
         var centerSVGY = (graphHeight * newScale / 2);

         var centerGraphX = (bounds.x * newScale) + (bounds.width * newScale / 2);
         var centerGraphY = (bounds.y * newScale) + (bounds.height * newScale / 2);

         // Translate
         var centerTranslate =
         [
            (graphWidth / 2) - centerSVGX + (centerSVGX - centerGraphX),
            (graphHeight / 2) - centerSVGY + (centerSVGY - centerGraphY)
         ];

         // Store values
         zoom
          .translate(centerTranslate)
          .scale(newScale);

         // Render transition
         graph.transition()
          .duration(duration)
          .attr('transform', 'translate(' + zoom.translate() + ')' + ' scale(' + zoom.scale() + ')');

         // Hides any existing node context menu.
         hideNodeContextMenu();

      }, delay);
   }

   /**
    * Helper function to determin if all nodes are fixed. This is run after any node is dragged and set to fixed.
    */
   function detectAllNodesFixed()
   {
      if (data)
      {
         var currentNodesFixed = data.allNodesFixed;
         var allNodesFixed = true;
         data.nodes.forEach(function(node) { if (!node.fixed) { allNodesFixed = false; } });
         data.allNodesFixed = allNodesFixed;
         if (currentNodesFixed !== allNodesFixed) { updateMenuUI(); } // Update freeze / unfreeze menu option.
      }
   }

   /**
    * Fades and unfades connected nodes to a given `targetNode`.
    *
    * @param {object}   targetNode - The target node from which fading occurs / connections are calculated.
    * @param {boolean}  selected - Indicates if the fade is in / out; true fades nodes / false un-fades nodes.
    * @param {Array}    nodes - An array of all graph nodes.
    * @param {Array}    links - An array of all graph links.
    */
   function fadeRelatedNodes(targetNode, selected, nodes, links)
   {
      var opacity = selected ? 0.1 : 1;

      var elm = findElementByNode('circle', targetNode);

      // Highlight circle
      elm.classed('selected', opacity < 1);

      // Clean links
      $('path.link').removeAttr('data-show');

      // Traverse all nodes and set `dimmed` class to nodes that are dimmed / not connected in addition to setting
      // fill and stroke opacity.
      nodes.style('stroke-opacity', function(otherNode)
      {
         var thisOpacity = isConnected(targetNode, otherNode) ? 1 : opacity;

         this.setAttribute('fill-opacity', thisOpacity);
         this.setAttribute('stroke-opacity', thisOpacity);

         // Depending on opacity add or remove 'dimmed' class.
         this.classList[thisOpacity === 1 ? 'remove' : 'add']('dimmed');

         return thisOpacity;
      });

      // Traverse all links and set `data-show` and `marker-end` for connected links given the `targetNode`.
      links.style('stroke-opacity', function(otherNode)
      {
         if (otherNode.source === targetNode)
         {
            // Highlight target / sources of the link
            var elmNodes = graph.selectAll('.' + formatClassName('node', otherNode.target));
            elmNodes.attr('fill-opacity', 1);
            elmNodes.attr('stroke-opacity', 1);

            elmNodes.classed('dimmed', false);

            // Highlight arrows
            var elmCurrentLink = $('path.link[data-source=' + otherNode.source.index + ']');
            elmCurrentLink.attr('data-show', true);
            elmCurrentLink.attr('marker-end', 'url(#regular)');

            return 1;
         }
         else
         {
            return opacity;
         }
      });

      // Modify all links that have not had 'data-show' added above.
      var elmAllLinks = $('path.link:not([data-show])');
      elmAllLinks.attr('marker-end', opacity === 1 ? 'url(#regular)' : '');
   }

   /**
    * A helper to select a given SVG element from given node data.
    *
    * @param {string}   prefix - String prefix.
    * @param {object}   node - Node object
    */
   function findElementByNode(prefix, node)
   {
      var selector = '.' + formatClassName(prefix, node);
      return graph.select(selector);
   }

   /**
    * Replaces semver and other special characters with `-`.
    *
    * @param {string}   prefix - String prefix.
    * @param {object}   node - Node object
    * @returns {string}
    */
   function formatClassName(prefix, node)
   {
      return prefix + '-' + node.id;
   }

   /**
    * Pass in the element and the screen coordinates are returned.
    *
    * @param element
    *
    * @returns {{x: number, y: number}}
    */
   function getElementCoords(element)
   {
      var ctm = element.getCTM();

      return { x: ctm.e + element.getAttribute('cx') * ctm.a, y: ctm.f + element.getAttribute('cy') * ctm.d };
   }

   /**
    * Gets a recycled SVG element from the pool, `svgElementMap`, or creates a new element for the given type. Any
    * data specified by D3 will be copied to the element. Returns a function which is evaluated by D3.
    *
    * @param {string}   elementType - SVG element: `circle`, `g`, `path`, or `text`.
    *
    * @returns {*}
    */
   function getSVG(elementType)
   {
      var returnVal;
      var svgElement;
      var cached;

      switch (elementType)
      {
         case 'circle':
         case 'g':
         case 'path':
         case 'text':
            returnVal = function(data)
            {
               svgElement = svgElementMap[elementType].pop();

               cached = svgElement != null;

               svgElement = svgElement != null ? svgElement : document.createElementNS('http://www.w3.org/2000/svg',
                elementType);

               // Copy data to SVG element.
               if (typeof data === 'object') { for (var key in data) { svgElement.setAttribute(key, data[key]); } }

               return svgElement;
            };
            break;

         default:
            throw new TypeError('getSVG error: unknown elementType.');
      }

      return returnVal;
   }

   /**
    * Hides the node context menu if visible and removes any node highlighting. If an event is supplied it is checked
    * against any existing context menu and is ignored if the context menu is within the parent hierarchy.
    *
    * @param {object|undefined}  event - Optional event
    */
   function hideNodeContextMenu(event)
   {
      // Provide an early out if there is no selected context node.
      if (typeof selectedContextNode === 'undefined') { return; }

      var contextMenuButton = $('#context-menu');
      var popupmenu = $('#contextpopup .mdl-menu__container');

      // If an event is defined then make sure it isn't targeting the context menu.
      if (event)
      {
         event.preventDefault();

         // Picked element is not the menu
         if (!$(event.target).parents('#contextpopup').length > 0)
         {
            // Hide menu if currently visible
            if (popupmenu.hasClass('is-visible')) { contextMenuButton.click(); }

            fadeRelatedNodes(selectedContextNode, false, nodes, links);
            selectedContextNode = undefined;
         }
      }
      else // No event defined so always close context menu and remove node highlighting.
      {
         // Hide menu if currently visible
         if (popupmenu.hasClass('is-visible')) { contextMenuButton.click(); }

         fadeRelatedNodes(selectedContextNode, false, nodes, links);
         selectedContextNode = undefined;
      }
   }

   /**
    * Checks if a target node is connected to another given node by checking `index` or the `linkedByIndex` map.
    *
    * @param {object}   targetNode
    * @param {object}   otherNode
    *
    * @returns {boolean}
    */
   function isConnected(targetNode, otherNode)
   {
      return targetNode.index === otherNode.index || linkedByIndex[targetNode.index + ',' + otherNode.index];
   }

   /**
    * Returns whether there is a currently selected node (drag or context).
    *
    * @returns {boolean}
    */
   function isNodeSelected()
   {
      return typeof selectedDragNode !== 'undefined' || typeof selectedContextNode !== 'undefined';
   }

   /**
    * Handles responding to the dependencies radio group.
    */
   function onControlDepsClicked()
   {
      // Do nothing if scope has not changed
      if (this.value === appOptions.currentScope) { return; }

      // If max depth is set to sticky and current level is equal max level of old scope then set max level for
      // the new scope.
      if (appOptions.maxDepthSticky && appOptions.currentLevel === dataPackageMap[appOptions.currentScope].maxLevel)
      {
         appOptions.currentLevel = dataPackageMap[this.value].maxLevel
      }

      appOptions.currentScope = this.value;

      var maxLevel = dataPackageMap[appOptions.currentScope].maxLevel;

      // Adjust current level if it is greater than max level for current scope.
      if (appOptions.currentLevel > maxLevel) { appOptions.currentLevel = maxLevel; }

      // Update control level UI based on current and max level for given scope.
      $('.control-level input').attr({ max: maxLevel });
      $('.control-level input').val(appOptions.currentLevel);
      $('.control-level label').html(appOptions.currentLevel);

      // Redraw graph data
      updateAll({ redrawOnly: true });

      // Center graph w/ zoom fit w/ 1 second transition applied after a potential 2 seconds delay for debounce.
      centerGraph(zoomFit, 1000, data.allNodesFixed ? 0 : 2000);
   }

   /**
    * Handles responding to the level slider.
    */
   function onControlLevelChanged()
   {
      appOptions.currentLevel = parseInt(this.value);

      $('.control-level input').val(appOptions.currentLevel);
      $('.control-level label').html(appOptions.currentLevel);

      // Redraw graph data
      updateAll({ redrawOnly: true });

      // Center graph w/ zoom fit w/ 1 second transition applied after a potential 2 seconds delay for debounce.
      centerGraph(zoomFit, 1000, data.allNodesFixed ? 0 : 2000);
   }

   /**
    * Handles reverse links input reversing the graph links then redrawing the graph.
    */
   function onControlLinksClicked()
   {
      reverseGraphLinks();

      renderGraph({ redrawOnly: true });
   }

   /**
    * Handles responding to overflow menu selections.
    */
   function onControlMenuClicked()
   {
      switch ($(this).data('action'))
      {
         case 'toggleFreezeAllNodes':
            setNodesFixed(!data.allNodesFixed);
            break;

         case 'showFullNames':
            appOptions.showFullNames = !appOptions.showFullNames;
            updateAll({ redrawOnly: true });
            break;

         case 'showTableView':
            appOptions.showTableView = !appOptions.showTableView;
            $('.control-table').toggleClass('hidden', !appOptions.showTableView);
            updateTableUIExtent();
            break;

         case 'maxDepthSticky':
            appOptions.maxDepthSticky = !appOptions.maxDepthSticky;
            break;
      }

      // Defer updating menu UI until menu is hidden.
      setTimeout(updateMenuUI, 200);
   }

   /**
    * Handles a context click on a table row showing the related node context menu.
    *
    * @param {object}   node - Target node.
    * @param {object}   event - mouse event
    */
   function onControlTableRowContextClick(node, event)
   {
      event.preventDefault(); // Prevents default browser context menu from showing.
      onNodeContextClick(node, { x: event.pageX, y: event.pageY });
   }

   /**
    * Handles a context click on a table row showing the related node context menu. Defers to `onNodeMouseOverOut`.
    *
    * @param {Array}    nodes - All graph nodes.
    * @param {Array}    links - All graph links.
    * @param {object}   node - Target node.
    * @param {boolean}  enter - true when mouse entering / false when mouse exiting.
    */
   function onControlTableRowMouseOver(nodes, links, node, enter)
   {
      // Hide the node context menu if currently showing when a new table row / node is moused over.
      if (node !== selectedContextNode) { hideNodeContextMenu(event); }

      onNodeMouseOverOut(nodes, links, enter, node);
   }

   /**
    * Handles clicks on zoom control buttons and invokes `centerGraph` with new scale value.
    */
   function onControlZoomClicked()
   {
      var newScale = 1;
      var scalePercentile = 0.20;

      // Add or subtract scale percentile from current scale value.
      switch ($(this).data('action'))
      {
         case 'zoom_in':
            newScale = Math.max(Math.min(zoom.scale() * (1 + scalePercentile), maxScaleExtent), minScaleExtent);
            break;

         case 'zoom_out':
            newScale = Math.max(zoom.scale() * (1 - scalePercentile), minScaleExtent);
            break;

         case 'zoom_all_out':
            newScale = zoomFit();
            break;
      }

      centerGraph(newScale);
   }

   /**
    * Handles clicks on the node context menu invoking any active actions.
    */
   function onNodeContextMenuClick()
   {
      // When a context menu is selected remove node highlighting.
      hideNodeContextMenu();
console.log('!! action: ' + $(this).data('action') + '; link: ' + $(this).data('link'));

      switch ($(this).data('action'))
      {
         case 'openLink':
            var link = $(this).data('link');

            if (typeof link === 'string')
            {
               window.open(link, '_blank', 'location=yes,menubar=yes,scrollbars=yes,status=yes');
            }
            break;
      }
   }

   /**
    * Shows the node context menu
    *
    * @param {object}   targetNode - The target node.
    *
    * @param {object}   coords - Object containing x / y position for context menu; if not provided Node position
    *                            determines coords.
    */
   function onNodeContextClick(targetNode, coords)
   {
      // Hides any existing node context menu.
      hideNodeContextMenu();

      if (typeof coords !== 'object') { coords = getElementCoords(this); }

      var popupmenu = $('#contextpopup .mdl-menu__container');

      var packageData = targetNode.packageData;

      var packageLink, packageType, scmLink, scmType;

      if (packageData)
      {
         if (packageData.packageLink)
         {
            packageLink = packageData.packageLink.link;
            packageType = packageData.packageLink.type;

            // Create proper name for package type.
            switch (packageType)
            {
               case 'npm':
                  packageType = 'NPM';
                  break;
            }
         }

         if (packageData.scmLink)
         {
            scmLink = packageData.scmLink.link;
            scmType = packageData.scmLink.type;

            // Create proper name for SCM type.
            switch (scmType)
            {
               case 'github':
                  scmType = 'Github';
                  break;
            }
         }
      }

      // Populate data for the context menu.
      popupmenu.find('li').each(function(index)
      {
         var liTarget = $(this);

         switch (index)
         {
            case 0:
               if (scmLink && scmType)
               {
                  liTarget.text('Open on ' + scmType);
                  liTarget.data('link', scmLink);
                  liTarget.removeClass('hidden');
               }
               else
               {
                  liTarget.addClass('hidden');
               }
               break;

            case 1:
               if (packageLink && packageType)
               {
                  liTarget.text('Open on ' + packageType);
                  liTarget.data('link', packageLink);
                  liTarget.removeClass('hidden');
               }
               else
               {
                  liTarget.addClass('hidden');
               }
               break;

            case 2:
               if (packageData && packageData.version)
               {
                  liTarget.text('Version: ' + packageData.version);
                  liTarget.removeClass('hidden');
               }
               else
               {
                  liTarget.addClass('hidden');
               }
               break;
         }
      });

      // Wrapping in a 100ms timeout allows MDL to draw animation when showing a context menu after one has been hidden.
      setTimeout(function()
      {
         // Assign new selected context node and highlight related nodes.
         selectedContextNode = targetNode;

         fadeRelatedNodes(selectedContextNode, true, nodes, links);

         // For MDL a programmatic click of the hidden context menu.
         var contextMenuButton = $("#context-menu");
         contextMenuButton.click();

         // Necessary to defer reposition of the context menu.
         setTimeout(function()
         {
            popupmenu.parent().css({ position: 'relative' });
            popupmenu.css({ left: coords.x, top: coords.y, position:'absolute' });
         }, 0);
      }, 100);
   }

   /**
    * Handles a mouse down action on a graph node. Hides any showing context menu. For left clicks the selected node
    * becomes the drag target and related nodes are faded. Any other mouse button is ignored.
    *
    * @param {Array}    nodes - All graph nodes.
    * @param {Array}    links - All graph links.
    * @param {object}   targetNode - The target node.
    */
   function onNodeMouseDown(nodes, links, targetNode)
   {
      hideNodeContextMenu();

      // Only select / drag nodes with left clicked otherwise stop propagation of event.
      if (d3.event.button === 0)
      {
         selectedDragNode = targetNode;

         // Select / highlight related nodes or remove attributes based on enter state.
         fadeRelatedNodes(targetNode, true, nodes, links);
      }
      else
      {
         d3.event.stopPropagation();
      }
   }

   /**
    * Handles fading related nodes to the given `targetNode` if there is no currently selected node.
    *
    * @param {Array}    nodes - All graph nodes.
    * @param {Array}    links - All graph links.
    * @param {boolean}  enter - true when mouse entering / false when mouse exiting.
    * @param {object}   targetNode - The target node.
    */
   function onNodeMouseOverOut(nodes, links, enter, targetNode)
   {
      // If there is an existing selected node then exit early.
      if (isNodeSelected()) { return; }

      // Select / highlight related nodes or remove attributes based on enter state.
      fadeRelatedNodes(targetNode, enter, nodes, links);
   }

   /**
    * Handles the window resize event.
    */
   function onResize()
   {
      // Update graph parameters.
      graphWidth = window.innerWidth;
      graphHeight = window.innerHeight;

      graph.attr('width', graphWidth).attr('height', graphHeight);

      layout.size([graphWidth, graphHeight]).resume();

      updateMenuUI();

      updateTableUIExtent();

      // Hides any existing node context menu.
      hideNodeContextMenu();

      centerGraph(zoomFit, 1000, data.allNodesFixed ? 0 : 2000);
   }

   /**
    * D3 tick handler for the forced graph layout.
    */
   function onTick()
   {
      nodes.attr('cx', function(node) { return node.x; })
       .attr('cy', function(node) { return node.y; })
       .attr('transform', function(node) { return 'translate(' + node.x + ',' + node.y + ')'; });

      // Pull data from data.nodes array. Provides a curve to the links.
      links.attr('d', function(link)
      {
         var sourceX = data.nodes[link.source.index].x;
         var sourceY = data.nodes[link.source.index].y;
         var targetX = data.nodes[link.target.index].x;
         var targetY = data.nodes[link.target.index].y;

         var dx = targetX - sourceX,
          dy = targetY - sourceY,
          dr = Math.sqrt(dx * dx + dy * dy);
         return 'M' + sourceX + ',' + sourceY + 'A' + dr + ',' + dr + ' 0 0,1 ' + targetX + ',' + targetY;
      });
   }

   /**
    * Handles the D3 zoom / scale event.
    */
   function onZoomChanged()
   {
      var newScale = Math.max(d3.event.scale, minScaleExtent);

      zoom.scale(newScale);

      graph.attr('transform', 'translate(' + d3.event.translate + ')' + ' scale(' + newScale + ')');
   }

   /**
    * Recycles all SVG elements above the top level SVGGElement defining the graph.
    */
   function recycleGraph()
   {
      var childNodes = graph.selectAll('g > *').remove();

      if (!Array.isArray(childNodes) && !Array.isArray(childNodes['0'])) { return; }

      // Get the child nodes group from selection.
      childNodes = childNodes['0'];

      for (var cntr = 0; cntr < childNodes.length; cntr++)
      {
         var childNode = childNodes[cntr];

         if (childNode instanceof SVGPathElement) { svgElementMap['path'].push(childNode); }
         else if (childNode instanceof SVGCircleElement) { svgElementMap['circle'].push(childNode); }
         else if (childNode instanceof SVGTextElement) { svgElementMap['text'].push(childNode); }
         else if (childNode instanceof SVGGElement)
         {
            childNode.removeAttribute('transform');  // Must remove current transform.
            svgElementMap['g'].push(childNode);
         }
      }
   }

   /**
    * Renders the graph after recycling any nodes above the top level `g` SVGGElement.
    *
    * @param {object}   options - Optional parameters:
    * ```
    * (boolean)   redrawOnly - sets layout.alpha to a low value to reduce bounce.
    * ```
    */
   function renderGraph(options)
   {
      options = typeof options === 'object' ? options : {};
      options.redrawOnly = typeof options.redrawOnly === 'boolean' ? options.redrawOnly : false;

      // Recycle all SVG elements above the first SVGGElement.
      recycleGraph();

      // Lines
      // Note: on second render link.source / target will be an object instead of a number.
      links = graph.append(getSVG('g')).selectAll('line')
       .data(data.links)
       .enter().append(getSVG('path'))
       .attr('class', 'link')
       .attr('marker-end', function() { return 'url(#regular)'; })
       .attr('data-source', function(link)
       {
          return typeof link.source === 'number' ? link.source : link.source.index;
       })
       .attr('data-target', function(link)
       {
          return typeof link.target === 'number' ? link.target : link.target.index;
       });

      // Nodes
      nodes = graph.append(getSVG('g')).selectAll('node')
       .data(data.nodes)
       .enter().append(getSVG('g'))
       .call(layout.drag)
       .attr('class', 'node');

      // Circles
      nodes.attr('class', function(node) { return formatClassName('node', node); });
      nodes.append(getSVG('circle'))
       .attr('id', function(node) { return formatClassName('id', node); })
       .attr('class', function(node) { return formatClassName('circle', node) + ' ' + node.packageData.jspmType; })
       .attr('r', circleRadius)
       .on('contextmenu', onNodeContextClick)
       .on('mousedown', onNodeMouseDown.bind(this, nodes, links))
       .on('mouseover', onNodeMouseOverOut.bind(this, nodes, links, true))
       .on('mouseout', onNodeMouseOverOut.bind(this, nodes, links, false))
       .on('dblclick.zoom', function(node) // Centers view on node.
       {
          d3.event.stopPropagation();

          var dcx = (window.innerWidth / 2 - node.x * zoom.scale());
          var dcy = (window.innerHeight / 2 - node.y * zoom.scale());

          zoom.translate([dcx, dcy]);

          graph.transition()
           .duration(500)
           .attr('transform', 'translate('+ dcx + ',' + dcy  + ')scale(' + zoom.scale() + ')');
       });

      // A copy of the text with a thick white stroke for legibility.
      nodes.append(getSVG('text'))
       .attr('x', 15)
       .attr('y', '.31em')
       .attr('class', function(node) { return 'shadow ' + formatClassName('text', node); })
       .text(function(node)
       {
          return (appOptions.showFullNames ? node.packageData.actualPackageName : node.packageData.packageName) +
           ' (' + node.minLevel +')';
       });

      nodes.append(getSVG('text'))
       .attr('class', function(node)
       {
          return node.packageData.isAlias ? 'isAliased ' : '' + formatClassName('text', node);
       })
       .attr('x', 15)
       .attr('y', '.31em')
       .text(function(node)
       {
          return (appOptions.showFullNames ? node.packageData.actualPackageName : node.packageData.packageName) +
           ' (' + node.minLevel +')';
       });

      // Set the force layout nodes / links and start the bounce.
      layout.nodes(data.nodes);
      layout.links(data.links);
      layout.start();

      linkedByIndex = {};

      // Build linked index
      data.links.forEach(function(node) { linkedByIndex[node.source.index + ',' + node.target.index] = true; });

      // Set a low alpha to provide minimal bounce when just redrawing.
      if (options.redrawOnly) { layout.alpha(0.01); }
   }

   /**
    * Reverses all graph links.
    */
   function reverseGraphLinks()
   {
      for (var key in dataPackageMap)
      {
         var graphData = dataPackageMap[key];

         graphData.links.forEach(function(link)
         {
            var linkSource = link.source;
            link.source = link.target;
            link.target = linkSource;
         });
      }
   }

   /**
    * Sets all nodes fixed (freeze) or unfixed (unfreeze).
    *
    * @param {boolean}  fixed - Fixed state for all nodes.
    */
   function setNodesFixed(fixed)
   {
      // Resets any fixed node state.
      if (nodes) { nodes.each(function(node) { d3.select(this).classed('fixed', node.fixed = fixed); }); }

      // Copy existing sim data to any package scope that contains the same node ID.
      for (var key in dataPackageMap) { dataPackageMap[key].nodes.forEach(function(node) { node.fixed = fixed; }); }

      if (data) { data.allNodesFixed = fixed; }
   }

   /**
    * Helper function that simply defers to the standard update / render process.
    *
    * @param renderOptions
    */
   function updateAll(renderOptions)
   {
      // Load graph data
      updateGraphData();

      renderGraph(renderOptions);

      updateMenuUI();

      updateTableUI();
   }

   /**
    * Filters graph data based on current app option parameters: `currentScope` and `currentLevel`.
    */
   function updateGraphData()
   {
      // Copy existing data / node parameters to new filtered data;
      if (data)
      {
         var existingNodeSimMap = {};

         // Collect existing simulation data.
         data.nodes.forEach(function(node)
         {
            existingNodeSimMap[node.id] =
            {
               weight: node.weight,
               x: node.x,
               y: node.y,
               px: node.px,
               py: node.py,
               fixed: node.fixed ? node.fixed : false
            };
         });

         // Copy existing sim data to any package scope that contains the same node ID.
         for (var mapKey in dataPackageMap)
         {
            var graphData = dataPackageMap[mapKey];

            graphData.nodes.forEach(function(node)
            {
               if (existingNodeSimMap[node.id])
               {
                  for (var key in existingNodeSimMap[node.id]) { node[key] = existingNodeSimMap[node.id][key]; }
               }
            });
         }
      }

      var allNodesFixed = true;

      // Set new data
      data =
      {
         directed: dataPackageMap[appOptions.currentScope].directed,
         multigraph: dataPackageMap[appOptions.currentScope].multigraph,
         graph: dataPackageMap[appOptions.currentScope].graph,
         links: dataPackageMap[appOptions.currentScope].links.filter(function(link)
         {
            return link.minLevel <= appOptions.currentLevel;
         }),
         nodes: dataPackageMap[appOptions.currentScope].nodes.filter(function(node)
         {
            if (typeof node.fixed === 'undefined' || node.fixed === false) { allNodesFixed = false; }
            return node.minLevel <= appOptions.currentLevel;
         })
      };

      data.allNodesFixed = allNodesFixed;
   }

   /**
    * Provides handling of changing menu text & material icons based on current state.
    */
   function updateMenuUI()
   {
      if (data)
      {
         $('.control-menu li[data-action=toggleFreezeAllNodes]').html(data.allNodesFixed ?
          'Unfreeze nodes' : 'Freeze nodes');
      }

      appMenuToggleOptions.forEach(function(key)
      {
         var icon = appOptions[key] ? 'check_box' : 'check_box_outline_blank';
         $('.control-menu li[data-action=' + key + '] i').html(icon);
      });
   }

   /**
    * Builds the table UI with all current node data and associates the active events on each row.
    */
   function updateTableUI()
   {
      var table = $('.control-table tbody');

      table.off('mouseenter', 'tr', onControlTableRowMouseOver);
      table.off('mouseleave', 'tr', onControlTableRowMouseOver);
      table.off('contextmenu', 'tr', onControlTableRowContextClick);

      table.empty();

      if (data)
      {
         data.nodes.forEach(function(node)
         {
            var nd = node.packageData;
            var name = appOptions.showFullNames ? nd.actualPackageName : nd.packageName;
            var isAliased = nd.isAlias ? ' isAliased' : '';

            var tr = $(
             '<tr>' +
                '<td class="mdl-data-table__cell--non-numeric' + isAliased + '">' + name + '</td>' +
                '<td class="mdl-data-table__cell--non-numeric">' + nd.jspmType + '</td>' +
                '<td class="mdl-data-table__cell--non-numeric">' + nd.version + '</td>' +
                '<td class="mdl-data-table__cell--non-numeric">' + node.minLevel + '</td>' +
             '</tr>');

            table.append(tr);

            tr.on('mouseenter', onControlTableRowMouseOver.bind(this, nodes, links, node, true));
            tr.on('mouseleave', onControlTableRowMouseOver.bind(this, nodes, links, node, false));
            tr.on('contextmenu', onControlTableRowContextClick.bind(this, node));
         });

         // Removes sort order for any header and signals update for new data
         $('#nodeTable th').removeClass('headerSortDown');
         $('#nodeTable th').removeClass('headerSortUp');
         $('#nodeTable').trigger('update');

         updateTableUIExtent();
      }
   }

   /**
    * Updates the node table max-height enabling scrolling as necessary.
    */
   function updateTableUIExtent()
   {
      var tableDiv = $('.control-table-inner');
      var nodeTable = $('#nodeTable');

      var tableHeight = nodeTable.height();

      var offset = tableDiv.offset();
      var maxTableHeight = window.innerHeight - offset.top - 20;

      tableDiv.css('max-height', maxTableHeight);
      nodeTable.css('margin-right', tableHeight > maxTableHeight ? '10px' : '0px');
   }

   /**
    * Determines a new scale that fits the entire graph into view.
    *
    * @returns {number}
    */
   function zoomFit()
   {
      var bounds = graph.node().getBBox();
      var parent = graph.node().parentElement;
      var fullWidth = parent.clientWidth, fullHeight = parent.clientHeight;
      var width = bounds.width, height = bounds.height;

      if (width === 0 || height === 0) { return 1; } // nothing to fit

      var scale = 0.75 / Math.max(width / fullWidth, height / fullHeight);

      scale = Math.max(Math.min(scale, maxScaleExtent), minScaleExtent);

      return scale;
   }

   // Prevent default browser context menu from being triggered.
   $(document).bind('contextmenu', function(event) { event.preventDefault(); });

   // Properly handle closing context menu when document mouse down clicked
   // This works when a node context click occurs because the new context menu is shown with a small timeout.
   $(document).bind('mousedown', hideNodeContextMenu);

   /**
    * This function removes the selected drag node highlighting if document mouse up event occurs outside the window
    * bounds the distance between the document mouse up value is greater than the radius of the current location
    * of the circle SVG element.
    */
   $(document).bind('mouseup', function(event)
   {
      if (typeof selectedDragNode !== 'undefined')
      {
         var pageX = event.pageX;
         var pageY = event.pageY;

         // Detect a drag / mouse up outside window bounds and automatically reset.
         if (pageX < 0 || pageY < 0 || pageX > window.innerWidth || pageY > window.innerHeight)
         {
            fadeRelatedNodes(selectedDragNode, false, nodes, links);
            selectedDragNode = undefined;
            return;
         }

         // Otherwise verify if the mouse up distance is greater than the SVGCircleElement location.

         var elm = findElementByNode('circle', selectedDragNode);

         var coords = getElementCoords(elm.node());

         var a = pageX - coords.x;
         var b = pageY - coords.y;

         var distance = Math.sqrt(a * a + b * b);

         // Remove node highlight and related nodes
         if (distance > circleRadius) { fadeRelatedNodes(selectedDragNode, false, nodes, links); }

         selectedDragNode = undefined;
      }
   });

   bootstrap();
})();


/**
 * @fileOverview Contains standard code in the namespace 'wbos' and code specifically written for Codeview in the namespace 'codeview'
 * @author Wouter Bos (www.thebrightlines.com)
 * @since 1.0 - 2010-09-10
 * @version 1.0 - 2010-09-10
 */

if (typeof(wbos) == "undefined") {
	/**
	 * @namespace Standard code of Wouter Bos (wbos)
	 */
	wbos = {}
}
if (typeof(wbos.CssTools) == "undefined") {
	/**
	 * @namespace Namespace for CSS-related functionality
	 */
	wbos.CssTools = {}
}

/**
 * @namespace Fallback for CSS advanced media query
 * @class
 * @since 1.0 - 2010-09-10
 * @version 1.0 - 2010-09-10
 */
wbos.CssTools.MediaQueryFallBack = ( function() {
	var config = {
		cssScreen: "/css/screen.css",
		cssHandheld: "/css/handheld.css",
		mobileMaxWidth: 660,
		testDivClass: "cssLoadCheck",
		dynamicCssLinkId: "DynCssLink",
		resizeDelay: 30
	}
	var noMediaQuery = false;
	var delay;
	var currentCssMediaType;

	// Adding events to elements in the DOM without overwriting it
	function addEvent(element, newFunction, eventType) {
		var oldEvent = eval("element." + eventType);
		var eventContentType = eval("typeof element." + eventType)

		if ( eventContentType != 'function' ) {
			eval("element." + eventType + " = newFunction")
		} else {
			eval("element." + eventType + " = function(e) { oldEvent(e); newFunction(e); }")
		}
	}

	// Get the the inner width of the browser window
	function getWindowWidth() {
		if (window.innerWidth) {
			return window.innerWidth;
		} else if (document.documentElement.clientWidth) {
			return document.documentElement.clientWidth;
		} else if (document.body.clientWidth) {
			return document.body.clientWidth;
		} else{
			return 0;
		}
	}

	function addCssLink(cssHref) {
		var cssNode = document.createElement('link');
		var windowWidth;
		cssNode.type = 'text/css';
		cssNode.rel = 'stylesheet';
		cssNode.media = 'screen, handheld, fallback';
		cssNode.href = cssHref;
		document.getElementsByTagName("head")[0].appendChild(cssNode);
	}

	/* Start public */
	return {
  		/**
		 * Adds link to CSS in the head if no CSS is loaded
		 *
		 * @since 1.0 - 2010-08-21
		 * @version 1.0 - 2010-08-21
		 * @param {String|Object} cssScreen URL to CSS file for larger screens
		 * @param {String|Object} cssHandheld URL to CSS file for smaller screens
		 * @param {Number} mobileMaxWidth Maximum  width for handheld devices
		 * @example
		 * wbos.CssTools.MediaQueryFallBack.LoadCss(['screen.css', 'screen2.css'], 'mobile.css', 480)
		 */
		LoadCss: function(cssScreen, cssHandheld, mobileMaxWidth) {
			// Set config values
			if (typeof(cssScreen) != "undefined") {
				config.cssScreen = cssScreen;
			}
			if (typeof(cssHandheld) != "undefined") {
				config.cssHandheld = cssHandheld;
			}
			if (typeof(mobileMaxWidth) != "undefined") {
				config.mobileMaxWidth = mobileMaxWidth;
			}

			// Check if CSS is loaded
			var cssloadCheckNode = document.createElement('div');
			cssloadCheckNode.className = config.testDivClass;
			document.getElementsByTagName("body")[0].appendChild(cssloadCheckNode);
			if (cssloadCheckNode.offsetWidth != 100 && noMediaQuery == false) {
				noMediaQuery = true;
			}
			cssloadCheckNode.parentNode.removeChild(cssloadCheckNode)

			if (noMediaQuery == true) {
				// Browser does not support Media Queries, so JavaScript will supply a fallback
				var cssHref = "";

				// Determines what CSS file to load
				if (getWindowWidth() <= config.mobileMaxWidth) {
					cssHref = config.cssHandheld;
					newCssMediaType = "handheld";
				} else {
					cssHref = config.cssScreen;
					newCssMediaType = "screen";
				}

				// Add CSS link to <head> of page
				if (cssHref != "" && currentCssMediaType != newCssMediaType) {
					var currentCssLinks = document.styleSheets
					for (var i = 0; i < currentCssLinks.length; i++) {
						for (var ii = 0; ii < currentCssLinks[i].media.length; ii++) {
							if (typeof(currentCssLinks[i].media) == "object") {
								if (currentCssLinks[i].media.item(ii) == "fallback") {
									currentCssLinks[i].ownerNode.parentNode.removeChild(currentCssLinks[i].ownerNode)
									i--
									break;
								}
							} else {
								if (currentCssLinks[i].media.indexOf("fallback") >= 0) {
									currentCssLinks[i].owningElement.parentNode.removeChild(currentCssLinks[i].owningElement)
									i--
									break;
								}
							}
						}
					}
					if (typeof(cssHref) == "object") {
						for (var i = 0; i < cssHref.length; i++) {
							addCssLink(cssHref[i])
						}
					} else {
						addCssLink(cssHref)
					}

					currentCssMediaType = newCssMediaType;
				}

				// Check screen size again if user resizes window
				addEvent(window, wbos.CssTools.MediaQueryFallBack.LoadCssDelayed, 'onresize')
			}
		},

  		/**
		 * Runs LoadCSS after a short delay
		 *
		 * @since 1.0 - 2010-08-21
		 * @version 1.0 - 2010-08-21
		 * @example
		 * wbos.CssTools.MediaQueryFallBack.LoadCssDelayed()
		 */
		LoadCssDelayed: function() {
			clearTimeout(delay);
			delay = setTimeout( "wbos.CssTools.MediaQueryFallBack.LoadCss()", config.resizeDelay)
		}

	}
	/* End public */
})();

/**
 * @namespace Adds a function to an event of a single element. Use this if
 * you don't want to use jQuery
 * @class
 * @since 1.0 - 2010-02-23
 * @version 1.0 - 2010-02-23
 */
wbos.Events = ( function() {
	/* Start public */
	return {
  		/**
		 * Adds a function to an event of a single element
		 *
		 * @since 1.0 - 2010-02-23
		 * @version 1.0 - 2010-02-23
		 * @param {Object} element The element on which the event is placed
		 * @param {Function} newFunction The function that has to be linked to the event
		 * @param {String} eventType Name of the event
		 * @example
		 * wbos.Events.AddEvent( document.getElementById('elementId'), functionName, "onclick" )
		 */
		AddEvent: function( element, newFunction, eventType ) {
			var oldEvent = eval("element." + eventType);
			var eventContentType = eval("typeof element." + eventType)

			if ( eventContentType != 'function' ) {
				eval("element." + eventType + " = newFunction")
			} else {
				eval("element." + eventType + " = function(e) { oldEvent(e); newFunction(e); }")
			}
		}
	}
	/* End public */
})();

if (typeof(codeview) == "undefined") {
	/**
	 * @namespace Code written for the Codeview template
	 */
	codeview = {}
}

/**
 * @namespace Enables filtering in class lists
 * @class
 * @since 1.0 - 2010-11-08
 * @version 1.0 - 2010-11-08
 */
codeview.classFilter = ( function() {
	function onkeyup_ClassFilter() {
		var listItems
		var search = document.getElementById('ClassFilter').value
		search = search.toLowerCase()
		if (document.getElementById('ClassList')) {
			listItems = document.getElementById('ClassList').getElementsByTagName('li')
			filterList(listItems, search)
		}
		if (document.getElementById('ClassList2')) {
			listItems = document.getElementById('ClassList2').getElementsByTagName('li')
			filterList(listItems, search)
		}
		if (document.getElementById('FileList')) {
			listItems = document.getElementById('FileList').getElementsByTagName('li')
			filterList(listItems, search)
		}
		if (document.getElementById('MethodsListInherited')) {
			var links = document.getElementById('MethodsListInherited').getElementsByTagName('a')
			var linksSelected = new Array()
			for (var i=0; i < links.length; i++) {
				if (links[i].parentNode.parentNode.tagName == "DD") {
					linksSelected.push(links[i])
				}
			}
			filterList(linksSelected, search)
		}
		if (document.getElementById('MethodsList')) {
			listItems = document.getElementById('MethodsList').getElementsByTagName('tbody')[0].getElementsByTagName('tr')
			filterList(listItems, search, document.getElementById('MethodDetail').getElementsByTagName('li'))
		}
	}

	function filterList(listItems, search, relatedElements) {
		var itemContent = ""
		for (var i=0; i < listItems.length; i++) {
			itemContent = listItems[i].textContent||listItems[i].innerText
			if (itemContent != undefined) {
				itemContent = itemContent.toLowerCase()
				itemContent = itemContent.replace(/\s/g, "")
				if (itemContent.indexOf(search) >= 0 || itemContent == "") {
					listItems[i].style.display = ""
				} else {
					listItems[i].style.display = "none"
				}
				if (relatedElements != null) {
					filterRelatedList(listItems[i], search, relatedElements)
				}
			}
		}
	}

	function filterRelatedList(listItem, search, relatedElements) {
		var itemIndex = parseInt(listItem.className.replace('item', ''))
		if (itemIndex <= relatedElements.length) {
			if (relatedElements[itemIndex].className == "item"+ itemIndex) {
				relatedElements[itemIndex].style.display = listItem.style.display
			}
		}
	}

	/* Start public */
	return {
		Init: function() {
			wbos.Events.AddEvent(
				document.getElementById('ClassFilter'),
				onkeyup_ClassFilter,
				"onkeyup"
			)
		}
	}
	/* End public */
})();


var through = require('through2');
var gutil = require('gulp-util');
var rebound = require('reboundjs').default;
var PluginError = gutil.PluginError;

// Consts
const PLUGIN_NAME = 'gulp-rebound';

function prefixStream(prefixText) {
  var stream = through();
  stream.write(prefixText);
  return stream;
}

// Plugin level function(dealing with files)
function gulpRebound(options) {

  options || (options = {});

  // Creating a stream through which each file will pass
  return through.obj(function(file, enc, cb) {
    // return empty file
    if (file.isNull()) return cb(null, file);

    if (file.isStream()) {
      throw new PluginError(PLUGIN_NAME, "Gulp Rebound doesn't handle streams!");
    }

    // Compile
    try{
      file.contents = new Buffer(rebound(file.contents.toString(enc), {
        name: file.path,
        baseDest: options.baseUrl || ''
      }).src, enc);
      gutil.log(gutil.colors.green('File ' + file.relative + ' compiled'));
      file.path = file.path.replace('.html', '.js');
    } catch(err){
      gutil.log(gutil.colors.red('Error in ' + file.relative));
      gutil.log(err);
    }

    cb(null, file);

  });

}

// Exporting the plugin main function
module.exports = gulpRebound;


var fs = require('fs');
var Utils = require('../core');

/* Utils.clear()
 * If folder exist, clear files inside
 * Else create it */

function clearFile (obj) {
	if (Utils.tools.isArray(obj))
		obj.forEach(_clearFile);
	else
		_clearFile(obj);

	return Utils;
}

function _clearFile (path) {
	fs.writeFileSync(path, '');
	return Utils.log('Cleared: ' + path);
};

function clearFolder (obj) {
	if (Utils.tools.isArray(obj))
		obj.forEach(_clearFolder);
	else
		_clearFolder(obj);

	return Utils;
}

function _clearFolder (path) {
	var exist = Utils.exist(path);
	var suf = path.slice(-1) === '/' ? '**/*' : '/**/*'
	return exist ? Utils.del(path + suf) : Utils.create.folder(path);
}

module.exports = {
	file: clearFile,
	folder: clearFolder
};


/*
librarianowl
https://github.com/franklin/node-librarianowl

Copyright (c) 2013 franklin
Licensed under the MIT license.
 */

(function() {
  "use strict";
  var Item, Librarianowl, Util;

  Librarianowl = (function() {
    var dive, fs, path, _;

    function Librarianowl() {}

    _ = require('underscore');

    dive = require("diveSync");

    path = require("path");

    fs = require("fs-extra");

    /*
    @var {Object}
     */

    Librarianowl.prototype.options = null;

    /*
    @var {String}
     */

    Librarianowl.prototype.target = null;

    /*
    @var {String}
     */

    Librarianowl.prototype.source = null;

    Librarianowl.prototype.compile = function(source, target, options) {
      var defaults;
      this.source = source;
      this.target = target;
      if (options == null) {
        options = {};
      }
      defaults = {
        helpers: {},
        filter: false,
        filename: this._filename,
        syntaxes: ["less", "sass", "scss", "styl"],
        template: "" + (path.dirname(__dirname)) + "/templates/default.hbs"
      };
      _.extend(this.options = {}, defaults, options);
      this._cleanTarget();
      return this._compileSource();
    };

    /*
    @param {Item} item
    @param {String} syntax
     */

    Librarianowl.prototype._filename = function(item, syntax) {
      return "" + item.module + "." + syntax;
    };

    /*
    Clean the target directory if exists
     */

    Librarianowl.prototype._cleanTarget = function() {
      if (fs.existsSync(this.target)) {
        return fs.removeSync(this.target);
      }
    };

    /*
    Scan the source directory and compile files
     */

    Librarianowl.prototype._compileSource = function() {
      return dive(this.source, (function(_this) {
        return function(error, file) {
          var item, syntax, _i, _len, _ref, _results;
          if (error) {
            throw error;
          }
          if (path.extname(file) !== ".yml") {
            return;
          }
          if (_this.options.filter && !_this.options.filter(path.basename(file))) {
            return;
          }
          item = new Item(_this.source, file);
          _ref = _this.options.syntaxes;
          _results = [];
          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
            syntax = _ref[_i];
            _results.push(_this._compileSourceItem(item, syntax));
          }
          return _results;
        };
      })(this));
    };

    /*
    @param {Item} item
    @param {String} syntax
     */

    Librarianowl.prototype._compileSourceItem = function(item, syntax) {
      var stream, targetDir, targetFile, targetFilename;
      targetDir = path.resolve(this.target, syntax, item["package"]);
      targetFilename = this.options.filename(item, syntax);
      targetFile = path.resolve(targetDir, targetFilename);
      stream = fs.existsSync(targetFile) ? fs.readFileSync(targetFile) : "";
      fs.mkdirpSync(targetDir);
      if ((item.yaml.imports != null) && (item.yaml.imports[syntax] != null)) {
        stream = item.yaml.imports[syntax] + "\n" + stream;
      } else {
        item.yaml.syntax = syntax;
        item.yaml.styles.syntax = item.yaml.styles[syntax];
        item.yaml.examples.syntax = item.yaml.examples[syntax];
        stream += Util.render(item.yaml, this.options.template, this.options.helpers);
      }
      return fs.writeFileSync(targetFile, stream);
    };

    return Librarianowl;

  })();

  module.exports = new Librarianowl;

  Item = (function() {
    var fs, path, yaml, _str;

    yaml = require('js-yaml');

    path = require("path");

    fs = require("fs-extra");

    _str = require("underscore.string");

    /*
    @var {String}
     */

    Item.prototype.file = null;

    /*
    @var {String}
     */

    Item.prototype.source = null;

    /*
    @var {String}
     */

    Item.prototype.dirname = null;

    /*
    @var {String}
     */

    Item.prototype.basename = null;

    /*
    @var {String}
     */

    Item.prototype.relPath = null;

    /*
    @var {String}
     */

    Item.prototype.module = null;

    /*
    @var {String}
     */

    Item.prototype["package"] = null;

    /*
    @var {Object}
     */

    Item.prototype.yaml = null;

    /*
    @param {String} source
    @param {String} file
     */

    function Item(source, file) {
      this.source = source;
      this.file = file;
      this.dirname = path.dirname(this.file);
      this.basename = path.basename(this.file, '.yml');
      this.relPath = path.relative(this.source, this.dirname);
      this.yaml = yaml.load(fs.readFileSync(this.file, 'utf8'));
      this["package"] = path.dirname(this.relPath);
      this.module = path.basename(this.relPath);
    }

    return Item;

  })();

  Util = (function() {
    var fs, hbs, path, _;

    function Util() {}

    _ = require('underscore');

    fs = require("fs-extra");

    path = require("path");

    hbs = require("handlebars");

    /*
    @param {Object} obj
    @param {String} template
    @param {Object} helpers
    @return {String}
     */

    Util.render = function(obj, template, helpers) {
      var builder, key, value;
      if (helpers == null) {
        helpers = null;
      }
      builder = hbs.compile(fs.readFileSync(path.resolve(template), "utf8"));
      if (_.isString(helpers) && fs.existsSync(helpers)) {
        GLOBAL.hbs = hbs;
        require(path.resolve(helpers));
      } else if (_.isObject(helpers)) {
        for (key in helpers) {
          value = helpers[key];
          hbs.registerHelper(key, value);
        }
      }
      return builder(obj);
    };

    return Util;

  })();

}).call(this);


/* Index */

var Q = require('q');
var api_config = require('nor-config').from(require('path').dirname(__dirname));
var IS = require('./is.js');
var helpers = require('./helpers.js');
var client = require('./client.js');
var errors = require('./errors.js');
var RequestRouter = require('./Router.js');
var api = module.exports = {};

var flags = require('./flags.js');
api.replySent = flags.replySent;
api.notFound = flags.notFound;

/** Sends successful HTTP reply */
function do_success(req, res, msg) {
	res.writeHead(200, {'Content-Type': ((typeof msg === 'string') ? 'text/plain' : 'application/json')});
	msg = (typeof msg === 'string') ? msg : helpers.stringify(msg);
	res.end(msg);
}

/** Sends failed HTTP reply */
function do_failure(req, res, opts) {
	opts = opts || {};
	var obj = {
		'type': opts.type || 'error',
		'code': opts.code || 501,
		'desc': opts.desc || (''+opts)
	};

	res.writeHead(obj.code, {'Content-Type': 'application/json'});
	res.end(helpers.stringify(obj) + '\n');
}

/** Builder for generic HTTP Request Handler */
function do_create_req(config, routes) {
	routes = routes || {};

	var version = routes.version || {};
	if(version && (typeof version === 'object')) {
	} else {
		version = {'self':routes.version || config.pkg.version};
	}

	if(!version.api) {
		version.api = api_config.pkg.version;
	}

	routes.version = version;

	var router = new RequestRouter(routes);
	var req_counter = 0;

	/* Inner Request handler */
	function do_req(req, res) {
		req_counter += 1;
		return router.resolve( req, res );
	} // do_req

	return do_req;
}

/** HTTP Server Creation */
function do_create_server(config, do_req) {
	var http = require('http');
	if(config.host) {
		http.createServer(do_req).listen(config.port, config.host);
		//console.log("Server running at http://"+config.host+":"+config.port+"/");
	} else {
		http.createServer(do_req).listen(config.port);
		//console.log("Server running at http://0.0.0.0:"+config.port);
	}
	return http;
}

/** API server builder */
function setup_server(config, opts) {
	config._def('port', 3000);
	var req_handler = do_create_req(config, opts);
	var server = do_create_server(config, function(req, res) {
		req_handler(req, res).then(function(obj) {
			if(obj === api.replySent) {
				// Silently return since reply has been handler already.
				return;
			} else if(obj === api.notFound) {
				do_failure(req, res, {'verb': 'notFound', 'desc':'The requested resource could not be found.', 'code':404});
			} else {
				do_success(req, res, obj);
			}
		}).fail(function(err) {
			do_failure(req, res, err);
			if(!(err instanceof errors.HTTPError)) {
				require('prettified').errors.print(err);
			}
		}).done();
	});
	return server;
}

/** Handle the request with a first matching request handler from the list */
/* Not tested yet
api.first = function(list) {
	function handler(req, res) {
		var init = {'type':'initial value'};
		var paths = [];

		function check_obj(obj) {
			if( (obj === api.notFound) || (obj === init) ) {
				//console.log('DEBUG: Skipping: ', obj);
			} else {
				//console.log('DEBUG: Found: ', obj);
				paths.push( obj );
			}
			return obj;
		}

		var p = list.map(function(f) {
			return f.bind(undefined, req, res);
		}).reduce(function (soFar, f) {
			return soFar.then(check_obj).then(f);
		}, Q.resolve(init)).then(check_obj).then(function() {
			if(paths.length === 0) {
				return api.notFound;
			}
			return paths.shift();
		});
		return p;
	}
	return handler;
}; // api.first
*/

// Exports
api.createHandler = do_create_req;
api.createServer = setup_server;
api.request = client.request;
api.errors = errors;

/* EOF */


// Cross-browser event listener
var addEvent = function(element, ev, fn){
	if(element.addEventListener)
		element.addEventListener(ev, fn, false);
	else element.attachEvent("on" + ev, fn);
}

// Synchronise preview scroll to editor scrolling
module.exports = function(){
	// If preview element isn't defined.
	if(this.$preview !== null){

		// Sync scrolling between editor and preview
		addEvent(this.$scroll, "scroll", function(){

			// Percentage editor is scrolled for the top.
			var editorPercentFromTop = (100 / this.$scroll.scrollTopMax) * this.$scroll.scrollTop,
				// How many pixels down the preview should be to match.
				previewScrolltop = (this.$preview.scrollTopMax / 100) * editorPercentFromTop;

			// Set preview scroll
			this.$preview.scrollTop = previewScrolltop;

		}.bind(this));

	}
}


/*
 * Copyright (C) 2013 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * @interface
 */
WebInspector.SuggestBoxDelegate = function()
{
}

WebInspector.SuggestBoxDelegate.prototype = {
    /**
     * @param {string} suggestion
     * @param {boolean=} isIntermediateSuggestion
     */
    applySuggestion: function(suggestion, isIntermediateSuggestion) { },

    /**
     * acceptSuggestion will be always called after call to applySuggestion with isIntermediateSuggestion being equal to false.
     */
    acceptSuggestion: function() { },
}

/**
 * @constructor
 * @param {!WebInspector.SuggestBoxDelegate} suggestBoxDelegate
 * @param {number=} maxItemsHeight
 */
WebInspector.SuggestBox = function(suggestBoxDelegate, maxItemsHeight)
{
    this._suggestBoxDelegate = suggestBoxDelegate;
    this._length = 0;
    this._selectedIndex = -1;
    this._selectedElement = null;
    this._maxItemsHeight = maxItemsHeight;
    this._maybeHideBound = this._maybeHide.bind(this);
    this._container = createElementWithClass("div", "suggest-box-container");
    this._element = this._container.createChild("div", "suggest-box");
    this._element.addEventListener("mousedown", this._onBoxMouseDown.bind(this), true);
    this._detailsPopup = this._container.createChild("div", "suggest-box details-popup monospace");
    this._detailsPopup.classList.add("hidden");
    this._asyncDetailsCallback = null;
    /** @type {!Map<number, !Promise<{detail: string, description: string}>>} */
    this._asyncDetailsPromises = new Map();
}

/**
 * @typedef Array.<{title: string, className: (string|undefined)}>
 */
WebInspector.SuggestBox.Suggestions;

WebInspector.SuggestBox.prototype = {
    /**
     * @return {boolean}
     */
    visible: function()
    {
        return !!this._container.parentElement;
    },

    /**
     * @param {!AnchorBox} anchorBox
     */
    setPosition: function(anchorBox)
    {
        this._updateBoxPosition(anchorBox);
    },

    /**
     * @param {!AnchorBox} anchorBox
     */
    _updateBoxPosition: function(anchorBox)
    {
        console.assert(this._overlay);
        if (this._lastAnchorBox && this._lastAnchorBox.equals(anchorBox))
            return;
        this._lastAnchorBox = anchorBox;

        // Position relative to main DevTools element.
        var container = WebInspector.Dialog.modalHostView().element;
        anchorBox = anchorBox.relativeToElement(container);
        var totalHeight = container.offsetHeight;
        var aboveHeight = anchorBox.y;
        var underHeight = totalHeight - anchorBox.y - anchorBox.height;

        this._overlay.setLeftOffset(anchorBox.x);

        var under = underHeight >= aboveHeight;
        if (under)
            this._overlay.setVerticalOffset(anchorBox.y + anchorBox.height, true);
        else
            this._overlay.setVerticalOffset(totalHeight - anchorBox.y, false);

        /** const */ var rowHeight = 17;
        /** const */ var spacer = 6;
        var maxHeight = this._maxItemsHeight ? this._maxItemsHeight * rowHeight : Math.max(underHeight, aboveHeight) - spacer;
        this._element.style.maxHeight = maxHeight + "px";
    },

    /**
     * @param {!Event} event
     */
    _onBoxMouseDown: function(event)
    {
        if (this._hideTimeoutId) {
            window.clearTimeout(this._hideTimeoutId);
            delete this._hideTimeoutId;
        }
        event.preventDefault();
    },

    _maybeHide: function()
    {
        if (!this._hideTimeoutId)
            this._hideTimeoutId = window.setTimeout(this.hide.bind(this), 0);
    },

    /**
     * // FIXME: make SuggestBox work for multiple documents.
     * @suppressGlobalPropertiesCheck
     */
    _show: function()
    {
        if (this.visible())
            return;
        this._bodyElement = document.body;
        this._bodyElement.addEventListener("mousedown", this._maybeHideBound, true);
        this._overlay = new WebInspector.SuggestBox.Overlay();
        this._overlay.setContentElement(this._container);
    },

    hide: function()
    {
        if (!this.visible())
            return;

        this._bodyElement.removeEventListener("mousedown", this._maybeHideBound, true);
        delete this._bodyElement;
        this._container.remove();
        this._overlay.dispose();
        delete this._overlay;
        delete this._selectedElement;
        this._selectedIndex = -1;
        delete this._lastAnchorBox;
    },

    removeFromElement: function()
    {
        this.hide();
    },

    /**
     * @param {boolean=} isIntermediateSuggestion
     */
    _applySuggestion: function(isIntermediateSuggestion)
    {
        if (!this.visible() || !this._selectedElement)
            return false;

        var suggestion = this._selectedElement.__fullValue;
        if (!suggestion)
            return false;

        this._suggestBoxDelegate.applySuggestion(suggestion, isIntermediateSuggestion);
        return true;
    },

    /**
     * @return {boolean}
     */
    acceptSuggestion: function()
    {
        var result = this._applySuggestion();
        this.hide();
        if (!result)
            return false;

        this._suggestBoxDelegate.acceptSuggestion();

        return true;
    },

    /**
     * @param {number} shift
     * @param {boolean=} isCircular
     * @return {boolean} is changed
     */
    _selectClosest: function(shift, isCircular)
    {
        if (!this._length)
            return false;

        if (this._selectedIndex === -1 && shift < 0)
            shift += 1;

        var index = this._selectedIndex + shift;

        if (isCircular)
            index = (this._length + index) % this._length;
        else
            index = Number.constrain(index, 0, this._length - 1);

        this._selectItem(index, true);
        this._applySuggestion(true);
        return true;
    },

    /**
     * @param {!Event} event
     */
    _onItemMouseDown: function(event)
    {
        this._selectedElement = event.currentTarget;
        this.acceptSuggestion();
        event.consume(true);
    },

    /**
     * @param {string} prefix
     * @param {string} text
     * @param {string|undefined} className
     * @param {number} index
     */
    _createItemElement: function(prefix, text, className, index)
    {
        var element = createElementWithClass("div", "suggest-box-content-item source-code " + (className || ""));
        element.tabIndex = -1;
        if (prefix && prefix.length && !text.indexOf(prefix)) {
            element.createChild("span", "prefix").textContent = prefix;
            element.createChild("span", "suffix").textContent = text.substring(prefix.length).trimEnd(50);
        } else {
            element.createChild("span", "suffix").textContent = text.trimEnd(50);
        }
        element.__fullValue = text;
        element.createChild("span", "spacer");
        element.addEventListener("mousedown", this._onItemMouseDown.bind(this), false);
        return element;
    },

    /**
     * @param {!WebInspector.SuggestBox.Suggestions} items
     * @param {string} userEnteredText
     * @param {function(number): !Promise<{detail:string, description:string}>=} asyncDetails
     */
    _updateItems: function(items, userEnteredText, asyncDetails)
    {
        this._length = items.length;
        this._asyncDetailsPromises.clear();
        this._asyncDetailsCallback = asyncDetails;
        this._element.removeChildren();
        delete this._selectedElement;

        for (var i = 0; i < items.length; ++i) {
            var item = items[i];
            var currentItemElement = this._createItemElement(userEnteredText, item.title, item.className, i);
            this._element.appendChild(currentItemElement);
        }
    },

    /**
     * @param {number} index
     * @return {!Promise<?{detail: string, description: string}>}
     */
    _asyncDetails: function(index)
    {
        if (!this._asyncDetailsCallback)
            return Promise.resolve(/** @type {?{description: string, detail: string}} */(null));
        if (!this._asyncDetailsPromises.has(index))
            this._asyncDetailsPromises.set(index, this._asyncDetailsCallback(index));
        return /** @type {!Promise<?{detail: string, description: string}>} */(this._asyncDetailsPromises.get(index));
    },

    /**
     * @param {?{detail: string, description: string}} details
     */
    _showDetailsPopup: function(details)
    {
        this._detailsPopup.removeChildren();
        if (!details)
            return;
        this._detailsPopup.createChild("section", "detail").createTextChild(details.detail);
        this._detailsPopup.createChild("section", "description").createTextChild(details.description);
        this._detailsPopup.classList.remove("hidden");
    },

    /**
     * @param {number} index
     * @param {boolean} scrollIntoView
     */
    _selectItem: function(index, scrollIntoView)
    {
        if (this._selectedElement)
            this._selectedElement.classList.remove("selected");

        this._selectedIndex = index;
        if (index < 0)
            return;

        this._selectedElement = this._element.children[index];
        this._selectedElement.classList.add("selected");
        this._detailsPopup.classList.add("hidden");
        var elem = this._selectedElement;
        this._asyncDetails(index).then(showDetails.bind(this), function(){});

        if (scrollIntoView)
            this._selectedElement.scrollIntoViewIfNeeded(false);

        /**
         * @param {?{detail: string, description: string}} details
         * @this {WebInspector.SuggestBox}
         */
        function showDetails(details)
        {
            if (elem === this._selectedElement)
                this._showDetailsPopup(details);
        }
    },

    /**
     * @param {!WebInspector.SuggestBox.Suggestions} completions
     * @param {boolean} canShowForSingleItem
     * @param {string} userEnteredText
     */
    _canShowBox: function(completions, canShowForSingleItem, userEnteredText)
    {
        if (!completions || !completions.length)
            return false;

        if (completions.length > 1)
            return true;

        // Do not show a single suggestion if it is the same as user-entered prefix, even if allowed to show single-item suggest boxes.
        return canShowForSingleItem && completions[0].title !== userEnteredText;
    },

    _ensureRowCountPerViewport: function()
    {
        if (this._rowCountPerViewport)
            return;
        if (!this._element.firstChild)
            return;

        this._rowCountPerViewport = Math.floor(this._element.offsetHeight / this._element.firstChild.offsetHeight);
    },

    /**
     * @param {!AnchorBox} anchorBox
     * @param {!WebInspector.SuggestBox.Suggestions} completions
     * @param {number} selectedIndex
     * @param {boolean} canShowForSingleItem
     * @param {string} userEnteredText
     * @param {function(number): !Promise<{detail:string, description:string}>=} asyncDetails
     */
    updateSuggestions: function(anchorBox, completions, selectedIndex, canShowForSingleItem, userEnteredText, asyncDetails)
    {
        if (this._canShowBox(completions, canShowForSingleItem, userEnteredText)) {
            this._updateItems(completions, userEnteredText, asyncDetails);
            this._show();
            this._updateBoxPosition(anchorBox);
            this._selectItem(selectedIndex, selectedIndex > 0);
            delete this._rowCountPerViewport;
        } else
            this.hide();
    },

    /**
     * @param {!KeyboardEvent} event
     * @return {boolean}
     */
    keyPressed: function(event)
    {
        switch (event.keyIdentifier) {
        case "Up":
            return this.upKeyPressed();
        case "Down":
            return this.downKeyPressed();
        case "PageUp":
            return this.pageUpKeyPressed();
        case "PageDown":
            return this.pageDownKeyPressed();
        case "Enter":
            return this.enterKeyPressed();
        }
        return false;
    },

    /**
     * @return {boolean}
     */
    upKeyPressed: function()
    {
        return this._selectClosest(-1, true);
    },

    /**
     * @return {boolean}
     */
    downKeyPressed: function()
    {
        return this._selectClosest(1, true);
    },

    /**
     * @return {boolean}
     */
    pageUpKeyPressed: function()
    {
        this._ensureRowCountPerViewport();
        return this._selectClosest(-this._rowCountPerViewport, false);
    },

    /**
     * @return {boolean}
     */
    pageDownKeyPressed: function()
    {
        this._ensureRowCountPerViewport();
        return this._selectClosest(this._rowCountPerViewport, false);
    },

    /**
     * @return {boolean}
     */
    enterKeyPressed: function()
    {
        var hasSelectedItem = !!this._selectedElement;
        this.acceptSuggestion();

        // Report the event as non-handled if there is no selected item,
        // to commit the input or handle it otherwise.
        return hasSelectedItem;
    }
}

/**
 * @constructor
 * // FIXME: make SuggestBox work for multiple documents.
 * @suppressGlobalPropertiesCheck
 */
WebInspector.SuggestBox.Overlay = function()
{
    this.element = createElementWithClass("div", "suggest-box-overlay");
    var root = WebInspector.createShadowRootWithCoreStyles(this.element, "ui/suggestBox.css");
    this._leftSpacerElement = root.createChild("div", "suggest-box-left-spacer");
    this._horizontalElement = root.createChild("div", "suggest-box-horizontal");
    this._topSpacerElement = this._horizontalElement.createChild("div", "suggest-box-top-spacer");
    this._bottomSpacerElement = this._horizontalElement.createChild("div", "suggest-box-bottom-spacer");
    this._resize();
    document.body.appendChild(this.element);
}

WebInspector.SuggestBox.Overlay.prototype = {
    /**
     * @param {number} offset
     */
    setLeftOffset: function(offset)
    {
        this._leftSpacerElement.style.flexBasis = offset + "px";
    },

    /**
     * @param {number} offset
     * @param {boolean} isTopOffset
     */
    setVerticalOffset: function(offset, isTopOffset)
    {
        this.element.classList.toggle("under-anchor", isTopOffset);

        if (isTopOffset) {
            this._bottomSpacerElement.style.flexBasis = "auto";
            this._topSpacerElement.style.flexBasis = offset + "px";
        } else {
            this._bottomSpacerElement.style.flexBasis = offset + "px";
            this._topSpacerElement.style.flexBasis = "auto";
        }
    },

    /**
     * @param {!Element} element
     */
    setContentElement: function(element)
    {
        this._horizontalElement.insertBefore(element, this._bottomSpacerElement);
    },

    _resize: function()
    {
        var container = WebInspector.Dialog.modalHostView().element;
        var containerBox = container.boxInWindow(container.ownerDocument.defaultView);

        this.element.style.left = containerBox.x + "px";
        this.element.style.top = containerBox.y + "px";
        this.element.style.height = containerBox.height + "px";
        this.element.style.width = containerBox.width + "px";
    },

    dispose: function()
    {
        this.element.remove();
    }
}


/*!
 * Module dependencies
 */
var util = require('util')
  , Resource = require('./')
  , updateMixin = require('../mixins/update')
  ;

/*!
 * Expose `Reply()`.
 */
exports = module.exports = Reply;

/**
 * Initialize a new `Reply` with the given `parent` and `definition`.
 *
 * @param {Function} parent The parent this resource is attached to.
 * @param {Object} definition The resource definition.
 * @api private
 */
function Reply(parent, definition) {
  var key;
  for (key in updateMixin) {
    this[key] = updateMixin[key];
  }
  Reply.super_.apply(this, arguments);
}
util.inherits(Reply, Resource);

Reply.create = require('../mixins/create');


//{@wmurldecode
(function(global) {

// --- dependency modules ----------------------------------
// --- define / local variables ----------------------------
//var _runOnNode = "process" in global;
//var _runOnWorker = "WorkerLocation" in global;
//var _runOnBrowser = "document" in global;

// --- class / interfaces ----------------------------------
// --- implements ------------------------------------------
function WMURL_decode(source) { // @arg String - percent encoded string.
                                // @ret String - decode string.
                                // @throws Error("invalid WMURL.decode")
                                // @desc decodeURIComponent impl.
//{@dev
    $valid($type(source, "String"), WMURL_decode, "source");
//}@dev

    return source.replace(/(%[\da-f][\da-f])+/g, function(match) {
        var rv = [];
        var ary = match.split("%").slice(1), i = 0, iz = ary.length;
        var a = 0, b = 0, c = 0; // UTF-8 bytes

        for (; i < iz; ++i) {
            a = parseInt(ary[i], 16);

            if (a !== a) { // isNaN(a)
                throw new Error("invalid WMURL.decode");
            }

            // decode UTF-8
            if (a < 0x80) { // ASCII(0x00 ~ 0x7f)
                rv.push(a);
            } else if (a < 0xE0) {
                b = parseInt(ary[++i], 16);
                rv.push((a & 0x1f) <<  6 | (b & 0x3f));
            } else if (a < 0xF0) {
                b = parseInt(ary[++i], 16);
                c = parseInt(ary[++i], 16);
                rv.push((a & 0x0f) << 12 | (b & 0x3f) << 6
                                         | (c & 0x3f));
            }
        }
        return String.fromCharCode.apply(null, rv);
    });
}

// --- validate / assertions -------------------------------
//{@dev
function $valid(val, fn, hint) { if (global["Valid"]) { global["Valid"](val, fn, hint); } }
function $type(obj, type) { return global["Valid"] ? global["Valid"].type(obj, type) : true; }
//function $keys(obj, str) { return global["Valid"] ? global["Valid"].keys(obj, str) : true; }
//function $some(val, str, ignore) { return global["Valid"] ? global["Valid"].some(val, str, ignore) : true; }
//function $args(fn, args) { if (global["Valid"]) { global["Valid"].args(fn, args); } }
//}@dev

// --- exports ---------------------------------------------
global["WMURL_" in global ? "WMURL_" : "WMURL"]["decode"] =
    global["decodeURIComponent"] || WMURL_decode; // WMURL.decode(source:String):String

})((this || 0).self || global); // WebModule idiom. http://git.io/WebModule
//}@wmurldecode


var fs = require('fs');
var url = require('url');
var path = require('path');
var mime = require('mime');
var gitteh = require('gitteh');

var GIT_DIR = '.git';

function setup(repoPath, options) {
  options = options || {};
  options.__proto__ = setup.DEFAULTS;

  // 'setup' can be synchronous...
  var isClone;
  try {
    isClone = fs.statSync(path.join(repoPath, GIT_DIR)).isDirectory();
  } catch(e) {
    if (e.code === 'ENOENT') {
      isClone = false;
    } else {
      throw e;
    }
  }
  if (isClone && repoPath.substring(repoPath.length-4) !== GIT_DIR) repoPath = path.join(repoPath, GIT_DIR);
  //console.log("Using repository:", repoPath, "isClone:", isClone);
  // Synchronously open the git repo
  var repo = gitteh.openRepository(repoPath);

  return function gitProvider(req, res, next) {
    // Ensure that 'next()' exists (in the case of a plain http.Server)
    next = next || function(err) {
      if (err) console.error(err);
      res.writeHead(err ? 500 : 404);
      res.end(err ? err.stack : "Not Found");
    };

    if (!req.hasOwnProperty("uri")) { req.uri = url.parse(req.url); }
    if (req.uri.pathname.substring(0, options.mountPoint.length) !== options.mountPoint) return next();

    var gitPath = req.uri.pathname.substring(options.mountPoint.length);

    var parts = gitPath.split('/');
    if (parts[parts.length-1].length === 0) {
      parts[parts.length-1] = options.indexFile;
    }

    // The first part may be a commit reference, a git tag/branch name,
    // or just simply the name of the file to serve from a top-level.
    var firstPart = parts[0];
    //console.log("First Part: " + firstPart);

    if (firstPart.length === 40) {
      // Check the commit id, then git tag, then serve file
      checkCommit(firstPart);
    } else {
      // Check for a git tag, then serve file
      checkTag(firstPart);
    }

    function checkCommit(commitRef) {
      repo.getCommit(commitRef, function(err, commit) {
        if (err) {
          if (err.gitError === gitteh.error.GIT_ENOTFOUND) {
            // first part isn't a git commit, next check for a git tag
            return checkTag(commitRef);
          } else {
            // Some other kind of error, baaddd!
            return next(err);
          }
        }
        // If there was no error then we got a valid commit id. Serve the file from parts[1]
        repo.getTree(commit.tree, function(err, tree) {
          serveGitFile(repo, tree, parts.slice(1), res, next);
        });
      });
    }
    function checkTag(tag) {
      listTags(repo, function(err, tags) {
        if (err) return next(err);

        //console.log("Tags: " + tags);

        var tagIndex = tags.indexOf(firstPart);
        if (tagIndex >= 0) {
          //console.log("Found tag: " + tags[tagIndex]);

          // Requested a file from a git tag. Resolve the git tag then serve the file from parts[1]
          resolveTag(repo, tag, function(err, commitRef) {
            if (err) return next(err);
            serveCommit(commitRef, parts.slice(1));
          });
        } else {
          //console.log("Tag not found");
          // No git tag, all the parts are the path of the file to server.
          // Serve from HEAD (bare repo) or a real file (cloned repo).
          if (isClone) {
            fs.readFile(path.join(repoPath, '..', parts.join('/')), function(err, buf) {
              if (err) {
                if (err.code === 'ENOENT') return next();
                else return next(err);
              }
              serveBuffer(buf, res, gitPath);
            });
          } else {
            // Is a bare repo, serve file from resolved HEAD
            resolveHead(repo, function(err, commitRef) {
              if (err) return next(err);
              serveCommit(commitRef, parts);
            });
          }
        }
      });
    }

    function serveCommit(commitRef, parts) {
      repo.getCommit(commitRef, function(err, commit) {
        if (err) return next(err);
        repo.getTree(commit.tree, function(err, tree) {
          if (err) return next(err);
          serveGitFile(repo, tree, parts, res, next);
        });
      });
    }
  }
}
module.exports = setup;

setup.DEFAULTS = {
  // The base request URL to serve from
  mountPoint: '/',
  // The name of the index file to serve when
  // a directory name is requested
  indexFile: 'index.html'
};

// Returns a Buffer of the specified file from the specified repo and commit reference.
// Recursively gets the tree instances until the last part, which is gets the rawObject
// for and serves back over HTTP.
function serveGitFile(repo, tree, parts, res, next) {
  //console.log("Serving git file: " + parts);
  var thisPart = parts.shift();
  var isLastPart = parts.length === 0;
  var entryIndex = -1;
  for (var i=0; i < tree.entries.length; i++) {
    if (tree.entries[i].name === thisPart) {
      entryIndex = i;
      break;
    }
  }
  if (entryIndex < 0) return next();
  var entry = tree.entries[entryIndex];
  if (isLastPart) {
    repo.getBlob(entry.id, function(err, buf) {
      if (err) return next(err);
      if (!buf.data) return next();
      serveBuffer(buf.data, res, thisPart);
    });
  } else {
    repo.getTree(entry.id, function(err, entryTree) {
      if (err) return next(err);
      serveGitFile(repo, entryTree, parts, res, next);
    });
  }
}

function serveBuffer(buf, res, filename) {
  var contentType = mime.lookup(filename);
  res.setHeader('Content-Length', buf.length);
  res.setHeader('Content-Type', contentType);
  res.end(buf);
}

function resolveReference(repo, reference, callback) {
  repo.getReference(reference, onRef);
  function onRef(err, ref) {
    if (err) return callback(err);
    if (ref.type === gitteh.GIT_REF_OID) {
      callback(null, ref.target);
    } else if (ref.type === gitteh.GIT_REF_SYMBOLIC) {
      ref.resolve(onRef);
    } else {
      callback(new Error('Got unknown reference type: ' + ref.type));
    }
  }
}

function resolveHead(repo, callback) {
  resolveReference(repo, 'HEAD', callback);
}

var TAG_REF = 'refs/tags/';
function resolveTag(repo, tag, callback) {
  resolveReference(repo, TAG_REF+tag, callback);
}

function listTags(repo, callback) {
  repo.listReferences(gitteh.GIT_REF_LISTALL, onList);
  function onList(err, refs) {
    if (err) return callback(err);
    refs = refs.filter(function(ref) { return /^refs\/tags\//.test(ref); });
    refs = refs.map(function(ref) { return ref.substring(TAG_REF.length); });
    callback(null, refs);
  }
}


#!/usr/bin/env node

/**
 * 关于代码规范
 * 所有模块、类库 引入后使用首字母大写的驼峰形式，
 * 本地变量与函数使用首字母小写的驼峰形式
 * module内的闭包变量（相当于module 作用域内的全局变量）如果用于储存信息，由于容易产生泄露，所以必须使用_开头的驼峰形式用于区分
 * 像下面代码中的packageInfo由于是只读的所以不需要加_
 */
"use strict";
var Program = require('commander');
var DebugServer = require('../lib/DebugServer');
var Config = require('../lib/components/Config');
var Builder = require('../lib/components/Builder');
var LogStyle = require('../common/LogStyle');
var Url = require('url');
var Fs = require('fs');
var Exit = require('exit');
var Path = require('path');
var IP = require('ip');
var LaunchDevTool = require('../lib/util/LaunchDevTool');
var Del = require('del');
var Watch = require('node-watch');
var MessageBus = require('../lib/components/MessageBus');
var Hosts = require('../lib/util/Hosts');
var UpgradeNotice = require('../lib/util/UpgradeNotice');
var packageInfo = require('../package.json');

Program
    .option('-h, --host [host]', 'set the host ip of debugger server')
    .option('-H, --help', 'display help')
    .option('-V, --verbose', 'display logs of debugger server')
    .option('-v, --version', 'display version')
    .option('-p, --port [port]', 'set debugger server port', '8088')
    .option('-e, --entry [entry]', 'set the entry bundlejs path when you specific the bundle server root path')
    .option('-w, --watch', 'watch we file changes auto build them and refresh debugger page![default enabled]', true)
    .option('-m, --mode [mode]', 'set build mode [transformer|loader]', 'loader')
    .option('-M, --manual', 'manual mode,this mode will not auto open chrome')
    .option('--min', '')
    .option('-l, --local', '');
//支持命令后跟一个file/directory参数
Program['arguments']('[file]')
    .action(function (file) {
        Program.file = file;
    });
Program.parse(process.argv);
//默认watch功能开启
Program.watch = true;
//fix tj's commander bug overwrite --help
if (Program.help == undefined) {
    Program.outputHelp();
    Exit(0);
}
//fix tj's commander bug overwrite --version
if (Program.version == undefined) {
    console.log(packageInfo.version);
    Exit(0);
}
var supportMode = ['loader', 'transformer'];

if (Program.host && !Hosts.isValidLocalHost(Program.host)) {
    console.log('[' + Program.host + '] is not your local address!');
    Exit(0);
}

Config.verbose = Program.verbose;
Config.port = Program.port;
Config.local = Program.local;
Config.min = Program.min;
if (supportMode.indexOf(Program.mode) == -1) {
    console.log('unsupported build mode:', Program.mode);
    Exit(0);
}
else {
    Config.buildMode = Program.mode;
}
//清空
try {
    Del.sync(Path.join(__dirname, '../frontend/', Config.bundleDir, '/*'), {force: true});
} catch (e) {
}

if (Program.file) {
    buildAndStart()
}
else {
    startServerAndLaunchDevtool()
}
UpgradeNotice.run();

////////////////////////////////////////////////////////////////////////////

function buildAndStart() {
    if (/^https?:\/\//.test(Program.file)) {
        var url = Program.file.replace(/^(https?:\/\/)([^/:]+)(?=:\d+|\/)/, function (m, a, b) {
            if (!/\d+\.\d+\.\d+\.\d+/.test(a)) {
                return a + Hosts.findRealHost(b);
            }
            else {
                return m;
            }
        });
        Config.entryBundleUrl = url;
        startServerAndLaunchDevtool();
    }
    else {
        var filePath = Path.resolve(Program.file);
        var ext = Path.extname(filePath);
        if (!Fs.existsSync(filePath)) {
            console.error(filePath + ': No such file or directory');
            return Exit(0);
        }
        if (ext == '.ju' || ext == '.vue') {
            console.log('building...');
            console.time('Build completed!');
            buildFileAndWatchIt(Program.mode, filePath).then(function () {
                console.timeEnd('Build completed!');
                startServerAndLaunchDevtool(Program.file);
            }, function (err) {
                if (err) {
                    console.log(err, err.stack);
                }
                Exit(0);
            })

        }
        else if (ext == '.js') {
            buildFileAndWatchIt('copy', filePath).then(function () {
                startServerAndLaunchDevtool(Program.file);
            })
        }
        else if (!ext) {
            //处理目录
            if (Fs.statSync(filePath).isDirectory()) {
                Config.root = filePath;
                startServerAndLaunchDevtool(Program.entry)
            }
            else {
                console.error(Program.file + ' is not a directory!');
                Exit(0);
            }
        }
        else {
            console.error('Error:unsupported file type: ', ext);
            return Exit(0);
        }
    }
}
function buildFileAndWatchIt(buildMode, filePath) {
    if (Program.watch) {
        Watch(filePath, function () {
            console.time('Rebuild completed! ')
            console.log(filePath + ' updated! rebuilding...')
            Builder[buildMode](filePath).then(function () {
                console.timeEnd('Rebuild completed! ')
                MessageBus.emit('page.refresh');
            });
        })
    }
    return Builder[buildMode](filePath);
}
function startServerAndLaunchDevtool(entry) {
    var port = Program.port;
    var ip = Program.host || IP.address();
    Config.ip = ip;
    console.info('start debugger server at ' + LogStyle.dressUp('http://' + ip + ':' + port, LogStyle.FG_YELLOW, LogStyle.BRIGHT));
    if (entry) {
        Config.entryBundleUrl = 'http://' + ip + ':' + port + Path.join('/' + Config.bundleDir, Path.basename(entry).replace(/\.(ju|vue)$/, '.js')).replace(/\\/g, '/');
        console.log('\nYou can visit we file(s) use ' + Config.entryBundleUrl);
        console.log('Also you can use Playground App to scan the qrcode on device list page.');
    }
    if (Config.entryBundleUrl) {
        Config.entryBundleUrl = Config.entryBundleUrl.replace(/127\.0\.0\.1/g, Config.ip);
        //fixme ugly 与具体耦合的逻辑 易变！
        var urlObj = Url.parse(Config.entryBundleUrl, true);
        if (!/wh_jud=true/.test(Config.entryBundleUrl) && !urlObj.query['_jud_tpl']) {
            urlObj.query['_jud_tpl'] = Config.entryBundleUrl;
            urlObj.search = '';
            Config.entryBundleUrl = Url.format(urlObj);
        }
    }
    if (Config.root) {
        console.log('\nDirectory[' + Program.file + '] has been mapped to http://' + ip + ':' + port + '/' + Config.bundleDir + '/');
    }

    console.info('\nThe websocket address for native is ' + LogStyle.dressUp('ws://' + ip + ':' + port + '/debugProxy/native', LogStyle.FG_YELLOW, LogStyle.BRIGHT));
    DebugServer.start(port);
    if (!Program.manual) {
        LaunchDevTool(ip, port);
    }
}


'use strict'

/* npm modules */
var crypto = require('crypto')
var microTimestamp = require('micro-timestamp')

/* public functions */
module.exports = randomUniqueId

/* generate process-global seed for unique id */

// sync get 32 bytes of random data - this runs once at startup
randomUniqueId.seed = crypto.randomBytes(32).toString('hex')
// generate process id from random data and micro-second timestamp
randomUniqueId.processId = crypto.createHash('sha256')
    // add random data to hash
    .update(randomUniqueId.seed)
    // add microsec timestamp to hash
    .update(microTimestamp())
    // get hash as hex
    .digest('hex')

// count of generated ids
randomUniqueId.count = 0

/**
 * @function randomUniqueId
 *
 * generate 128bit hex unique id using true random seed, micro-second timestamp
 * and counter for entropy.
 *
 * @returns {object}
 */
function randomUniqueId () {
    // increment count of generated ids
    randomUniqueId.count++
    // get timestamp
    var timestamp = microTimestamp()
    // create unique id
    randomUniqueId.id = crypto.createHash('sha256')
        // process id, microsec timestamp, and counter
        .update(randomUniqueId.processId + randomUniqueId.id + timestamp + randomUniqueId.count)
        // get hash as hex
        .digest('hex')
        // only get 128 bits
        .substring(0, 32)
    // return object with id and timestamp
    return {
        id: randomUniqueId.id,
        timestamp,
    }
}