var parserTestOptions = { includeComments: true };

var allNodeNames = [
  ";",
  "array",
  "assignment",
  "binary",
  "blockStmnt",
  "boolean",
  "bracket",
  "breakStmnt",
  "call",
  "case",
  "catch",
  "comma",
  "comment",
  "continueStmnt",
  "debuggerStmnt",
  "default",
  "doStmnt",
  "dot",
  "emptyStmnt",
  "expressionStmnt",
  "finally",
  "forInSpec",
  "forSpec",
  "forStmnt",
  "forVarInSpec",
  "forVarSpec",
  "functionDecl",
  "functionExpr",
  "idPropName",
  "identifier",
  "ifStmnt",
  "labelStmnt",
  "new",
  "newcall",
  "nil",
  "null",
  "numPropName",
  "number",
  "object",
  "parens",
  "postfix",
  "program",
  "prop",
  "regex",
  "returnStmnt",
  "strPropName",
  "string",
  "switchStmnt",
  "ternary",
  "this",
  "throwStmnt",
  "tryStmnt",
  "unary",
  "varDecl",
  "varStmnt",
  "whileStmnt",
  "withStmnt"
];

var allNodeNamesSet = {};
_.each(allNodeNames, function (n) { allNodeNamesSet[n] = true; });

var makeTester = function (test) {
  return {
    // Parse code and make sure it matches expectedTreeString.
    goodParse: function (code, expectedTreeString, regexTokenHints) {
      var expectedTree = ParseNode.unstringify(expectedTreeString);

      // first use lexer to collect all tokens
      var lexer = new JSLexer(code);
      var allTokensInOrder = [];
      while (! lexer.next().isEOF()) {
        var lex = lexer.lastLexeme;
        if (lex.isError())
          test.fail("Lexer error at " + lex.startPos());
        if (lex.isToken())
          allTokensInOrder.push(lex);
        if (regexTokenHints && regexTokenHints[allTokensInOrder.length])
          lexer.divisionPermitted = false;
      }

      var parser = new JSParser(code, parserTestOptions);
      var actualTree = parser.getSyntaxTree();

      var nextTokenIndex = 0;
      var check = function (tree) {
        if (tree instanceof ParseNode) {
          // This is a NODE (non-terminal).
          var nodeName = tree.name;
          if (! (nodeName && typeof nodeName === "string" &&
                 allNodeNamesSet[nodeName] === true))
            test.fail("Not a node name: " + nodeName);
          _.each(tree.children, check);
        } else if (typeof tree === 'object' &&
                   typeof tree.text === 'function') {
          // This is a TOKEN (terminal).
          // Make sure we are visiting every token once, in order.
          // Make an exception for any comment lexemes present,
          // because we couldn't know whether to include them in
          // allTokensInOrder.
          if (tree.type() !== "COMMENT") {
            if (nextTokenIndex >= allTokensInOrder.length)
              test.fail("Too many tokens: " + (nextTokenIndex + 1));
            var referenceToken = allTokensInOrder[nextTokenIndex++];
            if (tree.text() !== referenceToken.text())
              test.fail(tree.text() + " !== " + referenceToken.text());
            if (tree.startPos() !== referenceToken.startPos())
              test.fail(tree.startPos() + " !== " + referenceToken.startPos());
            if (code.substring(tree.startPos(), tree.endPos()) !== tree.text())
              test.fail("Didn't see " + tree.text() + " at " + tree.startPos() +
                        " in " + code);
          }
        } else {
          test.fail("Unknown tree part: " + tree);
        }
      };

      check(actualTree);
      if (nextTokenIndex !== allTokensInOrder.length)
        test.fail("Too few tokens: " + nextTokenIndex);

      test.equal(parser.pos, code.length);

      test.equal(ParseNode.stringify(actualTree),
                 ParseNode.stringify(expectedTree), code);
    },
    // Takes code with part of it surrounding with backticks.
    // Removes the two backtick characters, tries to parse the code,
    // and then asserts that there was a tokenization-level error,
    // with the part that was between the backticks called out as
    // the bad token.
    //
    // For example, the test "123`@`" will try to parse "123@" and
    // assert that a tokenization error occurred at '@'.
    badToken: function (code) {
      var constructMessage = function (pos, text) {
        var nicePos = JSLexer.prettyOffset(code, pos);
        return "Bad token at " + nicePos + ", text `" + text + "`";
      };
      var pos = code.indexOf('`');
      var text = code.match(/`(.*?)`/)[1];
      code = code.replace(/`/g, '');

      var parsed = false;
      var error = null;
      try {
        var tree = new JSParser(code, parserTestOptions).getSyntaxTree();
        parsed = true;
      } catch (e) {
        error = e;
      }
      test.isFalse(parsed);
      test.isTrue(error);
      test.equal(error.message, constructMessage(pos, text));
    },
    // Takes code with a backtick-quoted string embedded in it.
    // Removes the backticks and their contents, tries to parse the code,
    // and then asserts that there was a parse error at the location
    // where the backtick-quoted string was embedded.  The embedded
    // string must match whatever the error message says was "expected".
    //
    // For example, the test "{`statement`" will try to parse the code
    // "{" and then assert that an error occured at the end of the string
    // saying "Expected statement".  The test "1 `semicolon`2" will try
    // to parse "1 2" and assert that the error "Expected semicolon"
    // appeared after the space and before the 2.
    //
    // A second backtick-quoted string is used as the "found" token
    // in the error message.
    badParse: function (code) {
      var constructMessage = function (whatExpected, pos, found, after) {
        return "Expected " + whatExpected + (after ? " after " + after : "") +
          " at " + JSLexer.prettyOffset(code, pos) + ", found " + found;
      };
      var pos = code.indexOf('`');

      var backticked = code.match(/`.*?`/g);
      var whatExpected = backticked[0] && backticked[0].slice(1,-1);
      var found = backticked[1] && backticked[1].slice(1, -1);
      code = code.replace(/`.*?`/g, '');

      var parsed = false;
      var error = null;
      var parser = new JSParser(code, parserTestOptions);
      try {
        var tree = parser.getSyntaxTree();
        parsed = true;
      } catch (e) {
        error = e;
      }
      test.isFalse(parsed);
      test.isTrue(error);
      if (! parsed && error) {
        var after = parser.oldToken;
        found = (found || parser.newToken);
        test.equal(error.message,
                   constructMessage(whatExpected, pos, found, after),
                   code);
      }
    }
  };
};

Tinytest.add("jsparse - basics", function (test) {
  var tester = makeTester(test);
  tester.goodParse('1', "program(expressionStmnt(number(1) ;()))");
  tester.goodParse('1 + 1', "program(expressionStmnt(binary(number(1) + number(1)) ;()))");
  tester.goodParse('1*2+3*4', "program(expressionStmnt(binary(binary(number(1) * number(2)) + " +
                "binary(number(3) * number(4))) ;()))");
  tester.goodParse('1 + 1;', "program(expressionStmnt(binary(number(1) + number(1)) ;))");
  tester.goodParse('1 + 1;;', "program(expressionStmnt(binary(number(1) + number(1)) ;) emptyStmnt(;))");
  tester.goodParse('', "program()");
  tester.goodParse('\n', "program()");
  tester.goodParse(';;;\n\n;\n', "program(emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;))");
  tester.goodParse('foo', "program(expressionStmnt(identifier(foo) ;()))");
  tester.goodParse('foo();', "program(expressionStmnt(call(identifier(foo) `(` `)`) ;))");
  tester.goodParse('var x = 3', "program(varStmnt(var varDecl(x = number(3)) ;()))");
  tester.goodParse('++x;', "program(expressionStmnt(unary(++ identifier(x)) ;))");
  tester.goodParse('x++;', "program(expressionStmnt(postfix(identifier(x) ++) ;))");
  tester.goodParse(
    'throw new Error',
    "program(throwStmnt(throw new(new identifier(Error)) ;()))");
  tester.goodParse(
    'var x = function () { return 123; };',
    'program(varStmnt(var varDecl(x = functionExpr(function nil() `(` `)` ' +
      '{ returnStmnt(return number(123) ;) })) ;))');

  tester.badParse("var x = `expression`");
  tester.badParse("1 `semicolon`1");
  tester.badParse("1+1`semicolon`:");
});

Tinytest.add("jsparse - tokenization errors", function (test) {
  var tester = makeTester(test);
  tester.badToken("123`@`");
  tester.badToken("thisIsATestOf = `'unterminated `\n strings'");
  // make sure newlines aren't quietly included in regex literals
  tester.badToken("var x = `/`a\nb/;");
  tester.badToken("var x = `/`a\\\nb/;");
  tester.badToken("var x = `/`a[\n]b/;");
});

Tinytest.add("jsparse - syntax forms", function (test) {
  var tester = makeTester(test);
  var trials = [
    // STATEMENTS
    ['1',
     'program(expressionStmnt(number(1) ;()))'],
    ['1;;;;2',
     'program(expressionStmnt(number(1) ;) emptyStmnt(;) emptyStmnt(;) emptyStmnt(;) ' +
     'expressionStmnt(number(2) ;()))'],
    ['{}',
     'program(blockStmnt({ }))'],
    ['{null}',
     'program(blockStmnt({ expressionStmnt(null(null) ;()) }))'],
    ['{\nfoo()\nbar();\n}',
     'program(blockStmnt({ expressionStmnt(call(identifier(foo) `(` `)`) ;()) ' +
     'expressionStmnt(call(identifier(bar) `(` `)`) ;) }))'],
    ['{{{}}}',
     'program(blockStmnt({ blockStmnt({ blockStmnt({ }) }) }))'],
    ['var x = y, z,\n  a = b = c;',
     'program(varStmnt(var varDecl(x = identifier(y)) , varDecl(z) , varDecl(a = ' +
     'assignment(identifier(b) = identifier(c))) ;))'],
    ['if (x === y);',
     'program(ifStmnt(if `(` binary(identifier(x) === identifier(y)) `)` emptyStmnt(;)))'],
    ['if (z) return',
     'program(ifStmnt(if `(` identifier(z) `)` returnStmnt(return nil() ;())))'],
    ['if (a) b; else c',
     'program(ifStmnt(if `(` identifier(a) `)` expressionStmnt(identifier(b) ;) else ' +
     'expressionStmnt(identifier(c) ;())))'],
    ['if (n === 1) { foo(); } else if (n === 2) { bar(); } else { baz(); }',
     'program(ifStmnt(if `(` binary(identifier(n) === number(1)) `)` blockStmnt(' +
     '{ expressionStmnt(call(identifier(foo) `(` `)`) ;) }) else ifStmnt(' +
     'if `(` binary(identifier(n) === number(2)) `)` blockStmnt(' +
     '{ expressionStmnt(call(identifier(bar) `(` `)`) ;) }) else blockStmnt(' +
     '{ expressionStmnt(call(identifier(baz) `(` `)`) ;) }))))'],
    ['while (false);',
     'program(whileStmnt(while `(` boolean(false) `)` emptyStmnt(;)))'],
    ['while (/foo/.test(bar.baz)) {\n  bar = bar.baz;\n}',
     'program(whileStmnt(while `(` call(dot(regex(/foo/) . test) `(` ' +
     'dot(identifier(bar) . baz) `)`) `)` blockStmnt({ expressionStmnt(' +
     'assignment(identifier(bar) = dot(identifier(bar) . baz)) ;) })))'],
    ['while (false) while (false);',
     'program(whileStmnt(while `(` boolean(false) `)` ' +
     'whileStmnt(while `(` boolean (false) `)` emptyStmnt(;))))'],
    ['do a; while (b);',
     'program(doStmnt(do expressionStmnt(identifier(a) ;) while `(` identifier(b) `)` ;))'],
    ['do { x-- } while (x);',
     'program(doStmnt(do blockStmnt({ expressionStmnt(postfix(identifier(x) --) ;()) }) ' +
     'while `(` identifier(x) `)` ;))'],
    ['do a\n while (b)\n x++',
     'program(doStmnt(do expressionStmnt(identifier(a) ;()) while `(` identifier(b) `)` ;()) ' +
     'expressionStmnt(postfix(identifier(x) ++) ;()))'],
    ["for(;;);",
     "program(forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;)))"],
    ["for(x in y);",
     "program(forStmnt(for `(` forInSpec(identifier(x) in identifier(y)) `)` emptyStmnt(;)))"],
    ["for(var x in y);",
     "program(forStmnt(for `(` forVarInSpec(var varDecl(x) in identifier(y)) `)` emptyStmnt(;)))"],
    ["for(var x;;);",
     "program(forStmnt(for `(` forVarSpec(var varDecl(x) ; nil() ; nil()) `)` emptyStmnt(;)))"],
    ["for(var i=0;i<N;i++) {}",
     "program(forStmnt(for `(` forVarSpec(var varDecl(i = number(0)) ; " +
     "binary(identifier(i) < identifier(N)) ; postfix(identifier(i) ++)) `)` blockStmnt({ })))"],
    ["for (var x=3 in y);",
     "program(forStmnt(for `(` forVarInSpec(var varDecl(x = number(3)) in identifier(y)) `)` " +
     "emptyStmnt(;)))"],
    ["for (x.foo in y);",
     "program(forStmnt(for `(` forInSpec(dot(identifier(x) . foo) in identifier(y)) `)` emptyStmnt(;)))"],
    ["return",
     "program(returnStmnt(return nil() ;()))"],
    ["return;",
     "program(returnStmnt(return nil() ;))"],
    ["return null",
     "program(returnStmnt(return null(null) ;()))"],
    ["return null;",
     "program(returnStmnt(return null(null) ;))"],
    ["return\n1+1",
     "program(returnStmnt(return nil() ;()) expressionStmnt(binary(number(1) + number(1)) ;()))"],
    ["return 1\n  +1",
     "program(returnStmnt(return binary(number(1) + number(1)) ;()))"],
    ["continue",
     "program(continueStmnt(continue nil() ;()))"],
    ["continue foo",
     "program(continueStmnt(continue foo ;()))"],
    ["continue foo;",
     "program(continueStmnt(continue foo ;))"],
    ["continue\n  foo;",
     "program(continueStmnt(continue nil() ;()) expressionStmnt(identifier(foo) ;))"],
    ["break",
     "program(breakStmnt(break nil() ;()))"],
    ["break foo",
     "program(breakStmnt(break foo ;()))"],
    ["break foo;",
     "program(breakStmnt(break foo ;))"],
    ["break\n  foo;",
     "program(breakStmnt(break nil() ;()) expressionStmnt(identifier(foo) ;))"],
    ["throw e;",
     "program(throwStmnt(throw identifier(e) ;))"],
    ["throw e",
     "program(throwStmnt(throw identifier(e) ;()))"],
    ["throw new Error;",
     "program(throwStmnt(throw new(new identifier(Error)) ;))"],
    ["with(x);",
     "program(withStmnt(with `(` identifier(x) `)` emptyStmnt(;)))"],
    ["with(a=b) {}",
     "program(withStmnt(with `(` assignment(identifier(a) = identifier(b)) `)` blockStmnt({ })))"],
    ["switch(x) {}",
     "program(switchStmnt(switch `(` identifier(x) `)` { }))"],
    ["switch(x) {case 1:case 2:case 3:default:case 4:}",
     "program(switchStmnt(switch `(` identifier(x) `)` { " +
     "case(case number(1) :) case(case number(2) :) case(case number(3) :) " +
     "default(default :) case(case number(4) :) }))"],
    ["switch(x) {\ncase 1:\n  return\ncase 2:\ncase 3:\n  throw e}",
     "program(switchStmnt(switch `(` identifier(x) `)` { " +
     "case(case number(1) : returnStmnt(return nil() ;())) " +
     "case(case number(2) :) case(case number(3) : " +
     "throwStmnt(throw identifier(e) ;())) }))"],
    ["switch(x) {default:;}",
     "program(switchStmnt(switch `(` identifier(x) `)` { default(default : emptyStmnt(;)) }))"],
    ["try {} catch (e) {} finally {}",
     "program(tryStmnt(try blockStmnt({ }) catch(catch `(` e `)` blockStmnt({ })) " +
     "finally(finally blockStmnt({ }))))"],
    ["try {} finally {}",
     "program(tryStmnt(try blockStmnt({ }) nil() finally(finally blockStmnt({ }))))"],
    ["try {} catch (e) {}",
     "program(tryStmnt(try blockStmnt({ }) catch(catch `(` e `)` blockStmnt({ })) nil()))"],
    ["a:;",
     "program(labelStmnt(a : emptyStmnt(;)))"],
    ["{x:1}",
     "program(blockStmnt({ labelStmnt(x : expressionStmnt(number(1) ;())) }))"],
    ["{x:y:z:1}",
     "program(blockStmnt({ labelStmnt(x : labelStmnt(y : " +
     "labelStmnt(z : expressionStmnt(number(1) ;())))) }))"],
    [";;foo:\nfor(;;);",
     "program(emptyStmnt(;) emptyStmnt(;) labelStmnt(foo : " +
     "forStmnt(for `(` forSpec(nil() ; nil() ; nil()) `)` emptyStmnt(;))))"],
    ["debugger",
     "program(debuggerStmnt(debugger ;()))"],
    ["debugger;",
     "program(debuggerStmnt(debugger ;))"],
    ["function foo() {}",
     "program(functionDecl(function foo `(` `)` { }))"],
    ["function foo() {function bar() {}}",
     "program(functionDecl(function foo `(` `)` { functionDecl(function bar `(` `)` { }) }))"],
    [";;function f() {};;",
     "program(emptyStmnt(;) emptyStmnt(;) functionDecl(function f `(` `)` { }) " +
     "emptyStmnt(;) emptyStmnt(;))"],
    ["function foo(a,b,c) {}",
     "program(functionDecl(function foo `(` a , b , c `)` { }))"],

    // EXPRESSIONS
    ["null + this - 3 + true",
     "program(expressionStmnt(binary(binary(binary(null(null) + this(this)) - " +
     "number(3)) + boolean(true)) ;()))"],
    ["+.5",
     "program(expressionStmnt(unary(+ number(.5)) ;()))"],
    ["a1a1a",
     "program(expressionStmnt(identifier(a1a1a) ;()))"],
    ["/abc/mig",
     "program(expressionStmnt(regex(/abc/mig) ;()))"],
    ["/[]/",
     "program(expressionStmnt(regex(/[]/) ;()))"],
    ["/[/]/",
     "program(expressionStmnt(regex(/[/]/) ;()))"],
    ["/[[/]/",
     "program(expressionStmnt(regex(/[[/]/) ;()))"],
    ["/.\\/[a//b]\\[\\][[\\d/]/",
     "program(expressionStmnt(regex(/.\\/[a//b]\\[\\][[\\d/]/) ;()))"],
    ["a / /b/mgi / c",
     "program(expressionStmnt(binary(binary(identifier(a) / " +
     "regex(/b/mgi)) / identifier(c)) ;()))"],
    ["'a' + \"\" + \"b\" + '\\''",
     "program(expressionStmnt(binary(binary(binary(string('a') + string(\"\")) + " +
     "string(\"b\")) + string('\\'')) ;()))"],
    ["_ + x0123 + $",
     "program(expressionStmnt(binary(binary(identifier(_) + " +
     "identifier(x0123)) + identifier($)) ;()))"],
    ["if ((x = 1)) return ((1+2))*((1<<2));",
     "program(ifStmnt(if `(` parens(`(` assignment(identifier(x) = number(1)) `)`) " +
     "`)` returnStmnt(return binary(parens(`(` parens(`(` binary(number(1) + " +
     "number(2)) `)`) `)`) * parens(`(` parens(`(` binary(number(1) << number(2)) " +
     "`)`) `)`)) ;)))"],
    ["[];",
     "program(expressionStmnt(array([ ]) ;))"],
    ["[,,,];",
     "program(expressionStmnt(array([ , , , ]) ;))"],
    ["[(1,2),,3];",
     "program(expressionStmnt(array([ parens(`(` comma(number(1) , " +
     "number(2)) `)`) , , number(3) ]) ;))"],
    ["({});",
     "program(expressionStmnt(parens(`(` object({ }) `)`) ;))"],
    ["({1:1});",
     "program(expressionStmnt(parens(`(` object({ prop(numPropName(1) : number(1)) }) `)`) ;))"],
    ["({x:true});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : boolean(true)) }) `)`) ;))"],
    ["({'a':b, c:'d', 1:null});",
     "program(expressionStmnt(parens(`(` object({ prop(strPropName('a') : " +
     "identifier(b)) , prop(idPropName(c) : string('d')) , prop(numPropName(1) " +
     ": null(null)) }) `)`) ;))"],
    ["(function () {});",
     "program(expressionStmnt(parens(`(` functionExpr(function nil() `(` `)` { }) `)`) ;))"],
    ["(function foo() {});",
     "program(expressionStmnt(parens(`(` functionExpr(function foo `(` `)` { }) `)`) ;))"],
    ["x = function () {}.y;",
     "program(expressionStmnt(assignment(identifier(x) = dot(functionExpr(" +
     "function nil() `(` `)` { }) . y)) ;))"],
    ["(function (a) {})",
     "program(expressionStmnt(parens(`(` functionExpr(function nil() " +
     "`(` a `)` { }) `)`) ;()))"],
    ["(function (a,b,c) {})",
     "program(expressionStmnt(parens(`(` functionExpr(function nil() `(` " +
     "a , b , c `)` { }) `)`) ;()))"],
    ["foo.bar.baz;",
     "program(expressionStmnt(dot(dot(identifier(foo) . bar) . baz) ;))"],
    ["foo[bar,bar][baz].qux[1+1];",
     "program(expressionStmnt(bracket(dot(bracket(bracket(identifier(foo) " +
     "[ comma(identifier(bar) , identifier(bar)) ]) [ identifier(baz) ]) . qux) " +
     "[ binary(number(1) + number(1)) ]) ;))"],
    ["new new a.b.c[d]",
     "program(expressionStmnt(new(new new(new bracket(dot(dot(identifier(a) " +
     ". b) . c) [ identifier(d) ]))) ;()))"],
    ["new new a.b.c[d]()",
     "program(expressionStmnt(new(new newcall(new " +
     "bracket(dot(dot(identifier(a) . b) . c) [ identifier(d) ]) `(` `)`)) ;()))"],
    ["new new a.b.c[d]()()",
     "program(expressionStmnt(newcall(new newcall(new " +
     "bracket(dot(dot(identifier(a) . b) . c) [ identifier(d) ]) `(` `)`) `(` `)`) ;()))"],
    ["new foo(x).bar(y)",
     "program(expressionStmnt(call(dot(newcall(new identifier(foo) `(` " +
     "identifier(x) `)`) . bar) `(` identifier(y) `)`) ;()))"],
    ["new new foo().bar",
     "program(expressionStmnt(new(new dot(newcall(new identifier(foo) `(` `)`) . bar)) ;()))"],
    ["delete void typeof - + ~ ! -- ++ x;",
     "program(expressionStmnt(unary(delete unary(void unary(typeof unary(- unary(+ " +
     "unary(~ unary(! unary(-- unary(++ identifier(x)))))))))) ;))"],
    ["x++ + ++y",
     "program(expressionStmnt(binary(postfix(identifier(x) ++) + " +
     "unary(++ identifier(y))) ;()))"],
    ["1*2+3*4",
     "program(expressionStmnt(binary(binary(number(1) * number(2)) " +
     "+ binary(number(3) * number(4))) ;()))"],
    ["a*b/c%d+e-f<<g>>h>>>i<j>k<=l>=m instanceof n in o==p!=q===r!==s&t^u|v&&w||x",
     "program(expressionStmnt(binary(binary(binary(binary(binary(binary(binary(" +
     "binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(binary(" +
     "binary(binary(binary(binary(binary(identifier(a) * identifier(b)) / " +
     "identifier(c)) % identifier(d)) + identifier(e)) - identifier(f)) << identifier(g)) " +
     ">> identifier(h)) >>> identifier(i)) < identifier(j)) > identifier(k)) <= " +
     "identifier(l)) >= identifier(m)) instanceof identifier(n)) in identifier(o)) == " +
     "identifier(p)) != identifier(q)) === identifier(r)) !== identifier(s)) & " +
     "identifier(t)) ^ identifier(u)) | identifier(v)) && identifier(w)) || " +
     "identifier(x)) ;()))"],
    ["a||b&&c|d^e&f!==g===h!=i==j in k instanceof l>=m<=n<o<p>>>q>>r<<s-t+u%v/w*x",
     "program(expressionStmnt(binary(identifier(a) || binary(identifier(b) && " +
     "binary(identifier(c) | binary(identifier(d) ^ binary(identifier(e) & " +
     "binary(binary(binary(binary(identifier(f) !== identifier(g)) === identifier(h)) " +
     "!= identifier(i)) == binary(binary(binary(binary(binary(binary(identifier(j) in " +
     "identifier(k)) instanceof identifier(l)) >= identifier(m)) <= identifier(n)) < " +
     "identifier(o)) < binary(binary(binary(identifier(p) >>> identifier(q)) >> " +
     "identifier(r)) << binary(binary(identifier(s) - identifier(t)) + " +
     "binary(binary(binary(identifier(u) % identifier(v)) / identifier(w)) * " +
     "identifier(x))))))))))) ;()))"],
    ["a?b:c",
     "program(expressionStmnt(ternary(identifier(a) ? identifier(b) : " +
     "identifier(c)) ;()))"],
    ["1==2?3=4:5=6",
     "program(expressionStmnt(ternary(binary(number(1) == number(2)) ? " +
     "assignment(number(3) = number(4)) : assignment(number(5) = number(6))) ;()))"],
    ["a=b,c=d",
     "program(expressionStmnt(comma(assignment(identifier(a) = identifier(b)) , " +
     "assignment(identifier(c) = identifier(d))) ;()))"],
    ["a=b=c=d",
     "program(expressionStmnt(assignment(identifier(a) = assignment(identifier(b) " +
     "= assignment(identifier(c) = identifier(d)))) ;()))"],
    ["x[0]=x[1]=true",
     "program(expressionStmnt(assignment(bracket(identifier(x) [ number(0) ]) = " +
     "assignment(bracket(identifier(x) [ number(1) ]) = boolean(true))) ;()))"],
    ["a*=b/=c%=d+=e-=f<<=g>>=h>>>=i&=j^=k|=l",
     "program(expressionStmnt(assignment(identifier(a) *= assignment(identifier(b) " +
     "/= assignment(identifier(c) %= assignment(identifier(d) += " +
     "assignment(identifier(e) -= assignment(identifier(f) <<= " +
     "assignment(identifier(g) >>= assignment(identifier(h) >>>= " +
     "assignment(identifier(i) &= assignment(identifier(j) ^= " +
     "assignment(identifier(k) |= identifier(l)))))))))))) ;()))"],
    ["1;\n\n\n\n/* foo */\n// bar\n", // trailing whitespace and comments
     "program(expressionStmnt(number(1) ;) comment(`/* foo */`) comment(`// bar`))"],
    // includeComments option; comments in AST
    ["//foo",
     "program(comment(//foo))"],
    ["//foo\n",
     "program(comment(//foo))"],
    ["/*foo*/",
     "program(comment(/*foo*/))"],
    ["/*foo*/\n",
     "program(comment(/*foo*/))"],
    [";\n//foo",
     "program(emptyStmnt(;) comment(//foo))"],
    [";\n/*foo*/",
     "program(emptyStmnt(;) comment(/*foo*/))"],
    [";\n//foo\n;",
     "program(emptyStmnt(;) comment(//foo) emptyStmnt(;))"],
    [";\n/*foo*/\n;",
     "program(emptyStmnt(;) comment(/*foo*/) emptyStmnt(;))"],
    [";\n//foo\n//bar\n;",
     "program(emptyStmnt(;) comment(//foo) comment(//bar) emptyStmnt(;))"],
    [";\n/*foo*/ /*bar*/\n;",
     "program(emptyStmnt(;) comment(/*foo*/) comment(/*bar*/) emptyStmnt(;))"],
    [";//foo\n//bar\n;",
     "program(emptyStmnt(;) comment(//bar) emptyStmnt(;))"],
    [";/*foo*/\n/*bar*/\n;",
     "program(emptyStmnt(;) comment(/*bar*/) emptyStmnt(;))"],
    [";/*foo*//*bar*///baz\n;",
     "program(emptyStmnt(;) emptyStmnt(;))"],
    [";/*foo*//*bar*///baz",
     "program(emptyStmnt(;))"],
    ["/*foo*//*bar*///baz",
     "program(comment(/*foo*/) comment(/*bar*/) comment(//baz))"],
    ["//foo\n//bar\nfunction aaa() {}\nfunction bbb() {}",
     "program(comment(//foo) comment(//bar) functionDecl(function aaa `(` `)` { }) " +
     "functionDecl(function bbb `(` `)` { }))"],
    // comments don't interfere with parse
    ["if (true)\n//comment\nfoo();",
     "program(ifStmnt(if `(` boolean(true) `)` " +
     "expressionStmnt(call(identifier(foo) `(` `)`) ;)))"],
    // bare keywords allowed in property access and object literal
    ["foo.return();",
     "program(expressionStmnt(call(dot(identifier(foo) . return) `(` `)`) ;))"],
    ["foo.true();",
     "program(expressionStmnt(call(dot(identifier(foo) . true) `(` `)`) ;))"],
    ["foo.null();",
     "program(expressionStmnt(call(dot(identifier(foo) . null) `(` `)`) ;))"],
    ["({true:3})",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(true) : number(3)) }) `)`) ;()))"],
    ["({null:3})",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(null) : number(3)) }) `)`) ;()))"],
    ["({if:3})",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(if) : number(3)) }) `)`) ;()))"],
    // ES5 line continuations in string literals
    ["var x = 'a\\\nb\\\nc';",
     "program(varStmnt(var varDecl(x = string(`'a\\\nb\\\nc'`)) ;))"],
    // ES5 trailing comma in object literal
    ["({});",
     "program(expressionStmnt(parens(`(` object({ }) `)`) ;))"],
    ["({x:1});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) }) `)`) ;))"],
    ["({x:1,});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , }) `)`) ;))"],
    ["({x:1,y:2});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , " +
     "prop(idPropName(y) : number(2)) }) `)`) ;))"],
    ["({x:1,y:2,});",
     "program(expressionStmnt(parens(`(` object({ prop(idPropName(x) : number(1)) , " +
     "prop(idPropName(y) : number(2)) , }) `)`) ;))"]
  ];
  _.each(trials, function (tr) {
    tester.goodParse(tr[0], tr[1]);
  });
});

Tinytest.add("jsparse - bad parses", function (test) {
  var tester = makeTester(test);
  // string between backticks is pulled out and becomes what's "expected"
  // at that location, according to the correct error message
  var trials = [
    '{`statement`',
    'if (`expression`)',
    'if `(`else',
    'var`varDecl`;',
    'while (`expression`);',
    'while`(`;',
    'do a `semicolon`while b;',
    'do a\n while `(`b;',
    '1 `semicolon`2',
    'for (`forSpec`);',
    'for (1\n`semicolon`2\n3);',
    'continue `semicolon`1+1;',
    'break `semicolon`1+1;',
    'throw`expression`',
    'throw`expression`;',
    'throw\n`expression`',
    'throw\n`expression``end of line`e',
    'throw `expression`=;',
    'with(`expression`);',
    'switch(`expression`)',
    'switch(x)`{`;',
    'try`block`',
    'try {}`catch`',
    'try {} catch`(`;',
    'try {} catch(e)`block`;',
    '1+1`semicolon`:',
    '{a:`statement`}',
    'function `IDENTIFIER`() {}',
    'foo: `statement`function foo() {}',
    '[`expression`=',
    '[,,`expression`=',
    '({`propertyName`|:3})',
    '({1:2,3`:`})',
    '({1:2,`propertyName`',
    'x.`IDENTIFIER`,',
    'foo;`semicolon`:;',
    '1;`statement`=',
    'a+b`semicolon`=c;',
    'for(1+1 `semicolon`in {});',
    '`statement`=',
    'for(;`expression`var;) {}',
    '({`propertyName`',
    '({`propertyName`,})',
    '({`propertyName`:})',
    '({x`:`})',
    '({x:1,`propertyName`',
    '({x:1,`propertyName`,})',
    '({x:1`,`',
    '({x:1,`propertyName`,y:2})',
    '({x:1,`propertyName`,})',
    '({x:1,y:2`,`:',
    '({x:1,y:2,`propertyName`',
    '({x:1,y:2,`propertyName`:',
    '({x:1,y:2,`propertyName`,})'
  ];
  _.each(trials, function (tr) {
    tester.badParse(tr);
  });
});

Tinytest.add("jsparse - regex division ambiguity", function (test) {
  var tester = makeTester(test);
  tester.goodParse("if (e) /f/g;",
                   "program(ifStmnt(if `(` identifier(e) `)` expressionStmnt(regex(/f/g) ;)))",
                   {4: true});
  tester.goodParse("++/x/.y;",
                   "program(expressionStmnt(unary(++ dot(regex(/x/) . y)) ;))",
                   {1: true});
  tester.goodParse("x++/2/g;",
                   "program(expressionStmnt(binary(binary(postfix(identifier(x) ++) / " +
                   "number(2)) / identifier(g)) ;))");
  tester.goodParse("(1+1)/2/g;",
                   "program(expressionStmnt(binary(binary(parens(`(` binary(number(1) + " +
                   "number(1)) `)`) / " +
                   "number(2)) / identifier(g)) ;))");
  tester.goodParse("/x/",
                   "program(expressionStmnt(regex(/x/) ;()))");
});

Tinytest.add("jsparse - semicolon insertion", function (test) {
  var tester = makeTester(test);
  // Spec section 7.9.2
  tester.badParse("{ 1 `semicolon`2 } 3");
  tester.goodParse("{ 1\n2 } 3", "program(blockStmnt({ expressionStmnt(number(1) " +
                   ";()) expressionStmnt(number(2) ;()) }) expressionStmnt(number(3) ;()))");
  tester.badParse("for (a; b\n`semicolon`)");
  tester.goodParse("return\na + b",
                   "program(returnStmnt(return nil() ;()) " +
                   "expressionStmnt(binary(identifier(a) + identifier(b)) ;()))");
  tester.goodParse("a = b\n++c",
                   "program(expressionStmnt(assignment(identifier(a) = identifier(b)) ;())" +
                   "expressionStmnt(unary(++ identifier(c)) ;()))");
  tester.badParse("if (a > b)\n`statement`else c = d");
  tester.goodParse("a = b + c\n(d + e).print()",
                   "program(expressionStmnt(assignment(identifier(a) = " +
                   "binary(identifier(b) + call(dot(call(identifier(c) `(` " +
                   "binary(identifier(d) + identifier(e)) `)`) . print) `(` `)`))) ;()))");
});

Tinytest.add("jsparse - comments", function (test) {
  var tester = makeTester(test);
  // newline in multi-line comment makes it into a line break for semicolon
  // insertion purposes
  tester.badParse("1/**/`semicolon`2");
  tester.goodParse("1/*\n*/2",
                   "program(expressionStmnt(number(1) ;()) expressionStmnt(number(2) ;()))");
});

Tinytest.add("jsparse - initial lex error", function (test) {
  var doTest = function (code) {
    // this shouldn't throw
    var parser = new JSParser(code, parserTestOptions);
    // this should throw
    try {
      parser.getSyntaxTree();
      test.fail();
    } catch (e) {
      test.isTrue(/^Bad token/.test(e.message), e.message);
    }
  };

  doTest('/');
  doTest('@');
});


keep keep keep keep replace replace replace replace replace replace replace replace keep keep keep keep keep

////////// Requires //////////

var fs = Npm.require("fs");
var http = Npm.require("http");
var os = Npm.require("os");
var path = Npm.require("path");
var url = Npm.require("url");

var connect = Npm.require('connect');
var optimist = Npm.require('optimist');
var useragent = Npm.require('useragent');

// @export WebApp
WebApp = {};

var findGalaxy = _.once(function () {
  if (!('GALAXY' in process.env)) {
    console.log(
      "To do Meteor Galaxy operations like binding to a Galaxy " +
        "proxy, the GALAXY environment variable must be set.");
    process.exit(1);
  }

  return Meteor.connect(process.env['GALAXY']);
});

// Keepalives so that when the outer server dies unceremoniously and
// doesn't kill us, we quit ourselves. A little gross, but better than
// pidfiles.
// XXX This should really be part of the boot script, not the webapp package.
//     Or we should just get rid of it, and rely on containerization.

var initKeepalive = function () {
  var keepaliveCount = 0;

  process.stdin.on('data', function (data) {
    keepaliveCount = 0;
  });

  process.stdin.resume();

  setInterval(function () {
    keepaliveCount ++;
    if (keepaliveCount >= 3) {
      console.log("Failed to receive keepalive! Exiting.");
      process.exit(1);
    }
  }, 3000);
};

// #BrowserIdentification
//
// We have multiple places that want to identify the browser: the
// unsupported browser page, the appcache package, and, eventually
// delivering browser polyfills only as needed.
//
// To avoid detecting the browser in multiple places ad-hoc, we create a
// Meteor "browser" object. It uses but does not expose the npm
// useragent module (we could choose a different mechanism to identify
// the browser in the future if we wanted to).  The browser object
// contains
//
// * `name`: the name of the browser in camel case
// * `major`, `minor`, `patch`: integers describing the browser version
//
// Also here is an early version of a Meteor `request` object, intended
// to be a high-level description of the request without exposing
// details of connect's low-level `req`.  Currently it contains:
//
// * `browser`: browser identification object described above
// * `url`: parsed url, including parsed query params
//
// As a temporary hack there is a `categorizeRequest` function on
// __meteor_bootstrap__ which converts a connect `req` to a Meteor
// `request`. This can go away once smart packages such as appcache are
// being passed a `request` object directly when they serve content.
//
// This allows `request` to be used uniformly: it is passed to the html
// attributes hook, and the appcache package can use it when deciding
// whether to generate a 404 for the manifest.
//
// Real routing / server side rendering will probably refactor this
// heavily.

// e.g. "Mobile Safari" => "mobileSafari"
var camelCase = function (name) {
  var parts = name.split(' ');
  parts[0] = parts[0].toLowerCase();
  for (var i = 1;  i < parts.length;  ++i) {
    parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1);
  }
  return parts.join('');
};

var identifyBrowser = function (req) {
  var userAgent = useragent.lookup(req.headers['user-agent']);
  return {
    name: camelCase(userAgent.family),
    major: +userAgent.major,
    minor: +userAgent.minor,
    patch: +userAgent.patch
  };
};

var categorizeRequest = function (req) {
  return {
    browser: identifyBrowser(req),
    url: url.parse(req.url, true)
  };
};

var htmlAttributes = function (template, request) {
  var attributes = '';
  _.each(__meteor_bootstrap__.htmlAttributeHooks || [], function (hook) {
    var attribute = hook(request);
    if (attribute !== null && attribute !== undefined && attribute !== '')
      attributes += ' ' + attribute;
  });
  return template.replace('##HTML_ATTRIBUTES##', attributes);
};

// Serve app HTML for this URL?
var appUrl = function (url) {
  if (url === '/favicon.ico' || url === '/robots.txt')
    return false;

  // NOTE: app.manifest is not a web standard like favicon.ico and
  // robots.txt. It is a file name we have chosen to use for HTML5
  // appcache URLs. It is included here to prevent using an appcache
  // then removing it from poisoning an app permanently. Eventually,
  // once we have server side routing, this won't be needed as
  // unknown URLs with return a 404 automatically.
  if (url === '/app.manifest')
    return false;

  // Avoid serving app HTML for declared routes such as /sockjs/.
  if (RoutePolicy.classify(url))
    return false;

  // we currently return app HTML on all URLs by default
  return true;
};

Meteor._postStartup = function (callback) {
  __meteor_bootstrap__.postStartupHooks.push(callback);
};

var runWebAppServer = function () {
  // read the control for the client we'll be serving up
  var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
                                 __meteor_bootstrap__.configJson.client);
  var clientDir = path.dirname(clientJsonPath);
  var clientJson = JSON.parse(fs.readFileSync(clientJsonPath, 'utf8'));

  if (clientJson.format !== "browser-program-pre1")
    throw new Error("Unsupported format for client assets: " +
                    JSON.stringify(clientJson.format));

  // XXX change all this config to something more reasonable
  var deployConfig =
        process.env.METEOR_DEPLOY_CONFIG
        ? JSON.parse(process.env.METEOR_DEPLOY_CONFIG) : {};
  if (!deployConfig.packages)
    deployConfig.packages = {};
  if (!deployConfig.boot)
    deployConfig.boot = {};
  if (!deployConfig.boot.bind)
    deployConfig.boot.bind = {};

  // check environment for legacy env variables.
  if (process.env.PORT && !_.has(deployConfig.boot.bind, 'localPort')) {
    deployConfig.boot.bind.localPort = parseInt(process.env.PORT);
  }
  if (process.env.MONGO_URL) {
    if (!deployConfig.packages['mongo-livedata'])
      deployConfig.packages['mongo-livedata'] = {};
    deployConfig.packages['mongo-livedata'].url = process.env.MONGO_URL;
  }

  // webserver
  var app = connect();

  // Strip off the path prefix, if it exists.
  app.use(function (request, response, next) {
    var pathPrefix = __meteor_runtime_config__.PATH_PREFIX;
    var url = Npm.require('url').parse(request.url);
    var pathname = url.pathname;
    // check if the path in the url starts with the path prefix (and the part
    // after the path prefix must start with a / if it exists.)
    if (pathPrefix && pathname.substring(0, pathPrefix.length) === pathPrefix &&
       (pathname.length == pathPrefix.length
        || pathname.substring(pathPrefix.length, pathPrefix.length + 1) === "/")) {
      request.url = request.url.substring(pathPrefix.length);
      next();
    } else if (pathname === "/favicon.ico" || pathname === "/robots.txt") {
      next();
    } else if (pathPrefix) {
      response.writeHead(404);
      response.write("Unknown path");
      response.end();
    } else {
      next();
    }
  });
  // Parse the query string into res.query. Only oauth_server cares about this,
  // but it's overkill to have that package depend on its own copy of connect
  // just for this simple processing.
  app.use(connect.query());

  // Auto-compress any json, javascript, or text.
  app.use(connect.compress());

  if (clientJson.staticCacheable) {
    // cacheable files are files that should never change. Typically
    // named by their hash (eg meteor bundled js and css files).
    // cache them ~forever (1yr)
    app.use(connect.static(path.join(clientDir, clientJson.staticCacheable),
                           {maxAge: 1000 * 60 * 60 * 24 * 365}));
  }

  // cache non-cacheable file anyway. This isn't really correct, as
  // users can change the files and changes won't propogate
  // immediately. However, if we don't cache them, browsers will
  // 'flicker' when rerendering images. Eventually we will probably want
  // to rewrite URLs of static assets to include a query parameter to
  // bust caches. That way we can both get good caching behavior and
  // allow users to change assets without delay.
  // https://github.com/meteor/meteor/issues/773
  if (clientJson.static) {
    app.use(connect.static(path.join(clientDir, clientJson.static),
                           {maxAge: 1000 * 60 * 60 * 24}));
  }

  // Packages and apps can add handlers to this via WebApp.connectHandlers.
  // They are inserted before our default handler.
  var packageAndAppHandlers = connect();
  app.use(packageAndAppHandlers);

  var suppressConnectErrors = false;
  // connect knows it is an error handler because it has 4 arguments instead of
  // 3. go figure.  (It is not smart enough to find such a thing if it's hidden
  // inside packageAndAppHandlers.)
  app.use(function (err, req, res, next) {
    if (!err || !suppressConnectErrors || !req.headers['x-suppress-error']) {
      next(err);
      return;
    }
    res.writeHead(err.status, { 'Content-Type': 'text/plain' });
    res.end("An error message");
  });

  // Will be updated by main before we listen.
  var boilerplateHtml = null;
  app.use(function (req, res, next) {
    if (! appUrl(req.url))
      return next();

    if (!boilerplateHtml)
      throw new Error("boilerplateHtml should be set before listening!");

    var request = categorizeRequest(req);

    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});

    var requestSpecificHtml = htmlAttributes(boilerplateHtml, request);
    res.write(requestSpecificHtml);
    res.end();
    return undefined;
  });

  // Return 404 by default, if no other handlers serve this URL.
  app.use(function (req, res) {
    res.writeHead(404);
    res.end();
  });

  var httpServer = http.createServer(app);

  // start up app
  _.extend(WebApp, {
    connectHandlers: packageAndAppHandlers,
    httpServer: httpServer,
    // metadata about the client program that we serve
    clientProgram: {
      manifest: clientJson.manifest
      // XXX do we need a "root: clientDir" field here? it used to be here but
      // was unused.
    },
    // For testing.
    suppressConnectErrors: function () {
      suppressConnectErrors = true;
    },
    // Hack: allow http tests to call connect.basicAuth without making them
    // Npm.depends on another copy of connect. (That would be fine if we could
    // have test-only NPM dependencies but is overkill here.)
    __basicAuth__: connect.basicAuth
  });
  _.extend(__meteor_bootstrap__, {
    // function that takes a connect `req` object and returns a summary
    // object with information about the request. See
    // #BrowserIdentifcation
    categorizeRequest: categorizeRequest,
    // list of functions to be called to determine any attributes to be
    // added to the '<html>' tag. Each function is passed a 'request'
    // object (see #BrowserIdentifcation) and should return a string,
    htmlAttributeHooks: [],
    deployConfig: deployConfig
  });

  // Let the rest of the packages (and Meteor.startup hooks) insert connect
  // middlewares and update __meteor_runtime_config__, then keep going to set up
  // actually serving HTML.
  // @export main
  main = function (argv) {
    argv = optimist(argv).boolean('keepalive').argv;

    var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
    boilerplateHtml =
      fs.readFileSync(boilerplateHtmlPath, 'utf8')
      .replace(
        "// ##RUNTIME_CONFIG##",
        "__meteor_runtime_config__ = " +
          JSON.stringify(__meteor_runtime_config__) + ";")
      .replace(
          /##PATH_PREFIX##/g,
        __meteor_runtime_config__.PATH_PREFIX || "");

    // only start listening after all the startup code has run.
    var bind = deployConfig.boot.bind;
    httpServer.listen(bind.localPort || 0, Meteor.bindEnvironment(function() {
      if (argv.keepalive || true)
        console.log("LISTENING"); // must match run.js
      var port = httpServer.address().port;
      if (bind.viaProxy && bind.viaProxy.proxyEndpoint) {
        Meteor._bindToProxy(bind.viaProxy);
      } else if (bind.viaProxy) {
        // bind via the proxy, but we'll have to find it ourselves via
        // ultraworld.
        var galaxy = findGalaxy();
        galaxy.subscribe('servicesByName', 'proxy');
        var Proxies = new Meteor.Collection('services', {
          manager: galaxy
        });
        var doBinding = function (proxyService) {
          if (proxyService.providers.proxy) {
            Log("Attempting to bind to proxy at " + proxyService.providers.proxy);
            Meteor._bindToProxy(_.extend({
              proxyEndpoint: proxyService.providers.proxy
            }, bind.viaProxy));
          }
        };
        Proxies.find().observe({
          added: doBinding,
          changed: doBinding
        });
      }

      _.each(__meteor_bootstrap__.postStartupHooks, function (x) { x(); });

    }, function (e) {
      console.error("Error listening:", e);
      console.error(e.stack);
    }));

    if (argv.keepalive)
      initKeepalive();
    return 'DAEMON';
  };
};

Meteor._bindToProxy = function (proxyConfig) {

  var securePort = proxyConfig.securePort || 4433;
  var insecurePort = proxyConfig.insecurePort || 8080;
  var bindPathPrefix = proxyConfig.bindPathPrefix || "";
  // XXX also support galaxy-based lookup
  if (!proxyConfig.proxyEndpoint)
    throw new Error("missing proxyEndpoint");
  if (!proxyConfig.bindHost)
    throw new Error("missing bindHost");
  // XXX move these into deployConfig?
  if (!process.env.GALAXY_JOB)
    throw new Error("missing $GALAXY_JOB");
  if (!process.env.GALAXY_APP)
    throw new Error("missing $GALAXY_APP");
  if (!process.env.LAST_START)
    throw new Error("missing $LAST_START");

  // XXX rename pid argument to bindTo.
  var pid = {
    job: process.env.GALAXY_JOB,
    lastStarted: process.env.LAST_START,
    app: process.env.GALAXY_APP
  };
  var myHost = os.hostname();

  var ddpBindTo = {
    ddpUrl: 'ddp://' + proxyConfig.bindHost + ':' + securePort + bindPathPrefix + '/',
    insecurePort: insecurePort
  };

  // This is run after packages are loaded (in main) so we can use
  // Meteor.connect.
  var proxy = Meteor.connect(proxyConfig.proxyEndpoint);
  var route = process.env.ROUTE;
  var host = route.split(":")[0];
  var port = +route.split(":")[1];
  proxy.call('bindDdp', {
    pid: pid,
    bindTo: ddpBindTo,
    proxyTo: {
      host: host,
      port: port,
      pathPrefix: bindPathPrefix + '/websocket'
    }
  });
  proxy.call('bindHttp', {
    pid: pid,
    bindTo: {
      host: proxyConfig.bindHost,
      port: insecurePort,
      pathPrefix: bindPathPrefix
    },
    proxyTo: {
      host: host,
      port: port,
      pathPrefix: bindPathPrefix
    }
  });
  if (proxyConfig.securePort !== null) {
    proxy.call('bindHttp', {
      pid: pid,
      bindTo: {
        host: proxyConfig.bindHost,
        port: securePort,
        pathPrefix: bindPathPrefix,
        ssl: true
      },
      proxyTo: {
        host: host,
        port: port,
        pathPrefix: bindPathPrefix
      }
    });
  }
  Log("Bound to proxy");
};

runWebAppServer();


Tinytest.add("js-analyze - findGlobalDottedRefs", function (test) {

  var R = JSAnalyze.READ;
  var W = JSAnalyze.WRITE;
  var run = function (source) {
    return JSAnalyze.findGlobalDottedRefs(source);
  };

  test.equal(run('x'), {x: R});
  test.equal(run('x + y'), {x: R, y: R});
  test.equal(run('x = y'), {x: W, y: R});
  test.equal(run('var x; x = y'), {y: R});
  test.equal(run('var y; x = y'), {x: W});
  test.equal(run('var x,y; x = y'), {});
  test.equal(run('for (x in y);'), {x: W, y: R});
  test.equal(run('for (var x in y);'), {y: R});
  test.equal(run('x++'), {x: W});
  test.equal(run('var x = y'), {y: R});
  test.equal(run('a.b[c.d]'), {'a.b': R, 'c.d': R});
  test.equal(run('foo.bar[baz][c.d].z = 3'), {'foo.bar': W, baz: R, 'c.d': R});
  test.equal(run('foo.bar(baz)[c.d].z = 3'), {'foo.bar': R, baz: R, 'c.d': R});
  test.equal(run('var x = y.z; x.a = y; z.b;'), {'y.z': R, 'z.b': R, 'y': R});
  test.equal(run('Foo.Bar'), {'Foo.Bar': R});
  test.equal(run('Foo.Bar = 3'), {'Foo.Bar': W});
  test.equal(run(
    '(function (a, d) { var b = a, c; return f(a.z, b.z, c.z, d.z, e.z); })()'),
             { 'f': R, 'e.z': R });
  test.equal(run('try { Foo } catch (e) { e }'), {'Foo': R});
  test.equal(run('try { Foo } catch (e) { Foo }'), {'Foo': R});
  test.equal(run('try { Foo } catch (Foo) { Foo }'), {'Foo': R});
  test.equal(run('try { e } catch (Foo) { Foo }'), {'e': R});
  test.equal(run('var x = function y () { return String(y); }'), {'String': R});
  test.equal(run('a[b=c] = d'), {a: W, b: W, c: R, d: R});
  test.equal(run('a.a.a[b.b.b=c.c.c] = d.d.d'),
             {'a.a.a': W, 'b.b.b': W, 'c.c.c': R, 'd.d.d': R});
  // Without ignoreEval, this thinks J is global.
  test.equal(run('function x(){var J;J=3;eval("foo");}'), {eval: R});
});

Tinytest.add("js-analyze - findAssignedGlobals", function (test) {

  var run = function (source, expected) {
    test.equal(JSAnalyze.findAssignedGlobals(source), expected);
  };

  run('x', {});
  run('x + y', {});
  run('x = y', {x: true});
  run('var x; x = y', {});
  run('var y; x = y', {x: true});
  run('var x,y; x = y', {});
  run('for (x in y);', {x: true});
  run('for (var x in y);', {});
  // Update operators cause ReferenceError if the left-hand is not defined.
  run('x++', {});
  run('x += 5', {});
  run('var x = y', {});
  run('a.b[c.d]', {});
  run('foo.bar[baz][c.d].z = 3', {});
  run('foo.bar(baz)[c.d].z = 3', {});
  run('var x = y.z; x.a = y; z.b;', {});
  run('Foo.Bar', {});
  run('Foo.Bar = 3', {});
  run(
    '(function (a, d) { var b = a, c; return f(a.z, b.z, c.z, d.z, e.z); })()',
    {});
  // catch clause declares a name
  run('try { Foo } catch (e) { e = 5 }', {});
  run('try { Foo } catch (e) { Foo }', {});
  run('try { Foo } catch (Foo) { Foo }', {});
  run('try { e } catch (Foo) { Foo }', {});
  run('var x = function y () { return String(y); }', {});
  run('a[b=c] = d', {b: true});
  run('a.a.a[b.b.b=c.c.c] = d.d.d', {});
  // esprima ignores parentheses
  run('((((x)))) = 5', {x: true});
  // esprima ignores comments
  run('x /* foo */ = 5', {x: true});

  // Without ignoreEval, this thinks J is global.
  run('function x(){var J;J=3;eval("foo");}', {});

  test.throws(function (){JSAnalyze.findAssignedGlobals("x = ");},
              function (e) { return e.$ParseError; });
});


var VALID_KEYS = [
  'dropdownVisible',

  // XXX consider replacing these with one key that has an enum for values.
  'inSignupFlow',
  'inForgotPasswordFlow',
  'inChangePasswordFlow',
  'inMessageOnlyFlow',

  'errorMessage',
  'infoMessage',

  // dialogs with messages (info and error)
  'resetPasswordToken',
  'enrollAccountToken',
  'justVerifiedEmail',

  'configureLoginServiceDialogVisible',
  'configureLoginServiceDialogServiceName',
  'configureLoginServiceDialogSaveDisabled'
];

var validateKey = function (key) {
  if (!_.contains(VALID_KEYS, key))
    throw new Error("Invalid key in loginButtonsSession: " + key);
};

var KEY_PREFIX = "Meteor.loginButtons.";

// XXX we should have a better pattern for code private to a package like this one
Accounts._loginButtonsSession = {
  set: function(key, value) {
    validateKey(key);
    if (_.contains(['errorMessage', 'infoMessage'], key))
      throw new Error("Don't set errorMessage or infoMessage directly. Instead, use errorMessage() or infoMessage().");

    this._set(key, value);
  },

  _set: function(key, value) {
    Session.set(KEY_PREFIX + key, value);
  },

  get: function(key) {
    validateKey(key);
    return Session.get(KEY_PREFIX + key);
  },

  closeDropdown: function () {
    this.set('inSignupFlow', false);
    this.set('inForgotPasswordFlow', false);
    this.set('inChangePasswordFlow', false);
    this.set('inMessageOnlyFlow', false);
    this.set('dropdownVisible', false);
    this.resetMessages();
  },

  infoMessage: function(message) {
    this._set("errorMessage", null);
    this._set("infoMessage", message);
    this.ensureMessageVisible();
  },

  errorMessage: function(message) {
    this._set("errorMessage", message);
    this._set("infoMessage", null);
    this.ensureMessageVisible();
  },

  // is there a visible dialog that shows messages (info and error)
  isMessageDialogVisible: function () {
    return this.get('resetPasswordToken') ||
      this.get('enrollAccountToken') ||
      this.get('justVerifiedEmail');
  },

  // ensure that somethings displaying a message (info or error) is
  // visible.  if a dialog with messages is open, do nothing;
  // otherwise open the dropdown.
  //
  // notably this doesn't matter when only displaying a single login
  // button since then we have an explicit message dialog
  // (_loginButtonsMessageDialog), and dropdownVisible is ignored in
  // this case.
  ensureMessageVisible: function () {
    if (!this.isMessageDialogVisible())
      this.set("dropdownVisible", true);
  },

  resetMessages: function () {
    this._set("errorMessage", null);
    this._set("infoMessage", null);
  },

  configureService: function (name) {
    this.set('configureLoginServiceDialogVisible', true);
    this.set('configureLoginServiceDialogServiceName', name);
    this.set('configureLoginServiceDialogSaveDisabled', true);
  }
};


// XXX from Underscore.String (http://epeli.github.com/underscore.string/)
var startsWith = function(str, starts) {
  return str.length >= starts.length &&
    str.substring(0, starts.length) === starts;
};
var endsWith = function(str, ends) {
  return str.length >= ends.length &&
    str.substring(str.length - ends.length) === ends;
};

// @param url {String} URL to Meteor app, eg:
//   "/" or "madewith.meteor.com" or "https://foo.meteor.com"
//   or "ddp+sockjs://ddp--****-foo.meteor.com/sockjs"
// @returns {String} URL to the endpoint with the specific scheme and subPath, e.g.
// for scheme "http" and subPath "sockjs"
//   "http://subdomain.meteor.com/sockjs" or "/sockjs"
//   or "https://ddp--1234-foo.meteor.com/sockjs"
var translateUrl =  function(url, newSchemeBase, subPath) {
  if (! newSchemeBase) {
    newSchemeBase = "http";
  }

  var ddpUrlMatch = url.match(/^ddp(i?)\+sockjs:\/\//);
  var httpUrlMatch = url.match(/^http(s?):\/\//);
  var newScheme;
  if (ddpUrlMatch) {
    // Remove scheme and split off the host.
    var urlAfterDDP = url.substr(ddpUrlMatch[0].length);
    newScheme = ddpUrlMatch[1] === "i" ? newSchemeBase : newSchemeBase + "s";
    var slashPos = urlAfterDDP.indexOf('/');
    var host =
          slashPos === -1 ? urlAfterDDP : urlAfterDDP.substr(0, slashPos);
    var rest = slashPos === -1 ? '' : urlAfterDDP.substr(slashPos);

    // In the host (ONLY!), change '*' characters into random digits. This
    // allows different stream connections to connect to different hostnames
    // and avoid browser per-hostname connection limits.
    host = host.replace(/\*/g, function () {
      return Math.floor(Random.fraction()*10);
    });

    return newScheme + '://' + host + rest;
  } else if (httpUrlMatch) {
    newScheme = !httpUrlMatch[1] ? newSchemeBase : newSchemeBase + "s";
    var urlAfterHttp = url.substr(httpUrlMatch[0].length);
    url = newScheme + "://" + urlAfterHttp;
  }

  // Prefix FQDNs but not relative URLs
  if (url.indexOf("://") === -1 && !startsWith(url, "/")) {
    url = newSchemeBase + "://" + url;
  }

  url = Meteor._fixLink(url);

  if (endsWith(url, "/"))
    return url + subPath;
  else
    return url + "/" + subPath;
};

_.extend(Meteor._DdpClientStream.prototype, {

  // Register for callbacks.
  on: function (name, callback) {
    var self = this;

    if (name !== 'message' && name !== 'reset' && name !== 'update_available')
      throw new Error("unknown event type: " + name);

    if (!self.eventCallbacks[name])
      self.eventCallbacks[name] = [];
    self.eventCallbacks[name].push(callback);
  },

  _initCommon: function () {
    var self = this;
    //// Constants

    // how long to wait until we declare the connection attempt
    // failed.
    self.CONNECT_TIMEOUT = 10000;

    // time for initial reconnect attempt.
    self.RETRY_BASE_TIMEOUT = 1000;
    // exponential factor to increase timeout each attempt.
    self.RETRY_EXPONENT = 2.2;
    // maximum time between reconnects. keep this intentionally
    // high-ish to ensure a server can recover from a failure caused
    // by load
    self.RETRY_MAX_TIMEOUT = 5 * 60000; // 5 minutes
    // time to wait for the first 2 retries.  this helps page reload
    // speed during dev mode restarts, but doesn't hurt prod too
    // much (due to CONNECT_TIMEOUT)
    self.RETRY_MIN_TIMEOUT = 10;
    // how many times to try to reconnect 'instantly'
    self.RETRY_MIN_COUNT = 2;
    // fuzz factor to randomize reconnect times by. avoid reconnect
    // storms.
    self.RETRY_FUZZ = 0.5; // +- 25%

    self.eventCallbacks = {}; // name -> [callback]

    self._forcedToDisconnect = false;

    //// Reactive status
    self.currentStatus = {
      status: "connecting",
      connected: false,
      retryCount: 0
    };

    self.statusListeners = typeof Deps !== 'undefined' && new Deps.Dependency;
    self.statusChanged = function () {
      if (self.statusListeners)
        self.statusListeners.changed();
    };

    //// Retry logic
    self.retryTimer = null;
    self.connectionTimer = null;

  },

  // Trigger a reconnect.
  reconnect: function (options) {
    var self = this;
    options = options || {};

    if (options.url) {
      self._changeUrl(options.url);
    }

    if (self.currentStatus.connected) {
      if (options._force || options.url) {
        // force reconnect.
        self._lostConnection();
      } // else, noop.
      return;
    }

    // if we're mid-connection, stop it.
    if (self.currentStatus.status === "connecting") {
      self._lostConnection();
    }

    if (self.retryTimer)
      clearTimeout(self.retryTimer);
    self.retryTimer = null;
    self.currentStatus.retryCount -= 1; // don't count manual retries
    self._retryNow();
  },

  // Permanently disconnect a stream.
  forceDisconnect: function (optionalErrorMessage) {
    var self = this;
    self._forcedToDisconnect = true;
    self._cleanup();
    if (self.retryTimer) {
      clearTimeout(self.retryTimer);
      self.retryTimer = null;
    }
    self.currentStatus = {
      status: "failed",
      connected: false,
      retryCount: 0
    };
    if (optionalErrorMessage)
      self.currentStatus.reason = optionalErrorMessage;
    self.statusChanged();
  },

  _lostConnection: function () {
    var self = this;

    self._cleanup();
    self._retryLater(); // sets status. no need to do it here.
  },

  _retryTimeout: function (count) {
    var self = this;

    if (count < self.RETRY_MIN_COUNT)
      return self.RETRY_MIN_TIMEOUT;

    var timeout = Math.min(
      self.RETRY_MAX_TIMEOUT,
      self.RETRY_BASE_TIMEOUT * Math.pow(self.RETRY_EXPONENT, count));
    // fuzz the timeout randomly, to avoid reconnect storms when a
    // server goes down.
    timeout = timeout * ((Random.fraction() * self.RETRY_FUZZ) +
                         (1 - self.RETRY_FUZZ/2));
    return timeout;
  },

  // fired when we detect that we've gone online. try to reconnect
  // immediately.
  _online: function () {
    this.reconnect();
  },

  _retryLater: function () {
    var self = this;

    var timeout = self._retryTimeout(self.currentStatus.retryCount);
    if (self.retryTimer)
      clearTimeout(self.retryTimer);
    self.retryTimer = setTimeout(_.bind(self._retryNow, self), timeout);

    self.currentStatus.status = "waiting";
    self.currentStatus.connected = false;
    self.currentStatus.retryTime = (new Date()).getTime() + timeout;
    self.statusChanged();
  },

  _retryNow: function () {
    var self = this;

    if (self._forcedToDisconnect)
      return;

    self.currentStatus.retryCount += 1;
    self.currentStatus.status = "connecting";
    self.currentStatus.connected = false;
    delete self.currentStatus.retryTime;
    self.statusChanged();

    self._launchConnection();
  },

  // Get current status. Reactive.
  status: function () {
    var self = this;
    if (self.statusListeners)
      self.statusListeners.depend();
    return self.currentStatus;
  }
});

_.extend(Meteor._DdpClientStream, {

  _toSockjsUrl: function (url) {
    return translateUrl(url, "http", "sockjs");
  },

  _toWebsocketUrl: function (url) {
    var ret = translateUrl(url, "ws", "websocket");
    return ret;
  }
});


ipcMain.on('update-menu-states', (event, visibleStates, enabledStates, checkedStates) => {

ipcMain.on('download-status-change', (event, status) => {


import Icon from '@/components/Icons/Icon'

Icon.register({
  'task-pause-line': {
    'width': 24,
    'height': 24,
    'raw': `<rect x="3" y="2" fill="none" stroke-miterlimit="10" width="6" height="20" /><rect x="15" y="2" fill="none" stroke-miterlimit="10" width="6" height="20" />`,
    'g': {
      'stroke': 'currentColor',
      'stroke-linecap': 'round',
      'stroke-linejoin': 'round',
      'stroke-width': '2'
    }
  }
})


export default {
  'active': 'Baixando',
  'waiting': 'Aguardando',
  'stopped': 'Cancelado',
  'new-task': 'Nova Tarefa',
  'new-bt-task': 'Nova Tarefa BT',
  'open-file': 'Abra o arquivo Torrent...',
  'uri-task': 'URL',
  'torrent-task': 'Torrent',
  'uri-task-tip': 'Uma url por linha (magnet links são aceitos)',
  'thunder-link-tip': 'Dica: Thunder links não podem ser baixados após decodificados',
  'task-name': 'Nome da Tarefa',
  'task-out': 'Renomear',
  'task-out-tip': 'Opcional, suporte únicc tarefa',
  'task-split': 'Splits',
  'task-dir': 'Diretório',
  'pause-task': 'Pausar Tarefa',
  'task-ua': 'UA',
  'task-user-agent': 'User-Agent',
  'task-referer': 'Referer',
  'task-cookie': 'Cookie',
  'navigate-to-downloading': 'Navegar para o Downloading',
  'show-advanced-options': 'Opções Avançadas',
  'copyright-warning': 'Aviso de Copyright',
  'copyright-warning-message': 'O arquivo que você deseja baixa pode ser protegido por direitos de copyright de áudio ou vídeo, tenha certeza que você possui os direitos de licensa.',
  'copyright-yes': 'Sim, Eu Tenho',
  'copyright-no': 'Não',
  'copyright-error-message': 'A operação falhou devido os direitos de copyright',
  'pause-task-success': 'Tarefa "{{taskName}}" pausada com sucesso',
  'pause-task-fail': 'Falha ao pausar a tarefa "{{taskName}}"',
  'resume-task': 'Resumir Tarefa',
  'resume-task-success': 'Tarefa "{{taskName}}" resumida com sucesso',
  'resume-task-fail': 'Falha ao resumir a tarefa "{{taskName}}"',
  'delete-task': 'Apagar Tarefa',
  'delete-selected-tasks': 'Apagar as Tarefas Selecionadas',
  'delete-task-confirm': 'Você tem certeza de que deseja apagar a seguinte tarefa de download: "{{taskName}}"?',
  'delete-task-label': 'Apagar com os Arquivos',
  'delete-task-success': 'Tarefa "{{taskName}}" apagada com sucesso',
  'delete-task-fail': 'Falha ao apagar a tarefa "{{taskName}}"',
  'remove-task-file-fail': 'Falha ao tentar deletar o arquivo da tarefa, por favor, apague manualmente',
  'remove-task-config-file-fail': 'Falha ao tentar deletar o arquivo de configuração da tarefa, por favor, apague manualmente',
  'move-task-up': 'Mover Tarefa para Cima',
  'move-task-down': 'Mover Tarefa para Baixar',
  'pause-all-task': 'Pausar Todas as Tarefas',
  'pause-all-task-success': 'Todas as tarefas foram pausadas com sucesso',
  'pause-all-task-fail': 'Falha ao pausar todas as tarefas',
  'resume-all-task': 'Resumir todas as tarefas',
  'resume-all-task-success': 'Todas as tarefas foram resumidas com sucesso',
  'resume-all-task-fail': 'Falha ao resumir todas as tarefas',
  'clear-recent-tasks': 'Limpar todas as tarefas',
  'purge-record': 'Expurgar o Registro de Tarefas',
  'purge-record-success': 'O registro de tarefas foi expurgado com sucesso',
  'purge-record-fail': 'Falha ao expurgar o registro de tarefas',
  'refresh-list': 'Atualizar a Lista de Tarefas',
  'no-task': 'Não há downloads no momento',
  'copy-link': 'Copiar Link',
  'copy-link-success': 'Link copiado com sucesso',
  'remove-record': 'Apagar Registro de Tarefas',
  'remove-record-confirm': 'Você tem certeza de que deseja remover o registro de download da tarefa "{{taskName}}"?',
  'remove-record-label': 'Deletar com arquivos',
  'remove-record-success': 'Registro da tarefa "{{taskName}}" apagado com sucesso',
  'remove-record-fail': 'Falha ao apagar o registro da tarefa "{{taskName}}"',
  'show-in-folder': 'Exibir Tarefa na Pasta',
  'file-not-exist': 'O arquivo não existe ou foi apagado',
  'file-path-error': 'Erro no caminho do arquivo',
  'opening-task-message': 'Abrindo "{{taskName}}" ...',
  'get-task-name': 'Obter o nome da tarefa...',
  'remaining-prefix': 'Restante',
  'select-torrent': 'Arraste o torrent aqui, ou clique para selecionar',
  'task-info-dialog-title': '{{title}} Detalhes',
  'download-start-message': 'Iniciar download {{taskName}}',
  'download-pause-message': 'Pausar download {{taskName}}',
  'download-stop-message': '{{taskName}} download parado',
  'download-error-message': '{{taskName}} ocorreu um erro no download',
  'download-complete-message': '{{taskName}} download completado',
  'download-complete-notify': 'Download Completado',
  'download-fail-message': '{{taskName}} falha no download',
  'download-fail-notify': 'Falha no Download'
}


<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
  <g class="nc-icon-wrapper" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="#000000" stroke="#000000">
    <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="12" y1="2" x2="12" y2="22" />
    <line fill="none" stroke="#000000" stroke-miterlimit="10" x1="22" y1="12" x2="2" y2="12" />
  </g>
</svg>


const icon = status === 'active' ? this.activeIcon : this.normalIcon

keep keep keep keep replace keep keep keep

const icon = status ? this.activeIcon : this.normalIcon


export default {
  'task-list': 'Tasks',
  'preferences': 'Preferences'
}


<template>
  <el-dialog
    custom-class="task-info-dialog"
    width="61.8vw"
    v-if="task"
    :title="dialogTitle"
    :show-close="true"
    :visible.sync="visible"
    :before-close="handleClose"
    @closed="handleClosed"
  >

      {{ taskFullName }}

    <mo-task-item-actions mode="ITEM" :task="task" />

      <mo-task-progress :completed="Number(task.completedLength)" :total="Number(task.totalLength)" :status="task.status" />
      <el-row class="task-speed">
        <el-col :span="12" class="task-speed-left">

 0">
          {{ task.completedLength | bytesToSize }} / {{ task.totalLength | bytesToSize }}

        </el-col>
        <el-col :span="12" class="task-speed-right">

            {{ task.downloadSpeed | bytesToSize }}/s

              {{
                remaining | timeFormat({
                  prefix: $t('task.remaining-prefix'),
                  i18n: {
                    'gt1d': $t('app.gt1d'),
                    'hour': $t('app.hour'),
                    'minute': $t('app.minute'),
                    'second': $t('app.second')
                  }
                })
              }}

        </el-col>
      </el-row>

  </el-dialog>
</template>


module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
    node: true
  },
  extends: 'standard',
  globals: {
    appId: true,
    __static: true
  },
  plugins: [
    'html'
  ],
  'rules': {
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0,
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}


export default {
  'undo': 'Desfazer',
  'redo': 'Refazer',
  'cut': 'Recortar',
  'copy': 'Copiar',
  'paste': 'Copiar',
  'delete': 'Apagar',
  'select-all': 'Selecionar Tudo'
}


---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement ✨
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


export default {
name: 'mo-engine-client',
computed: {

data: function () {
return {
downloading: false
}
},

import { EventEmitter } from 'events'
import { app, shell, dialog, ipcMain } from 'electron'
import is from 'electron-is'
import * as fs from 'fs'
import { extname, basename } from 'path'
import logger from './core/Logger'
import ConfigManager from './core/ConfigManager'
import { setupLocaleManager } from '@/ui/Locale'
import Engine from './core/Engine'
import UpdateManager from './core/UpdateManager'
import EnergyManager from './core/EnergyManager'
import ProtocolManager from './core/ProtocolManager'
import WindowManager from './ui/WindowManager'
import MenuManager from './ui/MenuManager'
import TouchBarManager from './ui/TouchBarManager'
import TrayManager from './ui/TrayManager'

export default class Application extends EventEmitter {
  constructor () {
    super()
    this.isReady = false
    this.init()
  }

  init () {
    this.configManager = new ConfigManager()

    this.locale = this.configManager.getLocale()
    this.localeManager = setupLocaleManager(this.locale)
    this.i18n = this.localeManager.getI18n()

    this.windowManager = new WindowManager({
      userConfig: this.configManager.getUserConfig()
    })

    this.engine = new Engine({
      systemConfig: this.configManager.getSystemConfig(),
      userConfig: this.configManager.getUserConfig()
    })
    this.startEngine()

    this.menuManager = new MenuManager()
    this.menuManager.setup(this.locale)

    this.touchBarManager = new TouchBarManager()

    this.trayManager = new TrayManager()

    this.energyManager = new EnergyManager()

    this.initUpdaterManager()

    this.initProtocolManager()

    this.handleCommands()
    this.handleIpcMessages()
  }

  startEngine () {
    try {
      this.engine.start()
    } catch (err) {
      const { message } = err
      dialog.showMessageBox({
        type: 'error',
        title: this.i18n.t('app.system-error-title'),
        message: this.i18n.t('app.system-error-message', { message })
      }, () => {
        setTimeout(() => {
          app.quit()
        }, 100)
      })
    }
  }

  start (page) {
    this.showPage(page)
  }

  showPage (page) {
    const win = this.windowManager.openWindow(page)
    win.once('ready-to-show', () => {
      this.isReady = true
      this.emit('ready')
    })
    this.touchBarManager.setup(page, win)
  }

  show (page = 'index') {
    this.windowManager.showWindow(page)
  }

  hide (page) {
    if (page) {
      this.windowManager.hideWindow(page)
    } else {
      this.windowManager.hideAllWindow()
    }
  }

  toggle (page = 'index') {
    this.windowManager.toggleWindow(page)
  }

  closePage (page) {
    this.windowManager.destroyWindow(page)
  }

  stop () {
    this.engine.stop()
    this.energyManager.stopPowerSaveBlocker()
  }

  sendCommand (command, ...args) {
    if (!this.emit(command, ...args)) {
      const window = this.windowManager.getFocusedWindow()
      if (window) {
        this.windowManager.sendCommandTo(window, command, ...args)
      }
    }
  }

  sendCommandToAll (command, ...args) {
    if (!this.emit(command, ...args)) {
      this.windowManager.getWindowList().forEach(window => {
        this.windowManager.sendCommandTo(window, command, ...args)
      })
    }
  }

  sendMessageToAll (channel, ...args) {
    this.windowManager.getWindowList().forEach(window => {
      this.windowManager.sendMessageTo(window, channel, ...args)
    })
  }

  initProtocolManager () {
    if (is.dev() || is.mas()) {
      return
    }
    this.protocolManager = new ProtocolManager()
  }

  handleProtocol (url) {
    if (is.dev() || is.mas()) {
      return
    }

    this.show()

    this.protocolManager.handle(url)
  }

  handleFile (path) {
    if (!path) {
      return
    }
    if (extname(path).toLowerCase() !== '.torrent') {
      return
    }

    this.show()

    const fileName = basename(path)
    fs.readFile(path, (err, data) => {
      if (err) {
        logger.warn(`[Motrix] read file error: ${path}`, err.message)
        return
      }
      const file = Buffer.from(data).toString('base64')
      const args = [fileName, file]
      this.sendCommandToAll('application:new-bt-task-with-file', ...args)
    })
  }

  initUpdaterManager () {
    if (is.mas()) {
      return
    }
    this.updateManager = new UpdateManager()
    this.handleUpdaterEvents()
  }

  handleUpdaterEvents () {
    this.updateManager.on('checking', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', false)
    })

    this.updateManager.on('download-progress', (event) => {
      const win = this.windowManager.getWindow('index')
      win.setProgressBar(event.percent / 100)
    })

    this.updateManager.on('update-not-available', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
    })

    this.updateManager.on('update-downloaded', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
      const win = this.windowManager.getWindow('index')
      win.setProgressBar(0)
    })

    this.updateManager.on('will-updated', (event) => {
      this.windowManager.setWillQuit(true)
    })

    this.updateManager.on('update-error', (event) => {
      this.menuManager.updateMenuItemEnabledState('app.check-for-updates', true)
    })
  }

  relaunch (page = 'index') {
    this.stop()
    app.relaunch()
    app.exit()
    // this.closePage(page)
    // if (page === 'index') {
    //   this.engine.restart()
    // }
    // setTimeout(() => {
    //   this.showPage(page)
    // }, 500)
  }

  handleCommands () {
    this.on('application:relaunch', () => {
      this.relaunch()
    })

    this.on('application:exit', () => {
      this.engine.stop()
      app.exit()
    })

    this.on('application:show', (page) => {
      this.show(page)
    })

    this.on('application:hide', (page) => {
      this.hide(page)
    })

    this.on('application:reset', () => {
      this.configManager.reset()
      this.relaunch()
    })

    this.on('application:check-for-updates', () => {
      this.updateManager.check()
    })

    this.on('application:change-locale', (locale) => {
      logger.info('[Motrix] application:change-locale===>', locale)
      this.localeManager.changeLanguageByLocale(locale)
        .then(() => {
          this.menuManager.setup(locale)
          this.trayManager.setup(locale)
        })
    })

    this.on('application:open-file', (event) => {
      dialog.showOpenDialog({
        properties: ['openFile'],
        filters: [
          {
            name: 'Torrent',
            extensions: ['torrent']
          }
        ]
      }, (filePaths) => {
        if (!filePaths || filePaths.length === 0) {
          return
        }

        const [filePath] = filePaths
        this.handleFile(filePath)
      })
    })

    this.on('application:clear-recent-tasks', () => {
      app.clearRecentDocuments()
    })

    this.on('help:official-website', () => {
      const url = 'https://motrix.app/'
      shell.openExternal(url)
    })

    this.on('help:manual', () => {
      const url = 'https://motrix.app/manual'
      shell.openExternal(url)
    })

    this.on('help:release-notes', () => {
      const url = 'https://motrix.app/release'
      shell.openExternal(url)
    })

    this.on('help:report-problem', () => {
      const url = 'https://motrix.app/report'
      shell.openExternal(url)
    })
  }

  handleIpcMessages () {
    ipcMain.on('command', (event, command, ...args) => {
      logger.log('receive command', command, ...args)
      this.emit(command, ...args)
    })

    ipcMain.on('update-menu-states', (event, visibleStates, enabledStates, checkedStates) => {
      this.menuManager.updateStates(visibleStates, enabledStates, checkedStates)
    })
  }
}


'use strict'

process.env.NODE_ENV = 'production'

const { say } = require('cfonts')
const chalk = require('chalk')
const del = require('del')
const { spawn } = require('child_process')
const webpack = require('webpack')
const Multispinner = require('multispinner')

const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
const webConfig = require('./webpack.web.config')

const doneLog = chalk.bgGreen.white(' DONE ') + ' '
const errorLog = chalk.bgRed.white(' ERROR ') + ' '
const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
const isCI = process.env.CI || false

if (process.env.BUILD_TARGET === 'clean') clean()
else if (process.env.BUILD_TARGET === 'web') web()
else build()

function clean () {
  del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
  console.log(`\n${doneLog}\n`)
  process.exit()
}

function build () {
  greeting()

  del.sync(['dist/electron/*', '!.gitkeep'])

  const tasks = ['main', 'renderer']
  const m = new Multispinner(tasks, {
    preText: 'building',
    postText: 'process'
  })

  let results = ''

  m.on('success', () => {
    process.stdout.write('\x1B[2J\x1B[0f')
    console.log(`\n\n${results}`)
    console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
    process.exit()
  })

  pack(mainConfig).then(result => {
    results += result + '\n\n'
    m.success('main')
  }).catch(err => {
    m.error('main')
    console.log(`\n  ${errorLog}failed to build main process`)
    console.error(`\n${err}\n`)
    process.exit(1)
  })

  pack(rendererConfig).then(result => {
    results += result + '\n\n'
    m.success('renderer')
  }).catch(err => {
    m.error('renderer')
    console.log(`\n  ${errorLog}failed to build renderer process`)
    console.error(`\n${err}\n`)
    process.exit(1)
  })
}

function pack (config) {
  return new Promise((resolve, reject) => {
    config.mode = 'production'
    webpack(config, (err, stats) => {
      if (err) reject(err.stack || err)
      else if (stats.hasErrors()) {
        let err = ''

        stats.toString({
          chunks: false,
          colors: true
        })
        .split(/\r?\n/)
        .forEach(line => {
          err += `    ${line}\n`
        })

        reject(err)
      } else {
        resolve(stats.toString({
          chunks: false,
          colors: true
        }))
      }
    })
  })
}

function web () {
  del.sync(['dist/web/*', '!.gitkeep'])
  webConfig.mode = 'production'
  webpack(webConfig, (err, stats) => {
    if (err || stats.hasErrors()) console.log(err)

    console.log(stats.toString({
      chunks: false,
      colors: true
    }))

    process.exit()
  })
}

function greeting () {
  const cols = process.stdout.columns
  let text = ''

  if (cols > 85) text = 'lets-build'
  else if (cols > 60) text = 'lets-|build'
  else text = false

  if (text && !isCI) {
    say(text, {
      colors: ['yellow'],
      font: 'simple3d',
      space: false
    })
  } else console.log(chalk.yellow.bold('\n  lets-build'))
  console.log()
}


import { EventEmitter } from 'events'

export default class CommandManager extends EventEmitter {
  constructor () {
    super()

    this.commands = {}
  }

  register (id, fn) {
    if (this.commands[id]) {
      console.log('Attempting to register an already-registered command: ' + id)
      return null
    }
    if (!id || !fn) {
      console.error('Attempting to register a command with a missing id, or command function.')
      return null
    }
    this.commands[id] = fn

    this.emit('commandRegistered', id)
  }

  execute (id, ...args) {
    var fn = this.commands[id]
    if (fn) {
      try {
        this.emit('beforeExecuteCommand', id)
      } catch (err) {
        console.error(err)
      }
      const result = fn(...args)
      return result
    } else {
      return false
    }
  }
}


downloadSpeed: state => state.stat.downloadSpeed,


Copyright 2018 Dr_rOot

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


export default {
  'basic': '基础设置',
  'advanced': '进阶设置',
  'lab': '实验室',
  'save': '保存并应用',
  'discard': '放弃',
  'startup': '启动',
  'auto-resume-all': '启动后自动开始未完成任务',
  'default-dir': '默认下载路径',
  'mas-default-dir-tip': '因 App Store 的沙箱权限限制，默认下载路径建议设置为您的「下载」目录',
  'task-manage': '任务管理',
  'max-concurrent-downloads': '同时下载的最大任务数',
  'max-connection-per-server': '每个服务器最大连接数',
  'new-task-show-downloading': '新建任务后自动跳转到下载页面',
  'continue': '断点续传',
  'task-completed-notify': '下载完成后通知',
  'auto-purge-record': '当应用退出时自动清除下载记录',
  'ui': '界面',
  'language': '语言',
  'change-language': '切换语言',
  'hide-app-menu': '隐藏菜单栏（仅支持 Windows 和 Linux）',
  'proxy': '代理',
  'use-proxy': '使用代理服务器',
  'developer': '开发者',
  'mock-user-agent': '模拟用户代理（UA）',
  'app-log-path': '应用日志路径',
  'download-session-path': '下载会话路径',
  'factory-reset': '恢复初始设置',
  'factory-reset-confirm': '你确定要恢复为初始设置吗?',
  'lab-warning': '⚠启用实验特性可能造成应用崩溃或数据丢失，请自行决定！',
  'download-protocol': '下载协议',
  'support-more-download-protocols': '支持更多下载协议',
  'browser-extensions': '浏览器扩展',
  'baidu-exporter': '百度网盘助手',
  'browser-extensions-tip': '社区提供的浏览器扩展「不保证可用性」，',
  'baidu-exporter-help': '点此查看使用说明'
}


/**
 * Welcome to translate to more versions in other languages.
 * Please read the translation guide before starting work.
 * https://github.com/agalwood/Motrix/blob/master/CONTRIBUTING.md#-translation-guide
 *
 * Please keep the locale key in alphabetical order.
 */
export const availableLanguages = [
  {
    value: 'de',
    label: 'Deutsch'
  },
  {
    value: 'en-US',
    label: 'English'
  },
  {
    value: 'fr',
    label: 'Français'
  },
  {
    value: 'pt-BR',
    label: 'Português (Brasil)'
  },
  {
    value: 'tr',
    label: 'Türkçe'
  },
  {
    value: 'zh-CN',
    label: '简体中文'
  },
  {
    value: 'zh-TW',
    label: '繁體中文'
  }
]

function checkLngIsAvailable (locale) {
  return availableLanguages.some((lng) => lng.value === locale)
}

/**
 * getLanguage
 * @param { String } locale
 * https://electronjs.org/docs/api/locales
 */
export function getLanguage (locale = 'en-US') {
  if (checkLngIsAvailable(locale)) {
    return locale
  }

  if (locale.startsWith('de')) {
    return 'de'
  }

  if (locale.startsWith('en')) {
    return 'en-US'
  }

  // If there is a pt-PT translation in the future,
  // here will fallback to pt-PT.
  if (locale.startsWith('pt')) {
    return 'pt-BR'
  }

  if (locale.startsWith('zh')) {
    return 'zh-CN'
  }

  if (locale.startsWith('fr')) {
    return 'fr'
  }
}


taskItemInfoVisible: state => state.taskItemInfoVisible,

numActive: state => state.stat.numActive


<template>

      {{ taskFullName }}

    <mo-task-item-actions mode="LIST" :task="task" />

      <mo-task-progress :completed="Number(task.completedLength)" :total="Number(task.totalLength)" :status="task.status" />
      <el-row class="task-speed">
        <el-col :span="12" class="task-speed-left">

 0">
          {{ task.completedLength | bytesToSize }} / {{ task.totalLength | bytesToSize }}

        </el-col>
        <el-col :span="12" class="task-speed-right">

            {{ task.downloadSpeed | bytesToSize }}/s

              {{
                remaining | timeFormat({
                  prefix: $t('task.remaining-prefix'),
                  i18n: {
                    'gt1d': $t('app.gt1d'),
                    'hour': $t('app.hour'),
                    'minute': $t('app.minute'),
                    'second': $t('app.second')
                  }
                })
              }}

        </el-col>
      </el-row>

</template>


export default {
  'app': 'Motrix',
  'file': '檔案',
  'task': '任務',
  'edit': '編輯',
  'window': '視窗',
  'help': '說明'
}


if (val !== oldVal && this.isRenderer) {


import Icon from '@/components/Icons/Icon'

Icon.register({
  'win-minimize': {
    'width': 12,
    'height': 12,
    'raw': `<line x1="1" y1="6" x2="11" y2="6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>`,
    'g': {
      'stroke': 'currentColor',
      'stroke-linecap': 'round',
      'stroke-linejoin': 'round',
      'stroke-width': '1'
    }
  }
})


# Motrix

[English](./README.md) | 简体中文

## 一款全能的下载工具
[![Build Status](https://travis-ci.org/agalwood/Motrix.svg?branch=master)](https://travis-ci.org/agalwood/Motrix) [![Build status](https://ci.appveyor.com/api/projects/status/l11d5h05xwwcvoux/branch/master?svg=true)](https://ci.appveyor.com/project/agalwood/motrix/branch/master) ![Total Downloads](https://img.shields.io/github/downloads/agalwood/Motrix/total.svg) ![Support Platforms](https://camo.githubusercontent.com/a50c47295f350646d08f2e1ccd797ceca3840e52/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d6d61634f5325323025374325323057696e646f77732532302537432532304c696e75782d6c69676874677265792e737667)

我是个兴趣使然的桌面应用开发者🤓，利用搬砖之余开发了 Motrix。

Motirx 是一款全能的下载工具，支持下载 HTTP、FTP、BT、磁力链、百度网盘等资源。它的界面简洁易用，希望大家喜欢 👻。

✈ 去 [官网](https://motrix.app/zh-CN) 逛逛  |  📖 查看 [帮助手册](http://motrix.app/support/issues)

## 💽 安装稳定版
[GitHub](https://github.com/agalwood/Motrix/releases) 和 [官网](https://motrix.app/zh-CN) 提供了已经编译好的稳定版安装包，当然你也可以自己克隆代码编译打包。

更新：macOS 用户支持 `brew cask` 安装，感谢 [Mitscherlich](https://github.com/Mitscherlich) 的 [PR](https://github.com/Homebrew/homebrew-cask/pull/59494)。

```bash
brew update && brew cask install motrix
```

## ✨ 特性
- 🕹 简洁明了的图形操作界面
- 🦄 支持BT和磁力链任务
- 💾 支持下载百度云盘资源
- 🎛 最高支持 10 个任务同时下载
- 🚀 单任务最高支持 64 线程下载
- 🕶 模拟用户代理UA
- 🔔 下载完成后通知
- 💻 支持触控栏快捷健 (Mac 专享)
- 🗑 移除任务时可同时删除相关文件
- 🌍 国际化，[查看已可选的语言](#-国际化)
- 🎏 ...

## 🖥 应用界面
![motrix-screenshot-task-cn.png](https://cdn.nlark.com/yuque/0/2019/png/129147/1550151234585-e513bd4f-e127-402f-accb-1ebbba9b3c41.png)

## ⌨ 本地开发

### 克隆代码
```bash
git clone git@github.com:agalwood/Motrix.git
```

### 安装依赖
```bash
cd Motrix
npm install
```
天朝大陆用户建议使用淘宝的npm源
```bash
npm config set registry 'https://registry.npm.taobao.org'
export ELECTRON_MIRROR='https://npm.taobao.org/mirrors/electron/'
export SASS_BINARY_SITE='https://npm.taobao.org/mirrors/node-sass'
```
如果喜欢 [Yarn](https://yarnpkg.com/)，也可以使用 `yarn` 安装依赖

### 开发模式
```bash
npm run dev
```

### 编译打包
```bash
npm run build
```
完成之后可以在项目的 `release` 目录看到编译打包好的应用文件

## 🛠 技术栈
- [Electron](https://electronjs.org/)
- [Vue](https://vuejs.org/) + [VueX](https://vuex.vuejs.org/) + [Element](https://element.eleme.io)
- [Aria2](https://aria2.github.io/) (注：macOS 和 Linux 版本使用的是 64 位的 aria2c，Windows 版使用的 32 位的）

## ☑ TODO
开发计划请移步 [Trello](https://trello.com/b/qNUzA0bv/motrix) 查看

## 🤝 参与共建 [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
如果你有兴趣参与共同开发，欢迎 FORK 和 PR。

## 🌍 国际化
欢迎大家将 Motrix 翻译成更多的语言版本 🧐，开工之前请先阅读一下 [翻译指南](./CONTRIBUTING-CN.md#-翻译指南)。

| Key   | Name                | Status       |
|-------|:--------------------|:-------------|
| de    | German              | 下版本发布 [@Schloemicher](https://github.com/Schloemicher) |
| en-US | English             | ✔           |
| fr    | Français            | 下版本发布 [@gpatarin](https://github.com/gpatarin) |
| pt-BR | Portuguese (Brazil) | 下版本发布 [@andrenoberto](https://github.com/andrenoberto) |
| tr    | Türkçe              | 下版本发布 [@abdullah](https://github.com/abdullah) |
| zh-CN | 简体中文             | ✔           |
| zh-TW | 繁體中文             | 下版本发布 [@Yukaii](https://github.com/Yukaii) |

## 📜 开源许可
基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。


export default {
  'active': '下載中',
  'waiting': '等待中',
  'stopped': '已停止',
  'new-task': '新增任務',
  'new-bt-task': '新增 BT 任務',
  'open-file': '打開種子文件...',
  'uri-task': '連結任務',
  'torrent-task': '種子任務',
  'uri-task-tip': '新增多個下載連結時，請確保每行只有一個連結（支援磁力連結）',
  'thunder-link-tip': '友情提示：迅雷連結解碼之後的資源不一定存在',
  'task-name': '任務名稱',
  'task-out': '重新命名',
  'task-out-tip': '選填（僅支援單一任務）',
  'task-split': '下載執行緒',
  'task-dir': '儲存資料夾',
  'task-ua': 'UA',
  'task-user-agent': 'User-Agent',
  'task-referer': 'Referer',
  'task-cookie': 'Cookie',
  'navigate-to-downloading': '跳轉到下載頁面',
  'show-advanced-options': '進階選項',
  'copyright-warning': '版權警告',
  'copyright-warning-message': '您要下載的檔案可能是有版權的音訊視訊，請確保您有相應的版權方授權。',
  'copyright-yes': '是，我有版權方授權',
  'copyright-no': '否',
  'copyright-error-message': '因版權问题，新增任務失敗',
  'pause-task': '暫停任務',
  'pause-task-success': '暫停任務 "{{taskName}}" 成功',
  'pause-task-fail': '暫停任務 "{{taskName}}" 失敗',
  'resume-task': '繼續任務',
  'resume-task-success': '繼續任務 "{{taskName}}" 成功',
  'resume-task-fail': '繼續任務 "{{taskName}}" 失敗',
  'delete-task': '移除任務',
  'delete-selected-tasks': '移除選取的任務',
  'delete-task-confirm': '你確定要移除 "{{taskName}}" 下載任務嗎?',
  'delete-task-label': '同時删除檔案',
  'delete-task-success': '移除任務 "{{taskName}}" 成功',
  'delete-task-fail': '移除任務 "{{taskName}}" 失敗',
  'remove-task-file-fail': '删除任務檔案失敗，請手動删除',
  'remove-task-config-file-fail': '删除任務設定檔失敗，請手動删除',
  'move-task-up': '上移任務',
  'move-task-down': '下載任務',
  'pause-all-task': '暫停所有任務',
  'pause-all-task-success': '暫停所有任務成功',
  'pause-all-task-fail': '暫停所有任務失敗',
  'resume-all-task': '繼續所有任務',
  'resume-all-task-success': '繼續所有任務成功',
  'resume-all-task-fail': '繼續所有任務失敗',
  'clear-recent-tasks': '清除最近的下載紀錄',
  'purge-record': '清除下載紀錄',
  'purge-record-success': '清除下載紀錄成功',
  'purge-record-fail': '清除下載紀錄失敗',
  'refresh-list': '重整任務清單',
  'no-task': '目前没有下載任務',
  'copy-link': '複製連結',
  'copy-link-success': '複製連結成功',
  'remove-record': '移除下載紀錄',
  'remove-record-confirm': '你確定要移除 "{{taskName}}" 下載紀錄吗?',
  'remove-record-label': '同时删除檔案',
  'remove-record-success': '移除 "{{taskName}}" 下載紀錄成功',
  'remove-record-fail': '移除 "{{taskName}}" 下載紀錄失敗',
  'show-in-folder': '在資料夹中顯示',
  'file-not-exist': '目標檔案不存在或已删除',
  'file-path-error': '檔案路徑錯誤',
  'opening-task-message': '正在打開 "{{taskName}}" ...',
  'get-task-name': '抓取任務名稱中...',
  'remaining-prefix': '剩下',
  'select-torrent': '將種子拖曳到此，或點選來選取',
  'task-info-dialog-title': '{{title}} 詳細資訊',
  'download-start-message': '開始下載 {{taskName}}',
  'download-pause-message': '暫停下載 {{taskName}}',
  'download-stop-message': '{{taskName}} 下載中止',
  'download-error-message': '{{taskName}} 下載錯誤',
  'download-complete-message': '{{taskName}} 下載完成',
  'download-complete-notify': '下載完成',
  'download-fail-message': '{{taskName}} 下載失敗',
  'download-fail-notify': '下載失敗'
}