├── docs ├── _config.yml ├── logo.png ├── workflow.png ├── screenshots │ └── intro.2018.01.13.gif ├── demo.html ├── index.html ├── install.md ├── releases.md └── advanced.md ├── bin ├── leetcode ├── entrypoint ├── pkg.sh ├── pkg ├── pkg.bat └── install ├── .npmignore ├── test ├── mock │ ├── find-the-difference-star.json.20200821 │ ├── find-the-difference-unstar.json.20200821 │ ├── find-the-difference.json.20171216 │ ├── two-sum.submissions.json.20170425 │ └── add-two-numbers.20161015.json ├── helper.js ├── test_sprintf.js ├── test_cache.js ├── test_queue.js ├── test_session.js ├── test_icon.js ├── test_config.js ├── plugins │ ├── test_retry.js │ └── test_cache.js ├── test_log.js ├── test_chalk.js ├── test_file.js ├── test_plugin.js ├── test_helper.js └── test_core.js ├── .dockerignore ├── colors ├── dark.json ├── blue.json ├── pink.json ├── molokai.json ├── orange.json ├── solarized.json ├── solarized.light.json └── default.json ├── icons ├── ascii.json ├── default.json └── win7.json ├── templates ├── codeonly.tpl └── detailed.tpl ├── Dockerfile ├── lib ├── icon.js ├── cache.js ├── queue.js ├── commands │ ├── star.js │ ├── cache.js │ ├── version.js │ ├── config.js │ ├── session.js │ ├── test.js │ ├── plugin.js │ ├── submit.js │ ├── list.js │ ├── user.js │ ├── submission.js │ ├── show.js │ └── stat.js ├── session.js ├── sprintf.js ├── log.js ├── plugins │ ├── retry.js │ ├── solution.discuss.js │ ├── cache.js │ └── leetcode.cn.js ├── cli.js ├── chalk.js ├── config.js ├── core.js ├── file.js ├── plugin.js └── helper.js ├── .gitignore ├── .lc-completion.bash ├── .vscode └── launch.json ├── .travis.yml ├── .github └── ISSUE_TEMPLATE.md ├── .eslintrc.js ├── LICENSE ├── package.json └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /bin/leetcode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli').run(); 4 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/HEAD/docs/workflow.png -------------------------------------------------------------------------------- /docs/screenshots/intro.2018.01.13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/HEAD/docs/screenshots/intro.2018.01.13.gif -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | docs/ 3 | test/ 4 | .eslintrc.js 5 | .gitignore 6 | .travis.yml 7 | Dockerfile 8 | .dockerignore 9 | .nyc_output/ 10 | .vscode/ 11 | coverage/ -------------------------------------------------------------------------------- /test/mock/find-the-difference-star.json.20200821: -------------------------------------------------------------------------------- 1 | {"data":{"addQuestionToFavorite":{"ok":true,"error":null,"favoriteIdHash":"","questionId":"389","__typename":"AddQuestionToFavorite"}}} -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .npm 4 | .nyc_output 5 | .DS_Store 6 | 7 | coverage 8 | dist 9 | node_modules 10 | npm-debug.log* 11 | tmp 12 | 13 | *.log 14 | *.swp 15 | -------------------------------------------------------------------------------- /docs/demo.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Showcases 4 | --- 5 |
7 |
8 | \r\nGiven two strings s and t which consist of only lowercase letters.
\r\n\r\nString t is generated by random shuffling string s and then add one more letter at a random position.
\r\n\r\nFind the letter that was added in t.
\r\n\r\nExample:\r\n
\r\nInput:\r\ns = \"abcd\"\r\nt = \"abcde\"\r\n\r\nOutput:\r\ne\r\n\r\nExplanation:\r\n'e' is the letter that was added.\r\n","stats":"{\"totalAccepted\": \"89.7K\", \"totalSubmission\": \"175.7K\"}","codeDefinition":"[{\"text\": \"C++\", \"value\": \"cpp\", \"defaultCode\": \"class Solution {\\r\\npublic:\\r\\n char findTheDifference(string s, string t) {\\r\\n \\r\\n }\\r\\n};\"}, {\"text\": \"Java\", \"value\": \"java\", \"defaultCode\": \"class Solution {\\r\\n public char findTheDifference(String s, String t) {\\r\\n \\r\\n }\\r\\n}\"}, {\"text\": \"Python\", \"value\": \"python\", \"defaultCode\": \"class Solution(object):\\r\\n def findTheDifference(self, s, t):\\r\\n \\\"\\\"\\\"\\r\\n :type s: str\\r\\n :type t: str\\r\\n :rtype: str\\r\\n \\\"\\\"\\\"\\r\\n \"}, {\"text\": \"Python3\", \"value\": \"python3\", \"defaultCode\": \"class Solution:\\r\\n def findTheDifference(self, s, t):\\r\\n \\\"\\\"\\\"\\r\\n :type s: str\\r\\n :type t: str\\r\\n :rtype: str\\r\\n \\\"\\\"\\\"\\r\\n \"}, {\"text\": \"C\", \"value\": \"c\", \"defaultCode\": \"char findTheDifference(char* s, char* t) {\\r\\n \\r\\n}\"}, {\"text\": \"C#\", \"value\": \"csharp\", \"defaultCode\": \"public class Solution {\\r\\n public char FindTheDifference(string s, string t) {\\r\\n \\r\\n }\\r\\n}\"}, {\"text\": \"JavaScript\", \"value\": \"javascript\", \"defaultCode\": \"/**\\r\\n * @param {string} s\\r\\n * @param {string} t\\r\\n * @return {character}\\r\\n */\\r\\nvar findTheDifference = function(s, t) {\\r\\n \\r\\n};\"}, {\"text\": \"Ruby\", \"value\": \"ruby\", \"defaultCode\": \"# @param {String} s\\r\\n# @param {String} t\\r\\n# @return {Character}\\r\\ndef find_the_difference(s, t)\\r\\n \\r\\nend\"}, {\"text\": \"Swift\", \"value\": \"swift\", \"defaultCode\": \"class Solution {\\r\\n func findTheDifference(_ s: String, _ t: String) -> Character {\\r\\n \\r\\n }\\r\\n}\"}, {\"text\": \"Go\", \"value\": \"golang\", \"defaultCode\": \"func findTheDifference(s string, t string) byte {\\r\\n \\r\\n}\"}, {\"text\": \"Scala\", \"value\": \"scala\", \"defaultCode\": \"object Solution {\\n def findTheDifference(s: String, t: String): Char = {\\n \\n }\\n}\"}, {\"text\": \"Kotlin\", \"value\": \"kotlin\", \"defaultCode\": \"class Solution {\\n fun findTheDifference(s: String, t: String): Char {\\n \\n }\\n}\"}]","sampleTestCase":"\"abcd\"\n\"abcde\"","enableRunCode":true,"metaData":"{\r\n \"name\": \"findTheDifference\",\r\n \"params\": [\r\n {\r\n \"name\": \"s\",\r\n \"type\": \"string\"\r\n },\r\n {\r\n \"name\": \"t\",\r\n \"type\": \"string\"\r\n }\r\n ],\r\n \"return\": {\r\n \"type\": \"character\"\r\n }\r\n}","discussCategoryId":"511"}}} -------------------------------------------------------------------------------- /test/mock/two-sum.submissions.json.20170425: -------------------------------------------------------------------------------- 1 | {"has_next":true,"submissions_dump":[{"lang":"cpp","time":"1 month, 3 weeks","status_display":"Accepted","runtime":"12 ms","url":"/submissions/detail/95464136/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 1 week","status_display":"Accepted","runtime":"13 ms","url":"/submissions/detail/78502271/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Accepted","runtime":"9 ms","url":"/submissions/detail/77791021/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77790928/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Accepted","runtime":"13 ms","url":"/submissions/detail/77685402/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Wrong Answer","runtime":"N/A","url":"/submissions/detail/77685362/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Time Limit Exceeded","runtime":"N/A","url":"/submissions/detail/77685329/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77685279/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77685195/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Wrong Answer","runtime":"N/A","url":"/submissions/detail/77685140/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Wrong Answer","runtime":"N/A","url":"/submissions/detail/77684623/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77684584/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77684436/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Time Limit Exceeded","runtime":"N/A","url":"/submissions/detail/77684406/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77684353/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77683773/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77683680/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77683411/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77683347/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Time Limit Exceeded","runtime":"N/A","url":"/submissions/detail/77682978/","is_pending":false,"title":"Two Sum"}]} 2 | -------------------------------------------------------------------------------- /lib/chalk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | var style = require('ansi-styles'); 4 | var supportsColor = require('supports-color'); 5 | 6 | var file = require('./file'); 7 | 8 | const chalk = { 9 | enabled: supportsColor.stdout, 10 | use256: supportsColor.stdout && supportsColor.stdout.has256, 11 | use16m: supportsColor.stdout && supportsColor.stdout.has16m, 12 | themes: new Map(), 13 | theme: {} 14 | }; 15 | 16 | const pres = []; 17 | const posts = []; 18 | 19 | const DEFAULT = { 20 | black: '#000000', 21 | blue: '#0000ff', 22 | cyan: '#00ffff', 23 | gray: '#999999', 24 | green: '#00ff00', 25 | magenta: '#ff00ff', 26 | red: '#ff0000', 27 | white: '#ffffff', 28 | yellow: '#ffff00' 29 | }; 30 | 31 | chalk.setTheme = function(name) { 32 | this.theme = this.themes.get(name) || this.themes.get('default'); 33 | }; 34 | 35 | chalk.sprint = function(s, hex) { 36 | const color = chalk.use16m ? style.color.ansi16m.hex(hex) 37 | : chalk.use256 ? style.color.ansi256.hex(hex) 38 | : style.color.ansi.hex(hex); 39 | return color + s + style.color.close; 40 | }; 41 | 42 | chalk.print = function(s) { 43 | s = this.enabled ? pres.join('') + s + posts.join('') : s; 44 | pres.length = posts.length = 0; 45 | return s; 46 | }; 47 | 48 | chalk.wrap = function(pre, post) { 49 | pres.push(pre); 50 | posts.unshift(post); 51 | const f = x => chalk.print(x); 52 | Object.setPrototypeOf(f, chalk); 53 | return f; 54 | }; 55 | 56 | const bgName = x => 'bg' + x[0].toUpperCase() + x.substr(1); 57 | 58 | chalk.init = function() { 59 | for (let f of file.listCodeDir('colors')) { 60 | const theme = {}; 61 | const data = _.extendOwn({}, DEFAULT, f.data); 62 | for (let x of _.pairs(data)) { 63 | const k = x[0]; 64 | const v = x[1]; 65 | const bgK = bgName(k); 66 | 67 | if (chalk.use16m) { 68 | theme[k] = style.color.ansi16m.hex(v); 69 | theme[bgK] = style.bgColor.ansi16m.hex(v); 70 | } else if (chalk.use256) { 71 | theme[k] = style.color.ansi256.hex(v); 72 | theme[bgK] = style.bgColor.ansi256.hex(v); 73 | } else { 74 | theme[k] = style.color.ansi.hex(v); 75 | theme[bgK] = style.bgColor.ansi.hex(v); 76 | } 77 | } 78 | chalk.themes.set(f.name, theme); 79 | } 80 | 81 | for (let color of ['black', 'blue', 'cyan', 'gray', 'green', 'magenta', 'red', 'white', 'yellow']) { 82 | Object.defineProperty(chalk, color, { 83 | get: () => chalk.wrap(chalk.theme[color], style.color.close), 84 | configurable: true 85 | }); 86 | const bgcolor = bgName(color); 87 | Object.defineProperty(chalk, bgcolor, { 88 | get: () => chalk.wrap(chalk.theme[bgcolor], style.bgColor.close), 89 | configurable: true 90 | }); 91 | } 92 | 93 | for (let modifier of ['bold', 'dim', 'italic', 'inverse', 'strikethrough', 'underline']) { 94 | Object.defineProperty(chalk, modifier, { 95 | get: () => chalk.wrap(style[modifier].open, style[modifier].close), 96 | configurable: true 97 | }); 98 | } 99 | }; 100 | 101 | module.exports = chalk; 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://www.npmjs.com/package/leetcode-cli) 2 | [](https://github.com/skygragon/leetcode-cli/releases) 3 | [](https://github.com/skygragon/leetcode-cli/blob/master/LICENSE) 4 | [](https://travis-ci.org/skygragon/leetcode-cli) 5 | [](https://gitter.im/skygragon/leetcode-cli) 6 | 7 | # leetcode-cli 8 | 9 | > Note: This repository is forked from [leetcode-cli](https://github.com/skygragon/leetcode-cli) for temporary usage. 10 | > Note: Copy cookie from webbrowser and Using **leetcode user -c** can temporary fix can't [login problem](https://github.com/jdneo/vscode-leetcode/issues/478). 11 | 12 |
13 |
14 | A productive cli tool to enjoy leetcode!
15 |
16 | Great thanks to leetcode.com, a really awesome website!
17 |
18 | ⦙ [Releases](https://skygragon.github.io/leetcode-cli/releases) ⦙
19 | [Install](https://skygragon.github.io/leetcode-cli/install) ⦙
20 | [Docs](https://skygragon.github.io/leetcode-cli/) ⦙
21 | [Commands](https://skygragon.github.io/leetcode-cli/commands) ⦙
22 | [Advanced](https://skygragon.github.io/leetcode-cli/advanced) ⦙
23 | [Plugins](https://github.com/skygragon/leetcode-cli-plugins) ⦙
24 |
25 | * A very [**EFFICIENT**](#quick-start) way to fight questions.
26 | * [**CACHING**](https://skygragon.github.io/leetcode-cli/advanced#cache) questions to ease offline thinking.
27 | * [**GENERATING**](https://skygragon.github.io/leetcode-cli/commands#show) source code before coding.
28 | * Live [**TEST**](https://skygragon.github.io/leetcode-cli/commands#test) and [**SUBMIT**](https://skygragon.github.io/leetcode-cli/commands#submit) with leetcode.com.
29 | * Download your previous [**SUBMISSION**](https://skygragon.github.io/leetcode-cli/commands#submission).
30 | * Trace your coding [**STATUS**](https://skygragon.github.io/leetcode-cli/commands#stat).
31 | * [**AUTO LOGIN**](https://skygragon.github.io/leetcode-cli/advanced#auto-login) among multiple agents with single account.
32 | * Multiple [**THEMES**](https://skygragon.github.io/leetcode-cli/advanced#color-themes) support.
33 | * More [**PLUGINS**](https://skygragon.github.io/leetcode-cli/advanced#plugins) to enjoy extra features!
34 |
35 | ## Screenshot
36 |
37 |
38 |
39 | ## Quick Start
40 |
41 | Read help first $ leetcode help
42 | Login with your leetcode account $ leetcode user -l
43 | Login with third party account--GitHub $ leetcode user -g
44 | Login with third party account--LinkedIn $ leetcode user -i
45 | Cookie login with cookie $ leetcode user -c
46 | Browse all questions $ leetcode list
47 | Choose one question $ leetcode show 1 -g -l cpp
48 | Coding it!
49 | Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7'
50 | Submit final solution! $ leetcode submit ./two-sum.cpp
51 |
--------------------------------------------------------------------------------
/test/test_log.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('chai').assert;
3 | const rewire = require('rewire');
4 |
5 | const chalk = require('../lib/chalk');
6 |
7 | describe('log', function() {
8 | let log;
9 | let savedOutput;
10 | let expected;
11 |
12 | before(function() {
13 | chalk.init();
14 | });
15 |
16 | beforeEach(function() {
17 | log = rewire('../lib/log');
18 | savedOutput = log.output;
19 | log.output = x => expected = x;
20 |
21 | log.init();
22 | expected = '';
23 | });
24 |
25 | afterEach(function() {
26 | log.output = savedOutput;
27 | });
28 |
29 | describe('#setLevel', function() {
30 | it('should ok with known level', function() {
31 | log.setLevel('TRACE');
32 | assert.deepEqual(log.level, log.levels.get('TRACE'));
33 | log.setLevel('DEBUG');
34 | assert.deepEqual(log.level, log.levels.get('DEBUG'));
35 | log.setLevel('INFO');
36 | assert.deepEqual(log.level, log.levels.get('INFO'));
37 | log.setLevel('WARN');
38 | assert.deepEqual(log.level, log.levels.get('WARN'));
39 | log.setLevel('ERROR');
40 | assert.deepEqual(log.level, log.levels.get('ERROR'));
41 | });
42 |
43 | it('should ok with unknown level', function() {
44 | log.setLevel('');
45 | assert.deepEqual(log.level, log.levels.get('INFO'));
46 | });
47 | }); // #setLevel
48 |
49 | describe('#isEnabled', function() {
50 | it('should ok', function() {
51 | log.setLevel('DEBUG');
52 | assert.equal(log.isEnabled('TRACE'), false);
53 | assert.equal(log.isEnabled('DEBUG'), true);
54 | assert.equal(log.isEnabled('INFO'), true);
55 | assert.equal(log.isEnabled('WARN'), true);
56 | assert.equal(log.isEnabled('ERROR'), true);
57 | });
58 | }); // #isEnabled
59 |
60 | describe('#levels', function() {
61 | it('should ok with log.trace', function() {
62 | log.trace('some error');
63 | assert.equal(expected, '');
64 |
65 | log.setLevel('TRACE');
66 | log.trace('some error');
67 | assert.equal(expected, chalk.gray('[TRACE] some error'));
68 | });
69 |
70 | it('should ok with log.debug', function() {
71 | log.debug('some error');
72 | assert.equal(expected, '');
73 |
74 | log.setLevel('DEBUG');
75 | log.debug('some error');
76 | assert.equal(expected, chalk.gray('[DEBUG] some error'));
77 | });
78 |
79 | it('should ok with log.info', function() {
80 | log.info('some error');
81 | assert.equal(expected, 'some error');
82 | });
83 |
84 | it('should ok with log.warn', function() {
85 | log.warn('some error');
86 | assert.equal(expected, chalk.yellow('[WARN] some error'));
87 | });
88 |
89 | it('should ok with log.error', function() {
90 | log.error('some error');
91 | assert.equal(expected, chalk.red('[ERROR] some error'));
92 | });
93 |
94 | it('should ok with log.fail', function() {
95 | log.fail({msg: 'some error', statusCode: 500});
96 | assert.equal(expected, chalk.red('[ERROR] some error [code=500]'));
97 |
98 | log.fail('some error');
99 | assert.equal(expected, chalk.red('[ERROR] some error'));
100 | });
101 | }); // #levels
102 |
103 | describe('#printf', function() {
104 | it('should ok', function() {
105 | log.printf('%s and %s and %%', 'string', 100);
106 | assert.equal(expected, 'string and 100 and %');
107 | });
108 | }); // #printf
109 | });
110 |
--------------------------------------------------------------------------------
/test/test_chalk.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('chai').assert;
3 | const rewire = require('rewire');
4 |
5 | // refer to https://en.wikipedia.org/wiki/ANSI_escape_code
6 | describe('chalk', function() {
7 | let chalk;
8 |
9 | beforeEach(function() {
10 | chalk = rewire('../lib/chalk');
11 | chalk.enabled = true;
12 | chalk.use256 = true;
13 | chalk.use16m = false;
14 | });
15 |
16 | it('should ok w/ 256 colors', function() {
17 | chalk.init();
18 | chalk.setTheme('default');
19 |
20 | assert.equal(chalk.black(' '), '\u001b[38;5;16m \u001b[39m');
21 | assert.equal(chalk.red(' '), '\u001b[38;5;196m \u001b[39m');
22 | assert.equal(chalk.green(' '), '\u001b[38;5;46m \u001b[39m');
23 | assert.equal(chalk.yellow(' '), '\u001b[38;5;226m \u001b[39m');
24 | assert.equal(chalk.blue(' '), '\u001b[38;5;21m \u001b[39m');
25 | assert.equal(chalk.magenta(' '), '\u001b[38;5;201m \u001b[39m');
26 | assert.equal(chalk.cyan(' '), '\u001b[38;5;51m \u001b[39m');
27 | assert.equal(chalk.white(' '), '\u001b[38;5;231m \u001b[39m');
28 |
29 | assert.equal(chalk.bold(' '), '\u001b[1m \u001b[22m');
30 | assert.equal(chalk.dim(' '), '\u001b[2m \u001b[22m');
31 | assert.equal(chalk.italic(' '), '\u001b[3m \u001b[23m');
32 | assert.equal(chalk.inverse(' '), '\u001b[7m \u001b[27m');
33 | assert.equal(chalk.strikethrough(' '), '\u001b[9m \u001b[29m');
34 | assert.equal(chalk.underline(' '), '\u001b[4m \u001b[24m');
35 | });
36 |
37 | it('should ok w/ 8 colors', function() {
38 | chalk.use256 = false;
39 | chalk.init();
40 | chalk.setTheme('default');
41 |
42 | assert.equal(chalk.black(' '), '\u001b[30m \u001b[39m');
43 | assert.equal(chalk.red(' '), '\u001b[91m \u001b[39m');
44 | assert.equal(chalk.green(' '), '\u001b[92m \u001b[39m');
45 | assert.equal(chalk.yellow(' '), '\u001b[93m \u001b[39m');
46 | assert.equal(chalk.blue(' '), '\u001b[94m \u001b[39m');
47 | assert.equal(chalk.magenta(' '), '\u001b[95m \u001b[39m');
48 | assert.equal(chalk.cyan(' '), '\u001b[96m \u001b[39m');
49 | assert.equal(chalk.white(' '), '\u001b[97m \u001b[39m');
50 | });
51 |
52 | it('should ok w/o colors', function() {
53 | chalk.enabled = false;
54 | chalk.init();
55 | chalk.setTheme('default');
56 |
57 | assert.equal(chalk.black(' '), ' ');
58 | assert.equal(chalk.red(' '), ' ');
59 | assert.equal(chalk.green(' '), ' ');
60 | assert.equal(chalk.yellow(' '), ' ');
61 | assert.equal(chalk.blue(' '), ' ');
62 | assert.equal(chalk.magenta(' '), ' ');
63 | assert.equal(chalk.cyan(' '), ' ');
64 | assert.equal(chalk.white(' '), ' ');
65 | });
66 |
67 | it('should sprint w/ 256 colors ok', function() {
68 | chalk.init();
69 | chalk.setTheme('default');
70 | assert.equal(chalk.sprint(' ', '#00ff00'), '\u001b[38;5;46m \u001b[39m');
71 | });
72 |
73 | it('should sprint w/ 8 colors ok', function() {
74 | chalk.use256 = false;
75 | chalk.init();
76 | chalk.setTheme('default');
77 | assert.equal(chalk.sprint(' ', '#00ff00'), '\u001b[92m \u001b[39m');
78 | });
79 |
80 | it('should set theme ok', function() {
81 | chalk.init();
82 | chalk.setTheme('dark');
83 | assert.equal(chalk.sprint(' ', '#009900'), chalk.green(' '));
84 | });
85 |
86 | it('should set unknown theme ok', function() {
87 | chalk.init();
88 | chalk.setTheme('unknown');
89 | assert.equal(chalk.sprint(' ', '#00ff00'), chalk.green(' '));
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/lib/plugins/solution.discuss.js:
--------------------------------------------------------------------------------
1 | var request = require('request');
2 |
3 | var log = require('../log');
4 | var chalk = require('../chalk');
5 | var Plugin = require('../plugin');
6 | var session = require('../session');
7 |
8 | //
9 | // [Usage]
10 | //
11 | // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/solution.discuss.md
12 | //
13 | var plugin = new Plugin(200, 'solution.discuss', '2019.02.03',
14 | 'Plugin to fetch most voted solution in discussions.');
15 |
16 | var URL_DISCUSSES = 'https://leetcode.com/graphql';
17 | var URL_DISCUSS = 'https://leetcode.com/problems/$slug/discuss/$id';
18 |
19 | function getSolution(problem, lang, cb) {
20 | if (!problem) return cb();
21 |
22 | if (lang === 'python3') lang = 'python';
23 |
24 | var opts = {
25 | url: URL_DISCUSSES,
26 | json: true,
27 | body: {
28 | query: [
29 | 'query questionTopicsList($questionId: String!, $orderBy: TopicSortingOption, $skip: Int, $query: String, $first: Int!, $tags: [String!]) {',
30 | ' questionTopicsList(questionId: $questionId, orderBy: $orderBy, skip: $skip, query: $query, first: $first, tags: $tags) {',
31 | ' ...TopicsList',
32 | ' }',
33 | '}',
34 | 'fragment TopicsList on TopicConnection {',
35 | ' totalNum',
36 | ' edges {',
37 | ' node {',
38 | ' id',
39 | ' title',
40 | ' post {',
41 | ' content',
42 | ' voteCount',
43 | ' author {',
44 | ' username',
45 | ' }',
46 | ' }',
47 | ' }',
48 | ' }',
49 | '}'
50 | ].join('\n'),
51 |
52 | operationName: 'questionTopicsList',
53 | variables: JSON.stringify({
54 | query: '',
55 | first: 1,
56 | skip: 0,
57 | orderBy: 'most_votes',
58 | questionId: '' + problem.id,
59 | tags: [lang]
60 | })
61 | }
62 | };
63 | request(opts, function(e, resp, body) {
64 | if (e) return cb(e);
65 | if (resp.statusCode !== 200)
66 | return cb({msg: 'http error', statusCode: resp.statusCode});
67 |
68 | const solutions = body.data.questionTopicsList.edges;
69 | const solution = solutions.length > 0 ? solutions[0].node : null;
70 | return cb(null, solution);
71 | });
72 | }
73 |
74 | plugin.getProblem = function(problem, needTranslation, cb) {
75 | plugin.next.getProblem(problem, needTranslation, function(e, problem) {
76 | if (e || !session.argv.solution) return cb(e, problem);
77 |
78 | var lang = session.argv.lang;
79 | getSolution(problem, lang, function(e, solution) {
80 | if (e) return cb(e);
81 | if (!solution) return log.error('Solution not found for ' + lang);
82 |
83 | var link = URL_DISCUSS.replace('$slug', problem.slug).replace('$id', solution.id);
84 | var content = solution.post.content.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
85 |
86 | log.info();
87 | log.info(problem.name);
88 | log.info();
89 | log.info(solution.title);
90 | log.info();
91 | log.info(chalk.underline(link));
92 | log.info();
93 | log.info('* Lang: ' + lang);
94 | log.info('* Author: ' + solution.post.author.username);
95 | log.info('* Votes: ' + solution.post.voteCount);
96 | log.info();
97 | log.info(content);
98 | });
99 | });
100 | };
101 |
102 | module.exports = plugin;
103 |
--------------------------------------------------------------------------------
/test/mock/add-two-numbers.20161015.json:
--------------------------------------------------------------------------------
1 | {"state":"ac","id":2,"category":"algorithms","name":"Add Two Numbers","key":"add-two-numbers","link":"https://leetcode.com/problems/add-two-numbers","locked":false,"percent":25.368142876074806,"level":"Medium","starred":true,"totalAC":"195263","totalSubmit":"769711","likes": "1","dislikes": "1","desc":"You are given two linked lists representing two non-negative numbers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.\r\n\r\nInput: (2 -> 4 -> 3) + (5 -> 6 -> 4)\r\nOutput: 7 -> 0 -> 8","templates":[{"value":"cpp","text":"C++","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * struct ListNode {\r\n * int val;\r\n * ListNode *next;\r\n * ListNode(int x) : val(x), next(NULL) {}\r\n * };\r\n */\r\nclass Solution {\r\npublic:\r\n ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {\r\n \r\n }\r\n};"},{"value":"java","text":"Java","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * public class ListNode {\r\n * int val;\r\n * ListNode next;\r\n * ListNode(int x) { val = x; }\r\n * }\r\n */\r\npublic class Solution {\r\n public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\r\n \r\n }\r\n}"},{"value":"python","text":"Python","defaultCode":"# Definition for singly-linked list.\r\n# class ListNode(object):\r\n# def __init__(self, x):\r\n# self.val = x\r\n# self.next = None\r\n\r\nclass Solution(object):\r\n def addTwoNumbers(self, l1, l2):\r\n \"\"\"\r\n :type l1: ListNode\r\n :type l2: ListNode\r\n :rtype: ListNode\r\n \"\"\"\r\n "},{"value":"c","text":"C","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * struct ListNode {\r\n * int val;\r\n * struct ListNode *next;\r\n * };\r\n */\r\nstruct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) {\r\n \r\n}"},{"value":"csharp","text":"C#","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * public class ListNode {\r\n * public int val;\r\n * public ListNode next;\r\n * public ListNode(int x) { val = x; }\r\n * }\r\n */\r\npublic class Solution {\r\n public ListNode AddTwoNumbers(ListNode l1, ListNode l2) {\r\n \r\n }\r\n}"},{"value":"javascript","text":"JavaScript","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * function ListNode(val) {\r\n * this.val = val;\r\n * this.next = null;\r\n * }\r\n */\r\n/**\r\n * @param {ListNode} l1\r\n * @param {ListNode} l2\r\n * @return {ListNode}\r\n */\r\nvar addTwoNumbers = function(l1, l2) {\r\n \r\n};"},{"value":"ruby","text":"Ruby","defaultCode":"# Definition for singly-linked list.\r\n# class ListNode\r\n# attr_accessor :val, :next\r\n# def initialize(val)\r\n# @val = val\r\n# @next = nil\r\n# end\r\n# end\r\n\r\n# @param {ListNode} l1\r\n# @param {ListNode} l2\r\n# @return {ListNode}\r\ndef add_two_numbers(l1, l2)\r\n \r\nend"},{"value":"swift","text":"Swift","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * public class ListNode {\r\n * public var val: Int\r\n * public var next: ListNode?\r\n * public init(_ val: Int) {\r\n * self.val = val\r\n * self.next = nil\r\n * }\r\n * }\r\n */\r\nclass Solution {\r\n func addTwoNumbers(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {\r\n \r\n }\r\n}"},{"value":"golang","text":"Go","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * type ListNode struct {\r\n * Val int\r\n * Next *ListNode\r\n * }\r\n */\r\nfunc addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {\r\n \r\n}"}],"testcase":"[2,4,3]\n[5,6,4]","testable":true}
2 |
--------------------------------------------------------------------------------
/lib/commands/session.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var prompt = require('prompt');
3 |
4 | var h = require('../helper');
5 | var chalk = require('../chalk');
6 | var log = require('../log');
7 | var core = require('../core');
8 | var session = require('../session');
9 | var sprintf = require('../sprintf');
10 |
11 | const cmd = {
12 | command: 'session [keyword]',
13 | aliases: ['branch'],
14 | desc: 'Manage sessions',
15 | builder: function(yargs) {
16 | return yargs
17 | .option('c', {
18 | alias: 'create',
19 | type: 'boolean',
20 | describe: 'Create session',
21 | default: false
22 | })
23 | .option('d', {
24 | alias: 'delete',
25 | type: 'boolean',
26 | describe: 'Delete session',
27 | default: false
28 | })
29 | .option('e', {
30 | alias: 'enable',
31 | type: 'boolean',
32 | describe: 'Enable/activate session',
33 | default: false
34 | })
35 | .positional('keyword', {
36 | type: 'string',
37 | describe: 'Session name or id',
38 | default: ''
39 | })
40 | .example(chalk.yellow('leetcode session'), 'Show all cache')
41 | .example(chalk.yellow('leetcode session xxx'), 'Show session by keyword')
42 | .example('', '')
43 | .example(chalk.yellow('leetcode session -c xxx'), 'Create session with name')
44 | .example(chalk.yellow('leetcode session -e xxx'), 'Enable session by keyword')
45 | .example(chalk.yellow('leetcode session -d xxx'), 'Delete session by keyword');
46 | }
47 | };
48 |
49 | function printSessions(e, sessions) {
50 | if (e) return log.fail(e);
51 |
52 | log.info(chalk.gray(sprintf(' %6s %5s %18s %28s %16s',
53 | 'Active', 'Id', 'Name', 'AC Questions', 'AC Submits')));
54 | log.info(chalk.gray('-'.repeat(80)));
55 |
56 | for (let s of sessions) {
57 | let questionRate = 0;
58 | let submissionRate = 0;
59 | if (s.submitted_questions > 0)
60 | questionRate = s.ac_questions * 100 / s.submitted_questions;
61 | if (s.total_submitted > 0)
62 | submissionRate = s.total_acs * 100 / s.total_submitted;
63 |
64 | log.printf(' %s %8s %-26s %6s (%6s %%) %6s (%6s %%)',
65 | s.is_active ? h.prettyState('ac') : ' ',
66 | s.id,
67 | s.name || 'Anonymous Session',
68 | chalk.green(s.ac_questions),
69 | questionRate.toFixed(2),
70 | chalk.green(s.total_acs),
71 | submissionRate.toFixed(2));
72 | }
73 | }
74 |
75 | cmd.handler = function(argv) {
76 | session.argv = argv;
77 |
78 | if (argv.create)
79 | return core.createSession(argv.keyword, printSessions);
80 |
81 | core.getSessions(function(e, sessions) {
82 | if (e) return log.fail(e);
83 |
84 | if (argv.keyword) {
85 | const id = Number(argv.keyword);
86 | sessions = sessions.filter(x => x.name === argv.keyword || x.id === id);
87 | if (sessions.length > 1) return log.fail('Ambiguous sessions?');
88 |
89 | const session = sessions[0];
90 | if (!session) return log.fail('Session not found!');
91 |
92 | if (argv.enable && !session.is_active) {
93 | core.activateSession(session, function(e, sessions) {
94 | if (e) return log.fail(e);
95 | require('../session').deleteCodingSession();
96 | printSessions(e, sessions);
97 | });
98 | return;
99 | }
100 |
101 | if (argv.delete) {
102 | return core.deleteSession(session, printSessions);
103 | }
104 | }
105 | printSessions(null, sessions);
106 | });
107 | };
108 |
109 | module.exports = cmd;
110 |
--------------------------------------------------------------------------------
/lib/commands/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var _ = require('underscore');
3 | var lodash = require('lodash');
4 | var util = require('util');
5 |
6 | var h = require('../helper');
7 | var file = require('../file');
8 | var chalk = require('../chalk');
9 | var log = require('../log');
10 | var core = require('../core');
11 | var session = require('../session');
12 |
13 | const cmd = {
14 | command: 'test ')) {
47 | // do not hit problem without html tags in desc ( always exists for presenting testcase)
48 | log.debug('cache discarded for being no longer valid: ' + k + '.json');
49 | } else if (!['likes', 'dislikes'].every(p => p in _problem)) {
50 | // do not hit problem without likes & dislikes (logic will be improved in new lib)
51 | log.debug('cache discarded for being too old: ' + k + '.json');
52 | } else {
53 | // cache hit
54 | log.debug('cache hit: ' + k + '.json');
55 | _.extendOwn(problem, _problem);
56 | return cb(null, problem);
57 | }
58 | }
59 |
60 | plugin.next.getProblem(problem, needTranslation, function(e, _problem) {
61 | if (e) return cb(e);
62 |
63 | plugin.saveProblem(_problem);
64 | return cb(null, _problem);
65 | });
66 | };
67 |
68 | plugin.saveProblem = function(problem) {
69 | // it would be better to leave specific problem cache being user
70 | // independent, thus try to reuse existing cache as much as possible
71 | // after changing user.
72 | const _problem = _.omit(problem, ['locked', 'state', 'starred']);
73 | return cache.set(h.KEYS.problem(problem), _problem);
74 | };
75 |
76 | plugin.updateProblem = function(problem, kv) {
77 | const problems = cache.get(h.KEYS.problems);
78 | if (!problems) return false;
79 |
80 | const _problem = problems.find(x => x.id === problem.id);
81 | if (!_problem) return false;
82 |
83 | _.extend(_problem, kv);
84 | return cache.set(h.KEYS.problems, problems);
85 | };
86 |
87 | plugin.login = function(user, cb) {
88 | this.logout(user, false);
89 | plugin.next.login(user, function(e, user) {
90 | if (e) return cb(e);
91 | session.saveUser(user);
92 | return cb(null, user);
93 | });
94 | };
95 |
96 | plugin.logout = function(user, purge) {
97 | if (!user) user = session.getUser();
98 | if (purge) session.deleteUser();
99 | // NOTE: need invalidate any user related cache
100 | session.deleteCodingSession();
101 | return user;
102 | };
103 |
104 | module.exports = plugin;
105 |
--------------------------------------------------------------------------------
/lib/commands/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var h = require('../helper');
3 | var chalk = require('../chalk');
4 | var config = require('../config');
5 | var log = require('../log');
6 | var Plugin = require('../plugin');
7 | var session = require('../session');
8 | var sprintf = require('../sprintf');
9 |
10 | const cmd = {
11 | command: 'plugin [name]',
12 | aliases: ['extension', 'ext'],
13 | desc: 'Manage plugins',
14 | builder: function(yargs) {
15 | return yargs
16 | .option('c', {
17 | alias: 'config',
18 | type: 'boolean',
19 | describe: 'Show plugin config',
20 | default: false
21 | })
22 | .option('d', {
23 | alias: 'disable',
24 | type: 'boolean',
25 | describe: 'Disable plugin',
26 | default: false
27 | })
28 | .option('D', {
29 | alias: 'delete',
30 | type: 'boolean',
31 | describe: 'Delete plugin',
32 | default: false
33 | })
34 | .option('e', {
35 | alias: 'enable',
36 | type: 'boolean',
37 | describe: 'Enable plugin',
38 | default: false
39 | })
40 | .option('i', {
41 | alias: 'install',
42 | type: 'boolean',
43 | describe: 'Install plugin',
44 | default: false
45 | })
46 | .positional('name', {
47 | type: 'string',
48 | describe: 'Filter plugin by name',
49 | default: ''
50 | })
51 | .example(chalk.yellow('leetcode plugin'), 'Show all plugins')
52 | .example(chalk.yellow('leetcode plugin company'), 'Show company plugin')
53 | .example(chalk.yellow('leetcode plugin company -c'), 'Show config of company plugin')
54 | .example('', '')
55 | .example(chalk.yellow('leetcode plugin -i'), 'Install all missing plugins from GitHub')
56 | .example(chalk.yellow('leetcode plugin -i company'), 'Install company plugin from GitHub')
57 | .example(chalk.yellow('leetcode plugin -d company'), 'Disable company plugin')
58 | .example(chalk.yellow('leetcode plugin -e company'), 'Enable company plugin')
59 | .example(chalk.yellow('leetcode plugin -D company'), 'Delete company plugin');
60 | }
61 | };
62 |
63 | function print(plugins) {
64 | log.info(chalk.gray(sprintf(' %6s %-18s %-15s %s', 'Active', 'Name', 'Version', 'Desc')));
65 | log.info(chalk.gray('-'.repeat(100)));
66 |
67 | plugins = plugins || Plugin.plugins;
68 | for (let p of plugins)
69 | log.printf(' %s %-18s %-15s %s',
70 | h.prettyText('', p.enabled && !p.missing),
71 | p.name, p.ver, p.desc);
72 | }
73 |
74 | cmd.handler = function(argv) {
75 | session.argv = argv;
76 |
77 | let plugins = Plugin.plugins;
78 | const name = argv.name;
79 |
80 | if (argv.install) {
81 | const cb = function(e, p) {
82 | if (e) return log.fatal(e);
83 | p.help();
84 | p.save();
85 | Plugin.init();
86 | print();
87 | };
88 |
89 | if (name) {
90 | Plugin.install(name, cb);
91 | } else {
92 | Plugin.installMissings(cb);
93 | }
94 | return;
95 | }
96 |
97 | if (name) plugins = plugins.filter(x => x.name === name);
98 | if (plugins.length === 0) return log.fatal('Plugin not found!');
99 |
100 | const p = plugins[0];
101 | if (p.missing && (argv.enable || argv.disable))
102 | return log.fatal('Plugin missing, install it first');
103 |
104 | if (argv.enable) {
105 | p.enabled = true;
106 | p.save();
107 | print();
108 | } else if (argv.disable) {
109 | p.enabled = false;
110 | p.save();
111 | print();
112 | } else if (argv.delete) {
113 | p.delete();
114 | p.save();
115 | Plugin.init();
116 | print();
117 | } else if (argv.config) {
118 | log.info(JSON.stringify(config.plugins[name] || {}, null, 2));
119 | } else {
120 | print(plugins);
121 | }
122 | };
123 |
124 | module.exports = cmd;
125 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var _ = require('underscore');
3 | var nconf = require('nconf');
4 |
5 | var file = require('./file');
6 |
7 | const DEFAULT_CONFIG = {
8 | // usually you don't wanna change those
9 | sys: {
10 | categories: [
11 | 'algorithms',
12 | 'database',
13 | 'shell',
14 | 'concurrency'
15 | ],
16 | langs: [
17 | 'bash',
18 | 'c',
19 | 'cpp',
20 | 'csharp',
21 | 'golang',
22 | 'java',
23 | 'javascript',
24 | 'kotlin',
25 | 'mysql',
26 | 'php',
27 | 'python',
28 | 'python3',
29 | 'ruby',
30 | 'rust',
31 | 'scala',
32 | 'swift',
33 | 'typescript'
34 | ],
35 | urls: {
36 | // base urls
37 | base: 'https://leetcode.com',
38 | graphql: 'https://leetcode.com/graphql',
39 | login: 'https://leetcode.com/accounts/login/',
40 | // third part login base urls. TODO facebook google
41 | github_login: 'https://leetcode.com/accounts/github/login/?next=%2F',
42 | facebook_login: 'https://leetcode.com/accounts/facebook/login/?next=%2F',
43 | linkedin_login: 'https://leetcode.com/accounts/linkedin_oauth2/login/?next=%2F',
44 | // redirect urls
45 | leetcode_redirect: 'https://leetcode.com/',
46 | github_tf_redirect: 'https://github.com/sessions/two-factor',
47 | // simulate login urls
48 | github_login_request: 'https://github.com/login',
49 | github_session_request: 'https://github.com/session',
50 | github_tf_session_request: 'https://github.com/sessions/two-factor',
51 | linkedin_login_request: 'https://www.linkedin.com/login',
52 | linkedin_session_request: 'https://www.linkedin.com/checkpoint/lg/login-submit',
53 | // questions urls
54 | problems: 'https://leetcode.com/api/problems/$category/',
55 | problem: 'https://leetcode.com/problems/$slug/description/',
56 | test: 'https://leetcode.com/problems/$slug/interpret_solution/',
57 | session: 'https://leetcode.com/session/',
58 | submit: 'https://leetcode.com/problems/$slug/submit/',
59 | submissions: 'https://leetcode.com/api/submissions/$slug',
60 | submission: 'https://leetcode.com/submissions/detail/$id/',
61 | verify: 'https://leetcode.com/submissions/detail/$id/check/',
62 | favorites: 'https://leetcode.com/list/api/questions',
63 | favorite_delete: 'https://leetcode.com/list/api/questions/$hash/$id',
64 | plugin: 'https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/master/lib/plugins/$name.js'
65 | },
66 | },
67 |
68 | // but you will want change these
69 | autologin: {
70 | enable: false,
71 | retry: 2
72 | },
73 | code: {
74 | editor: 'vim',
75 | lang: 'cpp'
76 | },
77 | file: {
78 | show: '${fid}.${slug}',
79 | submission: '${fid}.${slug}.${sid}.${ac}'
80 | },
81 | color: {
82 | enable: true,
83 | theme: 'default'
84 | },
85 | icon: {
86 | theme: ''
87 | },
88 | network: {
89 | concurrency: 10,
90 | delay: 1
91 | },
92 | plugins: {}
93 | };
94 |
95 | function Config() {}
96 |
97 | Config.prototype.init = function() {
98 | nconf.file('local', file.configFile())
99 | .add('global', {type: 'literal', store: DEFAULT_CONFIG})
100 | .defaults({});
101 |
102 | const cfg = nconf.get();
103 | nconf.remove('local');
104 | nconf.remove('global');
105 |
106 | // HACK: remove old style configs
107 | for (const x in cfg) {
108 | if (x === x.toUpperCase()) delete cfg[x];
109 | }
110 | delete DEFAULT_CONFIG.type;
111 | delete cfg.type;
112 |
113 | _.extendOwn(this, cfg);
114 | };
115 |
116 | Config.prototype.getAll = function(useronly) {
117 | const cfg = _.extendOwn({}, this);
118 | if (useronly) delete cfg.sys;
119 | return cfg;
120 | };
121 |
122 | module.exports = new Config();
123 |
--------------------------------------------------------------------------------
/lib/commands/submit.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var util = require('util');
3 | var lodash = require('lodash');
4 |
5 | var h = require('../helper');
6 | var file = require('../file');
7 | var chalk = require('../chalk');
8 | var log = require('../log');
9 | var core = require('../core');
10 | var session = require('../session');
11 |
12 | const cmd = {
13 | command: 'submit ',
14 | aliases: ['push', 'commit'],
15 | desc: 'Submit code',
16 | builder: function(yargs) {
17 | return yargs
18 | .positional('filename', {
19 | type: 'string',
20 | describe: 'Code file to submit',
21 | default: ''
22 | })
23 | .example(chalk.yellow('leetcode submit 1.two-sum.cpp'), 'Submit code');
24 | }
25 | };
26 |
27 | function printResult(actual, k) {
28 | if (!actual.hasOwnProperty(k)) return;
29 |
30 | const v = actual[k] || '';
31 | const lines = Array.isArray(v) ? v : [v];
32 | for (let line of lines) {
33 | if (k !== 'state') line = lodash.startCase(k) + ': ' + line;
34 | log.info(' ' + h.prettyText(' ' + line, actual.ok));
35 | }
36 | }
37 |
38 | function printLine() {
39 | const args = Array.from(arguments);
40 | const actual = args.shift();
41 | const line = util.format.apply(util, args);
42 | log.info(' ' + h.prettyText(' ' + line, actual.ok));
43 | }
44 |
45 | cmd.handler = function(argv) {
46 | session.argv = argv;
47 | if (!file.exist(argv.filename))
48 | return log.fatal('File ' + argv.filename + ' not exist!');
49 |
50 | const meta = file.meta(argv.filename);
51 |
52 | // translation doesn't affect problem lookup
53 | core.getProblem(meta.id, true, function(e, problem) {
54 | if (e) return log.fail(e);
55 |
56 | problem.file = argv.filename;
57 | problem.lang = meta.lang;
58 |
59 | core.submitProblem(problem, function(e, results) {
60 | if (e) return log.fail(e);
61 |
62 | const result = results[0];
63 |
64 | printResult(result, 'state');
65 | printLine(result, '%d/%d cases passed (%s)',
66 | result.passed, result.total, result.runtime);
67 |
68 | if (result.ok) {
69 | session.updateStat('ac', 1);
70 | session.updateStat('ac.set', problem.fid);
71 |
72 | (function () {
73 | if (result.runtime_percentile)
74 | printLine(result, 'Your runtime beats %d %% of %s submissions',
75 | result.runtime_percentile.toFixed(2), result.lang);
76 | else
77 | return log.warn('Failed to get runtime percentile.');
78 | if (result.memory && result.memory_percentile)
79 | printLine(result, 'Your memory usage beats %d %% of %s submissions (%s)',
80 | result.memory_percentile.toFixed(2), result.lang, result.memory);
81 | else
82 | return log.warn('Failed to get memory percentile.');
83 | })();
84 |
85 | // core.getSubmission({id: result.id}, function(e, submission) {
86 | // if (e || !submission || !submission.distributionChart)
87 | // return log.warn('Failed to get submission beat ratio.');
88 |
89 | // const lang = submission.distributionChart.lang;
90 | // const scores = submission.distributionChart.distribution;
91 | // const myRuntime = parseFloat(result.runtime);
92 |
93 | // let ratio = 0.0;
94 | // for (let score of scores) {
95 | // if (parseFloat(score[0]) > myRuntime)
96 | // ratio += parseFloat(score[1]);
97 | // }
98 |
99 | // printLine(result, 'Your runtime beats %d %% of %s submissions',
100 | // ratio.toFixed(2), lang);
101 | // });
102 | } else {
103 | result.testcase = result.testcase.slice(1, -1).replace(/\\n/g, '\n');
104 | printResult(result, 'error');
105 | printResult(result, 'testcase');
106 | printResult(result, 'answer');
107 | printResult(result, 'expected_answer');
108 | printResult(result, 'stdout');
109 | }
110 |
111 | // update this problem status in local cache
112 | core.updateProblem(problem, {state: (result.ok ? 'ac' : 'notac')});
113 | });
114 | });
115 | };
116 |
117 | module.exports = cmd;
118 |
--------------------------------------------------------------------------------
/lib/commands/list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var _ = require('underscore');
3 |
4 | var h = require('../helper');
5 | var chalk = require('../chalk');
6 | var icon = require('../icon');
7 | var log = require('../log');
8 | var core = require('../core');
9 | var session = require('../session');
10 |
11 | const cmd = {
12 | command: 'list [keyword]',
13 | aliases: ['ls'],
14 | desc: 'List questions',
15 | builder: function(yargs) {
16 | return yargs
17 | .option('q', core.filters.query)
18 | .option('s', {
19 | alias: 'stat',
20 | type: 'boolean',
21 | default: false,
22 | describe: 'Show statistics of listed questions'
23 | })
24 | .option('t', core.filters.tag)
25 | .option('x', {
26 | alias: 'extra',
27 | type: 'boolean',
28 | default: false,
29 | describe: 'Show extra details: category, companies, tags.'
30 | })
31 | .option('T', {
32 | alias: 'dontTranslate',
33 | type: 'boolean',
34 | default: false,
35 | describe: 'Set to true to disable endpoint\'s translation',
36 | })
37 | .positional('keyword', {
38 | type: 'string',
39 | default: '',
40 | describe: 'Filter questions by keyword'
41 | })
42 | .example(chalk.yellow('leetcode list'), 'List all questions')
43 | .example(chalk.yellow('leetcode list -x'), 'Show extra info of questions, e.g. tags')
44 | .example('', '')
45 | .example(chalk.yellow('leetcode list array'), 'List questions that has "array" in name')
46 | .example(chalk.yellow('leetcode list -q eD'), 'List questions that with easy level and not done')
47 | .example(chalk.yellow('leetcode list -t google'), 'List questions from Google company (require plugin)')
48 | .example(chalk.yellow('leetcode list -t stack'), 'List questions realted to stack (require plugin)');
49 | }
50 | };
51 |
52 | cmd.handler = function(argv) {
53 | session.argv = argv;
54 | core.filterProblems(argv, function(e, problems) {
55 | if (e) return log.fail(e);
56 |
57 | const word = argv.keyword.toLowerCase();
58 | if (word) {
59 | if (word.endsWith(word.substr(-1).repeat(6))) {
60 | log.warn('Hmmm...you might need a new keyboard?');
61 | }
62 | problems = problems.filter(x => x.name.toLowerCase().includes(word));
63 | }
64 |
65 | const stat = {};
66 | for (let x of ['locked', 'starred', 'ac', 'notac', 'None', 'Easy', 'Medium', 'Hard']) stat[x] = 0;
67 |
68 | problems = _.sortBy(problems, x => -x.fid);
69 | for (let problem of problems) {
70 | stat[problem.level] = (stat[problem.level] || 0) + 1;
71 | stat[problem.state] = (stat[problem.state] || 0) + 1;
72 | if (problem.locked) ++stat.locked;
73 | if (problem.starred) ++stat.starred;
74 |
75 | log.printf('%s %s %s [%=4s] %-60s %-6s (%s %%)',
76 | (problem.starred ? chalk.yellow(icon.like) : icon.empty),
77 | (problem.locked ? chalk.red(icon.lock) : icon.nolock),
78 | h.prettyState(problem.state),
79 | problem.fid,
80 | problem.name,
81 | h.prettyLevel(problem.level),
82 | (problem.percent || 0).toFixed(2));
83 |
84 | if (argv.extra) {
85 | let badges = [problem.category];
86 | badges = badges.concat(problem.companies || []);
87 | badges = badges.concat(problem.tags || []);
88 |
89 | let buf = [];
90 | let len = 0;
91 | for (let x of badges) {
92 | if (len + x.length + 3 >= 60) {
93 | log.printf('%12s%s', ' ', chalk.gray(buf.join(' | ')));
94 | buf = [];
95 | len = 0;
96 | }
97 | buf.push(x);
98 | len += x.length + 3;
99 | }
100 | if (buf.length > 0)
101 | log.printf('%12s%s', ' ', chalk.gray(buf.join(' | ')));
102 | }
103 | }
104 |
105 | if (argv.stat) {
106 | log.info();
107 | log.printf(' Listed: %-9s Locked: %-9s Starred: %-9s', problems.length, stat.locked, stat.starred);
108 | log.printf(' Accept: %-9s Not-AC: %-9s Remain: %-9s', stat.ac, stat.notac, stat.None);
109 | log.printf(' Easy: %-9s Medium: %-9s Hard: %-9s', stat.Easy, stat.Medium, stat.Hard);
110 | }
111 | });
112 | };
113 |
114 | module.exports = cmd;
115 |
--------------------------------------------------------------------------------
/lib/core.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var util = require('util');
3 |
4 | var _ = require('underscore');
5 | var cheerio = require('cheerio');
6 | var he = require('he');
7 |
8 | var log = require('./log');
9 | var h = require('./helper');
10 | var file = require('./file');
11 | var Plugin = require('./plugin');
12 |
13 | const core = new Plugin(99999999, 'core', '20170722', 'Plugins manager');
14 |
15 | core.filters = {
16 | query: {
17 | alias: 'query',
18 | type: 'string',
19 | default: '',
20 | describe: [
21 | 'Filter questions by condition:',
22 | 'Uppercase means negative',
23 | 'e = easy E = m+h',
24 | 'm = medium M = e+h',
25 | 'h = hard H = e+m',
26 | 'd = done D = not done',
27 | 'l = locked L = non locked',
28 | 's = starred S = not starred'
29 | ].join('\n')
30 | },
31 | tag: {
32 | alias: 'tag',
33 | type: 'array',
34 | default: [],
35 | describe: 'Filter questions by tag'
36 | }
37 | };
38 |
39 | function hasTag(o, tag) {
40 | return Array.isArray(o) && o.some(x => x.indexOf(tag.toLowerCase()) >= 0);
41 | }
42 |
43 | const isLevel = (x, q) => x.level[0].toLowerCase() === q.toLowerCase();
44 | const isACed = x => x.state === 'ac';
45 | const isLocked = x => x.locked;
46 | const isStarred = x => x.starred;
47 |
48 | const QUERY_HANDLERS = {
49 | e: isLevel,
50 | E: _.negate(isLevel),
51 | m: isLevel,
52 | M: _.negate(isLevel),
53 | h: isLevel,
54 | H: _.negate(isLevel),
55 | l: isLocked,
56 | L: _.negate(isLocked),
57 | d: isACed,
58 | D: _.negate(isACed),
59 | s: isStarred,
60 | S: _.negate(isStarred)
61 | };
62 |
63 | core.filterProblems = function(opts, cb) {
64 | this.getProblems(!opts.dontTranslate, function(e, problems) {
65 | if (e) return cb(e);
66 |
67 | for (let q of (opts.query || '').split('')) {
68 | const f = QUERY_HANDLERS[q];
69 | if (!f) continue;
70 | problems = problems.filter(x => f(x, q));
71 | }
72 |
73 | for (let t of (opts.tag || [])) {
74 | problems = problems.filter(function(x) {
75 | return x.category === t ||
76 | hasTag(x.companies, t) ||
77 | hasTag(x.tags, t);
78 | });
79 | }
80 |
81 | return cb(null, problems);
82 | });
83 | };
84 |
85 | core.getProblem = function(keyword, needTranslation, cb) {
86 | if (keyword.id)
87 | return core.next.getProblem(keyword, needTranslation, cb);
88 |
89 | this.getProblems(needTranslation, function(e, problems) {
90 | if (e) return cb(e);
91 |
92 | keyword = Number(keyword) || keyword;
93 | const metaFid = file.exist(keyword) ? Number(file.meta(keyword).id) : NaN;
94 | const problem = problems.find(function(x) {
95 | return x.fid + '' === keyword + '' || x.fid + '' === metaFid + '' || x.name === keyword || x.slug === keyword;
96 | });
97 | if (!problem) return cb('Problem not found!');
98 | core.next.getProblem(problem, needTranslation, cb);
99 | });
100 | };
101 |
102 | core.starProblem = function(problem, starred, cb) {
103 | if (problem.starred === starred) {
104 | log.debug('problem is already ' + (starred ? 'starred' : 'unstarred'));
105 | return cb(null, starred);
106 | }
107 |
108 | core.next.starProblem(problem, starred, cb);
109 | };
110 |
111 | core.exportProblem = function(problem, opts) {
112 | const data = _.extend({}, problem);
113 |
114 | // unify format before rendering
115 | data.app = require('./config').app || 'leetcode';
116 | if (!data.fid) data.fid = data.id;
117 | if (!data.lang) data.lang = opts.lang;
118 | data.code = (opts.code || data.code || '').replace(/\r\n/g, '\n');
119 | data.comment = h.langToCommentStyle(data.lang);
120 | data.percent = data.percent.toFixed(2);
121 | data.testcase = util.inspect(data.testcase || '');
122 |
123 | if (opts.tpl === 'detailed') {
124 | let desc = data.desc;
125 | // Replace with '^' as the power operator
126 | desc = desc.replace(/<\/sup>/gm, '').replace(//gm, '^');
127 | desc = he.decode(cheerio.load(desc).root().text());
128 | // NOTE: wordwrap internally uses '\n' as EOL, so here we have to
129 | // remove all '\r' in the raw string.
130 | desc = desc.replace(/\r\n/g, '\n').replace(/^ /mg, '');
131 | const wrap = require('wordwrap')(79 - data.comment.line.length);
132 | data.desc = wrap(desc).split('\n');
133 | }
134 |
135 | return file.render(opts.tpl, data);
136 | };
137 |
138 | module.exports = core;
139 |
--------------------------------------------------------------------------------
/lib/commands/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var prompt = require('prompt');
3 |
4 | var h = require('../helper');
5 | var config = require('../config');
6 | var chalk = require('../chalk');
7 | var log = require('../log');
8 | var core = require('../core');
9 | var session = require('../session');
10 | var sprintf = require('../sprintf');
11 |
12 | const cmd = {
13 | command: 'user',
14 | aliases: ['account'],
15 | desc: 'Manage account',
16 | builder: function(yargs) {
17 | return yargs
18 | .option('l', {
19 | alias: 'login',
20 | type: 'boolean',
21 | default: false,
22 | describe: 'Login'
23 | })
24 | .option('c', {
25 | alias: 'cookie',
26 | type: 'boolean',
27 | default: false,
28 | describe: 'cookieLogin'
29 | })
30 | .option('g', {
31 | alias: 'github',
32 | type: 'boolean',
33 | default: false,
34 | describe: 'githubLogin'
35 | })
36 | .option('i', {
37 | alias: 'linkedin',
38 | type: 'boolean',
39 | default: false,
40 | describe: 'linkedinLogin'
41 | })
42 | .option('L', {
43 | alias: 'logout',
44 | type: 'boolean',
45 | default: false,
46 | describe: 'Logout'
47 | })
48 | .example(chalk.yellow('leetcode user'), 'Show current user')
49 | .example(chalk.yellow('leetcode user -l'), 'User login')
50 | .example(chalk.yellow('leetcode user -c'), 'User Cookie login')
51 | .example(chalk.yellow('leetcode user -g'), 'User GitHub login')
52 | .example(chalk.yellow('leetcode user -i'), 'User LinkedIn login')
53 | .example(chalk.yellow('leetcode user -L'), 'User logout');
54 | }
55 | };
56 |
57 | cmd.handler = function(argv) {
58 | session.argv = argv;
59 | let user = null;
60 | if (argv.login) {
61 | // login
62 | prompt.colors = false;
63 | prompt.message = '';
64 | prompt.start();
65 | prompt.get([
66 | {name: 'login', required: true},
67 | {name: 'pass', required: true, hidden: true}
68 | ], function(e, user) {
69 | if (e) return log.fail(e);
70 |
71 | core.login(user, function(e, user) {
72 | if (e) return log.fail(e);
73 | log.info('Successfully login as', chalk.yellow(user.name));
74 | });
75 | });
76 | } else if (argv.logout) {
77 | // logout
78 | user = core.logout(user, true);
79 | if (user)
80 | log.info('Successfully logout as', chalk.yellow(user.name));
81 | else
82 | log.fail('You are not login yet?');
83 | // third parties
84 | } else if (argv.github || argv.linkedin) {
85 | // add future third parties here
86 | const functionMap = new Map(
87 | [
88 | ['g', core.githubLogin],
89 | ['github', core.githubLogin],
90 | ['i', core.linkedinLogin],
91 | ['linkedin', core.linkedinLogin],
92 | ]
93 | );
94 | const keyword = Object.entries(argv).filter((i) => (i[1] === true))[0][0];
95 | const coreFunction = functionMap.get(keyword);
96 | prompt.colors = false;
97 | prompt.message = '';
98 | prompt.start();
99 | prompt.get([
100 | {name: 'login', required: true},
101 | {name: 'pass', required: true, hidden: true}
102 | ], function(e, user) {
103 | if (e) return log.fail(e);
104 | coreFunction(user, function(e, user) {
105 | if (e) return log.fail(e);
106 | log.info('Successfully third party login as', chalk.yellow(user.name));
107 | });
108 | });
109 | } else if (argv.cookie) {
110 | // session
111 | prompt.colors = false;
112 | prompt.message = '';
113 | prompt.start();
114 | prompt.get([
115 | {name: 'login', required: true},
116 | {name: 'cookie', required: true}
117 | ], function(e, user) {
118 | if (e) return log.fail(e);
119 | core.cookieLogin(user, function(e, user) {
120 | if (e) return log.fail(e);
121 | log.info('Successfully cookie login as', chalk.yellow(user.name));
122 | });
123 | });
124 | } else {
125 | // show current user
126 | user = session.getUser();
127 | if (user) {
128 | log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host')));
129 | log.info(chalk.gray('-'.repeat(60)));
130 | log.printf(' %s %-20s %s',
131 | h.prettyText('', user.paid || false),
132 | chalk.yellow(user.name),
133 | config.sys.urls.base);
134 | } else
135 | return log.fail('You are not login yet?');
136 | }
137 | };
138 |
139 | module.exports = cmd;
140 |
--------------------------------------------------------------------------------
/docs/releases.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Release Notes
4 | ---
5 | # 2.6.1
6 | * `submit`
7 | * fixes 500 error on windows.
8 |
9 | # 2.6.0
10 | * build all-in-one binary for linux/macos/windows.
11 | * `show`
12 | * support customized filename.
13 | * use "--" as comment in sql file.
14 | * `list`
15 | * fixes format issue.
16 | * fixes UT failures on windows.
17 |
18 | # 2.5.4
19 | * fixes error in fresh env without .lc existed.
20 | * embed meta in file content instead of file name.
21 | * update dependencies.
22 |
23 | # 2.5.3
24 |
25 | * fixes "Failed to load locked problem" issue.
26 | * move plugin's data into separate folders:
27 | * login info
28 | * problems list
29 | * problem cache
30 |
31 | # 2.5.2
32 |
33 | * `show`
34 | * fixes 400 error
35 | * support translated content for leetcode-cn
36 |
37 | # 2.5.1
38 |
39 | * auto install missing plugins after upgrade.
40 | * use 16m colors if possible.
41 | * enhance color output on windows.
42 | * `cache`
43 | * fix issue that can't delete cache by name.
44 | * `session`
45 | * fix issue if session name is a number.
46 | * `stat`
47 | * use level weight in calendar view.
48 |
49 | # 2.5.0
50 |
51 | * add `session` command to manage coding sessions on leetcode.com.
52 | * add more color themes.
53 | * molokai
54 | * solarized
55 | * solarized.light
56 | * `list`
57 | * fix id mismatch issue.
58 | * `show`
59 | * add `-o` option to specify output folder.
60 | * fix badge output in non-default color themes.
61 | * `stat`
62 | * calculate on AC-ed questions in calendar graph.
63 | * `test`
64 | * fix out-of-order output issue.
65 |
66 | # 2.4.0
67 |
68 | * only supports node's version >= 4.
69 | * Refactor folder structure:
70 | * now `~/.lc/` would be the only folder used by leetcode-cli.
71 | * move lcconfig file to `~/.lc/`.
72 | * move cache files to `~/.lc/cache/`.
73 | * `config`
74 | * fix string value parsing error.
75 | * `list`
76 | * show tag/lang badges in `-x` output.
77 | * `show`
78 | * add `-q` `-t` options to filter random questions.
79 | * `stat`
80 | * enhance output of `-g` option.
81 | * enhance output on windows.
82 | * add `-c` option to display calendar stat of how many AC-ed questions per day.
83 | * add `--no-lock` option to filter out locked questions.
84 | * add `-q` `-t` options to filter questions stat.
85 |
86 | # 2.3.0
87 |
88 | * `plugin`
89 | * only install necessary depedencies on specific platform.
90 | * add `-c` option to show plugin config.
91 | * support [cookie.chrome](https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.chrome.md) plugin.
92 | * support [cookie.firefox](https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.firefox.md) plugin.
93 | * docker
94 | * support running leetcode-cli as docker container for new user's tasting.
95 | * auto build docker image in Docker Hub.
96 | * UI
97 | * Add spinner message for long time running works.
98 | * Add logo and updte documents.
99 |
100 | # 2.2.1
101 |
102 | * add commands aliases.
103 | * enhance documents about install error on Ubuntu.
104 | * `config`
105 | * fix wrong parsing on non-string value.
106 | * `plugin`
107 | * fix bug when installing new npm modules.
108 | * `show`
109 | * use traditional `.py` for python3 filename.
110 | * `submission`
111 | * enhance recursive folder creation.
112 |
113 | # 2.2.0
114 |
115 | * `config`
116 | * add new `config` command to manage user configs.
117 | * try to save user from manually editing config file (~/.lcconfig).
118 | * start to use new json config format. (NOTE: not compatible with old format!)
119 | * `show`
120 | * print suppoerted language list.
121 | * add Release Notes page.
122 | * remove several legacy hacks.
123 |
124 | # 2.1.1
125 | * `show`
126 | * add `-e` option to open editor for coding.
127 | * add `-c` option to display source code only.
128 | * remove legacy `-t` `-d` options.
129 | * fix bad alignment in colorful output.
130 | * `list`
131 | * enhance `-t` option to support multiple tags, e.g. `leetcode list -t google -t array`
132 | * support latest `company` plugin to filter questions by tags like `array` or `dynamic programming`
133 | * config
134 | * add `EDITOR` to set default editor.
135 | * fix `--no-color` bug.
136 |
137 |
138 | # 2.1.0
139 | * `show`
140 | * fix "unknown language" error due to recent API changes on leetcode.com.
141 | * add `kotlin` language.
142 | * `cache`
143 | * remove `-a` option, now `leetcode cache -d` will directly clear all cache.
144 | * add keyword match, e.g. `leetcode cache 537` will only show the cache for question 537.
145 | * update most libray depedencies.
146 |
--------------------------------------------------------------------------------
/lib/file.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var fs = require('fs');
3 | var os = require('os');
4 | var path = require('path');
5 |
6 | var _ = require('underscore');
7 | var mkdirp = require('mkdirp');
8 |
9 | const file = {}
10 |
11 | file.init = function() {
12 | _.templateSettings = {
13 | evaluate: /\{\{(.+?)\}\}/g,
14 | interpolate: /\$\{(.+?)\}/g
15 | };
16 | };
17 |
18 | file.isWindows = function() {
19 | return process.platform === 'win32';
20 | };
21 |
22 | /// app dirs ///
23 | file.userHomeDir = function() {
24 | return process.env.HOME || process.env.USERPROFILE;
25 | };
26 |
27 | file.homeDir = function() {
28 | return path.join(this.userHomeDir(), '.lc');
29 | };
30 |
31 | file.appDir = function() {
32 | const config = require('./config');
33 | return path.join(this.homeDir(), config.app || 'leetcode');
34 | };
35 |
36 | file.cacheDir = function() {
37 | return path.join(this.appDir(), 'cache');
38 | };
39 |
40 | file.codeDir = function(dir) {
41 | return path.join(__dirname, '..', dir || '');
42 | };
43 |
44 | /// app files ///
45 | file.cacheFile = function(k) {
46 | return path.join(this.cacheDir(), k + '.json');
47 | };
48 |
49 | file.configFile = function() {
50 | return path.join(this.homeDir(), 'config.json');
51 | };
52 |
53 | file.pluginFile = function(name) {
54 | return path.join(this.codeDir('lib/plugins'), path.basename(name));
55 | };
56 |
57 | file.listCodeDir = function(dir) {
58 | dir = this.codeDir(dir);
59 | return this.list(dir).map(function(f) {
60 | const fullpath = path.join(dir, f);
61 | const ext = path.extname(f);
62 | const name = path.basename(f, ext);
63 |
64 | let data = null;
65 | switch (ext) {
66 | case '.js': data = require(fullpath); break;
67 | case '.json': data = JSON.parse(file.data(fullpath)); break;
68 | }
69 | return {name: name, data: data, file: f};
70 | });
71 | };
72 |
73 | /// general dirs & files ///
74 | file.mkdir = function(fullpath) {
75 | if (fs.existsSync(fullpath)) return;
76 | mkdirp.sync(fullpath);
77 | };
78 |
79 | file.exist = function(fullpath) {
80 | return fs.existsSync(fullpath);
81 | };
82 |
83 | file.rm = function(fullpath) {
84 | return fs.unlinkSync(fullpath);
85 | };
86 |
87 | file.mv = function(src, dst) {
88 | return fs.renameSync(src, dst);
89 | };
90 |
91 | file.list = function(dir) {
92 | return fs.readdirSync(dir);
93 | };
94 |
95 | file.stat = function(fullpath) {
96 | return fs.statSync(fullpath);
97 | };
98 |
99 | file.write = function(fullpath, data) {
100 | return fs.writeFileSync(fullpath, data);
101 | };
102 |
103 | file.name = function(fullpath) {
104 | return path.basename(fullpath, path.extname(fullpath));
105 | };
106 |
107 | file.data = function(fullpath) {
108 | return fs.existsSync(fullpath) ? fs.readFileSync(fullpath).toString() : null;
109 | };
110 |
111 | file.codeData = function(fullpath) {
112 | const data = this.data(fullpath);
113 |
114 | if (data === null) {
115 | return null;
116 | }
117 |
118 | const lines = data.split(/\r\n|\n|\r/);
119 | const start = lines.findIndex(x => x.indexOf('@lc code=start') !== -1);
120 | const end = lines.findIndex(x => x.indexOf('@lc code=end') !== -1);
121 |
122 | if (start !== -1 && end !== -1 && start + 1 <= end) {
123 | return lines.slice(start + 1, end).join(os.EOL);
124 | }
125 |
126 | return data;
127 | };
128 |
129 | /// templates & metadata ///
130 | file.render = function(tpl, data) {
131 | const tplfile = path.join(this.codeDir('templates'), tpl + '.tpl');
132 | let result = _.template(this.data(tplfile).replace(/\r\n/g, '\n'))(data);
133 |
134 | if (this.isWindows()) {
135 | result = result.replace(/\n/g, '\r\n');
136 | } else {
137 | result = result.replace(/\r\n/g, '\n');
138 | }
139 | return result;
140 | };
141 |
142 | file.fmt = function(format, data) {
143 | return _.template(format)(data);
144 | };
145 |
146 | file.metaByName = function(filename) {
147 | const m = {};
148 |
149 | // expect the 1st section in filename as id
150 | // e.g. 1.two-sum.cpp
151 | m.id = file.name(filename).split('.')[0];
152 |
153 | // HACK: compatible with old ext
154 | if (filename.endsWith('.py3') || filename.endsWith('.python3.py'))
155 | m.lang = 'python3';
156 | else
157 | m.lang = require('./helper').extToLang(filename);
158 |
159 | return m;
160 | };
161 |
162 | file.meta = function(filename) {
163 | const m = {};
164 |
165 | // first look into the file data
166 | const line = this.data(filename).split('\n')
167 | .find(x => x.indexOf(' @lc app=') >= 0) || '';
168 | line.split(' ').forEach(function(x) {
169 | const v = x.split('=');
170 | if (v.length == 2) {
171 | m[v[0]] = v[1].trim();
172 | }
173 | });
174 |
175 | // otherwise, look into file name
176 | if (!m.id || !m.lang) {
177 | const olddata = this.metaByName(filename);
178 | m.id = m.id || olddata.id;
179 | m.lang = m.lang || olddata.lang;
180 | }
181 |
182 | return m;
183 | };
184 |
185 | module.exports = file;
186 |
--------------------------------------------------------------------------------
/lib/plugins/leetcode.cn.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var request = require('request');
3 |
4 | var config = require('../config');
5 | var h = require('../helper');
6 | var log = require('../log');
7 | var Plugin = require('../plugin');
8 | var session = require('../session');
9 |
10 | //
11 | // [Usage]
12 | //
13 | // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/leetcode.cn.md
14 | //
15 | var plugin = new Plugin(15, 'leetcode.cn', '2018.11.25',
16 | 'Plugin to talk with leetcode-cn APIs.');
17 |
18 | plugin.init = function() {
19 | config.app = 'leetcode.cn';
20 | config.sys.urls.base = 'https://leetcode.cn';
21 | config.sys.urls.login = 'https://leetcode.cn/accounts/login/';
22 | config.sys.urls.problems = 'https://leetcode.cn/api/problems/$category/';
23 | config.sys.urls.problem = 'https://leetcode.cn/problems/$slug/description/';
24 | config.sys.urls.graphql = 'https://leetcode.cn/graphql';
25 | config.sys.urls.problem_detail = 'https://leetcode.cn/graphql';
26 | config.sys.urls.test = 'https://leetcode.cn/problems/$slug/interpret_solution/';
27 | config.sys.urls.session = 'https://leetcode.cn/session/';
28 | config.sys.urls.submit = 'https://leetcode.cn/problems/$slug/submit/';
29 | config.sys.urls.submissions = 'https://leetcode.cn/api/submissions/$slug';
30 | config.sys.urls.submission = 'https://leetcode.cn/submissions/detail/$id/';
31 | config.sys.urls.verify = 'https://leetcode.cn/submissions/detail/$id/check/';
32 | config.sys.urls.favorites = 'https://leetcode.cn/list/api/questions';
33 | config.sys.urls.favorite_delete = 'https://leetcode.cn/list/api/questions/$hash/$id';
34 | // third parties
35 | config.sys.urls.github_login = 'https://leetcode.cn/accounts/github/login/?next=%2F';
36 | config.sys.urls.linkedin_login = 'https://leetcode.cn/accounts/linkedin_oauth2/login/?next=%2F';
37 | config.sys.urls.leetcode_redirect = 'https://leetcode.cn/';
38 | };
39 |
40 | // FIXME: refactor those
41 | // update options with user credentials
42 | function signOpts(opts, user) {
43 | opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId +
44 | ';csrftoken=' + user.sessionCSRF + ';';
45 | opts.headers['X-CSRFToken'] = user.sessionCSRF;
46 | opts.headers['X-Requested-With'] = 'XMLHttpRequest';
47 | }
48 |
49 | function makeOpts(url) {
50 | const opts = {};
51 | opts.url = url;
52 | opts.headers = {};
53 |
54 | if (session.isLogin())
55 | signOpts(opts, session.getUser());
56 | return opts;
57 | }
58 |
59 | function checkError(e, resp, expectedStatus) {
60 | if (!e && resp && resp.statusCode !== expectedStatus) {
61 | const code = resp.statusCode;
62 | log.debug('http error: ' + code);
63 |
64 | if (code === 403 || code === 401) {
65 | e = session.errors.EXPIRED;
66 | } else {
67 | e = {msg: 'http error', statusCode: code};
68 | }
69 | }
70 | return e;
71 | }
72 |
73 | // overloading getProblems here to make sure everything related
74 | // to listing out problems can have a chance to be translated.
75 | // NOTE: Details of the problem is translated inside leetcode.js
76 | plugin.getProblems = function (needTranslation, cb) {
77 | plugin.next.getProblems(needTranslation, function(e, problems) {
78 | if (e) return cb(e);
79 |
80 | if (needTranslation) {
81 | // only translate titles of the list if user requested
82 | plugin.getProblemsTitle(function (e, titles) {
83 | if (e) return cb(e);
84 |
85 | problems.forEach(function (problem) {
86 | const title = titles[problem.id];
87 | if (title)
88 | problem.name = title;
89 | });
90 |
91 | return cb(null, problems);
92 | });
93 | } else {
94 | return cb(null, problems);
95 | }
96 | });
97 | };
98 |
99 | plugin.getProblemsTitle = function(cb) {
100 | log.debug('running leetcode.cn.getProblemNames');
101 |
102 | const opts = makeOpts(config.sys.urls.graphql);
103 | opts.headers.Origin = config.sys.urls.base;
104 | opts.headers.Referer = 'https://leetcode.cn/api/problems/algorithms/';
105 |
106 | opts.json = true;
107 | opts.body = {
108 | query: [
109 | 'query getQuestionTranslation($lang: String) {',
110 | ' translations: allAppliedQuestionTranslations(lang: $lang) {',
111 | ' title',
112 | ' questionId',
113 | ' __typename',
114 | ' }',
115 | '}'
116 | ].join('\n'),
117 | variables: {},
118 | operationName: 'getQuestionTranslation'
119 | };
120 |
121 | const spin = h.spin('Downloading questions titles');
122 | request.post(opts, function(e, resp, body) {
123 | spin.stop();
124 | e = checkError(e, resp, 200);
125 | if (e) return cb(e);
126 |
127 | const titles = [];
128 | body.data.translations.forEach(function(x) {
129 | titles[x.questionId] = x.title;
130 | });
131 |
132 | return cb(null, titles);
133 | });
134 | };
135 |
136 | module.exports = plugin;
137 |
--------------------------------------------------------------------------------
/lib/commands/submission.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var path = require('path');
3 |
4 | var _ = require('underscore');
5 |
6 | var h = require('../helper');
7 | var file = require('../file');
8 | var chalk = require('../chalk');
9 | var config = require('../config');
10 | var log = require('../log');
11 | var Queue = require('../queue');
12 | var core = require('../core');
13 | var session = require('../session');
14 |
15 | const cmd = {
16 | command: 'submission [keyword]',
17 | aliases: ['pull'],
18 | desc: 'Download submission code',
19 | builder: function(yargs) {
20 | return yargs
21 | .option('a', {
22 | alias: 'all',
23 | type: 'boolean',
24 | default: false,
25 | describe: 'Download all questions'
26 | })
27 | .option('l', {
28 | alias: 'lang',
29 | type: 'string',
30 | default: 'all',
31 | describe: 'Filter by programming language'
32 | })
33 | .option('o', {
34 | alias: 'outdir',
35 | type: 'string',
36 | describe: 'Where to save submission code',
37 | default: '.'
38 | })
39 | .option('x', {
40 | alias: 'extra',
41 | type: 'boolean',
42 | default: false,
43 | describe: 'Show extra question details in submission code'
44 | })
45 | .option('T', {
46 | alias: 'dontTranslate',
47 | type: 'boolean',
48 | default: false,
49 | describe: 'Set to true to disable endpoint\'s translation',
50 | })
51 | .positional('keyword', {
52 | type: 'string',
53 | default: '',
54 | describe: 'Download specific question by id'
55 | })
56 | .example(chalk.yellow('leetcode submission -a -o mydir'), 'Download all to folder mydir')
57 | .example(chalk.yellow('leetcode submission -x -a'), 'Add descriptions in the downloaded codes')
58 | .example(chalk.yellow('leetcode submission -l cpp 1'), 'Download cpp submission of question 1');
59 | }
60 | };
61 |
62 | function doTask(problem, queue, cb) {
63 | const argv = queue.ctx.argv;
64 |
65 | function onTaskDone(e, msg) {
66 | // NOTE: msg color means different purpose:
67 | // - red: error
68 | // - green: accepted, fresh download
69 | // - yellow: not ac-ed, fresh download
70 | // - white: existed already, skip download
71 | log.printf('[%=4s] %-60s %s', problem.fid, problem.name,
72 | (e ? chalk.red('ERROR: ' + (e.msg || e)) : msg));
73 | if (cb) cb(e);
74 | }
75 |
76 | if (argv.extra) {
77 | // have to get problem details, e.g. problem description.
78 | core.getProblem(problem.fid, !argv.dontTranslate, function(e, problem) {
79 | if (e) return cb(e);
80 | exportSubmission(problem, argv, onTaskDone);
81 | });
82 | } else {
83 | exportSubmission(problem, argv, onTaskDone);
84 | }
85 | }
86 |
87 | function exportSubmission(problem, argv, cb) {
88 | core.getSubmissions(problem, function(e, submissions) {
89 | if (e) return cb(e);
90 | if (submissions.length === 0)
91 | return cb('No submissions?');
92 |
93 | // get obj list contain required filetype
94 | submissions = submissions.filter(x => argv.lang === 'all' || argv.lang === x.lang);
95 | if (submissions.length === 0)
96 | return cb('No submissions in required language.');
97 |
98 | // if no accepted, use the latest non-accepted one
99 | const submission = submissions.find(x => x.status_display === 'Accepted') || submissions[0];
100 | submission.ac = (submission.status_display === 'Accepted');
101 |
102 | const data = _.extend({}, submission, problem);
103 | data.sid = submission.id;
104 | data.ac = submission.ac ? 'ac' : 'notac';
105 | const basename = file.fmt(config.file.submission, data);
106 | const f = path.join(argv.outdir, basename + h.langToExt(submission.lang));
107 |
108 | file.mkdir(argv.outdir);
109 | // skip the existing cached submissions
110 | if (file.exist(f))
111 | return cb(null, chalk.underline(f));
112 |
113 | core.getSubmission(submission, function(e, submission) {
114 | if (e) return cb(e);
115 |
116 | const opts = {
117 | lang: submission.lang,
118 | code: submission.code,
119 | tpl: argv.extra ? 'detailed' : 'codeonly'
120 | };
121 | file.write(f, core.exportProblem(problem, opts));
122 | cb(null, submission.ac ? chalk.green.underline(f)
123 | : chalk.yellow.underline(f));
124 | });
125 | });
126 | }
127 |
128 | cmd.handler = function(argv) {
129 | session.argv = argv;
130 | const q = new Queue(null, {argv: argv}, doTask);
131 |
132 | if (argv.all) {
133 | core.getProblems(function(e, problems) {
134 | if (e) return log.fail(e);
135 | problems = problems.filter(x => x.state === 'ac' || x.state === 'notac');
136 | q.addTasks(problems).run();
137 | });
138 | return;
139 | }
140 |
141 | if (!argv.keyword)
142 | return log.fail('missing keyword?');
143 |
144 | core.getProblem(argv.keyword, !argv.dontTranslate, function(e, problem) {
145 | if (e) return log.fail(e);
146 | q.addTask(problem).run();
147 | });
148 | };
149 |
150 | module.exports = cmd;
151 |
--------------------------------------------------------------------------------
/test/test_file.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | const assert = require('chai').assert;
6 | const rewire = require('rewire');
7 |
8 | const th = require('./helper');
9 |
10 | describe('file', function() {
11 | let file;
12 |
13 | beforeEach(function() {
14 | file = rewire('../lib/file');
15 | });
16 |
17 | describe('#dirAndFiles', function() {
18 | const HOME = path.join(__dirname, '..');
19 |
20 | it('should ok on linux', function() {
21 | if (file.isWindows()) this.skip();
22 | process.env.HOME = '/home/skygragon';
23 |
24 | assert.equal(file.userHomeDir(), '/home/skygragon');
25 | assert.equal(file.homeDir(), '/home/skygragon/.lc');
26 | assert.equal(file.cacheDir(), '/home/skygragon/.lc/leetcode/cache');
27 | assert.equal(file.cacheFile('xxx'), '/home/skygragon/.lc/leetcode/cache/xxx.json');
28 | assert.equal(file.configFile(), '/home/skygragon/.lc/config.json');
29 | assert.equal(file.name('/home/skygragon/.lc/leetcode/cache/xxx.json'), 'xxx');
30 | });
31 |
32 | it('should ok on windows', function() {
33 | if (!file.isWindows()) this.skip();
34 | process.env.HOME = '';
35 | process.env.USERPROFILE = 'C:\\Users\\skygragon';
36 | assert.equal(file.userHomeDir(), 'C:\\Users\\skygragon');
37 | assert.equal(file.homeDir(), 'C:\\Users\\skygragon\\.lc');
38 | assert.equal(file.cacheDir(), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache');
39 | assert.equal(file.cacheFile('xxx'), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json');
40 | assert.equal(file.configFile(), 'C:\\Users\\skygragon\\.lc\\config.json');
41 | assert.equal(file.name('C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json'), 'xxx');
42 | });
43 |
44 | it('should codeDir ok', function() {
45 | assert.equal(file.codeDir(), HOME);
46 | assert.equal(file.codeDir('.'), HOME);
47 | assert.equal(file.codeDir('icons'), path.join(HOME, 'icons'));
48 | assert.equal(file.codeDir('lib/plugins'), path.join(HOME, 'lib', 'plugins'));
49 | });
50 |
51 | it('should listCodeDir ok', function() {
52 | const files = file.listCodeDir('lib/plugins');
53 | assert.equal(files.length, 6);
54 | assert.equal(files[0].name, 'cache');
55 | assert.equal(files[1].name, 'company');
56 | assert.equal(files[2].name, 'leetcode.cn');
57 | assert.equal(files[3].name, 'leetcode');
58 | assert.equal(files[4].name, 'retry');
59 | assert.equal(files[5].name, 'solution.discuss');
60 | });
61 |
62 | it('should pluginFile ok', function() {
63 | const expect = path.join(HOME, 'lib/plugins/cache.js');
64 | assert.equal(file.pluginFile('cache.js'), expect);
65 | assert.equal(file.pluginFile('./cache.js'), expect);
66 | assert.equal(file.pluginFile('https://github.com/skygragon/cache.js'), expect);
67 | });
68 |
69 | it('should data ok with missing file', function() {
70 | assert.equal(file.data('non-exist'), null);
71 | });
72 | }); // #dirAndFiles
73 |
74 | describe('#meta', function() {
75 | it('should meta ok within file content', function() {
76 | file.data = x => [
77 | '/ *',
78 | ' * @lc app=leetcode id=123 lang=javascript',
79 | ' * /'
80 | ].join('\n');
81 | const meta = file.meta('dummy');
82 | assert.equal(meta.app, 'leetcode')
83 | assert.equal(meta.id, '123');
84 | assert.equal(meta.lang, 'javascript');
85 | });
86 |
87 | it('should meta ok with white space', function() {
88 | file.data = x => [
89 | '/ *',
90 | ' * @lc app=leetcode id=123\t \t lang=javascript\r',
91 | ' * /'
92 | ].join('\n');
93 | const meta = file.meta('dummy');
94 | assert.equal(meta.app, 'leetcode')
95 | assert.equal(meta.id, '123');
96 | assert.equal(meta.lang, 'javascript');
97 | });
98 |
99 | it('should meta ok within file name', function() {
100 | file.data = x => [
101 | '/ *',
102 | ' * no meta app=leetcode id=123 lang=javascript',
103 | ' * /'
104 | ].join('\n');
105 | const meta = file.meta('321.dummy.py');
106 | assert(!meta.app)
107 | assert.equal(meta.id, '321');
108 | assert.equal(meta.lang, 'python');
109 | });
110 |
111 | it('should meta ok within deprecated file name', function() {
112 | file.data = x => [
113 | '/ *',
114 | ' * no meta app=leetcode id=123 lang=javascript',
115 | ' * /'
116 | ].join('\n');
117 |
118 | var meta = file.meta('111.dummy.py3');
119 | assert(!meta.app)
120 | assert.equal(meta.id, '111');
121 | assert.equal(meta.lang, 'python3');
122 |
123 | meta = file.meta('222.dummy.python3.py');
124 | assert(!meta.app)
125 | assert.equal(meta.id, '222');
126 | assert.equal(meta.lang, 'python3');
127 | });
128 |
129 | it('should fmt ok', function() {
130 | file.init();
131 | const data = file.fmt('${id}', {id: 123});
132 | assert.equal(data, '123');
133 | });
134 | }); // #meta
135 |
136 | describe('#genneral', function() {
137 | beforeEach(function() {
138 | th.clean();
139 | });
140 | afterEach(function() {
141 | th.clean();
142 | });
143 |
144 | it('should mkdir ok', function() {
145 | const dir = th.DIR + 'dir';
146 | assert.equal(fs.existsSync(dir), false);
147 | file.mkdir(dir);
148 | assert.equal(fs.existsSync(dir), true);
149 | file.mkdir(dir);
150 | assert.equal(fs.existsSync(dir), true);
151 | });
152 |
153 | it('should mv ok', function() {
154 | const SRC = th.Dir + 'src';
155 | const DST = th.DIR + 'dst';
156 | assert.equal(fs.existsSync(SRC), false);
157 | assert.equal(fs.existsSync(DST), false);
158 | file.mkdir(SRC);
159 | assert.equal(fs.existsSync(SRC), true);
160 | assert.equal(fs.existsSync(DST), false);
161 | file.mv(SRC, DST);
162 | assert.equal(fs.existsSync(SRC), false);
163 | assert.equal(fs.existsSync(DST), true);
164 | });
165 | }); // #general
166 | });
167 |
--------------------------------------------------------------------------------
/lib/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var cp = require('child_process');
3 | var fs = require('fs');
4 | var path = require('path');
5 |
6 | var _ = require('underscore');
7 | var request = require('request');
8 |
9 | var h = require('./helper');
10 | var file = require('./file');
11 | var cache = require('./cache');
12 | var config = require('./config');
13 | var log = require('./log');
14 | var Queue = require('./queue');
15 |
16 | function Plugin(id, name, ver, desc, deps) {
17 | this.id = id;
18 | this.name = name;
19 | this.ver = ver || 'default';
20 | this.desc = desc || '';
21 |
22 | this.enabled = true;
23 | this.deleted = false;
24 | this.missing = (this.ver === 'missing');
25 | this.builtin = (this.ver === 'default');
26 |
27 | // only need deps for current platform
28 | this.deps = _.chain(deps || [])
29 | .filter(x => ! x.includes(':') || x.includes(':' + process.platform))
30 | .map(x => x.split(':')[0])
31 | .value();
32 | }
33 |
34 | Plugin.prototype.init = function() {
35 | this.config = config.plugins[this.name] || {};
36 | this.next = null;
37 | };
38 |
39 | Plugin.prototype.setNext = function(next) {
40 | Object.setPrototypeOf(this, next);
41 | this.next = next;
42 | };
43 |
44 | Plugin.prototype.delete = function() {
45 | if (!this.missing) {
46 | try {
47 | const fullpath = file.pluginFile(this.file);
48 | file.rm(fullpath);
49 | } catch(e) {
50 | return log.error(e.message);
51 | }
52 | }
53 | this.deleted = true;
54 | };
55 |
56 | Plugin.prototype.save = function() {
57 | const stats = cache.get(h.KEYS.plugins) || {};
58 |
59 | if (this.deleted) delete stats[this.name];
60 | else if (this.missing) return;
61 | else stats[this.name] = this.enabled;
62 |
63 | cache.set(h.KEYS.plugins, stats);
64 | };
65 |
66 | Plugin.prototype.install = function(cb) {
67 | if (this.deps.length === 0) return cb();
68 |
69 | const cmd = 'npm install --save ' + this.deps.join(' ');
70 | log.debug(cmd);
71 | const spin = h.spin(cmd);
72 | cp.exec(cmd, {cwd: file.codeDir()}, function(e) {
73 | spin.stop();
74 | return cb(e);
75 | });
76 | };
77 |
78 | Plugin.prototype.help = function() {};
79 |
80 | Plugin.plugins = [];
81 |
82 | Plugin.init = function(head) {
83 | log.trace('initializing all plugins');
84 | head = head || require('./core');
85 |
86 | const stats = cache.get(h.KEYS.plugins) || {};
87 |
88 | // 1. find installed plugins
89 | let installed = [];
90 | for (let f of file.listCodeDir('lib/plugins')) {
91 | const p = f.data;
92 | if (!p) continue;
93 | log.trace('found plugin: ' + p.name + '=' + p.ver);
94 |
95 | p.file = f.file;
96 | p.enabled = stats[p.name];
97 |
98 | if (!(p.name in stats)) {
99 | if (p.builtin) {
100 | log.trace('new builtin plugin, enable by default');
101 | p.enabled = true;
102 | } else {
103 | log.trace('new 3rd party plugin, disable by default');
104 | p.enabled = false;
105 | }
106 | }
107 | installed.push(p);
108 | }
109 | // the one with bigger `id` comes first
110 | installed = _.sortBy(installed, x => -x.id);
111 |
112 | // 2. init all in reversed order
113 | for (let i = installed.length - 1; i >= 0; --i) {
114 | const p = installed[i];
115 | if (p.enabled) {
116 | p.init();
117 | log.trace('inited plugin: ' + p.name);
118 | } else {
119 | log.trace('skipped plugin: ' + p.name);
120 | }
121 | }
122 |
123 | // 3. chain together
124 | const plugins = installed.filter(x => x.enabled);
125 | let last = head;
126 | for (let p of plugins) {
127 | last.setNext(p);
128 | last = p;
129 | }
130 |
131 | // 4. check missing plugins
132 | const missings = [];
133 | for (let k of _.keys(stats)) {
134 | if (installed.find(x => x.name === k)) continue;
135 | const p = new Plugin(-1, k, 'missing');
136 | p.enabled = stats[k];
137 | missings.push(p);
138 | log.trace('missing plugin:' + p.name);
139 | }
140 |
141 | Plugin.plugins = installed.concat(missings);
142 | return missings.length === 0;
143 | };
144 |
145 | Plugin.copy = function(src, cb) {
146 | // FIXME: remove local file support?
147 | if (path.extname(src) !== '.js') {
148 | src = config.sys.urls.plugin.replace('$name', src);
149 | }
150 | const dst = file.pluginFile(src);
151 |
152 | const srcstream = src.startsWith('https://') ? request(src) : fs.createReadStream(src);
153 | const dststream = fs.createWriteStream(dst);
154 | let error;
155 |
156 | srcstream.on('response', function(resp) {
157 | if (resp.statusCode !== 200)
158 | srcstream.emit('error', 'HTTP Error: ' + resp.statusCode);
159 | });
160 | srcstream.on('error', function(e) {
161 | dststream.emit('error', e);
162 | });
163 |
164 | dststream.on('error', function(e) {
165 | error = e;
166 | dststream.end();
167 | });
168 | dststream.on('close', function() {
169 | spin.stop();
170 | if (error) file.rm(dst);
171 | return cb(error, dst);
172 | });
173 |
174 | log.debug('copying from ' + src);
175 | const spin = h.spin('Downloading ' + src);
176 | srcstream.pipe(dststream);
177 | };
178 |
179 | Plugin.install = function(name, cb) {
180 | Plugin.copy(name, function(e, fullpath) {
181 | if (e) return cb(e);
182 | log.debug('copied to ' + fullpath);
183 |
184 | const p = require(fullpath);
185 | p.file = path.basename(fullpath);
186 | p.install(function() {
187 | return cb(null, p);
188 | });
189 | });
190 | };
191 |
192 | Plugin.installMissings = function(cb) {
193 | function doTask(plugin, queue, cb) {
194 | Plugin.install(plugin.name, function(e, p) {
195 | if (!e) {
196 | p.enabled = plugin.enabled;
197 | p.save();
198 | p.help();
199 | }
200 | return cb(e, p);
201 | });
202 | }
203 |
204 | const missings = Plugin.plugins.filter(x => x.missing);
205 | if (missings.length === 0) return cb();
206 |
207 | log.warn('Installing missing plugins, might take a while ...');
208 | const q = new Queue(missings, {}, doTask);
209 | q.run(1, cb);
210 | };
211 |
212 | Plugin.save = function() {
213 | for (let p of this.plugins) p.save();
214 | };
215 |
216 | module.exports = Plugin;
217 |
--------------------------------------------------------------------------------
/test/test_plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | const assert = require('chai').assert;
6 | const rewire = require('rewire');
7 |
8 | const chalk = require('../lib/chalk');
9 | const config = require('../lib/config');
10 | const log = require('../lib/log');
11 | const th = require('./helper');
12 |
13 | const Plugin = rewire('../lib/plugin');
14 |
15 | describe('plugin', function() {
16 | let file;
17 | let cache;
18 |
19 | const NOOP = () => {};
20 |
21 | before(function() {
22 | log.init();
23 | chalk.init();
24 | config.init();
25 |
26 | file = rewire('../lib/file');
27 | cache = rewire('../lib/cache');
28 | Plugin.__set__('file', file);
29 | Plugin.__set__('cache', cache);
30 | });
31 |
32 | beforeEach(function() {
33 | th.clean();
34 | cache.get = NOOP;
35 | });
36 |
37 | describe('#Plugin.init', function() {
38 | const p1 = new Plugin(0, 'leetcode', '2.0');
39 | const p2 = new Plugin(1, 'cache', '1.0');
40 | const p3 = new Plugin(2, 'retry', '3.0');
41 | const p4 = new Plugin(3, 'core', '4.0');
42 |
43 | before(function() {
44 | p1.init = p2.init = p3.init = p4.init = NOOP;
45 | file.listCodeDir = function() {
46 | return [
47 | {name: 'cache', data: p2, file: 'cache.js'},
48 | {name: 'leetcode', data: p1, file: 'leetcode.js'},
49 | {name: 'retry', data: p3, file: 'retry.js'},
50 | {name: 'bad', data: null}
51 | ];
52 | };
53 | });
54 |
55 | it('should init ok', function() {
56 | cache.get = () => {
57 | return {cache: true, leetcode: false, retry: true};
58 | };
59 | assert.deepEqual(Plugin.plugins, []);
60 |
61 | const res = Plugin.init(p4);
62 | assert.equal(res, true);
63 | assert.deepEqual(Plugin.plugins.length, 3);
64 |
65 | const names = Plugin.plugins.map(p => p.name);
66 | assert.deepEqual(names, ['retry', 'cache', 'leetcode']);
67 |
68 | assert.equal(p4.next, p3);
69 | assert.equal(p3.next, p2);
70 | assert.equal(p2.next, null);
71 | assert.equal(p1.next, null);
72 | });
73 |
74 | it('should find missing ok', function() {
75 | cache.get = () => {
76 | return {company: true, leetcode: false, solution: true};
77 | };
78 |
79 | const res = Plugin.init(p4);
80 | assert.equal(res, false);
81 | assert.deepEqual(Plugin.plugins.length, 5);
82 |
83 | const names = Plugin.plugins.map(p => p.name);
84 | assert.deepEqual(names, ['retry', 'cache', 'leetcode', 'company', 'solution']);
85 |
86 | assert.equal(p4.next, p3);
87 | assert.equal(p3.next, p2);
88 | assert.equal(p2.next, null);
89 | assert.equal(p1.next, null);
90 | });
91 | }); // #Plugin.init
92 |
93 | describe('#install', function() {
94 | let expected;
95 |
96 | before(function() {
97 | Plugin.__set__('cp', {
98 | exec: function(cmd, opts, cb) {
99 | expected = cmd;
100 | return cb();
101 | }
102 | });
103 | });
104 |
105 | it('should install no deps ok', function(done) {
106 | expected = '';
107 | const p = new Plugin(100, 'test', '2017.12.26', 'desc', []);
108 | p.install(function() {
109 | assert.equal(expected, '');
110 | done();
111 | });
112 | });
113 |
114 | it('should install deps ok', function(done) {
115 | const deps = ['a', 'b:linux', 'b:darwin', 'b:win32', 'c:bad', 'd'];
116 | const p = new Plugin(100, 'test', '2017.12.26', 'desc', deps);
117 | p.install(function() {
118 | assert.equal(expected, 'npm install --save a b d');
119 | done();
120 | });
121 | });
122 | }); // #install
123 |
124 | describe('#Plugin.copy', function() {
125 | const SRC = path.resolve(th.DIR, 'copy.src.js');
126 | const DST = path.resolve(th.DIR, 'copy.test.js');
127 |
128 | before(function() {
129 | file.pluginFile = () => DST;
130 | });
131 |
132 | it('should copy from http error', function(done) {
133 | Plugin.copy('non-exists', function(e, fullpath) {
134 | assert.equal(e, 'HTTP Error: 404');
135 | assert.equal(fs.existsSync(DST), false);
136 | done();
137 | });
138 | }).timeout(5000);
139 |
140 | it('should copy from local ok', function(done) {
141 | const data = [
142 | 'module.exports = {',
143 | ' x: 123,',
144 | ' install: function(cb) { cb(); }',
145 | '};'
146 | ];
147 | fs.writeFileSync(SRC, data.join('\n'));
148 |
149 | Plugin.copy(SRC, function(e, fullpath) {
150 | assert.notExists(e);
151 | assert.equal(fullpath, DST);
152 | assert.equal(fs.existsSync(DST), true);
153 | done();
154 | });
155 | });
156 | }); // #Plugin.copy
157 |
158 | describe('#Plugin.installMissings', function() {
159 | const PLUGINS = [
160 | new Plugin(0, '0', 'missing'),
161 | new Plugin(1, '1', '2018.01.01'),
162 | new Plugin(2, '2', 'missing'),
163 | ];
164 | let expected;
165 |
166 | beforeEach(function() {
167 | expected = [];
168 | file.pluginFile = x => th.DIR + x;
169 | Plugin.install = (name, cb) => {
170 | expected.push(name);
171 | return cb(null, PLUGINS[+name]);
172 | };
173 | });
174 |
175 | it('should ok', function(done) {
176 | Plugin.plugins = PLUGINS;
177 | Plugin.installMissings(function(e) {
178 | assert.notExists(e);
179 | assert.deepEqual(expected, ['0', '2']);
180 | done();
181 | });
182 | });
183 | }); // #Plugin.installMissings
184 |
185 | describe('#delete', function() {
186 | it('should ok', function() {
187 | file.pluginFile = x => th.DIR + x;
188 |
189 | const p = new Plugin(0, '0', '2018.01.01');
190 | p.file = '0.js';
191 | fs.writeFileSync('./tmp/0.js', '');
192 |
193 | assert.equal(p.deleted, false);
194 | assert.deepEqual(fs.readdirSync(th.DIR), ['0.js']);
195 | p.delete();
196 | assert.equal(p.deleted, true);
197 | assert.deepEqual(fs.readdirSync(th.DIR), []);
198 | p.delete();
199 | assert.equal(p.deleted, true);
200 | assert.deepEqual(fs.readdirSync(th.DIR), []);
201 | });
202 | }); // #delete
203 |
204 | describe('#save', function() {
205 | it('should ok', function() {
206 | let data = {};
207 | cache.get = () => data;
208 | cache.set = (k, x) => data = x;
209 |
210 | const p = new Plugin(0, '0', '2018.01.01');
211 | p.save();
212 | assert.deepEqual(data, {'0': true});
213 |
214 | p.enabled = false;
215 | p.save();
216 | assert.deepEqual(data, {'0': false});
217 |
218 | p.deleted = true;
219 | p.save();
220 | assert.deepEqual(data, {});
221 | });
222 | }); // #save
223 | });
224 |
--------------------------------------------------------------------------------
/lib/helper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var _ = require('underscore');
3 | var ora = require('ora');
4 |
5 | var file = require('./file');
6 |
7 | const UNITS_SIZE = [
8 | {unit: 'B', name: 'Bytes', count: 1024},
9 | {unit: 'K', name: 'KBytes', count: 1024},
10 | {unit: 'M', name: 'MBytes', count: 1024},
11 | {unit: 'G', name: 'GBytes', count: -1}
12 | ];
13 |
14 | const UNITS_TIME = [
15 | {unit: 's', name: 'seconds', count: 60},
16 | {unit: 'm', name: 'minutes', count: 60},
17 | {unit: 'h', name: 'hours', count: 24},
18 | {unit: 'd', name: 'days', count: 7},
19 | {unit: 'w', name: 'weeks', count: 4},
20 | {unit: 'm', name: 'months', count: 12},
21 | {unit: 'y', name: 'years', count: -1}
22 | ];
23 |
24 | function getUnit(units, v) {
25 | for (let i = 0; i < units.length; ++i) {
26 | if (units[i].count <= 0 || v < units[i].count)
27 | return [v, units[i]];
28 | v /= units[i].count;
29 | }
30 | }
31 |
32 | const LANGS = [
33 | {lang: 'bash', ext: '.sh', style: '#'},
34 | {lang: 'c', ext: '.c', style: 'c'},
35 | {lang: 'cpp', ext: '.cpp', style: 'c'},
36 | {lang: 'csharp', ext: '.cs', style: 'c'},
37 | {lang: 'golang', ext: '.go', style: 'c'},
38 | {lang: 'java', ext: '.java', style: 'c'},
39 | {lang: 'javascript', ext: '.js', style: 'c'},
40 | {lang: 'kotlin', ext: '.kt', style: 'c'},
41 | {lang: 'mysql', ext: '.sql', style: '--'},
42 | {lang: 'php', ext: '.php', style: 'c'},
43 | {lang: 'python', ext: '.py', style: '#'},
44 | {lang: 'python3', ext: '.py', style: '#'},
45 | {lang: 'ruby', ext: '.rb', style: '#'},
46 | {lang: 'rust', ext: '.rs', style: 'c'},
47 | {lang: 'scala', ext: '.scala', style: 'c'},
48 | {lang: 'swift', ext: '.swift', style: 'c'},
49 | {lang: 'typescript', ext: '.ts', style: 'c'}
50 | ];
51 |
52 | const h = {};
53 |
54 | h.KEYS = {
55 | user: '../user',
56 | stat: '../stat',
57 | plugins: '../../plugins',
58 | problems: 'problems',
59 | translation: 'translationConfig',
60 | problem: p => p.fid + '.' + p.slug + '.' + p.category
61 | };
62 |
63 | h.prettyState = function(state) {
64 | switch (state) {
65 | case 'ac': return this.prettyText('', true);
66 | case 'notac': return this.prettyText('', false);
67 | default: return ' ';
68 | }
69 | };
70 |
71 | h.prettyText = function(text, yesNo) {
72 | const chalk = require('./chalk');
73 | const icon = require('./icon');
74 | switch (yesNo) {
75 | case true: return chalk.green(icon.yes + text);
76 | case false: return chalk.red(icon.no + text);
77 | default: return text;
78 | }
79 | };
80 |
81 | h.prettySize = function(n) {
82 | const res = getUnit(UNITS_SIZE, n);
83 | return res[0].toFixed(2) + res[1].unit;
84 | };
85 |
86 | h.prettyTime = function(n) {
87 | const res = getUnit(UNITS_TIME, n);
88 | return res[0].toFixed(0) + ' ' + res[1].name;
89 | };
90 |
91 | h.prettyLevel = function(level) {
92 | const chalk = require('./chalk');
93 | switch (level.toLowerCase().trim()) {
94 | case 'easy': return chalk.green(level);
95 | case 'medium': return chalk.yellow(level);
96 | case 'hard': return chalk.red(level);
97 | default: return level;
98 | }
99 | };
100 |
101 | h.levelToName = function(level) {
102 | switch (level) {
103 | case 1: return 'Easy';
104 | case 2: return 'Medium';
105 | case 3: return 'Hard';
106 | default: return ' ';
107 | }
108 | };
109 |
110 | h.statusToName = function(sc) {
111 | switch (sc) {
112 | case 10: return 'Accepted';
113 | case 11: return 'Wrong Answer';
114 | case 12: return 'Memory Limit Exceeded';
115 | case 13: return 'Output Limit Exceeded';
116 | case 14: return 'Time Limit Exceeded';
117 | case 15: return 'Runtime Error';
118 | case 16: return 'Internal Error';
119 | case 20: return 'Compile Error';
120 | case 21: return 'Unknown Error';
121 | default: return 'Unknown';
122 | }
123 | };
124 |
125 | h.langToExt = function(lang) {
126 | const res = LANGS.find(x => x.lang === lang);
127 | return res ? res.ext : '.raw';
128 | };
129 |
130 | h.extToLang = function(fullpath) {
131 | const res = LANGS.find(x => fullpath.endsWith(x.ext));
132 | return res ? res.lang : 'unknown';
133 | };
134 |
135 | h.langToCommentStyle = function(lang) {
136 | const res = LANGS.find(x => x.lang === lang);
137 |
138 | return (res && res.style === 'c') ?
139 | {start: '/*', line: ' *', end: ' */', singleLine: '//'} :
140 | {start: res.style, line: res.style, end: res.style, singleLine: res.style};
141 | };
142 |
143 | h.readStdin = function(cb) {
144 | const stdin = process.stdin;
145 | const bufs = [];
146 |
147 | console.log('NOTE: to finish the input, press ' +
148 | (file.isWindows() ? ' and ' : ''));
149 |
150 | stdin.on('readable', function() {
151 | const data = stdin.read();
152 | if (data) {
153 | // windows doesn't treat ctrl-D as EOF
154 | if (file.isWindows() && data.toString() === '\x04\r\n') {
155 | stdin.emit('end');
156 | } else {
157 | bufs.push(data);
158 | }
159 | }
160 | });
161 | stdin.on('end', function() {
162 | cb(null, Buffer.concat(bufs).toString());
163 | });
164 | stdin.on('error', cb);
165 | };
166 |
167 | h.getSetCookieValue = function(resp, key) {
168 | const cookies = resp.headers['set-cookie'];
169 | if (!cookies) return null;
170 |
171 | for (let i = 0; i < cookies.length; ++i) {
172 | const sections = cookies[i].split(';');
173 | for (let j = 0; j < sections.length; ++j) {
174 | const kv = sections[j].trim().split('=');
175 | if (kv[0] === key) return kv[1];
176 | }
177 | }
178 | return null;
179 | };
180 |
181 | h.printSafeHTTP = function(msg) {
182 | return msg.replace(/(Cookie\s*:\s*)'.*?'/, '$1')
183 | .replace(/('X-CSRFToken'\s*:\s*)'.*?'/, '$1')
184 | .replace(/('set-cookie'\s*:\s*)\[.*?\]/, '$1');
185 | };
186 |
187 | h.spin = function(s) {
188 | return ora(require('./chalk').gray(s)).start();
189 | };
190 |
191 | const COLORS = {
192 | blue: {fg: 'white', bg: 'bgBlue'},
193 | cyan: {fg: 'white', bg: 'bgCyan'},
194 | gray: {fg: 'white', bg: 'bgGray'},
195 | green: {fg: 'black', bg: 'bgGreen'},
196 | magenta: {fg: 'white', bg: 'bgMagenta'},
197 | red: {fg: 'white', bg: 'bgRed'},
198 | yellow: {fg: 'black', bg: 'bgYellow'},
199 | white: {fg: 'black', bg: 'bgWhite'}
200 | };
201 | h.badge = function(s, color) {
202 | s = ' ' + s + ' ';
203 | if (color === 'random')
204 | color = _.chain(COLORS).keys().sample().value();
205 | const c = COLORS[color || 'blue'];
206 |
207 | const chalk = require('./chalk');
208 | return chalk[c.fg][c.bg](s);
209 | };
210 |
211 | module.exports = h;
212 |
--------------------------------------------------------------------------------
/lib/commands/show.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var util = require('util');
3 |
4 | var _ = require('underscore');
5 | var childProcess = require('child_process');
6 |
7 | var h = require('../helper');
8 | var file = require('../file');
9 | var chalk = require('../chalk');
10 | var icon = require('../icon');
11 | var log = require('../log');
12 | var config = require('../config');
13 | var core = require('../core');
14 | var session = require('../session');
15 |
16 | const cmd = {
17 | command: 'show [keyword]',
18 | aliases: ['view', 'pick'],
19 | desc: 'Show question',
20 | builder: function(yargs) {
21 | return yargs
22 | .option('c', {
23 | alias: 'codeonly',
24 | type: 'boolean',
25 | default: false,
26 | describe: 'Only show code template'
27 | })
28 | .option('e', {
29 | alias: 'editor',
30 | type: 'string',
31 | describe: 'Open source code in editor'
32 | })
33 | .option('g', {
34 | alias: 'gen',
35 | type: 'boolean',
36 | default: false,
37 | describe: 'Generate source code'
38 | })
39 | .option('l', {
40 | alias: 'lang',
41 | type: 'string',
42 | default: config.code.lang,
43 | describe: 'Programming language of the source code',
44 | choices: config.sys.langs
45 | })
46 | .option('o', {
47 | alias: 'outdir',
48 | type: 'string',
49 | describe: 'Where to save source code',
50 | default: '.'
51 | })
52 | .option('q', core.filters.query)
53 | .option('t', core.filters.tag)
54 | .option('x', {
55 | alias: 'extra',
56 | type: 'boolean',
57 | default: false,
58 | describe: 'Show extra question details in source code'
59 | })
60 | .option('T', {
61 | alias: 'dontTranslate',
62 | type: 'boolean',
63 | default: false,
64 | describe: 'Set to true to disable endpoint\'s translation',
65 | })
66 | .positional('keyword', {
67 | type: 'string',
68 | default: '',
69 | describe: 'Show question by name or id'
70 | })
71 | .example(chalk.yellow('leetcode show 1'), 'Show question 1')
72 | .example(chalk.yellow('leetcode show 1 -gx -l java'), 'Show question 1 and generate Java code')
73 | .example(chalk.yellow('leetcode show 1 -gxe'), 'Open generated code in editor')
74 | .example('', '')
75 | .example(chalk.yellow('leetcode show'), 'Show random question')
76 | .example(chalk.yellow('leetcode show -q h'), 'Show random hard question')
77 | .example(chalk.yellow('leetcode show -t google'), 'Show random question from Google (require plugin)');
78 | }
79 | };
80 |
81 | function genFileName(problem, opts) {
82 | const path = require('path');
83 | const params = [
84 | file.fmt(config.file.show, problem),
85 | '',
86 | h.langToExt(opts.lang)
87 | ];
88 |
89 | // try new name to avoid overwrite by mistake
90 | for (let i = 0; ; ++i) {
91 | const name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.'));
92 | if (!file.exist(name))
93 | return name;
94 | params[1] = i;
95 | }
96 | }
97 |
98 | function showProblem(problem, argv) {
99 | const taglist = [problem.category]
100 | .concat(problem.companies || [])
101 | .concat(problem.tags || [])
102 | .map(x => h.badge(x, 'blue'))
103 | .join(' ');
104 | const langlist = problem.templates
105 | .map(x => h.badge(x.value, 'yellow'))
106 | .sort()
107 | .join(' ');
108 |
109 | let code;
110 | const needcode = argv.gen || argv.codeonly;
111 | if (needcode) {
112 | const template = problem.templates.find(x => x.value === argv.lang);
113 | if (!template) {
114 | log.fail('Not supported language "' + argv.lang + '"');
115 | log.warn('Supported languages: ' + langlist);
116 | return;
117 | }
118 |
119 | const opts = {
120 | lang: argv.lang,
121 | code: template.defaultCode,
122 | tpl: argv.extra ? 'detailed' : 'codeonly'
123 | };
124 | code = core.exportProblem(problem, opts);
125 | }
126 |
127 | let filename;
128 | if (argv.gen) {
129 | file.mkdir(argv.outdir);
130 | filename = genFileName(problem, argv);
131 | file.write(filename, code);
132 |
133 | if (argv.editor !== undefined) {
134 | childProcess.spawn(argv.editor || config.code.editor, [filename], {
135 | // in case your editor of choice is vim or emacs
136 | stdio: 'inherit'
137 | });
138 | }
139 | } else {
140 | if (argv.codeonly) {
141 | log.info(chalk.yellow(code));
142 | return;
143 | }
144 | }
145 |
146 | log.printf('[%s] %s %s', problem.fid, problem.name,
147 | (problem.starred ? chalk.yellow(icon.like) : icon.empty));
148 | log.info();
149 | log.info(chalk.underline(problem.link));
150 | if (argv.extra) {
151 | log.info();
152 | log.info('Tags: ' + taglist);
153 | log.info();
154 | log.info('Langs: ' + langlist);
155 | }
156 |
157 | log.info();
158 | log.printf('* %s', problem.category);
159 | log.printf('* %s (%s%%)', h.prettyLevel(problem.level), problem.percent.toFixed(2));
160 |
161 | if (problem.likes)
162 | log.printf('* Likes: %s', problem.likes);
163 | if (problem.dislikes)
164 | log.printf('* Dislikes: %s', problem.dislikes);
165 | else
166 | log.printf('* Dislikes: -');
167 | if (problem.totalAC)
168 | log.printf('* Total Accepted: %s', problem.totalAC);
169 | if (problem.totalSubmit)
170 | log.printf('* Total Submissions: %s', problem.totalSubmit);
171 | if (problem.testable && problem.testcase)
172 | log.printf('* Testcase Example: %s', chalk.yellow(util.inspect(problem.testcase)));
173 | if (filename)
174 | log.printf('* Source Code: %s', chalk.yellow.underline(filename));
175 |
176 | log.info();
177 | log.info(problem.desc);
178 | }
179 |
180 | cmd.handler = function(argv) {
181 | session.argv = argv;
182 | if (argv.keyword.length > 0) {
183 | // show specific one
184 | core.getProblem(argv.keyword, !argv.dontTranslate, function(e, problem) {
185 | if (e) return log.fail(e);
186 | showProblem(problem, argv);
187 | });
188 | } else {
189 | // show random one
190 | core.filterProblems(argv, function(e, problems) {
191 | if (e) return log.fail(e);
192 |
193 | // random select one that not AC-ed yet
194 | const user = session.getUser();
195 | problems = problems.filter(function(x) {
196 | if (x.state === 'ac') return false;
197 | if (!user.paid && x.locked) return false;
198 | return true;
199 | });
200 | if (problems.length === 0) return log.fail('Problem not found!');
201 |
202 | const problem = _.sample(problems);
203 | core.getProblem(problem, !argv.dontTranslate, function(e, problem) {
204 | if (e) return log.fail(e);
205 | showProblem(problem, argv);
206 | });
207 | });
208 | }
209 | };
210 |
211 | module.exports = cmd;
212 |
--------------------------------------------------------------------------------
/docs/advanced.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Advanced Topic
4 | ---
5 |
6 | * [Aliases](#aliases)
7 | * [Auto Login](#auto-login)
8 | * [Bash Completion](#bash-completion)
9 | * [Cache](#cache)
10 | * [Configuration](#configuration)
11 | * [Color Themes](#color-themes)
12 | * [File Name](#file-name)
13 | * [Log Levels](#log-levels)
14 | * [Plugins](#plugins)
15 |
16 | # Aliases
17 |
18 | The commands in leetcode-cli usually has builtin aliases as below:
19 |
20 | |Command |Aliases |
21 | |----------|-----------------------|
22 | |config |conf, cfg, setting |
23 | |list |ls |
24 | |plugin |extension, ext |
25 | |session |branch |
26 | |show |view, pick |
27 | |star |like, favorite |
28 | |stat |stats, progress, report|
29 | |submission|pull |
30 | |submit |push, commit |
31 | |test |run |
32 | |user |account |
33 | |version |info, env |
34 |
35 | # Auto Login
36 |
37 | Leetcode.com is restricting only one session alive in the same time, which means if you login same account otherwhere, the existing login session will be expired immediately. This will greatly harm your experience since you have to re-login again and again among different sessions.
38 |
39 | The good news is leetcode-cli will help a lot on this by trying re-login transparently and automatically without interrupting your current work whenever it detects your current session is expired. To enable this feature you could add following in your config then login again:
40 |
41 | {
42 | "autologin": {
43 | "enable": true
44 | }
45 | }
46 |
47 | **NOTE: once enabled, your PASSWORD will be persisted locally for further using, so PLEASE be careful to ONLY enable this on your OWN computer for the sake of security!**
48 |
49 | # Bash Completion
50 |
51 | Copy `.lc-completion.bash` to your home directory, and source it in .bashrc (Linux) or .bash_profile (MacOS).
52 |
53 | $ cp .lc-completion.bash ~
54 | $ echo "source ~/.lc-completion.bash" >> ~/.bashrc
55 | $ source ~/.bashrc
56 |
57 | $ leetcode list --
58 | --help --keyword --query --stat
59 |
60 | **NOTE: it might become slower in bash with this enabled, personally I would NOT suggest to use it...**
61 |
62 | # Cache
63 |
64 | The local cache folder (`.lc/`) is in your home directory, e.g.
65 |
66 | $ ls -a1 ~/.lc/
67 | cache # folder of cached questions
68 | config.json # user customized config
69 | user.json # user account info
70 |
71 | $ ls -a1 ~/.lc/cache/
72 | problems.json # cached questions list
73 | 1.two-sum.algorithms.json # cached specific question
74 |
75 | **NOTE: Normally you don't need dig into the folder to manipulate those files. Use [cache command](https://skygragon.github.io/leetcode-cli/commands#cache) instead.**
76 |
77 | # Configuration
78 |
79 | The config file is saved in `~/.lc/config.json`, here is a full exmaple (includes default configs):
80 |
81 | $ cat ~/.lc/config.json
82 |
83 | {
84 | "auto_login": {
85 | "enable": false
86 | },
87 | "code": {
88 | "editor": "vim",
89 | "lang": "cpp"
90 | },
91 | "color": {
92 | "enable": true,
93 | "theme": "default"
94 | },
95 | "file": {
96 | "show": "${fid}.${slug}",
97 | "submission": "${fid}.${slug}.${sid}.${ac}"
98 | },
99 | "icon": {
100 | "theme": ""
101 | },
102 | "network": {
103 | "concurrency": 10
104 | },
105 | "plugins": {}
106 | }
107 |
108 | Here are some useful settings:
109 |
110 | * `autologin:enable` to enable auto login feature. (see [Auto Login](#auto-login))
111 | * `code:editor` to set editor used to open generated source file.
112 | * `code:lang` to set your default language used in coding.
113 | * `color:enable` to enable colorful output.
114 | * `color:theme` to set color theme used in output. (see [Color Theme](#color-theme))
115 | * `file.show` to set filename pattern for generated code file. (see [File Name](#file-name))
116 | * `icon:theme` to set icon them used in output.
117 | * `plugins` to config each installed plugins. (see [Plugins](#plugins))
118 |
119 | **NOTE: Normally you don't need dig into the folder to manipulate those files. Use [config command](https://skygragon.github.io/leetcode-cli/commands#config) instead.**
120 |
121 | *Example*
122 |
123 | Config for `github.js` and `cpp.lint.js` plugins:
124 |
125 | {
126 | "plugins": {
127 | "github": {
128 | "repo": "https://github.com/skygragon/test",
129 | "token": "abcdefghijklmnopqrstuvwxyz"
130 | },
131 | "cpp.lint": {
132 | "bin": "~/bin/cpplibt.py",
133 | "flags": []
134 | }
135 | }
136 | }
137 |
138 | # Color Themes
139 |
140 | You can choose to use colorful output or not.
141 |
142 | * `--color` to enable color.
143 | * `--no-color` to disable it.
144 |
145 | Or use configuration setting to avoid typing it repeatedly. (see [color:enable](#configuration))
146 |
147 | When color is enabled, you can choose your favor color theme as well. (see [color:theme](#configuration))
148 |
149 | Following are available themes:
150 |
151 | * `blue`
152 | * `dark` for night.
153 | * `default`
154 | * `molokai`
155 | * `orange`
156 | * `pink` for girls.
157 | * `solarized`
158 | * `solarized.light`
159 |
160 | Of course you can create your own themes if you like, look into `colors` folder in the source code for more tips.
161 |
162 | *Example*
163 |
164 | $ cat colors/default.json
165 | {
166 | "black": "#000000",
167 | "blue": "#0000ff",
168 | "cyan": "#00ffff",
169 | "green": "#00ff00",
170 | "magenta": "#ff00ff",
171 | "red": "#ff0000",
172 | "white": "#ffffff",
173 | "yellow": "#ffff00"
174 | }
175 |
176 | # File Name
177 |
178 | You could configure file name pattern in code generation.
179 |
180 | * config `file.show` for generated file in `show`.
181 | * config `file.submission` for downloaded file in `submission`.
182 |
183 | Followings are some variables you could used in the pattern:
184 |
185 | * `${fid}` for question id. (e.g. `123`)
186 | * `${slug}` for dash-separated question name. (e.g. `add-two`)
187 | * `${name}` for space-separated questions name. (e.g. `Add Two`)
188 | * `${level}` for question level. (e.g. `Hard`)
189 | * `${category}` for question category. (e.g. `algorithms`)
190 | * `${sid}` for submission id.
191 | * `${ac}` for accept status of existing submission.
192 |
193 | # Log Levels
194 |
195 | * `-v` to enable debug output.
196 | * `-vv` to enable trace output.
197 | * Will print detailed HTTP requests/responses.
198 |
199 | # Plugins
200 |
201 | You can easily introduce more features by installing other plugins form third parties. Here lists the avaible 3rd party plugins at the moment:
202 |
203 | * [leetcode-cli-plugins](https://github.com/skygragon/leetcode-cli-plugins)
204 |
205 | Feel free to try out the plugins above. Or you can develope your own plugins to enrich leetcode-cli's functionalities.
206 |
--------------------------------------------------------------------------------
/lib/commands/stat.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var moment = require('moment');
3 | var _ = require('underscore');
4 |
5 | var chalk = require('../chalk');
6 | var icon = require('../icon');
7 | var log = require('../log');
8 | var core = require('../core');
9 | var session = require('../session');
10 | var sprintf = require('../sprintf');
11 | var h = require('../helper');
12 |
13 | const cmd = {
14 | command: 'stat',
15 | desc: 'Show statistics',
16 | aliases: ['stats', 'progress', 'report'],
17 | builder: function(yargs) {
18 | return yargs
19 | .option('c', {
20 | alias: 'cal',
21 | type: 'boolean',
22 | default: false,
23 | describe: 'Show calendar statistics'
24 | })
25 | .option('g', {
26 | alias: 'graph',
27 | type: 'boolean',
28 | default: false,
29 | describe: 'Show graphic statistics'
30 | })
31 | .option('l', {
32 | alias: 'lock',
33 | type: 'boolean',
34 | default: true,
35 | describe: 'Include locked questions'
36 | })
37 | .option('q', core.filters.query)
38 | .option('t', core.filters.tag)
39 | .example(chalk.yellow('leetcode stat'), 'Show progress status')
40 | .example(chalk.yellow('leetcode stat -g'), 'Show detailed status in graph')
41 | .example(chalk.yellow('leetcode stat -c'), 'Show accepted status in calendar')
42 | .example('', '')
43 | .example(chalk.yellow('leetcode stat --no-lock'), 'Show status without locked questions')
44 | .example(chalk.yellow('leetcode stat -t algorithms'), 'Show status of algorithms questions only')
45 | .example(chalk.yellow('leetcode stat -q h'), 'Show status of hard questions only');
46 | }
47 | };
48 |
49 | function printLine(key, done, all) {
50 | const n = 30;
51 | const percent = (all > 0) ? done / all : 0;
52 | const x = Math.ceil(n * percent);
53 | log.printf(' %s\t%3s/%-3s (%6s %%) %s%s',
54 | h.prettyLevel(key), done, all,
55 | (100 * percent).toFixed(2),
56 | chalk.green('█'.repeat(x)),
57 | chalk.red('░'.repeat(n - x)));
58 | }
59 |
60 | function showProgress(problems) {
61 | const stats = {
62 | easy: {all: 0, ac: 0},
63 | medium: {all: 0, ac: 0},
64 | hard: {all: 0, ac: 0}
65 | };
66 |
67 | for (let problem of problems) {
68 | const level = problem.level.toLowerCase();
69 | const state = problem.state.toLowerCase();
70 |
71 | if (!(level in stats)) continue;
72 | ++stats[level].all;
73 |
74 | if (!(state in stats[level])) continue;
75 | ++stats[level][state];
76 | }
77 |
78 | printLine('Easy', stats.easy.ac, stats.easy.all);
79 | printLine('Medium', stats.medium.ac, stats.medium.all);
80 | printLine('Hard', stats.hard.ac, stats.hard.all);
81 | }
82 |
83 | function showGraph(problems) {
84 | const ICONS = {
85 | ac: chalk.green(icon.ac),
86 | notac: chalk.red(icon.notac),
87 | none: chalk.gray(icon.none),
88 | empty: icon.empty
89 | };
90 |
91 | // row header is 4 bytes
92 | // each question takes 2 bytes
93 | // each group has 10 questions, which takes (2*10=20) + 3 paddings
94 | let groups = Math.floor((h.width - 4) / (3 + 2 * 10));
95 | if (groups < 1) groups = 1;
96 | if (groups > 5) groups = 5;
97 |
98 | const header = _.range(groups)
99 | .map(x => sprintf('%4s%18s', x * 10 + 1, x * 10 + 10))
100 | .join('');
101 | log.info(' ' + header);
102 |
103 | const graph = [];
104 | for (let problem of problems)
105 | graph[problem.fid] = ICONS[problem.state] || ICONS.none;
106 |
107 | let line = [sprintf(' %04s', 0)];
108 | for (let i = 1, n = graph.length; i <= n; ++i) {
109 | // padding before group
110 | if (i % 10 === 1) line.push(' ');
111 |
112 | line.push(graph[i] || ICONS.empty);
113 |
114 | // time to start new row
115 | if (i % (10 * groups) === 0 || i === n) {
116 | log.info(line.join(' '));
117 | line = [sprintf(' %04s', i)];
118 | }
119 | }
120 |
121 | log.info();
122 | log.printf('%7s%s%3s%s%3s%s',
123 | ' ', ICONS.ac + chalk.green(' Accepted'),
124 | ' ', ICONS.notac + chalk.red(' Not Accepted'),
125 | ' ', ICONS.none + ' Remaining');
126 | }
127 |
128 | function showCal(problems) {
129 | const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
130 | const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
131 | const ICONS = [
132 | icon.none,
133 | chalk.white(icon.ac),
134 | chalk.green(icon.ac),
135 | chalk.yellow(icon.ac),
136 | chalk.red(icon.ac)
137 | ];
138 |
139 | const N_MONTHS = 12;
140 | const N_WEEKS = 53;
141 | const N_WEEKDAYS = 7;
142 |
143 | const now = moment();
144 |
145 | const SCORES = {easy: 1, medium: 2, hard: 5};
146 | function toScore(sum, id) {
147 | const problem = problems.find(x => x.fid === id);
148 | if (problem) sum += (SCORES[problem.level.toLowerCase()] || 1);
149 | return sum;
150 | }
151 |
152 | // load historical stats
153 | const graph = [];
154 | const stats = require('../cache').get(h.KEYS.stat) || {};
155 | for (let k of _.keys(stats)) {
156 | const score = (stats[k]['ac.set'] || []).reduce(toScore, 0);
157 | if (score === 0) continue;
158 |
159 | const d = moment(k, 'YYYY-MM-DD');
160 | graph[now.diff(d, 'days')] = score;
161 | }
162 |
163 | // print header
164 | const buf = Buffer.alloc(120, ' ', 'ascii');
165 | for (let i = 0; i <= N_MONTHS; ++i) {
166 | // for day 1 in each month, calculate its column position in graph
167 | const d = now.clone().subtract(i, 'months').date(1);
168 | const idx = now.diff(d, 'days');
169 |
170 | const j = (N_WEEKS - idx / N_WEEKDAYS + 1) * 2;
171 | if (j >= 0) buf.write(MONTHS[d.month()], j);
172 | }
173 | log.printf('%7s%s', ' ', buf.toString());
174 |
175 | // print graph
176 | for (let i = 0; i < N_WEEKDAYS; ++i) {
177 | const line = [];
178 | // print day in week
179 | const idx = (now.day() + i + 1) % N_WEEKDAYS;
180 | line.push(sprintf('%4s ', WEEKDAYS[idx]));
181 |
182 | for (let j = 0; j < N_WEEKS; ++j) {
183 | let idx = (N_WEEKS - j - 1) * N_WEEKDAYS + N_WEEKDAYS - i - 1;
184 | const d = now.clone().subtract(idx, 'days');
185 |
186 | // map count to icons index:
187 | // [0] => 0, [1,5] => 1, [6,10] => 2, [11,15] => 3, [16,) => 4
188 | const count = graph[idx] || 0;
189 | idx = Math.floor((count - 1) / 5) + 1;
190 | if (idx > 4) idx = 4;
191 |
192 | let icon = ICONS[idx];
193 | // use different colors for adjacent months
194 | if (idx === 0 && d.month() % 2) icon = chalk.gray(icon);
195 | line.push(icon);
196 | }
197 | log.info(line.join(' '));
198 | }
199 |
200 | log.info();
201 | log.printf('%8s%s%3s%s%3s%s%3s%s',
202 | ' ', ICONS[1] + ' 1~5',
203 | ' ', ICONS[2] + ' 6~10',
204 | ' ', ICONS[3] + ' 11~15',
205 | ' ', ICONS[4] + ' 16+');
206 | }
207 |
208 | cmd.handler = function(argv) {
209 | session.argv = argv;
210 | core.filterProblems(argv, function(e, problems) {
211 | if (e) return log.fail(e);
212 |
213 | if (!argv.lock)
214 | problems = problems.filter(x => !x.locked);
215 |
216 | log.info();
217 | if (argv.graph) showGraph(problems);
218 | else if (argv.cal) showCal(problems);
219 | else showProgress(problems);
220 | log.info();
221 | });
222 | };
223 |
224 | module.exports = cmd;
225 |
--------------------------------------------------------------------------------
/test/plugins/test_cache.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const _ = require('underscore');
3 | const assert = require('chai').assert;
4 | const rewire = require('rewire');
5 |
6 | const h = require('../../lib/helper');
7 | const log = require('../../lib/log');
8 | const config = require('../../lib/config');
9 | const th = require('../helper');
10 |
11 | describe('plugin:cache', function() {
12 | let plugin;
13 | let next;
14 | let cache;
15 | let file;
16 | let session;
17 |
18 | const PROBLEMS = [
19 | {id: 0, fid: 0, name: 'name0', slug: 'slug0', starred: false, desc: '', likes: '1', dislikes: '1', category: 'algorithms'},
20 | {id: 1, fid: 1, name: 'name1', slug: 'slug1', starred: true, desc: '', likes: '1', dislikes: '1', category: 'algorithms'}
21 | ];
22 | const TRANSLATION_CONFIGS = { useEndpointTranslation: false };
23 | const PROBLEM = {id: 0, fid: 0, slug: 'slug0', category: 'algorithms'};
24 |
25 | before(function() {
26 | log.init();
27 | config.init();
28 | });
29 |
30 | beforeEach(function() {
31 | th.clean();
32 | next = {};
33 |
34 | file = rewire('../../lib/file');
35 | file.cacheDir = () => th.DIR;
36 |
37 | cache = rewire('../../lib/cache');
38 | cache.__set__('file', file);
39 | cache.init();
40 |
41 | session = rewire('../../lib/session');
42 | session.__set__('cache', cache);
43 |
44 | plugin = rewire('../../lib/plugins/cache');
45 | plugin.__set__('cache', cache);
46 | plugin.__set__('session', session);
47 | plugin.init();
48 |
49 | plugin.setNext(next);
50 | });
51 |
52 | describe('#getProblems', function() {
53 | it('should getProblems w/ cache ok', function(done) {
54 | cache.set('problems', PROBLEMS);
55 | cache.set(h.KEYS.translation, TRANSLATION_CONFIGS);
56 |
57 | plugin.getProblems(false, function(e, problems) {
58 | assert.equal(e, null);
59 | assert.deepEqual(problems, PROBLEMS);
60 | done();
61 | });
62 | });
63 |
64 | it('should getProblems w/o cache ok', function(done) {
65 | cache.del('problems');
66 | next.getProblems = (needT, cb) => cb(null, PROBLEMS);
67 |
68 | plugin.getProblems(false, function(e, problems) {
69 | assert.equal(e, null);
70 | assert.deepEqual(problems, PROBLEMS);
71 | done();
72 | });
73 | });
74 |
75 | it('should getProblems w/o cache fail if client error', function(done) {
76 | cache.del('problems');
77 | next.getProblems = (needT, cb) => cb('client getProblems error');
78 |
79 | plugin.getProblems(false, function(e, problems) {
80 | assert.equal(e, 'client getProblems error');
81 | done();
82 | });
83 | });
84 | }); // #getProblems
85 |
86 | describe('#getProblem', function() {
87 | it('should getProblem w/ cache ok', function(done) {
88 | cache.set('problems', PROBLEMS);
89 | cache.set(h.KEYS.translation, TRANSLATION_CONFIGS);
90 | cache.set('0.slug0.algorithms', PROBLEMS[0]);
91 |
92 | plugin.getProblem(_.clone(PROBLEM), false, function(e, problem) {
93 | assert.equal(e, null);
94 | assert.deepEqual(problem, PROBLEMS[0]);
95 | done();
96 | });
97 | });
98 |
99 | it('should getProblem w/o cache ok', function(done) {
100 | cache.set('problems', PROBLEMS);
101 | cache.del('0.slug0.algorithms');
102 | next.getProblem = (problem, needT, cb) => cb(null, PROBLEMS[0]);
103 |
104 | plugin.getProblem(_.clone(PROBLEM), false, function(e, problem) {
105 | assert.equal(e, null);
106 | assert.deepEqual(problem, PROBLEMS[0]);
107 | done();
108 | });
109 | });
110 |
111 | it('should getProblem fail if client error', function(done) {
112 | cache.set('problems', PROBLEMS);
113 | cache.del('0.slug0.algorithms');
114 | next.getProblem = (problem, needT, cb) => cb('client getProblem error');
115 |
116 | plugin.getProblem(_.clone(PROBLEM), false, function(e, problem) {
117 | assert.equal(e, 'client getProblem error');
118 | done();
119 | });
120 | });
121 | }); // #getProblem
122 |
123 | describe('#saveProblem', function() {
124 | it('should ok', function() {
125 | cache.del('0.slug0.algorithms');
126 |
127 | const problem = _.clone(PROBLEMS[0]);
128 | problem.locked = true;
129 | problem.state = 'ac';
130 |
131 | const ret = plugin.saveProblem(problem);
132 | assert.equal(ret, true);
133 | assert.deepEqual(cache.get('0.slug0.algorithms'),
134 | {id: 0, fid: 0, slug: 'slug0', name: 'name0', desc: '', likes: '1', dislikes: '1', category: 'algorithms'});
135 | });
136 | }); // #saveProblem
137 |
138 | describe('#updateProblem', function() {
139 | it('should updateProblem ok', function(done) {
140 | cache.set('problems', PROBLEMS);
141 | cache.set(h.KEYS.translation, TRANSLATION_CONFIGS);
142 |
143 | const kv = {value: 'value00'};
144 | const ret = plugin.updateProblem(PROBLEMS[0], kv);
145 | assert.equal(ret, true);
146 |
147 | plugin.getProblems(false, function(e, problems) {
148 | assert.equal(e, null);
149 | assert.deepEqual(problems, [
150 | {id: 0, fid: 0, name: 'name0', slug: 'slug0', value: 'value00', starred: false, desc: '', likes: '1', dislikes: '1', category: 'algorithms'},
151 | {id: 1, fid: 1, name: 'name1', slug: 'slug1', starred: true, desc: '', likes: '1', dislikes: '1', category: 'algorithms'}
152 | ]);
153 | done();
154 | });
155 | });
156 |
157 | it('should updateProblem fail if no problems found', function() {
158 | cache.del('problems');
159 | const ret = plugin.updateProblem(PROBLEMS[0], {});
160 | assert.equal(ret, false);
161 | });
162 |
163 | it('should updateProblem fail if unknown problem', function() {
164 | cache.set('problems', [PROBLEMS[1]]);
165 | const ret = plugin.updateProblem(PROBLEMS[0], {});
166 | assert.equal(ret, false);
167 | });
168 | }); // #updateProblem
169 |
170 | describe('#user', function() {
171 | const USER = {name: 'test-user', pass: 'password'};
172 | const USER_SAFE = {name: 'test-user'};
173 |
174 | it('should login ok', function(done) {
175 | config.autologin.enable = true;
176 | // before login
177 | cache.del(h.KEYS.user);
178 | assert.equal(session.getUser(), null);
179 | assert.equal(session.isLogin(), false);
180 |
181 | next.login = (user, cb) => cb(null, user);
182 |
183 | plugin.login(USER, function(e, user) {
184 | assert.equal(e, null);
185 | assert.deepEqual(user, USER);
186 |
187 | // after login
188 | assert.deepEqual(session.getUser(), USER);
189 | assert.equal(session.isLogin(), true);
190 | done();
191 | });
192 | });
193 |
194 | it('should login ok w/ auto login', function(done) {
195 | config.autologin.enable = false;
196 | cache.del(h.KEYS.user);
197 |
198 | next.login = (user, cb) => cb(null, user);
199 |
200 | plugin.login(USER, function(e, user) {
201 | assert.equal(e, null);
202 | assert.deepEqual(user, USER);
203 | assert.deepEqual(session.getUser(), USER_SAFE);
204 | assert.equal(session.isLogin(), true);
205 | done();
206 | });
207 | });
208 |
209 | it('should login fail if client login error', function(done) {
210 | next.login = (user, cb) => cb('client login error');
211 |
212 | plugin.login(USER, function(e, user) {
213 | assert.equal(e, 'client login error');
214 | done();
215 | });
216 | });
217 |
218 | it('should logout ok', function(done) {
219 | // before logout
220 | cache.set(h.KEYS.user, USER);
221 | assert.deepEqual(session.getUser(), USER);
222 | assert.equal(session.isLogin(), true);
223 |
224 | // after logout
225 | plugin.logout(USER, true);
226 | assert.equal(session.getUser(), null);
227 | assert.equal(session.isLogin(), false);
228 | done();
229 | });
230 |
231 | it('should logout ok', function(done) {
232 | // before logout
233 | cache.set(h.KEYS.user, USER);
234 | assert.deepEqual(session.getUser(), USER);
235 | assert.equal(session.isLogin(), true);
236 |
237 | // after logout
238 | plugin.logout(null, true);
239 | assert.equal(session.getUser(), null);
240 | assert.equal(session.isLogin(), false);
241 | done();
242 | });
243 | }); // #user
244 | });
245 |
--------------------------------------------------------------------------------
/test/test_helper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('chai').assert;
3 | const rewire = require('rewire');
4 | const _ = require('underscore');
5 |
6 | const chalk = require('../lib/chalk');
7 |
8 | describe('helper', function() {
9 | let h;
10 |
11 | before(function() {
12 | chalk.init();
13 | });
14 |
15 | beforeEach(function() {
16 | h = rewire('../lib/helper');
17 | });
18 |
19 | describe('#prettyState', function() {
20 | it('should ok w/ color', function() {
21 | chalk.enabled = true;
22 |
23 | assert.equal(h.prettyState('ac'), chalk.green('✔'));
24 | assert.equal(h.prettyState('notac'), chalk.red('✘'));
25 | assert.equal(h.prettyState('none'), ' ');
26 | assert.equal(h.prettyState(''), ' ');
27 | assert.equal(h.prettyState(null), ' ');
28 | });
29 |
30 | it('should ok w/o color', function() {
31 | chalk.enabled = false;
32 |
33 | assert.equal(h.prettyState('ac'), '✔');
34 | assert.equal(h.prettyState('notac'), '✘');
35 | assert.equal(h.prettyState('none'), ' ');
36 | assert.equal(h.prettyState(''), ' ');
37 | assert.equal(h.prettyState(null), ' ');
38 | });
39 | }); // #prettyState
40 |
41 | describe('#prettyText', function() {
42 | it('should ok w/ color', function() {
43 | chalk.enabled = true;
44 |
45 | assert.equal(h.prettyText(' text', true), chalk.green('✔ text'));
46 | assert.equal(h.prettyText(' text', false), chalk.red('✘ text'));
47 | assert.equal(h.prettyText('text'), 'text');
48 | });
49 |
50 | it('should ok w/o color', function() {
51 | chalk.enabled = false;
52 |
53 | assert.equal(h.prettyText(' text', true), '✔ text');
54 | assert.equal(h.prettyText(' text', false), '✘ text');
55 | assert.equal(h.prettyText('text'), 'text');
56 | });
57 | }); // #prettyText
58 |
59 | describe('#prettyLevel', function() {
60 | it('should ok w/ color', function() {
61 | chalk.enabled = true;
62 |
63 | assert.equal(h.prettyLevel('Easy'), chalk.green('Easy'));
64 | assert.equal(h.prettyLevel('Medium'), chalk.yellow('Medium'));
65 | assert.equal(h.prettyLevel('Hard'), chalk.red('Hard'));
66 | assert.equal(h.prettyLevel('easy '), chalk.green('easy '));
67 | assert.equal(h.prettyLevel('medium'), chalk.yellow('medium'));
68 | assert.equal(h.prettyLevel('hard '), chalk.red('hard '));
69 | assert.equal(h.prettyLevel('unknown'), 'unknown');
70 | });
71 | }); // #prettyLevel
72 |
73 | describe('#prettySize', function() {
74 | it('should ok', function() {
75 | assert.equal(h.prettySize(0), '0.00B');
76 | assert.equal(h.prettySize(512), '512.00B');
77 | assert.equal(h.prettySize(1024), '1.00K');
78 | assert.equal(h.prettySize(1024 * 1024), '1.00M');
79 | assert.equal(h.prettySize(1024 * 1024 * 1024), '1.00G');
80 | });
81 | }); // #prettySize
82 |
83 | describe('#prettyTime', function() {
84 | it('should ok', function() {
85 | assert.equal(h.prettyTime(30), '30 seconds');
86 | assert.equal(h.prettyTime(60), '1 minutes');
87 | assert.equal(h.prettyTime(2400), '40 minutes');
88 | assert.equal(h.prettyTime(3600), '1 hours');
89 | assert.equal(h.prettyTime(7200), '2 hours');
90 | assert.equal(h.prettyTime(86400), '1 days');
91 | assert.equal(h.prettyTime(86400 * 3), '3 days');
92 | assert.equal(h.prettyTime(86400 * 7), '1 weeks');
93 | });
94 | }); // #prettyTime
95 |
96 | describe('#levelToName', function() {
97 | it('should ok', function() {
98 | assert.equal(h.levelToName(0), ' ');
99 | assert.equal(h.levelToName(1), 'Easy');
100 | assert.equal(h.levelToName(2), 'Medium');
101 | assert.equal(h.levelToName(3), 'Hard');
102 | assert.equal(h.levelToName(4), ' ');
103 | });
104 | }); // #levelToName
105 |
106 | describe('#statusToName', function() {
107 | it('should ok', function() {
108 | assert.equal(h.statusToName(10), 'Accepted');
109 | assert.equal(h.statusToName(11), 'Wrong Answer');
110 | assert.equal(h.statusToName(12), 'Memory Limit Exceeded');
111 | assert.equal(h.statusToName(13), 'Output Limit Exceeded');
112 | assert.equal(h.statusToName(14), 'Time Limit Exceeded');
113 | assert.equal(h.statusToName(15), 'Runtime Error');
114 | assert.equal(h.statusToName(16), 'Internal Error');
115 | assert.equal(h.statusToName(20), 'Compile Error');
116 | assert.equal(h.statusToName(21), 'Unknown Error');
117 | assert.equal(h.statusToName(99), 'Unknown');
118 | });
119 | }); // #statusToName
120 |
121 | describe('#langToExt', function() {
122 | it('should ok', function() {
123 | assert.equal(h.langToExt('bash'), '.sh');
124 | assert.equal(h.langToExt('c'), '.c');
125 | assert.equal(h.langToExt('cpp'), '.cpp');
126 | assert.equal(h.langToExt('csharp'), '.cs');
127 | assert.equal(h.langToExt('golang'), '.go');
128 | assert.equal(h.langToExt('java'), '.java');
129 | assert.equal(h.langToExt('javascript'), '.js');
130 | assert.equal(h.langToExt('mysql'), '.sql');
131 | assert.equal(h.langToExt('php'), '.php');
132 | assert.equal(h.langToExt('python'), '.py');
133 | assert.equal(h.langToExt('python3'), '.py');
134 | assert.equal(h.langToExt('ruby'), '.rb');
135 | assert.equal(h.langToExt('rust'), '.rs');
136 | assert.equal(h.langToExt('scala'), '.scala');
137 | assert.equal(h.langToExt('swift'), '.swift');
138 | assert.equal(h.langToExt('rust'), '.rs');
139 | assert.equal(h.langToExt('typescript'), '.ts');
140 | });
141 | }); // #langToExt
142 |
143 | describe('#extToLang', function() {
144 | it('should ok', function() {
145 | assert.equal(h.extToLang('/usr/bin/file.sh'), 'bash');
146 | assert.equal(h.extToLang('/home/skygragon/file.c'), 'c');
147 | assert.equal(h.extToLang('/var/log/file.cpp'), 'cpp');
148 | assert.equal(h.extToLang('./file.cs'), 'csharp');
149 | assert.equal(h.extToLang('../file.go'), 'golang');
150 | assert.equal(h.extToLang('file.java'), 'java');
151 | assert.equal(h.extToLang('~/leetcode/../file.sql'), 'mysql');
152 | assert.equal(h.extToLang('~/leetcode/hello.php'), 'php');
153 | assert.equal(h.extToLang('c:/file.js'), 'javascript');
154 | assert.equal(h.extToLang('c:/Users/skygragon/file.py'), 'python');
155 | assert.equal(h.extToLang('~/file.rb'), 'ruby');
156 | assert.equal(h.extToLang('~/leetcode/file.rs'), 'rust');
157 | assert.equal(h.extToLang('/tmp/file.scala'), 'scala');
158 | assert.equal(h.extToLang('~/leetcode/file.swift'), 'swift');
159 | assert.equal(h.extToLang('~/leetcode/../file.sql'), 'mysql');
160 | assert.equal(h.extToLang('/home/skygragon/file.dat'), 'unknown');
161 | assert.equal(h.extToLang('~/leetcode/file.rs'), 'rust');
162 | assert.equal(h.extToLang('~/leetcode/file.ts'), 'typescript');
163 | });
164 | }); // #extToLang
165 |
166 | describe('#langToCommentStyle', function() {
167 | it('should ok', function() {
168 | const C_STYLE = {start: '/*', line: ' *', end: ' */', singleLine: '//'};
169 | const RUBY_STYLE = {start: '#', line: '#', end: '#', singleLine: '#'};
170 | const SQL_STYLE = {start: '--', line: '--', end: '--', singleLine: '--'};
171 |
172 | assert.deepEqual(h.langToCommentStyle('bash'), RUBY_STYLE);
173 | assert.deepEqual(h.langToCommentStyle('c'), C_STYLE);
174 | assert.deepEqual(h.langToCommentStyle('cpp'), C_STYLE);
175 | assert.deepEqual(h.langToCommentStyle('csharp'), C_STYLE);
176 | assert.deepEqual(h.langToCommentStyle('golang'), C_STYLE);
177 | assert.deepEqual(h.langToCommentStyle('php'), C_STYLE);
178 | assert.deepEqual(h.langToCommentStyle('java'), C_STYLE);
179 | assert.deepEqual(h.langToCommentStyle('javascript'), C_STYLE);
180 | assert.deepEqual(h.langToCommentStyle('mysql'), SQL_STYLE);
181 | assert.deepEqual(h.langToCommentStyle('rust'), C_STYLE);
182 | assert.deepEqual(h.langToCommentStyle('python'), RUBY_STYLE);
183 | assert.deepEqual(h.langToCommentStyle('python3'), RUBY_STYLE);
184 | assert.deepEqual(h.langToCommentStyle('ruby'), RUBY_STYLE);
185 | assert.deepEqual(h.langToCommentStyle('scala'), C_STYLE);
186 | assert.deepEqual(h.langToCommentStyle('swift'), C_STYLE);
187 | assert.deepEqual(h.langToCommentStyle('typescript'), C_STYLE);
188 | });
189 | }); // #langToCommentStyle
190 |
191 | describe('#getSetCookieValue', function() {
192 | it('should ok', function() {
193 | const resp = {
194 | headers: {'set-cookie': [
195 | 'key1=value1; path=/; Httponly',
196 | 'key2=value2; path=/; Httponly']
197 | }
198 | };
199 | const respNoSetCookie = {
200 | headers: {}
201 | };
202 |
203 | assert.equal(h.getSetCookieValue(resp, 'key1'), 'value1');
204 | assert.equal(h.getSetCookieValue(resp, 'key2'), 'value2');
205 | assert.equal(h.getSetCookieValue(resp, 'key3'), null);
206 | assert.equal(h.getSetCookieValue(respNoSetCookie, 'key1'), null);
207 | });
208 | }); // #getSetCookieValue
209 |
210 | describe('#printSafeHTTP', function() {
211 | it('should hide sensitive info', function() {
212 | const raw = [
213 | "Cookie: 'xxxxxx'",
214 | "'X-CSRFToken': 'yyyyyy'",
215 | "'set-cookie': ['zzzzzz']"
216 | ].join('\r\n');
217 |
218 | const hide = [
219 | 'Cookie: ',
220 | "'X-CSRFToken': ",
221 | "'set-cookie': "
222 | ].join('\r\n');
223 |
224 | assert.equal(h.printSafeHTTP(raw), hide);
225 | });
226 | }); // #printSafeHTTP
227 |
228 | describe('#readStdin', function() {
229 | function hijackStdin(data) {
230 | const stream = require('stream');
231 | const rs = new stream.Readable();
232 | rs.push(data);
233 | rs.push(null);
234 |
235 | Object.defineProperty(process, 'stdin', {value: rs});
236 | }
237 |
238 | it('should ok', function(done) {
239 | hijackStdin('[1,2]\n3');
240 |
241 | h.readStdin(function(e, data) {
242 | assert.equal(data, '[1,2]\n3');
243 | done();
244 | });
245 | });
246 |
247 | it('should ok w/ empty input', function(done) {
248 | hijackStdin('');
249 |
250 | h.readStdin(function(e, data) {
251 | assert.equal(data, '');
252 | done();
253 | });
254 | });
255 | }); // #readStdin
256 |
257 | describe('#badge', function() {
258 | it('should ok', function() {
259 | chalk.enabled = true;
260 | assert.equal(h.badge('x'), chalk.white.bgBlue(' x '));
261 | assert.equal(h.badge('x', 'green'), chalk.black.bgGreen(' x '));
262 | });
263 |
264 | it('should ok with random', function() {
265 | const badges = _.values(h.__get__('COLORS'))
266 | .map(function(x) {
267 | return chalk[x.fg][x.bg](' random ');
268 | });
269 |
270 | const i = badges.indexOf(h.badge('random', 'random'));
271 | assert.equal(i >= 0, true);
272 | });
273 | }); // #badge
274 | });
275 |
--------------------------------------------------------------------------------
/test/test_core.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('chai').assert;
3 | const rewire = require('rewire');
4 |
5 | describe('core', function() {
6 | let core;
7 | let next;
8 |
9 | const PROBLEMS = [
10 | {
11 | category: 'algorithms',
12 | id: 0,
13 | fid: 0,
14 | name: 'name0',
15 | slug: 'slug0',
16 | level: 'Hard',
17 | locked: true,
18 | starred: false,
19 | state: 'ac',
20 | tags: ['google', 'facebook']
21 | },
22 | {
23 | category: 'algorithms',
24 | companies: ['amazon', 'facebook'],
25 | id: 1,
26 | fid: 1,
27 | name: 'name1',
28 | slug: 'slug1',
29 | level: 'Easy',
30 | locked: false,
31 | starred: true,
32 | state: 'none'
33 | }
34 | ];
35 |
36 | before(function() {
37 | const log = require('../lib/log');
38 | log.init();
39 | });
40 |
41 | beforeEach(function() {
42 | next = {};
43 | next.getProblems = (needTrans, cb) => cb(null, PROBLEMS);
44 | next.getProblem = (p, needTrans, cb) => cb(null, p);
45 |
46 | core = rewire('../lib/core');
47 | core.setNext(next);
48 | });
49 |
50 | describe('#filterProblems', function() {
51 | it('should filter by query ok', function(done) {
52 | const cases = [
53 | ['', [0, 1]],
54 | ['x', [0, 1]],
55 | ['h', [0]],
56 | ['H', [1]],
57 | ['m', []],
58 | ['M', [0, 1]],
59 | ['l', [0]],
60 | ['L', [1]],
61 | ['s', [1]],
62 | ['S', [0]],
63 | ['d', [0]],
64 | ['D', [1]],
65 | ['eLsD', [1]],
66 | ['Dh', []]
67 | ];
68 | let n = cases.length;
69 |
70 | for (let x of cases) {
71 | core.filterProblems({query: x[0], dontTranslate: false}, function(e, problems) {
72 | assert.notExists(e);
73 | assert.equal(problems.length, x[1].length);
74 |
75 | for (let i = 0; i < problems.length; ++i)
76 | assert.equal(problems[i], PROBLEMS[x[1][i]]);
77 | if (--n === 0) done();
78 | });
79 | }
80 | });
81 |
82 | it('should filter by tag ok', function(done) {
83 | const cases = [
84 | [[], [0, 1]],
85 | [['facebook'], [0, 1]],
86 | [['google'], [0]],
87 | [['amazon'], [1]],
88 | [['apple'], []],
89 | ];
90 | let n = cases.length;
91 |
92 | for (let x of cases) {
93 | core.filterProblems({ tag: x[0], dontTranslate: false}, function(e, problems) {
94 | assert.notExists(e);
95 | assert.equal(problems.length, x[1].length);
96 |
97 | for (let i = 0; i < problems.length; ++i)
98 | assert.equal(problems[i], PROBLEMS[x[1][i]]);
99 | if (--n === 0) done();
100 | });
101 | }
102 | });
103 |
104 | it('should fail if getProblems error', function(done) {
105 | next.getProblems = (needT, cb) => cb('getProblems error');
106 | core.filterProblems({}, function(e) {
107 | assert.equal(e, 'getProblems error');
108 | done();
109 | });
110 | });
111 | }); // #filterProblems
112 |
113 | describe('#starProblem', function() {
114 | it('should ok', function(done) {
115 | next.starProblem = (p, starred, cb) => cb(null, starred);
116 |
117 | assert.equal(PROBLEMS[0].starred, false);
118 | core.starProblem(PROBLEMS[0], true, function(e, starred) {
119 | assert.notExists(e);
120 | assert.equal(starred, true);
121 | done();
122 | });
123 | });
124 |
125 | it('should ok if already starred', function(done) {
126 | assert.equal(PROBLEMS[1].starred, true);
127 | core.starProblem(PROBLEMS[1], true, function(e, starred) {
128 | assert.notExists(e);
129 | assert.equal(starred, true);
130 | done();
131 | });
132 | });
133 |
134 | it('should ok if already unstarred', function(done) {
135 | assert.equal(PROBLEMS[0].starred, false);
136 | core.starProblem(PROBLEMS[0], false, function(e, starred) {
137 | assert.notExists(e);
138 | assert.equal(starred, false);
139 | done();
140 | });
141 | });
142 | }); // #starProblem
143 |
144 | describe('#exportProblem', function() {
145 | let file;
146 |
147 | beforeEach(function() {
148 | file = rewire('../lib/file');
149 | file.init();
150 | core.__set__('file', file);
151 | });
152 |
153 | it('should codeonly ok', function() {
154 | file.isWindows = () => false;
155 |
156 | const expected = [
157 | '/*',
158 | ' * @lc app=leetcode id=2 lang=cpp',
159 | ' *',
160 | ' * [2] Add Two Numbers',
161 | ' */',
162 | '',
163 | '// @lc code=start',
164 | '/**',
165 | ' * Definition for singly-linked list.',
166 | ' * struct ListNode {',
167 | ' * int val;',
168 | ' * ListNode *next;',
169 | ' * ListNode(int x) : val(x), next(NULL) {}',
170 | ' * };',
171 | ' */',
172 | 'class Solution {',
173 | 'public:',
174 | ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {',
175 | ' ',
176 | ' }',
177 | '};',
178 | '// @lc code=end',
179 | ''
180 | ].join('\n');
181 |
182 | const problem = require('./mock/add-two-numbers.20161015.json');
183 | const opts = {
184 | lang: 'cpp',
185 | code: problem.templates[0].defaultCode,
186 | tpl: 'codeonly'
187 | };
188 | assert.equal(core.exportProblem(problem, opts), expected);
189 | });
190 |
191 | it('should codeonly ok in windows', function() {
192 | file.isWindows = () => true;
193 |
194 | const expected = [
195 | '/*',
196 | ' * @lc app=leetcode id=2 lang=cpp',
197 | ' *',
198 | ' * [2] Add Two Numbers',
199 | ' */',
200 | '',
201 | '// @lc code=start',
202 | '/**',
203 | ' * Definition for singly-linked list.',
204 | ' * struct ListNode {',
205 | ' * int val;',
206 | ' * ListNode *next;',
207 | ' * ListNode(int x) : val(x), next(NULL) {}',
208 | ' * };',
209 | ' */',
210 | 'class Solution {',
211 | 'public:',
212 | ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {',
213 | ' ',
214 | ' }',
215 | '};',
216 | '// @lc code=end',
217 | ''
218 | ].join('\r\n');
219 |
220 | const problem = require('./mock/add-two-numbers.20161015.json');
221 | const opts = {
222 | lang: 'cpp',
223 | code: problem.templates[0].defaultCode,
224 | tpl: 'codeonly'
225 | };
226 | assert.equal(core.exportProblem(problem, opts), expected);
227 | });
228 |
229 | it('should detailed ok with cpp', function() {
230 | file.isWindows = () => false;
231 |
232 | const expected = [
233 | '/*',
234 | ' * @lc app=leetcode id=2 lang=cpp',
235 | ' *',
236 | ' * [2] Add Two Numbers',
237 | ' *',
238 | ' * https://leetcode.com/problems/add-two-numbers',
239 | ' *',
240 | ' * algorithms',
241 | ' * Medium (25.37%)',
242 | ' * Likes: 1',
243 | ' * Dislikes: 1',
244 | ' * Total Accepted: 195263',
245 | ' * Total Submissions: 769711',
246 | ' * Testcase Example: \'[2,4,3]\\n[5,6,4]\'',
247 | ' *',
248 | ' * You are given two linked lists representing two non-negative numbers. The',
249 | ' * digits are stored in reverse order and each of their nodes contain a single',
250 | ' * digit. Add the two numbers and return it as a linked list.',
251 | ' * ',
252 | ' * Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)',
253 | ' * Output: 7 -> 0 -> 8',
254 | ' */',
255 | '',
256 | '// @lc code=start',
257 | '/**',
258 | ' * Definition for singly-linked list.',
259 | ' * struct ListNode {',
260 | ' * int val;',
261 | ' * ListNode *next;',
262 | ' * ListNode(int x) : val(x), next(NULL) {}',
263 | ' * };',
264 | ' */',
265 | 'class Solution {',
266 | 'public:',
267 | ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {',
268 | ' ',
269 | ' }',
270 | '};',
271 | '// @lc code=end',
272 | ''
273 | ].join('\n');
274 |
275 | const problem = require('./mock/add-two-numbers.20161015.json');
276 | const opts = {
277 | lang: 'cpp',
278 | code: problem.templates[0].defaultCode,
279 | tpl: 'detailed'
280 | };
281 | assert.equal(core.exportProblem(problem, opts), expected);
282 | });
283 |
284 | it('should detailed ok with ruby', function() {
285 | file.isWindows = () => false;
286 |
287 | const expected = [
288 | '#',
289 | '# @lc app=leetcode id=2 lang=ruby',
290 | '#',
291 | '# [2] Add Two Numbers',
292 | '#',
293 | '# https://leetcode.com/problems/add-two-numbers',
294 | '#',
295 | '# algorithms',
296 | '# Medium (25.37%)',
297 | '# Likes: 1',
298 | '# Dislikes: 1',
299 | '# Total Accepted: 195263',
300 | '# Total Submissions: 769711',
301 | '# Testcase Example: \'\'',
302 | '#',
303 | '# You are given two linked lists representing two non-negative numbers. The',
304 | '# digits are stored in reverse order and each of their nodes contain a single',
305 | '# digit. Add the two numbers and return it as a linked list.',
306 | '# ',
307 | '# Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)',
308 | '# Output: 7 -> 0 -> 8',
309 | '#',
310 | '',
311 | '# @lc code=start',
312 | '# Definition for singly-linked list.',
313 | '# class ListNode',
314 | '# attr_accessor :val, :next',
315 | '# def initialize(val)',
316 | '# @val = val',
317 | '# @next = nil',
318 | '# end',
319 | '# end',
320 | '',
321 | '# @param {ListNode} l1',
322 | '# @param {ListNode} l2',
323 | '# @return {ListNode}',
324 | 'def add_two_numbers(l1, l2)',
325 | ' ',
326 | 'end',
327 | '# @lc code=end',
328 | ''
329 | ].join('\n');
330 |
331 | const problem = require('./mock/add-two-numbers.20161015.json');
332 | problem.testcase = null;
333 | const opts = {
334 | lang: 'ruby',
335 | code: problem.templates[6].defaultCode,
336 | tpl: 'detailed'
337 | };
338 | assert.equal(core.exportProblem(problem, opts), expected);
339 | });
340 | }); // #exportProblem
341 |
342 | describe('#getProblem', function() {
343 | it('should get by id ok', function (done) {
344 | // set needTranslate to false here because it's not used anyways
345 | core.getProblem(0, false, function(e, problem) {
346 | assert.notExists(e);
347 | assert.deepEqual(problem, PROBLEMS[0]);
348 | done();
349 | });
350 | });
351 |
352 | it('should get by key ok', function(done) {
353 | core.getProblem('slug0', false, function(e, problem) {
354 | assert.notExists(e);
355 | assert.deepEqual(problem, PROBLEMS[0]);
356 | done();
357 | });
358 | });
359 |
360 | it('should fail if not found', function(done) {
361 | core.getProblem(3, false, function(e, problem) {
362 | assert.equal(e, 'Problem not found!');
363 | done();
364 | });
365 | });
366 |
367 | it('should fail if client error', function(done) {
368 | next.getProblem = (problem, needT, cb) => cb('client getProblem error');
369 |
370 | core.getProblem(0, false, function(e, problem) {
371 | assert.equal(e, 'client getProblem error');
372 | done();
373 | });
374 | });
375 |
376 | it('should ok if problem is already there', function(done) {
377 | core.getProblem(PROBLEMS[1], false, function(e, problem) {
378 | assert.notExists(e);
379 | assert.deepEqual(problem, PROBLEMS[1]);
380 | done();
381 | });
382 | });
383 |
384 | it('should fail if getProblems error', function(done) {
385 | next.getProblems = (needT, cb) => cb('getProblems error');
386 |
387 | core.getProblem(0, false, function(e, problem) {
388 | assert.equal(e, 'getProblems error');
389 | done();
390 | });
391 | });
392 | }); // #getProblem
393 | });
394 |
--------------------------------------------------------------------------------