<commit_before>/*
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {DEFAULT_THEMES, THEME_PROPERTIES, setTheme} from './themes.js';

const WARN_LINES = 15;
const WARN_LINE_LENGTH = 80;

const $editor = $('#editor');
const $output = $('#output');

let config = {
  code: localStorage.highlighterCode || '',
  theme: localStorage.highlighterTheme || 'light',
  lang: localStorage.highlighterLang || '(auto)',
  font: localStorage.highlighterFont || 'Roboto Mono',
  tabSize: Number(localStorage.highlighterTabSize || '4'),
  typeSize: Number(localStorage.highlighterTypeSize || '40'),
  selectionTreatment: localStorage.highlighterSelectionTreatment || '--',
  customTheme: JSON.parse(localStorage.customTheme || JSON.stringify(DEFAULT_THEMES['light'])),
};

if (config.lang == '--') {
  config.lang = '(auto)';
}

let editor;

setupToolbar();
setupEditor();
setupOutputArea();
updateOutputArea();
setupCustomThemeEditor();
loadFont();
installServiceWorker();

function setupEditor() {
  editor = ace.edit($editor.get(0));
  editor.$blockScrolling = Infinity;
  editor.setValue(config.code, -1);
  editor.setTheme('ace/theme/chrome');
  editor.getSession().setMode('ace/mode/text');
  editor.setOptions({
    fontFamily: 'Roboto Mono',
    fontSize: '11pt',
  });
  editor.on('change', () => {
    localStorage.highlighterCode = config.code = editor.getValue();
    updateOutputArea();
  });
  editor.getSelection().on('changeCursor', () => updateOutputArea());
  editor.getSelection().on('changeSelection', () => updateOutputArea());
  updateEditorParams();
}

function updateEditorParams() {
  editor.setOptions({
    fontFamily: config.font,
    fontSize: '11pt',
  });
  editor.getSession().setTabSize(config.tabSize);
}

function setupOutputArea() {
  // select all on click
  $output.click(() => {
    var selection = window.getSelection();
    var range = document.createRange();
    range.selectNodeContents($output.find('pre').get(0));
    selection.removeAllRanges();
    selection.addRange(range);
  });

  // re-layout on window resize
  $(window).on('resize', () => updateOutputArea());
}

function setupToolbar() {
  $('#theme')
      .val(config.theme)
      .on('input', ev => {
        localStorage.highlighterTheme = config.theme = $(ev.target).val();
        updateOutputArea();
      });

  $('#lang')
      .val(config.lang)
      .on('input', ev => {
        localStorage.highlighterLang = config.lang = $(ev.target).val();
        updateOutputArea();
      });

  let $dl = $('#lang-datalist');
  let langs = Object.keys(Prism.languages)
      .filter(s => typeof Prism.languages[s] == 'object');
  for (let lang of langs) {
    $dl.append($('<option>').attr('value', lang));
  }

  $('#tab-size')
      .val(config.tabSize)
      .on('input', ev => {
        localStorage.highlighterTabSize = $(ev.target).val();
        config.tabSize = Number(localStorage.highlighterTabSize);
        updateEditorParams();
        updateOutputArea();
      });

  $('#font')
      .val(config.font)
      .on('input', ev => {
        localStorage.highlighterFont = config.font = $(ev.target).val();
        loadFont();
      });

  $('#selection-treatment')
      .val(config.selectionTreatment)
      .on('input', ev => {
        localStorage.highlighterSelectionTreatment = config.selectionTreatment = $(ev.target).val();
        updateOutputArea();
      });

  let $typeSize = $('#type-size');

  let setTypeSize_ = size => {
    if ($typeSize.val() != String(size)) {
      $typeSize.val(size);
    }
    config.typeSize = size;
    localStorage.highlighterTypeSize = String(config.typeSize);
    updateOutputArea();
  };

  $typeSize
      .val(config.typeSize)
      .on('input', () => {
        let val = parseInt($typeSize.val(), 10);
        if (!isNaN(val) && val > 8) {
          setTypeSize_(val);
        }
      })
      .on('keydown', ev => {
        if (!ev.shiftKey) {
          if (ev.keyCode == 38 || ev.keyCode == 40) {
            setTypeSize_(parseInt($typeSize.val(), 10) + (ev.keyCode == 38 ? 1 : -1));
            ev.preventDefault();
          }
        }
      })
      .on('blur', ev => setTypeSize_(config.typeSize));
}

function loadFont() {
  if (config.font == 'JetBrains Mono') {
    updateEditorParams();
    updateOutputArea();
    return;
  }

  WebFont.load({
    google: {
      families: [`${config.font}:400,700`]
    },
    active: () => {
      updateEditorParams();
      updateOutputArea();
    }
  });
}

function updateOutputArea() {
  let $messages = $('.edit-area .messages');
  $messages.empty();

  $output.empty();

  // set theme
  if (config.theme == 'custom') {
    $('.custom-theme-area').show();
    setTheme(config.customTheme, config.typeSize);
  } else {
    $('.custom-theme-area').hide();
    setTheme(DEFAULT_THEMES[config.theme], config.typeSize);
  }

  // build pre element
  let $pre = $('<pre>')
      .addClass('prettyprint')
      .css({
        'font-family': config.font,
        'font-size': `${config.typeSize}px`,
        'background': 'transparent',
      })
      // .text(cleanupCode(config.code).code)
      .appendTo($output);
  let lang = config.lang;
  if (lang == '(auto)') {
    lang = /\s*</.test(config.code) ? 'markup' : 'js';
  }

  if (!Prism.languages[lang]) {
    $('#lang').addClass('is-invalid');
    return;
  }

  $('#lang').removeClass('is-invalid');

  let html = Prism.highlight(cleanupCode(config.code).code,
      Prism.languages[lang], lang);
  $pre.html(html);
  highlightSelection();

  // find width by measuring the longest line
  let preWidth = Math.max(1, measureNaturalPreWidth($pre));
  let preHeight = Math.max(1, $pre.outerHeight());

  // center and scale the pre in the output area
  let scale = Math.min(1, Math.min(
      $output.width() / preWidth,
      $output.height() / preHeight));
  $pre.css({
    width: preWidth,
    transform: `translate(-50%, -50%) scale(${scale})`
  });

  // show messages
  let messages = [];

  if ((config.code.match(/\n/g) || []).length >= WARN_LINES) {
    messages.push({
      type: 'warning',
      message:
        `More than ${WARN_LINES} lines of code will be hard to read in a
        slide presentation.`
    });
  }

  let lines = config.code.split('\n') || [];

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].length > WARN_LINE_LENGTH) {
      messages.push({
        type: 'warning',
        message:
          `Line ${(i + 1)} has more than ${WARN_LINE_LENGTH} characters!`
      });
      break;
    }
  }

  messages.forEach(({type, message}) =>
      $('<div>')
          .addClass(`message message-${type}`)
          .text(message)
          .appendTo($messages));
}

const htmlEscape = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');

function highlightSelection() {
  $output.removeClass('has-highlights');
  $output.removeAttr('data-seltreat');

  if (config.selectionTreatment == '--') {
    return;
  }

  $output.attr('data-seltreat', config.selectionTreatment);

  let rawCode = config.code;
  let {code, commonIndent, leadingEmptyLines} = cleanupCode(rawCode);
  let preRoot = $output.find('pre').get(0);

  let rangeToCharPos = ({row, column}) => code.split(/\n/)
      .slice(0, row - leadingEmptyLines)
      .reduce((a, r) => a + r.length + 1, 0)
      + Math.max(0,
          ((rawCode.split(/\n/)[row] || '').substring(0, column).match(/\t/g) || []).length
            * (config.tabSize - 1)
          + column - commonIndent);

  let hasHighlights = false;
  for (let range of editor.getSelection().getAllRanges()) {
    let targetStartPos = rangeToCharPos(range.start);
    let targetEndPos = rangeToCharPos(range.end);

    if (targetEndPos == targetStartPos) {
      continue;
    }
    hasHighlights = true;

    let childStartPos = 0;
    for (let child of Array.from(preRoot.childNodes)) {
      let childContent = child.textContent;
      let childEndPos = childStartPos + childContent.length;

      if (targetStartPos < childEndPos && targetEndPos >= childStartPos) {
        // some overlap
        let startInChild = Math.max(0, targetStartPos - childStartPos);
        let endInChild = Math.min(childContent.length, childContent.length - (childEndPos - targetEndPos));

        let makeSub = (tag, start, end) => {
          if (start == end) {
            return null;
          }

          let f = document.createElement(tag);
          if (child.className) {
            f.className = child.className;
          }
          f.innerHTML = htmlEscape(childContent.substring(start, end));
          return f;
        };

        child.replaceWith.apply(child, [
          makeSub('span', 0, startInChild),
          makeSub('mark', startInChild, endInChild),
          makeSub('span', endInChild, childContent.length),
        ].filter(s => !!s));
      }

      childStartPos = childEndPos;
    }
  }

  $output.toggleClass('has-highlights', hasHighlights);
}

function cleanupCode(code) {
  let lines = code.split('\n');

  // Remove leading and trailing empty lines
  let leadingEmptyLines = 0;
  for (let line of lines) {
    if (line.match(/^\s*$/)) {
      ++leadingEmptyLines;
    } else {
      break;
    }
  }

  let trailingEmptyLines = 0;
  for (let line of [...lines].reverse()) {
    if (line.match(/^\s*$/)) {
      ++trailingEmptyLines;
    } else {
      break;
    }
  }

  if (leadingEmptyLines == lines.length) {
    trailingEmptyLines = 0;
  }

  lines = lines.slice(leadingEmptyLines, lines.length - trailingEmptyLines);

  // Tabs to 4 spaces
  lines = lines.map(line => line.replace(/\t/g, ' '.repeat(config.tabSize)));

  // Remove trailing whitespace
  lines = lines.map(line => line.replace(/ +$/g, ''));

  // Remove common indent
  let commonIndent = -1;
  for (let line of lines) {
    if (!$.trim(line)) {
      continue;
    }

    let indent = line.match(/^\s*/)[0].length;
    if (indent < commonIndent || commonIndent == -1) {
      commonIndent = indent;
    }
  }

  if (commonIndent > 0) {
    lines = lines.map(line => line.substring(commonIndent));
  }

  code = lines.join('\n');
  return {code, commonIndent, leadingEmptyLines, trailingEmptyLines};
}

function measureNaturalPreWidth(pre) {
  // compute the natural width of a monospace <pre> by computing
  // the length of its longest line
  let $pre = $(pre);
  let longestLine = $pre.text()
      .split('\n')
      .reduce((longest, line) => (longest.length > line.length) ? longest : line, '');

  let $preClone = $pre
      .clone()
      .css({
        position: 'fixed',
        left: -10000,
        top: 0,
        display: 'inline-block',
        width: 'auto',
        height: 'auto',
      })
      .text(longestLine)
      .appendTo(document.body);

  let naturalWidth = $preClone.width();
  $preClone.remove();
  return naturalWidth;
}

function setupCustomThemeEditor() {
  let rebuildCustomThemeProperties = () => {
    let $customThemeEditor = $('.custom-theme-editor').empty();
    for (let prop of THEME_PROPERTIES) {
      let $prop = $('<div>')
          .addClass('custom-theme-prop')
          .appendTo($customThemeEditor);
      let $label = $('<label>')
          .appendTo($prop);
      let $input = $('<input>')
          .attr('type', 'color')
          .val(config.customTheme[prop.id])
          .on('input', () => {
            config.customTheme[prop.id] = $input.val();
            localStorage.customTheme = JSON.stringify(config.customTheme);
            updateOutputArea();
          })
          .appendTo($label);
      $label.append(`<span>${prop.name}</span>`); // text
    }
  }

  rebuildCustomThemeProperties();

  $('.custom-theme-import-export').click(() => {
    let currentJSON = JSON.stringify(config.customTheme);
    let newJSON = window.prompt(
        'Copy the below JSON or paste new JSON for your custom theme.', currentJSON);
    if (newJSON && newJSON != currentJSON) {
      try {
        config.customTheme = Object.assign({}, DEFAULT_THEMES['light'], JSON.parse(newJSON) || {});
        localStorage.customTheme = JSON.stringify(config.customTheme);
        updateOutputArea();
        rebuildCustomThemeProperties();
      } catch (e) {
        alert('Error parsing the JSON: ' + e);
      }
    }
  });
}

function installServiceWorker() {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('sw.js').then(registration => {
        console.log('SW registered: ', registration);
      }).catch(registrationError => {
        console.log('SW registration failed: ', registrationError);
      });
    });
  }
}
<commit_msg>Improve highlighting nested spans... also hidden line numbers functionality<commit_after>/*
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {DEFAULT_THEMES, THEME_PROPERTIES, setTheme} from './themes.js';

const WARN_LINES = 15;
const WARN_LINE_LENGTH = 80;

const $editor = $('#editor');
const $output = $('#output');

let config = {
  code: localStorage.highlighterCode || '',
  theme: localStorage.highlighterTheme || 'light',
  lang: localStorage.highlighterLang || '(auto)',
  font: localStorage.highlighterFont || 'Roboto Mono',
  tabSize: Number(localStorage.highlighterTabSize || '4'),
  typeSize: Number(localStorage.highlighterTypeSize || '40'),
  selectionTreatment: localStorage.highlighterSelectionTreatment || '--',
  customTheme: JSON.parse(localStorage.customTheme || JSON.stringify(DEFAULT_THEMES['light'])),
};

if (config.lang == '--') {
  config.lang = '(auto)';
}

let editor;

setupToolbar();
setupEditor();
setupOutputArea();
updateOutputArea();
setupCustomThemeEditor();
loadFont();
installServiceWorker();

function setupEditor() {
  editor = ace.edit($editor.get(0));
  editor.$blockScrolling = Infinity;
  editor.setValue(config.code, -1);
  editor.setTheme('ace/theme/chrome');
  editor.getSession().setMode('ace/mode/text');
  editor.setOptions({
    fontFamily: 'Roboto Mono',
    fontSize: '11pt',
  });
  editor.on('change', () => {
    localStorage.highlighterCode = config.code = editor.getValue();
    updateOutputArea();
  });
  editor.getSelection().on('changeCursor', () => updateOutputArea());
  editor.getSelection().on('changeSelection', () => updateOutputArea());
  updateEditorParams();
}

function updateEditorParams() {
  editor.setOptions({
    fontFamily: config.font,
    fontSize: '11pt',
  });
  editor.getSession().setTabSize(config.tabSize);
}

function setupOutputArea() {
  // select all on click
  $output.click(() => {
    var selection = window.getSelection();
    var range = document.createRange();
    range.selectNodeContents($output.find('pre').get(0));
    selection.removeAllRanges();
    selection.addRange(range);
  });

  // re-layout on window resize
  $(window).on('resize', () => updateOutputArea());
}

function setupToolbar() {
  $('#theme')
      .val(config.theme)
      .on('input', ev => {
        localStorage.highlighterTheme = config.theme = $(ev.target).val();
        updateOutputArea();
      });

  $('#lang')
      .val(config.lang)
      .on('input', ev => {
        localStorage.highlighterLang = config.lang = $(ev.target).val();
        updateOutputArea();
      });

  let $dl = $('#lang-datalist');
  let langs = Object.keys(Prism.languages)
      .filter(s => typeof Prism.languages[s] == 'object');
  for (let lang of langs) {
    $dl.append($('<option>').attr('value', lang));
  }

  $('#tab-size')
      .val(config.tabSize)
      .on('input', ev => {
        localStorage.highlighterTabSize = $(ev.target).val();
        config.tabSize = Number(localStorage.highlighterTabSize);
        updateEditorParams();
        updateOutputArea();
      });

  $('#font')
      .val(config.font)
      .on('input', ev => {
        localStorage.highlighterFont = config.font = $(ev.target).val();
        loadFont();
      });

  $('#selection-treatment')
      .val(config.selectionTreatment)
      .on('input', ev => {
        localStorage.highlighterSelectionTreatment = config.selectionTreatment = $(ev.target).val();
        updateOutputArea();
      });

  let $typeSize = $('#type-size');

  let setTypeSize_ = size => {
    if ($typeSize.val() != String(size)) {
      $typeSize.val(size);
    }
    config.typeSize = size;
    localStorage.highlighterTypeSize = String(config.typeSize);
    updateOutputArea();
  };

  $typeSize
      .val(config.typeSize)
      .on('input', () => {
        let val = parseInt($typeSize.val(), 10);
        if (!isNaN(val) && val > 8) {
          setTypeSize_(val);
        }
      })
      .on('keydown', ev => {
        if (!ev.shiftKey) {
          if (ev.keyCode == 38 || ev.keyCode == 40) {
            setTypeSize_(parseInt($typeSize.val(), 10) + (ev.keyCode == 38 ? 1 : -1));
            ev.preventDefault();
          }
        }
      })
      .on('blur', ev => setTypeSize_(config.typeSize));
}

function loadFont() {
  if (config.font == 'JetBrains Mono') {
    updateEditorParams();
    updateOutputArea();
    return;
  }

  WebFont.load({
    google: {
      families: [`${config.font}:400,700`]
    },
    active: () => {
      updateEditorParams();
      updateOutputArea();
    }
  });
}

function updateOutputArea() {
  let $messages = $('.edit-area .messages');
  $messages.empty();

  $output.empty();

  // set theme
  if (config.theme == 'custom') {
    $('.custom-theme-area').show();
    setTheme(config.customTheme, config.typeSize);
  } else {
    $('.custom-theme-area').hide();
    setTheme(DEFAULT_THEMES[config.theme], config.typeSize);
  }

  // build pre element
  let $pre = $('<pre>')
      .addClass('prettyprint')
      .css({
        'font-family': config.font,
        'font-size': `${config.typeSize}px`,
        'background': 'transparent',
      })
      // .text(cleanupCode(config.code).code)
      .appendTo($output);
  let lang = config.lang;
  if (lang == '(auto)') {
    lang = /\s*</.test(config.code) ? 'markup' : 'js';
  }

  if (!Prism.languages[lang]) {
    $('#lang').addClass('is-invalid');
    return;
  }

  $('#lang').removeClass('is-invalid');

  let html = Prism.highlight(
      cleanupCode(config.code).code,
      Prism.languages[lang], lang);
  $pre.html(html);
  highlightSelection();

  // add line numbers
  if (false) {
    addLineNumbers();
  }

  // find width by measuring the longest line
  let preWidth = Math.max(1, measureNaturalPreWidth($pre));
  let preHeight = Math.max(1, $pre.outerHeight());

  // center and scale the pre in the output area
  let scale = Math.min(1, Math.min(
      $output.width() / preWidth,
      $output.height() / preHeight));
  $pre.css({
    width: preWidth,
    transform: `translate(-50%, -50%) scale(${scale})`
  });

  // show messages
  let messages = [];

  if ((config.code.match(/\n/g) || []).length >= WARN_LINES) {
    messages.push({
      type: 'warning',
      message:
        `More than ${WARN_LINES} lines of code will be hard to read in a
        slide presentation.`
    });
  }

  let lines = config.code.split('\n') || [];

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].length > WARN_LINE_LENGTH) {
      messages.push({
        type: 'warning',
        message:
          `Line ${(i + 1)} has more than ${WARN_LINE_LENGTH} characters!`
      });
      break;
    }
  }

  messages.forEach(({type, message}) =>
      $('<div>')
          .addClass(`message message-${type}`)
          .text(message)
          .appendTo($messages));
}

const htmlEscape = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');

function highlightSelection() {
  $output.removeClass('has-highlights');
  $output.removeAttr('data-seltreat');

  if (config.selectionTreatment == '--') {
    return;
  }

  $output.attr('data-seltreat', config.selectionTreatment);

  let rawCode = config.code;
  let {code, commonIndent, leadingEmptyLines} = cleanupCode(rawCode);
  let preRoot = $output.find('pre').get(0);

  let rangeToCharPos = ({row, column}) => code.split(/\n/)
      .slice(0, row - leadingEmptyLines)
      .reduce((a, r) => a + r.length + 1, 0)
      + Math.max(0,
          ((rawCode.split(/\n/)[row] || '').substring(0, column).match(/\t/g) || []).length
            * (config.tabSize - 1)
          + column - commonIndent);

  let hasHighlights = false;
  for (let range of editor.getSelection().getAllRanges()) {
    let targetStartPos = rangeToCharPos(range.start);
    let targetEndPos = rangeToCharPos(range.end);

    if (targetEndPos == targetStartPos) {
      continue;
    }
    hasHighlights = true;

    let childStartPos = 0;

    let traverse_ = (parent, emptyClass = '') => {
      for (let child of Array.from(parent.childNodes)) {
        if (child.childNodes.length >= 2 ||
          (child.childNodes.length >= 1
            && child.childNodes[0].nodeType != 3 /* TEXT */)) {
          // this is a complex element, traverse it instead of treating it
          // as a leaf nodeS
          traverse_(child, child.className);
          continue;
        }

        let childContent = child.textContent;
        let childEndPos = childStartPos + childContent.length;

        if (targetStartPos < childEndPos && targetEndPos >= childStartPos) {
          // some overlap
          let startInChild = Math.max(0, targetStartPos - childStartPos);
          let endInChild = Math.min(childContent.length, childContent.length - (childEndPos - targetEndPos));

          let makeSub = (tag, start, end) => {
            if (start == end) {
              return null;
            }

            let f = document.createElement(tag);
            if (child.className) {
              f.className = child.className;
            } else if (emptyClass) {
              f.className = emptyClass;
            }
            f.innerHTML = htmlEscape(childContent.substring(start, end));
            return f;
          };

          child.replaceWith.apply(child, [
            makeSub('span', 0, startInChild),
            makeSub('mark', startInChild, endInChild),
            makeSub('span', endInChild, childContent.length),
          ].filter(s => !!s));
        }

        childStartPos = childEndPos;
      }
    };

    traverse_(preRoot);
  }

  $output.toggleClass('has-highlights', hasHighlights);
}

function addLineNumbers() {
  let $pre = $output.find('pre');
  let htmlLines = $pre.html().split(/\n/);
  $pre.html(htmlLines
      .map((s, ind) => `<span style="color:grey">` +
        String(ind + 1).padStart(Math.ceil((htmlLines.length + 1) / 10), ' ') +
        `</span>  ${s}`)
      .join('\n'));
}

function cleanupCode(code) {
  let lines = code.split('\n');

  // Remove leading and trailing empty lines
  let leadingEmptyLines = 0;
  for (let line of lines) {
    if (line.match(/^\s*$/)) {
      ++leadingEmptyLines;
    } else {
      break;
    }
  }

  let trailingEmptyLines = 0;
  for (let line of [...lines].reverse()) {
    if (line.match(/^\s*$/)) {
      ++trailingEmptyLines;
    } else {
      break;
    }
  }

  if (leadingEmptyLines == lines.length) {
    trailingEmptyLines = 0;
  }

  lines = lines.slice(leadingEmptyLines, lines.length - trailingEmptyLines);

  // Tabs to 4 spaces
  lines = lines.map(line => line.replace(/\t/g, ' '.repeat(config.tabSize)));

  // Remove trailing whitespace
  lines = lines.map(line => line.replace(/ +$/g, ''));

  // Remove common indent
  let commonIndent = -1;
  for (let line of lines) {
    if (!$.trim(line)) {
      continue;
    }

    let indent = line.match(/^\s*/)[0].length;
    if (indent < commonIndent || commonIndent == -1) {
      commonIndent = indent;
    }
  }

  if (commonIndent > 0) {
    lines = lines.map(line => line.substring(commonIndent));
  }

  code = lines.join('\n');
  return {code, commonIndent, leadingEmptyLines, trailingEmptyLines};
}

function measureNaturalPreWidth(pre) {
  // compute the natural width of a monospace <pre> by computing
  // the length of its longest line
  let $pre = $(pre);
  let longestLine = $pre.text()
      .split('\n')
      .reduce((longest, line) => (longest.length > line.length) ? longest : line, '');

  let $preClone = $pre
      .clone()
      .css({
        position: 'fixed',
        left: -10000,
        top: 0,
        display: 'inline-block',
        width: 'auto',
        height: 'auto',
      })
      .text(longestLine)
      .appendTo(document.body);

  let naturalWidth = $preClone.width();
  $preClone.remove();
  return naturalWidth;
}

function setupCustomThemeEditor() {
  let rebuildCustomThemeProperties = () => {
    let $customThemeEditor = $('.custom-theme-editor').empty();
    for (let prop of THEME_PROPERTIES) {
      let $prop = $('<div>')
          .addClass('custom-theme-prop')
          .appendTo($customThemeEditor);
      let $label = $('<label>')
          .appendTo($prop);
      let $input = $('<input>')
          .attr('type', 'color')
          .val(config.customTheme[prop.id])
          .on('input', () => {
            config.customTheme[prop.id] = $input.val();
            localStorage.customTheme = JSON.stringify(config.customTheme);
            updateOutputArea();
          })
          .appendTo($label);
      $label.append(`<span>${prop.name}</span>`); // text
    }
  }

  rebuildCustomThemeProperties();

  $('.custom-theme-import-export').click(() => {
    let currentJSON = JSON.stringify(config.customTheme);
    let newJSON = window.prompt(
        'Copy the below JSON or paste new JSON for your custom theme.', currentJSON);
    if (newJSON && newJSON != currentJSON) {
      try {
        config.customTheme = Object.assign({}, DEFAULT_THEMES['light'], JSON.parse(newJSON) || {});
        localStorage.customTheme = JSON.stringify(config.customTheme);
        updateOutputArea();
        rebuildCustomThemeProperties();
      } catch (e) {
        alert('Error parsing the JSON: ' + e);
      }
    }
  });
}

function installServiceWorker() {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('sw.js').then(registration => {
        console.log('SW registered: ', registration);
      }).catch(registrationError => {
        console.log('SW registration failed: ', registrationError);
      });
    });
  }
}
<|endoftext|>


<commit_before>var app = require('http').createServer(handler);

//var github = require('octonode');

var statusCode = 200;

app.listen(9000, function() {
  console.log('Node app is running on port', 9000);
});

function handler (req, res) {
  var data = '';
  if (req.method == "POST") {
    req.on('data', function(chunk) {
      data += chunk;
    });
    req.on('end', function() {
      console.log('Received body data:');
      console.log(data.toString());
    });
  }
  res.writeHead(statusCode, {'Content-Type': 'text/plain'});
  res.end();
}
<commit_msg>Still testing<commit_after>var express = require("express");
var app = express();

//var github = require('octonode');

app.get('/', function(req, res){
  res.send('hello world');
});

app.listen(3000);
<|endoftext|>


<commit_before>export * from './src/monad';
export * from './src/browser-actions';
export * from './src/interpreter';
<commit_msg>Reexport monad, run, and browser actions<commit_after>export { default as Monad } from './src/monad';
export { default as run } from './src/interpreter';
export * from './src/browser-actions';
<|endoftext|>


<commit_before>var _     = require('lodash');
var gutil = require('gulp-util');
var map   = require('map-stream');
var pp    = require('preprocess');
var path  = require('path');

module.exports = function (options) {
  var opts    = _.merge({}, options);
  var context = _.merge({}, process.env, opts.context);

  function ppStream(file, callback) {
    var contents, extension;

    // TODO: support streaming files
    if (file.isNull()) return callback(null, file); // pass along
    if (file.isStream()) return callback(new Error("gulp-preprocess: Streaming not supported"));

    context.src = file.path;
    context.srcDir = path.dirname(file.path);
    context.NODE_ENV = context.NODE_ENV || 'development';

    contents = file.contents.toString('utf8');
    contents = pp.preprocess(contents, context, getExtension(context.src));
    file.contents = new Buffer(contents);

    callback(null, file);
  }

  return map(ppStream);
};

function getExtension(filename) {
  var ext = path.extname(filename||'').split('.');
  return ext[ext.length - 1];
}
<commit_msg>allow for setting base folder for include files<commit_after>var _     = require('lodash');
var map   = require('map-stream');
var pp    = require('preprocess');
var path  = require('path');

module.exports = function (options) {
  var opts    = _.merge({}, options);
  var context = _.merge({}, process.env, opts.context);

  function ppStream(file, callback) {
    var contents, extension;

    // TODO: support streaming files
    if (file.isNull()) return callback(null, file); // pass along
    if (file.isStream()) return callback(new Error("gulp-preprocess: Streaming not supported"));

    context.src = file.path;
    context.srcDir = context.base || path.dirname(file.path);
    context.NODE_ENV = context.NODE_ENV || 'development';

    contents = file.contents.toString('utf8');
    contents = pp.preprocess(contents, context, getExtension(context.src));
    file.contents = new Buffer(contents);

    callback(null, file);
  }

  return map(ppStream);
};

function getExtension(filename) {
  var ext = path.extname(filename||'').split('.');
  return ext[ext.length - 1];
}
<|endoftext|>


<commit_before>/*
 * Constants
 * */
var maxSegments = 3;

var excluded = {"dvdrip": 1, "hdtv": 1, "xvid": 1, "720p": 1, "1080p": 1, "480p": 1, "bdrip": 1, "brrip": 1};
var movieKeywords = ["1080p", "720p", "blurayrip", "brrip", "divx", "dvdrip", "hdrip", "hdtv", "tvrip", "xvid", "camrip"];

var SEGMENTS_SPLIT = /\.| |-|;|_/g;
var MATCH_FILES = /.mp4$|.mkv$|.avi$/;
var minYear = 1900, maxYear = 2060;

/*
 * TWO REFERENCES
 * * http://wiki.xbmc.org/index.php?title=Adding_videos_to_the_library/Naming_files/TV_shows
 * * http://wiki.xbmc.org/index.php?title=Advancedsettings.xml#.3Ctvshowmatching.3E
 */

var path = require("path");

function simplifyName(n) {
    return n.toLowerCase()
        .trim()
        .replace(/\([^\(]+\)$/, "") // remove brackets at end
        .replace(/&/g, "and")
        .replace(/[^0-9a-z ]+/g, " ") // remove special chars
        .split(" ").filter(function(r){return r}).join(" ")
};

function parseVideoName(filePath, options)
{
    //if (! filePath.match(MATCH_FILES)) return { type: "other" };

    var options = options || {};
    var meta = {};

    var segments = filePath
        .replace(/\\/g, "/") // Support Windows slashes, lol
        .split("/")
        .reverse()
        .filter(function(x) { return x })
        .slice(0, maxSegments);
    var firstNameSplit = segments[0].split(/\.| |_/);

    /*
     * Helpers to validate if we have found the proper metadata
     */
    function saneSeason()
    {
        return meta.hasOwnProperty("season") && !isNaN(meta.season)
    };
    function saneEpisode()
    {
        return Array.isArray(meta.episode) && meta.episode.length
    };

    /*
     * Test for a year in the name
     * */
    [segments[0], segments[1]].filter(function(x){return x}).forEach(function(seg)
    {
        var numbers = seg.match(/[^\d](\d\d\d\d)[^\d]/);
        if (numbers && numbers[1])
        {
            var number = parseInt(numbers[1], 10);
            if (number >= minYear && number <= maxYear)
                meta.year = number;
        }
    });

    /*
     * Test for "aired" stamp; if aired stamp is there, we have a series
     */
    var pad = function(x) { return ("00"+x).slice(-2) };
    [segments[0], segments[1]].filter(function(x){return x}).forEach(function(seg)
    {
        var aired = seg.match(/(\d\d\d\d)(\.|-| )(\d\d)(\.|-| )(\d\d)/);
        if (aired && aired[1])
        {
            var year = parseInt(aired[1], 10);
            if (year >= minYear && year <= maxYear)
                meta.aired = [year, pad(aired[3]), pad(aired[5])].join("-");
        }
    });

    /*
     * A typical pattern - "s05e12", "S01E01", etc. ; can be only "E01"
     * Those are matched only in the file name
     *
     * TODO: this stamp may be in another segment (e.g. directory name)
    * */
    [segments[0], segments[1]].forEach(function(seg) {
        if (seg) seg.split(/\.| |_/).forEach(function(x, i)
        {
            /*
             * Card type one
             */
            var seasonMatch = x.match(/S(\d{1,2})/i);   /* TODO: bug: uc-css4cd2.avi is a false positive */
            if (seasonMatch)
                meta.season = parseInt(seasonMatch[1], 10);

            /* TODO: consider the case when a hyphen is used for multiple episodes ; e.g. e1-3 */
            var episodeMatch = x.match(/E(\d{2})/ig);
            if (episodeMatch)
                meta.episode = episodeMatch.map(function(y) { return parseInt(y.slice(1), 10) });

            /*
             * 01x20
             */
            var xStampMatch = x.match(/(\d\d?)x(\d\d?)/i);
            if (xStampMatch)
            {
                meta.season = parseInt(xStampMatch[1], 10);
                meta.episode = [ parseInt(xStampMatch[2], 10) ];
            }
            // if (otherCardMatch)
        });

        /* Extract name from this match */
        var fullMatch = seg && seg.replace(/\.| |;|_/g, " ").match(/^([a-zA-Z0-9,-?!'& ]*) S(\d{1,2})E(\d{2})/i);
        if (!meta.name && meta.season && meta.episode && fullMatch && fullMatch[1]) meta.name = fullMatch[1];
        // TODO: consider not going through simplifyName
    });

    /*
     * This stamp must be tested before splitting (by dot)
     * a pattern of the kind [4.02]
     * This pattern should be arround characters which are not digits and not letters
     * */

    var dotStampMatch = segments[0].match(/[^\da-zA-Z](\d\d?)\.(\d\d?)[^\da-zA-Z]/i);
    if (! (saneSeason() && saneEpisode()) && dotStampMatch && !meta.year) // exclude meta.year to avoid confusing movie matches - especially if they contain 5.1 for suround sound
    {
        meta.season = parseInt(dotStampMatch[1], 10);
        meta.episode = [ parseInt(dotStampMatch[2], 10) ];
    }

    /*
     *  A stamp of the style "804", meaning season 8, episode 4
     * */
    if (!(saneSeason() && saneEpisode()) && !options.strict)
    {
        var stamp = firstNameSplit
        .reverse() /* search from the end */
        .map(function(x)
        {
            if (x.match(new RegExp("\\d\\d\\d\\d?e"))) x = x.slice(0, -1); /* This is a weird case, but I've seen it: dexter.801e.720p.x264-kyr.mkv */
            if (x.match(new RegExp("s\\d\\d\\d\\d?"))) x = x.slice(1); /* I've never seen this, but it might happen */
            return x;
        })
        .filter(function(x)
        {
            return !isNaN(x) && (x.length == 3 || (!meta.year && x.length == 4)) /* 4-digit only allowed if this has not been identified as a year */
        })[0]; /* Notice how always the first match is choosen ; the second might be a part of the episode name (e.g. "Southpark - 102 - weight gain 4000");
                * that presumes episode number/stamp comes before the name, which is where most human beings would put it */

        /* Since this one is risky, do it only if we haven't matched a year (most likely not a movie) or if year is BEFORE stamp, like: show.2014.801.mkv */
        if (stamp && (!meta.year || (meta.year && (firstNameSplit.indexOf(stamp.toString()) < firstNameSplit.indexOf(meta.year.toString())))))
        {
            meta.episode = [ parseInt(stamp.slice(-2), 10) ];
            meta.season = parseInt(stamp.slice(0, -2), 10);
        }
    }

    /*
     * "season 1", "season.1", "season1"
     * */
    if (! saneSeason())
    {
        var seasonMatch = segments.join("/").match(/season(\.| )?(\d{1,2})/ig);
        if (seasonMatch)
            meta.season = parseInt(seasonMatch[0].match(/\d/g).join(""), 10);

        var seasonEpMatch = segments.join("/").match(/Season (\d{1,2}) - (\d{1,2})/i);
        if (seasonEpMatch) {
            meta.season = parseInt(seasonEpMatch[1], 10);
            meta.episode = [ parseInt(seasonEpMatch[2], 10) ];
        }
    }

    /*
     * "episode 13", "episode.13", "episode13", "ep13", etc.
     * */
    if (! saneEpisode())
    {
        /* TODO: consider the case when a hyphen is used for multiple episodes ; e.g. e1-3*/
        var episodeMatch = segments.join("/").match(/ep(isode)?(\.| )?(\d+)/ig);
        if (episodeMatch)
            meta.episode = [ parseInt(episodeMatch[0].match(/\d/g).join(""), 10) ];
    }

    /*
     * Try plain number
     * This will lead to more false positives than true positives; disabling
     * /
     /*
    if (saneSeason() && !saneEpisode())
    {
        var epNumbers = firstNameSplit
            .filter(function(x) { return !isNaN(x) && (x.length == 2 || x.length == 1) })
            .map(function(x) { return parseInt(x, 10) })

        if (epNumbers.length)
            meta.episode = epNumbers;
    }*/

    /*
     * If still nothing, the number from the second dir can be used, if the first dir has the show name??
     *
     * OR, split everything by everything (hyphens, underscores), filter for numbers between 1 and 30 and use the assumption that the episode is on the rightmost and the series is left from it
     * (unless the numbers are a part of a date stamp)
     * */

    /*
     * Which part (for movies which are split into .cd1. and .cd2., etc.. files)
     * TODO: WARNING: this assumes it's in the filename segment
     *
     * */
    var diskNumberMatch = segments[0].match(/[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck]|d)[ _.-]*(\d)[^\d]/); /* weird regexp? */
    if (diskNumberMatch)
        meta.diskNumber = parseInt(diskNumberMatch[1], 10);

    /*
     * The name of the series / movie
     * */
    var isSample;

    (options.fromInside ? segments : [].concat(segments).reverse()).forEach(function(seg, i)
    {
        if (seg == segments[0])
            seg = seg.split(".").slice(0, -1).join("."); /* Remove extension */

        /*
         * WARNING: we must change how this works in order to handle cases like
         * "the office[1.01]" as well as "the office [1.01]"; if we split those at '[' or ']', we will get the name "the office 1 10"
         * For now, here's a hack to fix this
         */
        var squareBracket = seg.indexOf("[");
        if (squareBracket > -1) seg = seg.slice(0, squareBracket);

        //var segSplit = seg.split(/\.| |-|\[|\]|;|_/),
        var segSplit = seg.split(SEGMENTS_SPLIT),
            word,
            nameParts = [];

        isSample = isSample || seg.match(/^sample/i)|| seg.match(/^etrg/i);

        /* No need to go further;  */
        if (meta.name)
            return;

        var lastIndex;
        segSplit.some(function(word, i)
        {
            lastIndex = i;
            /* words with basic punctuation and two-digit numbers; or numbers in the first position */
            if (! (word.match("^[a-zA-Z,?!'&]*$") || (!isNaN(word) && word.length <= 2) || (!isNaN(word) && i==0))
                || excluded[word.toLowerCase()]
                || ((["ep", "episode", "season"].indexOf(word.toLowerCase()) > -1) && !isNaN(segSplit[i+1])) // TODO: more than that, match for stamp too
                )
                return true;

            nameParts.push(word);
        });
        if (nameParts.length == 1 && !isNaN(nameParts[0])) return; /* Only a number: unacceptable */

        meta.name = nameParts
            .filter(function(x) { return x && x.length > 0 })
            .map(function(x) { return x[0].toUpperCase() + x.slice(1).toLowerCase() })
            .join(" ");
    });

    isSample = isSample || ((segments[1] || "").toLowerCase() == "sample"); /* The directory where the file resides */

    /*
     * This is the time to decide the category of this video
     */
    var canBeMovie;
    if (options.strict) canBeMovie = meta.hasOwnProperty("year");
    else canBeMovie = meta.hasOwnProperty("year") || meta.hasOwnProperty("diskNumber") || movieKeywords.some(function(keyword) { return segments.join("/").toLowerCase().search(keyword) > -1 });

    if (meta.name && meta.aired)
        meta.type = "series";
    if (meta.name && saneSeason() && saneEpisode())
        meta.type = "series";
    else if (meta.name && canBeMovie)
        meta.type = "movie";
    // Must be deprioritized compared to movies
    else if (meta.type != "movie" && meta.name && saneSeason()) // assume extras or bloopers?
        meta.type = "extras";
    else
        meta.type = "other";

    if (options.fileLength && options.fileLength < (meta.type.match(/movie/) ? 80 : 50)*1024*1024 && meta.type.match(/movie|series/) && !isSample) {
        meta.type = "other";
    }

    if (meta.type != "series" || meta.aired) {
        delete meta.episode;
        delete meta.season;
    }

    // we have a year. put it to .aired instead
    if (meta.type == "series" && meta.year) {
     meta.aired = meta.aired || meta.year;
     delete meta.year;
    }

    meta.type += isSample ? "-sample" : "";

    meta.name = meta.name && simplifyName(meta.name);

    /* Try to find the IMDB id from the NFO / hints
     */
    //if (meta.nfo)
    //    meta.imdb_id = (fs.readFileSync(meta.nfo).toString().match("tt\\d{7}") || [])[0];

    if (options.hints && options.hints.imdb_id)
        meta.imdb_id = options.hints.imdb_id;

    meta.tag = [];
    if (filePath.match(/1080p/i)) { meta.tag.push("hd"); meta.tag.push("1080p"); }
    if (filePath.match(/720p/i)) { meta.tag.push("720p"); }
    if (filePath.match(/480p/i)) { meta.tag.push("480p"); }
    if (isSample) meta.tag.push("sample");

    return meta;
}

module.exports = parseVideoName;
<commit_msg>refactor excluded keywords<commit_after>/*
 * Constants
 * */
var maxSegments = 3;

var movieKeywords = ["1080p", "720p", "480p", "blurayrip", "brrip", "divx", "dvdrip", "hdrip", "hdtv", "tvrip", "xvid", "camrip"];

// Excluded is an object we use to exclude those keywords from consideration for detecting strings like "season X"
var excluded = { };
movieKeywords.forEach(function(x) { excluded[x] = 1 });

var SEGMENTS_SPLIT = /\.| |-|;|_/g;
var MATCH_FILES = /.mp4$|.mkv$|.avi$/;
var minYear = 1900, maxYear = 2060;

/*
 * TWO REFERENCES
 * * http://wiki.xbmc.org/index.php?title=Adding_videos_to_the_library/Naming_files/TV_shows
 * * http://wiki.xbmc.org/index.php?title=Advancedsettings.xml#.3Ctvshowmatching.3E
 */

var path = require("path");

function simplifyName(n) {
    return n.toLowerCase()
        .trim()
        .replace(/\([^\(]+\)$/, "") // remove brackets at end
        .replace(/&/g, "and")
        .replace(/[^0-9a-z ]+/g, " ") // remove special chars
        .split(" ").filter(function(r){return r}).join(" ")
};

function parseVideoName(filePath, options)
{
    //if (! filePath.match(MATCH_FILES)) return { type: "other" };

    var options = options || {};
    var meta = {};

    var segments = filePath
        .replace(/\\/g, "/") // Support Windows slashes, lol
        .split("/")
        .reverse()
        .filter(function(x) { return x })
        .slice(0, maxSegments);
    var firstNameSplit = segments[0].split(/\.| |_/);

    /*
     * Helpers to validate if we have found the proper metadata
     */
    function saneSeason()
    {
        return meta.hasOwnProperty("season") && !isNaN(meta.season)
    };
    function saneEpisode()
    {
        return Array.isArray(meta.episode) && meta.episode.length
    };

    /*
     * Test for a year in the name
     * */
    [segments[0], segments[1]].filter(function(x){return x}).forEach(function(seg)
    {
        var numbers = seg.match(/[^\d](\d\d\d\d)[^\d]/);
        if (numbers && numbers[1])
        {
            var number = parseInt(numbers[1], 10);
            if (number >= minYear && number <= maxYear)
                meta.year = number;
        }
    });

    /*
     * Test for "aired" stamp; if aired stamp is there, we have a series
     */
    var pad = function(x) { return ("00"+x).slice(-2) };
    [segments[0], segments[1]].filter(function(x){return x}).forEach(function(seg)
    {
        var aired = seg.match(/(\d\d\d\d)(\.|-| )(\d\d)(\.|-| )(\d\d)/);
        if (aired && aired[1])
        {
            var year = parseInt(aired[1], 10);
            if (year >= minYear && year <= maxYear)
                meta.aired = [year, pad(aired[3]), pad(aired[5])].join("-");
        }
    });

    /*
     * A typical pattern - "s05e12", "S01E01", etc. ; can be only "E01"
     * Those are matched only in the file name
     *
     * TODO: this stamp may be in another segment (e.g. directory name)
    * */
    [segments[0], segments[1]].forEach(function(seg) {
        if (seg) seg.split(/\.| |_/).forEach(function(x, i)
        {
            /*
             * Card type one
             */
            var seasonMatch = x.match(/S(\d{1,2})/i);   /* TODO: bug: uc-css4cd2.avi is a false positive */
            if (seasonMatch)
                meta.season = parseInt(seasonMatch[1], 10);

            /* TODO: consider the case when a hyphen is used for multiple episodes ; e.g. e1-3 */
            var episodeMatch = x.match(/E(\d{2})/ig);
            if (episodeMatch)
                meta.episode = episodeMatch.map(function(y) { return parseInt(y.slice(1), 10) });

            /*
             * 01x20
             */
            var xStampMatch = x.match(/(\d\d?)x(\d\d?)/i);
            if (xStampMatch)
            {
                meta.season = parseInt(xStampMatch[1], 10);
                meta.episode = [ parseInt(xStampMatch[2], 10) ];
            }
            // if (otherCardMatch)
        });

        /* Extract name from this match */
        var fullMatch = seg && seg.replace(/\.| |;|_/g, " ").match(/^([a-zA-Z0-9,-?!'& ]*) S(\d{1,2})E(\d{2})/i);
        if (!meta.name && meta.season && meta.episode && fullMatch && fullMatch[1]) meta.name = fullMatch[1];
        // TODO: consider not going through simplifyName
    });

    /*
     * This stamp must be tested before splitting (by dot)
     * a pattern of the kind [4.02]
     * This pattern should be arround characters which are not digits and not letters
     * */

    var dotStampMatch = segments[0].match(/[^\da-zA-Z](\d\d?)\.(\d\d?)[^\da-zA-Z]/i);
    if (! (saneSeason() && saneEpisode()) && dotStampMatch && !meta.year) // exclude meta.year to avoid confusing movie matches - especially if they contain 5.1 for suround sound
    {
        meta.season = parseInt(dotStampMatch[1], 10);
        meta.episode = [ parseInt(dotStampMatch[2], 10) ];
    }

    /*
     *  A stamp of the style "804", meaning season 8, episode 4
     * */
    if (!(saneSeason() && saneEpisode()) && !options.strict)
    {
        var stamp = firstNameSplit
        .reverse() /* search from the end */
        .map(function(x)
        {
            if (x.match(new RegExp("\\d\\d\\d\\d?e"))) x = x.slice(0, -1); /* This is a weird case, but I've seen it: dexter.801e.720p.x264-kyr.mkv */
            if (x.match(new RegExp("s\\d\\d\\d\\d?"))) x = x.slice(1); /* I've never seen this, but it might happen */
            return x;
        })
        .filter(function(x)
        {
            return !isNaN(x) && (x.length == 3 || (!meta.year && x.length == 4)) /* 4-digit only allowed if this has not been identified as a year */
        })[0]; /* Notice how always the first match is choosen ; the second might be a part of the episode name (e.g. "Southpark - 102 - weight gain 4000");
                * that presumes episode number/stamp comes before the name, which is where most human beings would put it */

        /* Since this one is risky, do it only if we haven't matched a year (most likely not a movie) or if year is BEFORE stamp, like: show.2014.801.mkv */
        if (stamp && (!meta.year || (meta.year && (firstNameSplit.indexOf(stamp.toString()) < firstNameSplit.indexOf(meta.year.toString())))))
        {
            meta.episode = [ parseInt(stamp.slice(-2), 10) ];
            meta.season = parseInt(stamp.slice(0, -2), 10);
        }
    }

    /*
     * "season 1", "season.1", "season1"
     * */
    if (! saneSeason())
    {
        var seasonMatch = segments.join("/").match(/season(\.| )?(\d{1,2})/ig);
        if (seasonMatch)
            meta.season = parseInt(seasonMatch[0].match(/\d/g).join(""), 10);

        var seasonEpMatch = segments.join("/").match(/Season (\d{1,2}) - (\d{1,2})/i);
        if (seasonEpMatch) {
            meta.season = parseInt(seasonEpMatch[1], 10);
            meta.episode = [ parseInt(seasonEpMatch[2], 10) ];
        }
    }

    /*
     * "episode 13", "episode.13", "episode13", "ep13", etc.
     * */
    if (! saneEpisode())
    {
        /* TODO: consider the case when a hyphen is used for multiple episodes ; e.g. e1-3*/
        var episodeMatch = segments.join("/").match(/ep(isode)?(\.| )?(\d+)/ig);
        if (episodeMatch)
            meta.episode = [ parseInt(episodeMatch[0].match(/\d/g).join(""), 10) ];
    }

    /*
     * Try plain number
     * This will lead to more false positives than true positives; disabling
     * /
     /*
    if (saneSeason() && !saneEpisode())
    {
        var epNumbers = firstNameSplit
            .filter(function(x) { return !isNaN(x) && (x.length == 2 || x.length == 1) })
            .map(function(x) { return parseInt(x, 10) })

        if (epNumbers.length)
            meta.episode = epNumbers;
    }*/

    /*
     * If still nothing, the number from the second dir can be used, if the first dir has the show name??
     *
     * OR, split everything by everything (hyphens, underscores), filter for numbers between 1 and 30 and use the assumption that the episode is on the rightmost and the series is left from it
     * (unless the numbers are a part of a date stamp)
     * */

    /*
     * Which part (for movies which are split into .cd1. and .cd2., etc.. files)
     * TODO: WARNING: this assumes it's in the filename segment
     *
     * */
    var diskNumberMatch = segments[0].match(/[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck]|d)[ _.-]*(\d)[^\d]/); /* weird regexp? */
    if (diskNumberMatch)
        meta.diskNumber = parseInt(diskNumberMatch[1], 10);

    /*
     * The name of the series / movie
     * */
    var isSample;

    (options.fromInside ? segments : [].concat(segments).reverse()).forEach(function(seg, i)
    {
        if (seg == segments[0])
            seg = seg.split(".").slice(0, -1).join("."); /* Remove extension */

        /*
         * WARNING: we must change how this works in order to handle cases like
         * "the office[1.01]" as well as "the office [1.01]"; if we split those at '[' or ']', we will get the name "the office 1 10"
         * For now, here's a hack to fix this
         */
        var squareBracket = seg.indexOf("[");
        if (squareBracket > -1) seg = seg.slice(0, squareBracket);

        //var segSplit = seg.split(/\.| |-|\[|\]|;|_/),
        var segSplit = seg.split(SEGMENTS_SPLIT),
            word,
            nameParts = [];

        isSample = isSample || seg.match(/^sample/i)|| seg.match(/^etrg/i);

        /* No need to go further;  */
        if (meta.name)
            return;

        var lastIndex;
        segSplit.some(function(word, i)
        {
            lastIndex = i;
            /* words with basic punctuation and two-digit numbers; or numbers in the first position */
            if (! (word.match("^[a-zA-Z,?!'&]*$") || (!isNaN(word) && word.length <= 2) || (!isNaN(word) && i==0))
                || excluded[word.toLowerCase()]
                || ((["ep", "episode", "season"].indexOf(word.toLowerCase()) > -1) && !isNaN(segSplit[i+1])) // TODO: more than that, match for stamp too
                )
                return true;

            nameParts.push(word);
        });
        if (nameParts.length == 1 && !isNaN(nameParts[0])) return; /* Only a number: unacceptable */

        meta.name = nameParts
            .filter(function(x) { return x && x.length > 0 })
            .map(function(x) { return x[0].toUpperCase() + x.slice(1).toLowerCase() })
            .join(" ");
    });

    isSample = isSample || ((segments[1] || "").toLowerCase() == "sample"); /* The directory where the file resides */

    /*
     * This is the time to decide the category of this video
     */
    var canBeMovie;
    if (options.strict) canBeMovie = meta.hasOwnProperty("year");
    else canBeMovie = meta.hasOwnProperty("year") || meta.hasOwnProperty("diskNumber") || movieKeywords.some(function(keyword) { return segments.join("/").toLowerCase().search(keyword) > -1 });

    if (meta.name && meta.aired)
        meta.type = "series";
    if (meta.name && saneSeason() && saneEpisode())
        meta.type = "series";
    else if (meta.name && canBeMovie)
        meta.type = "movie";
    // Must be deprioritized compared to movies
    else if (meta.type != "movie" && meta.name && saneSeason()) // assume extras or bloopers?
        meta.type = "extras";
    else
        meta.type = "other";

    if (options.fileLength && options.fileLength < (meta.type.match(/movie/) ? 80 : 50)*1024*1024 && meta.type.match(/movie|series/) && !isSample) {
        meta.type = "other";
    }

    if (meta.type != "series" || meta.aired) {
        delete meta.episode;
        delete meta.season;
    }

    // we have a year. put it to .aired instead
    if (meta.type == "series" && meta.year) {
     meta.aired = meta.aired || meta.year;
     delete meta.year;
    }

    meta.type += isSample ? "-sample" : "";

    meta.name = meta.name && simplifyName(meta.name);

    /* Try to find the IMDB id from the NFO / hints
     */
    //if (meta.nfo)
    //    meta.imdb_id = (fs.readFileSync(meta.nfo).toString().match("tt\\d{7}") || [])[0];

    if (options.hints && options.hints.imdb_id)
        meta.imdb_id = options.hints.imdb_id;

    meta.tag = [];
    if (filePath.match(/1080p/i)) { meta.tag.push("hd"); meta.tag.push("1080p"); }
    if (filePath.match(/720p/i)) { meta.tag.push("720p"); }
    if (filePath.match(/480p/i)) { meta.tag.push("480p"); }
    if (isSample) meta.tag.push("sample");

    return meta;
}

module.exports = parseVideoName;
<|endoftext|>


<commit_before><commit_msg>clean<commit_after><|endoftext|>


<commit_before>/* jshint node: true */
'use strict';

var fs = require( 'fs' );
var mergeTrees = require( 'broccoli-merge-trees' );
var path = require( 'path' );
var writeFile = require( 'broccoli-file-creator' );

module.exports = {

    /**
     * Name of addon
     *
     * @type {String}
     */
    name: 'ember-forge-ui',

    /**
     * Name of companion ember-forge-ui addon
     *
     * @type {?String}
     */
    emberForgeUiCompanionAddonName: null,

    /**
     * Set name of ember-forge-ui companion addon to local context if exists
     *
     * @param {String} environment
     * @param {Object} appConfig
     * @returns {undefined}
     */
    config: function( environment, appConfig ) {
        if ( appConfig[this.name] && appConfig[this.name].addonName ) {
            this.emberForgeUiCompanionAddonName = appConfig[this.name].addonName;
        }
    },

    /**
     * Return templates if ember-forge-ui companion addon is installed, default ones if not
     *
     * @param {Object} tree
     * @returns {Object}
     */
    treeForAddonTemplates: function() {
        if ( this.emberForgeUiCompanionAddonName ) {
            return this.treeGenerator(
                path.join( this.nodeModulesPath, this.emberForgeUiCompanionAddonName, 'addon', 'templates' )
            );

        } else {
            var componentTemplateTrees = [];
            var files = fs.readdirSync( path.join( this.nodeModulesPath, 'addon', 'components' ) );
            var placeholderContentPath = path.join(
                this.nodeModulesPath,
                'addon',
                'templates',
                'components'
            ) + path.sep + 'placeholder-content.hbs';
            var placeholderContent = require( placeholderContentPath );

            files.forEach( function( element ) {
                componentTemplateTrees.push(
                    writeFile( '/components/' + element.replace( '.js', '' ) + '.hbs', placeholderContent() )
                );
            });

            return mergeTrees( componentTemplateTrees );
        }
    }
};
<commit_msg>Previous reversion was not properly reverted<commit_after>/* jshint node: true */
'use strict';

var fs = require( 'fs' );
var mergeTrees = require( 'broccoli-merge-trees' );
var path = require( 'path' );
var writeFile = require( 'broccoli-file-creator' );

module.exports = {

    /**
     * Name of addon
     *
     * @type {String}
     */
    name: 'ember-forge-ui',

    /**
     * Name of companion ember-forge-ui addon
     *
     * @type {?String}
     */
    emberForgeUiCompanionAddonName: null,

    /**
     * Set name of ember-forge-ui companion addon to local context if exists
     *
     * @param {String} environment
     * @param {Object} appConfig
     * @returns {undefined}
     */
    config: function( environment, appConfig ) {
        if ( appConfig[this.name] && appConfig[this.name].addonName ) {
            this.emberForgeUiCompanionAddonName = appConfig[this.name].addonName;
        }
    },

    /**
     * Return templates if ember-forge-ui companion addon is installed, default ones if not
     *
     * @param {Object} tree
     * @returns {Object}
     */
    treeForAddonTemplates: function() {
        if ( this.emberForgeUiCompanionAddonName ) {
            return this.treeGenerator(
                path.join( this.nodeModulesPath, this.emberForgeUiCompanionAddonName, 'addon', 'templates' )
            );

        } else {
            var componentTemplateTrees = [];
            var files = fs.readdirSync( path.join( this.nodeModulesPath, this.name, 'addon', 'components' ) );
            var placeholderContentPath = path.join(
                this.nodeModulesPath,
                this.name,
                'addon',
                'templates',
                'components'
            ) + path.sep + 'placeholder-content.hbs';
            var placeholderContent = require( placeholderContentPath );

            files.forEach( function( element ) {
                componentTemplateTrees.push(
                    writeFile( '/components/' + element.replace( '.js', '' ) + '.hbs', placeholderContent() )
                );
            });

            return mergeTrees( componentTemplateTrees );
        }
    }
};
<|endoftext|>


<commit_before>#! /usr/bin/env node

var fs = require('fs');

var fse = require('fs-extra');

var properties = require ('properties');

var moment = require('moment');

var XLSX = require('xlsx');

var fileExists = require('file-exists');

var getUsage = require('command-line-usage')

var marked = require('marked');

var main = function(args) {

	var subcommand = args[0];

	if(subcommand == 'merge') {

		if(args.length != 3) raiseArgsError();

		var fromPath = args[1];

		var intoPath = args[2];

		merge(fromPath, intoPath);

	} else if(subcommand == 'format' || subcommand == 'sort') {

		if(args.length != 2) raiseArgsError();

		var path = args[1];

		format(path);

	} else if(subcommand == 'subset') {

		if(args.length != 3) raiseArgsError();

		var path = args[1];

		var pattern = args[2];

		subset(path, pattern);

	} else if(subcommand == 'from-xlsx') {

		if(args.length != 4) raiseArgsError();

		var excelPath = args[1];

		var configPath = args[2];

		var path = args[3];

		var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

		extractFromExcel(excelPath, config, path);

	}  else {

		raiseArgsError();
	}
}

var read = function(path) {

	var content = fs.readFileSync(path, 'utf8').toString();

	var data = properties.parse (content, {});

	return data;
};

var write = function(path, data) {

	var content = properties.stringify(data);

	var content = content.split('\n').sort().join('\n');

	fs.writeFileSync(path, content, {
		encoding: 'utf8',
		flag: 'w'
	});
};

var backup = function(path) {

	var extension = '.backup-' + moment().format('YYYYMMDDHHmmssSSS');

	fse.copySync(path, path + extension);

}

var merge = function(fromPath, intoPath) {

	backup(intoPath);

	var fromData = read(fromPath);
	var intoData = read(intoPath);

	Object.assign(intoData, fromData);

	write(intoPath, intoData);
};

var format = function(path) {

	backup(path);

	var data = read(path);

	write(path, data);
};

var subset = function(path, pattern) {

	var regexp = new RegExp(pattern);

	var data = read(path);

	var subsetData = {};

	Object.keys(data).forEach(function(key) {
		var result = regexp.exec(key);
	    if(result != null && result[0] === key) {
	    	subsetData[key] = data[key];
	    }
	});

	var subsetPath = path + '.subset';

	write(subsetPath, subsetData);
};

var extractFromExcel = function(excelPath, config, path) {

	var data = {};

	if(fileExists(path)) {

		console.log('Target file exists, properties will be merged');

		backup(path);

		data = read(path);
	}

	/*

	Config sample :

		{
			"sheet": "Sheet 1",
			"keyColumn": "I",
			"valueColumn": "H",
			"firstLine": 2,
			"lastLine": 7
		}
	*/

	var keyCol = config.keyColumn,
	 	valueCol = config.valueColumn,
	 	start = config.firstLine
	 	end = config.lastLine;

	var workbook = XLSX.readFile(excelPath);

	var worksheet = workbook.Sheets[config.sheet];

	if(!worksheet || worksheet == null) {
		workbook.Sheets[workbook.SheetNames[0]];
	}

	for (var i = start; i <= end; i++) {

		var propKeyAddress = keyCol + i;

		if(!worksheet.hasOwnProperty(propKeyAddress )) continue;

		var propKey = worksheet[propKeyAddress].v;

		var propValueAddress = valueCol + i;

		var propValue = worksheet[propValueAddress].v;

		if(propKey && propKey !== "" && propValue && propValue !== "" ) {

			data[propKey] = propValue;
		}
	};

	write(path, data);
};

var raiseArgsError = function() {
	printUsage();
	process.exit();
}

var printUsage = function() {

	var usage = getUsage([
	  {
	    header: 'NAME',
	    content: '  props - working on Java properties files.',
	    raw: true
	  },
	  {
	    header: 'SYNOPSIS',
	    content: '$ props <command> <command-args>'
	  },
	  {
	    header: 'COMMAND LIST',
	    content: [
	      {
	      	name: 'merge',
	      	summary: 'Merge a properties file into an other properties file.'
	      },
	      {
	      	name: 'sort',
	      	summary: 'Sort '
	      },
	      {
	      	name: 'format',
	      	summary: 'Alias for sort command.'
	      },
	      { name: 'subset',

	      	summary: 'Select a subset of properties according to a pattern for keys.'
	      },
	      {
	      	name: 'from-xlsx',
	      	summary: 'Extract properties file from an XLSX (Excel) file.'
	      }
	    ]
	  },
	  {
	    header: 'MERGE',
	    content:
	    	'$ props merge <from-properties-file>  <into-properties-file>\n\n'
	    	+ 'Each property of <from-properties-file> is added to '
	    	+ '<into-properties-file>.'
	    	+ 'For each property with same key inside the two files, the value '
	    	+ 'from <from-properties-file> is used to overwrite the property in '
	    	+ '<into-properties-file>.'
	  },
	  {
	    header: 'FROM-XLSX',
	    content:
	    	'$ props from-xlsx <from-excel-file> '
	    	+ '<excel-file-structure-description> <into-properties-file>\n\n'
	    	+ 'Each property extracted from <from-excel-file> is added to '
	    	+ '<into-properties-file>.'
	    	+ 'For each property with same key inside the two files, the value '
	    	+ 'from <from-excel-file> is used to overwrite the property in '
	    	+ '<into-properties-file>.\n'
	    	+ 'If <into-properties-file> file does not exist, it will be '
	    	+ 'created.\n\n'
	    	+ '<excel-file-structure-description> is a JSON file describing '
	    	+ 'where properties keys and values are stored in the Excel file. See sample below: \n\n'
	  },
	  {
	    content:
	    	  '     {\n'
			+ '          "sheet": "Sheet 1",\n'
			+ '          "keyColumn": "I",\n'
			+ '          "valueColumn": "H",\n'
			+ '          "firstLine": 2,\n'
			+ '          "lastLine": 7\n'
			+ '     }\n',
		raw: true
	  }
	]);

	console.log(usage);
};

var args = process.argv.slice(2);

main(args);
<commit_msg>Manage some limit cases<commit_after>#! /usr/bin/env node

var fs = require('fs');

var fse = require('fs-extra');

var properties = require ('properties');

var moment = require('moment');

var XLSX = require('xlsx');

var fileExists = require('file-exists');

var getUsage = require('command-line-usage')

var marked = require('marked');

var main = function(args) {

	var subcommand = args[0];

	if(subcommand == 'merge') {

		if(args.length != 3) raiseArgsError();

		var fromPath = args[1];

		var intoPath = args[2];

		merge(fromPath, intoPath);

	} else if(subcommand == 'format' || subcommand == 'sort') {

		if(args.length != 2) raiseArgsError();

		var path = args[1];

		format(path);

	} else if(subcommand == 'subset') {

		if(args.length != 3) raiseArgsError();

		var path = args[1];

		var pattern = args[2];

		subset(path, pattern);

	} else if(subcommand == 'from-xlsx') {

		if(args.length != 4) raiseArgsError();

		var excelPath = args[1];

		var configPath = args[2];

		var path = args[3];

		var config = JSON.parse(fs.readFileSync(configPath, 'utf8'));

		extractFromExcel(excelPath, config, path);

	}  else {

		raiseArgsError();
	}
}

var read = function(path) {

	var content = fs.readFileSync(path, 'utf8').toString();

	var data = properties.parse (content, {});

	return data;
};

var write = function(path, data) {

	var content = properties.stringify(data);

	var content = content.split('\n').sort().join('\n');

	fs.writeFileSync(path, content, {
		encoding: 'utf8',
		flag: 'w'
	});
};

var backup = function(path) {

	var extension = '.backup-' + moment().format('YYYYMMDDHHmmssSSS');

	fse.copySync(path, path + extension);

}

var merge = function(fromPath, intoPath) {

	backup(intoPath);

	var fromData = read(fromPath);
	var intoData = read(intoPath);

	Object.assign(intoData, fromData);

	write(intoPath, intoData);
};

var format = function(path) {

	backup(path);

	var data = read(path);

	write(path, data);
};

var subset = function(path, pattern) {

	var regexp = new RegExp(pattern);

	var data = read(path);

	var subsetData = {};

	Object.keys(data).forEach(function(key) {
		var result = regexp.exec(key);
	    if(result != null && result[0] === key) {
	    	subsetData[key] = data[key];
	    }
	});

	var subsetPath = path + '.subset';

	write(subsetPath, subsetData);
};

var extractFromExcel = function(excelPath, config, path) {

	var data = {};

	if(fileExists(path)) {

		console.log('Target file exists, properties will be merged');

		backup(path);

		data = read(path);
	}

	/*

	Config sample :

		{
			"sheet": "Sheet 1",
			"keyColumn": "I",
			"valueColumn": "H",
			"firstLine": 2,
			"lastLine": 7
		}
	*/

	var keyCol = config.keyColumn,
	 	valueCol = config.valueColumn,
	 	start = config.firstLine
	 	end = config.lastLine;

	var workbook = XLSX.readFile(excelPath);

	var worksheet = workbook.Sheets[config.sheet];

	if(!worksheet || worksheet == null) {
		workbook.Sheets[workbook.SheetNames[0]];
	}

	for (var i = start; i <= end; i++) {

		var propKeyAddress = keyCol + i;

		if(!worksheet.hasOwnProperty(propKeyAddress )) {
			console.log('Key missing at cell ' + propKeyAddress);
			continue;
		}

		var propKey = worksheet[propKeyAddress].v;

		var propValueAddress = valueCol + i;

		if(!worksheet.hasOwnProperty(propValueAddress)) {
			console.log('Value missing at cell ' + propValueAddress + ' (for key ' + propKey  + ')');
			continue;
		}

		var propValue = worksheet[propValueAddress].v;

		if(propKey && propKey !== "" && propValue && propValue !== "" ) {

			data[propKey] = propValue;
		}
	};

	write(path, data);
};

var raiseArgsError = function() {
	printUsage();
	process.exit();
}

var printUsage = function() {

	var usage = getUsage([
	  {
	    header: 'NAME',
	    content: '  props - working on Java properties files.',
	    raw: true
	  },
	  {
	    header: 'SYNOPSIS',
	    content: '$ props <command> <command-args>'
	  },
	  {
	    header: 'COMMAND LIST',
	    content: [
	      {
	      	name: 'merge',
	      	summary: 'Merge a properties file into an other properties file.'
	      },
	      {
	      	name: 'sort',
	      	summary: 'Sort '
	      },
	      {
	      	name: 'format',
	      	summary: 'Alias for sort command.'
	      },
	      { name: 'subset',

	      	summary: 'Select a subset of properties according to a pattern for keys.'
	      },
	      {
	      	name: 'from-xlsx',
	      	summary: 'Extract properties file from an XLSX (Excel) file.'
	      }
	    ]
	  },
	  {
	    header: 'MERGE',
	    content:
	    	'$ props merge <from-properties-file>  <into-properties-file>\n\n'
	    	+ 'Each property of <from-properties-file> is added to '
	    	+ '<into-properties-file>.'
	    	+ 'For each property with same key inside the two files, the value '
	    	+ 'from <from-properties-file> is used to overwrite the property in '
	    	+ '<into-properties-file>.'
	  },
	  {
	    header: 'FROM-XLSX',
	    content:
	    	'$ props from-xlsx <from-excel-file> '
	    	+ '<excel-file-structure-description> <into-properties-file>\n\n'
	    	+ 'Each property extracted from <from-excel-file> is added to '
	    	+ '<into-properties-file>.'
	    	+ 'For each property with same key inside the two files, the value '
	    	+ 'from <from-excel-file> is used to overwrite the property in '
	    	+ '<into-properties-file>.\n'
	    	+ 'If <into-properties-file> file does not exist, it will be '
	    	+ 'created.\n\n'
	    	+ '<excel-file-structure-description> is a JSON file describing '
	    	+ 'where properties keys and values are stored in the Excel file. See sample below: \n\n'
	  },
	  {
	    content:
	    	  '     {\n'
			+ '          "sheet": "Sheet 1",\n'
			+ '          "keyColumn": "I",\n'
			+ '          "valueColumn": "H",\n'
			+ '          "firstLine": 2,\n'
			+ '          "lastLine": 7\n'
			+ '     }\n',
		raw: true
	  }
	]);

	console.log(usage);
};

var args = process.argv.slice(2);

main(args);
<|endoftext|>


<commit_before>/*jslint indent: 2, node: true, nomen: true*/
"use strict";

var kindof = require("kindof");

function regexpErrMsg(args) {
  return "expected" + (args.optional ? " optional" : "")
    + " argument " + args.propName
    + " to match " + args.inputPattern.toString()
    + " (was \"" + args.input + "\")";
}

function errMsg(args) {
  return "expected" + (args.optional ? " optional" : "")
    + " argument " + args.propName
    + " to be of type " + args.schemaType
    + " (was " + args.inputType + ")";
}

function getErrs(args) {
  // not checking whether args is valid because... seriously?
  var prop,
    schema = args.schema,
    input = args.input,
    prefix = args.prefix || "",
    optional = args.optional || false,
    type,
    schemaType,
    errs = [];

  for (prop in schema) {
    if (schema.hasOwnProperty(prop)) {
      type = kindof(input[prop]);
      schemaType = kindof(schema[prop]);

      if (schemaType === "object") {
        if (type === "object") {
          // if both input and schema properties are objects
          // we'll need to recurse
          errs.push(getErrs({
            input: input[prop],
            schema: schema[prop],
            prefix: prefix + prop + ".",
            optional: optional
          }));
        } else {
          errs.push(errMsg({
            propName: prefix + prop,
            inputType: type,
            schemaType: "object",
            optional: optional
          }));
        }
      } else if (schemaType === "regexp") {
        if (type === "string" && !schema[prop].test(input[prop])) {
          errs.push(regexpErrMsg({
            propName: prefix + prop,
            inputPattern: schema[prop],
            input: input[prop],
            optional: optional
          }));
        } else if (type !== "string") {
          errs.push(errMsg({
            propName: prefix + prop,
            inputType: type,
            schemaType: "string",
            optional: optional
          }));
        }
      } else if (type !== schema[prop]
          && !(type === "undefined" && optional)) {
        errs.push(errMsg({
          propName: prefix + prop,
          inputType: type,
          schemaType: schema[prop],
          optional: optional
        }));
      }
    }
  }

  return errs;
}

exports.err = function (input, schema, optionalSchema) {
  var errs = getErrs({
    input: input,
    schema: schema
  });

  if (optionalSchema) {
    errs = errs.concat(getErrs({
      input: input,
      schema: optionalSchema,
      optional: true
    }));
  }

  return errs.length ? errs.join(", ") : null;
};
<commit_msg>control flow improvements and comments<commit_after>/*jslint indent: 2, node: true, continue: true*/
"use strict";

var kindof = require("kindof");

function regexpErrMsg(args) {
  return "expected" + (args.optional ? " optional" : "")
    + " argument " + args.propName
    + " to match " + args.inputPattern.toString()
    + " (was \"" + args.input + "\")";
}

function errMsg(args) {
  return "expected" + (args.optional ? " optional" : "")
    + " argument " + args.propName
    + " to be of type " + args.schemaType
    + " (was " + args.inputType + ")";
}

function getErrs(args) {
  // not checking whether args is valid because... seriously?
  var prop,
    schema = args.schema,
    input = args.input,
    prefix = args.prefix || "",
    optional = args.optional || false,
    type,
    schemaType,
    expectedType,
    errs = [];

  for (prop in schema) {
    if (schema.hasOwnProperty(prop)) {
      type = kindof(input[prop]);
      expectedType = schema[prop];
      schemaType = kindof(schema[prop]);

      // any kinds of checks are pointless if optional schema
      if (optional && type === "undefined") {
        continue;
      }

      // special cases where the schema prop is not defined as a string
      // if input prop does not fit the special case,
      // switch to a simpler expected type
      if (schemaType === "object") {
        if (type === "object") {
          // if both input and schema properties are objects
          // we'll need to recurse
          errs.push(getErrs({
            input: input[prop],
            schema: schema[prop],
            prefix: prefix + prop + ".",
            optional: optional
          }));

          continue;
        } else {
          expectedType = "object";
        }
      } else if (schemaType === "regexp") {
        // special case regex
        // assuming regex matched fields are always strings
        if (type === "string" && !schema[prop].test(input[prop])) {
          errs.push(regexpErrMsg({
            propName: prefix + prop,
            inputPattern: schema[prop],
            input: input[prop],
            optional: optional
          }));

          continue;
        } else {
          expectedType = "string";
        }
      }

      if (type !== expectedType) {
        errs.push(errMsg({
          propName: prefix + prop,
          inputType: type,
          schemaType: expectedType,
          optional: optional
        }));
      }
    }
  }

  return errs;
}

exports.err = function (input, schema, optionalSchema) {
  var errs = getErrs({
    input: input,
    schema: schema
  });

  if (optionalSchema) {
    errs = errs.concat(getErrs({
      input: input,
      schema: optionalSchema,
      optional: true
    }));
  }

  return errs.length ? errs.join(", ") : null;
};
<|endoftext|>


<commit_before>var stream = require('readable-stream')
var inherits = require('inherits')
var varint = require('varint')
var sodium = require('sodium-universal')
var feed = require('./feed')
var messages = require('./messages')

module.exports = Protocol

function Protocol (opts) {
  if (!(this instanceof Protocol)) return new Protocol(opts)
  if (!opts) opts = {}

  stream.Duplex.call(this)

  this.id = opts.id || randomBytes(32)
  this.live = !!opts.live
  this.userData = opts.userData || null
  this.remoteId = null
  this.remoteLive = false
  this.remoteUserData = null

  this.destroyed = false
  this.encrypted = opts.encrypt !== false
  this.key = null
  this.discoveryKey = null
  this.remoteDiscoveryKey = null
  this.feeds = []
  this.expectedFeeds = opts.expectedFeeds || 0

  this._localFeeds = []
  this._remoteFeeds = []
  this._feeds = {}

  this._nonce = null
  this._remoteNonce = null
  this._xor = null
  this._remoteXor = null
  this._needsKey = false
  this._length = new Buffer(varint.encodingLength(8388608))
  this._missing = 0
  this._buf = null
  this._pointer = 0
  this._data = null
  this._start = 0
  this._cb = null
  this._interval = null
  this._keepAlive = 0
  this._remoteKeepAlive = 0

  if (opts.timeout !== 0 && opts.timeout !== false) this.setTimeout(opts.timeout || 5000, this._ontimeout)
  this.on('finish', this.finalize)
}

inherits(Protocol, stream.Duplex)

Protocol.prototype.setTimeout = function (ms, ontimeout) {
  if (this.destroyed) return
  if (ontimeout) this.once('timeout', ontimeout)

  var self = this

  this._keepAlive = 0
  this._remoteKeepAlive = 0

  clearInterval(this._interval)
  if (!ms) return

  this._interval = setInterval(kick, (ms / 4) | 0)
  if (this._interval.unref) this._interval.unref()

  function kick () {
    self._kick()
  }
}

Protocol.prototype.feed = function (key, opts) {
  if (this.destroyed) return null
  if (!opts) opts = {}

  var dk = opts.discoveryKey || discoveryKey(key)
  var ch = this._feed(dk)

  if (ch.id > -1) return ch

  if (this._localFeeds.length >= 128) {
    this._tooManyFeeds()
    return null
  }

  ch.id = this._localFeeds.push(ch) - 1
  ch.header = ch.id << 4
  ch.headerLength = varint.encodingLength(ch.header)
  ch.key = key
  ch.discoveryKey = dk
  if (opts.peer) ch.peer = opts.peer

  this.feeds.push(ch)

  var first = !this.key
  var feed = {
    discoveryKey: dk,
    nonce: null
  }

  if (first) {
    this.key = key
    this.discoveryKey = dk

    if (!this._sameKey()) return null

    if (this.encrypted) {
      feed.nonce = this._nonce = randomBytes(24)
      this._xor = sodium.crypto_stream_xor_instance(this._nonce, this.key)
      if (this._remoteNonce) {
        this._remoteXor = sodium.crypto_stream_xor_instance(this._remoteNonce, this.key)
      }
    }

    if (this._needsKey) {
      this._needsKey = false
      this._resume()
    }
  }

  var box = encodeFeed(feed, ch.id)
  if (!feed.nonce && this.encrypted) this._xor.update(box, box)
  this._keepAlive = 0
  this.push(box)

  if (this.destroyed) return null

  if (first) {
    ch.handshake({id: this.id, live: this.live, userData: this.userData})
  }

  if (ch._buffer.length) ch._resume()
  else ch._buffer = null

  return ch
}

Protocol.prototype._resume = function () {
  var self = this
  process.nextTick(resume)

  function resume () {
    if (!self._data) return

    var data = self._data
    var start = self._start
    var cb = self._cb

    self._data = null
    self._start = 0
    self._cb = null
    self._parse(data, start, cb)
  }
}

Protocol.prototype._kick = function () {
  if (this._remoteKeepAlive > 4) {
    clearInterval(this._interval)
    this.emit('timeout')
    return
  }

  for (var i = 0; i < this.feeds.length; i++) {
    var ch = this.feeds[i]
    if (ch.peer) ch.peer.ontick()
    else ch.emit('tick')
  }

  this._remoteKeepAlive++

  if (this._keepAlive > 2) {
    this.ping()
    this._keepAlive = 0
  } else {
    this._keepAlive++
  }
}

Protocol.prototype.ping = function () {
  if (!this.key) return true
  var ping = new Buffer([0])
  if (this._xor) this._xor.update(ping, ping)
  return this.push(ping)
}

Protocol.prototype.destroy = function (err) {
  if (this.destroyed) return
  this.destroyed = true
  if (err) this.emit('error', err)
  this._close()
  this.emit('close')
}

Protocol.prototype.finalize = function () {
  if (this.destroyed) return
  this.destroyed = true
  this._close()
  this.push(null)
}

Protocol.prototype._close = function () {
  var feeds = this.feeds
  this.feeds = []
  for (var i = 0; i < feeds.length; i++) feeds[i]._onclose()
}

Protocol.prototype._read = function () {
  // do nothing, user back-pressures
}

Protocol.prototype._push = function (data) {
  if (this.destroyed) return
  this._keepAlive = 0
  if (this._xor) this._xor.update(data, data)
  return this.push(data)
}

Protocol.prototype._write = function (data, enc, cb) {
  this._remoteKeepAlive = 0
  this._parse(data, 0, cb)
}

Protocol.prototype._feed = function (dk) {
  var hex = dk.toString('hex')
  var ch = this._feeds[hex]
  if (ch) return ch
  ch = this._feeds[hex] = feed(this)
  return ch
}

Protocol.prototype._onhandshake = function (handshake) {
  if (this.remoteId) return
  this.remoteId = handshake.id || randomBytes(32)
  this.remoteLive = handshake.live
  this.remoteUserData = handshake.userData
  this.emit('handshake')
}

Protocol.prototype._onopen = function (id, data, start, end) {
  var feed = decodeFeed(data, start, end)

  if (!feed) return this._badFeed()

  if (!this.remoteDiscoveryKey) {
    this.remoteDiscoveryKey = feed.discoveryKey
    if (!this._sameKey()) return

    if (this.encrypted && !this._remoteNonce) {
      if (!feed.nonce) {
        this.destroy(new Error('Remote did not include a nonce'))
        return
      }
      this._remoteNonce = feed.nonce
    }

    if (this.encrypted && this.key && !this._remoteXor) {
      this._remoteXor = sodium.crypto_stream_xor_instance(this._remoteNonce, this.key)
    }
  }

  this._remoteFeeds[id] = this._feed(feed.discoveryKey)
  feed.remoteId = id

  this.emit('feed', feed.discoveryKey)
}

Protocol.prototype._onmessage = function (data, start, end) {
  if (end - start < 2) return

  var header = decodeHeader(data, start)
  if (header === -1) return this.destroy(new Error('Remote sent invalid header'))

  start += varint.decode.bytes

  var id = header >> 4
  var type = header & 15

  if (id >= 128) return this._tooManyFeeds()
  while (this._remoteFeeds.length < id) this._remoteFeeds.push(null)

  var ch = this._remoteFeeds[id]

  if (type === 0) {
    if (ch) ch._onclose()
    return this._onopen(id, data, start, end)
  }

  if (!ch) return this._badFeed()
  ch._onmessage(type, data, start, end)
}

Protocol.prototype._parse = function (data, start, cb) {
  var decrypted = !!this._remoteXor

  if (start) {
    data = data.slice(start)
    start = 0
  }

  if (this._remoteXor) this._remoteXor.update(data, data)

  while (start < data.length && !this.destroyed) {
    if (this._missing) start = this._parseMessage(data, start)
    else start = this._parseLength(data, start)

    if (this._needsKey) {
      this._data = data
      this._start = start
      this._cb = cb
      return
    }

    if (!decrypted && this._remoteXor) {
      return this._parse(data, start, cb)
    }
  }

  cb()
}

Protocol.prototype._parseMessage = function (data, start) {
  var end = start + this._missing

  if (end <= data.length) {
    var ret = end

    if (this._buf) {
      data.copy(this._buf, this._pointer, start)
      data = this._buf
      start = 0
      end = data.length
      this._buf = null
    }

    this._missing = 0
    this._pointer = 0
    if (this.encrypted && !this.key) this._needsKey = true
    this._onmessage(data, start, end)

    return ret
  }

  if (!this._buf) {
    this._buf = new Buffer(this._missing)
    this._pointer = 0
  }

  var rem = data.length - start

  data.copy(this._buf, this._pointer, start)
  this._pointer += rem
  this._missing -= rem

  return data.length
}

Protocol.prototype._parseLength = function (data, start) {
  while (!this._missing && start < data.length) {
    var byte = this._length[this._pointer++] = data[start++]

    if (!(byte & 0x80)) {
      this._missing = varint.decode(this._length)
      this._pointer = 0
      if (this._missing > 8388608) return this._tooBig(data.length)
      return start
    }

    if (this._pointer >= this._length.length) return this._tooBig(data.length)
  }

  return start
}

Protocol.prototype._sameKey = function () {
  if (!this.discoveryKey || !this.remoteDiscoveryKey) return true
  if (this.remoteDiscoveryKey.toString('hex') === this.discoveryKey.toString('hex')) return true
  this.destroy(new Error('First shared hypercore must be the same'))
  return false
}

Protocol.prototype._tooManyFeeds = function () {
  this.destroy(new Error('Only 128 feeds currently supported. Open a Github issue if you need more'))
}

Protocol.prototype._tooBig = function (len) {
  this.destroy(new Error('Remote message is larger than 8MB (max allowed)'))
  return len
}

Protocol.prototype._badFeed = function () {
  this.destroy(new Error('Remote sent invalid feed message'))
}

Protocol.prototype._ontimeout = function () {
  this.destroy(new Error('Remote timed out'))
}

function decodeHeader (data, start) {
  try {
    return varint.decode(data, start)
  } catch (err) {
    return -1
  }
}

function decodeFeed (data, start, end) {
  var feed = null

  try {
    feed = messages.Feed.decode(data, start, end)
  } catch (err) {
    return null
  }

  if (feed.discoveryKey.length !== 32) return null
  if (feed.nonce && feed.nonce.length !== 24) return null

  return feed
}

function encodeFeed (feed, id) {
  var header = id << 4
  var len = varint.encodingLength(header) + messages.Feed.encodingLength(feed)
  var box = new Buffer(varint.encodingLength(len) + len)
  var offset = 0

  varint.encode(len, box, offset)
  offset += varint.encode.bytes

  varint.encode(header, box, offset)
  offset += varint.encode.bytes

  messages.Feed.encode(feed, box, offset)
  return box
}

function discoveryKey (key) {
  var buf = new Buffer(32)
  sodium.crypto_generichash(buf, new Buffer('hypercore'), key)
  return buf
}

function randomBytes (n) {
  var buf = new Buffer(n)
  sodium.randombytes_buf(buf)
  return buf
}
<commit_msg>clear interval on close<commit_after>var stream = require('readable-stream')
var inherits = require('inherits')
var varint = require('varint')
var sodium = require('sodium-universal')
var feed = require('./feed')
var messages = require('./messages')

module.exports = Protocol

function Protocol (opts) {
  if (!(this instanceof Protocol)) return new Protocol(opts)
  if (!opts) opts = {}

  stream.Duplex.call(this)

  this.id = opts.id || randomBytes(32)
  this.live = !!opts.live
  this.userData = opts.userData || null
  this.remoteId = null
  this.remoteLive = false
  this.remoteUserData = null

  this.destroyed = false
  this.encrypted = opts.encrypt !== false
  this.key = null
  this.discoveryKey = null
  this.remoteDiscoveryKey = null
  this.feeds = []
  this.expectedFeeds = opts.expectedFeeds || 0

  this._localFeeds = []
  this._remoteFeeds = []
  this._feeds = {}

  this._nonce = null
  this._remoteNonce = null
  this._xor = null
  this._remoteXor = null
  this._needsKey = false
  this._length = new Buffer(varint.encodingLength(8388608))
  this._missing = 0
  this._buf = null
  this._pointer = 0
  this._data = null
  this._start = 0
  this._cb = null
  this._interval = null
  this._keepAlive = 0
  this._remoteKeepAlive = 0

  if (opts.timeout !== 0 && opts.timeout !== false) this.setTimeout(opts.timeout || 5000, this._ontimeout)
  this.on('finish', this.finalize)
}

inherits(Protocol, stream.Duplex)

Protocol.prototype.setTimeout = function (ms, ontimeout) {
  if (this.destroyed) return
  if (ontimeout) this.once('timeout', ontimeout)

  var self = this

  this._keepAlive = 0
  this._remoteKeepAlive = 0

  clearInterval(this._interval)
  if (!ms) return

  this._interval = setInterval(kick, (ms / 4) | 0)
  if (this._interval.unref) this._interval.unref()

  function kick () {
    self._kick()
  }
}

Protocol.prototype.feed = function (key, opts) {
  if (this.destroyed) return null
  if (!opts) opts = {}

  var dk = opts.discoveryKey || discoveryKey(key)
  var ch = this._feed(dk)

  if (ch.id > -1) return ch

  if (this._localFeeds.length >= 128) {
    this._tooManyFeeds()
    return null
  }

  ch.id = this._localFeeds.push(ch) - 1
  ch.header = ch.id << 4
  ch.headerLength = varint.encodingLength(ch.header)
  ch.key = key
  ch.discoveryKey = dk
  if (opts.peer) ch.peer = opts.peer

  this.feeds.push(ch)

  var first = !this.key
  var feed = {
    discoveryKey: dk,
    nonce: null
  }

  if (first) {
    this.key = key
    this.discoveryKey = dk

    if (!this._sameKey()) return null

    if (this.encrypted) {
      feed.nonce = this._nonce = randomBytes(24)
      this._xor = sodium.crypto_stream_xor_instance(this._nonce, this.key)
      if (this._remoteNonce) {
        this._remoteXor = sodium.crypto_stream_xor_instance(this._remoteNonce, this.key)
      }
    }

    if (this._needsKey) {
      this._needsKey = false
      this._resume()
    }
  }

  var box = encodeFeed(feed, ch.id)
  if (!feed.nonce && this.encrypted) this._xor.update(box, box)
  this._keepAlive = 0
  this.push(box)

  if (this.destroyed) return null

  if (first) {
    ch.handshake({id: this.id, live: this.live, userData: this.userData})
  }

  if (ch._buffer.length) ch._resume()
  else ch._buffer = null

  return ch
}

Protocol.prototype._resume = function () {
  var self = this
  process.nextTick(resume)

  function resume () {
    if (!self._data) return

    var data = self._data
    var start = self._start
    var cb = self._cb

    self._data = null
    self._start = 0
    self._cb = null
    self._parse(data, start, cb)
  }
}

Protocol.prototype._kick = function () {
  if (this._remoteKeepAlive > 4) {
    clearInterval(this._interval)
    this.emit('timeout')
    return
  }

  for (var i = 0; i < this.feeds.length; i++) {
    var ch = this.feeds[i]
    if (ch.peer) ch.peer.ontick()
    else ch.emit('tick')
  }

  this._remoteKeepAlive++

  if (this._keepAlive > 2) {
    this.ping()
    this._keepAlive = 0
  } else {
    this._keepAlive++
  }
}

Protocol.prototype.ping = function () {
  if (!this.key) return true
  var ping = new Buffer([0])
  if (this._xor) this._xor.update(ping, ping)
  return this.push(ping)
}

Protocol.prototype.destroy = function (err) {
  if (this.destroyed) return
  this.destroyed = true
  if (err) this.emit('error', err)
  this._close()
  this.emit('close')
}

Protocol.prototype.finalize = function () {
  if (this.destroyed) return
  this.destroyed = true
  this._close()
  this.push(null)
}

Protocol.prototype._close = function () {
  clearInterval(this._interval)

  var feeds = this.feeds
  this.feeds = []
  for (var i = 0; i < feeds.length; i++) feeds[i]._onclose()
}

Protocol.prototype._read = function () {
  // do nothing, user back-pressures
}

Protocol.prototype._push = function (data) {
  if (this.destroyed) return
  this._keepAlive = 0
  if (this._xor) this._xor.update(data, data)
  return this.push(data)
}

Protocol.prototype._write = function (data, enc, cb) {
  this._remoteKeepAlive = 0
  this._parse(data, 0, cb)
}

Protocol.prototype._feed = function (dk) {
  var hex = dk.toString('hex')
  var ch = this._feeds[hex]
  if (ch) return ch
  ch = this._feeds[hex] = feed(this)
  return ch
}

Protocol.prototype._onhandshake = function (handshake) {
  if (this.remoteId) return
  this.remoteId = handshake.id || randomBytes(32)
  this.remoteLive = handshake.live
  this.remoteUserData = handshake.userData
  this.emit('handshake')
}

Protocol.prototype._onopen = function (id, data, start, end) {
  var feed = decodeFeed(data, start, end)

  if (!feed) return this._badFeed()

  if (!this.remoteDiscoveryKey) {
    this.remoteDiscoveryKey = feed.discoveryKey
    if (!this._sameKey()) return

    if (this.encrypted && !this._remoteNonce) {
      if (!feed.nonce) {
        this.destroy(new Error('Remote did not include a nonce'))
        return
      }
      this._remoteNonce = feed.nonce
    }

    if (this.encrypted && this.key && !this._remoteXor) {
      this._remoteXor = sodium.crypto_stream_xor_instance(this._remoteNonce, this.key)
    }
  }

  this._remoteFeeds[id] = this._feed(feed.discoveryKey)
  feed.remoteId = id

  this.emit('feed', feed.discoveryKey)
}

Protocol.prototype._onmessage = function (data, start, end) {
  if (end - start < 2) return

  var header = decodeHeader(data, start)
  if (header === -1) return this.destroy(new Error('Remote sent invalid header'))

  start += varint.decode.bytes

  var id = header >> 4
  var type = header & 15

  if (id >= 128) return this._tooManyFeeds()
  while (this._remoteFeeds.length < id) this._remoteFeeds.push(null)

  var ch = this._remoteFeeds[id]

  if (type === 0) {
    if (ch) ch._onclose()
    return this._onopen(id, data, start, end)
  }

  if (!ch) return this._badFeed()
  ch._onmessage(type, data, start, end)
}

Protocol.prototype._parse = function (data, start, cb) {
  var decrypted = !!this._remoteXor

  if (start) {
    data = data.slice(start)
    start = 0
  }

  if (this._remoteXor) this._remoteXor.update(data, data)

  while (start < data.length && !this.destroyed) {
    if (this._missing) start = this._parseMessage(data, start)
    else start = this._parseLength(data, start)

    if (this._needsKey) {
      this._data = data
      this._start = start
      this._cb = cb
      return
    }

    if (!decrypted && this._remoteXor) {
      return this._parse(data, start, cb)
    }
  }

  cb()
}

Protocol.prototype._parseMessage = function (data, start) {
  var end = start + this._missing

  if (end <= data.length) {
    var ret = end

    if (this._buf) {
      data.copy(this._buf, this._pointer, start)
      data = this._buf
      start = 0
      end = data.length
      this._buf = null
    }

    this._missing = 0
    this._pointer = 0
    if (this.encrypted && !this.key) this._needsKey = true
    this._onmessage(data, start, end)

    return ret
  }

  if (!this._buf) {
    this._buf = new Buffer(this._missing)
    this._pointer = 0
  }

  var rem = data.length - start

  data.copy(this._buf, this._pointer, start)
  this._pointer += rem
  this._missing -= rem

  return data.length
}

Protocol.prototype._parseLength = function (data, start) {
  while (!this._missing && start < data.length) {
    var byte = this._length[this._pointer++] = data[start++]

    if (!(byte & 0x80)) {
      this._missing = varint.decode(this._length)
      this._pointer = 0
      if (this._missing > 8388608) return this._tooBig(data.length)
      return start
    }

    if (this._pointer >= this._length.length) return this._tooBig(data.length)
  }

  return start
}

Protocol.prototype._sameKey = function () {
  if (!this.discoveryKey || !this.remoteDiscoveryKey) return true
  if (this.remoteDiscoveryKey.toString('hex') === this.discoveryKey.toString('hex')) return true
  this.destroy(new Error('First shared hypercore must be the same'))
  return false
}

Protocol.prototype._tooManyFeeds = function () {
  this.destroy(new Error('Only 128 feeds currently supported. Open a Github issue if you need more'))
}

Protocol.prototype._tooBig = function (len) {
  this.destroy(new Error('Remote message is larger than 8MB (max allowed)'))
  return len
}

Protocol.prototype._badFeed = function () {
  this.destroy(new Error('Remote sent invalid feed message'))
}

Protocol.prototype._ontimeout = function () {
  this.destroy(new Error('Remote timed out'))
}

function decodeHeader (data, start) {
  try {
    return varint.decode(data, start)
  } catch (err) {
    return -1
  }
}

function decodeFeed (data, start, end) {
  var feed = null

  try {
    feed = messages.Feed.decode(data, start, end)
  } catch (err) {
    return null
  }

  if (feed.discoveryKey.length !== 32) return null
  if (feed.nonce && feed.nonce.length !== 24) return null

  return feed
}

function encodeFeed (feed, id) {
  var header = id << 4
  var len = varint.encodingLength(header) + messages.Feed.encodingLength(feed)
  var box = new Buffer(varint.encodingLength(len) + len)
  var offset = 0

  varint.encode(len, box, offset)
  offset += varint.encode.bytes

  varint.encode(header, box, offset)
  offset += varint.encode.bytes

  messages.Feed.encode(feed, box, offset)
  return box
}

function discoveryKey (key) {
  var buf = new Buffer(32)
  sodium.crypto_generichash(buf, new Buffer('hypercore'), key)
  return buf
}

function randomBytes (n) {
  var buf = new Buffer(n)
  sodium.randombytes_buf(buf)
  return buf
}
<|endoftext|>