<commit_before>'use strict';

var Reflux = require('reflux');
var Q = require('q');
var binsPromise = require('gramene-bins-client').binsPromise;
var treesPromise = require('gramene-trees-client').promise;
var VisualizationActions = require('../actions/visualizationActions');
var searchStore = require('./searchStore');
var resultTypes = require('gramene-search-client').resultTypes;
var QueryActions = require('../actions/queryActions');

function getFieldName(type, param) {
  return type ? type + '_' + param + '_bin' : undefined;
}

function getMethodName(type) {
  return type ? type + 'BinMapper' : undefined;
}

module.exports = Reflux.createStore({
  listenables: VisualizationActions,

  init: function () {
    this.listenTo(searchStore, this.updateBinState);
    Q.all([
      binsPromise.get(),
      treesPromise.get()
    ]).spread(this.initBinsGeneratorAndSpeciesTree);
  },

  onRemoveDistribution: function() {
    this.onSetDistribution(); // set distribution to undefined
  },

  onSetDistribution: function (type, param) {
    var newFieldName = getFieldName(type, param);
    console.log('visStore will assemble distributions from field',
      newFieldName);

    if (this.fieldName) {
      if (this.fieldName !== newFieldName) {
        // don't need this data anymore.
        QueryActions.removeResultType(this.fieldName);
      }
      else {
        // we already have requested this distribution
        return;
      }
    }

    this.fieldName = newFieldName;
    this.binMapperMethodName = getMethodName(type);
    this.binMapperParam = param;

    if(!newFieldName) {
      this.trigger({});
      return;
    }

    this.possiblyInitBinnedGenomesAndTrigger();

    // notify that we would like this data please
    QueryActions.setResultType(
      this.fieldName,
      resultTypes.get(
        'distribution',
        {'facet.field': this.fieldName}
      )
    );
  },

  updateBinState: function (searchState) {
    this.binnedResults = searchState.results[this.fieldName];
    if(this.binnedResults) {
      console.log('visStore received new results');
      this.possiblyTrigger();
    }
    else {
      console.log('visStore did not receive data');
    }
  },

  initBinsGeneratorAndSpeciesTree: function (binsGenerator, speciesTree) {
    this.binsGenerator = binsGenerator;
    this.speciesTree = speciesTree;
    console.log('binsGenerator initialized');
    this.possiblyInitBinnedGenomesAndTrigger();
  },

  binnedGenomesIsUpToDate: function() {
    return this.binnedGenomes &&
      this.binnedGenomes.params.methodName === this.binMapperMethodName &&
      this.binnedGenomes.params.param === this.binMapperParam;
  },

  canInitBinnedGenomes: function() {
    return this.binsGenerator &&
      this.binMapperMethodName &&
      this.binMapperParam;
  },

  possiblyInitBinnedGenomesAndTrigger: function() {
    if (this.canInitBinnedGenomes() &&
        !this.binnedGenomesIsUpToDate()) {
      var binFunction = this.binsGenerator[this.binMapperMethodName];
      this.binnedGenomes = binFunction(this.binMapperParam).binnedGenomes();
      this.binnedGenomes.params = {
        methodName: this.binMapperMethodName,
        param: this.binMapperParam
      };
      this.possiblyTrigger();
    }
  },

  haveNecessaryDataToTrigger: function() {
    return this.binnedGenomesIsUpToDate() &&
      this.binnedResults;
  },

  possiblyTrigger: function () {
    // ensure binsGenerator, binMapperMethodName,
    // binMapperParam and binResults populated
    if (this.haveNecessaryDataToTrigger()) {
      console.log('visStore triggering');
      this.trigger({
        binnedGenomes: this.binnedGenomes,
        binnedResults: this.binnedResults
      });
    }
  }
});
<commit_msg>species tree available to visualization<commit_after>'use strict';

var Reflux = require('reflux');
var Q = require('q');
var binsPromise = require('gramene-bins-client').binsPromise;
var treesPromise = require('gramene-trees-client').promise;
var VisualizationActions = require('../actions/visualizationActions');
var searchStore = require('./searchStore');
var resultTypes = require('gramene-search-client').resultTypes;
var QueryActions = require('../actions/queryActions');

function getFieldName(type, param) {
  return type ? type + '_' + param + '_bin' : undefined;
}

function getMethodName(type) {
  return type ? type + 'BinMapper' : undefined;
}

module.exports = Reflux.createStore({
  listenables: VisualizationActions,

  init: function () {
    this.listenTo(searchStore, this.updateBinState);
    Q.all([
      binsPromise.get(),
      treesPromise.get()
    ]).spread(this.initBinsGeneratorAndSpeciesTree);
  },

  onRemoveDistribution: function() {
    this.onSetDistribution(); // set distribution to undefined
  },

  onSetDistribution: function (type, param) {
    var newFieldName = getFieldName(type, param);
    console.log('visStore will assemble distributions from field',
      newFieldName);

    if (this.fieldName) {
      if (this.fieldName !== newFieldName) {
        // don't need this data anymore.
        QueryActions.removeResultType(this.fieldName);
      }
      else {
        // we already have requested this distribution
        return;
      }
    }

    this.fieldName = newFieldName;
    this.binMapperMethodName = getMethodName(type);
    this.binMapperParam = param;

    if(!newFieldName) {
      this.trigger({});
      return;
    }

    this.possiblyInitBinnedGenomesAndTrigger();

    // notify that we would like this data please
    QueryActions.setResultType(
      this.fieldName,
      resultTypes.get(
        'distribution',
        {'facet.field': this.fieldName}
      )
    );
  },

  updateBinState: function (searchState) {
    this.binnedResults = searchState.results[this.fieldName];
    if(this.binnedResults) {
      console.log('visStore received new results');
      this.possiblyTrigger();
    }
    else {
      console.log('visStore did not receive data');
    }
  },

  initBinsGeneratorAndSpeciesTree: function (binsGenerator, speciesTree) {
    this.binsGenerator = binsGenerator;
    this.speciesTree = speciesTree;
    console.log('binsGenerator initialized');
    this.possiblyInitBinnedGenomesAndTrigger();
  },

  binnedGenomesIsUpToDate: function() {
    return this.binnedGenomes &&
      this.binnedGenomes.params.methodName === this.binMapperMethodName &&
      this.binnedGenomes.params.param === this.binMapperParam;
  },

  canInitBinnedGenomes: function() {
    return this.binsGenerator &&
      this.binMapperMethodName &&
      this.binMapperParam;
  },

  possiblyInitBinnedGenomesAndTrigger: function() {
    if (this.canInitBinnedGenomes() &&
        !this.binnedGenomesIsUpToDate()) {
      var binFunction = this.binsGenerator[this.binMapperMethodName];
      this.binnedGenomes = binFunction(this.binMapperParam).binnedGenomes();
      this.binnedGenomes.params = {
        methodName: this.binMapperMethodName,
        param: this.binMapperParam
      };
      this.possiblyTrigger();
    }
  },

  haveNecessaryDataToTrigger: function() {
    return this.binnedGenomesIsUpToDate() &&
      this.binnedResults;
  },

  possiblyTrigger: function () {
    // ensure binsGenerator, binMapperMethodName,
    // binMapperParam and binResults populated
    if (this.haveNecessaryDataToTrigger()) {
      this.binnedGenomes.setResults(this.binnedResults);

      console.log('visStore triggering');
      this.trigger({
        binnedGenomes: this.binnedGenomes,
        speciesTree: this.speciesTree
      });
    }
  }
});
<|endoftext|>


<commit_before>

var hasOwnProperty = {}.hasOwnProperty;
var renderElements = {};
var renderidOffset = 0;
var renderidClosed = 0;
var addRenderElement = function () {
    var element = this;
    if (!isNode(element)) return;
    renderElements[element.renderid] = element;
    rebuild(element);
};
var removeRenderElement = function () {
    var element = this;
    delete renderElements[element.renderid];
};

function refresh() {
    for (var k in renderElements) {
        var element = renderElements[k];
        var props = extend({}, element);
        rebuild(element);
        var changes = getChanges(element, props);
        if (changes) {
            var event = createEvent('changes');
            event.changes = changes;
            dispatch(event, element);
        }
    }
}
function rebuild(element) {
    element.renders.forEach(a => a.call(element));
}
var createGetter = function (search) {
    var [withContext, searchContext] = search;
    return new Function(`try{${withContext}with(this.$scope)return ${searchContext}}catch(e){/*console.warn(String(e))*/}`);
};
var initialComment = function (renders, type, expression) {
    var comment = document.createComment(`${type} ${expression}`);
    comment.renders = renders;
    comment.renderid = ++renderidOffset;
    onappend(comment, addRenderElement);
    onremove(comment, removeRenderElement);
    appendChild.after(this, comment);
    remove(this);
    rebuild(comment);
    return comment;
};

var createRepeat = function (search) {
    // 懒渲染
    // throw new Error("repeat is not supported! use list component instead");

    var [context, expression] = search;
    var reg =
        // /////////////////////////////////////////// i //       r       /////////////////////////  o  ///// a ///////////////////// t /////
        /^(?:let\b|var\b|const\b)?\s*(?:[\(\{\[]\s*)?(.+?)((?:\s*,\s*.+?)*)?(?:\s*[\)\}\]]\s*|\s+)(in|of)\s+(.+?)(\s+?:track\s*by\s+(.+?))?$/i;
    var res = reg.exec(expression);
    if (!res) throw new Error(`no recognition for repeat expression: ${expression} `);
    var [_, i, k, r, s, t] = res;
    var keyName, itemName, indexName, trackBy = t, srcName = s;
    switch (r) {
        case "in":
            if (i) itemName = i;
            if (k) {
                var [keyName, indexName] = k.split(/,/).map(a => a.trim()).filter(a => !!a);
            }
            break;
        case "of":
            if (i) itemName = i;
            if (k) {
                keyName = i;
                var [itemName, indexName] = k.split(/,/).map(a => a.trim()).filter(a => !!a);
            }
            break;
    }
    // 懒渲染
    var getter = createGetter([context, srcName]).bind(this);
    var element = this, clonedElements = [], savedValue;
    var renders = [function () {
        var result = getter();
        result = result instanceof Array ? result.slice(0) : Object.assign({}, result);
        if (deepEqual.shallow(savedValue, result)) return;
        savedValue = result;
        remove(clonedElements);
        var keys = Object.keys(result);
        if (keys.length > 600) {
            throw new Error("数据量过大，取消绘制！");
        }
        var $parentScopes = element.$parentScopes || [];
        clonedElements = keys.map(function (key, cx) {
            var clone = element.cloneNode();
            clone.innerHTML = element.innerHTML;
            clone.renderid = 0;
            clone.$parentScopes = $parentScopes.concat(element.$scope);
            renderElement(clone, {
                [keyName || '$key']: key,
                [itemName || '$item']: result[key],
                [indexName || '$index']: cx
            }, clone.$parentScopes);
            return clone;
        }, this);
        appendChild.before(this, clonedElements);
    }];
    if (this.parentNode) {
        initialComment.call(this, renders, 'repeat', expression);
    } else {
        once("append")(this, initialComment.bind(this, renders, "repeat", expression));
    }
};
var structures = {
    "if"(search) {
        // 懒渲染
        var getter = createGetter(search).bind(this);
        var element = this;
        var renders = [function () {
            var result = getter();
            if (result) {
                if (!element.parentNode) appendChild.before(this, element);
                if (element.renderid < 0) {
                    element.renderid = 0;
                    renderElement(element);
                }
            } else {
                remove(element);
            }
        }];
        if (this.parentNode) {
            initialComment.call(this, renders, "if", search[1]);
        } else {
            once("append")(this, initialComment.bind(this, renders, "if", search[1]));
        }
    },
    repeat(search) {
        createRepeat.call(this, search);
    },
    for(search) {
        createRepeat.call(this, search);
    },
    each(search) {
        createRepeat.call(this, search);
    }
};
var directives = {
    click(search) {
        var getter = createGetter(search);
        onclick(this, getter);
    },
    src(search) {
        var getter = createGetter(search).bind(this);
        var oldValue, pending;
        var refresh = function () {
            that.src = oldValue;
            removeClass(that, "pending");
            pending = 0;
        };
        var img = document.createElement("img");
        var that = this;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(value, oldValue)) return;
            oldValue = value;
            value = value || "";
            if (!/img/i.test(this.tagName) || !isString(value)) return this.src = value;
            this.setAttribute("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=");
            img.src = value;
            if (img.complete) {
                this.src = value;
            } else if (!pending) {
                addClass(this, "pending");
                pending = setTimeout(refresh);
            }
        });
    },
    bind(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(value, oldValue)) return;
            oldValue = value;
            if (text(this) !== value) text(this, value);
        });
    },
    model(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        if (/^input$/i.test(this.tagName) && /^checkbox$/i.test(this.type) || /^checkbox$/i.test(this.tagName)) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                this.checked = value;
            });
            var change = new Function(`${search[0]}with(this.$scope)${search[1]}=this.checked`).bind(this);
        } else if (("value" in this || this.getValue instanceof Function) && this.setValue instanceof Function) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                if ((this.getValue instanceof Function ? this.getValue() : this.value) !== value) this.setValue(value);
            });
            var change = new Function(`${search[0]}with(this.$scope)${search[1]}=this.value`).bind(this);
        } else if (/^(select|input|textarea)$/i.test(this.tagName) || "value" in this) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                if (this.value !== value) this.value = value;
            });
            var change = new Function(`${search[0]}with(this.$scope)${search[1]}=this.value`).bind(this);
        } else {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                if (html(this) !== value) html(this, value);
            });
            var change = new Function("html", `${search[0]}with(this.$scope)${search[1]}=html(this)`).bind(this, html);
        }
        var onchange = lazy(change);
        eventsHandlers.map(on => on(this, onchange));
    },
    hide(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(oldValue, value)) return;
            oldValue = value;
            if (value) {
                this.style.display = "none";
            } else {
                this.style.display = "";
            }
        });
    },
    show(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(oldValue, value)) return;
            oldValue = value;
            if (value) {
                this.style.display = "";
            } else {
                this.style.display = "none";
            }
        });
    },

    "class"(search) {
        var getter = createGetter(search).bind(this);
        var generatedClassNames = {};
        var oldValue;
        this.renders.push(function () {
            var className = getter();
            if (deepEqual(oldValue, className)) return;
            oldValue = className;
            var originalClassNames = [];
            this.className.split(/\s+/).map(function (k) {
                if (k && !hasOwnProperty.call(generatedClassNames, k) && !hasOwnProperty.call(originalClassNames, k)) {
                    if (!/^\d+$/.test(k)) originalClassNames.push(originalClassNames[k] = k);
                }
            });
            var deltaClassNames = [];
            if (isString(className)) {
                className.split(/\s+/).map(function (k) {
                    if (!hasOwnProperty.call(originalClassNames, k)) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                });
            } else if (className instanceof Object) {
                for (var k in className) {
                    if (!hasOwnProperty.call(originalClassNames, k) && className[k]) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                }
            }
            var destClassName = originalClassNames.concat(deltaClassNames).join(" ");
            generatedClassNames = deltaClassNames;
            if (this.className !== destClassName) {
                this.className = destClassName;
            }
        });
    },
    style(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var stylesheet = getter();
            if (deepEqual(oldValue, stylesheet)) return;
            oldValue = stylesheet;
            if (isString(stylesheet)) {
                stylesheet.replace(/[\s\u00a0]+/g, "").split(/;/).map(function (kv) {
                    var [k, v] = kv.split(":");
                    if (this.style[k] !== v) {
                        this.style[k] = v;
                    }
                }, this);
            } else if (stylesheet instanceof Object) {
                for (var k in stylesheet) {
                    if (this.style[k] !== stylesheet[k]) {
                        this.style[k] = stylesheet[k];
                    }
                }
            }
        });
    }
};

function renderElement(element, scope = element.$scope, parentScopes = element.$parentScopes) {
    var children = element.children;
    if (!children) {
        return [].concat.apply([], element).map(function (element) {
            return renderElement(element, scope, parentScopes);
        });
    }
    if (!isNumber(element.renderid)) {
        renderStructure(element, scope, parentScopes);
    }
    if (element.renderid < 0) {
        return scope;
    }
    if (children.length) renderElement(children, scope, parentScopes);
    if (element.renderid) return scope;
    element.renderid = -2;
    var attrs = [].concat.apply([], element.attributes);
    var { tagName, parentNode, nextSibling } = element;
    if (parentNode) {
        if (!scope[tagName]) tagName = tagName.toLowerCase();
        if (!scope[tagName])
            tagName = tagName.replace(/(?:^|\-+)([a-z])/ig, (_, w) => w.toUpperCase());
        if (!scope[tagName]) tagName = tagName.slice(0, 1).toLowerCase() + tagName.slice(1);
        if (isFunction(scope[tagName])) var replacer = scope[tagName](element);
        if (isElement(replacer) && element !== replacer) {
            if (nextSibling) appendChild.before(nextSibling, replacer);
            else appendChild(parentNode, replacer);
            if (element.parentNode === parentNode) remove(element);
            attrs.map(function (attr) {
                var { name, value } = attr;
                switch (name.toLowerCase()) {
                    case "class":
                        addClass(replacer, value);
                        break;
                    case "style":
                        css(replacer, value);
                        break;
                    case "src":
                    case "placeholder":
                        replacer[name] = value;
                        break;
                    default:
                        if (!/[\-]/.test(name)) {
                            replacer.setAttribute(name, value);
                        }
                }
            });
            element = replacer;
            element.$scope = scope;
        }
    }
    element.renders = [];
    var withContext = parentScopes ? parentScopes.map((_, cx) => `with(this.$parentScopes[${cx}])`).join("") : '';
    attrs.map(function (attr) {
        var { name, value } = attr;
        if (/^(?:class|style|src)$/i.test(name)) return;
        var key = name.replace(/^(ng|v|.*?)\-/i, "").toLowerCase();
        if (directives.hasOwnProperty(key) && isFunction(directives[key])) {
            directives[key].call(element, [withContext, value]);
        }
    });
    if (element.renders.length) {
        element.renderid = ++renderidOffset;
        onappend(element, addRenderElement);
        onremove(element, removeRenderElement);
        if (element.isMounted) addRenderElement.call(element);
    }
    return scope;
}
function renderStructure(element, scope, parentScopes = []) {
    // 处理结构流
    if (parentScopes !== null && !isArray(parentScopes)) {
        throw new Error('父级作用域链应以数组的类型传入');
    }
    element.$scope = scope;
    if (parentScopes) {
        if (element.renderid && !element.$parentScopes || element.$parentScopes && element.$parentScopes.length !== parentScopes.length) {
            return new Error("父作用域链的长度必须相等着");
        }
        element.$parentScopes = parentScopes;
    }
    var attrs = [].concat.apply([], element.attributes);
    var withContext = parentScopes ? parentScopes.map((_, cx) => `with(this.$parentScopes[${cx}])`).join("") : '';
    attrs.map(function (attr) {
        var { name, value } = attr;
        var key = name.replace(/^(ng|v|.*?)\-/i, "").toLowerCase();
        if (structures.hasOwnProperty(key) && isFunction(structures[key])) {
            if (element.renderid) {
                throw new Error(`请不要在同一元素上使用两次结构属性${attrs.map(a => a.name)}!`);
            }
            element.renderid = -1;
            structures[key].call(element, [withContext, value]);
        }
    });
}
function render(element, scope, parentScopes) {
    return renderElement(element, scope, parentScopes);
}

var digest = lazy(refresh);
render.digest = render.apply = render.refresh = digest;

var eventsHandlers = "change,paste,resize,keydown,keypress,keyup,mousedown,mouseup,touchend,touchcancel,touchstart,dragend,drop,click".split(",").map(k => on(k));
eventsHandlers.map(on => on(window, digest));<commit_msg>支持为元素初始化scope的对象<commit_after>

var hasOwnProperty = {}.hasOwnProperty;
var renderElements = {};
var renderidOffset = 0;
var renderidClosed = 0;
var addRenderElement = function () {
    var element = this;
    if (!isNode(element)) return;
    renderElements[element.renderid] = element;
    rebuild(element);
};
var removeRenderElement = function () {
    var element = this;
    delete renderElements[element.renderid];
};

function refresh() {
    for (var k in renderElements) {
        var element = renderElements[k];
        var props = extend({}, element);
        rebuild(element);
        var changes = getChanges(element, props);
        if (changes) {
            var event = createEvent('changes');
            event.changes = changes;
            dispatch(event, element);
        }
    }
}
function rebuild(element) {
    element.renders.forEach(a => a.call(element));
}
var createGetter = function (search) {
    var [withContext, searchContext] = search;
    return new Function(`try{${withContext}with(this.$scope)return ${searchContext}}catch(e){/*console.warn(String(e))*/}`);
};
var initialComment = function (renders, type, expression) {
    var comment = document.createComment(`${type} ${expression}`);
    comment.renders = renders;
    comment.renderid = ++renderidOffset;
    onappend(comment, addRenderElement);
    onremove(comment, removeRenderElement);
    appendChild.after(this, comment);
    remove(this);
    rebuild(comment);
    return comment;
};

var createRepeat = function (search) {
    // 懒渲染
    // throw new Error("repeat is not supported! use list component instead");

    var [context, expression] = search;
    var reg =
        // /////////////////////////////////////////// i //       r       /////////////////////////  o  ///// a ///////////////////// t /////
        /^(?:let\b|var\b|const\b)?\s*(?:[\(\{\[]\s*)?(.+?)((?:\s*,\s*.+?)*)?(?:\s*[\)\}\]]\s*|\s+)(in|of)\s+(.+?)(\s+?:track\s*by\s+(.+?))?$/i;
    var res = reg.exec(expression);
    if (!res) throw new Error(`no recognition for repeat expression: ${expression} `);
    var [_, i, k, r, s, t] = res;
    var keyName, itemName, indexName, trackBy = t, srcName = s;
    switch (r) {
        case "in":
            if (i) itemName = i;
            if (k) {
                var [keyName, indexName] = k.split(/,/).map(a => a.trim()).filter(a => !!a);
            }
            break;
        case "of":
            if (i) itemName = i;
            if (k) {
                keyName = i;
                var [itemName, indexName] = k.split(/,/).map(a => a.trim()).filter(a => !!a);
            }
            break;
    }
    // 懒渲染
    var getter = createGetter([context, srcName]).bind(this);
    var element = this, clonedElements = [], savedValue;
    var renders = [function () {
        var result = getter();
        result = result instanceof Array ? result.slice(0) : Object.assign({}, result);
        if (deepEqual.shallow(savedValue, result)) return;
        savedValue = result;
        remove(clonedElements);
        var keys = Object.keys(result);
        if (keys.length > 600) {
            throw new Error("数据量过大，取消绘制！");
        }
        var $parentScopes = element.$parentScopes || [];
        clonedElements = keys.map(function (key, cx) {
            var clone = element.cloneNode();
            clone.innerHTML = element.innerHTML;
            clone.renderid = 0;
            clone.$parentScopes = $parentScopes.concat(element.$scope);
            renderElement(clone, {
                [keyName || '$key']: key,
                [itemName || '$item']: result[key],
                [indexName || '$index']: cx
            }, clone.$parentScopes);
            return clone;
        }, this);
        appendChild.before(this, clonedElements);
    }];
    if (this.parentNode) {
        initialComment.call(this, renders, 'repeat', expression);
    } else {
        once("append")(this, initialComment.bind(this, renders, "repeat", expression));
    }
};
var structures = {
    "if"(search) {
        // 懒渲染
        var getter = createGetter(search).bind(this);
        var element = this;
        var renders = [function () {
            var result = getter();
            if (result) {
                if (!element.parentNode) appendChild.before(this, element);
                if (element.renderid < 0) {
                    element.renderid = 0;
                    renderElement(element);
                }
            } else {
                remove(element);
            }
        }];
        if (this.parentNode) {
            initialComment.call(this, renders, "if", search[1]);
        } else {
            once("append")(this, initialComment.bind(this, renders, "if", search[1]));
        }
    },
    repeat(search) {
        createRepeat.call(this, search);
    },
    for(search) {
        createRepeat.call(this, search);
    },
    each(search) {
        createRepeat.call(this, search);
    }
};
var directives = {
    click(search) {
        var getter = createGetter(search);
        onclick(this, getter);
    },
    src(search) {
        var getter = createGetter(search).bind(this);
        var oldValue, pending;
        var refresh = function () {
            that.src = oldValue;
            removeClass(that, "pending");
            pending = 0;
        };
        var img = document.createElement("img");
        var that = this;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(value, oldValue)) return;
            oldValue = value;
            value = value || "";
            if (!/img/i.test(this.tagName) || !isString(value)) return this.src = value;
            this.setAttribute("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=");
            img.src = value;
            if (img.complete) {
                this.src = value;
            } else if (!pending) {
                addClass(this, "pending");
                pending = setTimeout(refresh);
            }
        });
    },
    bind(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(value, oldValue)) return;
            oldValue = value;
            if (text(this) !== value) text(this, value);
        });
    },
    model(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        if (/^input$/i.test(this.tagName) && /^checkbox$/i.test(this.type) || /^checkbox$/i.test(this.tagName)) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                this.checked = value;
            });
            var change = new Function(`${search[0]}with(this.$scope)${search[1]}=this.checked`).bind(this);
        } else if (("value" in this || this.getValue instanceof Function) && this.setValue instanceof Function) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                if ((this.getValue instanceof Function ? this.getValue() : this.value) !== value) this.setValue(value);
            });
            var change = new Function(`${search[0]}with(this.$scope)${search[1]}=this.value`).bind(this);
        } else if (/^(select|input|textarea)$/i.test(this.tagName) || "value" in this) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                if (this.value !== value) this.value = value;
            });
            var change = new Function(`${search[0]}with(this.$scope)${search[1]}=this.value`).bind(this);
        } else {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (deepEqual(oldValue, value)) return;
                oldValue = value;
                if (html(this) !== value) html(this, value);
            });
            var change = new Function("html", `${search[0]}with(this.$scope)${search[1]}=html(this)`).bind(this, html);
        }
        var onchange = lazy(change);
        eventsHandlers.map(on => on(this, onchange));
    },
    hide(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(oldValue, value)) return;
            oldValue = value;
            if (value) {
                this.style.display = "none";
            } else {
                this.style.display = "";
            }
        });
    },
    show(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var value = getter();
            if (deepEqual(oldValue, value)) return;
            oldValue = value;
            if (value) {
                this.style.display = "";
            } else {
                this.style.display = "none";
            }
        });
    },

    "class"(search) {
        var getter = createGetter(search).bind(this);
        var generatedClassNames = {};
        var oldValue;
        this.renders.push(function () {
            var className = getter();
            if (deepEqual(oldValue, className)) return;
            oldValue = className;
            var originalClassNames = [];
            this.className.split(/\s+/).map(function (k) {
                if (k && !hasOwnProperty.call(generatedClassNames, k) && !hasOwnProperty.call(originalClassNames, k)) {
                    if (!/^\d+$/.test(k)) originalClassNames.push(originalClassNames[k] = k);
                }
            });
            var deltaClassNames = [];
            if (isString(className)) {
                className.split(/\s+/).map(function (k) {
                    if (!hasOwnProperty.call(originalClassNames, k)) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                });
            } else if (className instanceof Object) {
                for (var k in className) {
                    if (!hasOwnProperty.call(originalClassNames, k) && className[k]) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                }
            }
            var destClassName = originalClassNames.concat(deltaClassNames).join(" ");
            generatedClassNames = deltaClassNames;
            if (this.className !== destClassName) {
                this.className = destClassName;
            }
        });
    },
    style(search) {
        var getter = createGetter(search).bind(this);
        var oldValue;
        this.renders.push(function () {
            var stylesheet = getter();
            if (deepEqual(oldValue, stylesheet)) return;
            oldValue = stylesheet;
            if (isString(stylesheet)) {
                stylesheet.replace(/[\s\u00a0]+/g, "").split(/;/).map(function (kv) {
                    var [k, v] = kv.split(":");
                    if (this.style[k] !== v) {
                        this.style[k] = v;
                    }
                }, this);
            } else if (stylesheet instanceof Object) {
                for (var k in stylesheet) {
                    if (this.style[k] !== stylesheet[k]) {
                        this.style[k] = stylesheet[k];
                    }
                }
            }
        });
    }
};

function renderElement(element, scope = element.$scope, parentScopes = element.$parentScopes) {
    var children = element.children;
    if (!children) {
        return [].concat.apply([], element).map(function (element) {
            return renderElement(element, scope, parentScopes);
        });
    }
    if (!isNumber(element.renderid)) {
        renderStructure(element, scope, parentScopes);
    }
    var elementid = element.getAttribute("renderid") || element.getAttribute("elementid") || element.getAttribute("id");
    if (elementid && scope[elementid]) {
        throw new Error("同一个id不能使用两次:" + elementid);
    }
    if (elementid) scope[elementid] = element;
    if (element.renderid < 0) {
        return scope;
    }
    if (children.length) renderElement(children, scope, parentScopes);
    if (element.renderid) return scope;
    element.renderid = -2;
    var attrs = [].concat.apply([], element.attributes);
    var { tagName, parentNode, nextSibling } = element;
    if (parentNode) {
        if (!scope[tagName]) tagName = tagName.toLowerCase();
        if (!scope[tagName])
            tagName = tagName.replace(/(?:^|\-+)([a-z])/ig, (_, w) => w.toUpperCase());
        if (!scope[tagName]) tagName = tagName.slice(0, 1).toLowerCase() + tagName.slice(1);
        if (isFunction(scope[tagName])) var replacer = scope[tagName](element);
        if (isElement(replacer) && element !== replacer) {
            if (nextSibling) appendChild.before(nextSibling, replacer);
            else appendChild(parentNode, replacer);
            if (element.parentNode === parentNode) remove(element);
            attrs.map(function (attr) {
                var { name, value } = attr;
                switch (name.toLowerCase()) {
                    case "class":
                        addClass(replacer, value);
                        break;
                    case "style":
                        css(replacer, value);
                        break;
                    case "src":
                    case "placeholder":
                        replacer[name] = value;
                        break;
                    default:
                        if (!/[\-]/.test(name)) {
                            replacer.setAttribute(name, value);
                        }
                }
            });
            element = replacer;
            element.$scope = scope;
        }
    }
    element.renders = [];
    var withContext = parentScopes ? parentScopes.map((_, cx) => `with(this.$parentScopes[${cx}])`).join("") : '';
    attrs.map(function (attr) {
        var { name, value } = attr;
        if (/^(?:class|style|src)$/i.test(name)) return;
        var key = name.replace(/^(ng|v|.*?)\-/i, "").toLowerCase();
        if (directives.hasOwnProperty(key) && isFunction(directives[key])) {
            directives[key].call(element, [withContext, value]);
        }
    });
    if (element.renders.length) {
        element.renderid = ++renderidOffset;
        onappend(element, addRenderElement);
        onremove(element, removeRenderElement);
        if (element.isMounted) addRenderElement.call(element);
    }
    if (elementid) scope[elementid] = element;
    return scope;
}
function renderStructure(element, scope, parentScopes = []) {
    // 处理结构流
    if (parentScopes !== null && !isArray(parentScopes)) {
        throw new Error('父级作用域链应以数组的类型传入');
    }
    element.$scope = scope;
    if (parentScopes) {
        if (element.renderid && !element.$parentScopes || element.$parentScopes && element.$parentScopes.length !== parentScopes.length) {
            return new Error("父作用域链的长度必须相等着");
        }
        element.$parentScopes = parentScopes;
    }
    var attrs = [].concat.apply([], element.attributes);
    var withContext = parentScopes ? parentScopes.map((_, cx) => `with(this.$parentScopes[${cx}])`).join("") : '';
    attrs.map(function (attr) {
        var { name, value } = attr;
        var key = name.replace(/^(ng|v|.*?)\-/i, "").toLowerCase();
        if (structures.hasOwnProperty(key) && isFunction(structures[key])) {
            if (element.renderid) {
                throw new Error(`请不要在同一元素上使用两次结构属性${attrs.map(a => a.name)}!`);
            }
            element.renderid = -1;
            structures[key].call(element, [withContext, value]);
        }
    });
}
function render(element, scope, parentScopes) {
    return renderElement(element, scope, parentScopes);
}

var digest = lazy(refresh);
render.digest = render.apply = render.refresh = digest;

var eventsHandlers = "change,paste,resize,keydown,keypress,keyup,mousedown,mouseup,touchend,touchcancel,touchstart,dragend,drop,click".split(",").map(k => on(k));
eventsHandlers.map(on => on(window, digest));<|endoftext|>


<commit_before>

var renderElements = {};
var renderidOffset = 0;
var addRenderElement = function () {
    var element = this;
    if (!isNode(element)) return;
    if (!element.renderid) {
        element.renderid = ++renderidOffset;
    }
    renderElements[element.renderid] = element;
};
var removeRenderElement = function () {
    var element = this;
    delete renderElements[element.renderid];
};
function refresh() {
    for (var k in renderElements) {
        var element = renderElements[k];
        rebuild(element);
    }
}
function rebuild(element) {
    element.renders.map(a => a.call(element));
}
var createGetter = function (search) {
    return new Function(`try{with(this.$scope)return ${search}}catch(e){/*console.warn(String(e))*/}`);
};
var directives = {
    click(search) {
        var getter = createGetter(search);
        onclick(this, getter);
    },
    src(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            var value = getter() || "";
            if (this.src !== value) this.src = value;
        });
    },
    bind(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            var value = getter();
            if (text(this) !== value) text(this, value);
        });
    },
    model(search) {
        var getter = createGetter(search).bind(this);
        if (/^input$/i.test(this.tagName) && /^checkbox$/i.test(this.type) || /^checkbox$/i.test(this.tagName)) {
            this.renders.push(function (event) {
                var value = getter();
                if (value === undefined) value = "";
                if (this.checked != value) this.checked = value;
            });
            var change = new Function(`with(this.$scope)${search}=this.checked`).bind(this);
        } else if (/^(select|input|textarea)$/i.test(this.tagName)) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (this.value != value) this.value = value;

            });
            var change = new Function(`with(this.$scope)${search}=this.value`).bind(this);
        } else {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (html(this) != value) html(this, value);
            });
            var change = new Function("html", `with(this.$scope)${search}=html(this)`).bind(this, html);
        }
        var onchange = lazy(change);
        eventsHandlers.map(on => on(this, onchange));
    },
    hide(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            if (getter()) {
                this.style.display = "none";
            } else {
                this.style.display = "";
            }
        });
    },
    show(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            if (getter()) {
                this.style.display = "";
            } else {
                this.style.display = "none";
            }
        });
    },
    "if"(search) {
        // 懒渲染
        var getter = createGetter(search).bind(this);
        var cancelonappend = onappend(this, function () {
            cancelonappend();
            var comment = document.createComment("-if:" + search);
            comment.renders = [function () {
                if (getter()) {
                    if (!this.parentNode) appendChild.before(comment, this);
                } else {
                    remove(this);
                }
            }.bind(this)];
            onappend(comment, addRenderElement);
            onremove(comment, removeRenderElement);
            appendChild.after(this, comment);
            rebuild(comment);
        });
    },
    repeat(search) {
        // 懒渲染
        throw new Error("repeat is not supported! use list component instead");
    },
    "class"(search) {
        var getter = createGetter(search).bind(this);
        var generatedClassNames = {};
        this.renders.push(function () {
            var originalClassNames = [];
            this.className.split(/\s+/).map(function (k) {
                if (k && !hasOwnProperty.call(generatedClassNames, k) && !hasOwnProperty.call(originalClassNames, k)) {
                    if (!/^\d+$/.test(k)) originalClassNames.push(originalClassNames[k] = k);
                }
            });
            var className = getter();
            var deltaClassNames = [];
            if (isString(className)) {
                className.split(/\s+/).map(function (k) {
                    if (!hasOwnProperty.call(originalClassNames, k)) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                });
            } else if (className instanceof Object) {
                for (var k in className) {
                    if (!hasOwnProperty.call(originalClassNames, k) && className[k]) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                }
            }
            var destClassName = originalClassNames.concat(deltaClassNames).join(" ");
            generatedClassNames = deltaClassNames;
            if (this.className !== destClassName) {
                this.className = destClassName;
            }
        });
    },
    style(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            var stylesheet = getter();
            if (isString(stylesheet)) {
                stylesheet.replace(/[\s\u00a0]+/g, "").split(/;/).map(function (kv) {
                    var [k, v] = kv.split(":");
                    if (this.style[k] !== v) {
                        this.style[k] = v;
                    }
                }, this);
            } else if (stylesheet instanceof Object) {
                for (var k in stylesheet) {
                    if (this.style[k] !== stylesheet[k]) {
                        this.style[k] = stylesheet[k];
                    }
                }
            }
        });
    }
};
function renderElement(element, scope) {
    var children = element.children;
    if (!children) {
        return [].slice.call(element, 0).map(function (element) {
            return renderElement(element, scope);
        });
    }
    element.$scope = scope;
    if (element.renderid) return;
    var attrs = element.attributes;
    var { tagName, parentNode, previousSibling } = element;
    if (parentNode) {
        if (!scope[tagName]) tagName = tagName.toLowerCase();
        if (!scope[tagName])
            tagName = tagName.replace(/(?:^|\-+)([a-z])/ig, (_, w) => w.toUpperCase());
        if (!scope[tagName]) tagName = tagName.slice(0, 1).toLowerCase() + tagName.slice(1);
        if (isFunction(scope[tagName])) var replacer = scope[tagName](element);
        if (isElement(replacer) && element !== replacer) {
            if (previousSibling) appendChild.after(replacer, previousSibling);
            else appendChild(parentNode, replacer);
            if (element.parentNode === parentNode) remove(element);
            element = replacer;
            element.$scope = scope;
        }
    }
    element.renders = [];
    [].slice.call(attrs, 0).map(function (attr) {
        var { name, value } = attr;
        if (/^(?:class|style|src)$/i.test(name)) return;
        name = name.replace(/^(ng|v|.*?)\-/i, "").toLowerCase();
        if (directives.hasOwnProperty(name) && isFunction(directives[name])) {
            directives[name].call(element, value);
        }
    });
    if (children.length) renderElement(children, scope);
    if (element.renders.length) {
        onappend(element, addRenderElement);
        onremove(element, removeRenderElement);
        rebuild(element);
    }
}
function render(element, scope) {
    return renderElement(element, scope);
}

var digest = lazy(refresh);
render.digest = digest;

var eventsHandlers = "change,paste,resize,keydown,keypress,keyup,mousedown,mouseup,dragend,drop".split(",").map(k => on(k));
eventsHandlers.map(on => on(window, digest));<commit_msg>appendChild.after传参顺序的问题修复<commit_after>

var renderElements = {};
var renderidOffset = 0;
var addRenderElement = function () {
    var element = this;
    if (!isNode(element)) return;
    if (!element.renderid) {
        element.renderid = ++renderidOffset;
    }
    renderElements[element.renderid] = element;
};
var removeRenderElement = function () {
    var element = this;
    delete renderElements[element.renderid];
};
function refresh() {
    for (var k in renderElements) {
        var element = renderElements[k];
        rebuild(element);
    }
}
function rebuild(element) {
    element.renders.map(a => a.call(element));
}
var createGetter = function (search) {
    return new Function(`try{with(this.$scope)return ${search}}catch(e){/*console.warn(String(e))*/}`);
};
var directives = {
    click(search) {
        var getter = createGetter(search);
        onclick(this, getter);
    },
    src(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            var value = getter() || "";
            if (this.src !== value) this.src = value;
        });
    },
    bind(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            var value = getter();
            if (text(this) !== value) text(this, value);
        });
    },
    model(search) {
        var getter = createGetter(search).bind(this);
        if (/^input$/i.test(this.tagName) && /^checkbox$/i.test(this.type) || /^checkbox$/i.test(this.tagName)) {
            this.renders.push(function (event) {
                var value = getter();
                if (value === undefined) value = "";
                if (this.checked != value) this.checked = value;
            });
            var change = new Function(`with(this.$scope)${search}=this.checked`).bind(this);
        } else if (/^(select|input|textarea)$/i.test(this.tagName)) {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (this.value != value) this.value = value;

            });
            var change = new Function(`with(this.$scope)${search}=this.value`).bind(this);
        } else {
            this.renders.push(function () {
                var value = getter();
                if (value === undefined) value = "";
                if (html(this) != value) html(this, value);
            });
            var change = new Function("html", `with(this.$scope)${search}=html(this)`).bind(this, html);
        }
        var onchange = lazy(change);
        eventsHandlers.map(on => on(this, onchange));
    },
    hide(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            if (getter()) {
                this.style.display = "none";
            } else {
                this.style.display = "";
            }
        });
    },
    show(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            if (getter()) {
                this.style.display = "";
            } else {
                this.style.display = "none";
            }
        });
    },
    "if"(search) {
        // 懒渲染
        var getter = createGetter(search).bind(this);
        var cancelonappend = onappend(this, function () {
            cancelonappend();
            var comment = document.createComment("-if:" + search);
            comment.renders = [function () {
                if (getter()) {
                    if (!this.parentNode) appendChild.before(comment, this);
                } else {
                    remove(this);
                }
            }.bind(this)];
            onappend(comment, addRenderElement);
            onremove(comment, removeRenderElement);
            appendChild.after(this, comment);
            rebuild(comment);
        });
    },
    repeat(search) {
        // 懒渲染
        throw new Error("repeat is not supported! use list component instead");
    },
    "class"(search) {
        var getter = createGetter(search).bind(this);
        var generatedClassNames = {};
        this.renders.push(function () {
            var originalClassNames = [];
            this.className.split(/\s+/).map(function (k) {
                if (k && !hasOwnProperty.call(generatedClassNames, k) && !hasOwnProperty.call(originalClassNames, k)) {
                    if (!/^\d+$/.test(k)) originalClassNames.push(originalClassNames[k] = k);
                }
            });
            var className = getter();
            var deltaClassNames = [];
            if (isString(className)) {
                className.split(/\s+/).map(function (k) {
                    if (!hasOwnProperty.call(originalClassNames, k)) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                });
            } else if (className instanceof Object) {
                for (var k in className) {
                    if (!hasOwnProperty.call(originalClassNames, k) && className[k]) {
                        if (!/^\d+$/.test(k)) deltaClassNames.push(deltaClassNames[k] = k);
                    }
                }
            }
            var destClassName = originalClassNames.concat(deltaClassNames).join(" ");
            generatedClassNames = deltaClassNames;
            if (this.className !== destClassName) {
                this.className = destClassName;
            }
        });
    },
    style(search) {
        var getter = createGetter(search).bind(this);
        this.renders.push(function () {
            var stylesheet = getter();
            if (isString(stylesheet)) {
                stylesheet.replace(/[\s\u00a0]+/g, "").split(/;/).map(function (kv) {
                    var [k, v] = kv.split(":");
                    if (this.style[k] !== v) {
                        this.style[k] = v;
                    }
                }, this);
            } else if (stylesheet instanceof Object) {
                for (var k in stylesheet) {
                    if (this.style[k] !== stylesheet[k]) {
                        this.style[k] = stylesheet[k];
                    }
                }
            }
        });
    }
};
function renderElement(element, scope) {
    var children = element.children;
    if (!children) {
        return [].slice.call(element, 0).map(function (element) {
            return renderElement(element, scope);
        });
    }
    element.$scope = scope;
    if (element.renderid) return;
    var attrs = element.attributes;
    var { tagName, parentNode, previousSibling } = element;
    if (parentNode) {
        if (!scope[tagName]) tagName = tagName.toLowerCase();
        if (!scope[tagName])
            tagName = tagName.replace(/(?:^|\-+)([a-z])/ig, (_, w) => w.toUpperCase());
        if (!scope[tagName]) tagName = tagName.slice(0, 1).toLowerCase() + tagName.slice(1);
        if (isFunction(scope[tagName])) var replacer = scope[tagName](element);
        if (isElement(replacer) && element !== replacer) {
            if (previousSibling) appendChild.after(previousSibling, replacer);
            else appendChild(parentNode, replacer);
            if (element.parentNode === parentNode) remove(element);
            element = replacer;
            element.$scope = scope;
        }
    }
    element.renders = [];
    [].slice.call(attrs, 0).map(function (attr) {
        var { name, value } = attr;
        if (/^(?:class|style|src)$/i.test(name)) return;
        name = name.replace(/^(ng|v|.*?)\-/i, "").toLowerCase();
        if (directives.hasOwnProperty(name) && isFunction(directives[name])) {
            directives[name].call(element, value);
        }
    });
    if (children.length) renderElement(children, scope);
    if (element.renders.length) {
        onappend(element, addRenderElement);
        onremove(element, removeRenderElement);
        rebuild(element);
    }
}
function render(element, scope) {
    return renderElement(element, scope);
}

var digest = lazy(refresh);
render.digest = digest;

var eventsHandlers = "change,paste,resize,keydown,keypress,keyup,mousedown,mouseup,dragend,drop".split(",").map(k => on(k));
eventsHandlers.map(on => on(window, digest));<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var Promise = require('es6-promise').Promise;

var Store = require('./store');
var validate = require('../validate');
var Prompt = require('../cli/prompt');

var targetMap = require('./map');
var Target = require('./target');

var link = exports;
link.output = {};

var CREATE_ORG_VALUE = 'Create New Org';
var CREATE_PROJECT_VALUE = 'Create New Project';

/* eslint-disable consistent-return */
link.output.success = function (ctx, target) {
  var programName = ctx.program.name;

  if (ctx.target && ctx.target.disabled()) {
    return console.log('\nContext is disabled for your CLI,' +
      ' use \'' + programName + ' prefs\' to enabled it\n');
  }

  console.log('\nYour current working directory and all sub directories have ' +
              'been linked to:\n');

  console.log('Org: ' + target.org);
  console.log('Project: ' + target.project + '\n');

  console.log('Use \'' + programName + ' status\' to display ' +
              'your current working context.\n');

  if (target.service) {
    console.log('A "' + target.service + '" service has been created for the new ' +
                '"' + target.project + '" project!');
  }
};

link.output.failure = function (ctx) {
  var programName = ctx.program.name;
  if (ctx.target && ctx.target.disabled()) {
    console.log('\nContext is disabled for your CLI,' +
      ' use \'' + programName + ' prefs\' to enabled it\n');
  } else {
    console.log('\nFailed to link your current working directory.\n');
  }
};
/* eslint-enable consistent-return */

link.execute = function (ctx) {
  return new Promise(function (resolve, reject) {
    var force = ctx.option('force').value;
    var shouldOverwrite = force !== false;

    // Return early when context disabled
    if (ctx.target.disabled()) {
      return resolve(ctx.target);
    }

    // Display linked org/project unless --force supplied
    if (!shouldOverwrite && ctx.target && ctx.target.exists()) {
      return resolve(ctx.target);
    }

    // Prompt for org and project
    var store = new Store(ctx.api);
    return link._prompt(store).then(function (answers) {
      // Retrieve data objects for supplied values
      return link._retrieveObjects(store, answers);
    }).then(function (objects) {
      // Create target from inputs
      var target = new Target({
        path: ctx.target.path(),
        context: {
          org: objects.org.body.name,
          project: objects.project.body.name,
          service: _.get(objects, 'service.body.name', null)
        }
      });

      // Link the current directory
      return targetMap.link(target).then(function () {
        return resolve(target);
      });
    })
    .catch(reject);
  });
};

/**
 * Retrieve or create the org, project objects using the store
 * and answers we got from the user.
 *
 * @param {Store} store
 * @param {Object} answers
 * @returns {Promise}
 */
link._retrieveObjects = function (store, answers) {
  var getOrg = (answers.org) ?
    Promise.resolve(answers.org) : store.create('orgs', {
      name: answers.orgName
    });

  return getOrg.then(function (org) {
    var getProject = (answers.project) ?
      Promise.resolve(answers.project) : store.create('projects', {
        name: answers.projectName,
        org_id: org.id
      });

    return getProject.then(function (project) {
      // If they selected a pre-existing project or used the ---bare flag then
      // don't create a default service.
      if (!answers.createService) {
        return {
          org: org,
          project: project,
          service: null
        };
      }

      return store.create('services', {
        name: 'default',
        org_id: org.id,
        project_id: project.id
      }).then(function (service) {
        return {
          org: org,
          project: project,
          service: service
        };
      });
    });
  });
};

link._prompt = function (store) {
  var prompt = new Prompt({
    stages: link._questions,
    questionArgs: [
      store
    ]
  });

  return prompt.start();
};

link._questions = function (store) {
  // We use state to keep track of the accrued objects so we don't have to keep
  // calling into the store since we can't access the answers hash in a filter.
  var state = {};

  // Use when, filter, and choices along with the store (cache) to figure out
  // which objects we're trying to link.
  //
  // Keep track of the state in the answers hash. If we take in a name we store
  // it in a different property of the answers hash so we can take advantage of
  // validation.
  return [
    {
      type: 'list',
      name: 'org',
      message: 'Select an org',
      choices: function () {
        return store.get('orgs').then(function (orgs) {
          var choices = orgs.map(function (org) {
            return org.body.name;
          });

          choices.push(CREATE_ORG_VALUE);

          return choices;
        });
      },
      filter: function (orgName) {
        if (orgName === CREATE_ORG_VALUE) {
          return undefined;
        }

        var filter = { body: { name: orgName } };
        return store.get('orgs', filter).then(function (orgs) {
          if (orgs.length !== 1) {
            throw new Error('org not found: ' + orgName);
          }

          state.org = orgs[0];
          return state.org;
        });
      }
    },
    {
      type: 'input',
      name: 'orgName',
      message: 'New org name?',
      validate: validate.slug,
      when: function (answers) {
        return (answers.org === undefined);
      }
    },
    {
      type: 'list',
      name: 'project',
      message: 'Select a project',
      choices: function (answers) {
        var filter = { body: { org_id: answers.org.id } };
        return store.get('projects', filter).then(function (projects) {
          var choices = projects.map(function (project) {
            return project.body.name;
          });

          choices.push(CREATE_PROJECT_VALUE);
          return choices;
        });
      },
      filter: function (projectName) {
        if (projectName === CREATE_PROJECT_VALUE) {
          return undefined;
        }

        var filter = {
          body: {
            org_id: state.org.id,
            name: projectName
          }
        };
        return store.get('projects', filter).then(function (projects) {
          if (projects.length !== 1) {
            throw new Error('project not found: ' + projectName);
          }

          state.project = projects[0];
          return state.project;
        });
      },
      when: function (answers) {
        return (answers.org !== undefined);
      }
    },
    {
      type: 'input',
      name: 'projectName',
      message: 'New project name?',
      validate: validate.slug,
      when: function (answers) {
        return (answers.project === undefined);
      }
    },
    {
      type: 'confirm',
      name: 'createService',
      message: 'Create a "default" service for your new project?',
      when: function (answers) {
        return (answers.project === undefined);
      }
    }
  ];
};
<commit_msg>Create keypairs during link if new org is created<commit_after>'use strict';

var _ = require('lodash');
var Promise = require('es6-promise').Promise;

var Store = require('./store');
var validate = require('../validate');
var Prompt = require('../cli/prompt');

var targetMap = require('./map');
var Target = require('./target');

var link = exports;
link.output = {};

var CREATE_ORG_VALUE = 'Create New Org';
var CREATE_PROJECT_VALUE = 'Create New Project';

/* eslint-disable consistent-return */
link.output.success = function (ctx, target) {
  var programName = ctx.program.name;

  if (ctx.target && ctx.target.disabled()) {
    return console.log('\nContext is disabled for your CLI,' +
      ' use \'' + programName + ' prefs\' to enabled it\n');
  }

  console.log('\nYour current working directory and all sub directories have ' +
              'been linked to:\n');

  console.log('Org: ' + target.org);
  console.log('Project: ' + target.project + '\n');

  console.log('Use \'' + programName + ' status\' to display ' +
              'your current working context.\n');

  if (target.service) {
    console.log('A "' + target.service + '" service has been created for the new ' +
                '"' + target.project + '" project!');
  }
};

link.output.failure = function (ctx) {
  var programName = ctx.program.name;
  if (ctx.target && ctx.target.disabled()) {
    console.log('\nContext is disabled for your CLI,' +
      ' use \'' + programName + ' prefs\' to enabled it\n');
  } else {
    console.log('\nFailed to link your current working directory.\n');
  }
};
/* eslint-enable consistent-return */

link.execute = function (ctx) {
  return new Promise(function (resolve, reject) {
    var force = ctx.option('force').value;
    var shouldOverwrite = force !== false;

    // Return early when context disabled
    if (ctx.target.disabled()) {
      return resolve(ctx.target);
    }

    // Display linked org/project unless --force supplied
    if (!shouldOverwrite && ctx.target && ctx.target.exists()) {
      return resolve(ctx.target);
    }

    // Prompt for org and project
    var store = new Store(ctx.api);
    return link._prompt(store).then(function (answers) {
      // Retrieve data objects for supplied values
      return link._retrieveObjects(ctx, store, answers);
    }).then(function (objects) {
      // Create target from inputs
      var target = new Target({
        path: ctx.target.path(),
        context: {
          org: objects.org.body.name,
          project: objects.project.body.name,
          service: _.get(objects, 'service.body.name', null)
        }
      });

      // Link the current directory
      return targetMap.link(target).then(function () {
        return resolve(target);
      });
    })
    .catch(reject);
  });
};

/**
 * Retrieve or create the org, project objects using the store
 * and answers we got from the user.
 *
 * @param {Context} ctx
 * @param {Store} store
 * @param {Object} answers
 * @returns {Promise}
 */
link._retrieveObjects = function (ctx, store, answers) {
  var getOrg = (answers.org) ?
    Promise.resolve(answers.org) : store.create('orgs', {
      name: answers.orgName
    })
    .then(function (org) {
      return ctx.api.keypairs.generate({ org_id: org.id })
      .then(function () {
        return org;
      });
    });

  return getOrg.then(function (org) {
    var getProject = (answers.project) ?
      Promise.resolve(answers.project) : store.create('projects', {
        name: answers.projectName,
        org_id: org.id
      });

    return getProject.then(function (project) {
      // If they selected a pre-existing project or used the ---bare flag then
      // don't create a default service.
      if (!answers.createService) {
        return {
          org: org,
          project: project,
          service: null
        };
      }

      return store.create('services', {
        name: 'default',
        org_id: org.id,
        project_id: project.id
      }).then(function (service) {
        return {
          org: org,
          project: project,
          service: service
        };
      });
    });
  });
};

link._prompt = function (store) {
  var prompt = new Prompt({
    stages: link._questions,
    questionArgs: [
      store
    ]
  });

  return prompt.start();
};

link._questions = function (store) {
  // We use state to keep track of the accrued objects so we don't have to keep
  // calling into the store since we can't access the answers hash in a filter.
  var state = {};

  // Use when, filter, and choices along with the store (cache) to figure out
  // which objects we're trying to link.
  //
  // Keep track of the state in the answers hash. If we take in a name we store
  // it in a different property of the answers hash so we can take advantage of
  // validation.
  return [
    {
      type: 'list',
      name: 'org',
      message: 'Select an org',
      choices: function () {
        return store.get('orgs').then(function (orgs) {
          var choices = orgs.map(function (org) {
            return org.body.name;
          });

          choices.push(CREATE_ORG_VALUE);

          return choices;
        });
      },
      filter: function (orgName) {
        if (orgName === CREATE_ORG_VALUE) {
          return undefined;
        }

        var filter = { body: { name: orgName } };
        return store.get('orgs', filter).then(function (orgs) {
          if (orgs.length !== 1) {
            throw new Error('org not found: ' + orgName);
          }

          state.org = orgs[0];
          return state.org;
        });
      }
    },
    {
      type: 'input',
      name: 'orgName',
      message: 'New org name?',
      validate: validate.slug,
      when: function (answers) {
        return (answers.org === undefined);
      }
    },
    {
      type: 'list',
      name: 'project',
      message: 'Select a project',
      choices: function (answers) {
        var filter = { body: { org_id: answers.org.id } };
        return store.get('projects', filter).then(function (projects) {
          var choices = projects.map(function (project) {
            return project.body.name;
          });

          choices.push(CREATE_PROJECT_VALUE);
          return choices;
        });
      },
      filter: function (projectName) {
        if (projectName === CREATE_PROJECT_VALUE) {
          return undefined;
        }

        var filter = {
          body: {
            org_id: state.org.id,
            name: projectName
          }
        };
        return store.get('projects', filter).then(function (projects) {
          if (projects.length !== 1) {
            throw new Error('project not found: ' + projectName);
          }

          state.project = projects[0];
          return state.project;
        });
      },
      when: function (answers) {
        return (answers.org !== undefined);
      }
    },
    {
      type: 'input',
      name: 'projectName',
      message: 'New project name?',
      validate: validate.slug,
      when: function (answers) {
        return (answers.project === undefined);
      }
    },
    {
      type: 'confirm',
      name: 'createService',
      message: 'Create a "default" service for your new project?',
      when: function (answers) {
        return (answers.project === undefined);
      }
    }
  ];
};
<|endoftext|>


<commit_before>'use strict';

var fs = require('fs');
var path = require('path');
var childProcess = require('child_process');

var _ = require('lodash');
var Promise = require('es6-promise').Promise;
var Backoff = require('backo');

var Daemon = require('./object').Daemon;
var Config = require('../config');

var daemon = exports;

// The daemon binary is placed in the cli's `bin` folder during the build
// process.
daemon.DAEMON_PATH = path.join(__dirname, '../../bin/ag-daemon');

/**
 * Retrieves a deamon object, returns null if the daemon is not running
 *
 * @param cfg {Config}
 * @returns Promise
 */
daemon.get = function (cfg) {
  return new Promise(function (resolve, reject) {
    if (!(cfg instanceof Config)) {
      return reject(new TypeError('cfg must be a Config object'));
    }

    return daemon.status(cfg).then(function (status) {
      if (!status.exists) {
        return resolve(null);
      }

      var d = new Daemon(cfg);
      return d.connect().then(function () {
        resolve(d);
      }).catch(reject);
    }).catch(reject);
  });
};

/**
 * Starts the daemon and returns a daemon instance. Rejects the promise with an
 * error if the daemon is already running
 *
 * @param cfg {Config}
 * @returns Promise
 */
daemon.start = function (cfg) {
  return new Promise(function (resolve, reject) {
    if (!(cfg instanceof Config)) {
      return reject(new TypeError('cfg must be a Config object'));
    }

    function retry(backoff) {
      if (backoff.attempts > 3) {
        return reject(new Error('Daemon did not start'));
      }

      return setTimeout(function () {
        daemon.get(cfg).then(function (d) {
          if (!d) {
            return retry(backoff);
          }

          return resolve(d);
        }).catch(reject);
      }, backoff.duration());
    }

    return daemon.status(cfg).then(function (status) {
      if (status.exists) {
        return reject(new Error('Daemon is already running'));
      }

      var env = _.clone(process.env);
      env.ARIGATO_ROOT = cfg.arigatoRoot;

      var opt = {
        stdio: ['ignore', 'ignore', 'ignore'],
        env: env,
        cwd: cfg.arigatoRoot,
        detached: true
      };

      // TODO: We should be logging the pid and other information
      var child = childProcess.spawn(daemon.DAEMON_PATH, [], opt);
      child.unref();

      // XXX: Wait a second before trying to connect to the daemon
      var backoff = new Backoff({
        min: 100, max: 1000, jitter: 0.75, factor: 2 });
      return retry(backoff);
    }).catch(reject);
  });
};

/**
 * Stops the currently running daemon. Rejects the promise with an error if the
 * daemon is not running.
 *
 * @param cfg {Config}
 * @returns Promise
 */
daemon.stop = function (cfg) {
  return new Promise(function (resolve, reject) {
    if (!(cfg instanceof Config)) {
      return reject(new TypeError('cfg must be a Config object'));
    }

    return daemon.status(cfg).then(function (status) {
      if (!status.exists) {
        return reject(new Error('Daemon is not running'));
      }

      // Send a SIGTERM and let the daemon shutdown gracefully.
      //
      // TODO: Add a grace period, if it's still alive after N seconds then
      // send a SIGKILL.
      try {
        process.kill(status.pid, 'SIGTERM');
      } catch (err) {
        if (err.code === 'ESRCH') {
          return reject(new Error('Unknown pid cannot kill: ' + status.pid));
        }

        return reject(err);
      }

      return resolve();
    });
  });
};

daemon.status = function (cfg) {
  return new Promise(function (resolve, reject) {
    fs.readFile(cfg.pidPath, 'utf-8', function (err, pid) {
      if (err) {
        if (err && err.code === 'ENOENT') {
          return resolve({ exists: false, pid: null });
        }

        return reject(err);
      }

      pid = parseInt(pid, 10);
      if (_.isNaN(pid)) {
        return reject(new Error('Invalid pid in file ' + cfg.pidPath));
      }

      var exists = true;
      try {
        // Process.kill sends a signal, if you set the vlaue to 0 it
        // just checks if the process exists.
        //
        // An error is thrown if the process does not exist.
        process.kill(pid, 0);
      } catch (e) {
        if (e.code !== 'ESRCH') {
          return reject(e);
        }

        exists = false;
        pid = null;
      }

      return resolve({ exists: exists, pid: pid });
    });
  });
};
<commit_msg>Fix daemon typo<commit_after>'use strict';

var fs = require('fs');
var path = require('path');
var childProcess = require('child_process');

var _ = require('lodash');
var Promise = require('es6-promise').Promise;
var Backoff = require('backo');

var Daemon = require('./object').Daemon;
var Config = require('../config');

var daemon = exports;

// The daemon binary is placed in the cli's `bin` folder during the build
// process.
daemon.DAEMON_PATH = path.join(__dirname, '../../bin/ag-daemon');

/**
 * Retrieves a daemon object, returns null if the daemon is not running
 *
 * @param cfg {Config}
 * @returns Promise
 */
daemon.get = function (cfg) {
  return new Promise(function (resolve, reject) {
    if (!(cfg instanceof Config)) {
      return reject(new TypeError('cfg must be a Config object'));
    }

    return daemon.status(cfg).then(function (status) {
      if (!status.exists) {
        return resolve(null);
      }

      var d = new Daemon(cfg);
      return d.connect().then(function () {
        resolve(d);
      }).catch(reject);
    }).catch(reject);
  });
};

/**
 * Starts the daemon and returns a daemon instance. Rejects the promise with an
 * error if the daemon is already running
 *
 * @param cfg {Config}
 * @returns Promise
 */
daemon.start = function (cfg) {
  return new Promise(function (resolve, reject) {
    if (!(cfg instanceof Config)) {
      return reject(new TypeError('cfg must be a Config object'));
    }

    function retry(backoff) {
      if (backoff.attempts > 3) {
        return reject(new Error('Daemon did not start'));
      }

      return setTimeout(function () {
        daemon.get(cfg).then(function (d) {
          if (!d) {
            return retry(backoff);
          }

          return resolve(d);
        }).catch(reject);
      }, backoff.duration());
    }

    return daemon.status(cfg).then(function (status) {
      if (status.exists) {
        return reject(new Error('Daemon is already running'));
      }

      var env = _.clone(process.env);
      env.ARIGATO_ROOT = cfg.arigatoRoot;

      var opt = {
        stdio: ['ignore', 'ignore', 'ignore'],
        env: env,
        cwd: cfg.arigatoRoot,
        detached: true
      };

      // TODO: We should be logging the pid and other information
      var child = childProcess.spawn(daemon.DAEMON_PATH, [], opt);
      child.unref();

      // XXX: Wait a second before trying to connect to the daemon
      var backoff = new Backoff({
        min: 100, max: 1000, jitter: 0.75, factor: 2 });
      return retry(backoff);
    }).catch(reject);
  });
};

/**
 * Stops the currently running daemon. Rejects the promise with an error if the
 * daemon is not running.
 *
 * @param cfg {Config}
 * @returns Promise
 */
daemon.stop = function (cfg) {
  return new Promise(function (resolve, reject) {
    if (!(cfg instanceof Config)) {
      return reject(new TypeError('cfg must be a Config object'));
    }

    return daemon.status(cfg).then(function (status) {
      if (!status.exists) {
        return reject(new Error('Daemon is not running'));
      }

      // Send a SIGTERM and let the daemon shutdown gracefully.
      //
      // TODO: Add a grace period, if it's still alive after N seconds then
      // send a SIGKILL.
      try {
        process.kill(status.pid, 'SIGTERM');
      } catch (err) {
        if (err.code === 'ESRCH') {
          return reject(new Error('Unknown pid cannot kill: ' + status.pid));
        }

        return reject(err);
      }

      return resolve();
    });
  });
};

daemon.status = function (cfg) {
  return new Promise(function (resolve, reject) {
    fs.readFile(cfg.pidPath, 'utf-8', function (err, pid) {
      if (err) {
        if (err && err.code === 'ENOENT') {
          return resolve({ exists: false, pid: null });
        }

        return reject(err);
      }

      pid = parseInt(pid, 10);
      if (_.isNaN(pid)) {
        return reject(new Error('Invalid pid in file ' + cfg.pidPath));
      }

      var exists = true;
      try {
        // Process.kill sends a signal, if you set the vlaue to 0 it
        // just checks if the process exists.
        //
        // An error is thrown if the process does not exist.
        process.kill(pid, 0);
      } catch (e) {
        if (e.code !== 'ESRCH') {
          return reject(e);
        }

        exists = false;
        pid = null;
      }

      return resolve({ exists: exists, pid: pid });
    });
  });
};
<|endoftext|>


<commit_before>const path = require('path')
const debug = require('debug')
const log = debug('app:provider:file')
const { promisify } = require('util')
const fs = require('fs')
const stat = promisify(fs.stat)
const mp3duration = promisify(require('mp3-duration'))
const getFiles = require('./lib/getFiles')
const Scanner = require('../Scanner')
const Media = require('../../Media')
const parseMeta = require('../../lib/parseMeta')
const parseMetaCfg = require('./lib/parseMetaCfg') // look for .js, .json

const mediaExts = ['.cdg', '.mp4', '.m4v']
const mediaExtPerms = mediaExts.reduce((perms, ext) => perms.concat(getPerms(ext)), [])

const audioExts = ['.m4a', '.mp3']
const audioExtPerms = audioExts.reduce((perms, ext) => perms.concat(getPerms(ext)), [])

class FileScanner extends Scanner {
  constructor (prefs) {
    super()
    this.paths = prefs.paths || []
  }

  async scan () {
    const offlinePaths = []
    const validIds = [] // mediaIds for cleanup
    let files = []

    // emit start
    this.emitStatus('Gathering file list', 0)

    // count files to scan from all paths
    for (const p of this.paths) {
      try {
        log('Searching path: %s', p)
        let list = await getFiles(p)
        list = list.filter(file => mediaExtPerms.includes(path.extname(file)))

        log('  => found %s files with valid extensions (%s)', list.length, mediaExts.join(', '))
        files = files.concat(list)
      } catch (err) {
        log(`  => ${err.message} (path offline)`)
        offlinePaths.push(p)
      }

      if (this.isCanceling) {
        return
      }
    }

    log('Processing %s total files', files.length)

    // process files
    for (let i = 0; i < files.length; i++) {
      const file = files[i]
      const basePath = this.paths.find(p => file.indexOf(p) === 0)

      // emit progress
      log('[%s/%s] %s', i + 1, files.length, file)
      this.emitStatus(`Scanning media files (${i + 1} of ${files.length})`, ((i + 1) / files.length) * 100)

      try {
        const mediaId = await this.processFile({ file, basePath })

        // successfuly processed
        validIds.push(mediaId)
      } catch (err) {
        log(err)
        // just try the next file...
      }

      if (this.isCanceling) {
        return
      }
    } // end for

    // cleanup
    log('Cleanup: getting orphaned songs')

    try {
      // get all media from valid/online paths
      const res = await Media.searchMedia({
        provider: 'file',
        providerData: {
          basePath: this.paths.filter(p => !offlinePaths.includes(p)),
        }
      })

      // media we didn't encounter during the scan are invalid
      const invalidIds = res.result.filter(id => !validIds.includes(id))

      log('  => removing %s songs', invalidIds.length)
      await Media.remove(invalidIds)
    } catch (err) {
      log(err)
    }

    // done
  }

  async processFile ({ file, basePath }) {
    const media = {
      provider: 'file',
      providerData: {
        basePath,
        relPath: file.substr(basePath.length)
      }
    }

    // already in database with the same path?
    try {
      const res = await Media.searchMedia(media)
      log('  => %s result(s) for existing media', res.result.length)

      if (res.result.length) {
        log('  => found media in library (same path)')
        return Promise.resolve(res.result[0])
      }
    } catch (err) {
      return Promise.reject(err)
    }

    // @todo: calculate current metadata and update db if different
    // (there may be a new parser configuration, for example)

    // new media
    // -------------------------------

    // need to look for an audio file?
    if (path.extname(file).toLowerCase() === '.cdg') {
      let audioFile

      // look for all uppercase and lowercase permutations
      // since we may be on a case-sensitive fs
      for (const ext of audioExtPerms) {
        audioFile = file.substr(0, file.length - path.extname(file).length) + ext

        try {
          await stat(audioFile)

          // success
          log('  => found %s audio file', ext)
          media.providerData.audioExt = ext.toLowerCase()
          break
        } catch (err) {
          // keep looking...
        }
      } // end for

      // get audio duration
      try {
        if (media.providerData.audioExt === '.mp3') {
          log('  => getting duration (mp3-duration)')
          media.duration = await mp3duration(audioFile)
        }
      } catch (err) {
        log(err)
      }
    } else {
      // @todo: get video duration
    }

    if (!media.duration) {
      return Promise.reject(new Error(`Couldn't determine media duration`))
    }

    // try getting artist and title from filename
    const { artist, title } = parseMeta(path.parse(file).name, parseMetaCfg)

    media.artist = artist
    media.title = title

    // add song
    try {
      const mediaId = await Media.add(media)
      if (!Number.isInteger(mediaId)) {
        throw new Error('got invalid lastID')
      }

      this.emitLibrary()

      return Promise.resolve(mediaId)
    } catch (err) {
      return Promise.reject(err)
    }
  }
}

// if (typeof song !== 'object') {
//   // try parent folder?
//   log(`  => couldn't parse artist/title from filename; trying parent folder name`)
//   song = parseMeta(cdgPathInfo.dir.split(cdgPathInfo.sep).pop())
//
//   if (typeof song !== 'object') {
//     return Promise.reject(new Error(`couldn't parse artist/title`))
//   }
// }

module.exports = FileScanner

// return all uppercase and lowercase permutations of str
// based on https://stackoverflow.com/a/27995370
function getPerms (str) {
  var results = []
  var arr = str.split('')
  var len = Math.pow(arr.length, 2)

  for (var i = 0; i < len; i++) {
    for (var k = 0, j = i; k < arr.length; k++, j >>= 1) {
      arr[k] = (j & 1) ? arr[k].toUpperCase() : arr[k].toLowerCase()
    }
    var combo = arr.join('')
    results.push(combo)
  }
  // remove duplicates
  return results.filter((ext, pos, self) => self.indexOf(ext) === pos)
}
<commit_msg>provider(file): clarify log output<commit_after>const path = require('path')
const debug = require('debug')
const log = debug('app:provider:file')
const { promisify } = require('util')
const fs = require('fs')
const stat = promisify(fs.stat)
const mp3duration = promisify(require('mp3-duration'))
const getFiles = require('./lib/getFiles')
const Scanner = require('../Scanner')
const Media = require('../../Media')
const parseMeta = require('../../lib/parseMeta')
const parseMetaCfg = require('./lib/parseMetaCfg') // look for .js, .json

const mediaExts = ['.cdg', '.mp4', '.m4v']
const mediaExtPerms = mediaExts.reduce((perms, ext) => perms.concat(getPerms(ext)), [])

const audioExts = ['.m4a', '.mp3']
const audioExtPerms = audioExts.reduce((perms, ext) => perms.concat(getPerms(ext)), [])

class FileScanner extends Scanner {
  constructor (prefs) {
    super()
    this.paths = prefs.paths || []
  }

  async scan () {
    const offlinePaths = []
    const validIds = [] // mediaIds for cleanup
    let files = []

    // emit start
    this.emitStatus('Gathering file list', 0)

    // count files to scan from all paths
    for (const p of this.paths) {
      try {
        log('Searching path: %s', p)
        let list = await getFiles(p)
        list = list.filter(file => mediaExtPerms.includes(path.extname(file)))

        log('  => found %s files with valid extensions (%s)', list.length, mediaExts.join(', '))
        files = files.concat(list)
      } catch (err) {
        log(`  => ${err.message} (path offline)`)
        offlinePaths.push(p)
      }

      if (this.isCanceling) {
        return
      }
    }

    log('Processing %s total files', files.length)

    // process files
    for (let i = 0; i < files.length; i++) {
      const file = files[i]
      const basePath = this.paths.find(p => file.indexOf(p) === 0)

      // emit progress
      log('[%s/%s] %s', i + 1, files.length, file)
      this.emitStatus(`Scanning media files (${i + 1} of ${files.length})`, ((i + 1) / files.length) * 100)

      try {
        const mediaId = await this.processFile({ file, basePath })

        // successfuly processed
        validIds.push(mediaId)
      } catch (err) {
        log(err)
        // just try the next file...
      }

      if (this.isCanceling) {
        return
      }
    } // end for

    // cleanup
    log('cleanup: getting media entries no longer associated with a file')

    try {
      // get all media from valid/online paths
      const res = await Media.searchMedia({
        provider: 'file',
        providerData: {
          basePath: this.paths.filter(p => !offlinePaths.includes(p)),
        }
      })

      // media we didn't encounter during the scan are invalid
      const invalidIds = res.result.filter(id => !validIds.includes(id))

      log('  => removing %s songs', invalidIds.length)
      await Media.remove(invalidIds)
    } catch (err) {
      log(err)
    }

    // done
  }

  async processFile ({ file, basePath }) {
    const media = {
      provider: 'file',
      providerData: {
        basePath,
        relPath: file.substr(basePath.length)
      }
    }

    // already in database with the same path?
    try {
      const res = await Media.searchMedia(media)
      log('  => %s result(s) for existing media', res.result.length)

      if (res.result.length) {
        log('  => found media in library (same path)')
        return Promise.resolve(res.result[0])
      }
    } catch (err) {
      return Promise.reject(err)
    }

    // @todo: calculate current metadata and update db if different
    // (there may be a new parser configuration, for example)

    // new media
    // -------------------------------

    // need to look for an audio file?
    if (path.extname(file).toLowerCase() === '.cdg') {
      let audioFile

      // look for all uppercase and lowercase permutations
      // since we may be on a case-sensitive fs
      for (const ext of audioExtPerms) {
        audioFile = file.substr(0, file.length - path.extname(file).length) + ext

        try {
          await stat(audioFile)

          // success
          log('  => found %s audio file', ext)
          media.providerData.audioExt = ext.toLowerCase()
          break
        } catch (err) {
          // keep looking...
        }
      } // end for

      // get audio duration
      try {
        if (media.providerData.audioExt === '.mp3') {
          log('  => getting duration (mp3-duration)')
          media.duration = await mp3duration(audioFile)
        }
      } catch (err) {
        log(err)
      }
    } else {
      // @todo: get video duration
    }

    if (!media.duration) {
      return Promise.reject(new Error(`Couldn't determine media duration`))
    }

    // try getting artist and title from filename
    const { artist, title } = parseMeta(path.parse(file).name, parseMetaCfg)

    media.artist = artist
    media.title = title

    // add song
    try {
      const mediaId = await Media.add(media)
      if (!Number.isInteger(mediaId)) {
        throw new Error('got invalid lastID')
      }

      this.emitLibrary()

      return Promise.resolve(mediaId)
    } catch (err) {
      return Promise.reject(err)
    }
  }
}

// if (typeof song !== 'object') {
//   // try parent folder?
//   log(`  => couldn't parse artist/title from filename; trying parent folder name`)
//   song = parseMeta(cdgPathInfo.dir.split(cdgPathInfo.sep).pop())
//
//   if (typeof song !== 'object') {
//     return Promise.reject(new Error(`couldn't parse artist/title`))
//   }
// }

module.exports = FileScanner

// return all uppercase and lowercase permutations of str
// based on https://stackoverflow.com/a/27995370
function getPerms (str) {
  var results = []
  var arr = str.split('')
  var len = Math.pow(arr.length, 2)

  for (var i = 0; i < len; i++) {
    for (var k = 0, j = i; k < arr.length; k++, j >>= 1) {
      arr[k] = (j & 1) ? arr[k].toUpperCase() : arr[k].toLowerCase()
    }
    var combo = arr.join('')
    results.push(combo)
  }
  // remove duplicates
  return results.filter((ext, pos, self) => self.indexOf(ext) === pos)
}
<|endoftext|>


<commit_before>const co = require('co');
const pathToRegexp = require('path-to-regexp');
const {isEmpty, flatten} = require('lodash');

class RouterUrl {
  constructor(url) {
    this.url = url;
  }

  getUrls() {
    // why return Promise with array?
    // because RouterParamUrl return Promise with array of urls
    // so we make these two class return same data format
    return Promise.resolve([this.url]);
  }
}

class RouterParamUrl {
  constructor({url, dataProvider = () => Promise.resolve([])}) {
    this.url = url;
    this.toPath = pathToRegexp.compile(this.url);
    this.dataProvider = dataProvider;
  }

  getUrls() {
    return this.dataProvider()
    .then(data => {
      return data.map(row => {
        try {
          return this.toPath(row);
        } catch (err) {
          // row not pass toPath, will throw TypeError
          // https://github.com/pillarjs/path-to-regexp#compile-reverse-path-to-regexp
          if (err instanceof TypeError) {
            return null;
          }

          // another error, throw
          throw err;
        }
      })
      // filter null
      .filter(row => row);
    });
  }
}

class RoutesCollector {
  constructor({componentProps}) {
    // urls collected from component, RouterUrl or RouterParamUrl
    this.urlPromises = [];
    // routes generated in prepare from this.urls
    this.routes = [];
    this.componentProps = componentProps;
  }

  ifEnter(component) {
    return component.defineRoutes;
  }

  componentDidImport(id, component) {
    const props = Object.assign({}, this.componentProps, {Url: RouterUrl, ParamUrl: RouterParamUrl});
    this.urlPromises.push(component.defineRoutes(props).getUrls());
  }

  appWillRender() {
    const self = this;
    return co(function * () {
      const urls = yield self.urlPromises;
      self.routes = flatten(urls);
    });
  }

  // default render "/" if empty
  getRoutes() {
    return isEmpty(this.routes) ? ["/"] : this.routes;
  }

  wrapClientImport() {
    return `
      import {BrowserRouter as Router} from 'react-router-dom';
    `;
  }

  wrapClientRender($children) {
    return `
      <Router>
        ${$children}
      </Router>
    `;
  }
}

module.exports = RoutesCollector;
<commit_msg>routesCollector return data and path<commit_after>const co = require('co');
const pathToRegexp = require('path-to-regexp');
const {isEmpty, flatten} = require('lodash');
// uniformed data format
const route = (path, data) => ({path, data});

class RouterUrl {
  constructor(url) {
    this.url = url;
  }

  getUrls() {
    // why return Promise with array?
    // because RouterParamUrl return Promise with array of urls
    // so we make these two class return same data format
    return Promise.resolve([route(this.url)]);
  }
}

class RouterParamUrl {
  constructor({url, dataProvider = () => Promise.resolve([])}) {
    this.url = url;
    this.toPath = pathToRegexp.compile(this.url);
    this.dataProvider = dataProvider;
  }

  getUrls() {
    return this.dataProvider()
    .then(data => {
      return data.map(row => {
        try {
          return route(this.toPath(row), row);
        } catch (err) {
          // row not pass toPath, will throw TypeError
          // https://github.com/pillarjs/path-to-regexp#compile-reverse-path-to-regexp
          if (err instanceof TypeError) {
            return null;
          }

          // another error, throw
          throw err;
        }
      })
      // filter null
      .filter(row => row);
    });
  }
}

class RoutesCollector {
  constructor({componentProps}) {
    // urls collected from component, RouterUrl or RouterParamUrl
    this.urlPromises = [];
    // routes generated in prepare from this.urls
    this.routes = [];
    this.componentProps = componentProps;
  }

  ifEnter(component) {
    return component.defineRoutes;
  }

  componentDidImport(id, component) {
    const props = Object.assign({}, this.componentProps, {Url: RouterUrl, ParamUrl: RouterParamUrl});
    this.urlPromises.push(component.defineRoutes(props).getUrls());
  }

  appWillRender() {
    const self = this;
    return co(function * () {
      const urls = yield self.urlPromises;
      self.routes = flatten(urls);
    });
  }

  // default render "/" if empty
  getRoutes() {
    return isEmpty(this.routes) ? [route("/")] : this.routes;
  }

  wrapClientImport() {
    return `
      import {BrowserRouter as Router} from 'react-router-dom';
    `;
  }

  wrapClientRender($children) {
    return `
      <Router>
        ${$children}
      </Router>
    `;
  }
}

module.exports = RoutesCollector;
<|endoftext|>