diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c34cc8cd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.npm +.nyc_output +.DS_Store + +coverage +dist +node_modules +npm-debug.log* +tmp + +*.log +*.swp diff --git a/.eslintrc.js b/.eslintrc.js index ef5d84c8..5ef04108 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,21 +1,37 @@ module.exports = { "env": { "browser": false, - "node": true, - "mocha": true + "es6": true, + "mocha": true, + "node": true }, - "extends": "google", + "extends": [ + "google", + "eslint:recommended" + ], "rules": { - "key-spacing": [2, { "align": "value" }], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": [2, {properties: "never"}], + "comma-dangle": 0, + "curly": 0, + "key-spacing": [2, {align: "value"}], "max-len": [1, 120], + "no-control-regex": 0, + "no-console": 1, + "no-empty": [2, { "allowEmptyCatch": true }], "no-eval": 1, // we use it on purpose "no-loop-func": 1, - "no-multi-spaces": [2, { exceptions: { "SwitchCase": true }}], + "no-multi-spaces": 0, "no-proto": 1, "no-unused-expressions": 1, "no-unused-vars": 1, + "no-var": 0, "no-warning-comments": 0, + "prefer-rest-params": 0, + "prefer-spread": 0, "quote-props": 1, + "quotes": [2, "single", {avoidEscape: true}], "require-jsdoc": 0, } }; diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..310b2326 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ + + +### Problem Summary + + +### How to reproduce + + +### Environment + + +- leetcode-cli version: + +- OS version: +- Node version: +- Npm version: diff --git a/.gitignore b/.gitignore index 02923dc0..c08d1d39 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,7 @@ jspm_packages # Optional REPL history .node_repl_history +dist/ tmp/ +*.swp .DS_Store diff --git a/.travis.yml b/.travis.yml index e9f9d47a..247f9281 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,26 @@ language: node_js node_js: - - stable + - 10 + +os: + - linux + - osx + - windows + install: - npm install + +script: + - npm test + - if [ -n "$TRAVIS_TAG" ]; then npm run travis; fi + +deploy: + provider: releases + api_key: + secure: "ayYe6HlYFrFposeIh2xX1DbdF3CRFnAHM5VvdtfVh/TtpcEvg4GRCanvzaSvsVajLjFZOZhGVgm+uZ1H6ba6jQuoOUvFJ667EVwQk7c8KDJrvZIMvzMxCgvSHb6N8VBh/5svWYa+7Kbd++3WP7XmkLpWli/DXvOSu6I6M7w+m/OI157mWPp0a7iy+Q+o1vSl/3INNIrd/vMT5F+ae1iBLFn3aHndtezhdQr+HrQCHaVP8OiK96rtjzaiRp+dyoMf4U71LoJGRpGZURv9imyXholoQutlT+bhRaumPqrqiwFRGMaL+xhfBZMySMND8wcO9rQnabiQf5Wo9J5aJMnixWjEIg9gGhJ8E96j9VwdUBA7yfHAbVhLrQ0h2TkZuUdqU1EnOWIbnPtjC9exv8R5X2WRs1fMz9j+XpNYclB4YdLclQ662nfsquccqfksDG1rS249WkSl1RIxr9fcD+60xYXgkG78wrTN8cr9NMGk5/AyMyHcvYjA+rGg1V8DZhzC3WZn9Q0NRJoc3b+xx9pxkaO7epBck5sAsNPO8b/bMGGKmgmR5tKSZUN+lTUKLI2znJcUC1dMKKpRCqr1To94ZYVe0G7SFbe+MH4guQXkd7sB6GnsR8/7g8OsVcAtV4DoEWfHwJQIE0bg/UzqubyBPSGPs1JBZm8nks/zTpOJ65o=" + file: leetcode-cli.* + file_glob: true + skip_cleanup: true + overwrite: true + on: + tags: true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..6ad1e841 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:alpine +LABEL maintainer="skygragon@gmail.com" + +WORKDIR /tmp/leetcode-cli +COPY . . +COPY bin/entrypoint / +RUN npm install && \ + tar zcf /leetcode-cli.tar.gz . && \ + rm -rf /tmp/leetcode-cli + +WORKDIR /root +VOLUME ["/root"] +ENTRYPOINT ["/entrypoint"] diff --git a/README.md b/README.md index b09f789c..258b5df8 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,44 @@ -# leetcode-cli - -A cli tool to enjoy leetcode! - -Great thanks to leetcode.com, a really awesome website! - [![npm version](https://img.shields.io/npm/v/leetcode-cli.svg?style=flat)](https://www.npmjs.com/package/leetcode-cli) [![Releases](https://img.shields.io/github/release/skygragon/leetcode-cli.svg?style=flat)](https://github.com/skygragon/leetcode-cli/releases) [![license](https://img.shields.io/npm/l/leetcode-cli.svg?style=flat)](https://github.com/skygragon/leetcode-cli/blob/master/LICENSE) [![Build](https://img.shields.io/travis/skygragon/leetcode-cli.svg?style=flat)](https://travis-ci.org/skygragon/leetcode-cli) [![Join chat at https://gitter.im/skygragon/leetcode-cli](https://img.shields.io/gitter/room/skygragon/leetcode-cli.svg?style=flat)](https://gitter.im/skygragon/leetcode-cli) -⦙ [Installation Guide](https://skygragon.github.io/leetcode-cli/install) ⦙ -[Documentations](https://skygragon.github.io/leetcode-cli/) ⦙ -[Commands](https://skygragon.github.io/leetcode-cli/commands) ⦙ -[Advanced Tips](https://skygragon.github.io/leetcode-cli/advanced) ⦙ +# leetcode-cli -* A very [**EFFICIENT**](#quick-start) way to fight problems. -* [**CACHING**](https://skygragon.github.io/leetcode-cli/advanced#cache) problems locally thus you can easily navigate & think it offline. -* Do everything in **CLI**, no one even knows you are doing leetcode :p -* [**GENERATING**](https://skygragon.github.io/leetcode-cli/commands#show) source code template for further coding. -* Support live [**TEST**](https://skygragon.github.io/leetcode-cli/commands#test) and [**SUBMIT**](https://skygragon.github.io/leetcode-cli/commands#submit) againts leetcode.com. -* [**AUTO LOGIN**](https://skygragon.github.io/leetcode-cli/advanced#auto-login) among multiple sessions with single leetcode account. -* Retrieve your previous [**SUBMISSION**](https://skygragon.github.io/leetcode-cli/commands#submission) thus you can easily backup and manage your code. + -## Showcases +A productive cli tool to enjoy leetcode! -`help`/`user`/`list`/`show`/`test` +Great thanks to leetcode.com, a really awesome website! + +⦙ [Releases](https://skygragon.github.io/leetcode-cli/releases) ⦙ +[Install](https://skygragon.github.io/leetcode-cli/install) ⦙ +[Docs](https://skygragon.github.io/leetcode-cli/) ⦙ +[Commands](https://skygragon.github.io/leetcode-cli/commands) ⦙ +[Advanced](https://skygragon.github.io/leetcode-cli/advanced) ⦙ +[Plugins](https://github.com/skygragon/leetcode-cli-plugins) ⦙ -![screenshot1](https://github.com/skygragon/leetcode-cli/raw/master/docs/screenshots/intro.gif) +* A very [**EFFICIENT**](#quick-start) way to fight questions. +* [**CACHING**](https://skygragon.github.io/leetcode-cli/advanced#cache) questions to ease offline thinking. +* [**GENERATING**](https://skygragon.github.io/leetcode-cli/commands#show) source code before coding. +* Live [**TEST**](https://skygragon.github.io/leetcode-cli/commands#test) and [**SUBMIT**](https://skygragon.github.io/leetcode-cli/commands#submit) with leetcode.com. +* Download your previous [**SUBMISSION**](https://skygragon.github.io/leetcode-cli/commands#submission). +* Trace your coding [**STATUS**](https://skygragon.github.io/leetcode-cli/commands#stat). +* [**AUTO LOGIN**](https://skygragon.github.io/leetcode-cli/advanced#auto-login) among multiple agents with single account. +* Multiple [**THEMES**](https://skygragon.github.io/leetcode-cli/advanced#color-themes) support. +* More [**PLUGINS**](https://skygragon.github.io/leetcode-cli/advanced#plugins) to enjoy extra features! -`test`/`submit`/`stat`/`submission` +## Screenshot -![screenshot2](https://github.com/skygragon/leetcode-cli/raw/master/docs/screenshots/intro2.gif) + ## Quick Start Read help first $ leetcode help Login with your leetcode account $ leetcode user -l - Browse all problems $ leetcode list - Choose one problem $ leetcode show 1 -g -l cpp + Browse all questions $ leetcode list + Choose one question $ leetcode show 1 -g -l cpp Coding it! Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7' Submit final solution! $ leetcode submit ./two-sum.cpp diff --git a/bin/entrypoint b/bin/entrypoint new file mode 100755 index 00000000..c2ebd337 --- /dev/null +++ b/bin/entrypoint @@ -0,0 +1,13 @@ +#!/bin/sh + +srcdir=/root/leetcode-cli +leetcode=$srcdir/bin/leetcode + +if [ ! -f "$leetcode" ]; then + echo "Unpacking leetcode-cli code ..." + mkdir -p $srcdir + tar zxf /leetcode-cli.tar.gz -C $srcdir +fi + +export TERM=xterm-256color +exec $leetcode $@ diff --git a/bin/install b/bin/install new file mode 100755 index 00000000..6f39955a --- /dev/null +++ b/bin/install @@ -0,0 +1,36 @@ +#!/bin/bash + +ENVFILE=.env.json + +check() { + printf "Checking $1 ... " + $1 --version > /dev/null 2>&1 + if [ $? != 0 ]; then + echo "No" + echo "[ERROR] Missing $1!" + exit 1; + fi + echo "Yes" +} + +create() { + cat << EOF > $1 +{ + "commit": { + "full": "`git rev-parse HEAD`", + "short": "`git rev-parse --short HEAD`" + }, + "node": "`node -v`", + "npm": "`npm -v`" +} +EOF +} + +check git +check node +check npm + +create $ENVFILE + +npm install -g . +echo "leetcode-cli successfully installed." diff --git a/bin/pkg b/bin/pkg new file mode 100755 index 00000000..0cc22db1 --- /dev/null +++ b/bin/pkg @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const arch = require('os').arch(); +var os = process.platform; +const ver = process.versions.node.split('.')[0]; + +var bin = './bin/pkg.sh'; +var args = [arch, os, ver]; + +if (os === 'darwin') { + args[1] = 'macos'; +} else if (os === 'win32') { + bin = 'cmd.exe'; + args = ['/c', 'bin\\pkg.bat'].concat(args); +} + +var proc = require('child_process').spawn(bin, args); +proc.stdout.on('data', x => console.log(x.toString().trimRight('\n'))); +proc.stderr.on('data', x => console.log(x.toString().trimRight('\n'))); +proc.on('close', process.exit); \ No newline at end of file diff --git a/bin/pkg.bat b/bin/pkg.bat new file mode 100644 index 00000000..0294b7f5 --- /dev/null +++ b/bin/pkg.bat @@ -0,0 +1,25 @@ +@echo off +set arch=%1 +set os=%2 +set ver=%3 + +set dist=dist\ +set file=leetcode-cli.node%ver%.%os%.%arch%.zip + +mkdir %dist% +del /q %dist%\* +del /q *.zip + +for %%x in (company cookie.chrome cookie.firefox cpp.lint cpp.run github leetcode.cn lintcode solution.discuss) do ( + echo [%%x] + node bin\leetcode ext -i %%x + if %ERRORLEVEL% gtr 0 exit /b 1 +) + +for /r . %%x in (*.node) do copy %%x %dist% +call npm run pkg -- node%ver%-%os%-%arch% +if %ERRORLEVEL% gtr 0 exit /b 1 + +7z a %file% %dist% +if %ERRORLEVEL% gtr 0 exit /b 1 +exit 0 \ No newline at end of file diff --git a/bin/pkg.sh b/bin/pkg.sh new file mode 100755 index 00000000..abeb778d --- /dev/null +++ b/bin/pkg.sh @@ -0,0 +1,26 @@ +#!/bin/bash -e + +arch=$1 +os=$2 +ver=$3 + +DIST=./dist +FILE=leetcode-cli.node$ver.$os.$arch.tar.gz + +mkdir -p $DIST +rm -rf $DIST/* +rm -rf $FILE + +plugins="company cookie.chrome cookie.firefox cpp.lint cpp.run github leetcode.cn lintcode solution.discuss" + +for plugin in $plugins; do + echo "[$plugin]" + ./bin/leetcode ext -i $plugin +done + +find node_modules -name "*.node" -exec cp {} $DIST \; +npm run pkg -- node$ver-$os-$arch + +tar zcvf $FILE $DIST +ls -al $FILE +exit 0 \ No newline at end of file diff --git a/colors/blue.json b/colors/blue.json new file mode 100644 index 00000000..7577d79d --- /dev/null +++ b/colors/blue.json @@ -0,0 +1,10 @@ +{ + "blue": "#0000ff", + "cyan": "#b0c4de", + "gray": "#483d8b", + "green": "#00bfff", + "magenta": "#6a5acd", + "red": "#ae81ff", + "white": "#f0f8ff", + "yellow": "#87cefa" +} diff --git a/colors/dark.json b/colors/dark.json index baafa01e..86e60471 100644 --- a/colors/dark.json +++ b/colors/dark.json @@ -1,10 +1,9 @@ { - "black": "#000000", "blue": "#000099", "cyan": "#009999", + "gray": "#455354", "green": "#009900", "magenta": "#990099", "red": "#990000", - "white": "#ffffff", "yellow": "#999900" } diff --git a/colors/default.json b/colors/default.json index 0b8e1061..d17a20a9 100644 --- a/colors/default.json +++ b/colors/default.json @@ -2,6 +2,7 @@ "black": "#000000", "blue": "#0000ff", "cyan": "#00ffff", + "gray": "#999999", "green": "#00ff00", "magenta": "#ff00ff", "red": "#ff0000", diff --git a/colors/molokai.json b/colors/molokai.json new file mode 100644 index 00000000..1e75d1ea --- /dev/null +++ b/colors/molokai.json @@ -0,0 +1,10 @@ +{ + "blue": "#66D9EF", + "cyan": "#AE81FF", + "gray": "#75715E", + "green": "#87FF00", + "magenta": "#FF46FF", + "red": "#D7005F", + "white": "#F8F8F2", + "yellow": "#FD971F" +} diff --git a/colors/orange.json b/colors/orange.json new file mode 100644 index 00000000..2fcc4378 --- /dev/null +++ b/colors/orange.json @@ -0,0 +1,10 @@ +{ + "blue": "#808000", + "cyan": "#b8860b", + "gray": "#deb887", + "green": "#ffa500", + "magenta": "#d2691e", + "red": "#ff4500", + "white": "#fdf5eb", + "yellow": "#ffd700" +} diff --git a/colors/pink.json b/colors/pink.json index f6df28ae..762a8a90 100644 --- a/colors/pink.json +++ b/colors/pink.json @@ -1,10 +1,10 @@ { - "black": "#000000", - "blue": "#0000ff", - "cyan": "#00ffff", - "green": "#ff1493", - "magenta": "#ff00ff", - "red": "#dc143c", - "white": "#ffffff", - "yellow": "#ff4500" + "blue": "#8a2be2", + "cyan": "#800080", + "gray": "#d8bfd8", + "green": "#ff00ff", + "magenta": "#db7093", + "red": "#ff1493", + "white": "#fff0f5", + "yellow": "#ffc0cb" } diff --git a/colors/solarized.json b/colors/solarized.json new file mode 100644 index 00000000..62069b2b --- /dev/null +++ b/colors/solarized.json @@ -0,0 +1,10 @@ +{ + "black": "#073642", + "blue": "#268bd2", + "cyan": "#2aa198", + "green": "#859900", + "magenta": "#d33682", + "red": "#dc322f", + "white": "#eee8d5", + "yellow": "#b58900" +} diff --git a/colors/solarized.light.json b/colors/solarized.light.json new file mode 100644 index 00000000..e22c61d3 --- /dev/null +++ b/colors/solarized.light.json @@ -0,0 +1,10 @@ +{ + "black": "#262626", + "blue": "#0087ff", + "cyan": "#00afaf", + "green": "#5f8700", + "magenta": "#af005f", + "red": "#d70000", + "white": "#d7d7af", + "yellow": "#af8700" +} diff --git a/docs/advanced.md b/docs/advanced.md index 5af7c6ff..42904e91 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -3,20 +3,46 @@ layout: default title: Advanced Topic --- +* [Aliases](#aliases) * [Auto Login](#auto-login) * [Bash Completion](#bash-completion) * [Cache](#cache) * [Configuration](#configuration) -* [Color Theme](#color-theme) -* [Log Level](#log-level) - -# Auto login +* [Color Themes](#color-themes) +* [File Name](#file-name) +* [Log Levels](#log-levels) +* [Plugins](#plugins) + +# Aliases + +The commands in leetcode-cli usually has builtin aliases as below: + +|Command |Aliases | +|----------|-----------------------| +|config |conf, cfg, setting | +|list |ls | +|plugin |extension, ext | +|session |branch | +|show |view, pick | +|star |like, favorite | +|stat |stats, progress, report| +|submission|pull | +|submit |push, commit | +|test |run | +|user |account | +|version |info, env | + +# Auto Login 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. 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: - "AUTO_LOGIN": true + { + "autologin": { + "enable": true + } + } **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!** @@ -24,12 +50,12 @@ The good news is leetcode-cli will help a lot on this by trying re-login transpa Copy `.lc-completion.bash` to your home directory, and source it in .bashrc (Linux) or .bash_profile (MacOS). - $ cp .lc-completion.bash ~ - $ echo "source ~/.lc-completion.bash" >> ~/.bashrc - $ source ~/.bashrc + $ cp .lc-completion.bash ~ + $ echo "source ~/.lc-completion.bash" >> ~/.bashrc + $ source ~/.bashrc - $ leetcode list -- - --help --keyword --query --stat + $ leetcode list -- + --help --keyword --query --stat **NOTE: it might become slower in bash with this enabled, personally I would NOT suggest to use it...** @@ -37,54 +63,105 @@ Copy `.lc-completion.bash` to your home directory, and source it in .bashrc (Lin The local cache folder (`.lc/`) is in your home directory, e.g. - $ ls -a1 ~/.lc/ + $ ls -a1 ~/.lc/ + cache # folder of cached questions + config.json # user customized config + user.json # user account info - .user.json # your account info - all.json # problems list - two-sum.json # specific problem info + $ ls -a1 ~/.lc/cache/ + problems.json # cached questions list + 1.two-sum.algorithms.json # cached specific question + +**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.** # Configuration -Create a JSON file named `.lcconfig` in your home directory, e.g. +The config file is saved in `~/.lc/config.json`, here is a full exmaple (includes default configs): - $ cat ~/.lcconfig + $ cat ~/.lc/config.json - { - "LANG": "java", - "USE_COLOR": true, - "COLOR_THEME": "default", - "AUTO_LOGIN": false - } + { + "auto_login": { + "enable": false + }, + "code": { + "editor": "vim", + "lang": "cpp" + }, + "color": { + "enable": true, + "theme": "default" + }, + "file": { + "show": "${fid}.${slug}", + "submission": "${fid}.${slug}.${sid}.${ac}" + }, + "icon": { + "theme": "" + }, + "network": { + "concurrency": 10 + }, + "plugins": {} + } Here are some useful settings: -* `AUTO_LOGIN` to enable auto login feature, see [Auto Login](#auto-login). -* `COLOR_THEME` to set color theme used in output, see [Color Theme](#color-theme). -* `LANG` to set your default language used in coding. -* `USE_COLOR` to enable colorful output. +* `autologin:enable` to enable auto login feature. (see [Auto Login](#auto-login)) +* `code:editor` to set editor used to open generated source file. +* `code:lang` to set your default language used in coding. +* `color:enable` to enable colorful output. +* `color:theme` to set color theme used in output. (see [Color Theme](#color-theme)) +* `file.show` to set filename pattern for generated code file. (see [File Name](#file-name)) +* `icon:theme` to set icon them used in output. +* `plugins` to config each installed plugins. (see [Plugins](#plugins)) + +**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.** + +*Example* + +Config for `github.js` and `cpp.lint.js` plugins: + + { + "plugins": { + "github": { + "repo": "https://github.com/skygragon/test", + "token": "abcdefghijklmnopqrstuvwxyz" + }, + "cpp.lint": { + "bin": "~/bin/cpplibt.py", + "flags": [] + } + } + } -# Color Theme +# Color Themes You can choose to use colorful output or not. * `--color` to enable color. * `--no-color` to disable it. -Or use configuration setting to avoid typing it repeatedly, see [USE_COLOR](#configuration). +Or use configuration setting to avoid typing it repeatedly. (see [color:enable](#configuration)) -When color is enabled, you can choose your favor color theme as well, see [COLOR_THEME](#configuration). +When color is enabled, you can choose your favor color theme as well. (see [color:theme](#configuration)) Following are available themes: -* `default` +* `blue` * `dark` for night. +* `default` +* `molokai` +* `orange` * `pink` for girls. +* `solarized` +* `solarized.light` -Of course you can create your own themes if you like, please see `colors` folder in the source code. +Of course you can create your own themes if you like, look into `colors` folder in the source code for more tips. *Example* - $ cat colors/default.json + $ cat colors/default.json { "black": "#000000", "blue": "#0000ff", @@ -96,7 +173,33 @@ Of course you can create your own themes if you like, please see `colors` folder "yellow": "#ffff00" } -# Log Level +# File Name + +You could configure file name pattern in code generation. + +* config `file.show` for generated file in `show`. +* config `file.submission` for downloaded file in `submission`. + +Followings are some variables you could used in the pattern: + +* `${fid}` for question id. (e.g. `123`) +* `${slug}` for dash-separated question name. (e.g. `add-two`) +* `${name}` for space-separated questions name. (e.g. `Add Two`) +* `${level}` for question level. (e.g. `Hard`) +* `${category}` for question category. (e.g. `algorithms`) +* `${sid}` for submission id. +* `${ac}` for accept status of existing submission. + +# Log Levels * `-v` to enable debug output. -* `-vv` to enable trace output. \ No newline at end of file +* `-vv` to enable trace output. + * Will print detailed HTTP requests/responses. + +# Plugins + +You can easily introduce more features by installing other plugins form third parties. Here lists the avaible 3rd party plugins at the moment: + +* [leetcode-cli-plugins](https://github.com/skygragon/leetcode-cli-plugins) + +Feel free to try out the plugins above. Or you can develope your own plugins to enrich leetcode-cli's functionalities. diff --git a/docs/commands.md b/docs/commands.md index e047f6ee..1893dbc4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -4,10 +4,14 @@ title: Commands Help --- * [help](#help) +* [cache](#cache) +* [config](#config) * [list](#list) +* [plugin](#plugin) * [show](#show) * [star](#star) * [stat](#stat) +* [session](#session) * [submission](#submission) * [submit](#submit) * [test](#test) @@ -16,114 +20,247 @@ title: Commands Help # help -Display help message. +Display help message. It also shows examples of the commands. * `leetcode help ` to see help on sub command. -* `leetcode --help` also works. +* `leetcode -h` also works. *Examples* - $ leetcode help - - list [keyword] list problems - show show problem by name or index - star Star problem by name or index - stat show statistics - submission [keyword] retrieve earlier submission by name or index - submit submit final solution to leetcode - test send solution to leetcode and run test - user login/logout with leetcode account - version show version info + $ leetcode -h + leetcode [command] + + Commands: + leetcode cache [keyword] Manage local cache + leetcode config [key] [value] Manage user configs [aliases: conf, cfg, setting] + leetcode list [keyword] List questions [aliases: ls] + leetcode plugin [name] Manage plugins [aliases: extension, ext] + leetcode session [keyword] Manage sessions [aliases: branch] + leetcode show [keyword] Show question [aliases: view, pick] + leetcode star Star favorite question [aliases: like, favorite] + leetcode stat Show statistics [aliases: stats, progress, report] + leetcode submission [keyword] Download submission code [aliases: pull] + leetcode submit Submit code [aliases: push, commit] + leetcode test Test code [aliases: run] + leetcode user Manage account [aliases: account] + leetcode version Show version info [aliases: info, env] + leetcode completion generate bash completion script + + Options: + -h, --help Show help [boolean] + + Seek more help at https://skygragon.github.io/leetcode-cli/commands Show help on sub command: - $ leetcode help list + $ leetcode cache -h + leetcode cache [keyword] + + Manage local cache + + Positionals: + keyword Cache name or question id [string] [default: ""] + + Options: + -h, --help Show help [boolean] + -d, --delete Delete cache by keyword [boolean] [default: false] + + Examples: + leetcode cache Show all cache + leetcode cache 1 Show cache of question 1 + + leetcode cache -d Delete all cache + leetcode cache 1 -d Delete cache of question 1 + +# cache + +Show local cached questions. + +* `leetcode cache ` to show specific question cache by id. +* `-d ` to delete specific question cache by id. +* `-d` to delete all cached questions. + +*Examples* + +Show cache: + + $ leetcode cache + problems 190.71K 7 hours ago + 1.two-sum.algorithms 2.82K 13 hours ago + 733.flood-fill.algorithms 4.52K 7 hours ago + 746.min-cost-climbing-stairs.algorithms 2.96K 8 hours ago + ...... - leetcode list [keyword] +Delete cache of question 733: - Options: - --help Show help [boolean] - --keyword Filter problems by keyword [string] - --query, -q Filter problems by conditions: - e(easy),m(medium),h(hard),d(done),l(locked) - Uppercase means negative, e.g. D(not done) [string] - --stat, -s Show problems statistics [boolean] + $ leetcode cache -d 733 + +# config + +Manage user config. + +* `leetcode config` to show all user customized configs. +* `-a` to show all user configs (includes default ones). +* `leetcode config ` to show config by key. +* `leetcode config ` to set config by key. +* `-d` to delete config by key. + +*Examples* + +Set config: + + $ leetcode config color:enable false + +**NOTE: the key is using colon ":" as the separator, not dot "."** + +Show config by key: + + $ leetcode config color + { + "enable": true + } # list -Navigate the problems. +Navigate the quations. * Symbols - * `✔` means you have AC-ed the problem. - * `✘` means not AC-ed. - * `★` means starred problem. - * `🔒` means locked problem. + * `✔` means you have AC-ed this question. + * `✘` means not AC-ed. + * `★` means starred question. + * `🔒` means locked question. * `-q` to query by conditions. - * `e` = easy, `E` = not easy = m + h. - * `m` = medium, `M` = not medium = e + h. - * `h` = hard, `H` = not hard = e + m. - * `d` = done = AC-ed, `D` = not AC-ed. - * `l` = locked, `L` = not locked. - * `s` = starred, `S` = unstarred. -* `-s` to show statistic counters. + * `e` = easy, `E` = not easy = m + h. + * `m` = medium, `M` = not medium = e + h. + * `h` = hard, `H` = not hard = e + m. + * `d` = done = AC-ed, `D` = not AC-ed. + * `l` = locked, `L` = not locked. + * `s` = starred, `S` = unstarred. +* `-t` to filter by given tag. + * by category + * `algorithms` + * `database` + * `shell` + * by company (require plugin) + * by topic (require plugin) +* `-s` to show statistic counters of the output list. * `leetcode list ` to search by keyword matching. *Examples* Show statistcis: - $ leetcode list -s - [385] Mini Parser Medium (26.5%) - ✘ [384] Shuffle an Array Medium (45.7%) - ✔ [383] Ransom Note Easy (44.5%) - ✔ [382] Linked List Random Node Medium (46.6%) - ...... - ✔ [ 4] Median of Two Sorted Arrays Hard (19.6%) - ✔ [ 3] Longest Substring Without Repeating Characters Medium (22.9%) - ★ ✔ [ 2] Add Two Numbers Medium (25.37 %) - ★ ✔ [ 1] Two Sum Easy (27.61 %) - - All: 400 Listed: 400 - Locked: 73 Starred: 3 - Accept: 196 Not-AC: 15 New: 189 - Easy: 106 Medium: 207 Hard: 87 + $ leetcode list -s + [385] Mini Parser Medium (26.5%) + ✘ [384] Shuffle an Array Medium (45.7%) + ✔ [383] Ransom Note Easy (44.5%) + ✔ [382] Linked List Random Node Medium (46.6%) + ...... + ✔ [ 4] Median of Two Sorted Arrays Hard (19.6%) + ✔ [ 3] Longest Substring Without Repeating Characters Medium (22.9%) + ★ ✔ [ 2] Add Two Numbers Medium (25.37 %) + ★ ✔ [ 1] Two Sum Easy (27.61 %) + + Listed: 400 Locked: 73 Starred: 3 + Accept: 196 Not-AC: 15 Remain: 189 + Easy: 106 Medium: 207 Hard: 87 Use keyword search and query: - $ leetcode list -q Dml array + $ leetcode list -q Dml array + + 🔒 [360] Sort Transformed Array Medium (41.0%) + 🔒 [325] Maximum Size Subarray Sum Equals k Medium (40.9%) + +# plugin + +Manage plugins. To install 3rd party plugins, please check the [Advanced Tips](https://skygragon.github.io/leetcode-cli/advanced#plugins). + +* `-i` to install new plugin. +* `-d` to disable plugin. +* `-e` to enable plugin. +* `-D` to physically delete plugin. +* `-c` to show plugin's config. + +*Example* + +Install plugin from github: + + $ leetcode plugin -i company + +(Deprecated) Install plugin from local file: + + $ leetcode plugin -i /company.js + +List all the plugins, `✘` means the plugin is disabled. + + $ leetcode plugin + ✔ retry default Plugin to retry last failed request if autologin is on. + ✔ cache default Plugin to provide local cache. + ✔ leetcode default Plugin to talk with leetcode APIs. + +# session + +Manage coding sessions, each session has individual status traced. + +* `-c` to create new session. +* `-e` to enable/activate specific seesion. +* `-d` to delete session. + +*Examples* + +Show all sessions: + + $ leetcode session + Active Id Name AC Questions AC Submits + -------------------------------------------------------------------------------- + ✔ 77299 Anonymous Session 393 ( 98.50 %) 896 ( 47.13 %) + 1111667 Untitled Session 0 ( 0.00 %) 0 ( 0.00 %) - 🔒 [360] Sort Transformed Array Medium (41.0%) - 🔒 [325] Maximum Size Subarray Sum Equals k Medium (40.9%) # show -Display problem details. With `-g`/`-l`/`-x`, the code template could be auto generated for you. +Display question details. With `-g`/`-l`/`-x`, the code template would be auto generated for you. * `-g` to generate source file. -* `-x` to add problem description in the generated source file. +* `-x` to add question description in the generated source file. +* `-e` to open editor with generated source file. +* `-o` to specify the output folder. * `-l` to choose programming language. (Depends on which langs are provided on leetcode) - * c - * cpp - * csharp - * golang - * java - * javascript - * python - * ruby - * swift -* Instead of index number, you can use name to select a problem. - * `leetcode show 1` - * `leetcode show "Two Sum"` - * `leetcode show two-sum` + * bash + * c + * cpp + * csharp + * golang + * java + * javascript + * kotlin + * mysql + * php + * python + * python3 + * ruby + * scala + * swift +* `-c` to only show code template. +* Instead of id, you can use name to select specific question. + * `leetcode show 1` + * `leetcode show "Two Sum"` + * `leetcode show two-sum` +* If no id/name provided, a random question will be selected for you. + * `leetcode show` + * `-q` to filter questions by query. (same as `list` command) + * `-t` to filter questions by tags. (same as `list` command) *Examples* - $ leetcode show 1 -g -l cpp + $ leetcode show 1 -g -l cpp - [1] Two Sum (File: two-sum.cpp) + [1] Two Sum (File: two-sum.cpp) https://leetcode.com/problems/two-sum/ + * algorithms * Easy (25.6%) * Total Accepted: 274880 * Total Submissions: 1074257 @@ -143,88 +280,124 @@ Display problem details. With `-g`/`-l`/`-x`, the code template could be auto ge UPDATE (2016/2/13): The return format had been changed to zero-based indices. Please read the above updated description carefully. +Only show the code template: + + $ leetcode show -c 1 + class Solution { + public: + vector twoSum(vector& nums, int target) { + + } + }; + +Random select question: easy + unlocked + + $ leetcode show -q eL + # star -Mark your favorite problems. The starred problem will be shown with a `★`. +Mark your favorite questions. The starred question will be shown with a `★`. * `-d` to unstar. -* Instead of index number, you can use name to star a problem. - * `leetcode star "Two Sum"` - * `leetcode star two-sum` +* Instead of id, you can use name to star a question. + * `leetcode star "Two Sum"` + * `leetcode star two-sum` *Example* - $ leetcode star 1 - [1] Two Sum ★ + $ leetcode star 1 + [1] Two Sum ★ - $ leetcode star 1 -d - [1] Two Sum ☆ + $ leetcode star 1 -d + [1] Two Sum ☆ # stat -Show your personal statistics of the problems progress. +Show your personal statistics of the question progress. -* `-g` to show the heatmap graph. +* `-g` to show the heatmap graph of all the questions. +* `-c` to show how many AC-ed questions per day by calendar. +* `-q` to filter questions by query. (same as `list` command) +* `-t` to filter questions by tags. (same as `list` command) +* `--no-lock` to exclude lokced questions. *Example* Show AC-ed progress: - $ leetcode stat - Easy 116/136 (85.29%) ██████████████████████████░░░░ - Medium 195/280 (69.64%) █████████████████████░░░░░░░░░ - Hard 50/103 (48.54%) ███████████████░░░░░░░░░░░░░░░ + $ leetcode stat - Without Locked: - Easy 116/121 (95.87%) █████████████████████████████░ - Medium 195/220 (88.64%) ███████████████████████████░░░ - Hard 50/83 (60.24%) ███████████████████░░░░░░░░░░░ + Easy 141/205 ( 68.78 %) █████████████████████░░░░░░░░░ + Medium 200/365 ( 54.79 %) █████████████████░░░░░░░░░░░░░ + Hard 52/148 ( 35.14 %) ███████████░░░░░░░░░░░░░░░░░░░ Show heatmap graph: - $ leetcode stat -g - 1 10 11 20 21 30 31 40 41 50 - 001 ██████████ ██████████ █████████░ ██████░███ ██████████ - 050 ██████████ ██████████ ██████████ ██████████ ██████████ - 100 ██████████ ██████████ █████░████ ██████████ ██████████ - 150 █████░░░░█ ░█░██████░ ████ █ ░████ █ ███ - 200 ██████████ █░█████░█X ███░██████ ██████████ ██░░░░░░░░ - 250 ░░░░░░██░█ ░ ██░░░█░░ ░░░██░░██░ ░░██░░█░░█ ░█░░█░█░██ - 300 █░██░██░█░ ░██░██░██░ ░█░X░█░███ ██░██X██░░ █████░█░██ - 350 ░█░░░░█░░░ ░░░░█░██░░ ████░░█░░█ █████████X ░█████████ - 400 ██████░░█░ ░██████░░░ ░░█░░ ░ ██░██░░ ██ ░█░██░░ - 450 ███░██ █░ ███░░░█░░ ░░█████ ░ ██░░██░░ ░ ░█░███ █░█ - 500 █░██░███ █░██X █ ░░██X█░ ██ ░█░ █░███ ███░░░░░░ - - █ Accepted X Not Accepted ░ Remaining + $ leetcode stat -g + + 1 10 11 20 21 30 31 40 41 50 + 000 ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ + 050 ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ + 100 ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ + 150 ▣ ▣ ▣ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ▣ ▣ ▣ ▣ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ▣ ▣ ▣ + 200 ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▤ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + 250 ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ▣ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ▣ ⬚ ▣ ⬚ ⬚ ▣ ⬚ ▣ ⬚ ▣ ▣ + 300 ▣ ⬚ ▣ ▣ ⬚ ▣ ▣ ⬚ ▣ ⬚ ⬚ ▣ ▣ ⬚ ▣ ▣ ⬚ ▣ ▣ ⬚ ⬚ ▣ ⬚ ▤ ⬚ ▣ ⬚ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▤ ▣ ▣ ⬚ ⬚ ▣ ▣ ▣ ▣ ▣ ⬚ ▣ ⬚ ▣ ▣ + 350 ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ▣ ▣ ⬚ ⬚ ▣ ▣ ▣ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▤ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ ▣ + 400 ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ▣ ▣ ▣ ▣ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ▣ ▣ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ▣ ⬚ ▣ ▣ ⬚ ⬚ + 450 ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ▣ ▣ ▣ ▣ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ▣ ▣ ▣ ▣ ⬚ ▣ + 500 ▣ ⬚ ▣ ▣ ⬚ ▣ ▣ ▣ ▣ ⬚ ▣ ▣ ▤ ▣ ⬚ ⬚ ▣ ▣ ▤ ▣ ⬚ ▣ ▣ ⬚ ▣ ⬚ ▣ ⬚ ▣ ▣ ▣ ⬚ ▣ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + 550 ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ▣ ⬚ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ▣ + 600 ⬚ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ + 650 ⬚ ▣ ▣ ▣ ⬚ ⬚ ▣ ▣ ⬚ ▣ ⬚ ▣ ⬚ ⬚ ▣ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + 700 ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ ⬚ ▣ ▣ ⬚ ⬚ ⬚ + 750 ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + + ▣ Accepted ▤ Not Accepted ⬚ Remaining + +Show calendar graph: + + $ leetcode stat -c + + Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan + Sun ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + Mon ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + Tue ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + Wed ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + Thu ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + Fri ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ + Sat ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ⬚ ▣ + + ▣ 1~5 ▣ 6~10 ▣ 11~15 ▣ 16+ # submission -Retrieve your old submissions from leetcode.com and save to local files. +Download your former submissions. -* For AC-ed problem, the last accepted submission will be retrieved, which output in green color. -* For non AC-ed problem, the last non-accepted submission will be retrieved, which output in yellow. -* If the submission file already exists in local, it will skip retrieving and output in white. +* For AC-ed question, the last accepted submission will be downloaded, which output in green color. +* For non AC-ed question, the last non-accepted submission will be downloaded, which output in yellow. +* If the submission file already exists in local, it will skip downloading and output in white. Available options: * `-o` to specify the output folder. -* `-a` to work against all problems. -* `-x` to add problem details in the output file. -* Or work against specfic problem only. - * `leetcode submission 1` - * `leetcode submission two-sum` +* `-a` to work against all questions. +* `-l` to filter by specific programming language. +* `-x` to add question details in the output file. +* Or work against specfic question only. + * `leetcode submission 1` + * `leetcode submission two-sum` *Examples* - $ leetcode submission -a -o tmp + $ leetcode submission -a -o tmp - [303] Range Sum Query - Immutable tmp/range-sum-query-immutable.52178990.ac.cpp - [319] Bulb Switcher tmp/bulb-switcher.52257927.ac.cpp - [313] Super Ugly Number tmp/super-ugly-number.52256965.ac.cpp - ...... - [ 1] Two Sum tmp/two-sum.73790064.ac.cpp + [303] Range Sum Query - Immutable tmp/range-sum-query-immutable.52178990.ac.cpp + [319] Bulb Switcher tmp/bulb-switcher.52257927.ac.cpp + [313] Super Ugly Number tmp/super-ugly-number.52256965.ac.cpp + ...... + [ 1] Two Sum tmp/two-sum.73790064.ac.cpp # submit @@ -232,35 +405,39 @@ Submit code to leetcode.com. *Examples* - $ leetcode submit ./two-sum.cpp + $ leetcode submit ./two-sum.cpp - ✔ Accepted - ✔ 16/16 cases passed (12 ms) + ✔ Accepted + ✔ 16/16 cases passed (12 ms) + ✔ Your runtime beats 49.89 % of cpp submissions # test -Customize your testcase and run it against leetcode. If no testcase provided, a default testcase will be used. +Test code on leetcode.com. If no testcase provided, a default testcase will be used. * `-t` to provide test case in command line. + * NOTE: use single quote `'` to surround your test case. (double quote is NOT safe in bash shell due to escaping) * `-i` to provide test case in interactive mode. + * on Linux/MacOS, press `Ctrl-D` to finish input. + * on Windows, press `Ctrl-D` and `Return` to finish input. *Examples* - $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7' + $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7' - Input data: - [3,2,4] - 7 + Input data: + [3,2,4] + 7 - Your - ✔ runtime: 0 ms - ✘ answer: [1,2] - ✔ output: + Actual + ✔ runtime: 0 ms + ✘ answer: [1,2] + ✔ output: - Expected - ✔ runtime: 0 ms - ✔ answer: [0,2] - ✔ output: + Expected + ✔ runtime: 0 ms + ✔ answer: [0,2] + ✔ output: # user @@ -274,50 +451,56 @@ Login with your leetcode account (username or email). Login: - $ leetcode user -l - login: - pass: - Successfully login as + $ leetcode user -l + login: + pass: + Successfully login as # version Display version information. -* `-v` to show verbose info, e.g. config, cache dir. +* `-v` to show verbose info. *Examples* Short: - $ leetcode version - 0.9.0 + $ leetcode version + 2.6.2 Verbose: - $ leetcode version -v - _ _ _ - | | | | | | - | | ___ ___| |_ ___ ___ __| | ___ - | |/ _ \/ _ \ __|/ __|/ _ \ / _` |/ _ \ - | | __/ __/ |_ (__| (_) | (_| | __/ - |_|\___|\___|\__|\___|\___/ \__,_|\___| CLI v0.9.0 - - [Environment] - Cache: /Users/skygragon/.lc/ - Config: /Users/skygragon/.lcconfig - - [Configuration] - AUTO_LOGIN: true - COLOR_THEME: default - LANG: java - MAX_WORKERS: 10 - URL_BASE: https://leetcode.com - URL_LOGIN: https://leetcode.com/accounts/login/ - URL_PROBLEM: https://leetcode.com/problems/$id - URL_PROBLEMS: https://leetcode.com/api/problems/algorithms/ - URL_SUBMISSION: https://leetcode.com/submissions/detail/$id/ - URL_SUBMISSIONS: https://leetcode.com/problems/$key/submissions/ - URL_SUBMIT: https://leetcode.com/problems/$key/submit/ - URL_TEST: https://leetcode.com/problems/$key/interpret_solution/ - URL_VERIFY: https://leetcode.com/submissions/detail/$id/check/ - USE_COLOR: true + $ leetcode version -v + _ _ _ + | | | | | | + | | ___ ___| |_ ___ ___ __| | ___ + | |/ _ \/ _ \ __|/ __|/ _ \ / _` |/ _ \ + | | __/ __/ |_ (__| (_) | (_| | __/ + |_|\___|\___|\__|\___|\___/ \__,_|\___| CLI v2.6.2 + + [Environment] + Node v8.1.4 + OS darwin 15.6.0 + Cache /Users/skygragon/.lc/cache + Config /Users/skygragon/.lc/config.json + + [Configuration] + autologin {"enable":true} + code {"editor":"vim","lang":"cpp"} + color {"enable":true,"theme":"molokai"} + icon {"theme":""} + network {"concurrency":10} + + [Themes] + Colors blue,dark,default,molokai,orange,pink,solarized,solarized.light + Icons ascii,default,win7 + + [Plugins] + solution.discuss 2017.12.21 + company 2017.12.18 + github 2017.08.10 + cache default + retry default + leetcode default + diff --git a/docs/index.html b/docs/index.html index 2b271af0..9deeb3a7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -10,6 +10,7 @@
Installation + Release Notes Showcases Commands Advanced Tips diff --git a/docs/install.md b/docs/install.md index 04ae4252..c2e20595 100644 --- a/docs/install.md +++ b/docs/install.md @@ -3,31 +3,70 @@ layout: default title: Installation --- +# All in One (beta) + +No need to install node.js. Now available on 64bits linux, mac, and windows. + +[Download](https://github.com/skygragon/leetcode-cli/releases) + # Prerequisites -`node.js` (`npm` included) required, please follow the installation guide: +Install the latest LTS version of `node.js` (`npm` included): -* https://nodejs.org/en/download/package-manager/ -* https://nodejs.org/en/download/ +* [Install from package manager](https://nodejs.org/en/download/package-manager/) +* [Install from directly download](https://nodejs.org/en/download/) -Then verify the result: +Check before going next: $ node -v $ npm -v # Installation -Choose one of the following ways to install leetcode-cli: +There are different ways to install `leetcode-cli`: + +### From npm + +This will install the latest STABLE version, but not include the latest DEV version. -**From npm repo** + $ npm install -g leetcode-cli + $ leetcode version - $ sudo npm install -g leetcode-cli +In case Ubuntu failed due to **permission denied**, try following: -**From source code** + $ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash + $ source ~/.bashrc + $ nvm install --lts + +Find more details [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions). + +### From GitHub + +This will install the latest DEV version from GitHub repo. + + $ npm install -g skygragon/leetcode-cli + $ leetcode version + +### From source + +Similar with above, while you can introduce your own changes as you wish. $ git clone http://github.com/skygragon/leetcode-cli - $ cd leetcode-cli && npm install && sudo npm install -g . + $ cd leetcode-cli && ./bin/install + $ leetcode version + +### From source (all-in-one) -Then verify the result: + $ git clone http://github.com/skygragon/leetcode-cli + $ cd leetcode-cli && node ./bin/pkg +### From docker + +NOTE: This is just a tiny taste to let you feel that leetcode-cli is. Please use other ways above to install leetcode-cli if you like it. + + $ alias leetcode='docker run -it --rm skygragon/leetcode-cli' $ leetcode version + +To persistent user data, you can mount a folder like this: + + $ alias leetcode='docker run -it --rm -v /Users/skygragon/data:/root skygragon/leetcode-cli' diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 00000000..0ad15c7a Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 00000000..77cce5eb --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,149 @@ +--- +layout: default +title: Release Notes +--- +# 2.6.2 +* `submit` + * fixes beta ratio issue + +# 2.6.1 +* `submit` + * fixes 500 error on windows. + +# 2.6.0 +* build all-in-one binary for linux/macos/windows. +* `show` + * support customized filename. + * use "--" as comment in sql file. +* `list` + * fixes format issue. +* fixes UT failures on windows. + +# 2.5.4 +* fixes error in fresh env without .lc existed. +* embed meta in file content instead of file name. +* update dependencies. + +# 2.5.3 + +* fixes "Failed to load locked problem" issue. +* move plugin's data into separate folders: + * login info + * problems list + * problem cache + +# 2.5.2 + +* `show` + * fixes 400 error + * support translated content for leetcode-cn + +# 2.5.1 + +* auto install missing plugins after upgrade. +* use 16m colors if possible. +* enhance color output on windows. +* `cache` + * fix issue that can't delete cache by name. +* `session` + * fix issue if session name is a number. +* `stat` + * use level weight in calendar view. + +# 2.5.0 + +* add `session` command to manage coding sessions on leetcode.com. +* add more color themes. + * molokai + * solarized + * solarized.light +* `list` + * fix id mismatch issue. +* `show` + * add `-o` option to specify output folder. + * fix badge output in non-default color themes. +* `stat` + * calculate on AC-ed questions in calendar graph. +* `test` + * fix out-of-order output issue. + +# 2.4.0 + +* only supports node's version >= 4. +* Refactor folder structure: + * now `~/.lc/` would be the only folder used by leetcode-cli. + * move lcconfig file to `~/.lc/`. + * move cache files to `~/.lc/cache/`. +* `config` + * fix string value parsing error. +* `list` + * show tag/lang badges in `-x` output. +* `show` + * add `-q` `-t` options to filter random questions. +* `stat` + * enhance output of `-g` option. + * enhance output on windows. + * add `-c` option to display calendar stat of how many AC-ed questions per day. + * add `--no-lock` option to filter out locked questions. + * add `-q` `-t` options to filter questions stat. + +# 2.3.0 + +* `plugin` + * only install necessary depedencies on specific platform. + * add `-c` option to show plugin config. + * support [cookie.chrome](https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.chrome.md) plugin. + * support [cookie.firefox](https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.firefox.md) plugin. +* docker + * support running leetcode-cli as docker container for new user's tasting. + * auto build docker image in Docker Hub. +* UI + * Add spinner message for long time running works. +* Add logo and updte documents. + +# 2.2.1 + +* add commands aliases. +* enhance documents about install error on Ubuntu. +* `config` + * fix wrong parsing on non-string value. +* `plugin` + * fix bug when installing new npm modules. +* `show` + * use traditional `.py` for python3 filename. +* `submission` + * enhance recursive folder creation. + +# 2.2.0 + +* `config` + * add new `config` command to manage user configs. + * try to save user from manually editing config file (~/.lcconfig). + * start to use new json config format. (NOTE: not compatible with old format!) +* `show` + * print suppoerted language list. +* add Release Notes page. +* remove several legacy hacks. + +# 2.1.1 +* `show` + * add `-e` option to open editor for coding. + * add `-c` option to display source code only. + * remove legacy `-t` `-d` options. + * fix bad alignment in colorful output. +* `list` + * enhance `-t` option to support multiple tags, e.g. `leetcode list -t google -t array` + * support latest `company` plugin to filter questions by tags like `array` or `dynamic programming` +* config + * add `EDITOR` to set default editor. +* fix `--no-color` bug. + + +# 2.1.0 +* `show` + * fix "unknown language" error due to recent API changes on leetcode.com. + * add `kotlin` language. +* `cache` + * remove `-a` option, now `leetcode cache -d` will directly clear all cache. + * add keyword match, e.g. `leetcode cache 537` will only show the cache for question 537. +* update most libray depedencies. diff --git a/docs/screenshots/intro.2018.01.13.gif b/docs/screenshots/intro.2018.01.13.gif new file mode 100644 index 00000000..9d7123aa Binary files /dev/null and b/docs/screenshots/intro.2018.01.13.gif differ diff --git a/docs/screenshots/intro.gif b/docs/screenshots/intro.gif deleted file mode 100644 index 77d16c31..00000000 Binary files a/docs/screenshots/intro.gif and /dev/null differ diff --git a/docs/screenshots/intro2.gif b/docs/screenshots/intro2.gif deleted file mode 100644 index 2744d121..00000000 Binary files a/docs/screenshots/intro2.gif and /dev/null differ diff --git a/icons/ascii.json b/icons/ascii.json new file mode 100644 index 00000000..1a7664f1 --- /dev/null +++ b/icons/ascii.json @@ -0,0 +1,12 @@ +{ + "yes": "v", + "no": "X", + "like": "*", + "unlike": " ", + "lock": "$", + "nolock": " ", + "empty": " ", + "ac": "O", + "notac": "X", + "none": "o" +} diff --git a/icons/default.json b/icons/default.json new file mode 100644 index 00000000..a5263a7f --- /dev/null +++ b/icons/default.json @@ -0,0 +1,12 @@ +{ + "yes": "✔", + "no": "✘", + "like": "★", + "unlike": "☆", + "lock": "🔒", + "nolock": " ", + "empty": " ", + "ac": "▣", + "notac": "▤", + "none": "⬚" +} diff --git a/icons/win7.json b/icons/win7.json new file mode 100644 index 00000000..0e79a481 --- /dev/null +++ b/icons/win7.json @@ -0,0 +1,12 @@ +{ + "yes": "√", + "no": "×", + "like": "♥", + "unlike": " ", + "lock": "$", + "nolock": " ", + "empty": " ", + "ac": "O", + "notac": "X", + "none": "o" +} diff --git a/lib/cache.js b/lib/cache.js index 4acec5cb..42efad0b 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,32 +1,47 @@ -var fs = require('fs'); +'use strict'; +var path = require('path'); -var h = require('./helper'); +var file = require('./file'); -var cache = {}; +const cache = {}; + +cache.init = function() { + file.mkdir(file.cacheDir()); +}; cache.get = function(k) { - var fullpath = h.getCacheFile(k); - if (!fs.existsSync(fullpath)) return null; + const fullpath = file.cacheFile(k); + if (!file.exist(fullpath)) return null; - var v = JSON.parse(fs.readFileSync(fullpath)); - return v; + return JSON.parse(file.data(fullpath)); }; cache.set = function(k, v) { - var dir = h.getCacheDir(); - if (!fs.existsSync(dir)) fs.mkdirSync(dir); - - var fullpath = h.getCacheFile(k); - fs.writeFileSync(fullpath, JSON.stringify(v)); + const fullpath = file.cacheFile(k); + file.write(fullpath, JSON.stringify(v)); return true; }; cache.del = function(k) { - var fullpath = h.getCacheFile(k); - if (!fs.existsSync(fullpath)) return false; + const fullpath = file.cacheFile(k); + if (!file.exist(fullpath)) return false; - fs.unlinkSync(fullpath); + file.rm(fullpath); return true; }; +cache.list = function() { + return file.list(file.cacheDir()) + .filter(x => path.extname(x) === '.json') + .map(function(filename) { + const k = path.basename(filename, '.json'); + const stat = file.stat(file.cacheFile(k)); + return { + name: k, + size: stat.size, + mtime: stat.mtime + }; + }); +}; + module.exports = cache; diff --git a/lib/chalk.js b/lib/chalk.js index a41b382a..ef78e9d8 100644 --- a/lib/chalk.js +++ b/lib/chalk.js @@ -1,17 +1,42 @@ +'use strict'; var _ = require('underscore'); var style = require('ansi-styles'); +var supportsColor = require('supports-color'); -var chalk = { - enabled: true, - themes: {}, +var file = require('./file'); + +const chalk = { + enabled: supportsColor.stdout, + use256: supportsColor.stdout && supportsColor.stdout.has256, + use16m: supportsColor.stdout && supportsColor.stdout.has16m, + themes: new Map(), theme: {} }; -var pres = []; -var posts = []; +const pres = []; +const posts = []; + +const DEFAULT = { + black: '#000000', + blue: '#0000ff', + cyan: '#00ffff', + gray: '#999999', + green: '#00ff00', + magenta: '#ff00ff', + red: '#ff0000', + white: '#ffffff', + yellow: '#ffff00' +}; chalk.setTheme = function(name) { - this.theme = this.themes[name] || this.themes.default || {}; + this.theme = this.themes.get(name) || this.themes.get('default'); +}; + +chalk.sprint = function(s, hex) { + const color = chalk.use16m ? style.color.ansi16m.hex(hex) + : chalk.use256 ? style.color.ansi256.hex(hex) + : style.color.ansi.hex(hex); + return color + s + style.color.close; }; chalk.print = function(s) { @@ -23,43 +48,54 @@ chalk.print = function(s) { chalk.wrap = function(pre, post) { pres.push(pre); posts.unshift(post); - var f = function(s) { - return chalk.print(s); - }; - f.__proto__ = chalk; + const f = x => chalk.print(x); + Object.setPrototypeOf(f, chalk); return f; }; +const bgName = x => 'bg' + x[0].toUpperCase() + x.substr(1); + chalk.init = function() { - var fs = require('fs'); - var path = require('path'); + for (let f of file.listCodeDir('colors')) { + const theme = {}; + const data = _.extendOwn({}, DEFAULT, f.data); + for (let x of _.pairs(data)) { + const k = x[0]; + const v = x[1]; + const bgK = bgName(k); - var dir = path.join(__dirname, '..', 'colors'); - _.each(fs.readdirSync(dir), function(f) { - var theme = JSON.parse(fs.readFileSync(path.join(dir, f))); - chalk.themes[path.basename(f, '.json')] = _.mapObject(theme, function(v, k) { - return style.color.ansi256.hex(v); - }); - }); + if (chalk.use16m) { + theme[k] = style.color.ansi16m.hex(v); + theme[bgK] = style.bgColor.ansi16m.hex(v); + } else if (chalk.use256) { + theme[k] = style.color.ansi256.hex(v); + theme[bgK] = style.bgColor.ansi256.hex(v); + } else { + theme[k] = style.color.ansi.hex(v); + theme[bgK] = style.bgColor.ansi.hex(v); + } + } + chalk.themes.set(f.name, theme); + } - _.chain(['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']) - .each(function(color) { + for (let color of ['black', 'blue', 'cyan', 'gray', 'green', 'magenta', 'red', 'white', 'yellow']) { Object.defineProperty(chalk, color, { - get: function() { - return chalk.wrap(chalk.theme[color], style.color.close); - } + get: () => chalk.wrap(chalk.theme[color], style.color.close), + configurable: true }); - }); + const bgcolor = bgName(color); + Object.defineProperty(chalk, bgcolor, { + get: () => chalk.wrap(chalk.theme[bgcolor], style.bgColor.close), + configurable: true + }); + } - _.chain(['bold', 'dim', 'italic', 'inverse', 'strikethrough', 'underline']) - .each(function(modifier) { + for (let modifier of ['bold', 'dim', 'italic', 'inverse', 'strikethrough', 'underline']) { Object.defineProperty(chalk, modifier, { - get: function() { - return chalk.wrap(style[modifier].open, style[modifier].close); - } + get: () => chalk.wrap(style[modifier].open, style[modifier].close), + configurable: true }); - }); + } }; -chalk.init(); module.exports = chalk; diff --git a/lib/cli.js b/lib/cli.js index e81e7e9b..e59cf7f5 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,61 +1,107 @@ -var fs = require('fs'); - -var log = require('loglevel'); +'use strict'; +var _ = require('underscore'); var chalk = require('./chalk'); +var cache = require('./cache'); var config = require('./config'); +var h = require('./helper'); +var file = require('./file'); +var icon = require('./icon'); +var log = require('./log'); +var Plugin = require('./plugin'); // We are expecting a tier configuration like: // global config < local config < cli params // Color is a tricky one so we manually handle it here. -function setColorMode() { - var useColor = config.USE_COLOR || false; - if (process.argv.indexOf('--color') >= 0) useColor = true; - if (process.argv.indexOf('--no-color') >= 0) useColor = false; +function initColor() { + chalk.enabled = config.color.enable && chalk.enabled; + chalk.init(); + chalk.setTheme(config.color.theme); +} - chalk.enabled = useColor; - chalk.setTheme(config.COLOR_THEME); +function initIcon() { + icon.init(); + icon.setTheme(config.icon.theme); } -function setLogLevel() { - var level = log.levels.INFO; - if (process.argv.indexOf('-v') >= 0) level = log.levels.DEBUG; - if (process.argv.indexOf('-vv') >= 0) level = log.levels.TRACE; +function initLogLevel() { + log.init(); - log.setLevel(level); + let level = 'INFO'; + if (process.argv.indexOf('-v') >= 0) level = 'DEBUG'; + if (process.argv.indexOf('-vv') >= 0) level = 'TRACE'; + + // print HTTP details in TRACE + if (level === 'TRACE') { + const request = require('request'); + request.debug = true; + + console.error = _.wrap(console.error, function(func) { + let args = Array.from(arguments); + args.shift(); - log.fail = function(e) { - log.error(chalk.red('ERROR: ' + (e.msg || e))); - }; + // FIXME: hack HTTP request log, hope no one else use it... + if (args.length > 0 && args[0].indexOf('REQUEST ') === 0) { + args = args.map((x) => h.printSafeHTTP(x)); + log.trace.apply(log, args); + } else { + log.info.apply(log, args); + } + }); + } + + log.setLevel(level); } -function checkCache() { - var h = require('./helper'); - var cacheDir = h.getCacheDir(); +function initDir() { + file.init(); + file.mkdir(file.homeDir()) +} - if (!fs.existsSync(cacheDir)) - fs.mkdirSync(cacheDir); +function initPlugins(cb) { + if (Plugin.init()) { + Plugin.save(); + return cb(); + } else { + Plugin.installMissings(function(e) { + if (e) return cb(e); + Plugin.init(); + return cb(); + }); + } } var cli = {}; -cli.run = function() { - config.init(); - - checkCache(); - setColorMode(); - setLogLevel(); +function runCommand() { + var yargs = require('yargs'); + h.width = yargs.terminalWidth(); + yargs.commandDir('commands') + .completion() + .help('h') + .alias('h', 'help') + .version(false) + .epilog('Seek more help at https://skygragon.github.io/leetcode-cli/commands') + .wrap(Math.min(h.width, 120)) + .argv; +} +cli.run = function() { process.stdout.on('error', function(e) { if (e.code === 'EPIPE') process.exit(); }); - require('yargs') - .commandDir('commands') - .completion() - .help() - .strict() - .argv; + config.init(); + + initColor(); + initIcon(); + initLogLevel(); + initDir() + initPlugins(function(e) { + if (e) return log.fatal(e); + cache.init(); + runCommand(); + }); }; module.exports = cli; diff --git a/lib/commands/cache.js b/lib/commands/cache.js new file mode 100644 index 00000000..154a3302 --- /dev/null +++ b/lib/commands/cache.js @@ -0,0 +1,67 @@ +'use strict'; +var _ = require('underscore'); + +var h = require('../helper'); +var chalk = require('../chalk'); +var log = require('../log'); +var cache = require('../cache'); +var session = require('../session'); +var sprintf = require('../sprintf'); + +const cmd = { + command: 'cache [keyword]', + desc: 'Manage local cache', + builder: function(yargs) { + return yargs + .option('d', { + alias: 'delete', + type: 'boolean', + describe: 'Delete cache by keyword', + default: false + }) + .positional('keyword', { + type: 'string', + describe: 'Cache name or question id', + default: '' + }) + .example(chalk.yellow('leetcode cache'), 'Show all cache') + .example(chalk.yellow('leetcode cache 1'), 'Show cache of question 1') + .example('', '') + .example(chalk.yellow('leetcode cache -d'), 'Delete all cache') + .example(chalk.yellow('leetcode cache 1 -d'), 'Delete cache of question 1'); + } +}; + +cmd.handler = function(argv) { + session.argv = argv; + + const name = argv.keyword; + const isInteger = Number.isInteger(Number(name)); + + const caches = cache.list() + .filter(function(f) { + return (name.length === 0) || + (isInteger ? f.name.startsWith(name + '.') : f.name === name); + }); + + if (argv.delete) { + for (let f of caches) cache.del(f.name); + } else { + log.info(chalk.gray(sprintf(' %s %63s %s', 'Cache', 'Size', 'Created'))); + log.info(chalk.gray('-'.repeat(86))); + + _.sortBy(caches, function(f) { + let x = parseInt(f.name.split('.')[0], 10); + if (Number.isNaN(x)) x = 0; + return x; + }) + .forEach(function(f) { + log.printf(' %-60s %8s %s ago', + chalk.green(f.name), + h.prettySize(f.size), + h.prettyTime((Date.now() - f.mtime) / 1000)); + }); + } +}; + +module.exports = cmd; diff --git a/lib/commands/config.js b/lib/commands/config.js new file mode 100644 index 00000000..6a1fe364 --- /dev/null +++ b/lib/commands/config.js @@ -0,0 +1,98 @@ +'use strict'; +var _ = require('underscore'); +var nconf = require('nconf'); + +var file = require('../file'); +var chalk = require('../chalk'); +var config = require('../config'); +var log = require('../log'); +var session = require('../session'); + +const cmd = { + command: 'config [key] [value]', + aliases: ['conf', 'cfg', 'setting'], + desc: 'Manage user configs', + builder: function(yargs) { + return yargs + .option('a', { + alias: 'all', + type: 'boolean', + describe: 'Show all config', + default: false + }) + .option('d', { + alias: 'delete', + type: 'boolean', + describe: 'Delete config by key', + default: false + }) + .positional('key', { + type: 'string', + describe: 'Config key, delimited by colon', + default: '' + }) + .positional('value', { + type: 'string', + describe: 'Config value', + default: '' + }) + .example(chalk.yellow('leetcode config'), 'Show user configs') + .example(chalk.yellow('leetcode config -a'), 'Show all configs = user + default') + .example(chalk.yellow('leetcode config plugins:github'), 'Show config by key') + .example('', '') + .example(chalk.yellow('leetcode config plugins:github:repo "your repo URL"'), 'Set config by key') + .example(chalk.yellow('leetcode config plugins:github -d'), 'Delete config by key'); + } +}; + +function prettyConfig(cfg) { + return JSON.stringify(cfg, null, 2); +} + +function loadConfig(showall) { + const cfg = showall ? config.getAll(true) : nconf.get(); + return _.omit(cfg, 'type'); +} + +function saveConfig() { + file.write(file.configFile(), prettyConfig(loadConfig(false))); +} + +cmd.handler = function(argv) { + session.argv = argv; + nconf.file('local', file.configFile()); + + // show all + if (argv.key.length === 0) + return log.info(prettyConfig(loadConfig(argv.all))); + + // sugar: notice user that use ':' instead of '.' + if (argv.key.includes('.') && !argv.key.includes(':')) + return log.printf('Key should use colon(:) as the delimiter, do you mean %s?', + chalk.yellow(argv.key.replace(/\./g, ':'))); + + const v = nconf.get(argv.key); + + // delete + if (argv.delete) { + if (v === undefined) return log.fatal('Key not found: ' + argv.key); + nconf.clear(argv.key); + return saveConfig(); + } + + // show + if (argv.value.length === 0) { + if (v === undefined) return log.fatal('Key not found: ' + argv.key); + return log.info(prettyConfig(v)); + } + + // set + try { + nconf.set(argv.key, JSON.parse(argv.value)); + } catch (e) { + nconf.set(argv.key, JSON.parse('"' + argv.value + '"')); + } + return saveConfig(); +}; + +module.exports = cmd; diff --git a/lib/commands/list.js b/lib/commands/list.js index 75b478ef..c010de86 100644 --- a/lib/commands/list.js +++ b/lib/commands/list.js @@ -1,118 +1,106 @@ +'use strict'; var _ = require('underscore'); -var sprintf = require('sprintf-js').sprintf; -var log = require('loglevel'); +var h = require('../helper'); var chalk = require('../chalk'); +var icon = require('../icon'); +var log = require('../log'); var core = require('../core'); -var h = require('../helper'); +var session = require('../session'); -var cmd = { +const cmd = { command: 'list [keyword]', - desc: 'list problems', - builder: { - keyword: { - type: 'string', - default: '', - describe: 'Filter problems by keyword' - }, - query: { - alias: 'q', - type: 'string', - default: '', - describe: 'Filter problems by conditions:\n' + - 'e(easy),m(medium),h(hard),d(done),l(locked),s(starred)\n' + - 'Uppercase means negative, e.g. D(not done)' - }, - stat: { - alias: 's', - type: 'boolean', - default: false, - describe: 'Show problems statistics' - } + aliases: ['ls'], + desc: 'List questions', + builder: function(yargs) { + return yargs + .option('q', core.filters.query) + .option('s', { + alias: 'stat', + type: 'boolean', + default: false, + describe: 'Show statistics of listed questions' + }) + .option('t', core.filters.tag) + .option('x', { + alias: 'extra', + type: 'boolean', + default: false, + describe: 'Show extra details: category, companies, tags.' + }) + .positional('keyword', { + type: 'string', + default: '', + describe: 'Filter questions by keyword' + }) + .example(chalk.yellow('leetcode list'), 'List all questions') + .example(chalk.yellow('leetcode list -x'), 'Show extra info of questions, e.g. tags') + .example('', '') + .example(chalk.yellow('leetcode list array'), 'List questions that has "array" in name') + .example(chalk.yellow('leetcode list -q eD'), 'List questions that with easy level and not done') + .example(chalk.yellow('leetcode list -t google'), 'List questions from Google company (require plugin)') + .example(chalk.yellow('leetcode list -t stack'), 'List questions realted to stack (require plugin)'); } }; -function byLevel(x, q) { - return x.level[0].toLowerCase() === q.toLowerCase(); -} - -function byStateAC(x, q) { - return x.state === 'ac'; -} - -function byLocked(x, q) { - return x.locked; -} - -function byStarred(x, q) { - return x.starred; -} - -var QUERY_HANDLERS = { - e: byLevel, - E: _.negate(byLevel), - m: byLevel, - M: _.negate(byLevel), - h: byLevel, - H: _.negate(byLevel), - l: byLocked, - L: _.negate(byLocked), - d: byStateAC, - D: _.negate(byStateAC), - s: byStarred, - S: _.negate(byStarred) -}; - cmd.handler = function(argv) { - core.getProblems(function(e, problems) { + session.argv = argv; + core.filterProblems(argv, function(e, problems) { if (e) return log.fail(e); - var all = problems.length; - - if (argv.query) { - argv.query.split('').forEach(function(q) { - var f = QUERY_HANDLERS[q]; - if (!f) return; - - problems = _.filter(problems, _.partial(f, _, q)); - }); - } - - var word = argv.keyword.toLowerCase(); + const word = argv.keyword.toLowerCase(); if (word) { if (word.endsWith(word.substr(-1).repeat(6))) { log.warn('Hmmm...you might need a new keyboard?'); } - problems = _.filter(problems, function(x) { - return x.name.toLowerCase().indexOf(word) !== -1; - }); + problems = problems.filter(x => x.name.toLowerCase().includes(word)); } - var stat = {locked: 0, starred: 0}; - problems.forEach(function(problem) { + const stat = {}; + for (let x of ['locked', 'starred', 'ac', 'notac', 'None', 'Easy', 'Medium', 'Hard']) stat[x] = 0; + + problems = _.sortBy(problems, x => -x.fid); + for (let problem of problems) { stat[problem.level] = (stat[problem.level] || 0) + 1; stat[problem.state] = (stat[problem.state] || 0) + 1; if (problem.locked) ++stat.locked; if (problem.starred) ++stat.starred; - log.info(sprintf('%s %s %s [%3d] %-60s %-6s (%.2f %%)', - (problem.starred ? chalk.yellow('★') : ' '), - (problem.locked ? '🔒' : ' '), - h.prettyState(problem.state), - problem.id, - problem.name, - problem.level, - problem.percent)); - }); + log.printf('%s %s %s [%=4s] %-60s %-6s (%s %%)', + (problem.starred ? chalk.yellow(icon.like) : icon.empty), + (problem.locked ? chalk.red(icon.lock) : icon.nolock), + h.prettyState(problem.state), + problem.fid, + problem.name, + h.prettyLevel(problem.level), + problem.percent.toFixed(2)); + + if (argv.extra) { + let badges = [problem.category]; + badges = badges.concat(problem.companies || []); + badges = badges.concat(problem.tags || []); + + let buf = []; + let len = 0; + for (let x of badges) { + if (len + x.length + 3 >= 60) { + log.printf('%12s%s', ' ', chalk.gray(buf.join(' | '))); + buf = []; + len = 0; + } + buf.push(x); + len += x.length + 3; + } + if (buf.length > 0) + log.printf('%12s%s', ' ', chalk.gray(buf.join(' | '))); + } + } if (argv.stat) { log.info(); - log.info(sprintf(' All: %-9d Listed: %-9d', all, problems.length)); - log.info(sprintf(' Locked: %-9d Starred: %-9d', stat.locked, stat.starred)); - log.info(sprintf(' Accept: %-9d Not-AC: %-9d New: %-9d', - (stat.ac || 0), (stat.notac || 0), (stat.None || 0))); - log.info(sprintf(' Easy: %-9d Medium: %-9d Hard: %-9d', - (stat.Easy || 0), (stat.Medium || 0), (stat.Hard || 0))); + log.printf(' Listed: %-9s Locked: %-9s Starred: %-9s', problems.length, stat.locked, stat.starred); + log.printf(' Accept: %-9s Not-AC: %-9s Remain: %-9s', stat.ac, stat.notac, stat.None); + log.printf(' Easy: %-9s Medium: %-9s Hard: %-9s', stat.Easy, stat.Medium, stat.Hard); } }); }; diff --git a/lib/commands/plugin.js b/lib/commands/plugin.js new file mode 100644 index 00000000..ca5a9cff --- /dev/null +++ b/lib/commands/plugin.js @@ -0,0 +1,124 @@ +'use strict'; +var h = require('../helper'); +var chalk = require('../chalk'); +var config = require('../config'); +var log = require('../log'); +var Plugin = require('../plugin'); +var session = require('../session'); +var sprintf = require('../sprintf'); + +const cmd = { + command: 'plugin [name]', + aliases: ['extension', 'ext'], + desc: 'Manage plugins', + builder: function(yargs) { + return yargs + .option('c', { + alias: 'config', + type: 'boolean', + describe: 'Show plugin config', + default: false + }) + .option('d', { + alias: 'disable', + type: 'boolean', + describe: 'Disable plugin', + default: false + }) + .option('D', { + alias: 'delete', + type: 'boolean', + describe: 'Delete plugin', + default: false + }) + .option('e', { + alias: 'enable', + type: 'boolean', + describe: 'Enable plugin', + default: false + }) + .option('i', { + alias: 'install', + type: 'boolean', + describe: 'Install plugin', + default: false + }) + .positional('name', { + type: 'string', + describe: 'Filter plugin by name', + default: '' + }) + .example(chalk.yellow('leetcode plugin'), 'Show all plugins') + .example(chalk.yellow('leetcode plugin company'), 'Show company plugin') + .example(chalk.yellow('leetcode plugin company -c'), 'Show config of company plugin') + .example('', '') + .example(chalk.yellow('leetcode plugin -i'), 'Install all missing plugins from GitHub') + .example(chalk.yellow('leetcode plugin -i company'), 'Install company plugin from GitHub') + .example(chalk.yellow('leetcode plugin -d company'), 'Disable company plugin') + .example(chalk.yellow('leetcode plugin -e company'), 'Enable company plugin') + .example(chalk.yellow('leetcode plugin -D company'), 'Delete company plugin'); + } +}; + +function print(plugins) { + log.info(chalk.gray(sprintf(' %6s %-18s %-15s %s', 'Active', 'Name', 'Version', 'Desc'))); + log.info(chalk.gray('-'.repeat(100))); + + plugins = plugins || Plugin.plugins; + for (let p of plugins) + log.printf(' %s %-18s %-15s %s', + h.prettyText('', p.enabled && !p.missing), + p.name, p.ver, p.desc); +} + +cmd.handler = function(argv) { + session.argv = argv; + + let plugins = Plugin.plugins; + const name = argv.name; + + if (argv.install) { + const cb = function(e, p) { + if (e) return log.fatal(e); + p.help(); + p.save(); + Plugin.init(); + print(); + }; + + if (name) { + Plugin.install(name, cb); + } else { + Plugin.installMissings(cb); + } + return; + } + + if (name) plugins = plugins.filter(x => x.name === name); + if (plugins.length === 0) return log.fatal('Plugin not found!'); + + const p = plugins[0]; + if (p.missing && (argv.enable || argv.disable)) + return log.fatal('Plugin missing, install it first'); + + if (argv.enable) { + p.enabled = true; + p.save(); + print(); + } else if (argv.disable) { + p.enabled = false; + p.save(); + print(); + } else if (argv.delete) { + p.delete(); + p.save(); + Plugin.init(); + print(); + } else if (argv.config) { + log.info(JSON.stringify(config.plugins[name] || {}, null, 2)); + } else { + print(plugins); + } +}; + +module.exports = cmd; diff --git a/lib/commands/session.js b/lib/commands/session.js new file mode 100644 index 00000000..64d460d6 --- /dev/null +++ b/lib/commands/session.js @@ -0,0 +1,126 @@ +'use strict'; +var prompt = require('prompt'); + +var h = require('../helper'); +var chalk = require('../chalk'); +var log = require('../log'); +var core = require('../core'); +var session = require('../session'); +var sprintf = require('../sprintf'); + +const cmd = { + command: 'session [keyword]', + aliases: ['branch'], + desc: 'Manage sessions', + builder: function(yargs) { + return yargs + .option('c', { + alias: 'create', + type: 'boolean', + describe: 'Create session', + default: false + }) + .option('d', { + alias: 'delete', + type: 'boolean', + describe: 'Delete session', + default: false + }) + .option('e', { + alias: 'enable', + type: 'boolean', + describe: 'Enable/activate session', + default: false + }) + .positional('keyword', { + type: 'string', + describe: 'Session name or id', + default: '' + }) + .example(chalk.yellow('leetcode session'), 'Show all cache') + .example(chalk.yellow('leetcode session xxx'), 'Show session by keyword') + .example('', '') + .example(chalk.yellow('leetcode session -c xxx'), 'Create session with name') + .example(chalk.yellow('leetcode session -e xxx'), 'Enable session by keyword') + .example(chalk.yellow('leetcode session -d xxx'), 'Delete session by keyword'); + } +}; + +function printSessions(e, sessions) { + if (e) return log.fail(e); + + log.info(chalk.gray(sprintf(' %6s %5s %18s %28s %16s', + 'Active', 'Id', 'Name', 'AC Questions', 'AC Submits'))); + log.info(chalk.gray('-'.repeat(80))); + + for (let s of sessions) { + let questionRate = 0; + let submissionRate = 0; + if (s.submitted_questions > 0) + questionRate = s.ac_questions * 100 / s.submitted_questions; + if (s.total_submitted > 0) + submissionRate = s.total_acs * 100 / s.total_submitted; + + log.printf(' %s %8s %-26s %6s (%6s %%) %6s (%6s %%)', + s.is_active ? h.prettyState('ac') : ' ', + s.id, + s.name || 'Anonymous Session', + chalk.green(s.ac_questions), + questionRate.toFixed(2), + chalk.green(s.total_acs), + submissionRate.toFixed(2)); + } +} + +cmd.handler = function(argv) { + session.argv = argv; + + if (argv.create) + return core.createSession(argv.keyword, printSessions); + + core.getSessions(function(e, sessions) { + if (e) return log.fail(e); + + if (argv.keyword) { + const id = Number(argv.keyword); + sessions = sessions.filter(x => x.name === argv.keyword || x.id === id); + if (sessions.length > 1) return log.fail('Ambiguous sessions?'); + + const session = sessions[0]; + if (!session) return log.fail('Session not found!'); + + if (argv.enable && !session.is_active) { + core.activateSession(session, function(e, sessions) { + if (e) return log.fail(e); + require('../session').deleteCodingSession(); + printSessions(e, sessions); + }); + return; + } + + if (argv.delete) { + log.info([ + chalk.red.bold('CAREFUL! This action CANNOT be undone!'), + '\nThis will permanently delete all your submissions', + 'and progress associated with this session.', + '\nAre you sure you want to delete this session?\n', + '\nPlease type in the session\'s', + chalk.yellow.bold('number of accepted submissions'), + 'to confirm.\n' + ].join(' ')); + + prompt.colors = false; + prompt.message = ''; + prompt.start(); + prompt.get([{name: 'answer', type: 'integer', required: true}], function(e, x) { + if (x.answer !== session.total_acs) return; + return core.deleteSession(session, printSessions); + }); + return; + } + } + printSessions(null, sessions); + }); +}; + +module.exports = cmd; diff --git a/lib/commands/show.js b/lib/commands/show.js index 5e79f48a..7c66204a 100644 --- a/lib/commands/show.js +++ b/lib/commands/show.js @@ -1,84 +1,199 @@ -var fs = require('fs'); +'use strict'; var util = require('util'); var _ = require('underscore'); -var log = require('loglevel'); -var sprintf = require('sprintf-js').sprintf; +var childProcess = require('child_process'); +var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); +var icon = require('../icon'); +var log = require('../log'); var config = require('../config'); var core = require('../core'); -var h = require('../helper'); +var session = require('../session'); -var cmd = { - command: 'show ', - desc: 'show problem by name or index', - builder: { - gen: { - alias: 'g', - type: 'boolean', - default: false, - describe: 'Generate source file from template' - }, - lang: { - alias: 'l', - type: 'string', - default: config.LANG, - describe: 'Program language to use' - }, - extra: { - alias: 'x', - type: 'boolean', - default: false, - describe: 'Provide extra problem details in generated file' - } +const cmd = { + command: 'show [keyword]', + aliases: ['view', 'pick'], + desc: 'Show question', + builder: function(yargs) { + return yargs + .option('c', { + alias: 'codeonly', + type: 'boolean', + default: false, + describe: 'Only show code template' + }) + .option('e', { + alias: 'editor', + type: 'string', + describe: 'Open source code in editor' + }) + .option('g', { + alias: 'gen', + type: 'boolean', + default: false, + describe: 'Generate source code' + }) + .option('l', { + alias: 'lang', + type: 'string', + default: config.code.lang, + describe: 'Programming language of the source code', + choices: config.sys.langs + }) + .option('o', { + alias: 'outdir', + type: 'string', + describe: 'Where to save source code', + default: '.' + }) + .option('q', core.filters.query) + .option('t', core.filters.tag) + .option('x', { + alias: 'extra', + type: 'boolean', + default: false, + describe: 'Show extra question details in source code' + }) + .positional('keyword', { + type: 'string', + default: '', + describe: 'Show question by name or id' + }) + .example(chalk.yellow('leetcode show 1'), 'Show question 1') + .example(chalk.yellow('leetcode show 1 -gx -l java'), 'Show question 1 and generate Java code') + .example(chalk.yellow('leetcode show 1 -gxe'), 'Open generated code in editor') + .example('', '') + .example(chalk.yellow('leetcode show'), 'Show random question') + .example(chalk.yellow('leetcode show -q h'), 'Show random hard question') + .example(chalk.yellow('leetcode show -t google'), 'Show random question from Google (require plugin)'); } }; -cmd.handler = function(argv) { - core.getProblem(argv.keyword, function(e, problem) { - if (e) return log.fail(e); +function genFileName(problem, opts) { + const path = require('path'); + const params = [ + file.fmt(config.file.show, problem), + '', + h.langToExt(opts.lang) + ]; - var msg = ''; - if (argv.gen) { - var template = _.find(problem.templates, function(x) { - return x.value === argv.lang; - }); - if (!template) - return log.fail('Failed to generate source file, ' + - 'unknown language "' + argv.lang + '"'); - problem.code = template.defaultCode; - - // try to use a new filename to avoid overwrite by mistake - var filename = problem.id + '.' + problem.key + h.langToExt(argv.lang); - var i = 0; - while (fs.existsSync(filename)) { - filename = problem.id + '.' + - problem.key + '.' + - (i++) + - h.langToExt(argv.lang); - } - - core.exportProblem(problem, filename, !argv.extra); - msg = sprintf('(File: %s)', chalk.yellow.underline(filename)); + // try new name to avoid overwrite by mistake + for (let i = 0; ; ++i) { + const name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.')); + if (!file.exist(name)) + return name; + params[1] = i; + } +} + +function showProblem(problem, argv) { + const taglist = [problem.category] + .concat(problem.companies || []) + .concat(problem.tags || []) + .map(x => h.badge(x, 'blue')) + .join(' '); + const langlist = problem.templates + .map(x => h.badge(x.value, 'yellow')) + .sort() + .join(' '); + + let code; + const needcode = argv.gen || argv.codeonly; + if (needcode) { + const template = problem.templates.find(x => x.value === argv.lang); + if (!template) { + log.fail('Not supported language "' + argv.lang + '"'); + log.warn('Supported languages: ' + langlist); + return; } - log.info(sprintf('[%d] %s %s\t%s\n', - problem.id, - problem.name, - (problem.starred ? chalk.yellow('★') : ' '), - msg)); - log.info(sprintf('%s\n', chalk.underline(problem.link))); - log.info(sprintf('* %s (%.2f%%)', problem.level, problem.percent)); - log.info(sprintf('* Total Accepted: %d', problem.totalAC)); - log.info(sprintf('* Total Submissions: %d', problem.totalSubmit)); - if (problem.testable && problem.testcase) { - log.info(sprintf('* Testcase Example: %s', - chalk.yellow(util.inspect(problem.testcase)))); + const opts = { + lang: argv.lang, + code: template.defaultCode, + tpl: argv.extra ? 'detailed' : 'codeonly' + }; + code = core.exportProblem(problem, opts); + } + + let filename; + if (argv.gen) { + file.mkdir(argv.outdir); + filename = genFileName(problem, argv); + file.write(filename, code); + + if (argv.editor !== undefined) { + childProcess.spawn(argv.editor || config.code.editor, [filename], { + // in case your editor of choice is vim or emacs + stdio: 'inherit' + }); + } + } else { + if (argv.codeonly) { + log.info(chalk.yellow(code)); + return; } + } + + log.printf('[%s] %s %s', problem.fid, problem.name, + (problem.starred ? chalk.yellow(icon.like) : icon.empty)); + log.info(); + log.info(chalk.underline(problem.link)); + if (argv.extra) { + log.info(); + log.info('Tags: ' + taglist); log.info(); - log.info(problem.desc); - }); + log.info('Langs: ' + langlist); + } + + log.info(); + log.printf('* %s', problem.category); + log.printf('* %s (%s%%)', h.prettyLevel(problem.level), problem.percent.toFixed(2)); + + if (filename) + log.printf('* Source Code: %s', chalk.yellow.underline(filename)); + if (problem.totalAC) + log.printf('* Total Accepted: %s', problem.totalAC); + if (problem.totalSubmit) + log.printf('* Total Submissions: %s', problem.totalSubmit); + if (problem.testable && problem.testcase) + log.printf('* Testcase Example: %s', chalk.yellow(util.inspect(problem.testcase))); + + log.info(); + log.info(problem.desc); +} + +cmd.handler = function(argv) { + session.argv = argv; + if (argv.keyword.length > 0) { + // show specific one + core.getProblem(argv.keyword, function(e, problem) { + if (e) return log.fail(e); + showProblem(problem, argv); + }); + } else { + // show random one + core.filterProblems(argv, function(e, problems) { + if (e) return log.fail(e); + + // random select one that not AC-ed yet + const user = session.getUser(); + problems = problems.filter(function(x) { + if (x.state === 'ac') return false; + if (!user.paid && x.locked) return false; + return true; + }); + if (problems.length === 0) return log.fail('Problem not found!'); + + const problem = _.sample(problems); + core.getProblem(problem, function(e, problem) { + if (e) return log.fail(e); + showProblem(problem, argv); + }); + }); + } }; module.exports = cmd; diff --git a/lib/commands/star.js b/lib/commands/star.js index bf48155b..3660432b 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -1,33 +1,42 @@ -var log = require('loglevel'); -var sprintf = require('sprintf-js').sprintf; - +'use strict'; var chalk = require('../chalk'); +var icon = require('../icon'); +var log = require('../log'); var core = require('../core'); +var session = require('../session'); -var cmd = { +const cmd = { command: 'star ', - desc: 'Star problem by name or index', - builder: { - delete: { - alias: 'd', - type: 'boolean', - describe: 'Unstar the problem', - default: false - } + aliases: ['like', 'favorite'], + desc: 'Star favorite question', + builder: function(yargs) { + return yargs + .option('d', { + alias: 'delete', + type: 'boolean', + describe: 'Unstar question', + default: false + }) + .positional('keyword', { + type: 'string', + describe: 'Question name or id', + default: '' + }) + .example(chalk.yellow('leetcode star 1'), 'Mark favorite to question 1') + .example(chalk.yellow('leetcode star 1 -d'), 'Unmark favorite to question 1'); } }; cmd.handler = function(argv) { + session.argv = argv; core.getProblem(argv.keyword, function(e, problem) { if (e) return log.fail(e); core.starProblem(problem, !argv.delete, function(e, starred) { if (e) return log.fail(e); - log.info(sprintf('[%d] %s %s', - problem.id, - problem.name, - chalk.yellow(starred ? '★' : '☆'))); + log.printf('[%s] %s %s', problem.fid, problem.name, + chalk.yellow(starred ? icon.like : icon.unlike)); core.updateProblem(problem, {starred: starred}); }); diff --git a/lib/commands/stat.js b/lib/commands/stat.js index 0de56d1f..772499c4 100644 --- a/lib/commands/stat.js +++ b/lib/commands/stat.js @@ -1,114 +1,224 @@ -var log = require('loglevel'); -var sprintf = require('sprintf-js').sprintf; +'use strict'; +var moment = require('moment'); var _ = require('underscore'); var chalk = require('../chalk'); +var icon = require('../icon'); +var log = require('../log'); var core = require('../core'); +var session = require('../session'); +var sprintf = require('../sprintf'); +var h = require('../helper'); -var cmd = { +const cmd = { command: 'stat', - desc: 'show statistics', - builder: { - graph: { - alias: 'g', - type: 'boolean', - default: false, - describe: 'Show graphic statistics' - } + desc: 'Show statistics', + aliases: ['stats', 'progress', 'report'], + builder: function(yargs) { + return yargs + .option('c', { + alias: 'cal', + type: 'boolean', + default: false, + describe: 'Show calendar statistics' + }) + .option('g', { + alias: 'graph', + type: 'boolean', + default: false, + describe: 'Show graphic statistics' + }) + .option('l', { + alias: 'lock', + type: 'boolean', + default: true, + describe: 'Include locked questions' + }) + .option('q', core.filters.query) + .option('t', core.filters.tag) + .example(chalk.yellow('leetcode stat'), 'Show progress status') + .example(chalk.yellow('leetcode stat -g'), 'Show detailed status in graph') + .example(chalk.yellow('leetcode stat -c'), 'Show accepted status in calendar') + .example('', '') + .example(chalk.yellow('leetcode stat --no-lock'), 'Show status without locked questions') + .example(chalk.yellow('leetcode stat -t algorithms'), 'Show status of algorithms questions only') + .example(chalk.yellow('leetcode stat -q h'), 'Show status of hard questions only'); } }; -function bar(c, n) { - return _.range(n) - .map(function(i) { - return c; - }) - .join(''); +function printLine(key, done, all) { + const n = 30; + const percent = (all > 0) ? done / all : 0; + const x = Math.ceil(n * percent); + log.printf(' %s\t%3s/%-3s (%6s %%) %s%s', + h.prettyLevel(key), done, all, + (100 * percent).toFixed(2), + chalk.green('█'.repeat(x)), + chalk.red('░'.repeat(n - x))); } -function prettyLine(key, done, all) { - done = done || 0; - var n = 30; - var x = Math.ceil(n * done / all); - return sprintf(' %-8s %3d/%-3d (%.2f%%)\t%s%s', - key, done, all, done * 100 / all, - chalk.green(bar('█', x)), - chalk.red(bar('░', n - x))); +function showProgress(problems) { + const stats = { + easy: {all: 0, ac: 0}, + medium: {all: 0, ac: 0}, + hard: {all: 0, ac: 0} + }; + + for (let problem of problems) { + const level = problem.level.toLowerCase(); + const state = problem.state.toLowerCase(); + + if (!(level in stats)) continue; + ++stats[level].all; + + if (!(state in stats[level])) continue; + ++stats[level][state]; + } + + printLine('Easy', stats.easy.ac, stats.easy.all); + printLine('Medium', stats.medium.ac, stats.medium.all); + printLine('Hard', stats.hard.ac, stats.hard.all); } -function showSummary(problems) { - var stats = {}; - var statsNoLock = {}; +function showGraph(problems) { + const ICONS = { + ac: chalk.green(icon.ac), + notac: chalk.red(icon.notac), + none: chalk.gray(icon.none), + empty: icon.empty + }; + + // row header is 4 bytes + // each question takes 2 bytes + // each group has 10 questions, which takes (2*10=20) + 3 paddings + let groups = Math.floor((h.width - 4) / (3 + 2 * 10)); + if (groups < 1) groups = 1; + if (groups > 5) groups = 5; + + const header = _.range(groups) + .map(x => sprintf('%4s%18s', x * 10 + 1, x * 10 + 10)) + .join(''); + log.info(' ' + header); - problems.forEach(function(problem) { - var keyAll = 'all' + problem.level; - var keyAC = problem.state + problem.level; - stats[keyAll] = (stats[keyAll] || 0) + 1; - stats[keyAC] = (stats[keyAC] || 0) + 1; + const graph = []; + for (let problem of problems) + graph[problem.fid] = ICONS[problem.state] || ICONS.none; - if (!problem.locked) { - statsNoLock[keyAll] = (statsNoLock[keyAll] || 0) + 1; - statsNoLock[keyAC] = (statsNoLock[keyAC] || 0) + 1; - } - }); + let rowNumFormat = ' %04s'; + let line = [sprintf(rowNumFormat, 0)]; + for (let i = 1, n = graph.length; i <= n; ++i) { + // padding before group + if (i % 10 === 1) line.push(' '); - log.info(prettyLine('Easy', stats.acEasy, stats.allEasy)); - log.info(prettyLine('Medium', stats.acMedium, stats.allMedium)); - log.info(prettyLine('Hard', stats.acHard, stats.allHard)); + line.push(graph[i] || ICONS.empty); + + // time to start new row + if (i % (10 * groups) === 0 || i === n) { + log.info(line.join(' ')); + line = [sprintf(rowNumFormat, i)]; + } + } log.info(); - log.info('Without Locked:'); - log.info(prettyLine('Easy', statsNoLock.acEasy, statsNoLock.allEasy)); - log.info(prettyLine('Medium', statsNoLock.acMedium, statsNoLock.allMedium)); - log.info(prettyLine('Hard', statsNoLock.acHard, statsNoLock.allHard)); + log.printf('%7s%s%3s%s%3s%s', + ' ', ICONS.ac + chalk.green(' Accepted'), + ' ', ICONS.notac + chalk.red(' Not Accepted'), + ' ', ICONS.none + ' Remaining'); } -function showGraph(problems) { - var ac = chalk.green('█'); - var notac = chalk.enabled ? chalk.red('█') : 'X'; - var none = '░'; - - var graph = []; - _.each(problems, function(problem) { - if (problem.state === 'ac') { - graph[problem.id] = ac; - } else if (problem.state === 'notac') { - graph[problem.id] = notac; - } else { - graph[problem.id] = none; - } - }); +function showCal(problems) { + const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const ICONS = [ + icon.none, + chalk.white(icon.ac), + chalk.green(icon.ac), + chalk.yellow(icon.ac), + chalk.red(icon.ac) + ]; + + const N_MONTHS = 12; + const N_WEEKS = 53; + const N_WEEKDAYS = 7; + + const now = moment(); + + const SCORES = {easy: 1, medium: 2, hard: 5}; + function toScore(sum, id) { + const problem = problems.find(x => x.fid === id); + if (problem) sum += (SCORES[problem.level.toLowerCase()] || 1); + return sum; + } + + // load historical stats + const graph = []; + const stats = require('../cache').get(h.KEYS.stat) || {}; + for (let k of _.keys(stats)) { + const score = (stats[k]['ac.set'] || []).reduce(toScore, 0); + if (score === 0) continue; - log.info(sprintf('%8d%9d%5d%8d%5d%8d%5d%8d%5d%8d', - 1, 10, 11, 20, 21, 30, 31, 40, 41, 50)); + const d = moment(k, 'YYYY-MM-DD'); + graph[now.diff(d, 'days')] = score; + } + + // print header + const buf = Buffer.alloc(120, ' ', 'ascii'); + for (let i = 0; i <= N_MONTHS; ++i) { + // for day 1 in each month, calculate its column position in graph + const d = now.clone().subtract(i, 'months').date(1); + const idx = now.diff(d, 'days'); - var line = [sprintf(' %03d ', 1)]; - for (var i = 1, n = graph.length; i < n; ++i) { - line.push(graph[i] || ' '); - if (i % 10 === 0) line.push(' '); - if (i % 50 === 0 || i === n) { - log.info(line.join('')); - line = [sprintf(' %03d ', i)]; + const j = (N_WEEKS - idx / N_WEEKDAYS + 1) * 2; + if (j >= 0) buf.write(MONTHS[d.month()], j); + } + log.printf('%7s%s', ' ', buf.toString()); + + // print graph + for (let i = 0; i < N_WEEKDAYS; ++i) { + const line = []; + // print day in week + const idx = (now.day() + i + 1) % N_WEEKDAYS; + line.push(sprintf('%4s ', WEEKDAYS[idx])); + + for (let j = 0; j < N_WEEKS; ++j) { + let idx = (N_WEEKS - j - 1) * N_WEEKDAYS + N_WEEKDAYS - i - 1; + const d = now.clone().subtract(idx, 'days'); + + // map count to icons index: + // [0] => 0, [1,5] => 1, [6,10] => 2, [11,15] => 3, [16,) => 4 + const count = graph[idx] || 0; + idx = Math.floor((count - 1) / 5) + 1; + if (idx > 4) idx = 4; + + let icon = ICONS[idx]; + // use different colors for adjacent months + if (idx === 0 && d.month() % 2) icon = chalk.gray(icon); + line.push(icon); } + log.info(line.join(' ')); } log.info(); - log.info(sprintf('%7s%s%3s%s%3s%s', - ' ', ac + chalk.green(' Accepted'), - ' ', notac + chalk.red(' Not Accepted'), - ' ', none + ' Remaining')); - log.info(); + log.printf('%8s%s%3s%s%3s%s%3s%s', + ' ', ICONS[1] + ' 1~5', + ' ', ICONS[2] + ' 6~10', + ' ', ICONS[3] + ' 11~15', + ' ', ICONS[4] + ' 16+'); } cmd.handler = function(argv) { - core.getProblems(function(e, problems) { + session.argv = argv; + core.filterProblems(argv, function(e, problems) { if (e) return log.fail(e); - if (argv.graph) { - showGraph(problems); - } else { - showSummary(problems); - } + if (!argv.lock) + problems = problems.filter(x => !x.locked); + + log.info(); + if (argv.graph) showGraph(problems); + else if (argv.cal) showCal(problems); + else showProgress(problems); + log.info(); }); }; diff --git a/lib/commands/submission.js b/lib/commands/submission.js index 9471a51c..de0449a3 100644 --- a/lib/commands/submission.js +++ b/lib/commands/submission.js @@ -1,121 +1,133 @@ -var fs = require('fs'); +'use strict'; +var path = require('path'); var _ = require('underscore'); -var log = require('loglevel'); -var sprintf = require('sprintf-js').sprintf; +var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); +var config = require('../config'); +var log = require('../log'); +var Queue = require('../queue'); var core = require('../core'); -var h = require('../helper'); -var queue = require('../queue'); +var session = require('../session'); -var cmd = { +const cmd = { command: 'submission [keyword]', - desc: 'retrieve earlier submission by name or index', - builder: { - all: { - alias: 'a', - type: 'boolean', - default: false, - describe: 'Retrieve for all problems' - }, - outdir: { - alias: 'o', - type: 'string', - describe: 'Where to save the submissions', - default: '.' - }, - extra: { - alias: 'x', - type: 'boolean', - default: false, - describe: 'Provide extra problem details in submission file' - } + aliases: ['pull'], + desc: 'Download submission code', + builder: function(yargs) { + return yargs + .option('a', { + alias: 'all', + type: 'boolean', + default: false, + describe: 'Download all questions' + }) + .option('l', { + alias: 'lang', + type: 'string', + default: 'all', + describe: 'Filter by programming language' + }) + .option('o', { + alias: 'outdir', + type: 'string', + describe: 'Where to save submission code', + default: '.' + }) + .option('x', { + alias: 'extra', + type: 'boolean', + default: false, + describe: 'Show extra question details in submission code' + }) + .positional('keyword', { + type: 'string', + default: '', + describe: 'Download specific question by id' + }) + .example(chalk.yellow('leetcode submission -a -o mydir'), 'Download all to folder mydir') + .example(chalk.yellow('leetcode submission -x -a'), 'Add descriptions in the downloaded codes') + .example(chalk.yellow('leetcode submission -l cpp 1'), 'Download cpp submission of question 1'); } }; -function onTaskDone(e, msg, problem, cb) { - // NOTE: msg color means different purpose: - // - red: error - // - green: accepted, fresh download - // - yellow: not ac-ed, fresh download - // - white: existed already, skip download - log.info(sprintf('[%3d] %-60s %s', - problem.id, - problem.name, - (e ? chalk.red('ERROR: ' + e) : msg) - )); - if (cb) cb(e); -} - -function onTaskRun(argv, problem, cb) { - var done = _.partial(onTaskDone, _, _, problem, cb); +function doTask(problem, queue, cb) { + const argv = queue.ctx.argv; + + function onTaskDone(e, msg) { + // NOTE: msg color means different purpose: + // - red: error + // - green: accepted, fresh download + // - yellow: not ac-ed, fresh download + // - white: existed already, skip download + log.printf('[%=4s] %-60s %s', problem.fid, problem.name, + (e ? chalk.red('ERROR: ' + (e.msg || e)) : msg)); + if (cb) cb(e); + } if (argv.extra) { // have to get problem details, e.g. problem description. - core.getProblem(problem.id, function(e, problem) { - if (e) return done(e); - - exportSubmission(argv, problem, done); + core.getProblem(problem.fid, function(e, problem) { + if (e) return cb(e); + exportSubmission(problem, argv, onTaskDone); }); } else { - exportSubmission(argv, problem, done); + exportSubmission(problem, argv, onTaskDone); } } -function exportSubmission(argv, problem, cb) { +function exportSubmission(problem, argv, cb) { core.getSubmissions(problem, function(e, submissions) { if (e) return cb(e); - if (submissions.length === 0) return cb('no submissions?'); + if (submissions.length === 0) + return cb('No submissions?'); - // find the latest accepted one - var submission = _.find(submissions, function(x) { - // TODO: select by lang? - return x.status_display === 'Accepted'; - }); + // get obj list contain required filetype + submissions = submissions.filter(x => argv.lang === 'all' || argv.lang === x.lang); + if (submissions.length === 0) + return cb('No submissions in required language.'); // if no accepted, use the latest non-accepted one - submission = submission || submissions[0]; + const submission = submissions.find(x => x.status_display === 'Accepted') || submissions[0]; + submission.ac = (submission.status_display === 'Accepted'); - var filename = sprintf('%s/%d.%s.%s.%s%s', - argv.outdir, - problem.id, - problem.key, - submission.id, - problem.state, - h.langToExt(submission.lang)); + const data = _.extend({}, submission, problem); + data.sid = submission.id; + data.ac = submission.ac ? 'ac' : 'notac'; + const basename = file.fmt(config.file.submission, data); + const f = path.join(argv.outdir, basename + h.langToExt(submission.lang)); + file.mkdir(argv.outdir); // skip the existing cached submissions - if (fs.existsSync(filename)) { - return cb(null, chalk.underline(filename)); - } + if (file.exist(f)) + return cb(null, chalk.underline(f)); core.getSubmission(submission, function(e, submission) { if (e) return cb(e); - problem.code = submission.code; - core.exportProblem(problem, filename, !argv.extra); - - if (submission.status_display === 'Accepted') - cb(null, chalk.green.underline(filename)); - else - cb(null, chalk.yellow.underline(filename)); + const opts = { + lang: submission.lang, + code: submission.code, + tpl: argv.extra ? 'detailed' : 'codeonly' + }; + file.write(f, core.exportProblem(problem, opts)); + cb(null, submission.ac ? chalk.green.underline(f) + : chalk.yellow.underline(f)); }); }); } cmd.handler = function(argv) { - var doTask = _.partial(onTaskRun, argv, _, _); + session.argv = argv; + const q = new Queue(null, {argv: argv}, doTask); if (argv.all) { core.getProblems(function(e, problems) { if (e) return log.fail(e); - - problems = problems.filter(function(q) { - return q.state === 'ac' || q.state === 'notac'; - }); - - queue.run(problems, doTask); + problems = problems.filter(x => x.state === 'ac' || x.state === 'notac'); + q.addTasks(problems).run(); }); return; } @@ -125,8 +137,7 @@ cmd.handler = function(argv) { core.getProblem(argv.keyword, function(e, problem) { if (e) return log.fail(e); - - queue.run([problem], doTask); + q.addTask(problem).run(); }); }; diff --git a/lib/commands/submit.js b/lib/commands/submit.js index 2e7f2155..56f5ed04 100644 --- a/lib/commands/submit.js +++ b/lib/commands/submit.js @@ -1,76 +1,98 @@ +'use strict'; var util = require('util'); -var _ = require('underscore'); -var log = require('loglevel'); - -var core = require('../core'); var h = require('../helper'); +var file = require('../file'); +var chalk = require('../chalk'); +var log = require('../log'); +var core = require('../core'); +var session = require('../session'); -var cmd = { +const cmd = { command: 'submit ', - desc: 'submit final solution to leetcode', - builder: { + aliases: ['push', 'commit'], + desc: 'Submit code', + builder: function(yargs) { + return yargs + .positional('filename', { + type: 'string', + describe: 'Code file to submit', + default: '' + }) + .example(chalk.yellow('leetcode submit 1.two-sum.cpp'), 'Submit code'); } }; -var INDENT = ' '; +function printResult(actual, k) { + if (!actual.hasOwnProperty(k)) return; + + const v = actual[k] || ''; + const lines = Array.isArray(v) ? v : [v]; + for (let line of lines) { + if (k !== 'state') line = k + ': ' + line; + log.info(' ' + h.prettyText(' ' + line, actual.ok)); + } +} + +function printLine() { + const args = Array.from(arguments); + const actual = args.shift(); + const line = util.format.apply(util, args); + log.info(' ' + h.prettyText(' ' + line, actual.ok)); +} cmd.handler = function(argv) { - // use the 1st section in filename as keyword - // e.g. two-sum.cpp, or two-sum.78502271.ac.cpp - var keyword = h.getFilename(argv.filename).split('.')[0]; + session.argv = argv; + if (!file.exist(argv.filename)) + return log.fatal('File ' + argv.filename + ' not exist!'); + + const meta = file.meta(argv.filename); - core.getProblem(keyword, function(e, problem) { + core.getProblem(meta.id, function(e, problem) { if (e) return log.fail(e); problem.file = argv.filename; + problem.lang = meta.lang; core.submitProblem(problem, function(e, results) { if (e) return log.fail(e); - var result = results[0]; - var ok = (result.status_code === 10) && - (result.total_correct === result.total_testcases); - - var runOk = result.run_success; - - var line = util.format(' %s', h.statusToName(result.status_code)); - log.info(INDENT + h.prettyText(line, ok)); - - // show "xxx_error" message - _.chain(result) - .pick(function(v, k, obj) { - return /_error$/.test(k) && v.length > 0; - }) - .values() - .each(function(v) { - log.info(INDENT + h.prettyText(' ' + v, ok)); - }); - - // show success ratio - line = util.format(' %d/%d cases passed (%s)', - result.total_correct, - result.total_testcases, - result.status_runtime); - log.info(INDENT + h.prettyText(line, ok)); - - // show testcase - var testcase = result.input || result.last_testcase; - if (!ok && testcase) { - line = util.format(' testcase: %s', util.inspect(testcase)); - log.info(INDENT + h.prettyText(line, ok)); - } - - if (!ok && runOk) { - line = util.format(' output: %s', result.code_output); - log.info(INDENT + h.prettyText(line, ok)); - - line = util.format(' expected: %s', result.expected_output); - log.info(INDENT + h.prettyText(line, ok)); + const result = results[0]; + + printResult(result, 'state'); + printLine(result, '%d/%d cases passed (%s)', + result.passed, result.total, result.runtime); + + if (result.ok) { + session.updateStat('ac', 1); + session.updateStat('ac.set', problem.fid); + core.getSubmission({id: result.id}, function(e, submission) { + if (e || !submission || !submission.distributionChart) + return log.warn('Failed to get submission beat ratio.'); + + const lang = submission.distributionChart.lang; + const scores = submission.distributionChart.distribution; + const myRuntime = parseFloat(result.runtime); + + let ratio = 0.0; + for (let score of scores) { + if (parseFloat(score[0]) >= myRuntime) + ratio += parseFloat(score[1]); + } + + printLine(result, 'Your runtime beats %d %% of %s submissions', + ratio.toFixed(2), lang); + }); + } else { + printResult(result, 'error'); + printResult(result, 'testcase'); + printResult(result, 'answer'); + printResult(result, 'expected_answer'); + printResult(result, 'stdout'); } // update this problem status in local cache - core.updateProblem(problem, {state: (ok ? 'ac' : 'notac')}); + core.updateProblem(problem, {state: (result.ok ? 'ac' : 'notac')}); }); }); }; diff --git a/lib/commands/test.js b/lib/commands/test.js index 9f594c0b..21c4a4eb 100644 --- a/lib/commands/test.js +++ b/lib/commands/test.js @@ -1,60 +1,64 @@ -var util = require('util'); - +'use strict'; var _ = require('underscore'); -var log = require('loglevel'); +var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); +var log = require('../log'); var core = require('../core'); -var h = require('../helper'); +var session = require('../session'); -var cmd = { +const cmd = { command: 'test ', - desc: 'send solution to leetcode and run test', - builder: { - testcase: { - alias: 't', - type: 'string', - default: '', - describe: 'Provide test case in command line' - }, - i: { - type: 'boolean', - default: false, - describe: 'Provide test case interactively' - } + aliases: ['run'], + desc: 'Test code', + builder: function(yargs) { + return yargs + .option('i', { + alias: 'interactive', + type: 'boolean', + default: false, + describe: 'Provide test case interactively' + }) + .option('t', { + alias: 'testcase', + type: 'string', + default: '', + describe: 'Provide test case' + }) + .positional('filename', { + type: 'string', + default: '', + describe: 'Code file to test' + }) + .example(chalk.yellow('leetcode test 1.two-sum.cpp'), 'Test code with default test case') + .example(chalk.yellow('leetcode test 1.two-sum.cpp -t "[1,2,3]\\n4"'), 'Test code with customized test case'); } }; -function prettyLine(actual, expected, key) { - if (!actual.hasOwnProperty(key)) - return; - - // hack: leetcode will return status_code = 10 even - // if the answer is not right! - if (key === 'status_code' && actual[key] === 10) - return; +function printResult(actual, expect, k) { + if (!actual.hasOwnProperty(k)) return; + // HACk: leetcode still return 'Accepted' even the answer is wrong!! + const v = actual[k] || ''; + if (k === 'state' && v === 'Accepted') return; - var ok = true; + let ok = actual.ok; + if (expect && !_.isEqual(actual[k], expect[k])) ok = false; - if (!actual.run_success) { - ok = false; - } else if (expected && !_.isEqual(actual[key], expected[key])) { - ok = false; + const lines = Array.isArray(v) ? v : [v]; + for (let line of lines) { + if (k !== 'state') line = k + ': ' + line; + log.info(' ' + h.prettyText(' ' + line, ok)); } - - var line = (key === 'status_code') ? - util.format(' %s', h.statusToName(actual[key])) : - util.format(' %s: %s', key.split('_').pop(), actual[key]); - - log.info(' ' + h.prettyText(line, ok)); } function runTest(argv) { - // use the 1st section in filename as keyword - // e.g. two-sum.cpp, or two-sum.78502271.ac.cpp - var keyword = h.getFilename(argv.filename).split('.')[0]; + if (!file.exist(argv.filename)) + return log.fatal('File ' + argv.filename + ' not exist!'); - core.getProblem(keyword, function(e, problem) { + const meta = file.meta(argv.filename); + + core.getProblem(meta.id, function(e, problem) { if (e) return log.fail(e); if (!problem.testable) @@ -67,6 +71,7 @@ function runTest(argv) { return log.fail('missing testcase?'); problem.file = argv.filename; + problem.lang = meta.lang; log.info('\nInput data:'); log.info(problem.testcase); @@ -74,30 +79,23 @@ function runTest(argv) { core.testProblem(problem, function(e, results) { if (e) return log.fail(e); - for (var i = 0; i < results.length; ++i) { + results = _.sortBy(results, x => x.type); + for (let i = 0; i < results.length; ++i) { log.info(); - log.info(chalk.yellow(results[i].name)); - - prettyLine(results[i], null, 'status_code'); - prettyLine(results[i], null, 'status_runtime'); - prettyLine(results[i], results[i + 1], 'code_answer'); - prettyLine(results[i], results[i + 1], 'code_output'); - - // show "xxx_error" message - _.chain(results[i]) - .pick(function(v, k, obj) { - return /_error$/.test(k) && v.length > 0; - }) - .keys() - .each(function(k) { - prettyLine(results[i], null, k); - }); + log.info(chalk.yellow(results[i].type)); + + printResult(results[i], null, 'state'); + printResult(results[i], null, 'runtime'); + printResult(results[i], results[i + 1], 'answer'); + printResult(results[i], results[i + 1], 'stdout'); + printResult(results[i], null, 'error'); } }); }); } cmd.handler = function(argv) { + session.argv = argv; if (!argv.i) return runTest(argv); diff --git a/lib/commands/user.js b/lib/commands/user.js index 05db9546..4cd903cd 100644 --- a/lib/commands/user.js +++ b/lib/commands/user.js @@ -1,30 +1,41 @@ -var log = require('loglevel'); +'use strict'; var prompt = require('prompt'); +var h = require('../helper'); +var config = require('../config'); var chalk = require('../chalk'); +var log = require('../log'); var core = require('../core'); +var session = require('../session'); +var sprintf = require('../sprintf'); -var cmd = { +const cmd = { command: 'user', - desc: 'login/logout with leetcode account', - builder: { - login: { - alias: 'l', - type: 'boolean', - default: false, - describe: 'Login' - }, - logout: { - alias: 'L', - type: 'boolean', - default: false, - describe: 'Logout' - } + aliases: ['account'], + desc: 'Manage account', + builder: function(yargs) { + return yargs + .option('l', { + alias: 'login', + type: 'boolean', + default: false, + describe: 'Login' + }) + .option('L', { + alias: 'logout', + type: 'boolean', + default: false, + describe: 'Logout' + }) + .example(chalk.yellow('leetcode user'), 'Show current user') + .example(chalk.yellow('leetcode user -l'), 'User login') + .example(chalk.yellow('leetcode user -L'), 'User logout'); } }; cmd.handler = function(argv) { - var user = null; + session.argv = argv; + let user = null; if (argv.login) { // login prompt.colors = false; @@ -38,23 +49,27 @@ cmd.handler = function(argv) { core.login(user, function(e, user) { if (e) return log.fail(e); - log.info('Successfully login as', chalk.yellow(user.name)); }); }); } else if (argv.logout) { // logout - user = core.logout(null); + user = core.logout(user, true); if (user) log.info('Successfully logout as', chalk.yellow(user.name)); else log.fail('You are not login yet?'); } else { // show current user - user = core.getUser(); - if (user) - log.info('You are now login as', chalk.yellow(user.name)); - else + user = session.getUser(); + if (user) { + log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host'))); + log.info(chalk.gray('-'.repeat(60))); + log.printf(' %s %-20s %s', + h.prettyText('', user.paid || false), + chalk.yellow(user.name), + config.sys.urls.base); + } else return log.fail('You are not login yet?'); } }; diff --git a/lib/commands/version.js b/lib/commands/version.js index 2882b5d3..4ba16749 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -1,39 +1,77 @@ -var log = require('loglevel'); +'use strict'; +var _ = require('underscore'); -var cmd = { +var file = require('../file'); +var chalk = require('../chalk'); +var icon = require('../icon'); +var log = require('../log'); +var Plugin = require('../plugin'); +var session = require('../session'); + +const cmd = { command: 'version', - desc: 'show version info', - builder: { + aliases: ['info', 'env'], + desc: 'Show version info', + builder: function(yargs) { + return yargs + .example(chalk.yellow('leetcode version'), 'Show version number') + .example(chalk.yellow('leetcode version -v'), 'Show more details'); } }; +function printLine(k, v) { + log.printf('%-20s %s', k, v); +} + +function getVersion() { + let version = require('../../package.json').version; + + try { + const commit = require('../../.env.json').commit.short; + if (commit) version += '-' + commit; + } catch (e) {} + + return version; +} + cmd.handler = function(argv) { - var version = require('../../package.json').version; - if (log.getLevel() >= log.levels.INFO) { + session.argv = argv; + const version = getVersion(); + + if (!log.isEnabled('DEBUG')) return log.info(version); - } - var logo = [ + const logo = [ ' _ _ _ ', '| | | | | | ', '| | ___ ___| |_ ___ ___ __| | ___ ', '| |/ _ \\/ _ \\ __|/ __|/ _ \\ / _` |/ _ \\', '| | __/ __/ |_ (__| (_) | (_| | __/', - '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___| CLI v' + version + '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___| CLI ' + chalk.green('v' + version) ].join('\n'); - log.debug(logo); - - var h = require('../helper'); - log.debug('\n[Environment]'); - log.debug('Cache: ', h.getCacheDir()); - log.debug('Config:', h.getConfigFile()); - - var config = require('../config'); - var sprintf = require('sprintf-js').sprintf; - log.debug('\n[Configuration]'); - Object.getOwnPropertyNames(config).sort().forEach(function(k) { - log.debug(sprintf('%-16s %s', k + ':', config[k])); + log.info(logo); + + const os = require('os'); + const config = require('../config'); + + log.info('\n[Environment]'); + printLine('Node', process.version); + printLine('OS', os.platform() + ' ' + os.release()); + printLine('Cache', file.cacheDir()); + printLine('Config', file.configFile()); + + log.info('\n[Configuration]'); + _.each(config.getAll(true), function(v, k) { + if (k === 'plugins') return; + printLine(k, JSON.stringify(v)); }); + + log.info('\n[Themes]'); + printLine('Colors', Array.from(chalk.themes.keys())); + printLine('Icons', Array.from(icon.themes.keys())); + + log.info('\n[Plugins]'); + for (let p of Plugin.plugins) printLine(p.name, p.ver); }; module.exports = cmd; diff --git a/lib/config.js b/lib/config.js index ae236a17..373b9f0f 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,36 +1,105 @@ +'use strict'; var _ = require('underscore'); +var nconf = require('nconf'); -var h = require('./helper'); +var file = require('./file'); -var DEFAULT_CONFIG = { +const DEFAULT_CONFIG = { // usually you don't wanna change those - URL_BASE: 'https://leetcode.com', - URL_LOGIN: 'https://leetcode.com/accounts/login/', - URL_PROBLEMS: 'https://leetcode.com/api/problems/algorithms/', - URL_PROBLEM: 'https://leetcode.com/problems/$id', - URL_TEST: 'https://leetcode.com/problems/$key/interpret_solution/', - URL_SUBMIT: 'https://leetcode.com/problems/$key/submit/', - URL_SUBMISSIONS: 'https://leetcode.com/api/submissions/$key', - URL_SUBMISSION: 'https://leetcode.com/submissions/detail/$id/', - URL_VERIFY: 'https://leetcode.com/submissions/detail/$id/check/', - URL_STAR: 'https://leetcode.com/problems/favor/', + sys: { + categories: [ + 'algorithms', + 'database', + 'shell' + ], + langs: [ + 'bash', + 'c', + 'cpp', + 'csharp', + 'golang', + 'java', + 'javascript', + 'kotlin', + 'mysql', + 'php', + 'python', + 'python3', + 'ruby', + 'rust', + 'scala', + 'swift' + ], + urls: { + base: 'https://leetcode.com', + graphql: 'https://leetcode.com/graphql', + login: 'https://leetcode.com/accounts/login/', + problems: 'https://leetcode.com/api/problems/$category/', + problem: 'https://leetcode.com/problems/$slug/description/', + test: 'https://leetcode.com/problems/$slug/interpret_solution/', + session: 'https://leetcode.com/session/', + submit: 'https://leetcode.com/problems/$slug/submit/', + submissions: 'https://leetcode.com/api/submissions/$slug', + submission: 'https://leetcode.com/submissions/detail/$id/', + verify: 'https://leetcode.com/submissions/detail/$id/check/', + favorites: 'https://leetcode.com/list/api/questions', + favorite_delete: 'https://leetcode.com/list/api/questions/$hash/$id', + plugin: 'https://github.com/skygragon/leetcode-cli-plugins/raw/master/plugins/$name.js' + } + }, // but you will want change these - LANG: 'cpp', // avail: [c,cpp,csharp,golang,java,javascript,python,ruby,swift] - USE_COLOR: true, - COLOR_THEME: 'default', - AUTO_LOGIN: false, - MAX_WORKERS: 10 + autologin: { + enable: false, + retry: 2 + }, + code: { + editor: 'vim', + lang: 'cpp' + }, + file: { + show: '${fid}.${slug}', + submission: '${fid}.${slug}.${sid}.${ac}' + }, + color: { + enable: true, + theme: 'default' + }, + icon: { + theme: '' + }, + network: { + concurrency: 10, + delay: 1 + }, + plugins: {} }; function Config() {} Config.prototype.init = function() { - _.extendOwn(this, DEFAULT_CONFIG); + nconf.file('local', file.configFile()) + .add('global', {type: 'literal', store: DEFAULT_CONFIG}) + .defaults({}); - // check local config: ~/.lcconfig - var localConfig = JSON.parse(h.getFileData(h.getConfigFile())) || {}; - _.extendOwn(this, localConfig); + const cfg = nconf.get(); + nconf.remove('local'); + nconf.remove('global'); + + // HACK: remove old style configs + for (let x in cfg) { + if (x === x.toUpperCase()) delete cfg[x]; + } + delete DEFAULT_CONFIG.type; + delete cfg.type; + + _.extendOwn(this, cfg); +}; + +Config.prototype.getAll = function(useronly) { + const cfg = _.extendOwn({}, this); + if (useronly) delete cfg.sys; + return cfg; }; module.exports = new Config(); diff --git a/lib/core.js b/lib/core.js index b9683558..74362f78 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,176 +1,131 @@ -var fs = require('fs'); -var path = require('path'); +'use strict'; var util = require('util'); var _ = require('underscore'); -var log = require('loglevel'); -var cache = require('./cache'); -var config = require('./config'); -var client = require('./leetcode_client'); +var log = require('./log'); var h = require('./helper'); +var file = require('./file'); +var Plugin = require('./plugin'); + +const core = new Plugin(99999999, 'core', '20170722', 'Plugins manager'); + +core.filters = { + query: { + alias: 'query', + type: 'string', + default: '', + describe: [ + 'Filter questions by condition:', + 'Uppercase means negative', + 'e = easy E = m+h', + 'm = medium M = e+h', + 'h = hard H = e+m', + 'd = done D = not done', + 'l = locked L = non locked', + 's = starred S = not starred' + ].join('\n') + }, + tag: { + alias: 'tag', + type: 'array', + default: [], + describe: 'Filter questions by tag' + } +}; -function saveProblem(problem) { - // it would be better to leave specific problem cache being user - // independent, thus try to reuse existing cache as much as possible - // after changing user. - var p = _.omit(problem, ['locked', 'state', 'starred']); - return cache.set(p.key, p); +function hasTag(o, tag) { + return Array.isArray(o) && o.some(x => x.indexOf(tag.toLowerCase()) >= 0); } -function saveUser(user) { - // when auto login enabled, have to save password to re-login later - // otherwise don't dump password for the sake of security. - var u = _.omit(user, config.AUTO_LOGIN ? [] : ['pass']); - cache.set('.user', u); -} +const isLevel = (x, q) => x.level[0].toLowerCase() === q.toLowerCase(); +const isACed = x => x.state === 'ac'; +const isLocked = x => x.locked; +const isStarred = x => x.starred; + +const QUERY_HANDLERS = { + e: isLevel, + E: _.negate(isLevel), + m: isLevel, + M: _.negate(isLevel), + h: isLevel, + H: _.negate(isLevel), + l: isLocked, + L: _.negate(isLocked), + d: isACed, + D: _.negate(isACed), + s: isStarred, + S: _.negate(isStarred) +}; -var core = {}; +core.filterProblems = function(opts, cb) { + this.getProblems(function(e, problems) { + if (e) return cb(e); -core.getProblems = function(cb) { - var problems = cache.get('all'); - if (problems) { - log.debug('loading from all.json'); - return cb(null, problems); - } + for (let q of (opts.query || '').split('')) { + const f = QUERY_HANDLERS[q]; + if (!f) continue; + problems = problems.filter(x => f(x, q)); + } - client.getProblems(function(e, problems) { - if (e) return cb(e); + for (let t of (opts.tag || [])) { + problems = problems.filter(function(x) { + return x.category === t || + hasTag(x.companies, t) || + hasTag(x.tags, t); + }); + } - cache.set('all', problems); return cb(null, problems); }); }; core.getProblem = function(keyword, cb) { + if (keyword.id) + return core.next.getProblem(keyword, cb); + this.getProblems(function(e, problems) { if (e) return cb(e); keyword = Number(keyword) || keyword; - - var problem = _.find(problems, function(x) { - return x.id === keyword || - x.name === keyword || - x.key === keyword; - }); - if (!problem) - return cb('Problem not found!'); - - var problemDetail = cache.get(problem.key); - if (problemDetail) { - log.debug('loading from ' + problem.key + '.json'); - _.extendOwn(problem, problemDetail); - return cb(null, problem); - } - - client.getProblem(problem, function(e, problem) { - if (e) return cb(e); - - saveProblem(problem); - return cb(null, problem); + const problem = problems.find(function(x) { + return x.fid === keyword || x.name === keyword || x.slug === keyword; }); + if (!problem) return cb('Problem not found!'); + core.next.getProblem(problem, cb); }); }; -core.getSubmissions = function(problem, cb) { - client.getSubmissions(problem, cb); -}; - -core.getSubmission = function(submission, cb) { - client.getSubmission(submission, cb); -}; - -core.testProblem = function(problem, cb) { - client.testProblem(problem, cb); -}; - -core.submitProblem = function(problem, cb) { - client.submitProblem(problem, cb); -}; - -core.updateProblem = function(problem, kv) { - var problems = cache.get('all'); - if (!problems) return false; - - var oldProblem = _.find(problems, function(x) { - return x.id === problem.id; - }); - if (!oldProblem) return false; - - _.extend(oldProblem, kv); - _.extend(problem, kv); - - var singleUpdated = saveProblem(problem); - var allUpdated = cache.set('all', problems); - return singleUpdated && allUpdated; -}; - core.starProblem = function(problem, starred, cb) { if (problem.starred === starred) { log.debug('problem is already ' + (starred ? 'starred' : 'unstarred')); return cb(null, starred); } - client.starProblem(problem, starred, cb); + core.next.starProblem(problem, starred, cb); }; -core.exportProblem = function(problem, f, codeOnly) { - var output = ''; +core.exportProblem = function(problem, opts) { + const data = _.extend({}, problem); - if (codeOnly) { - output = problem.code; - } else { - var input = h.langToCommentStyle(h.extToLang(f)); - // copy problem attrs thus we can render it in template - _.extend(input, problem); - input.percent = input.percent.toFixed(2); - input.testcase = util.inspect(input.testcase || ''); + // unify format before rendering + data.app = require('./config').app || 'leetcode'; + if (!data.fid) data.fid = data.id; + if (!data.lang) data.lang = opts.lang; + data.code = (opts.code || data.code || '').replace(/\r\n/g, '\n'); + data.comment = h.langToCommentStyle(data.lang); + data.percent = data.percent.toFixed(2); + data.testcase = util.inspect(data.testcase || ''); + if (opts.tpl === 'detailed') { // NOTE: wordwrap internally uses '\n' as EOL, so here we have to // remove all '\r' in the raw string. - // FIXME: while in template file we still use '\r\n' for the sake - // of windows, is it really necessary? - var desc = input.desc.replace(/\r\n/g, '\n') - .replace(/^ /mg, '⁠'); - - var wrap = require('wordwrap')(79 - input.commentLine.length); - input.desc = wrap(desc).split('\n'); - - var tpl = fs.readFileSync(path.resolve(__dirname, '../source.tpl'), 'utf-8'); - output = _.template(tpl)(input); + const desc = data.desc.replace(/\r\n/g, '\n').replace(/^ /mg, '⁠'); + const wrap = require('wordwrap')(79 - data.comment.line.length); + data.desc = wrap(desc).split('\n'); } - fs.writeFileSync(f, output); -}; - -core.login = function(user, cb) { - var self = this; - client.login(user, function(e, user) { - if (e) return cb(e); - - self.logout(); - - saveUser(user); - return cb(null, user); - }); -}; - -core.logout = function(user) { - user = this.getUser(); - if (user) { - // NOTE: need invalidate any user related cache - cache.del('.user'); - cache.del('all'); - } - return user; -}; - -core.getUser = function() { - return cache.get('.user'); -}; - -core.isLogin = function() { - return this.getUser() !== null; + return file.render(opts.tpl, data); }; module.exports = core; diff --git a/lib/file.js b/lib/file.js new file mode 100644 index 00000000..51ea21c3 --- /dev/null +++ b/lib/file.js @@ -0,0 +1,166 @@ +'use strict'; +var fs = require('fs'); +var path = require('path'); + +var _ = require('underscore'); +var mkdirp = require('mkdirp'); + +const file = {} + +file.init = function() { + _.templateSettings = { + evaluate: /\{\{(.+?)\}\}/g, + interpolate: /\$\{(.+?)\}/g + }; +}; + +file.isWindows = function() { + return process.platform === 'win32'; +}; + +/// app dirs /// +file.userHomeDir = function() { + return process.env.HOME || process.env.USERPROFILE; +}; + +file.homeDir = function() { + return path.join(this.userHomeDir(), '.lc'); +}; + +file.appDir = function() { + const config = require('./config'); + return path.join(this.homeDir(), config.app || 'leetcode'); +}; + +file.cacheDir = function() { + return path.join(this.appDir(), 'cache'); +}; + +file.codeDir = function(dir) { + return path.join(__dirname, '..', dir || ''); +}; + +/// app files /// +file.cacheFile = function(k) { + return path.join(this.cacheDir(), k + '.json'); +}; + +file.configFile = function() { + return path.join(this.homeDir(), 'config.json'); +}; + +file.pluginFile = function(name) { + return path.join(this.codeDir('lib/plugins'), path.basename(name)); +}; + +file.listCodeDir = function(dir) { + dir = this.codeDir(dir); + return this.list(dir).map(function(f) { + const fullpath = path.join(dir, f); + const ext = path.extname(f); + const name = path.basename(f, ext); + + let data = null; + switch (ext) { + case '.js': data = require(fullpath); break; + case '.json': data = JSON.parse(file.data(fullpath)); break; + } + return {name: name, data: data, file: f}; + }); +}; + +/// general dirs & files /// +file.mkdir = function(fullpath) { + if (fs.existsSync(fullpath)) return; + mkdirp.sync(fullpath); +}; + +file.exist = function(fullpath) { + return fs.existsSync(fullpath); +}; + +file.rm = function(fullpath) { + return fs.unlinkSync(fullpath); +}; + +file.mv = function(src, dst) { + return fs.renameSync(src, dst); +}; + +file.list = function(dir) { + return fs.readdirSync(dir); +}; + +file.stat = function(fullpath) { + return fs.statSync(fullpath); +}; + +file.write = function(fullpath, data) { + return fs.writeFileSync(fullpath, data); +}; + +file.name = function(fullpath) { + return path.basename(fullpath, path.extname(fullpath)); +}; + +file.data = function(fullpath) { + return fs.existsSync(fullpath) ? fs.readFileSync(fullpath).toString() : null; +}; + +/// templates & metadata /// +file.render = function(tpl, data) { + const tplfile = path.join(this.codeDir('templates'), tpl + '.tpl'); + let result = _.template(this.data(tplfile).replace(/\r\n/g, '\n'))(data); + + if (this.isWindows()) { + result = result.replace(/\n/g, '\r\n'); + } else { + result = result.replace(/\r\n/g, '\n'); + } + return result; +}; + +file.fmt = function(format, data) { + return _.template(format)(data); +}; + +file.metaByName = function(filename) { + const m = {}; + + // expect the 1st section in filename as id + // e.g. 1.two-sum.cpp + m.id = file.name(filename).split('.')[0]; + + // HACK: compatible with old ext + if (filename.endsWith('.py3') || filename.endsWith('.python3.py')) + m.lang = 'python3'; + else + m.lang = require('./helper').extToLang(filename); + + return m; +}; + +file.meta = function(filename) { + const m = {}; + + // first look into the file data + const line = this.data(filename).split('\n') + .find(x => x.indexOf(' @lc ') >= 0) || ''; + line.split(' ').forEach(function(x) { + const v = x.split('='); + if (v.length == 2) { + m[v[0]] = v[1].trim(); + } + }); + + // otherwise, look into file name + if (!m.id || !m.lang) { + const olddata = this.metaByName(filename); + m.id = m.id || olddata.id; + m.lang = m.lang || olddata.lang; + } + + return m; +}; + +module.exports = file; diff --git a/lib/helper.js b/lib/helper.js index 6c4df7a9..8806086e 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -1,7 +1,62 @@ -var fs = require('fs'); -var path = require('path'); - -var h = {}; +'use strict'; +var _ = require('underscore'); +var ora = require('ora'); + +var file = require('./file'); + +const UNITS_SIZE = [ + {unit: 'B', name: 'Bytes', count: 1024}, + {unit: 'K', name: 'KBytes', count: 1024}, + {unit: 'M', name: 'MBytes', count: 1024}, + {unit: 'G', name: 'GBytes', count: -1} +]; + +const UNITS_TIME = [ + {unit: 's', name: 'seconds', count: 60}, + {unit: 'm', name: 'minutes', count: 60}, + {unit: 'h', name: 'hours', count: 24}, + {unit: 'd', name: 'days', count: 7}, + {unit: 'w', name: 'weeks', count: 4}, + {unit: 'm', name: 'months', count: 12}, + {unit: 'y', name: 'years', count: -1} +]; + +function getUnit(units, v) { + for (let i = 0; i < units.length; ++i) { + if (units[i].count <= 0 || v < units[i].count) + return [v, units[i]]; + v /= units[i].count; + } +} + +const LANGS = [ + {lang: 'bash', ext: '.sh', style: '#'}, + {lang: 'c', ext: '.c', style: 'c'}, + {lang: 'cpp', ext: '.cpp', style: 'c'}, + {lang: 'csharp', ext: '.cs', style: 'c'}, + {lang: 'golang', ext: '.go', style: 'c'}, + {lang: 'java', ext: '.java', style: 'c'}, + {lang: 'javascript', ext: '.js', style: 'c'}, + {lang: 'kotlin', ext: '.kt', style: 'c'}, + {lang: 'mysql', ext: '.sql', style: '--'}, + {lang: 'php', ext: '.php', style: 'c'}, + {lang: 'python', ext: '.py', style: '#'}, + {lang: 'python3', ext: '.py', style: '#'}, + {lang: 'ruby', ext: '.rb', style: '#'}, + {lang: 'rust', ext: '.rs', style: 'c'}, + {lang: 'scala', ext: '.scala', style: 'c'}, + {lang: 'swift', ext: '.swift', style: 'c'} +]; + +const h = {}; + +h.KEYS = { + user: '../user', + stat: '../stat', + plugins: '../../plugins', + problems: 'problems', + problem: p => p.fid + '.' + p.slug + '.' + p.category +}; h.prettyState = function(state) { switch (state) { @@ -12,14 +67,35 @@ h.prettyState = function(state) { }; h.prettyText = function(text, yesNo) { - var chalk = require('./chalk'); + const chalk = require('./chalk'); + const icon = require('./icon'); switch (yesNo) { - case true: return chalk.green('✔' + text); - case false: return chalk.red('✘' + text); + case true: return chalk.green(icon.yes + text); + case false: return chalk.red(icon.no + text); default: return text; } }; +h.prettySize = function(n) { + const res = getUnit(UNITS_SIZE, n); + return res[0].toFixed(2) + res[1].unit; +}; + +h.prettyTime = function(n) { + const res = getUnit(UNITS_TIME, n); + return res[0].toFixed(0) + ' ' + res[1].name; +}; + +h.prettyLevel = function(level) { + const chalk = require('./chalk'); + switch (level.toLowerCase().trim()) { + case 'easy': return chalk.green(level); + case 'medium': return chalk.yellow(level); + case 'hard': return chalk.red(level); + default: return level; + } +}; + h.levelToName = function(level) { switch (level) { case 1: return 'Easy'; @@ -45,92 +121,40 @@ h.statusToName = function(sc) { }; h.langToExt = function(lang) { - switch (lang) { - case 'c': return '.c'; - case 'cpp': return '.cpp'; - case 'csharp': return '.cs'; - case 'golang': return '.go'; - case 'java': return '.java'; - case 'javascript': return '.js'; - case 'python': return '.py'; - case 'ruby': return '.rb'; - case 'swift': return '.swift'; - default: return '.raw'; - } + const res = LANGS.find(x => x.lang === lang); + return res ? res.ext : '.raw'; }; h.extToLang = function(fullpath) { - var ext = path.extname(fullpath); - switch (ext) { - case '.c': return 'c'; - case '.cpp': return 'cpp'; - case '.cs': return 'csharp'; - case '.go': return 'golang'; - case '.java': return 'java'; - case '.js': return 'javascript'; - case '.py': return 'python'; - case '.rb' : return 'ruby'; - case '.swift': return 'swift'; - default: return 'unknown'; - } + const res = LANGS.find(x => fullpath.endsWith(x.ext)); + return res ? res.lang : 'unknown'; }; h.langToCommentStyle = function(lang) { - switch (lang) { - case 'c': - case 'cpp': - case 'csharp': - case 'golang': - case 'java': - case 'javascript': - case 'swift': - default: - return { - commentHeader: '/*', - commentLine: ' *', - commentFooter: ' */' - }; - case 'python': - case 'ruby': - return { - commentHeader: '#', - commentLine: '#', - commentFooter: '#' - }; - } -}; - -h.getFileData = function(path) { - return fs.existsSync(path) ? fs.readFileSync(path).toString() : null; -}; + const res = LANGS.find(x => x.lang === lang); -h.getFilename = function(fullpath) { - return path.basename(fullpath, path.extname(fullpath)); -}; - -h.getHomeDir = function() { - return process.env.HOME || process.env.USERPROFILE; -}; - -h.getCacheDir = function() { - return this.getHomeDir() + '/.lc/'; -}; - -h.getCacheFile = function(k) { - return this.getCacheDir() + k + '.json'; -}; - -h.getConfigFile = function() { - return this.getHomeDir() + '/.lcconfig'; + return (res && res.style === 'c') ? + {start: '/*', line: ' *', end: ' */'} : + {start: res.style, line: res.style, end: res.style}; }; h.readStdin = function(cb) { - var stdin = process.stdin; - var bufs = []; + const stdin = process.stdin; + const bufs = []; + + console.log('NOTE: to finish the input, press ' + + (file.isWindows() ? ' and ' : '')); stdin.on('readable', function() { - var data = stdin.read(); - if (data) bufs.push(data); + const data = stdin.read(); + if (data) { + // windows doesn't treat ctrl-D as EOF + if (file.isWindows() && data.toString() === '\x04\r\n') { + stdin.emit('end'); + } else { + bufs.push(data); + } + } }); stdin.on('end', function() { cb(null, Buffer.concat(bufs).toString()); @@ -139,17 +163,47 @@ h.readStdin = function(cb) { }; h.getSetCookieValue = function(resp, key) { - var cookies = resp.headers['set-cookie']; + const cookies = resp.headers['set-cookie']; if (!cookies) return null; - for (var i = 0; i < cookies.length; ++i) { - var sections = cookies[i].split(';'); - for (var j = 0; j < sections.length; ++j) { - var kv = sections[j].trim().split('='); + for (let i = 0; i < cookies.length; ++i) { + const sections = cookies[i].split(';'); + for (let j = 0; j < sections.length; ++j) { + const kv = sections[j].trim().split('='); if (kv[0] === key) return kv[1]; } } return null; }; +h.printSafeHTTP = function(msg) { + return msg.replace(/(Cookie\s*:\s*)'.*?'/, '$1') + .replace(/('X-CSRFToken'\s*:\s*)'.*?'/, '$1') + .replace(/('set-cookie'\s*:\s*)\[.*?\]/, '$1'); +}; + +h.spin = function(s) { + return ora(require('./chalk').gray(s)).start(); +}; + +const COLORS = { + blue: {fg: 'white', bg: 'bgBlue'}, + cyan: {fg: 'white', bg: 'bgCyan'}, + gray: {fg: 'white', bg: 'bgGray'}, + green: {fg: 'black', bg: 'bgGreen'}, + magenta: {fg: 'white', bg: 'bgMagenta'}, + red: {fg: 'white', bg: 'bgRed'}, + yellow: {fg: 'black', bg: 'bgYellow'}, + white: {fg: 'black', bg: 'bgWhite'} +}; +h.badge = function(s, color) { + s = ' ' + s + ' '; + if (color === 'random') + color = _.chain(COLORS).keys().sample().value(); + const c = COLORS[color || 'blue']; + + const chalk = require('./chalk'); + return chalk[c.fg][c.bg](s); +}; + module.exports = h; diff --git a/lib/icon.js b/lib/icon.js new file mode 100644 index 00000000..c147a792 --- /dev/null +++ b/lib/icon.js @@ -0,0 +1,31 @@ +'use strict'; +var _ = require('underscore'); + +var file = require('./file'); + +const icons = { + yes: '✔', + no: '✘', + like: '★', + unlike: '☆', + lock: '🔒', + empty: ' ', + ac: '▣', + notac: '▤', + none: '⬚', + + themes: new Map() +}; + +icons.setTheme = function(name) { + const defaultName = file.isWindows() ? 'win7' : 'default'; + const theme = this.themes.get(name) || this.themes.get(defaultName) || {}; + _.extendOwn(this, theme); +}; + +icons.init = function() { + for (let f of file.listCodeDir('icons')) + icons.themes.set(f.name, f.data); +}; + +module.exports = icons; diff --git a/lib/leetcode_client.js b/lib/leetcode_client.js deleted file mode 100644 index 7b6373f7..00000000 --- a/lib/leetcode_client.js +++ /dev/null @@ -1,330 +0,0 @@ -var _ = require('underscore'); -var cheerio = require('cheerio'); -var he = require('he'); -var log = require('loglevel'); -var request = require('request'); - -var config = require('./config'); -var h = require('./helper'); - -// update options with user credentials -function signOpts(opts, user) { - opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId + - ';csrftoken=' + user.sessionCSRF + ';'; - opts.headers['X-CSRFToken'] = user.sessionCSRF; - opts.headers['X-Requested-With'] = 'XMLHttpRequest'; -} - -function makeOpts(url) { - var opts = {url: url, headers: {}, _expectedStatus: 200}; - var core = require('./core'); - if (core.isLogin()) signOpts(opts, core.getUser()); - return opts; -} - -function checkError(e, resp, expectedStatus, msg) { - if (e) return e; - - if (resp && resp.statusCode !== expectedStatus) { - if (resp.statusCode === 403) { - msg = msg || 'session expired, please login again'; - - var core = require('./core'); - core.logout(); - } - - return { - msg: msg || 'http error', - statusCode: resp.statusCode - }; - } -} - -// leetcode.com is limiting one session alive in the same time, -// which means once you login on web, your cli session will get -// expired immediately. In that case we will try to re-login in -// the backend to give a seamless user experience. -function requestWithReLogin(opts, cb) { - if (!config.AUTO_LOGIN) - return request(opts, cb); - - var core = require('./core'); - var user = core.getUser(); - - request(opts, function(e, resp, body) { - e = checkError(e, resp, opts._expectedStatus); - - // not 403: transparently pass down - if (!e || e.statusCode !== 403) - return cb(e, resp, body); - - // if 403: try re-login - log.debug('session expired, auto re-login...'); - - core.login(user, function(e2, user) { - if (e2) return cb(e, resp, body); - - log.debug('login successfully, cont\'d...'); - signOpts(opts, user); - - request(opts, cb); - }); - }); -} - -var leetcodeClient = {}; - -leetcodeClient.getProblems = function(cb) { - var opts = makeOpts(config.URL_PROBLEMS); - - requestWithReLogin(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - var json = JSON.parse(body); - - // leetcode permits anonymous access to the problem list - // while we require login first to make a better experience. - if (json.user_name.length === 0) - return cb('session expired, please login again'); - - var problems = json.stat_status_pairs - .filter(function(p) { - return !p.stat.question__hide; - }) - .map(function(p) { - return { - state: p.status || 'None', - id: p.stat.question_id, - name: p.stat.question__title, - key: p.stat.question__title_slug, - link: config.URL_PROBLEM.replace('$id', p.stat.question__title_slug), - locked: p.paid_only, - percent: p.stat.total_acs * 100 / p.stat.total_submitted, - level: h.levelToName(p.difficulty.level), - starred: p.is_favor - }; - }); - - return cb(null, problems); - }); -}; - -// hacking ;P -var aceCtrl = { - init: function() { - return Array.prototype.slice.call(arguments); - } -}; - -leetcodeClient.getProblem = function(problem, cb) { - var opts = makeOpts(); - opts.url = problem.link; - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - // FIXME: if session expired, this will still return 200 - if (e) return cb(e); - - var $ = cheerio.load(body); - var info = $('div[class="question-info text-info"] ul li strong'); - - problem.totalAC = $(info[0]).text(); - problem.totalSubmit = $(info[1]).text(); - problem.desc = $('meta[property="og:description"]').attr('content'); - problem.desc = he.decode(problem.desc); - - var pageData; - var r = /(var pageData[^;]+;)/m; - var result = body.match(r); - if (!result) - return cb('failed to load' + (problem.locked ? ' locked ' : ' ') + - 'problem!'); - - eval(result[1]); - problem.templates = pageData.codeDefinition; - problem.testcase = pageData.sampleTestCase; - problem.testable = pageData.enableRunCode; - - return cb(null, problem); - }); -}; - -leetcodeClient.getSubmissions = function(problem, cb) { - var opts = makeOpts(); - opts.url = config.URL_SUBMISSIONS.replace('$key', problem.key); - opts.headers.Referer = config.URL_PROBLEM.replace('$id', problem.key); - - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - // FIXME: if session expired, this will still return 200 - if (e) return cb(e); - - // FIXME: this only return the 1st 20 submissions, we should get next if necessary. - var submissions = JSON.parse(body).submissions_dump; - _.each(submissions, function(submission) { - submission.id = _.last(_.compact(submission.url.split('/'))); - }); - - return cb(null, submissions); - }); -}; - -leetcodeClient.getSubmission = function(submission, cb) { - var opts = makeOpts(); - opts.url = config.URL_SUBMISSION.replace('$id', submission.id); - - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - var re = body.match(/submissionCode:\s('[^']*')/); - if (re) { - submission.code = eval(re[1]); - } - return cb(null, submission); - }); -}; - -leetcodeClient.login = function(user, cb) { - request(config.URL_LOGIN, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); - - var opts = { - url: config.URL_LOGIN, - headers: { - Origin: config.URL_BASE, - Referer: config.URL_LOGIN, - Cookie: 'csrftoken=' + user.loginCSRF + ';' - }, - form: { - csrfmiddlewaretoken: user.loginCSRF, - login: user.login, - password: user.pass - } - }; - request.post(opts, function(e, resp, body) { - e = checkError(e, resp, 302, 'invalid password?'); - if (e) return cb(e); - - user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); - user.sessionId = h.getSetCookieValue(resp, 'LEETCODE_SESSION'); - user.name = h.getSetCookieValue(resp, 'messages') - .match('Successfully signed in as ([^.]*)')[1]; - - return cb(null, user); - }); - }); -}; - -function verifyResult(opts, jobs, results, cb) { - if (jobs.length === 0) - return cb(null, results); - - opts.method = 'GET'; - opts.url = config.URL_VERIFY.replace('$id', jobs[0].id); - - requestWithReLogin(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - var result = JSON.parse(body); - if (result.state === 'SUCCESS') { - result.name = jobs[0].name; - results.push(result); - jobs.shift(); - } - - setImmediate(verifyResult, opts, jobs, results, cb); - }); -} - -function runCode(opts, problem, cb) { - opts.method = 'POST'; - opts.headers.Origin = config.URL_BASE; - opts.headers.Referer = problem.link; - opts.json = true; - opts._delay = opts._delay || 1; // in seconds - - opts.body = opts.body || {}; - _.extendOwn(opts.body, { - 'lang': h.extToLang(problem.file), - 'question_id': parseInt(problem.id, 10), - 'test_mode': false, - 'typed_code': h.getFileData(problem.file) - }); - - requestWithReLogin(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - if (body.error) { - if (body.error.indexOf('run code too soon') < 0) - return cb(body.error); - - // hit 'run code too soon' error, have to wait a bit - log.debug(body.error); - - // linear wait - ++opts._delay; - log.debug('Will retry after %d seconds...', opts._delay); - - var reRun = _.partial(runCode, opts, problem, cb); - return setTimeout(reRun, opts._delay * 1000); - } - - opts.json = false; - opts.body = null; - - return cb(null, body); - }); -} - -leetcodeClient.testProblem = function(problem, cb) { - var opts = makeOpts(); - opts.url = config.URL_TEST.replace('$key', problem.key); - opts.body = {'data_input': problem.testcase}; - - runCode(opts, problem, function(e, task) { - if (e) return cb(e); - - var jobs = [ - {name: 'Your', id: task.interpret_id}, - {name: 'Expected', id: task.interpret_expected_id} - ]; - verifyResult(opts, jobs, [], cb); - }); -}; - -leetcodeClient.submitProblem = function(problem, cb) { - var opts = makeOpts(); - opts.url = config.URL_SUBMIT.replace('$key', problem.key); - opts.body = {'judge_type': 'large'}; - - runCode(opts, problem, function(e, task) { - if (e) return cb(e); - - var jobs = [{name: 'Your', id: task.submission_id}]; - verifyResult(opts, jobs, [], cb); - }); -}; - -leetcodeClient.starProblem = function(problem, starred, cb) { - var opts = makeOpts(config.URL_STAR); - opts.method = (starred ? 'POST' : 'DELETE'); - opts.headers.Origin = config.URL_BASE; - opts.headers.Referer = problem.link; - opts.json = true; - opts.body = {'qid': problem.id}; - - requestWithReLogin(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); - - cb(null, body.is_favor); - }); -}; - -module.exports = leetcodeClient; diff --git a/lib/log.js b/lib/log.js new file mode 100644 index 00000000..394b356c --- /dev/null +++ b/lib/log.js @@ -0,0 +1,63 @@ +'use strict'; +var _ = require('underscore'); + +var chalk = require('./chalk'); +var sprintf = require('./sprintf'); + +const log = { + output: _.bind(console.log, console), + level: null, + levels: new Map([ + ['TRACE', {value: 0, color: 'gray'}], + ['DEBUG', {value: 1, color: 'gray'}], + ['INFO', {value: 2, color: ''}], + ['WARN', {value: 3, color: 'yellow'}], + ['ERROR', {value: 4, color: 'red'}] + ]) +}; + +log.setLevel = function(name) { + this.level = this.levels.get(name) || this.levels.get('INFO'); +}; + +log.isEnabled = function(name) { + return this.level.value <= this.levels.get(name).value; +}; + +log.fail = function(e) { + let msg = sprintf('%s', (e.msg || e)); + if (e.statusCode) { + msg += sprintf(' [code=%s]', e.statusCode); + } + log.error(msg); +}; + +log.fatal = function(e) { + log.error(e); + process.exit(1); +}; + +log.printf = function() { + log.info(sprintf.apply(null, Array.from(arguments))); +}; + +log.init = function() { + this.setLevel('INFO'); + + for (let name of this.levels.keys()) { + log[name.toLowerCase()] = function() { + const level = log.levels.get(name); + if (log.level.value > level.value) return; + + const args = Array.from(arguments); + if (name !== 'INFO') args.unshift('[' + name + ']'); + + let s = args.map(x => x.toString()).join(' '); + if (level.color) s = chalk[level.color](s); + + this.output(s); + }; + } +}; + +module.exports = log; diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100644 index 00000000..bbd6da44 --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,216 @@ +'use strict'; +var cp = require('child_process'); +var fs = require('fs'); +var path = require('path'); + +var _ = require('underscore'); +var request = require('request'); + +var h = require('./helper'); +var file = require('./file'); +var cache = require('./cache'); +var config = require('./config'); +var log = require('./log'); +var Queue = require('./queue'); + +function Plugin(id, name, ver, desc, deps) { + this.id = id; + this.name = name; + this.ver = ver || 'default'; + this.desc = desc || ''; + + this.enabled = true; + this.deleted = false; + this.missing = (this.ver === 'missing'); + this.builtin = (this.ver === 'default'); + + // only need deps for current platform + this.deps = _.chain(deps || []) + .filter(x => ! x.includes(':') || x.includes(':' + process.platform)) + .map(x => x.split(':')[0]) + .value(); +} + +Plugin.prototype.init = function() { + this.config = config.plugins[this.name] || {}; + this.next = null; +}; + +Plugin.prototype.setNext = function(next) { + Object.setPrototypeOf(this, next); + this.next = next; +}; + +Plugin.prototype.delete = function() { + if (!this.missing) { + try { + const fullpath = file.pluginFile(this.file); + file.rm(fullpath); + } catch(e) { + return log.error(e.message); + } + } + this.deleted = true; +}; + +Plugin.prototype.save = function() { + const stats = cache.get(h.KEYS.plugins) || {}; + + if (this.deleted) delete stats[this.name]; + else if (this.missing) return; + else stats[this.name] = this.enabled; + + cache.set(h.KEYS.plugins, stats); +}; + +Plugin.prototype.install = function(cb) { + if (this.deps.length === 0) return cb(); + + const cmd = 'npm install --save ' + this.deps.join(' '); + log.debug(cmd); + const spin = h.spin(cmd); + cp.exec(cmd, {cwd: file.codeDir()}, function(e) { + spin.stop(); + return cb(e); + }); +}; + +Plugin.prototype.help = function() {}; + +Plugin.plugins = []; + +Plugin.init = function(head) { + log.trace('initializing all plugins'); + head = head || require('./core'); + + const stats = cache.get(h.KEYS.plugins) || {}; + + // 1. find installed plugins + let installed = []; + for (let f of file.listCodeDir('lib/plugins')) { + const p = f.data; + if (!p) continue; + log.trace('found plugin: ' + p.name + '=' + p.ver); + + p.file = f.file; + p.enabled = stats[p.name]; + + if (!(p.name in stats)) { + if (p.builtin) { + log.trace('new builtin plugin, enable by default'); + p.enabled = true; + } else { + log.trace('new 3rd party plugin, disable by default'); + p.enabled = false; + } + } + installed.push(p); + } + // the one with bigger `id` comes first + installed = _.sortBy(installed, x => -x.id); + + // 2. init all in reversed order + for (let i = installed.length - 1; i >= 0; --i) { + const p = installed[i]; + if (p.enabled) { + p.init(); + log.trace('inited plugin: ' + p.name); + } else { + log.trace('skipped plugin: ' + p.name); + } + } + + // 3. chain together + const plugins = installed.filter(x => x.enabled); + let last = head; + for (let p of plugins) { + last.setNext(p); + last = p; + } + + // 4. check missing plugins + const missings = []; + for (let k of _.keys(stats)) { + if (installed.find(x => x.name === k)) continue; + const p = new Plugin(-1, k, 'missing'); + p.enabled = stats[k]; + missings.push(p); + log.trace('missing plugin:' + p.name); + } + + Plugin.plugins = installed.concat(missings); + return missings.length === 0; +}; + +Plugin.copy = function(src, cb) { + // FIXME: remove local file support? + if (path.extname(src) !== '.js') { + src = config.sys.urls.plugin.replace('$name', src); + } + const dst = file.pluginFile(src); + + const srcstream = src.startsWith('https://') ? request(src) : fs.createReadStream(src); + const dststream = fs.createWriteStream(dst); + let error; + + srcstream.on('response', function(resp) { + if (resp.statusCode !== 200) + srcstream.emit('error', 'HTTP Error: ' + resp.statusCode); + }); + srcstream.on('error', function(e) { + dststream.emit('error', e); + }); + + dststream.on('error', function(e) { + error = e; + dststream.end(); + }); + dststream.on('close', function() { + spin.stop(); + if (error) file.rm(dst); + return cb(error, dst); + }); + + log.debug('copying from ' + src); + const spin = h.spin('Downloading ' + src); + srcstream.pipe(dststream); +}; + +Plugin.install = function(name, cb) { + Plugin.copy(name, function(e, fullpath) { + if (e) return cb(e); + log.debug('copied to ' + fullpath); + + const p = require(fullpath); + p.file = path.basename(fullpath); + p.install(function() { + return cb(null, p); + }); + }); +}; + +Plugin.installMissings = function(cb) { + function doTask(plugin, queue, cb) { + Plugin.install(plugin.name, function(e, p) { + if (!e) { + p.enabled = plugin.enabled; + p.save(); + p.help(); + } + return cb(e, p); + }); + } + + const missings = Plugin.plugins.filter(x => x.missing); + if (missings.length === 0) return cb(); + + log.warn('Installing missing plugins, might take a while ...'); + const q = new Queue(missings, {}, doTask); + q.run(1, cb); +}; + +Plugin.save = function() { + for (let p of this.plugins) p.save(); +}; + +module.exports = Plugin; diff --git a/lib/plugins/cache.js b/lib/plugins/cache.js new file mode 100644 index 00000000..677c6c84 --- /dev/null +++ b/lib/plugins/cache.js @@ -0,0 +1,80 @@ +'use strict'; +var _ = require('underscore'); + +var cache = require('../cache'); +var h = require('../helper'); +var log = require('../log'); +var Plugin = require('../plugin'); +var session = require('../session'); + +const plugin = new Plugin(50, 'cache', '', 'Plugin to provide local cache.'); + +plugin.getProblems = function(cb) { + const problems = cache.get(h.KEYS.problems); + if (problems) { + log.debug('cache hit: problems.json'); + return cb(null, problems); + } + + plugin.next.getProblems(function(e, problems) { + if (e) return cb(e); + + cache.set(h.KEYS.problems, problems); + return cb(null, problems); + }); +}; + +plugin.getProblem = function(problem, cb) { + const k = h.KEYS.problem(problem); + const _problem = cache.get(k); + if (_problem) { + log.debug('cache hit: ' + k + '.json'); + _.extendOwn(problem, _problem); + return cb(null, problem); + } + + plugin.next.getProblem(problem, function(e, _problem) { + if (e) return cb(e); + + plugin.saveProblem(_problem); + return cb(null, _problem); + }); +}; + +plugin.saveProblem = function(problem) { + // it would be better to leave specific problem cache being user + // independent, thus try to reuse existing cache as much as possible + // after changing user. + const _problem = _.omit(problem, ['locked', 'state', 'starred']); + return cache.set(h.KEYS.problem(problem), _problem); +}; + +plugin.updateProblem = function(problem, kv) { + const problems = cache.get(h.KEYS.problems); + if (!problems) return false; + + const _problem = problems.find(x => x.id === problem.id); + if (!_problem) return false; + + _.extend(_problem, kv); + return cache.set(h.KEYS.problems, problems); +}; + +plugin.login = function(user, cb) { + this.logout(user, false); + plugin.next.login(user, function(e, user) { + if (e) return cb(e); + session.saveUser(user); + return cb(null, user); + }); +}; + +plugin.logout = function(user, purge) { + if (!user) user = session.getUser(); + if (purge) session.deleteUser(); + // NOTE: need invalidate any user related cache + session.deleteCodingSession(); + return user; +}; + +module.exports = plugin; diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js new file mode 100644 index 00000000..24331ec6 --- /dev/null +++ b/lib/plugins/leetcode.js @@ -0,0 +1,532 @@ +'use strict'; +var util = require('util'); + +var _ = require('underscore'); +var cheerio = require('cheerio'); +var he = require('he'); +var request = require('request'); + +var config = require('../config'); +var h = require('../helper'); +var file = require('../file'); +var log = require('../log'); +var Plugin = require('../plugin'); +var Queue = require('../queue'); +var session = require('../session'); + +const plugin = new Plugin(10, 'leetcode', '', + 'Plugin to talk with leetcode APIs.'); + +var spin; + +// update options with user credentials +plugin.signOpts = function(opts, user) { + opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId + + ';csrftoken=' + user.sessionCSRF + ';'; + opts.headers['X-CSRFToken'] = user.sessionCSRF; + opts.headers['X-Requested-With'] = 'XMLHttpRequest'; +}; + +plugin.makeOpts = function(url) { + const opts = {}; + opts.url = url; + opts.headers = {}; + + if (session.isLogin()) + plugin.signOpts(opts, session.getUser()); + return opts; +}; + +plugin.checkError = function(e, resp, expectedStatus) { + if (!e && resp && resp.statusCode !== expectedStatus) { + const code = resp.statusCode; + log.debug('http error: ' + code); + + if (code === 403 || code === 401) { + e = session.errors.EXPIRED; + } else { + e = {msg: 'http error', statusCode: code}; + } + } + return e; +}; + +plugin.init = function() { + config.app = 'leetcode'; +} + +plugin.getProblems = function(cb) { + log.debug('running leetcode.getProblems'); + let problems = []; + const getCategory = function(category, queue, cb) { + plugin.getCategoryProblems(category, function(e, _problems) { + if (e) { + log.debug(category + ': failed to getProblems: ' + e.msg); + } else { + log.debug(category + ': getProblems got ' + _problems.length + ' problems'); + problems = problems.concat(_problems); + } + return cb(e); + }); + }; + + spin = h.spin('Downloading problems'); + const q = new Queue(config.sys.categories, {}, getCategory); + q.run(null, function(e) { + spin.stop(); + return cb(e, problems); + }); +}; + +plugin.getCategoryProblems = function(category, cb) { + log.debug('running leetcode.getCategoryProblems: ' + category); + const opts = plugin.makeOpts(config.sys.urls.problems.replace('$category', category)); + + spin.text = 'Downloading category ' + category; + request(opts, function(e, resp, body) { + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + const json = JSON.parse(body); + + // leetcode permits anonymous access to the problem list + // while we require login first to make a better experience. + if (json.user_name.length === 0) { + log.debug('no user info in list response, maybe session expired...'); + return cb(session.errors.EXPIRED); + } + + const problems = json.stat_status_pairs + .filter(p => !p.stat.question__hide) + .map(function(p) { + return { + state: p.status || 'None', + id: p.stat.question_id, + fid: p.stat.frontend_question_id, + name: p.stat.question__title, + slug: p.stat.question__title_slug, + link: config.sys.urls.problem.replace('$slug', p.stat.question__title_slug), + locked: p.paid_only, + percent: p.stat.total_acs * 100 / p.stat.total_submitted, + level: h.levelToName(p.difficulty.level), + starred: p.is_favor, + category: json.category_slug + }; + }); + + return cb(null, problems); + }); +}; + +plugin.getProblem = function(problem, cb) { + log.debug('running leetcode.getProblem'); + const user = session.getUser(); + if (problem.locked && !user.paid) return cb('failed to load locked problem!'); + + const opts = plugin.makeOpts(config.sys.urls.graphql); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; + + opts.json = true; + opts.body = { + query: [ + 'query getQuestionDetail($titleSlug: String!) {', + ' question(titleSlug: $titleSlug) {', + ' content', + ' stats', + ' codeDefinition', + ' sampleTestCase', + ' enableRunCode', + ' metaData', + ' translatedContent', + ' }', + '}' + ].join('\n'), + variables: {titleSlug: problem.slug}, + operationName: 'getQuestionDetail' + }; + + const spin = h.spin('Downloading ' + problem.slug); + request.post(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + const q = body.data.question; + if (!q) return cb('failed to load problem!'); + + problem.totalAC = JSON.parse(q.stats).totalAccepted; + problem.totalSubmit = JSON.parse(q.stats).totalSubmission; + + let content = q.translatedContent ? q.translatedContent : q.content; + // Replace with '^' as the power operator + content = content.replace(/<\/sup>/gm, '').replace(//gm, '^'); + problem.desc = he.decode(cheerio.load(content).root().text()); + + problem.templates = JSON.parse(q.codeDefinition); + problem.testcase = q.sampleTestCase; + problem.testable = q.enableRunCode; + problem.templateMeta = JSON.parse(q.metaData); + // @si-yao: seems below property is never used. + //problem.discuss = q.discussCategoryId; + + return cb(null, problem); + }); +}; + +function runCode(opts, problem, cb) { + opts.method = 'POST'; + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; + opts.json = true; + opts._delay = opts._delay || config.network.delay || 1; // in seconds + + opts.body = opts.body || {}; + _.extendOwn(opts.body, { + lang: problem.lang, + question_id: parseInt(problem.id, 10), + test_mode: false, + typed_code: file.data(problem.file) + }); + + const spin = h.spin('Sending code to judge'); + request(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + if (body.error) { + if (!body.error.includes('too soon')) + return cb(body.error); + + // hit 'run code too soon' error, have to wait a bit + log.debug(body.error); + + // linear wait + ++opts._delay; + log.debug('Will retry after %d seconds...', opts._delay); + + const reRun = _.partial(runCode, opts, problem, cb); + return setTimeout(reRun, opts._delay * 1000); + } + + opts.json = false; + opts.body = null; + + return cb(null, body); + }); +} + +function verifyResult(task, queue, cb) { + const opts = queue.ctx.opts; + opts.method = 'GET'; + opts.url = config.sys.urls.verify.replace('$id', task.id); + + const spin = h.spin('Waiting for judge result'); + request(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + let result = JSON.parse(body); + if (result.state === 'SUCCESS') { + result = formatResult(result); + _.extendOwn(result, task); + queue.ctx.results.push(result); + } else { + queue.addTask(task); + } + return cb(); + }); +} + +function formatResult(result) { + const x = { + ok: result.run_success, + answer: result.code_answer || '', + runtime: result.status_runtime || '', + state: h.statusToName(result.status_code), + testcase: util.inspect(result.input || result.last_testcase || ''), + passed: result.total_correct || 0, + total: result.total_testcases || 0 + }; + + x.error = _.chain(result) + .pick((v, k) => /_error$/.test(k) && v.length > 0) + .values() + .value(); + + if (result.judge_type === 'large') { + x.answer = result.code_output; + x.expected_answer = result.expected_output; + x.stdout = result.std_output; + } else { + if (typeof(result.code_output) === 'string') { + x.stdout = util.inspect(result.code_output); + } else if (Array.isArray(result.code_output)) { + x.stdout = util.inspect(result.code_output.join('\n')); + } else { + x.stdout = util.inspect(''); + } + } + + // make sure we pass eveything! + if (x.passed !== x.total) x.ok = false; + if (x.state !== 'Accepted') x.ok = false; + if (x.error.length > 0) x.ok = false; + + return x; +} + +plugin.testProblem = function(problem, cb) { + log.debug('running leetcode.testProblem'); + const opts = plugin.makeOpts(config.sys.urls.test.replace('$slug', problem.slug)); + opts.body = {data_input: problem.testcase}; + + runCode(opts, problem, function(e, task) { + if (e) return cb(e); + + const tasks = [ + {type: 'Actual', id: task.interpret_id}, + {type: 'Expected', id: task.interpret_expected_id} + ]; + const q = new Queue(tasks, {opts: opts, results: []}, verifyResult); + q.run(null, function(e, ctx) { + return cb(e, ctx.results); + }); + }); +}; + +plugin.submitProblem = function(problem, cb) { + log.debug('running leetcode.submitProblem'); + const opts = plugin.makeOpts(config.sys.urls.submit.replace('$slug', problem.slug)); + opts.body = {judge_type: 'large'}; + + runCode(opts, problem, function(e, task) { + if (e) return cb(e); + + const tasks = [{type: 'Actual', id: task.submission_id}]; + const q = new Queue(tasks, {opts: opts, results: []}, verifyResult); + q.run(null, function(e, ctx) { + return cb(e, ctx.results); + }); + }); +}; + +plugin.getSubmissions = function(problem, cb) { + log.debug('running leetcode.getSubmissions'); + const opts = plugin.makeOpts(config.sys.urls.submissions.replace('$slug', problem.slug)); + opts.headers.Referer = config.sys.urls.problem.replace('$slug', problem.slug); + + request(opts, function(e, resp, body) { + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + // FIXME: this only return the 1st 20 submissions, we should get next if necessary. + const submissions = JSON.parse(body).submissions_dump; + for (let submission of submissions) + submission.id = _.last(_.compact(submission.url.split('/'))); + + return cb(null, submissions); + }); +}; + +plugin.getSubmission = function(submission, cb) { + log.debug('running leetcode.getSubmission'); + const opts = plugin.makeOpts(config.sys.urls.submission.replace('$id', submission.id)); + + request(opts, function(e, resp, body) { + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + let re = body.match(/submissionCode:\s('[^']*')/); + if (re) submission.code = eval(re[1]); + + re = body.match(/runtimeDistributionFormatted:\s('[^']+')/); + if (re) submission.distributionChart = JSON.parse(eval(re[1])); + return cb(null, submission); + }); +}; + +plugin.starProblem = function(problem, starred, cb) { + log.debug('running leetcode.starProblem'); + const opts = plugin.makeOpts(); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; + + const user = session.getUser(); + if (starred) { + opts.url = config.sys.urls.favorites; + opts.method = 'POST'; + opts.json = true; + opts.body = { + favorite_id_hash: user.hash, + question_id: problem.id + }; + } else { + opts.url = config.sys.urls.favorite_delete + .replace('$hash', user.hash) + .replace('$id', problem.id); + opts.method = 'DELETE'; + } + + request(opts, function(e, resp, body) { + e = plugin.checkError(e, resp, 204); + if (e) return cb(e); + + cb(null, starred); + }); +}; + +plugin.getFavorites = function(cb) { + log.debug('running leetcode.getFavorites'); + const opts = plugin.makeOpts(config.sys.urls.favorites); + + const spin = h.spin('Retrieving user favorites'); + request(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + const favorites = JSON.parse(body); + return cb(null, favorites); + }); +}; + +plugin.getUserInfo = function(cb) { + log.debug('running leetcode.getUserInfo'); + const opts = plugin.makeOpts(config.sys.urls.graphql); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = config.sys.urls.base; + opts.json = true; + opts.body = { + query: [ + '{', + ' user {', + ' username', + ' isCurrentUserPremium', + ' }', + '}' + ].join('\n'), + variables: {} + }; + + const spin = h.spin('Retrieving user profile'); + request.post(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + const user = body.data.user; + return cb(null, user); + }); +}; + +function runSession(method, data, cb) { + const opts = plugin.makeOpts(config.sys.urls.session); + opts.json = true; + opts.method = method; + opts.body = data; + + const spin = h.spin('Waiting session result'); + request(opts, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e && e.statusCode === 302) e = session.errors.EXPIRED; + + return e ? cb(e) : cb(null, body.sessions); + }); +} + +plugin.getSessions = function(cb) { + log.debug('running leetcode.getSessions'); + runSession('POST', {}, cb); +}; + +plugin.activateSession = function(session, cb) { + log.debug('running leetcode.activateSession'); + const data = {func: 'activate', target: session.id}; + runSession('PUT', data, cb); +}; + +plugin.createSession = function(name, cb) { + log.debug('running leetcode.createSession'); + const data = {func: 'create', name: name}; + runSession('PUT', data, cb); +}; + +plugin.deleteSession = function(session, cb) { + log.debug('running leetcode.deleteSession'); + const data = {target: session.id}; + runSession('DELETE', data, cb); +}; + +plugin.signin = function(user, cb) { + log.debug('running leetcode.signin'); + const spin = h.spin('Signing in leetcode.com'); + request(config.sys.urls.login, function(e, resp, body) { + spin.stop(); + e = plugin.checkError(e, resp, 200); + if (e) return cb(e); + + user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); + + const opts = { + url: config.sys.urls.login, + headers: { + Origin: config.sys.urls.base, + Referer: config.sys.urls.login, + Cookie: 'csrftoken=' + user.loginCSRF + ';' + }, + form: { + csrfmiddlewaretoken: user.loginCSRF, + login: user.login, + password: user.pass + } + }; + request.post(opts, function(e, resp, body) { + if (e) return cb(e); + if (resp.statusCode !== 302) return cb('invalid password?'); + + user.sessionCSRF = h.getSetCookieValue(resp, 'csrftoken'); + user.sessionId = h.getSetCookieValue(resp, 'LEETCODE_SESSION'); + session.saveUser(user); + return cb(null, user); + }); + }); +}; + +plugin.getUser = function(user, cb) { + plugin.getFavorites(function(e, favorites) { + if (!e) { + const f = favorites.favorites.private_favorites.find(f => f.name === 'Favorite'); + if (f) { + user.hash = f.id_hash; + user.name = favorites.user_name; + } else { + log.warn('Favorite not found?'); + } + } else { + log.warn('Failed to retrieve user favorites: ' + e); + } + + plugin.getUserInfo(function(e, _user) { + if (!e) { + user.paid = _user.isCurrentUserPremium; + user.name = _user.username; + } + session.saveUser(user); + return cb(null, user); + }); + }); +}; + +plugin.login = function(user, cb) { + log.debug('running leetcode.login'); + plugin.signin(user, function(e, user) { + if (e) return cb(e); + plugin.getUser(user, cb); + }); +}; + +module.exports = plugin; diff --git a/lib/plugins/retry.js b/lib/plugins/retry.js new file mode 100644 index 00000000..5dcf35e0 --- /dev/null +++ b/lib/plugins/retry.js @@ -0,0 +1,84 @@ +'use strict'; +var config = require('../config'); +var log = require('../log'); +var Plugin = require('../plugin'); +var session = require('../session'); + +var plugin = new Plugin(30, 'retry', '', + 'Plugin to retry last failed request if autologin.enable is on.'); + +const count = {}; + +function canRetry(e, name) { + return config.autologin.enable && + (e === session.errors.EXPIRED) && + (count[name] || 0) < config.autologin.retry; +} + +plugin.init = function() { + const names = [ + 'activateSession', + 'createSession', + 'deleteSession', + 'getProblems', + 'getProblem', + 'getSessions', + 'getSubmissions', + 'getSubmission', + 'getFavorites', + 'testProblem', + 'submitProblem', + 'starProblem' + ]; + + for (let name of names) { + count[name] = 0; + plugin[name] = function() { + const args = Array.from(arguments); + const cb = args.pop(); + + const _cb = function() { + const results = Array.from(arguments); + const e = results[0]; + if (!canRetry(e, name)) { + count[name] = 0; + return cb.apply(null, results); + } + + ++count[name]; + plugin.relogin(function() { + // for now we don't care result, just blindly retry + plugin[name].apply(plugin, args.concat(cb)); + }); + }; + + const next = plugin.next; + next[name].apply(next, args.concat(_cb)); + }; + } +}; + +// leetcode.com is limiting one session alive in the same time, +// which means once you login on web, your cli session will get +// expired immediately. In that case we will try to re-login in +// the backend to give a seamless user experience. +plugin.relogin = function(cb) { + log.debug('session expired, try to re-login...'); + + const user = session.getUser(); + if (!user) { + log.debug('relogin failed: no user found, please login again'); + return cb(); + } + + this.login(user, function(e) { + if (e) { + log.debug('login failed:' + e.msg); + } else { + log.debug('login successfully, cont\'d...'); + } + return cb(); + }); +}; + +module.exports = plugin; diff --git a/lib/queue.js b/lib/queue.js index e1427957..68c24ce3 100644 --- a/lib/queue.js +++ b/lib/queue.js @@ -1,33 +1,51 @@ +'use strict'; +var _ = require('underscore'); + var config = require('./config'); -var queue = {}; +function Queue(tasks, ctx, onTask) { + this.tasks = _.clone(tasks) || []; + this.ctx = ctx || {}; + this.onTask = onTask; + this.error = null; +} + +Queue.prototype.addTask = function(task) { + this.tasks.push(task); + return this; +}; + +Queue.prototype.addTasks = function(tasks) { + this.tasks = this.tasks.concat(tasks); + return this; +}; + +Queue.prototype.run = function(concurrency, onDone) { + this.concurrency = concurrency || config.network.concurrency || 1; + this.onDone = onDone; -function startWorker(ctx) { + const self = this; + for (let i = 0; i < this.concurrency; ++i) { + setImmediate(function() { self.workerRun(); }); + } +}; + +Queue.prototype.workerRun = function() { // no more tasks, quit now - if (ctx.tasks.length === 0) { - if (--ctx.workers === 0 && ctx.cb) - ctx.cb(); + if (this.tasks.length === 0) { + if (--this.concurrency === 0 && this.onDone) + this.onDone(this.error, this.ctx); return; } - var task = ctx.tasks.shift(); - ctx.doTask(task, function(e) { + const task = this.tasks.shift(); + const self = this; + this.onTask(task, self, function(e) { + if (e) self.error = e; + // TODO: could retry failed task here. - startWorker(ctx); + setImmediate(function() { self.workerRun(); }); }); -} - -queue.run = function(tasks, doTask, cb) { - var ctx = { - tasks: tasks, - doTask: doTask, - cb: cb, - workers: config.MAX_WORKERS - }; - - for (var i = 0; i < ctx.workers; ++i) { - startWorker(ctx); - } }; -module.exports = queue; +module.exports = Queue; diff --git a/lib/session.js b/lib/session.js new file mode 100644 index 00000000..6e70ed90 --- /dev/null +++ b/lib/session.js @@ -0,0 +1,58 @@ +'use strict'; +var moment = require('moment'); +var _ = require('underscore'); + +var cache = require('./cache'); +var config = require('./config'); +var h = require('./helper'); + +const session = {}; + +session.errors = { + EXPIRED: { + msg: 'session expired, please login again', + statusCode: -1 + } +}; + +session.getUser = function() { + return cache.get(h.KEYS.user); +}; + +session.saveUser = function(user) { + // when auto login enabled, have to save password to re-login later + // otherwise don't dump password for the sake of security. + const _user = _.omit(user, config.autologin.enable ? [] : ['pass']); + cache.set(h.KEYS.user, _user); +}; + +session.deleteUser = function() { + cache.del(h.KEYS.user); +}; + +session.deleteCodingSession = function() { + cache.del(h.KEYS.problems); +}; + +session.isLogin = function() { + return this.getUser() !== null; +}; + +session.updateStat = function(k, v) { + // TODO: use other storage if too many stat data + const today = moment().format('YYYY-MM-DD'); + const stats = cache.get(h.KEYS.stat) || {}; + const stat = stats[today] = stats[today] || {}; + + if (k.endsWith('.set')) { + const s = new Set(stat[k] || []); + s.add(v); + stat[k] = Array.from(s); + } else { + stat[k] = (stat[k] || 0) + v; + } + + cache.set(h.KEYS.stat, stats); +}; + +module.exports = session; diff --git a/lib/sprintf.js b/lib/sprintf.js new file mode 100644 index 00000000..739f3d06 --- /dev/null +++ b/lib/sprintf.js @@ -0,0 +1,60 @@ +'use strict' + +function len(s) { + let s1 = s.replace(/\u001b\[[^m]*m/g, ''); // remove color controls + s1 = s1.replace(/[^\x00-\xff]/g, ' '); // fix non-ascii + return s1.length; +} + +function padLeft(s, n, c) { + let k = Math.max(0, n - len(s)); + return c.repeat(k) + s; +} + +function padRight(s, n , c) { + let k = Math.max(0, n - len(s)); + return s + c.repeat(k); +} + +function padCenter(s, n, c) { + let k = Math.max(0, n - len(s)); + let r = (k - k % 2) / 2, l = k - r; + return c.repeat(l) + s + c.repeat(r); +} + +const tsprintf = function() { + const args = Array.from(arguments); + let fmt = args.shift(); + return fmt.replace(/%[^s%]*[s%]/g, function(s) { + if (s === '%%') return '%'; + + let x = '' + args.shift(); + let n = 0; + + s = s.slice(1, s.length-1); + if (s.length > 0) { + switch (s[0]) { + case '-': + n = parseInt(s.slice(1)) || 0; + x = padRight(x, n, ' '); + break; + case '=': + n = parseInt(s.slice(1)) || 0; + x = padCenter(x, n, ' '); + break; + case '0': + n = parseInt(s.slice(1)) || 0; + x = padLeft(x, n, '0'); + break; + default: + n = parseInt(s) || 0; + x = padLeft(x, n, ' '); + break; + } + } + + return x; + }); +}; + +module.exports = tsprintf; diff --git a/package.json b/package.json index 67bff4f5..a9af9f7c 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,34 @@ { "name": "leetcode-cli", - "version": "0.9.0", + "version": "2.6.2", "description": "A cli tool to enjoy leetcode!", "preferGlobal": "true", + "engines": { + "node": ">=4" + }, "bin": { "leetcode": "./bin/leetcode" }, "scripts": { "lint": "eslint lib/ test/", - "test": "npm run lint && nyc mocha test/" + "test": "npm run lint && nyc mocha test test/plugins && nyc report --reporter=lcov", + "travis": "node bin/pkg", + "pkg": "pkg . --out-path=dist/ --targets" + }, + "pkg": { + "scripts": [ + "lib" + ], + "assets": [ + "colors", + "icons", + "templates" + ], + "targets": [ + "node10-linux-x64", + "node10-macos-x64", + "node10-win-x64" + ] }, "repository": { "type": "git", @@ -30,24 +50,28 @@ }, "homepage": "https://github.com/skygragon/leetcode-cli#readme", "dependencies": { - "ansi-styles": "^3.0.0", - "cheerio": "^0.20.0", - "he": "^1.1.1", - "loglevel": "^1.4.1", - "prompt": "^1.0.0", - "request": "^2.74.0", - "sprintf-js": "^1.0.3", - "underscore": "^1.8.3", - "wordwrap": "^1.0.0", - "yargs": "^5.0.0" + "ansi-styles": "3.2.1", + "cheerio": "0.20.0", + "he": "1.2.0", + "mkdirp": "0.5.1", + "moment": "^2.20.1", + "nconf": "0.10.0", + "ora": "3.0.0", + "prompt": "1.0.0", + "request": "2.88.0", + "supports-color": "5.5.0", + "underscore": "1.9.1", + "wordwrap": "1.0.0", + "yargs": "12.0.4" }, "devDependencies": { - "chai": "^3.5.0", - "eslint": "^3.3.1", - "eslint-config-google": "^0.6.0", - "mocha": "^3.0.2", - "nock": "^8.0.0", - "nyc": "^8.1.0", - "rewire": "^2.5.2" + "chai": "4.2.0", + "eslint": "5.9.0", + "eslint-config-google": "0.11.0", + "mocha": "5.2.0", + "nock": "10.0.2", + "nyc": "13.1.0", + "pkg": "^4.3.4", + "rewire": "4.0.1" } } diff --git a/source.tpl b/source.tpl deleted file mode 100644 index 2c912323..00000000 --- a/source.tpl +++ /dev/null @@ -1,13 +0,0 @@ -<%= commentHeader %> -<%= commentLine %> [<%= id %>] <%= name %> -<%= commentLine %> -<%= commentLine %> <%= link %> -<%= commentLine %> -<%= commentLine %> <%= level %> (<%= percent %>%) -<%= commentLine %> Total Accepted: <%= totalAC %> -<%= commentLine %> Total Submissions: <%= totalSubmit %> -<%= commentLine %> Testcase Example: <%= testcase %> -<%= commentLine %> -<% _.each(desc, function(x) { %><%= commentLine %> <%= x %> -<% }) %><%= commentFooter %> -<%= code %> diff --git a/templates/codeonly.tpl b/templates/codeonly.tpl new file mode 100644 index 00000000..d8baa802 --- /dev/null +++ b/templates/codeonly.tpl @@ -0,0 +1 @@ +${code} diff --git a/templates/detailed.tpl b/templates/detailed.tpl new file mode 100644 index 00000000..cf501c0d --- /dev/null +++ b/templates/detailed.tpl @@ -0,0 +1,16 @@ +${comment.start} +${comment.line} @lc app=${app} id=${fid} lang=${lang} +${comment.line} +${comment.line} [${fid}] ${name} +${comment.line} +${comment.line} ${link} +${comment.line} +${comment.line} ${category} +${comment.line} ${level} (${percent}%) +${comment.line} Total Accepted: ${totalAC} +${comment.line} Total Submissions: ${totalSubmit} +${comment.line} Testcase Example: ${testcase} +${comment.line} +{{ desc.forEach(function(x) { }}${comment.line} ${x} +{{ }) }}${comment.end} +${code} diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 00000000..ca0f532e --- /dev/null +++ b/test/helper.js @@ -0,0 +1,20 @@ +'use_strict'; +const fs = require('fs'); + +const h = { + DIR: './tmp/' +}; + +h.clean = function() { + if (!fs.existsSync(this.DIR)) + fs.mkdirSync(this.DIR); + for (let f of fs.readdirSync(this.DIR)) { + const fullpath = this.DIR + f; + if (fs.statSync(fullpath).isDirectory()) + fs.rmdirSync(fullpath); + else + fs.unlinkSync(fullpath); + } +}; + +module.exports = h; diff --git a/test/mock/add-two-numbers.20161015.json b/test/mock/add-two-numbers.20161015.json index 85d7c3e3..c8a4d61f 100644 --- a/test/mock/add-two-numbers.20161015.json +++ b/test/mock/add-two-numbers.20161015.json @@ -1 +1 @@ -{"state":"ac","id":2,"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","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,"code":"class Solution {\r\npublic:\r\n ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {\r\n\r\n }\r\n};"} \ No newline at end of file +{"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","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} diff --git a/test/mock/favorites.json.20170716 b/test/mock/favorites.json.20170716 new file mode 100644 index 00000000..6dbefba2 --- /dev/null +++ b/test/mock/favorites.json.20170716 @@ -0,0 +1 @@ +{"user_name":"skygragon","favorites":{"private_favorites":[{"id_hash":"abcdefg","name":"Favorite","description":"","questions":[],"is_public_favorite":false,"view_count":0,"creator":"skygragon","current_user":"","is_watched":false}],"public_favorites":[],"leetcode_favorites":[{"id_hash":"79h8rn6","name":"Top 100 Liked Questions","description":"","questions":[{"id":1,"title":"Two Sum","title_slug":"two-sum"},{"id":2,"title":"Add Two Numbers","title_slug":"add-two-numbers"},{"id":3,"title":"Longest Substring Without Repeating Characters","title_slug":"longest-substring-without-repeating-characters"},{"id":4,"title":"Median of Two Sorted Arrays","title_slug":"median-of-two-sorted-arrays"},{"id":5,"title":"Longest Palindromic Substring","title_slug":"longest-palindromic-substring"},{"id":10,"title":"Regular Expression Matching","title_slug":"regular-expression-matching"},{"id":11,"title":"Container With Most Water","title_slug":"container-with-most-water"},{"id":15,"title":"3Sum","title_slug":"3sum"},{"id":17,"title":"Letter Combinations of a Phone Number","title_slug":"letter-combinations-of-a-phone-number"},{"id":20,"title":"Valid Parentheses","title_slug":"valid-parentheses"},{"id":21,"title":"Merge Two Sorted Lists","title_slug":"merge-two-sorted-lists"},{"id":22,"title":"Generate Parentheses","title_slug":"generate-parentheses"},{"id":23,"title":"Merge k Sorted Lists","title_slug":"merge-k-sorted-lists"},{"id":32,"title":"Longest Valid Parentheses","title_slug":"longest-valid-parentheses"},{"id":33,"title":"Search in Rotated Sorted Array","title_slug":"search-in-rotated-sorted-array"},{"id":34,"title":"Search for a Range","title_slug":"search-for-a-range"},{"id":39,"title":"Combination Sum","title_slug":"combination-sum"},{"id":42,"title":"Trapping Rain Water","title_slug":"trapping-rain-water"},{"id":46,"title":"Permutations","title_slug":"permutations"},{"id":53,"title":"Maximum Subarray","title_slug":"maximum-subarray"},{"id":56,"title":"Merge Intervals","title_slug":"merge-intervals"},{"id":62,"title":"Unique Paths","title_slug":"unique-paths"},{"id":70,"title":"Climbing Stairs","title_slug":"climbing-stairs"},{"id":72,"title":"Edit Distance","title_slug":"edit-distance"},{"id":76,"title":"Minimum Window Substring","title_slug":"minimum-window-substring"},{"id":77,"title":"Combinations","title_slug":"combinations"},{"id":78,"title":"Subsets","title_slug":"subsets"},{"id":79,"title":"Word Search","title_slug":"word-search"},{"id":84,"title":"Largest Rectangle in Histogram","title_slug":"largest-rectangle-in-histogram"},{"id":85,"title":"Maximal Rectangle","title_slug":"maximal-rectangle"},{"id":92,"title":"Reverse Linked List II","title_slug":"reverse-linked-list-ii"},{"id":96,"title":"Unique Binary Search Trees","title_slug":"unique-binary-search-trees"},{"id":98,"title":"Validate Binary Search Tree","title_slug":"validate-binary-search-tree"},{"id":101,"title":"Symmetric Tree","title_slug":"symmetric-tree"},{"id":104,"title":"Maximum Depth of Binary Tree","title_slug":"maximum-depth-of-binary-tree"},{"id":108,"title":"Convert Sorted Array to Binary Search Tree","title_slug":"convert-sorted-array-to-binary-search-tree"},{"id":110,"title":"Balanced Binary Tree","title_slug":"balanced-binary-tree"},{"id":121,"title":"Best Time to Buy and Sell Stock","title_slug":"best-time-to-buy-and-sell-stock"},{"id":124,"title":"Binary Tree Maximum Path Sum","title_slug":"binary-tree-maximum-path-sum"},{"id":128,"title":"Longest Consecutive Sequence","title_slug":"longest-consecutive-sequence"},{"id":136,"title":"Single Number","title_slug":"single-number"},{"id":138,"title":"Copy List with Random Pointer","title_slug":"copy-list-with-random-pointer"},{"id":139,"title":"Word Break","title_slug":"word-break"},{"id":141,"title":"Linked List Cycle","title_slug":"linked-list-cycle"},{"id":142,"title":"Linked List Cycle II","title_slug":"linked-list-cycle-ii"},{"id":146,"title":"LRU Cache","title_slug":"lru-cache"},{"id":148,"title":"Sort List","title_slug":"sort-list"},{"id":152,"title":"Maximum Product Subarray","title_slug":"maximum-product-subarray"},{"id":155,"title":"Min Stack","title_slug":"min-stack"},{"id":160,"title":"Intersection of Two Linked Lists","title_slug":"intersection-of-two-linked-lists"},{"id":169,"title":"Majority Element","title_slug":"majority-element"},{"id":198,"title":"House Robber","title_slug":"house-robber"},{"id":200,"title":"Number of Islands","title_slug":"number-of-islands"},{"id":206,"title":"Reverse Linked List","title_slug":"reverse-linked-list"},{"id":207,"title":"Course Schedule","title_slug":"course-schedule"},{"id":208,"title":"Implement Trie (Prefix Tree)","title_slug":"implement-trie-prefix-tree"},{"id":209,"title":"Minimum Size Subarray Sum","title_slug":"minimum-size-subarray-sum"},{"id":212,"title":"Word Search II","title_slug":"word-search-ii"},{"id":215,"title":"Kth Largest Element in an Array","title_slug":"kth-largest-element-in-an-array"},{"id":221,"title":"Maximal Square","title_slug":"maximal-square"},{"id":226,"title":"Invert Binary Tree","title_slug":"invert-binary-tree"},{"id":234,"title":"Palindrome Linked List","title_slug":"palindrome-linked-list"},{"id":236,"title":"Lowest Common Ancestor of a Binary Tree","title_slug":"lowest-common-ancestor-of-a-binary-tree"},{"id":238,"title":"Product of Array Except Self","title_slug":"product-of-array-except-self"},{"id":239,"title":"Sliding Window Maximum","title_slug":"sliding-window-maximum"},{"id":240,"title":"Search a 2D Matrix II","title_slug":"search-a-2d-matrix-ii"},{"id":279,"title":"Perfect Squares","title_slug":"perfect-squares"},{"id":283,"title":"Move Zeroes","title_slug":"move-zeroes"},{"id":287,"title":"Find the Duplicate Number","title_slug":"find-the-duplicate-number"},{"id":297,"title":"Serialize and Deserialize Binary Tree","title_slug":"serialize-and-deserialize-binary-tree"},{"id":300,"title":"Longest Increasing Subsequence","title_slug":"longest-increasing-subsequence"},{"id":301,"title":"Remove Invalid Parentheses","title_slug":"remove-invalid-parentheses"},{"id":309,"title":"Best Time to Buy and Sell Stock with Cooldown","title_slug":"best-time-to-buy-and-sell-stock-with-cooldown"},{"id":310,"title":"Minimum Height Trees","title_slug":"minimum-height-trees"},{"id":315,"title":"Count of Smaller Numbers After Self","title_slug":"count-of-smaller-numbers-after-self"},{"id":337,"title":"House Robber III","title_slug":"house-robber-iii"},{"id":338,"title":"Counting Bits","title_slug":"counting-bits"},{"id":377,"title":"Combination Sum IV","title_slug":"combination-sum-iv"},{"id":380,"title":"Insert Delete GetRandom O(1)","title_slug":"insert-delete-getrandom-o1"},{"id":394,"title":"Decode String","title_slug":"decode-string"},{"id":406,"title":"Queue Reconstruction by Height","title_slug":"queue-reconstruction-by-height"},{"id":437,"title":"Path Sum III","title_slug":"path-sum-iii"},{"id":438,"title":"Find All Anagrams in a String","title_slug":"find-all-anagrams-in-a-string"},{"id":448,"title":"Find All Numbers Disappeared in an Array","title_slug":"find-all-numbers-disappeared-in-an-array"},{"id":461,"title":"Hamming Distance","title_slug":"hamming-distance"},{"id":494,"title":"Target Sum","title_slug":"target-sum"},{"id":501,"title":"Find Mode in Binary Search Tree","title_slug":"find-mode-in-binary-search-tree"},{"id":538,"title":"Convert BST to Greater Tree","title_slug":"convert-bst-to-greater-tree"},{"id":543,"title":"Diameter of Binary Tree","title_slug":"diameter-of-binary-tree"},{"id":547,"title":"Friend Circles","title_slug":"friend-circles"},{"id":557,"title":"Reverse Words in a String III","title_slug":"reverse-words-in-a-string-iii"},{"id":560,"title":"Subarray Sum Equals K","title_slug":"subarray-sum-equals-k"},{"id":565,"title":"Array Nesting","title_slug":"array-nesting"},{"id":566,"title":"Reshape the Matrix","title_slug":"reshape-the-matrix"},{"id":572,"title":"Subtree of Another Tree","title_slug":"subtree-of-another-tree"},{"id":581,"title":"Shortest Unsorted Continuous Subarray","title_slug":"shortest-unsorted-continuous-subarray"},{"id":583,"title":"Delete Operation for Two Strings","title_slug":"delete-operation-for-two-strings"},{"id":617,"title":"Merge Two Binary Trees","title_slug":"merge-two-binary-trees"},{"id":621,"title":"Task Scheduler","title_slug":"task-scheduler"},{"id":624,"title":"Maximum Distance in Arrays","title_slug":"maximum-distance-in-arrays"}],"is_public_favorite":true,"view_count":1,"creator":"leetcode","current_user":"","is_watched":false},{"id_hash":"7p5x763","name":"Top Amazon Questions","description":"","questions":[{"id":1,"title":"Two Sum","title_slug":"two-sum"},{"id":2,"title":"Add Two Numbers","title_slug":"add-two-numbers"},{"id":3,"title":"Longest Substring Without Repeating Characters","title_slug":"longest-substring-without-repeating-characters"},{"id":5,"title":"Longest Palindromic Substring","title_slug":"longest-palindromic-substring"},{"id":8,"title":"String to Integer (atoi)","title_slug":"string-to-integer-atoi"},{"id":15,"title":"3Sum","title_slug":"3sum"},{"id":17,"title":"Letter Combinations of a Phone Number","title_slug":"letter-combinations-of-a-phone-number"},{"id":20,"title":"Valid Parentheses","title_slug":"valid-parentheses"},{"id":21,"title":"Merge Two Sorted Lists","title_slug":"merge-two-sorted-lists"},{"id":23,"title":"Merge k Sorted Lists","title_slug":"merge-k-sorted-lists"},{"id":42,"title":"Trapping Rain Water","title_slug":"trapping-rain-water"},{"id":48,"title":"Rotate Image","title_slug":"rotate-image"},{"id":49,"title":"Group Anagrams","title_slug":"group-anagrams"},{"id":73,"title":"Set Matrix Zeroes","title_slug":"set-matrix-zeroes"},{"id":78,"title":"Subsets","title_slug":"subsets"},{"id":89,"title":"Gray Code","title_slug":"gray-code"},{"id":98,"title":"Validate Binary Search Tree","title_slug":"validate-binary-search-tree"},{"id":102,"title":"Binary Tree Level Order Traversal","title_slug":"binary-tree-level-order-traversal"},{"id":119,"title":"Pascal's Triangle II","title_slug":"pascals-triangle-ii"},{"id":121,"title":"Best Time to Buy and Sell Stock","title_slug":"best-time-to-buy-and-sell-stock"},{"id":126,"title":"Word Ladder II","title_slug":"word-ladder-ii"},{"id":127,"title":"Word Ladder","title_slug":"word-ladder"},{"id":138,"title":"Copy List with Random Pointer","title_slug":"copy-list-with-random-pointer"},{"id":139,"title":"Word Break","title_slug":"word-break"},{"id":141,"title":"Linked List Cycle","title_slug":"linked-list-cycle"},{"id":146,"title":"LRU Cache","title_slug":"lru-cache"},{"id":155,"title":"Min Stack","title_slug":"min-stack"},{"id":160,"title":"Intersection of Two Linked Lists","title_slug":"intersection-of-two-linked-lists"},{"id":167,"title":"Two Sum II - Input array is sorted","title_slug":"two-sum-ii-input-array-is-sorted"},{"id":199,"title":"Binary Tree Right Side View","title_slug":"binary-tree-right-side-view"},{"id":200,"title":"Number of Islands","title_slug":"number-of-islands"},{"id":204,"title":"Count Primes","title_slug":"count-primes"},{"id":206,"title":"Reverse Linked List","title_slug":"reverse-linked-list"},{"id":215,"title":"Kth Largest Element in an Array","title_slug":"kth-largest-element-in-an-array"},{"id":234,"title":"Palindrome Linked List","title_slug":"palindrome-linked-list"},{"id":235,"title":"Lowest Common Ancestor of a Binary Search Tree","title_slug":"lowest-common-ancestor-of-a-binary-search-tree"},{"id":236,"title":"Lowest Common Ancestor of a Binary Tree","title_slug":"lowest-common-ancestor-of-a-binary-tree"},{"id":238,"title":"Product of Array Except Self","title_slug":"product-of-array-except-self"},{"id":239,"title":"Sliding Window Maximum","title_slug":"sliding-window-maximum"},{"id":240,"title":"Search a 2D Matrix II","title_slug":"search-a-2d-matrix-ii"},{"id":242,"title":"Valid Anagram","title_slug":"valid-anagram"},{"id":297,"title":"Serialize and Deserialize Binary Tree","title_slug":"serialize-and-deserialize-binary-tree"},{"id":380,"title":"Insert Delete GetRandom O(1)","title_slug":"insert-delete-getrandom-o1"},{"id":387,"title":"First Unique Character in a String","title_slug":"first-unique-character-in-a-string"},{"id":396,"title":"Rotate Function","title_slug":"rotate-function"},{"id":451,"title":"Sort Characters By Frequency","title_slug":"sort-characters-by-frequency"},{"id":460,"title":"LFU Cache","title_slug":"lfu-cache"},{"id":534,"title":"Design TinyURL","title_slug":"design-tinyurl"},{"id":535,"title":"Encode and Decode TinyURL","title_slug":"encode-and-decode-tinyurl"},{"id":617,"title":"Merge Two Binary Trees","title_slug":"merge-two-binary-trees"}],"is_public_favorite":true,"view_count":1,"creator":"leetcode","current_user":"","is_watched":false},{"id_hash":"7p59281","name":"Top Facebook Questions","description":"","questions":[{"id":1,"title":"Two Sum","title_slug":"two-sum"},{"id":10,"title":"Regular Expression Matching","title_slug":"regular-expression-matching"},{"id":13,"title":"Roman to Integer","title_slug":"roman-to-integer"},{"id":15,"title":"3Sum","title_slug":"3sum"},{"id":17,"title":"Letter Combinations of a Phone Number","title_slug":"letter-combinations-of-a-phone-number"},{"id":20,"title":"Valid Parentheses","title_slug":"valid-parentheses"},{"id":23,"title":"Merge k Sorted Lists","title_slug":"merge-k-sorted-lists"},{"id":26,"title":"Remove Duplicates from Sorted Array","title_slug":"remove-duplicates-from-sorted-array"},{"id":28,"title":"Implement strStr()","title_slug":"implement-strstr"},{"id":33,"title":"Search in Rotated Sorted Array","title_slug":"search-in-rotated-sorted-array"},{"id":38,"title":"Count and Say","title_slug":"count-and-say"},{"id":43,"title":"Multiply Strings","title_slug":"multiply-strings"},{"id":49,"title":"Group Anagrams","title_slug":"group-anagrams"},{"id":50,"title":"Pow(x, n)","title_slug":"powx-n"},{"id":56,"title":"Merge Intervals","title_slug":"merge-intervals"},{"id":67,"title":"Add Binary","title_slug":"add-binary"},{"id":69,"title":"Sqrt(x)","title_slug":"sqrtx"},{"id":76,"title":"Minimum Window Substring","title_slug":"minimum-window-substring"},{"id":78,"title":"Subsets","title_slug":"subsets"},{"id":79,"title":"Word Search","title_slug":"word-search"},{"id":85,"title":"Maximal Rectangle","title_slug":"maximal-rectangle"},{"id":88,"title":"Merge Sorted Array","title_slug":"merge-sorted-array"},{"id":91,"title":"Decode Ways","title_slug":"decode-ways"},{"id":102,"title":"Binary Tree Level Order Traversal","title_slug":"binary-tree-level-order-traversal"},{"id":121,"title":"Best Time to Buy and Sell Stock","title_slug":"best-time-to-buy-and-sell-stock"},{"id":125,"title":"Valid Palindrome","title_slug":"valid-palindrome"},{"id":127,"title":"Word Ladder","title_slug":"word-ladder"},{"id":128,"title":"Longest Consecutive Sequence","title_slug":"longest-consecutive-sequence"},{"id":139,"title":"Word Break","title_slug":"word-break"},{"id":146,"title":"LRU Cache","title_slug":"lru-cache"},{"id":168,"title":"Excel Sheet Column Title","title_slug":"excel-sheet-column-title"},{"id":173,"title":"Binary Search Tree Iterator","title_slug":"binary-search-tree-iterator"},{"id":200,"title":"Number of Islands","title_slug":"number-of-islands"},{"id":206,"title":"Reverse Linked List","title_slug":"reverse-linked-list"},{"id":215,"title":"Kth Largest Element in an Array","title_slug":"kth-largest-element-in-an-array"},{"id":218,"title":"The Skyline Problem","title_slug":"the-skyline-problem"},{"id":234,"title":"Palindrome Linked List","title_slug":"palindrome-linked-list"},{"id":235,"title":"Lowest Common Ancestor of a Binary Search Tree","title_slug":"lowest-common-ancestor-of-a-binary-search-tree"},{"id":236,"title":"Lowest Common Ancestor of a Binary Tree","title_slug":"lowest-common-ancestor-of-a-binary-tree"},{"id":238,"title":"Product of Array Except Self","title_slug":"product-of-array-except-self"},{"id":273,"title":"Integer to English Words","title_slug":"integer-to-english-words"},{"id":278,"title":"First Bad Version","title_slug":"first-bad-version"},{"id":282,"title":"Expression Add Operators","title_slug":"expression-add-operators"},{"id":283,"title":"Move Zeroes","title_slug":"move-zeroes"},{"id":297,"title":"Serialize and Deserialize Binary Tree","title_slug":"serialize-and-deserialize-binary-tree"},{"id":301,"title":"Remove Invalid Parentheses","title_slug":"remove-invalid-parentheses"},{"id":334,"title":"Increasing Triplet Subsequence","title_slug":"increasing-triplet-subsequence"},{"id":461,"title":"Hamming Distance","title_slug":"hamming-distance"},{"id":534,"title":"Design TinyURL","title_slug":"design-tinyurl"},{"id":535,"title":"Encode and Decode TinyURL","title_slug":"encode-and-decode-tinyurl"}],"is_public_favorite":true,"view_count":1,"creator":"leetcode","current_user":"","is_watched":false},{"id_hash":"7p55wqm","name":"Top Google Questions","description":"","questions":[{"id":4,"title":"Median of Two Sorted Arrays","title_slug":"median-of-two-sorted-arrays"},{"id":10,"title":"Regular Expression Matching","title_slug":"regular-expression-matching"},{"id":17,"title":"Letter Combinations of a Phone Number","title_slug":"letter-combinations-of-a-phone-number"},{"id":20,"title":"Valid Parentheses","title_slug":"valid-parentheses"},{"id":22,"title":"Generate Parentheses","title_slug":"generate-parentheses"},{"id":23,"title":"Merge k Sorted Lists","title_slug":"merge-k-sorted-lists"},{"id":31,"title":"Next Permutation","title_slug":"next-permutation"},{"id":42,"title":"Trapping Rain Water","title_slug":"trapping-rain-water"},{"id":50,"title":"Pow(x, n)","title_slug":"powx-n"},{"id":54,"title":"Spiral Matrix","title_slug":"spiral-matrix"},{"id":56,"title":"Merge Intervals","title_slug":"merge-intervals"},{"id":66,"title":"Plus One","title_slug":"plus-one"},{"id":128,"title":"Longest Consecutive Sequence","title_slug":"longest-consecutive-sequence"},{"id":133,"title":"Clone Graph","title_slug":"clone-graph"},{"id":139,"title":"Word Break","title_slug":"word-break"},{"id":140,"title":"Word Break II","title_slug":"word-break-ii"},{"id":146,"title":"LRU Cache","title_slug":"lru-cache"},{"id":155,"title":"Min Stack","title_slug":"min-stack"},{"id":162,"title":"Find Peak Element","title_slug":"find-peak-element"},{"id":166,"title":"Fraction to Recurring Decimal","title_slug":"fraction-to-recurring-decimal"},{"id":173,"title":"Binary Search Tree Iterator","title_slug":"binary-search-tree-iterator"},{"id":200,"title":"Number of Islands","title_slug":"number-of-islands"},{"id":208,"title":"Implement Trie (Prefix Tree)","title_slug":"implement-trie-prefix-tree"},{"id":214,"title":"Shortest Palindrome","title_slug":"shortest-palindrome"},{"id":218,"title":"The Skyline Problem","title_slug":"the-skyline-problem"},{"id":224,"title":"Basic Calculator","title_slug":"basic-calculator"},{"id":228,"title":"Summary Ranges","title_slug":"summary-ranges"},{"id":231,"title":"Power of Two","title_slug":"power-of-two"},{"id":240,"title":"Search a 2D Matrix II","title_slug":"search-a-2d-matrix-ii"},{"id":257,"title":"Binary Tree Paths","title_slug":"binary-tree-paths"},{"id":279,"title":"Perfect Squares","title_slug":"perfect-squares"},{"id":280,"title":"Wiggle Sort","title_slug":"wiggle-sort"},{"id":282,"title":"Expression Add Operators","title_slug":"expression-add-operators"},{"id":289,"title":"Game of Life","title_slug":"game-of-life"},{"id":295,"title":"Find Median from Data Stream","title_slug":"find-median-from-data-stream"},{"id":297,"title":"Serialize and Deserialize Binary Tree","title_slug":"serialize-and-deserialize-binary-tree"},{"id":312,"title":"Burst Balloons","title_slug":"burst-balloons"},{"id":315,"title":"Count of Smaller Numbers After Self","title_slug":"count-of-smaller-numbers-after-self"},{"id":316,"title":"Remove Duplicate Letters","title_slug":"remove-duplicate-letters"},{"id":318,"title":"Maximum Product of Word Lengths","title_slug":"maximum-product-of-word-lengths"},{"id":326,"title":"Power of Three","title_slug":"power-of-three"},{"id":336,"title":"Palindrome Pairs","title_slug":"palindrome-pairs"},{"id":345,"title":"Reverse Vowels of a String","title_slug":"reverse-vowels-of-a-string"},{"id":374,"title":"Guess Number Higher or Lower","title_slug":"guess-number-higher-or-lower"},{"id":388,"title":"Longest Absolute File Path","title_slug":"longest-absolute-file-path"},{"id":421,"title":"Maximum XOR of Two Numbers in an Array","title_slug":"maximum-xor-of-two-numbers-in-an-array"},{"id":448,"title":"Find All Numbers Disappeared in an Array","title_slug":"find-all-numbers-disappeared-in-an-array"},{"id":463,"title":"Island Perimeter","title_slug":"island-perimeter"},{"id":534,"title":"Design TinyURL","title_slug":"design-tinyurl"},{"id":535,"title":"Encode and Decode TinyURL","title_slug":"encode-and-decode-tinyurl"}],"is_public_favorite":true,"view_count":1,"creator":"leetcode","current_user":"","is_watched":false},{"id_hash":"wpwgkgt","name":"Top Interview Questions","description":"151","questions":[{"id":1,"title":"Two Sum","title_slug":"two-sum"},{"id":2,"title":"Add Two Numbers","title_slug":"add-two-numbers"},{"id":3,"title":"Longest Substring Without Repeating Characters","title_slug":"longest-substring-without-repeating-characters"},{"id":4,"title":"Median of Two Sorted Arrays","title_slug":"median-of-two-sorted-arrays"},{"id":5,"title":"Longest Palindromic Substring","title_slug":"longest-palindromic-substring"},{"id":7,"title":"Reverse Integer","title_slug":"reverse-integer"},{"id":8,"title":"String to Integer (atoi)","title_slug":"string-to-integer-atoi"},{"id":10,"title":"Regular Expression Matching","title_slug":"regular-expression-matching"},{"id":11,"title":"Container With Most Water","title_slug":"container-with-most-water"},{"id":13,"title":"Roman to Integer","title_slug":"roman-to-integer"},{"id":14,"title":"Longest Common Prefix","title_slug":"longest-common-prefix"},{"id":15,"title":"3Sum","title_slug":"3sum"},{"id":17,"title":"Letter Combinations of a Phone Number","title_slug":"letter-combinations-of-a-phone-number"},{"id":19,"title":"Remove Nth Node From End of List","title_slug":"remove-nth-node-from-end-of-list"},{"id":20,"title":"Valid Parentheses","title_slug":"valid-parentheses"},{"id":21,"title":"Merge Two Sorted Lists","title_slug":"merge-two-sorted-lists"},{"id":22,"title":"Generate Parentheses","title_slug":"generate-parentheses"},{"id":23,"title":"Merge k Sorted Lists","title_slug":"merge-k-sorted-lists"},{"id":26,"title":"Remove Duplicates from Sorted Array","title_slug":"remove-duplicates-from-sorted-array"},{"id":28,"title":"Implement strStr()","title_slug":"implement-strstr"},{"id":29,"title":"Divide Two Integers","title_slug":"divide-two-integers"},{"id":33,"title":"Search in Rotated Sorted Array","title_slug":"search-in-rotated-sorted-array"},{"id":34,"title":"Search for a Range","title_slug":"search-for-a-range"},{"id":36,"title":"Valid Sudoku","title_slug":"valid-sudoku"},{"id":38,"title":"Count and Say","title_slug":"count-and-say"},{"id":41,"title":"First Missing Positive","title_slug":"first-missing-positive"},{"id":42,"title":"Trapping Rain Water","title_slug":"trapping-rain-water"},{"id":44,"title":"Wildcard Matching","title_slug":"wildcard-matching"},{"id":46,"title":"Permutations","title_slug":"permutations"},{"id":48,"title":"Rotate Image","title_slug":"rotate-image"},{"id":49,"title":"Group Anagrams","title_slug":"group-anagrams"},{"id":50,"title":"Pow(x, n)","title_slug":"powx-n"},{"id":53,"title":"Maximum Subarray","title_slug":"maximum-subarray"},{"id":54,"title":"Spiral Matrix","title_slug":"spiral-matrix"},{"id":55,"title":"Jump Game","title_slug":"jump-game"},{"id":56,"title":"Merge Intervals","title_slug":"merge-intervals"},{"id":62,"title":"Unique Paths","title_slug":"unique-paths"},{"id":66,"title":"Plus One","title_slug":"plus-one"},{"id":69,"title":"Sqrt(x)","title_slug":"sqrtx"},{"id":70,"title":"Climbing Stairs","title_slug":"climbing-stairs"},{"id":73,"title":"Set Matrix Zeroes","title_slug":"set-matrix-zeroes"},{"id":75,"title":"Sort Colors","title_slug":"sort-colors"},{"id":76,"title":"Minimum Window Substring","title_slug":"minimum-window-substring"},{"id":78,"title":"Subsets","title_slug":"subsets"},{"id":79,"title":"Word Search","title_slug":"word-search"},{"id":84,"title":"Largest Rectangle in Histogram","title_slug":"largest-rectangle-in-histogram"},{"id":88,"title":"Merge Sorted Array","title_slug":"merge-sorted-array"},{"id":91,"title":"Decode Ways","title_slug":"decode-ways"},{"id":94,"title":"Binary Tree Inorder Traversal","title_slug":"binary-tree-inorder-traversal"},{"id":98,"title":"Validate Binary Search Tree","title_slug":"validate-binary-search-tree"},{"id":101,"title":"Symmetric Tree","title_slug":"symmetric-tree"},{"id":102,"title":"Binary Tree Level Order Traversal","title_slug":"binary-tree-level-order-traversal"},{"id":103,"title":"Binary Tree Zigzag Level Order Traversal","title_slug":"binary-tree-zigzag-level-order-traversal"},{"id":104,"title":"Maximum Depth of Binary Tree","title_slug":"maximum-depth-of-binary-tree"},{"id":105,"title":"Construct Binary Tree from Preorder and Inorder Traversal","title_slug":"construct-binary-tree-from-preorder-and-inorder-traversal"},{"id":108,"title":"Convert Sorted Array to Binary Search Tree","title_slug":"convert-sorted-array-to-binary-search-tree"},{"id":116,"title":"Populating Next Right Pointers in Each Node","title_slug":"populating-next-right-pointers-in-each-node"},{"id":118,"title":"Pascal's Triangle","title_slug":"pascals-triangle"},{"id":121,"title":"Best Time to Buy and Sell Stock","title_slug":"best-time-to-buy-and-sell-stock"},{"id":122,"title":"Best Time to Buy and Sell Stock II","title_slug":"best-time-to-buy-and-sell-stock-ii"},{"id":124,"title":"Binary Tree Maximum Path Sum","title_slug":"binary-tree-maximum-path-sum"},{"id":125,"title":"Valid Palindrome","title_slug":"valid-palindrome"},{"id":127,"title":"Word Ladder","title_slug":"word-ladder"},{"id":128,"title":"Longest Consecutive Sequence","title_slug":"longest-consecutive-sequence"},{"id":130,"title":"Surrounded Regions","title_slug":"surrounded-regions"},{"id":131,"title":"Palindrome Partitioning","title_slug":"palindrome-partitioning"},{"id":134,"title":"Gas Station","title_slug":"gas-station"},{"id":136,"title":"Single Number","title_slug":"single-number"},{"id":138,"title":"Copy List with Random Pointer","title_slug":"copy-list-with-random-pointer"},{"id":139,"title":"Word Break","title_slug":"word-break"},{"id":140,"title":"Word Break II","title_slug":"word-break-ii"},{"id":141,"title":"Linked List Cycle","title_slug":"linked-list-cycle"},{"id":146,"title":"LRU Cache","title_slug":"lru-cache"},{"id":148,"title":"Sort List","title_slug":"sort-list"},{"id":149,"title":"Max Points on a Line","title_slug":"max-points-on-a-line"},{"id":150,"title":"Evaluate Reverse Polish Notation","title_slug":"evaluate-reverse-polish-notation"},{"id":152,"title":"Maximum Product Subarray","title_slug":"maximum-product-subarray"},{"id":155,"title":"Min Stack","title_slug":"min-stack"},{"id":160,"title":"Intersection of Two Linked Lists","title_slug":"intersection-of-two-linked-lists"},{"id":162,"title":"Find Peak Element","title_slug":"find-peak-element"},{"id":163,"title":"Missing Ranges","title_slug":"missing-ranges"},{"id":166,"title":"Fraction to Recurring Decimal","title_slug":"fraction-to-recurring-decimal"},{"id":169,"title":"Majority Element","title_slug":"majority-element"},{"id":171,"title":"Excel Sheet Column Number","title_slug":"excel-sheet-column-number"},{"id":172,"title":"Factorial Trailing Zeroes","title_slug":"factorial-trailing-zeroes"},{"id":179,"title":"Largest Number","title_slug":"largest-number"},{"id":189,"title":"Rotate Array","title_slug":"rotate-array"},{"id":190,"title":"Reverse Bits","title_slug":"reverse-bits"},{"id":191,"title":"Number of 1 Bits","title_slug":"number-of-1-bits"},{"id":198,"title":"House Robber","title_slug":"house-robber"},{"id":200,"title":"Number of Islands","title_slug":"number-of-islands"},{"id":202,"title":"Happy Number","title_slug":"happy-number"},{"id":204,"title":"Count Primes","title_slug":"count-primes"},{"id":206,"title":"Reverse Linked List","title_slug":"reverse-linked-list"},{"id":207,"title":"Course Schedule","title_slug":"course-schedule"},{"id":208,"title":"Implement Trie (Prefix Tree)","title_slug":"implement-trie-prefix-tree"},{"id":210,"title":"Course Schedule II","title_slug":"course-schedule-ii"},{"id":212,"title":"Word Search II","title_slug":"word-search-ii"},{"id":215,"title":"Kth Largest Element in an Array","title_slug":"kth-largest-element-in-an-array"},{"id":217,"title":"Contains Duplicate","title_slug":"contains-duplicate"},{"id":218,"title":"The Skyline Problem","title_slug":"the-skyline-problem"},{"id":227,"title":"Basic Calculator II","title_slug":"basic-calculator-ii"},{"id":230,"title":"Kth Smallest Element in a BST","title_slug":"kth-smallest-element-in-a-bst"},{"id":234,"title":"Palindrome Linked List","title_slug":"palindrome-linked-list"},{"id":236,"title":"Lowest Common Ancestor of a Binary Tree","title_slug":"lowest-common-ancestor-of-a-binary-tree"},{"id":237,"title":"Delete Node in a Linked List","title_slug":"delete-node-in-a-linked-list"},{"id":238,"title":"Product of Array Except Self","title_slug":"product-of-array-except-self"},{"id":239,"title":"Sliding Window Maximum","title_slug":"sliding-window-maximum"},{"id":240,"title":"Search a 2D Matrix II","title_slug":"search-a-2d-matrix-ii"},{"id":242,"title":"Valid Anagram","title_slug":"valid-anagram"},{"id":251,"title":"Flatten 2D Vector","title_slug":"flatten-2d-vector"},{"id":253,"title":"Meeting Rooms II","title_slug":"meeting-rooms-ii"},{"id":268,"title":"Missing Number","title_slug":"missing-number"},{"id":269,"title":"Alien Dictionary","title_slug":"alien-dictionary"},{"id":277,"title":"Find the Celebrity","title_slug":"find-the-celebrity"},{"id":279,"title":"Perfect Squares","title_slug":"perfect-squares"},{"id":283,"title":"Move Zeroes","title_slug":"move-zeroes"},{"id":285,"title":"Inorder Successor in BST","title_slug":"inorder-successor-in-bst"},{"id":287,"title":"Find the Duplicate Number","title_slug":"find-the-duplicate-number"},{"id":289,"title":"Game of Life","title_slug":"game-of-life"},{"id":295,"title":"Find Median from Data Stream","title_slug":"find-median-from-data-stream"},{"id":297,"title":"Serialize and Deserialize Binary Tree","title_slug":"serialize-and-deserialize-binary-tree"},{"id":300,"title":"Longest Increasing Subsequence","title_slug":"longest-increasing-subsequence"},{"id":308,"title":"Range Sum Query 2D - Mutable","title_slug":"range-sum-query-2d-mutable"},{"id":315,"title":"Count of Smaller Numbers After Self","title_slug":"count-of-smaller-numbers-after-self"},{"id":322,"title":"Coin Change","title_slug":"coin-change"},{"id":324,"title":"Wiggle Sort II","title_slug":"wiggle-sort-ii"},{"id":326,"title":"Power of Three","title_slug":"power-of-three"},{"id":328,"title":"Odd Even Linked List","title_slug":"odd-even-linked-list"},{"id":329,"title":"Longest Increasing Path in a Matrix","title_slug":"longest-increasing-path-in-a-matrix"},{"id":334,"title":"Increasing Triplet Subsequence","title_slug":"increasing-triplet-subsequence"},{"id":340,"title":"Longest Substring with At Most K Distinct Characters","title_slug":"longest-substring-with-at-most-k-distinct-characters"},{"id":341,"title":"Flatten Nested List Iterator","title_slug":"flatten-nested-list-iterator"},{"id":344,"title":"Reverse String","title_slug":"reverse-string"},{"id":347,"title":"Top K Frequent Elements","title_slug":"top-k-frequent-elements"},{"id":348,"title":"Design Tic-Tac-Toe","title_slug":"design-tic-tac-toe"},{"id":350,"title":"Intersection of Two Arrays II","title_slug":"intersection-of-two-arrays-ii"},{"id":371,"title":"Sum of Two Integers","title_slug":"sum-of-two-integers"},{"id":378,"title":"Kth Smallest Element in a Sorted Matrix","title_slug":"kth-smallest-element-in-a-sorted-matrix"},{"id":380,"title":"Insert Delete GetRandom O(1)","title_slug":"insert-delete-getrandom-o1"},{"id":384,"title":"Shuffle an Array","title_slug":"shuffle-an-array"},{"id":387,"title":"First Unique Character in a String","title_slug":"first-unique-character-in-a-string"},{"id":395,"title":"Longest Substring with At Least K Repeating Characters","title_slug":"longest-substring-with-at-least-k-repeating-characters"},{"id":412,"title":"Fizz Buzz","title_slug":"fizz-buzz"},{"id":454,"title":"4Sum II","title_slug":"4sum-ii"}],"is_public_favorite":true,"view_count":0,"creator":"leetcode","current_user":"","is_watched":false},{"id_hash":"7p5ept7","name":"Top LinkedIn Questions","description":"","questions":[{"id":1,"title":"Two Sum","title_slug":"two-sum"},{"id":21,"title":"Merge Two Sorted Lists","title_slug":"merge-two-sorted-lists"},{"id":23,"title":"Merge k Sorted Lists","title_slug":"merge-k-sorted-lists"},{"id":33,"title":"Search in Rotated Sorted Array","title_slug":"search-in-rotated-sorted-array"},{"id":34,"title":"Search for a Range","title_slug":"search-for-a-range"},{"id":46,"title":"Permutations","title_slug":"permutations"},{"id":47,"title":"Permutations II","title_slug":"permutations-ii"},{"id":50,"title":"Pow(x, n)","title_slug":"powx-n"},{"id":53,"title":"Maximum Subarray","title_slug":"maximum-subarray"},{"id":56,"title":"Merge Intervals","title_slug":"merge-intervals"},{"id":57,"title":"Insert Interval","title_slug":"insert-interval"},{"id":65,"title":"Valid Number","title_slug":"valid-number"},{"id":68,"title":"Text Justification","title_slug":"text-justification"},{"id":76,"title":"Minimum Window Substring","title_slug":"minimum-window-substring"},{"id":101,"title":"Symmetric Tree","title_slug":"symmetric-tree"},{"id":102,"title":"Binary Tree Level Order Traversal","title_slug":"binary-tree-level-order-traversal"},{"id":103,"title":"Binary Tree Zigzag Level Order Traversal","title_slug":"binary-tree-zigzag-level-order-traversal"},{"id":104,"title":"Maximum Depth of Binary Tree","title_slug":"maximum-depth-of-binary-tree"},{"id":127,"title":"Word Ladder","title_slug":"word-ladder"},{"id":149,"title":"Max Points on a Line","title_slug":"max-points-on-a-line"},{"id":150,"title":"Evaluate Reverse Polish Notation","title_slug":"evaluate-reverse-polish-notation"},{"id":152,"title":"Maximum Product Subarray","title_slug":"maximum-product-subarray"},{"id":156,"title":"Binary Tree Upside Down","title_slug":"binary-tree-upside-down"},{"id":170,"title":"Two Sum III - Data structure design","title_slug":"two-sum-iii-data-structure-design"},{"id":173,"title":"Binary Search Tree Iterator","title_slug":"binary-search-tree-iterator"},{"id":187,"title":"Repeated DNA Sequences","title_slug":"repeated-dna-sequences"},{"id":198,"title":"House Robber","title_slug":"house-robber"},{"id":205,"title":"Isomorphic Strings","title_slug":"isomorphic-strings"},{"id":236,"title":"Lowest Common Ancestor of a Binary Tree","title_slug":"lowest-common-ancestor-of-a-binary-tree"},{"id":238,"title":"Product of Array Except Self","title_slug":"product-of-array-except-self"},{"id":243,"title":"Shortest Word Distance","title_slug":"shortest-word-distance"},{"id":244,"title":"Shortest Word Distance II","title_slug":"shortest-word-distance-ii"},{"id":245,"title":"Shortest Word Distance III","title_slug":"shortest-word-distance-iii"},{"id":254,"title":"Factor Combinations","title_slug":"factor-combinations"},{"id":256,"title":"Paint House","title_slug":"paint-house"},{"id":277,"title":"Find the Celebrity","title_slug":"find-the-celebrity"},{"id":297,"title":"Serialize and Deserialize Binary Tree","title_slug":"serialize-and-deserialize-binary-tree"},{"id":311,"title":"Sparse Matrix Multiplication","title_slug":"sparse-matrix-multiplication"},{"id":339,"title":"Nested List Weight Sum","title_slug":"nested-list-weight-sum"},{"id":364,"title":"Nested List Weight Sum II","title_slug":"nested-list-weight-sum-ii"},{"id":366,"title":"Find Leaves of Binary Tree","title_slug":"find-leaves-of-binary-tree"},{"id":367,"title":"Valid Perfect Square","title_slug":"valid-perfect-square"},{"id":464,"title":"Can I Win","title_slug":"can-i-win"},{"id":515,"title":"Find Largest Value in Each Tree Row","title_slug":"find-largest-value-in-each-tree-row"},{"id":605,"title":"Can Place Flowers","title_slug":"can-place-flowers"}],"is_public_favorite":true,"view_count":1,"creator":"leetcode","current_user":"","is_watched":false}]}} diff --git a/test/mock/find-the-difference.html.20160911 b/test/mock/find-the-difference.html.20160911 deleted file mode 100644 index a69b42ca..00000000 --- a/test/mock/find-the-difference.html.20160911 +++ /dev/null @@ -1,815 +0,0 @@ - - - - - - - Find the Difference | LeetCode OJ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
-
- -
Auto Saved.
- -
-
-
-
-
-

389. Find the Difference

- - - - Please login first. - -
- -
- - - - -
- My Submissions - -
-
-
- -
-
-
-
-
    -
  • Total Accepted: 15674
  • -
  • Total Submissions: 32141
  • -
  • Difficulty: Easy
  • -
-
-
-

-Given two strings s and t which consist of only lowercase letters.

- -

String t is generated by random shuffling string s and then add one more letter at a random position.

- -

Find the letter that was added in t.

- -

Example: -

-Input:
-s = "abcd"
-t = "abcde"
-
-Output:
-e
-
-Explanation:
-'e' is the letter that was added.
-

- -
-

Subscribe to see which companies asked this question

-
- - - -
-
Show Tags
- - - Hash Table - - Bit Manipulation - - -
- - - -
-
Show Similar Problems
- - - (E) Single Number - - -
- -
-
-
-
-
- -
-
- - - - - - - - -
-
-
- Have you met this question in a real interview? - - -
- Yes -
-
- No -
-
-
-
- When did you meet this question? - - - -
- last week -
- -
- last month -
- -
- last 3 month -
- -
- last 6 month -
- -
- more than 6 months -
- -
- other -
- -
-
- -
- Which company asked you this question? - - - -
- Adobe -
- -
- Airbnb -
- -
- Alation -
- -
- Alibaba -
- -
- Amazon -
- -
- Apple -
- -
- Arista -
- -
- Baidu -
- -
- Blend Labs -
- -
- Blizzard -
- -
- Bloomberg -
- -
- Box -
- -
- Bungie -
- -
- Cisco -
- -
- Conviva -
- -
- Coursera -
- -
- CreditEase -
- -
- Deutsche Bank -
- -
- Dropbox -
- -
- eBay -
- -
- Electronic Arts -
- -
- EMC -
- -
- Epic Systems -
- -
- Expedia -
- -
- Facebook -
- -
- Flipkart -
- -
- Fortinet -
- -
- FreeWheel -
- -
- Goldman Sachs -
- -
- Google -
- -
- GrabTaxi -
- -
- Groupon -
- -
- Hedvig -
- -
- Hulu -
- -
- Intel -
- -
- Jane Street -
- -
- JPMorgan -
- -
- Jump Trading -
- -
- Lending Club -
- -
- LinkedIn -
- -
- LiveRamp -
- -
- Marvel -
- -
- Matlab -
- -
- McKesson -
- -
- Microsoft -
- -
- Morgan Stanley -
- -
- Nvidia -
- -
- Oracle -
- -
- Orbitz -
- -
- Palantir -
- -
- Paypal -
- -
- Pinterest -
- -
- Pocket Gems -
- -
- Qualtrics -
- -
- Qumulo -
- -
- Quora -
- -
- Rackspace -
- -
- Salesforce -
- -
- Sina -
- -
- Snapchat -
- -
- Square -
- -
- Sumologic -
- -
- Symantec -
- -
- Tencent -
- -
- TinyCo -
- -
- Tradeshift -
- -
- TripAdvisor -
- -
- Twitter -
- -
- Two Sigma -
- -
- Uber -
- -
- VMware -
- -
- Walmart -
- -
- Yahoo -
- -
- Yandex -
- -
- Yelp -
- -
- Zenefits -
- -
- Zynga -
- - -
-
-
-
-
-
- - -

- - Discuss - - - - - Pick One -

- -
-
- -
- -
-
- - - -
-
-
-
-
-
- -
- -
- -
- -
- -
- -
- -
- -
-
-
-
-
- - - - You have not signed in, cannot submit your code. - - -
-
- -
- -
- - - - - - - Send Feedback - - - - - - - - - - - - - - - - - - - - diff --git a/test/mock/find-the-difference.html.20170424 b/test/mock/find-the-difference.html.20170424 deleted file mode 100644 index 711b3790..00000000 --- a/test/mock/find-the-difference.html.20170424 +++ /dev/null @@ -1,1097 +0,0 @@ - - - - - - - Find the Difference | LeetCode OJ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- -
Auto Saved.
- -
- -
- -
-
-
-

- 389. Find the Difference -

- -
-
-
- Loading Question ... -
-
- - - -
-
-
-
    -
  • Total Accepted: 63380
  • -
    -
  • Total Submissions: 123178
  • -
    -
  • Difficulty: Easy
  • -
    - -
  • - Contributor: - - LeetCode - -
  • - -
-
-
-
-

-Given two strings s and t which consist of only lowercase letters.

- -

String t is generated by random shuffling string s and then add one more letter at a random position.

- -

Find the letter that was added in t.

- -

Example: -

-Input:
-s = "abcd"
-t = "abcde"
-
-Output:
-e
-
-Explanation:
-'e' is the letter that was added.
-

- -
-

Subscribe to see which companies asked this question.

-
- - - -
-
Show Tags
- - - Hash Table - - Bit Manipulation - - -
- - - -
-
Show Similar Problems
- - - (E) Single Number - - -
- -
-
- -
-
- - - - - - - - -
-
-
- Have you met this question in a real interview? - - -
- Yes -
-
- No -
-
-
-
- When did you meet this question? - - - -
- last week -
- -
- last month -
- -
- last 3 month -
- -
- last 6 month -
- -
- more than 6 months -
- -
- other -
- -
-
- -
- Which company asked you this question? - - - -
- Adobe -
- -
- Aetion -
- -
- Affirm -
- -
- Airbnb -
- -
- Alation -
- -
- Alibaba -
- -
- Amazon -
- -
- AppDynamics -
- -
- Apple -
- -
- Arista -
- -
- Baidu -
- -
- Bank of America -
- -
- BlackRock -
- -
- Blend Labs -
- -
- Blizzard -
- -
- Bloomberg -
- -
- Booking -
- -
- Box -
- -
- Bungie -
- -
- Capital One -
- -
- CareerBuilder -
- -
- Cisco -
- -
- Citadel -
- -
- Coinbase -
- -
- Concur -
- -
- Conviva -
- -
- Coupang -
- -
- Coursera -
- -
- CreditEase -
- -
- CVTE -
- -
- Dell -
- -
- Deutsche Bank -
- -
- DoorDash -
- -
- Dropbox -
- -
- Duolingo -
- -
- EasyNet -
- -
- eBay -
- -
- Electronic Arts -
- -
- EMC -
- -
- Epic Systems -
- -
- Equinix -
- -
- Evernote -
- -
- Expedia -
- -
- Facebook -
- -
- FactSet -
- -
- Fitbit -
- -
- Flipkart -
- -
- Fortinet -
- -
- FourSquare -
- -
- FreeWheel -
- -
- GoDaddy -
- -
- Goldman Sachs -
- -
- Google -
- -
- GrabTaxi -
- -
- Groupon -
- -
- HBO -
- -
- Hedvig -
- -
- HomeAway -
- -
- HTC -
- -
- Huawei -
- -
- Hulu -
- -
- IBG -
- -
- IBM -
- -
- Indeed -
- -
- InnovatureLabs -
- -
- Intel -
- -
- IXL -
- -
- Jane Street -
- -
- JPMorgan -
- -
- Jump Trading -
- -
- Lending Club -
- -
- LinkedIn -
- -
- LiveRamp -
- -
- Loovee -
- -
- Marvel -
- -
- Matlab -
- -
- McKesson -
- -
- Microsoft -
- -
- Morgan Stanley -
- -
- NetEase -
- -
- Nintendo -
- -
- Nutanix -
- -
- Nvidia -
- -
- Oracle -
- -
- Orbitz -
- -
- Palantir -
- -
- Paypal -
- -
- Pinterest -
- -
- Pocket Gems -
- -
- Point72 -
- -
- Qualcomm -
- -
- Qualtrics -
- -
- Qumulo -
- -
- Quora -
- -
- Rackspace -
- -
- Redfin -
- -
- Rubrik -
- -
- Salesforce -
- -
- Samsung -
- -
- SAP -
- -
- ServiceNow -
- -
- Sina -
- -
- Snapchat -
- -
- SoftwareOne -
- -
- Sony -
- -
- SoundHound -
- -
- Square -
- -
- Sumologic -
- -
- SurveyMonkey -
- -
- Symantec -
- -
- Tableau -
- -
- Tencent -
- -
- Tesorio -
- -
- TinyCo -
- -
- Tradeshift -
- -
- TripAdvisor -
- -
- Twilio -
- -
- Twitter -
- -
- Two Sigma -
- -
- Uber -
- -
- Veritas -
- -
- Visa -
- -
- VMware -
- -
- Walmart -
- -
- Wealthfront -
- -
- Whitepages -
- -
- Works Applications -
- -
- Yahoo -
- -
- Yandex -
- -
- Yelp -
- -
- Zappos -
- -
- Zenefits -
- -
- Zillow -
- -
- Zynga -
- - - - - - -
-
-
-
-
-
- - -
- - - - Discuss - - - - Pick One - - - - - -
- -
- -
-
-
- -
- - -
- - - - - - - Send Feedback - - - - - - - - - - - - - - - - - - - diff --git a/test/mock/find-the-difference.json.20171216 b/test/mock/find-the-difference.json.20171216 new file mode 100644 index 00000000..bd1f7dc6 --- /dev/null +++ b/test/mock/find-the-difference.json.20171216 @@ -0,0 +1 @@ +{"data":{"question":{"content":"

\r\nGiven two strings s and t which consist of only lowercase letters.

\r\n\r\n

String t is generated by random shuffling string s and then add one more letter at a random position.

\r\n\r\n

Find the letter that was added in t.

\r\n\r\n

Example:\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"}}} \ No newline at end of file diff --git a/test/mock/two-sum.submissions.html.20161006 b/test/mock/two-sum.submissions.html.20161006 deleted file mode 100644 index 28f02f97..00000000 --- a/test/mock/two-sum.submissions.html.20161006 +++ /dev/null @@ -1,385 +0,0 @@ - - - - - - - Two Sum | Submissions | LeetCode OJ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
-
-
-

- My Submissions for Two Sum -

-
- - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Submit Time - - Question - - Status - - Run Time - - Language -
3 weeks, 4 days ago - Two Sum - - - Accepted - - - 9 ms - cpp
3 weeks, 6 days ago - Two Sum - - - Wrong Answer - - - N/A - cpp
-
- - - - - - -
- -
-
-
- -
- - - - - - - Send Feedback - - - - - - - - - - - - - - diff --git a/test/plugins/test_cache.js b/test/plugins/test_cache.js new file mode 100644 index 00000000..6b3114ca --- /dev/null +++ b/test/plugins/test_cache.js @@ -0,0 +1,240 @@ +'use strict'; +const _ = require('underscore'); +const assert = require('chai').assert; +const rewire = require('rewire'); + +const h = require('../../lib/helper'); +const log = require('../../lib/log'); +const config = require('../../lib/config'); +const th = require('../helper'); + +describe('plugin:cache', function() { + let plugin; + let next; + let cache; + let file; + let session; + + const PROBLEMS = [ + {id: 0, fid: 0, name: 'name0', slug: 'slug0', starred: false, category: 'algorithms'}, + {id: 1, fid: 1, name: 'name1', slug: 'slug1', starred: true, category: 'algorithms'} + ]; + const PROBLEM = {id: 0, fid: 0, slug: 'slug0', category: 'algorithms'}; + + before(function() { + log.init(); + config.init(); + }); + + beforeEach(function() { + th.clean(); + next = {}; + + file = rewire('../../lib/file'); + file.cacheDir = () => th.DIR; + + cache = rewire('../../lib/cache'); + cache.__set__('file', file); + cache.init(); + + session = rewire('../../lib/session'); + session.__set__('cache', cache); + + plugin = rewire('../../lib/plugins/cache'); + plugin.__set__('cache', cache); + plugin.__set__('session', session); + plugin.init(); + + plugin.setNext(next); + }); + + describe('#getProblems', function() { + it('should getProblems w/ cache ok', function(done) { + cache.set('problems', PROBLEMS); + + plugin.getProblems(function(e, problems) { + assert.equal(e, null); + assert.deepEqual(problems, PROBLEMS); + done(); + }); + }); + + it('should getProblems w/o cache ok', function(done) { + cache.del('problems'); + next.getProblems = cb => cb(null, PROBLEMS); + + plugin.getProblems(function(e, problems) { + assert.equal(e, null); + assert.deepEqual(problems, PROBLEMS); + done(); + }); + }); + + it('should getProblems w/o cache fail if client error', function(done) { + cache.del('problems'); + next.getProblems = cb => cb('client getProblems error'); + + plugin.getProblems(function(e, problems) { + assert.equal(e, 'client getProblems error'); + done(); + }); + }); + }); // #getProblems + + describe('#getProblem', function() { + it('should getProblem w/ cache ok', function(done) { + cache.set('problems', PROBLEMS); + cache.set('0.slug0.algorithms', PROBLEMS[0]); + + plugin.getProblem(_.clone(PROBLEM), function(e, problem) { + assert.equal(e, null); + assert.deepEqual(problem, PROBLEMS[0]); + done(); + }); + }); + + it('should getProblem w/o cache ok', function(done) { + cache.set('problems', PROBLEMS); + cache.del('0.slug0.algorithms'); + next.getProblem = (problem, cb) => cb(null, PROBLEMS[0]); + + plugin.getProblem(_.clone(PROBLEM), function(e, problem) { + assert.equal(e, null); + assert.deepEqual(problem, PROBLEMS[0]); + done(); + }); + }); + + it('should getProblem fail if client error', function(done) { + cache.set('problems', PROBLEMS); + cache.del('0.slug0.algorithms'); + next.getProblem = (problem, cb) => cb('client getProblem error'); + + plugin.getProblem(_.clone(PROBLEM), function(e, problem) { + assert.equal(e, 'client getProblem error'); + done(); + }); + }); + }); // #getProblem + + describe('#saveProblem', function() { + it('should ok', function() { + cache.del('0.slug0.algorithms'); + + const problem = _.clone(PROBLEMS[0]); + problem.locked = true; + problem.state = 'ac'; + + const ret = plugin.saveProblem(problem); + assert.equal(ret, true); + assert.deepEqual(cache.get('0.slug0.algorithms'), + {id: 0, fid: 0, slug: 'slug0', name: 'name0', category: 'algorithms'}); + }); + }); // #saveProblem + + describe('#updateProblem', function() { + it('should updateProblem ok', function(done) { + cache.set('problems', PROBLEMS); + + const kv = {value: 'value00'}; + const ret = plugin.updateProblem(PROBLEMS[0], kv); + assert.equal(ret, true); + + plugin.getProblems(function(e, problems) { + assert.equal(e, null); + assert.deepEqual(problems, [ + {id: 0, fid: 0, name: 'name0', slug: 'slug0', value: 'value00', starred: false, category: 'algorithms'}, + {id: 1, fid: 1, name: 'name1', slug: 'slug1', starred: true, category: 'algorithms'} + ]); + done(); + }); + }); + + it('should updateProblem fail if no problems found', function() { + cache.del('problems'); + const ret = plugin.updateProblem(PROBLEMS[0], {}); + assert.equal(ret, false); + }); + + it('should updateProblem fail if unknown problem', function() { + cache.set('problems', [PROBLEMS[1]]); + const ret = plugin.updateProblem(PROBLEMS[0], {}); + assert.equal(ret, false); + }); + }); // #updateProblem + + describe('#user', function() { + const USER = {name: 'test-user', pass: 'password'}; + const USER_SAFE = {name: 'test-user'}; + + it('should login ok', function(done) { + config.autologin.enable = true; + // before login + cache.del(h.KEYS.user); + assert.equal(session.getUser(), null); + assert.equal(session.isLogin(), false); + + next.login = (user, cb) => cb(null, user); + + plugin.login(USER, function(e, user) { + assert.equal(e, null); + assert.deepEqual(user, USER); + + // after login + assert.deepEqual(session.getUser(), USER); + assert.equal(session.isLogin(), true); + done(); + }); + }); + + it('should login ok w/ auto login', function(done) { + config.autologin.enable = false; + cache.del(h.KEYS.user); + + next.login = (user, cb) => cb(null, user); + + plugin.login(USER, function(e, user) { + assert.equal(e, null); + assert.deepEqual(user, USER); + assert.deepEqual(session.getUser(), USER_SAFE); + assert.equal(session.isLogin(), true); + done(); + }); + }); + + it('should login fail if client login error', function(done) { + next.login = (user, cb) => cb('client login error'); + + plugin.login(USER, function(e, user) { + assert.equal(e, 'client login error'); + done(); + }); + }); + + it('should logout ok', function(done) { + // before logout + cache.set(h.KEYS.user, USER); + assert.deepEqual(session.getUser(), USER); + assert.equal(session.isLogin(), true); + + // after logout + plugin.logout(USER, true); + assert.equal(session.getUser(), null); + assert.equal(session.isLogin(), false); + done(); + }); + + it('should logout ok', function(done) { + // before logout + cache.set(h.KEYS.user, USER); + assert.deepEqual(session.getUser(), USER); + assert.equal(session.isLogin(), true); + + // after logout + plugin.logout(null, true); + assert.equal(session.getUser(), null); + assert.equal(session.isLogin(), false); + done(); + }); + }); // #user +}); diff --git a/test/plugins/test_leetcode.js b/test/plugins/test_leetcode.js new file mode 100644 index 00000000..ef099b9b --- /dev/null +++ b/test/plugins/test_leetcode.js @@ -0,0 +1,719 @@ +'use strict'; +const _ = require('underscore'); +const assert = require('chai').assert; +const nock = require('nock'); +const rewire = require('rewire'); + +const config = require('../../lib/config'); +const chalk = require('../../lib/chalk'); +const log = require('../../lib/log'); + +const plugin = rewire('../../lib/plugins/leetcode'); +const session = rewire('../../lib/session'); + +describe('plugin:leetcode', function() { + const USER = {hash: 'abcdef'}; + const PROBLEM = { + id: 389, + name: 'Find the Difference', + slug: 'find-the-difference', + link: 'https://leetcode.com/problems/find-the-difference', + locked: false, + file: '/dev/null' + }; + const SUBMISSION = { + id: '73790064', + lang: 'cpp', + runtime: '9 ms', + path: '/submissions/detail/73790064/', + state: 'Accepted' + }; + + before(function() { + log.init(); + config.init(); + chalk.init(); + plugin.init(); + + session.getUser = () => USER; + session.saveUser = () => {}; + plugin.__set__('session', session); + }); + + describe('#login', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .get('/accounts/login/') + .reply(200, '', { 'Set-Cookie': [ + 'csrftoken=LOGIN_CSRF_TOKEN; Max-Age=31449600; Path=/; secure' + ]}); + + nock('https://leetcode.com') + .post('/accounts/login/') + .reply(302, '', { + 'Set-Cookie': [ + 'csrftoken=SESSION_CSRF_TOKEN; Max-Age=31449600; Path=/; secure', + 'LEETCODE_SESSION=SESSION_ID; Max-Age=31449600; Path=/; secure' + ]}); + + nock('https://leetcode.com') + .get('/list/api/questions') + .reply(200, JSON.stringify({ + user_name: 'Eric', + favorites: { + private_favorites: [{id_hash: 'abcdef', name: 'Favorite'}] + } + })); + + plugin.login({}, function(e, user) { + assert.equal(e, null); + + assert.equal(user.loginCSRF, 'LOGIN_CSRF_TOKEN'); + assert.equal(user.sessionCSRF, 'SESSION_CSRF_TOKEN'); + assert.equal(user.sessionId, 'SESSION_ID'); + assert.equal(user.name, 'Eric'); + assert.equal(user.hash, 'abcdef'); + done(); + }); + }); + + it('should fail if http error', function(done) { + nock('https://leetcode.com') + .get('/accounts/login/') + .reply(200, '', { + 'Set-Cookie': [ + 'csrftoken=LOGIN_CSRF_TOKEN; Max-Age=31449600; Path=/; secure' + ]}); + + nock('https://leetcode.com') + .post('/accounts/login/') + .replyWithError('unknown error!'); + + plugin.login({}, function(e, user) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + + it('should fail if http error, 2nd', function(done) { + nock('https://leetcode.com') + .get('/accounts/login/') + .replyWithError('unknown error!'); + + plugin.login({}, function(e, user) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + }); // #login + + describe('#getProblems', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .get('/api/problems/algorithms/') + .replyWithFile(200, './test/mock/problems.json.20160911'); + + nock('https://leetcode.com') + .get('/api/problems/database/') + .replyWithFile(200, './test/mock/problems.json.20160911'); + + nock('https://leetcode.com') + .get('/api/problems/shell/') + .replyWithFile(200, './test/mock/problems.json.20160911'); + + plugin.getProblems(function(e, problems) { + assert.equal(e, null); + assert.equal(problems.length, 377 * 3); + done(); + }); + }); + + it('should fail if error occurs', function(done) { + nock('https://leetcode.com') + .get('/api/problems/algorithms/') + .replyWithFile(200, './test/mock/problems.json.20160911'); + + nock('https://leetcode.com') + .get('/api/problems/database/') + .replyWithError('unknown error'); + + nock('https://leetcode.com') + .get('/api/problems/shell/') + .replyWithFile(200, './test/mock/problems.json.20160911'); + + plugin.getProblems(function(e, problems) { + assert.equal(e.message, 'unknown error'); + done(); + }); + }); + }); // #getProblems + + describe('#getCategoryProblems', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .get('/api/problems/algorithms/') + .replyWithFile(200, './test/mock/problems.json.20160911'); + + plugin.getCategoryProblems('algorithms', function(e, problems) { + assert.equal(e, null); + assert.equal(problems.length, 377); + done(); + }); + }); + + it('should fail if not login', function(done) { + config.autologin.enable = false; + nock('https://leetcode.com') + .get('/api/problems/algorithms/') + .replyWithFile(200, './test/mock/problems.nologin.json.20161015'); + + plugin.getCategoryProblems('algorithms', function(e, problems) { + assert.deepEqual(e, session.errors.EXPIRED); + done(); + }); + }); + }); // #getCategoryProblems + + describe('#getProblem', function() { + beforeEach(function() { + PROBLEM.locked = false; + }); + + it('should ok', function(done) { + nock('https://leetcode.com') + .post('/graphql') + .replyWithFile(200, './test/mock/find-the-difference.json.20171216'); + + plugin.getProblem(PROBLEM, function(e, problem) { + assert.equal(e, null); + assert.equal(problem.totalAC, '89.7K'); + assert.equal(problem.totalSubmit, '175.7K'); + assert.equal(problem.desc, + [ + '', + 'Given two strings s and t which consist of only lowercase letters.', + '', + 'String t is generated by random shuffling string s and then add one more letter at a random position.', + '', + 'Find the letter that was added in t.', + '', + 'Example:', + '', + 'Input:', + 's = "abcd"', + 't = "abcde"', + '', + 'Output:', + 'e', + '', + 'Explanation:', + "'e' is the letter that was added.", + '' + ].join('\r\n')); + + assert.equal(problem.templates.length, 12); + + assert.equal(problem.templates[0].value, 'cpp'); + assert.equal(problem.templates[0].text, 'C++'); + assert.equal(problem.templates[0].defaultCode, + [ + 'class Solution {', + 'public:', + ' char findTheDifference(string s, string t) {', + ' ', + ' }', + '};' + ].join('\r\n')); + + assert.equal(problem.templates[1].value, 'java'); + assert.equal(problem.templates[1].text, 'Java'); + assert.equal(problem.templates[1].defaultCode, + [ + 'class Solution {', + ' public char findTheDifference(String s, String t) {', + ' ', + ' }', + '}' + ].join('\r\n')); + + assert.equal(problem.templates[2].value, 'python'); + assert.equal(problem.templates[2].text, 'Python'); + assert.equal(problem.templates[2].defaultCode, + [ + 'class Solution(object):', + ' def findTheDifference(self, s, t):', + ' """', + ' :type s: str', + ' :type t: str', + ' :rtype: str', + ' """', + ' ' + ].join('\r\n')); + + assert.equal(problem.templates[3].value, 'python3'); + assert.equal(problem.templates[3].text, 'Python3'); + assert.equal(problem.templates[3].defaultCode, + [ + 'class Solution:', + ' def findTheDifference(self, s, t):', + ' """', + ' :type s: str', + ' :type t: str', + ' :rtype: str', + ' """', + ' ' + ].join('\r\n')); + + assert.equal(problem.templates[4].value, 'c'); + assert.equal(problem.templates[4].text, 'C'); + assert.equal(problem.templates[4].defaultCode, + [ + 'char findTheDifference(char* s, char* t) {', + ' ', + '}' + ].join('\r\n')); + + assert.equal(problem.templates[5].value, 'csharp'); + assert.equal(problem.templates[5].text, 'C#'); + assert.equal(problem.templates[5].defaultCode, + [ + 'public class Solution {', + ' public char FindTheDifference(string s, string t) {', + ' ', + ' }', + '}' + ].join('\r\n')); + + assert.equal(problem.templates[6].value, 'javascript'); + assert.equal(problem.templates[6].text, 'JavaScript'); + assert.equal(problem.templates[6].defaultCode, + [ + '/**', + ' * @param {string} s', + ' * @param {string} t', + ' * @return {character}', + ' */', + 'var findTheDifference = function(s, t) {', + ' ', + '};' + ].join('\r\n')); + + assert.equal(problem.templates[7].value, 'ruby'); + assert.equal(problem.templates[7].text, 'Ruby'); + assert.equal(problem.templates[7].defaultCode, + [ + '# @param {String} s', + '# @param {String} t', + '# @return {Character}', + 'def find_the_difference(s, t)', + ' ', + 'end' + ].join('\r\n')); + + assert.equal(problem.templates[8].value, 'swift'); + assert.equal(problem.templates[8].text, 'Swift'); + assert.equal(problem.templates[8].defaultCode, + [ + 'class Solution {', + ' func findTheDifference(_ s: String, _ t: String) -> Character {', + ' ', + ' }', + '}' + ].join('\r\n')); + + assert.equal(problem.templates[9].value, 'golang'); + assert.equal(problem.templates[9].text, 'Go'); + assert.equal(problem.templates[9].defaultCode, + [ + 'func findTheDifference(s string, t string) byte {', + ' ', + '}' + ].join('\r\n')); + + assert.equal(problem.templates[10].value, 'scala'); + assert.equal(problem.templates[10].text, 'Scala'); + assert.equal(problem.templates[10].defaultCode, + [ + 'object Solution {', + ' def findTheDifference(s: String, t: String): Char = {', + ' ', + ' }', + '}' + ].join('\n')); + + assert.equal(problem.templates[11].value, 'kotlin'); + assert.equal(problem.templates[11].text, 'Kotlin'); + assert.equal(problem.templates[11].defaultCode, + [ + 'class Solution {', + ' fun findTheDifference(s: String, t: String): Char {', + ' ', + ' }', + '}' + ].join('\n')); + + done(); + }); + }); + + it('should fail if no permission for locked', function(done) { + PROBLEM.locked = true; + + plugin.getProblem(PROBLEM, function(e, problem) { + assert.equal(e, 'failed to load locked problem!'); + done(); + }); + }); + + it('should fail if session expired', function(done) { + nock('https://leetcode.com').post('/graphql').reply(403); + + plugin.getProblem(PROBLEM, function(e, problem) { + assert.equal(e, session.errors.EXPIRED); + done(); + }); + }); + + it('should fail if http error', function(done) { + nock('https://leetcode.com').post('/graphql').reply(500); + + plugin.getProblem(PROBLEM, function(e, problem) { + assert.deepEqual(e, {msg: 'http error', statusCode: 500}); + done(); + }); + }); + + it('should fail if unknown error', function(done) { + nock('https://leetcode.com').post('/graphql').replyWithError('unknown error!'); + + plugin.getProblem(PROBLEM, function(e, problem) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + }); // #getProblem + + describe('#testProblem', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .post('/problems/find-the-difference/interpret_solution/') + .reply(200, '{"interpret_expected_id": "id1", "interpret_id": "id2"}'); + + nock('https://leetcode.com') + .get('/submissions/detail/id1/check/') + .reply(200, '{"state": "SUCCESS", "run_success": true, "status_code": 10}'); + + nock('https://leetcode.com') + .get('/submissions/detail/id2/check/') + .reply(200, '{"state": "SUCCESS", "run_success": false, "status_code": 15}'); + + plugin.testProblem(PROBLEM, function(e, results) { + assert.equal(e, null); + assert.equal(results[0].id, 'id2'); + assert.equal(results[0].ok, false); + assert.equal(results[1].id, 'id1'); + assert.equal(results[1].ok, true); + done(); + }); + }); + + it('should fail if http error', function(done) { + nock('https://leetcode.com') + .post('/problems/find-the-difference/interpret_solution/') + .replyWithError('unknown error!'); + + plugin.testProblem(PROBLEM, function(e, results) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + }); // #testProblem + + describe('#submitProblem', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .post('/problems/find-the-difference/submit/') + .reply(200, '{"submission_id": "id1"}'); + + nock('https://leetcode.com') + .get('/submissions/detail/id1/check/') + .reply(200, '{"state": "SUCCESS", "run_success": true, "status_code": 10}'); + + plugin.submitProblem(PROBLEM, function(e, results) { + assert.equal(e, null); + assert.equal(results[0].id, 'id1'); + assert.equal(results[0].ok, true); + done(); + }); + }); + + it('should ok after delay', function(done) { + nock('https://leetcode.com') + .post('/problems/find-the-difference/submit/') + .reply(200, '{"error": "You run code too soon"}'); + nock('https://leetcode.com') + .post('/problems/find-the-difference/submit/') + .reply(200, '{"submission_id": "id1"}'); + + nock('https://leetcode.com') + .get('/submissions/detail/id1/check/') + .reply(200, '{"state": "STARTED"}'); + nock('https://leetcode.com') + .get('/submissions/detail/id1/check/') + .reply(200, '{"state": "SUCCESS", "run_success": true, "status_code": 10}'); + + plugin.submitProblem(PROBLEM, function(e, results) { + assert.equal(e, null); + assert.equal(results[0].id, 'id1'); + assert.equal(results[0].ok, true); + done(); + }); + }).timeout(5000); + + it('should fail if server error', function(done) { + nock('https://leetcode.com') + .post('/problems/find-the-difference/submit/') + .reply(200, '{"error": "maybe internal error?"}'); + + plugin.submitProblem(PROBLEM, function(e, results) { + assert.equal(e, 'maybe internal error?'); + done(); + }); + }); + + it('should fail if server error in check result', function(done) { + nock('https://leetcode.com') + .post('/problems/find-the-difference/submit/') + .reply(200, '{"submission_id": "id1"}'); + + nock('https://leetcode.com') + .get('/submissions/detail/id1/check/') + .replyWithError('unknown error!'); + + plugin.submitProblem(PROBLEM, function(e, results) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + }); // #submitProblem + + describe('#starProblem', function() { + it('should star ok', function(done) { + nock('https://leetcode.com') + .post('/list/api/questions') + .reply(204, ''); + + plugin.starProblem(PROBLEM, true, function(e, starred) { + assert.equal(e, null); + assert.equal(starred, true); + done(); + }); + }); + + it('should unstar ok', function(done) { + nock('https://leetcode.com') + .delete('/list/api/questions/abcdef/389') + .reply(204, ''); + + plugin.starProblem(PROBLEM, false, function(e, starred) { + assert.equal(e, null); + assert.equal(starred, false); + done(); + }); + }); + + it('should star fail if http error', function(done) { + nock('https://leetcode.com') + .post('/list/api/questions') + .replyWithError('unknown error!'); + + plugin.starProblem(PROBLEM, true, function(e, starred) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + }); // #starProblem + + describe('#getSubmissions', function() { + it('should ok', function(done) { + const problem = { + id: 1, + name: 'Two Sum', + slug: 'two-sum', + link: 'https://leetcode.com/problems/two-sum', + locked: false + }; + + nock('https://leetcode.com') + .get('/api/submissions/two-sum') + .replyWithFile(200, './test/mock/two-sum.submissions.json.20170425'); + + plugin.getSubmissions(problem, function(e, submissions) { + assert.equal(e, null); + assert.equal(submissions.length, 20); + + assert.deepEqual(submissions[0], { + id: '95464136', + title: 'Two Sum', + is_pending: false, + lang: 'cpp', + time: '1 month, 3 weeks', + runtime: '12 ms', + url: '/submissions/detail/95464136/', + status_display: 'Accepted' + }); + + assert.deepEqual(submissions[1], { + id: '78502271', + title: 'Two Sum', + is_pending: false, + lang: 'cpp', + time: '6 months, 1 week', + runtime: '13 ms', + url: '/submissions/detail/78502271/', + status_display: 'Accepted' + }); + done(); + }); + }); + + it('should fail if http error', function(done) { + nock('https://leetcode.com') + .get('/api/submissions/find-the-difference') + .replyWithError('unknown error!'); + + plugin.getSubmissions(PROBLEM, function(e, submissions) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + }); // #getSubmissions + + describe('#getSubmission', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .get('/submissions/detail/73790064/') + .replyWithFile(200, './test/mock/two-sum.submission.73790064.html.20161006'); + + plugin.getSubmission(_.clone(SUBMISSION), function(e, submission) { + assert.equal(e, null); + assert.deepEqual(submission.code, + [ + 'class Solution {', + 'public:', + ' vector twoSum(vector& nums, int target) {', + ' return res;', + ' }', + '};', + '' + ].join('\r\n')); + done(); + }); + }); + + it('should fail if http error', function(done) { + nock('https://leetcode.com') + .get('/submissions/detail/73790064/') + .replyWithError('unknown error!'); + + plugin.getSubmission(_.clone(SUBMISSION), function(e, submission) { + assert.equal(e.message, 'unknown error!'); + done(); + }); + }); + + it('should fail if no matching submission', function(done) { + nock('https://leetcode.com') + .get('/submissions/detail/73790064/') + .replyWithFile(200, './test/mock/locked.html.20161015'); + + plugin.getSubmission(_.clone(SUBMISSION), function(e, submission) { + assert.equal(e, null); + assert.equal(submission.code, null); + done(); + }); + }); + }); // #getSubmission + + describe('#getFavorites', function() { + it('should ok', function(done) { + nock('https://leetcode.com') + .get('/list/api/questions') + .replyWithFile(200, './test/mock/favorites.json.20170716'); + + plugin.getFavorites(function(e, favorites) { + assert.equal(e, null); + + const my = favorites.favorites.private_favorites; + assert.equal(my.length, 1); + assert.equal(my[0].name, 'Favorite'); + assert.equal(my[0].id_hash, 'abcdefg'); + done(); + }); + }); + }); // #getFavorites + + describe('#session', function() { + const DATA = {sessions: []}; + + it('should getSessions ok', function(done) { + nock('https://leetcode.com') + .post('/session/') + .reply(200, JSON.stringify(DATA)); + + plugin.getSessions(function(e, sessions) { + assert.notExists(e); + assert.deepEqual(sessions, []); + done(); + }); + }); + + it('should activateSessions ok', function(done) { + nock('https://leetcode.com') + .put('/session/', {func: 'activate', target: 1}) + .reply(200, JSON.stringify(DATA)); + + plugin.activateSession({id: 1}, function(e, sessions) { + assert.notExists(e); + assert.deepEqual(sessions, []); + done(); + }); + }); + + it('should createSessions ok', function(done) { + nock('https://leetcode.com') + .put('/session/', {func: 'create', name: 's1'}) + .reply(200, JSON.stringify(DATA)); + + plugin.createSession('s1', function(e, sessions) { + assert.notExists(e); + assert.deepEqual(sessions, []); + done(); + }); + }); + + it('should deleteSessions ok', function(done) { + nock('https://leetcode.com') + .delete('/session/', {target: 1}) + .reply(200, JSON.stringify(DATA)); + + plugin.deleteSession({id: 1}, function(e, sessions) { + assert.notExists(e); + assert.deepEqual(sessions, []); + done(); + }); + }); + + it('should fail if 302 returned', function(done) { + nock('https://leetcode.com') + .post('/session/') + .reply(302); + + plugin.getSessions(function(e, sessions) { + assert.deepEqual(e, session.errors.EXPIRED); + assert.notExists(sessions); + done(); + }); + }); + }); // #session +}); diff --git a/test/plugins/test_retry.js b/test/plugins/test_retry.js new file mode 100644 index 00000000..dbdb060c --- /dev/null +++ b/test/plugins/test_retry.js @@ -0,0 +1,98 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + +const log = require('../../lib/log'); + +const config = rewire('../../lib/config'); +const session = rewire('../../lib/session'); +const plugin = rewire('../../lib/plugins/retry'); + +describe('plugin:retry', function() { + const USER = {}; + const NEXT = {}; + const PROBLEMS = [{id: 0, name: 'name0'}]; + + before(function() { + log.init(); + config.init(); + plugin.init(); + + session.getUser = () => USER; + + plugin.__set__('config', config); + plugin.__set__('session', session); + plugin.setNext(NEXT); + }); + + it('should fail if auto login disabled', function(done) { + config.autologin.enable = false; + NEXT.getProblems = cb => cb(session.errors.EXPIRED); + + plugin.getProblems(function(e, problems) { + assert.equal(e, session.errors.EXPIRED); + done(); + }); + }); + + it('should retry ok if finally ok', function(done) { + config.autologin.enable = true; + config.autologin.retry = 3; + + let n = 0; + NEXT.getProblems = function(cb) { + return ++n <= 3 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS); + }; + NEXT.login = (user, cb) => cb(null, user); + + plugin.getProblems(function(e, problems) { + assert.notExists(e); + assert.equal(problems, PROBLEMS); + done(); + }); + }); + + it('should retry fail if always failed', function(done) { + config.autologin.enable = true; + config.autologin.retry = 2; + + let n = 0; + NEXT.getProblems = function(cb) { + return ++n <= 3 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS); + }; + NEXT.login = (user, cb) => { + return n == 1 ? cb(null, user) : cb('login failed'); + } + + plugin.getProblems(function(e) { + assert.deepEqual(e, session.errors.EXPIRED); + done(); + }); + }); + + it('should fail if user expired locally', function(done) { + config.autologin.enable = true; + + let n = 0; + NEXT.getProblems = function(cb) { + return ++n === 1 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS); + }; + session.getUser = () => null; + + plugin.getProblems(function(e, problems) { + assert.notExists(e); + assert.equal(problems, PROBLEMS); + done(); + }); + }); + + it('should fail if other errors', function(done) { + config.autologin.enable = true; + NEXT.getProblems = cb => cb('unknown error'); + + plugin.getProblems(function(e, problems) { + assert.equal(e, 'unknown error'); + done(); + }); + }); +}); diff --git a/test/test_cache.js b/test/test_cache.js index fd9d47a1..caba14c1 100644 --- a/test/test_cache.js +++ b/test/test_cache.js @@ -1,36 +1,48 @@ -var execSync = require('child_process').execSync; +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); -var assert = require('chai').assert; -var rewire = require('rewire'); - -var cache = rewire('../lib/cache'); -var h = rewire('../lib/helper'); +const th = require('./helper'); describe('cache', function() { - var k = '.test'; - var v = {test: 'data'}; + let cache; + + const K = '.test'; + const V = {test: 'data'}; + + beforeEach(function() { + th.clean(); - before(function() { - var cachedir = './tmp'; - execSync('rm -rf ' + cachedir); + const file = rewire('../lib/file'); + file.cacheDir = () => th.DIR; - h.getCacheDir = function() { - return cachedir; - }; - cache.__set__('h', h); + cache = rewire('../lib/cache'); + cache.__set__('file', file); + cache.init(); }); - it('should ok when not cached', function() { - cache.del(k); + it('should get ok when not cached', function() { + cache.del(K); + assert.equal(cache.get(K), null); + assert.equal(cache.del(K), false); + }); - assert.equal(cache.get(k), null); - assert.equal(cache.del(k), false); + it('should get ok when cached', function() { + assert.equal(cache.set(K, V), true); + assert.deepEqual(cache.get(K), V); + assert.equal(cache.del(K), true); }); - it('should ok when cached', function() { - assert.equal(cache.set(k, v), true); + it('should list ok when no cached', function() { + const items = cache.list(); + assert.equal(items.length, 0); + }); - assert.deepEqual(cache.get(k), v); - assert.equal(cache.del(k), true); + it('should list ok when cached', function() { + assert.equal(cache.set(K, V), true); + const items = cache.list(); + assert.equal(items.length, 1); + assert.equal(items[0].name, K); + assert.equal(items[0].size, JSON.stringify(V).length); }); }); diff --git a/test/test_chalk.js b/test/test_chalk.js new file mode 100644 index 00000000..b0c17089 --- /dev/null +++ b/test/test_chalk.js @@ -0,0 +1,91 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + +// refer to https://en.wikipedia.org/wiki/ANSI_escape_code +describe('chalk', function() { + let chalk; + + beforeEach(function() { + chalk = rewire('../lib/chalk'); + chalk.enabled = true; + chalk.use256 = true; + chalk.use16m = false; + }); + + it('should ok w/ 256 colors', function() { + chalk.init(); + chalk.setTheme('default'); + + assert.equal(chalk.black(' '), '\u001b[38;5;16m \u001b[39m'); + assert.equal(chalk.red(' '), '\u001b[38;5;196m \u001b[39m'); + assert.equal(chalk.green(' '), '\u001b[38;5;46m \u001b[39m'); + assert.equal(chalk.yellow(' '), '\u001b[38;5;226m \u001b[39m'); + assert.equal(chalk.blue(' '), '\u001b[38;5;21m \u001b[39m'); + assert.equal(chalk.magenta(' '), '\u001b[38;5;201m \u001b[39m'); + assert.equal(chalk.cyan(' '), '\u001b[38;5;51m \u001b[39m'); + assert.equal(chalk.white(' '), '\u001b[38;5;231m \u001b[39m'); + + assert.equal(chalk.bold(' '), '\u001b[1m \u001b[22m'); + assert.equal(chalk.dim(' '), '\u001b[2m \u001b[22m'); + assert.equal(chalk.italic(' '), '\u001b[3m \u001b[23m'); + assert.equal(chalk.inverse(' '), '\u001b[7m \u001b[27m'); + assert.equal(chalk.strikethrough(' '), '\u001b[9m \u001b[29m'); + assert.equal(chalk.underline(' '), '\u001b[4m \u001b[24m'); + }); + + it('should ok w/ 8 colors', function() { + chalk.use256 = false; + chalk.init(); + chalk.setTheme('default'); + + assert.equal(chalk.black(' '), '\u001b[30m \u001b[39m'); + assert.equal(chalk.red(' '), '\u001b[91m \u001b[39m'); + assert.equal(chalk.green(' '), '\u001b[92m \u001b[39m'); + assert.equal(chalk.yellow(' '), '\u001b[93m \u001b[39m'); + assert.equal(chalk.blue(' '), '\u001b[94m \u001b[39m'); + assert.equal(chalk.magenta(' '), '\u001b[95m \u001b[39m'); + assert.equal(chalk.cyan(' '), '\u001b[96m \u001b[39m'); + assert.equal(chalk.white(' '), '\u001b[97m \u001b[39m'); + }); + + it('should ok w/o colors', function() { + chalk.enabled = false; + chalk.init(); + chalk.setTheme('default'); + + assert.equal(chalk.black(' '), ' '); + assert.equal(chalk.red(' '), ' '); + assert.equal(chalk.green(' '), ' '); + assert.equal(chalk.yellow(' '), ' '); + assert.equal(chalk.blue(' '), ' '); + assert.equal(chalk.magenta(' '), ' '); + assert.equal(chalk.cyan(' '), ' '); + assert.equal(chalk.white(' '), ' '); + }); + + it('should sprint w/ 256 colors ok', function() { + chalk.init(); + chalk.setTheme('default'); + assert.equal(chalk.sprint(' ', '#00ff00'), '\u001b[38;5;46m \u001b[39m'); + }); + + it('should sprint w/ 8 colors ok', function() { + chalk.use256 = false; + chalk.init(); + chalk.setTheme('default'); + assert.equal(chalk.sprint(' ', '#00ff00'), '\u001b[92m \u001b[39m'); + }); + + it('should set theme ok', function() { + chalk.init(); + chalk.setTheme('dark'); + assert.equal(chalk.sprint(' ', '#009900'), chalk.green(' ')); + }); + + it('should set unknown theme ok', function() { + chalk.init(); + chalk.setTheme('unknown'); + assert.equal(chalk.sprint(' ', '#00ff00'), chalk.green(' ')); + }); +}); diff --git a/test/test_config.js b/test/test_config.js index 684ed29f..9ae828dd 100644 --- a/test/test_config.js +++ b/test/test_config.js @@ -1,37 +1,64 @@ -var assert = require('chai').assert; -var rewire = require('rewire'); -var _ = require('underscore'); +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); +const _ = require('underscore'); + +const th = require('./helper'); describe('config', function() { - it('should ok w/o local config', function() { - var h = rewire('../lib/helper'); - h.getConfigFile = function() { - return 'local-config-not-exist-at-all'; - }; + let config; + const FILE = './tmp/config.json'; + + beforeEach(function() { + th.clean(); + + const file = rewire('../lib/file'); + file.configFile = () => FILE; + + config = rewire('../lib/config'); + config.__set__('file', file); + }); - var config = rewire('../lib/config'); - config.__set__('h', h); + function createConfigFile(data) { + const fs = require('fs'); + fs.writeFileSync(FILE, JSON.stringify(data)); + } + + it('should ok w/o local config', function() { + const DEFAULT_CONFIG = config.__get__('DEFAULT_CONFIG'); config.init(); - var expect = config.__get__('DEFAULT_CONFIG'); - var actual = _.extendOwn({}, config); // remove 'init' function - assert.equal(_.isEqual(actual, expect), true); + let actual = config.getAll(); + let expect = DEFAULT_CONFIG; + assert.deepEqual(actual, expect); + + actual = config.getAll(true); + expect = _.omit(expect, 'sys'); + assert.deepEqual(actual, expect); }); it('should ok w/ local config', function() { - var localConfig = {LANG: 'ruby', USE_COLOR: false, AUTO_LOGIN: false}; + createConfigFile({ + autologin: {enable: false}, + code: {lang: 'ruby'}, + color: {enable: false} + }); + config.init(); - var h = rewire('../lib/helper'); - h.getFileData = function() { - return JSON.stringify(localConfig); - }; + assert.equal(config.autologin.enable, false); + assert.equal(config.code.lang, 'ruby'); + assert.equal(config.color.enable, false); + assert.equal(config.code.editor, 'vim'); + }); - var config = rewire('../lib/config'); - config.__set__('h', h); + it('should remove legacy keys', function() { + createConfigFile({ + USE_COLOR: true, + code: {lang: 'ruby'} + }); config.init(); - var expect = _.extendOwn(config.__get__('DEFAULT_CONFIG'), localConfig); - var actual = _.extendOwn({}, config); // remove 'init' function - assert.equal(_.isEqual(actual, expect), true); + assert.equal(config.USE_COLOR, undefined); + assert.equal(config.code.lang, 'ruby'); }); }); diff --git a/test/test_core.js b/test/test_core.js index 3f0168eb..0a436bb4 100644 --- a/test/test_core.js +++ b/test/test_core.js @@ -1,463 +1,366 @@ -var execSync = require('child_process').execSync; -var fs = require('fs'); - -var _ = require('underscore'); -var assert = require('chai').assert; -var rewire = require('rewire'); - -// mock depedencies -var cache = rewire('../lib/cache'); -var client = rewire('../lib/leetcode_client'); -var config = rewire('../lib/config'); -var core = rewire('../lib/core'); -var h = rewire('../lib/helper'); +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); describe('core', function() { + let core; + let next; + + const PROBLEMS = [ + { + category: 'algorithms', + id: 0, + fid: 0, + name: 'name0', + slug: 'slug0', + level: 'Hard', + locked: true, + starred: false, + state: 'ac', + tags: ['google', 'facebook'] + }, + { + category: 'algorithms', + companies: ['amazon', 'facebook'], + id: 1, + fid: 1, + name: 'name1', + slug: 'slug1', + level: 'Easy', + locked: false, + starred: true, + state: 'none' + } + ]; + before(function() { - var home = './tmp'; - execSync('rm -rf ' + home); - fs.mkdirSync(home); - - h.getHomeDir = function() { - return home; - }; - - cache.__set__('h', h); - core.__set__('cache', cache); - core.__set__('client', client); - core.__set__('config', config); + const log = require('../lib/log'); + log.init(); }); - describe('#user', function() { - var USER = {name: 'test-user', pass: 'password'}; - var SAFE_USER = {name: 'test-user'}; + beforeEach(function() { + next = {}; + next.getProblems = cb => cb(null, PROBLEMS); + next.getProblem = (p, cb) => cb(null, p); - it('should login ok', function(done) { - config.AUTO_LOGIN = true; - // before login - cache.del('.user'); - assert.equal(core.getUser(), null); - assert.equal(core.isLogin(), false); + core = rewire('../lib/core'); + core.setNext(next); + }); - client.login = function(user, cb) { - return cb(null, user); - }; + describe('#filterProblems', function() { + it('should filter by query ok', function(done) { + const cases = [ + ['', [0, 1]], + ['x', [0, 1]], + ['h', [0]], + ['H', [1]], + ['m', []], + ['M', [0, 1]], + ['l', [0]], + ['L', [1]], + ['s', [1]], + ['S', [0]], + ['d', [0]], + ['D', [1]], + ['eLsD', [1]], + ['Dh', []] + ]; + let n = cases.length; + + for (let x of cases) { + core.filterProblems({query: x[0]}, function(e, problems) { + assert.notExists(e); + assert.equal(problems.length, x[1].length); + + for (let i = 0; i < problems.length; ++i) + assert.equal(problems[i], PROBLEMS[x[1][i]]); + if (--n === 0) done(); + }); + } + }); - core.login(USER, function(e, user) { - assert.equal(e, null); - assert.deepEqual(USER, user); + it('should filter by tag ok', function(done) { + const cases = [ + [[], [0, 1]], + [['facebook'], [0, 1]], + [['google'], [0]], + [['amazon'], [1]], + [['apple'], []], + ]; + let n = cases.length; + + for (let x of cases) { + core.filterProblems({tag: x[0]}, function(e, problems) { + assert.notExists(e); + assert.equal(problems.length, x[1].length); + + for (let i = 0; i < problems.length; ++i) + assert.equal(problems[i], PROBLEMS[x[1][i]]); + if (--n === 0) done(); + }); + } + }); - // after login - assert.deepEqual(core.getUser(), user); - assert.equal(core.isLogin(), true); + it('should fail if getProblems error', function(done) { + next.getProblems = cb => cb('getProblems error'); + core.filterProblems({}, function(e) { + assert.equal(e, 'getProblems error'); done(); }); }); + }); // #filterProblems - it('should login ok w/ auto login', function(done) { - config.AUTO_LOGIN = false; - cache.del('.user'); + describe('#starProblem', function() { + it('should ok', function(done) { + next.starProblem = (p, starred, cb) => cb(null, starred); - client.login = function(user, cb) { - return cb(null, user); - }; - - core.login(USER, function(e, user) { - assert.equal(e, null); - assert.deepEqual(USER, user); - assert.deepEqual(SAFE_USER, core.getUser()); - assert.equal(core.isLogin(), true); + assert.equal(PROBLEMS[0].starred, false); + core.starProblem(PROBLEMS[0], true, function(e, starred) { + assert.notExists(e); + assert.equal(starred, true); done(); }); }); - it('should login fail if client login error', function(done) { - client.login = function(user, cb) { - return cb('client login error'); - }; - - core.login(USER, function(e, user) { - assert.equal(e, 'client login error'); + it('should ok if already starred', function(done) { + assert.equal(PROBLEMS[1].starred, true); + core.starProblem(PROBLEMS[1], true, function(e, starred) { + assert.notExists(e); + assert.equal(starred, true); done(); }); }); - it('should logout ok', function(done) { - // before logout - cache.set('.user', USER); - assert.deepEqual(core.getUser(), USER); - assert.equal(core.isLogin(), true); - - // after logout - core.logout(USER); - assert.equal(core.getUser(), null); - assert.equal(core.isLogin(), false); - done(); - }); - }); // #user - - describe('#problems', function() { - var PROBLEMS = [ - {id: 0, name: 'name0', key: 'key0', starred: false}, - {id: 1, name: 'name1', key: 'key1', starred: true} - ]; - var RESULTS = [ - {name: 'result0'}, - {name: 'result1'} - ]; - - describe('#getProblems', function() { - it('should getProblems w/ cache ok', function(done) { - cache.set('all', PROBLEMS); - - core.getProblems(function(e, problems) { - assert.equal(e, null); - assert.deepEqual(problems, PROBLEMS); - done(); - }); - }); - - it('should getProblems w/o cache ok', function(done) { - cache.del('all'); - - client.getProblems = function(cb) { - return cb(null, PROBLEMS); - }; - - core.getProblems(function(e, problems) { - assert.equal(e, null); - assert.deepEqual(problems, PROBLEMS); - done(); - }); - }); - - it('should getProblems w/o cache fail if client error', function(done) { - cache.del('all'); - - client.getProblems = function(cb) { - return cb('client getProblems error'); - }; - - core.getProblems(function(e, problems) { - assert.equal(e, 'client getProblems error'); - done(); - }); - }); - }); // #getProblems - - describe('#getProblem', function() { - it('should getProblem by id w/ cache ok', function(done) { - cache.set('all', PROBLEMS); - cache.set('key0', PROBLEMS[0]); - - core.getProblem(0, function(e, problem) { - assert.equal(e, null); - assert.deepEqual(problem, PROBLEMS[0]); - done(); - }); - }); - - it('should getProblem by name w/ cache ok', function(done) { - cache.set('all', PROBLEMS); - cache.set('key0', PROBLEMS[0]); - - core.getProblem('name0', function(e, problem) { - assert.equal(e, null); - assert.deepEqual(problem, PROBLEMS[0]); - done(); - }); - }); - - it('should getProblem by key w/ cache ok', function(done) { - cache.set('all', PROBLEMS); - cache.set('key0', PROBLEMS[0]); - - core.getProblem('key0', function(e, problem) { - assert.equal(e, null); - assert.deepEqual(problem, PROBLEMS[0]); - done(); - }); - }); - - it('should getProblem by id w/o cache ok', function(done) { - cache.set('all', PROBLEMS); - cache.del('key0'); - - client.getProblem = function(problem, cb) { - return cb(null, problem); - }; - - core.getProblem(0, function(e, problem) { - assert.equal(e, null); - assert.deepEqual(problem, PROBLEMS[0]); - done(); - }); - }); - - it('should getProblem error if not found', function(done) { - cache.set('all', PROBLEMS); - - core.getProblem(3, function(e, problem) { - assert.equal(e, 'Problem not found!'); - done(); - }); - }); - - it('should getProblem fail if client error', function(done) { - cache.set('all', PROBLEMS); - cache.del('key0'); - - client.getProblem = function(problem, cb) { - return cb('client getProblem error'); - }; - - core.getProblem(0, function(e, problem) { - assert.equal(e, 'client getProblem error'); - done(); - }); + it('should ok if already unstarred', function(done) { + assert.equal(PROBLEMS[0].starred, false); + core.starProblem(PROBLEMS[0], false, function(e, starred) { + assert.notExists(e); + assert.equal(starred, false); + done(); }); + }); + }); // #starProblem - it('should getProblem fail if getProblems error', function(done) { - cache.del('all'); - client.getProblems = function(cb) { - return cb('getProblems error'); - }; + describe('#exportProblem', function() { + let file; - core.getProblem(0, function(e, problem) { - assert.equal(e, 'getProblems error'); - done(); - }); - }); - }); // #getProblem - - describe('#updateProblem', function() { - it('should updateProblem ok', function(done) { - cache.set('all', PROBLEMS); - cache.del('key0'); - - var kv = {value: 'value00'}; - var ret = core.updateProblem(PROBLEMS[0], kv); - assert.equal(ret, true); - - core.getProblem(0, function(e, problem) { - assert.equal(e, null); - assert.deepEqual(problem, - {id: 0, name: 'name0', key: 'key0', value: 'value00', starred: false}); - done(); - }); - }); - - it('should updateProblem fail if no problems found', function() { - cache.del('all'); - var ret = core.updateProblem(PROBLEMS[0], {}); - assert.equal(ret, false); - }); + beforeEach(function() { + file = rewire('../lib/file'); + file.init(); + core.__set__('file', file); + }); - it('should updateProblem fail if unknown problem', function() { - cache.set('all', [PROBLEMS[1]]); - var ret = core.updateProblem(PROBLEMS[0], {}); - assert.equal(ret, false); - }); - }); // #updateProblem - - describe('#starProblem', function() { - it('should starProblem ok', function(done) { - client.starProblem = function(problem, starred, cb) { - return cb(null, starred); - }; - - assert.equal(PROBLEMS[0].starred, false); - core.starProblem(PROBLEMS[0], true, function(e, starred) { - assert.equal(e, null); - assert.equal(starred, true); - done(); - }); - }); + it('should codeonly ok', function() { + file.isWindows = () => false; + + const expected = [ + '/**', + ' * Definition for singly-linked list.', + ' * struct ListNode {', + ' * int val;', + ' * ListNode *next;', + ' * ListNode(int x) : val(x), next(NULL) {}', + ' * };', + ' */', + 'class Solution {', + 'public:', + ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {', + ' ', + ' }', + '};', + '' + ].join('\n'); + + const problem = require('./mock/add-two-numbers.20161015.json'); + const opts = { + lang: 'cpp', + code: problem.templates[0].defaultCode, + tpl: 'codeonly' + }; + assert.equal(core.exportProblem(problem, opts), expected); + }); - it('should starProblem ok if already starred', function(done) { - assert.equal(PROBLEMS[1].starred, true); - core.starProblem(PROBLEMS[1], true, function(e, starred) { - assert.equal(e, null); - assert.equal(starred, true); - done(); - }); - }); + it('should codeonly ok in windows', function() { + file.isWindows = () => true; + + const expected = [ + '/**', + ' * Definition for singly-linked list.', + ' * struct ListNode {', + ' * int val;', + ' * ListNode *next;', + ' * ListNode(int x) : val(x), next(NULL) {}', + ' * };', + ' */', + 'class Solution {', + 'public:', + ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {', + ' ', + ' }', + '};', + '' + ].join('\r\n'); + + const problem = require('./mock/add-two-numbers.20161015.json'); + const opts = { + lang: 'cpp', + code: problem.templates[0].defaultCode, + tpl: 'codeonly' + }; + assert.equal(core.exportProblem(problem, opts), expected); + }); - it('should starProblem ok if already unstarred', function(done) { - assert.equal(PROBLEMS[0].starred, false); - core.starProblem(PROBLEMS[0], false, function(e, starred) { - assert.equal(e, null); - assert.equal(starred, false); - done(); - }); - }); - }); // #starProblem + it('should detailed ok with cpp', function() { + file.isWindows = () => false; + + const expected = [ + '/*', + ' * @lc app=leetcode id=2 lang=cpp', + ' *', + ' * [2] Add Two Numbers', + ' *', + ' * https://leetcode.com/problems/add-two-numbers', + ' *', + ' * algorithms', + ' * Medium (25.37%)', + ' * Total Accepted: 195263', + ' * Total Submissions: 769711', + ' * Testcase Example: \'[2,4,3]\\n[5,6,4]\'', + ' *', + ' * 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.', + ' * ', + ' * Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)', + ' * Output: 7 -> 0 -> 8', + ' */', + '/**', + ' * Definition for singly-linked list.', + ' * struct ListNode {', + ' * int val;', + ' * ListNode *next;', + ' * ListNode(int x) : val(x), next(NULL) {}', + ' * };', + ' */', + 'class Solution {', + 'public:', + ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {', + ' ', + ' }', + '};', + '' + ].join('\n'); + + const problem = require('./mock/add-two-numbers.20161015.json'); + const opts = { + lang: 'cpp', + code: problem.templates[0].defaultCode, + tpl: 'detailed' + }; + assert.equal(core.exportProblem(problem, opts), expected); + }); - // dummy test - it('should testProblem ok', function(done) { - client.testProblem = function(problem, cb) { - return cb(null, RESULTS); + it('should detailed ok with ruby', function() { + file.isWindows = () => false; + + const expected = [ + '#', + '# @lc app=leetcode id=2 lang=ruby', + '#', + '# [2] Add Two Numbers', + '#', + '# https://leetcode.com/problems/add-two-numbers', + '#', + '# algorithms', + '# Medium (25.37%)', + '# Total Accepted: 195263', + '# Total Submissions: 769711', + '# Testcase Example: \'\'', + '#', + '# 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.', + '# ', + '# Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)', + '# Output: 7 -> 0 -> 8', + '#', + '# Definition for singly-linked list.', + '# class ListNode', + '# attr_accessor :val, :next', + '# def initialize(val)', + '# @val = val', + '# @next = nil', + '# end', + '# end', + '', + '# @param {ListNode} l1', + '# @param {ListNode} l2', + '# @return {ListNode}', + 'def add_two_numbers(l1, l2)', + ' ', + 'end', + '' + ].join('\n'); + + const problem = require('./mock/add-two-numbers.20161015.json'); + problem.testcase = null; + const opts = { + lang: 'ruby', + code: problem.templates[6].defaultCode, + tpl: 'detailed' }; + assert.equal(core.exportProblem(problem, opts), expected); + }); + }); // #exportProblem - core.testProblem(PROBLEMS[0], function(e, results) { - assert.equal(e, null); - assert.deepEqual(results, RESULTS); + describe('#getProblem', function() { + it('should get by id ok', function(done) { + core.getProblem(0, function(e, problem) { + assert.notExists(e); + assert.deepEqual(problem, PROBLEMS[0]); done(); }); }); - // dummy test - it('should submitProblem ok', function(done) { - client.submitProblem = function(problem, cb) { - return cb(null, RESULTS); - }; - - core.submitProblem(PROBLEMS[1], function(e, results) { - assert.equal(e, null); - assert.deepEqual(results, RESULTS); + it('should get by key ok', function(done) { + core.getProblem('slug0', function(e, problem) { + assert.notExists(e); + assert.deepEqual(problem, PROBLEMS[0]); done(); }); }); - describe('#exportProblem', function() { - function injectVerify(expected, done) { - core.__set__('fs', { - writeFileSync: function(f, data) { - assert.equal(data, expected); - done(); - }, - readFileSync: fs.readFileSync - }); - } - - it('should ok w/ code only', function(done) { - var expected = [ - 'class Solution {', - 'public:', - ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {', - '', - ' }', - '};' - ].join('\r\n'); - - injectVerify(expected, done); - - var problem = require('./mock/add-two-numbers.20161015.json'); - core.exportProblem(problem, 'test.cpp', true); + it('should fail if not found', function(done) { + core.getProblem(3, function(e, problem) { + assert.equal(e, 'Problem not found!'); + done(); }); + }); - it('should ok w/ detailed comments', function(done) { - var expected = [ - '/*', - ' * [2] Add Two Numbers', - ' *', - ' * https://leetcode.com/problems/add-two-numbers', - ' *', - ' * Medium (25.37%)', - ' * Total Accepted: 195263', - ' * Total Submissions: 769711', - ' * Testcase Example: \'[2,4,3]\\n[5,6,4]\'', - ' *', - ' * 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.', - ' * ', - ' * Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)', - ' * Output: 7 -> 0 -> 8', - ' */', - 'class Solution {', - 'public:', - ' ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {', - '', - ' }', - '};', - '' - ].join('\r\n'); - - injectVerify(expected, done); - - var problem = require('./mock/add-two-numbers.20161015.json'); - core.exportProblem(problem, 'test.cpp', false); - }); + it('should fail if client error', function(done) { + next.getProblem = (problem, cb) => cb('client getProblem error'); - it('should ok w/ detailed comments, 2nd', function(done) { - var expected = [ - '#', - '# [2] Add Two Numbers', - '#', - '# https://leetcode.com/problems/add-two-numbers', - '#', - '# Medium (25.37%)', - '# Total Accepted: 195263', - '# Total Submissions: 769711', - '# Testcase Example: \'\'', - '#', - '# 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.', - '# ', - '# Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)', - '# Output: 7 -> 0 -> 8', - '#', - '# Definition for singly-linked list.', - '# class ListNode', - '# attr_accessor :val, :next', - '# def initialize(val)', - '# @val = val', - '# @next = nil', - '# end', - '# end', - '', - '# @param {ListNode} l1', - '# @param {ListNode} l2', - '# @return {ListNode}', - 'def add_two_numbers(l1, l2)', - ' ', - 'end', - '' - ].join('\r\n'); - - injectVerify(expected, done); - - var problem = require('./mock/add-two-numbers.20161015.json'); - problem.testcase = null; - problem.code = _.find(problem.templates, function(template) { - return template.value === 'ruby'; - }).defaultCode; - core.exportProblem(problem, 'test.rb', false); + core.getProblem(0, function(e, problem) { + assert.equal(e, 'client getProblem error'); + done(); }); - }); // #exportProblem - }); // #problems - - describe('#submission', function() { - var SUBMISSIONS = [ - {id: 1234, state: 'Accepted'} - ]; - - // dummy test - it('should getSubmissions ok', function(done) { - client.getSubmissions = function(problem, cb) { - return cb(null, SUBMISSIONS); - }; + }); - core.getSubmissions({}, function(e, submissions) { - assert.equal(e, null); - assert.deepEqual(submissions, SUBMISSIONS); + it('should ok if problem is already there', function(done) { + core.getProblem(PROBLEMS[1], function(e, problem) { + assert.notExists(e); + assert.deepEqual(problem, PROBLEMS[1]); done(); }); }); - // dummy test - it('should getSubmission ok', function(done) { - client.getSubmission = function(submission, cb) { - return cb(null, SUBMISSIONS[0]); - }; + it('should fail if getProblems error', function(done) { + next.getProblems = cb => cb('getProblems error'); - core.getSubmission({}, function(e, submission) { - assert.equal(e, null); - assert.deepEqual(submission, SUBMISSIONS[0]); + core.getProblem(0, function(e, problem) { + assert.equal(e, 'getProblems error'); done(); }); }); - }); // #submission + }); // #getProblem }); diff --git a/test/test_file.js b/test/test_file.js new file mode 100644 index 00000000..d458e83f --- /dev/null +++ b/test/test_file.js @@ -0,0 +1,163 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const assert = require('chai').assert; +const rewire = require('rewire'); + +const th = require('./helper'); + +describe('file', function() { + let file; + + beforeEach(function() { + file = rewire('../lib/file'); + }); + + describe('#dirAndFiles', function() { + const HOME = path.join(__dirname, '..'); + + it('should ok on linux', function() { + if (file.isWindows()) this.skip(); + process.env.HOME = '/home/skygragon'; + + assert.equal(file.userHomeDir(), '/home/skygragon'); + assert.equal(file.homeDir(), '/home/skygragon/.lc'); + assert.equal(file.cacheDir(), '/home/skygragon/.lc/leetcode/cache'); + assert.equal(file.cacheFile('xxx'), '/home/skygragon/.lc/leetcode/cache/xxx.json'); + assert.equal(file.configFile(), '/home/skygragon/.lc/config.json'); + assert.equal(file.name('/home/skygragon/.lc/leetcode/cache/xxx.json'), 'xxx'); + }); + + it('should ok on windows', function() { + if (!file.isWindows()) this.skip(); + process.env.HOME = ''; + process.env.USERPROFILE = 'C:\\Users\\skygragon'; + assert.equal(file.userHomeDir(), 'C:\\Users\\skygragon'); + assert.equal(file.homeDir(), 'C:\\Users\\skygragon\\.lc'); + assert.equal(file.cacheDir(), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache'); + assert.equal(file.cacheFile('xxx'), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json'); + assert.equal(file.configFile(), 'C:\\Users\\skygragon\\.lc\\config.json'); + assert.equal(file.name('C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json'), 'xxx'); + }); + + it('should codeDir ok', function() { + assert.equal(file.codeDir(), HOME); + assert.equal(file.codeDir('.'), HOME); + assert.equal(file.codeDir('icons'), path.join(HOME, 'icons')); + assert.equal(file.codeDir('lib/plugins'), path.join(HOME, 'lib', 'plugins')); + }); + + it('should listCodeDir ok', function() { + const files = file.listCodeDir('lib/plugins'); + assert.equal(files.length, 3); + assert.equal(files[0].name, 'cache'); + assert.equal(files[1].name, 'leetcode'); + assert.equal(files[2].name, 'retry'); + }); + + it('should pluginFile ok', function() { + const expect = path.join(HOME, 'lib/plugins/cache.js'); + assert.equal(file.pluginFile('cache.js'), expect); + assert.equal(file.pluginFile('./cache.js'), expect); + assert.equal(file.pluginFile('https://github.com/skygragon/cache.js'), expect); + }); + + it('should data ok with missing file', function() { + assert.equal(file.data('non-exist'), null); + }); + }); // #dirAndFiles + + describe('#meta', function() { + it('should meta ok within file content', function() { + file.data = x => [ + '/ *', + ' * @lc app=leetcode id=123 lang=javascript', + ' * /' + ].join('\n'); + const meta = file.meta('dummy'); + assert.equal(meta.app, 'leetcode') + assert.equal(meta.id, '123'); + assert.equal(meta.lang, 'javascript'); + }); + + it('should meta ok with white space', function() { + file.data = x => [ + '/ *', + ' * @lc app=leetcode id=123\t \t lang=javascript\r', + ' * /' + ].join('\n'); + const meta = file.meta('dummy'); + assert.equal(meta.app, 'leetcode') + assert.equal(meta.id, '123'); + assert.equal(meta.lang, 'javascript'); + }); + + it('should meta ok within file name', function() { + file.data = x => [ + '/ *', + ' * no meta app=leetcode id=123 lang=javascript', + ' * /' + ].join('\n'); + const meta = file.meta('321.dummy.py'); + assert(!meta.app) + assert.equal(meta.id, '321'); + assert.equal(meta.lang, 'python'); + }); + + it('should meta ok within deprecated file name', function() { + file.data = x => [ + '/ *', + ' * no meta app=leetcode id=123 lang=javascript', + ' * /' + ].join('\n'); + + var meta = file.meta('111.dummy.py3'); + assert(!meta.app) + assert.equal(meta.id, '111'); + assert.equal(meta.lang, 'python3'); + + meta = file.meta('222.dummy.python3.py'); + assert(!meta.app) + assert.equal(meta.id, '222'); + assert.equal(meta.lang, 'python3'); + }); + + it('should fmt ok', function() { + file.init(); + const data = file.fmt('${id}', {id: 123}); + assert.equal(data, '123'); + }); + }); // #meta + + describe('#genneral', function() { + beforeEach(function() { + th.clean(); + }); + afterEach(function() { + th.clean(); + }); + + it('should mkdir ok', function() { + const dir = th.DIR + 'dir'; + assert.equal(fs.existsSync(dir), false); + file.mkdir(dir); + assert.equal(fs.existsSync(dir), true); + file.mkdir(dir); + assert.equal(fs.existsSync(dir), true); + }); + + it('should mv ok', function() { + const SRC = th.Dir + 'src'; + const DST = th.DIR + 'dst'; + assert.equal(fs.existsSync(SRC), false); + assert.equal(fs.existsSync(DST), false); + file.mkdir(SRC); + assert.equal(fs.existsSync(SRC), true); + assert.equal(fs.existsSync(DST), false); + file.mv(SRC, DST); + assert.equal(fs.existsSync(SRC), false); + assert.equal(fs.existsSync(DST), true); + }); + }); // #general +}); diff --git a/test/test_helper.js b/test/test_helper.js index f6e0a333..143bda9e 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -1,9 +1,21 @@ -var assert = require('chai').assert; +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); +const _ = require('underscore'); -var chalk = require('../lib/chalk'); -var h = require('../lib/helper'); +const chalk = require('../lib/chalk'); describe('helper', function() { + let h; + + before(function() { + chalk.init(); + }); + + beforeEach(function() { + h = rewire('../lib/helper'); + }); + describe('#prettyState', function() { it('should ok w/ color', function() { chalk.enabled = true; @@ -44,6 +56,43 @@ describe('helper', function() { }); }); // #prettyText + describe('#prettyLevel', function() { + it('should ok w/ color', function() { + chalk.enabled = true; + + assert.equal(h.prettyLevel('Easy'), chalk.green('Easy')); + assert.equal(h.prettyLevel('Medium'), chalk.yellow('Medium')); + assert.equal(h.prettyLevel('Hard'), chalk.red('Hard')); + assert.equal(h.prettyLevel('easy '), chalk.green('easy ')); + assert.equal(h.prettyLevel('medium'), chalk.yellow('medium')); + assert.equal(h.prettyLevel('hard '), chalk.red('hard ')); + assert.equal(h.prettyLevel('unknown'), 'unknown'); + }); + }); // #prettyLevel + + describe('#prettySize', function() { + it('should ok', function() { + assert.equal(h.prettySize(0), '0.00B'); + assert.equal(h.prettySize(512), '512.00B'); + assert.equal(h.prettySize(1024), '1.00K'); + assert.equal(h.prettySize(1024 * 1024), '1.00M'); + assert.equal(h.prettySize(1024 * 1024 * 1024), '1.00G'); + }); + }); // #prettySize + + describe('#prettyTime', function() { + it('should ok', function() { + assert.equal(h.prettyTime(30), '30 seconds'); + assert.equal(h.prettyTime(60), '1 minutes'); + assert.equal(h.prettyTime(2400), '40 minutes'); + assert.equal(h.prettyTime(3600), '1 hours'); + assert.equal(h.prettyTime(7200), '2 hours'); + assert.equal(h.prettyTime(86400), '1 days'); + assert.equal(h.prettyTime(86400 * 3), '3 days'); + assert.equal(h.prettyTime(86400 * 7), '1 weeks'); + }); + }); // #prettyTime + describe('#levelToName', function() { it('should ok', function() { assert.equal(h.levelToName(0), ' '); @@ -71,29 +120,39 @@ describe('helper', function() { describe('#langToExt', function() { it('should ok', function() { + assert.equal(h.langToExt('bash'), '.sh'); assert.equal(h.langToExt('c'), '.c'); assert.equal(h.langToExt('cpp'), '.cpp'); assert.equal(h.langToExt('csharp'), '.cs'); assert.equal(h.langToExt('golang'), '.go'); assert.equal(h.langToExt('java'), '.java'); assert.equal(h.langToExt('javascript'), '.js'); + assert.equal(h.langToExt('mysql'), '.sql'); + assert.equal(h.langToExt('php'), '.php'); assert.equal(h.langToExt('python'), '.py'); + assert.equal(h.langToExt('python3'), '.py'); assert.equal(h.langToExt('ruby'), '.rb'); + assert.equal(h.langToExt('rust'), '.rs'); + assert.equal(h.langToExt('scala'), '.scala'); assert.equal(h.langToExt('swift'), '.swift'); - assert.equal(h.langToExt('rust'), '.raw'); }); }); // #langToExt describe('#extToLang', function() { it('should ok', function() { + assert.equal(h.extToLang('/usr/bin/file.sh'), 'bash'); assert.equal(h.extToLang('/home/skygragon/file.c'), 'c'); assert.equal(h.extToLang('/var/log/file.cpp'), 'cpp'); assert.equal(h.extToLang('./file.cs'), 'csharp'); assert.equal(h.extToLang('../file.go'), 'golang'); assert.equal(h.extToLang('file.java'), 'java'); assert.equal(h.extToLang('c:/file.js'), 'javascript'); + assert.equal(h.extToLang('~/leetcode/../file.sql'), 'mysql'); + assert.equal(h.extToLang('~/leetcode/hello.php'), 'php'); assert.equal(h.extToLang('c:/Users/skygragon/file.py'), 'python'); assert.equal(h.extToLang('~/file.rb'), 'ruby'); + assert.equal(h.extToLang('~/leetcode/file.rs'), 'rust'); + assert.equal(h.extToLang('/tmp/file.scala'), 'scala'); assert.equal(h.extToLang('~/leetcode/file.swift'), 'swift'); assert.equal(h.extToLang('/home/skygragon/file.dat'), 'unknown'); }); @@ -101,54 +160,37 @@ describe('helper', function() { describe('#langToCommentStyle', function() { it('should ok', function() { - var C_STYLE = { - commentHeader: '/*', - commentLine: ' *', - commentFooter: ' */' - }; - var RUBY_STYLE = { - commentHeader: '#', - commentLine: '#', - commentFooter: '#' - }; + const C_STYLE = {start: '/*', line: ' *', end: ' */'}; + const RUBY_STYLE = {start: '#', line: '#', end: '#'}; + const SQL_STYLE = {start: '--', line: '--', end: '--'}; + assert.deepEqual(h.langToCommentStyle('bash'), RUBY_STYLE); assert.deepEqual(h.langToCommentStyle('c'), C_STYLE); assert.deepEqual(h.langToCommentStyle('cpp'), C_STYLE); assert.deepEqual(h.langToCommentStyle('csharp'), C_STYLE); assert.deepEqual(h.langToCommentStyle('golang'), C_STYLE); assert.deepEqual(h.langToCommentStyle('java'), C_STYLE); assert.deepEqual(h.langToCommentStyle('javascript'), C_STYLE); + assert.deepEqual(h.langToCommentStyle('mysql'), SQL_STYLE); + assert.deepEqual(h.langToCommentStyle('php'), C_STYLE); assert.deepEqual(h.langToCommentStyle('python'), RUBY_STYLE); + assert.deepEqual(h.langToCommentStyle('python3'), RUBY_STYLE); assert.deepEqual(h.langToCommentStyle('ruby'), RUBY_STYLE); + assert.deepEqual(h.langToCommentStyle('rust'), C_STYLE); + assert.deepEqual(h.langToCommentStyle('scala'), C_STYLE); assert.deepEqual(h.langToCommentStyle('swift'), C_STYLE); }); }); // #langToCommentStyle - describe('#dirAndFiles', function() { - it('should ok', function() { - process.env.HOME = '/home/skygragon'; - - assert.equal(h.getHomeDir(), '/home/skygragon'); - assert.equal(h.getCacheDir(), '/home/skygragon/.lc/'); - assert.equal(h.getCacheFile('xxx'), '/home/skygragon/.lc/xxx.json'); - assert.equal(h.getConfigFile(), '/home/skygragon/.lcconfig'); - assert.equal(h.getFilename('/home/skygragon/.lc/xxx.json'), 'xxx'); - - process.env.HOME = ''; - process.env.USERPROFILE = 'C:\\Users\\skygragon'; - assert.equal(h.getHomeDir(), 'C:\\Users\\skygragon'); - }); - }); // #dirAndFiles - describe('#getSetCookieValue', function() { it('should ok', function() { - var resp = { + const resp = { headers: {'set-cookie': [ 'key1=value1; path=/; Httponly', 'key2=value2; path=/; Httponly'] } }; - var respNoSetCookie = { + const respNoSetCookie = { headers: {} }; @@ -159,10 +201,28 @@ describe('helper', function() { }); }); // #getSetCookieValue + describe('#printSafeHTTP', function() { + it('should hide sensitive info', function() { + const raw = [ + "Cookie: 'xxxxxx'", + "'X-CSRFToken': 'yyyyyy'", + "'set-cookie': ['zzzzzz']" + ].join('\r\n'); + + const hide = [ + 'Cookie: ', + "'X-CSRFToken': ", + "'set-cookie': " + ].join('\r\n'); + + assert.equal(h.printSafeHTTP(raw), hide); + }); + }); // #printSafeHTTP + describe('#readStdin', function() { function hijackStdin(data) { - var stream = require('stream'); - var rs = new stream.Readable(); + const stream = require('stream'); + const rs = new stream.Readable(); rs.push(data); rs.push(null); @@ -187,4 +247,22 @@ describe('helper', function() { }); }); }); // #readStdin + + describe('#badge', function() { + it('should ok', function() { + chalk.enabled = true; + assert.equal(h.badge('x'), chalk.white.bgBlue(' x ')); + assert.equal(h.badge('x', 'green'), chalk.black.bgGreen(' x ')); + }); + + it('should ok with random', function() { + const badges = _.values(h.__get__('COLORS')) + .map(function(x) { + return chalk[x.fg][x.bg](' random '); + }); + + const i = badges.indexOf(h.badge('random', 'random')); + assert.equal(i >= 0, true); + }); + }); // #badge }); diff --git a/test/test_icon.js b/test/test_icon.js new file mode 100644 index 00000000..5da832ab --- /dev/null +++ b/test/test_icon.js @@ -0,0 +1,55 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + +describe('icon', function() { + let icon; + let file; + + beforeEach(function() { + file = rewire('../lib/file'); + file.listCodeDir = function() { + return [ + {name: 'mac', data: {yes: 'yes', no: 'no', lock: 'lock', like: 'like', unlike: 'unlike'}}, + {name: 'win7', data: {yes: 'YES', no: 'NO', lock: 'LOCK', like: 'LIKE', unlike: 'UNLIKE'}} + ]; + }; + + icon = rewire('../lib/icon'); + icon.__set__('file', file); + icon.init(); + }); + + describe('#setTheme', function() { + it('should ok with known theme', function() { + icon.setTheme('mac'); + assert.equal(icon.yes, 'yes'); + assert.equal(icon.no, 'no'); + assert.equal(icon.lock, 'lock'); + assert.equal(icon.like, 'like'); + assert.equal(icon.unlike, 'unlike'); + }); + + it('should ok with unknown theme on linux', function() { + file.isWindows = () => false; + + icon.setTheme('non-exist'); + assert.equal(icon.yes, '✔'); + assert.equal(icon.no, '✘'); + assert.equal(icon.lock, '🔒'); + assert.equal(icon.like, '★'); + assert.equal(icon.unlike, '☆'); + }); + + it('should ok with unknown theme on windows', function() { + file.isWindows = () => true; + + icon.setTheme('non-exist'); + assert.equal(icon.yes, 'YES'); + assert.equal(icon.no, 'NO'); + assert.equal(icon.lock, 'LOCK'); + assert.equal(icon.like, 'LIKE'); + assert.equal(icon.unlike, 'UNLIKE'); + }); + }); // #setTheme +}); diff --git a/test/test_leetcode_client.js b/test/test_leetcode_client.js deleted file mode 100644 index 4c272eaf..00000000 --- a/test/test_leetcode_client.js +++ /dev/null @@ -1,576 +0,0 @@ -var _ = require('underscore'); -var assert = require('chai').assert; -var nock = require('nock'); - -var client = require('../lib/leetcode_client'); -var config = require('../lib/config'); -var core = require('../lib/core'); - -describe('leetcode_client', function() { - var PROBLEM = { - id: 389, - name: 'Find the Difference', - key: 'find-the-difference', - link: 'https://leetcode.com/problems/find-the-difference', - locked: false, - file: '/dev/null' - }; - - before(function() { - config.init(); - }); - - describe('#autologin', function() { - var _core; - - before(function() { - _core = _.clone(core); - - core.getUser = function() { - return {}; - }; - core.login = function(user, cb) { - return cb(null, user); - }; - }); - - // restore to original 'core' - after(function() { - _.extendOwn(core, _core); - }); - - it('should ok', function(done) { - config.AUTO_LOGIN = true; - nock(config.URL_PROBLEMS).get('/').reply(403); - nock(config.URL_PROBLEMS).get('/').replyWithFile(200, './test/mock/problems.json.20160911'); - - client.getProblems(function(e, problems) { - assert.equal(e, null); - assert.equal(problems.length, 377); - done(); - }); - }); - - it('should fail if no auto login', function(done) { - config.AUTO_LOGIN = false; - nock(config.URL_PROBLEMS).get('/').reply(403); - - client.getProblems(function(e, problems) { - var expected = { - msg: 'session expired, please login again', - statusCode: 403 - }; - assert.deepEqual(e, expected); - done(); - }); - }); - - it('should fail if other error', function(done) { - config.AUTO_LOGIN = true; - nock(config.URL_PROBLEMS).get('/').reply(503); - - client.getProblems(function(e, problems) { - var expected = { - msg: 'http error', - statusCode: 503 - }; - assert.deepEqual(e, expected); - done(); - }); - }); - - it('should fail if http error in relogin', function(done) { - config.AUTO_LOGIN = true; - nock(config.URL_PROBLEMS).get('/').reply(403); - core.login = function(user, cb) { - return cb('unknown error!'); - }; - - // the original error will be returned instead - var expected = { - msg: 'session expired, please login again', - statusCode: 403 - }; - client.getProblems(function(e, problems) { - assert.deepEqual(e, expected); - done(); - }); - }); - }); - - describe('#getProblems', function() { - it('should ok', function(done) { - nock(config.URL_PROBLEMS).get('/').replyWithFile(200, './test/mock/problems.json.20160911'); - - client.getProblems(function(e, problems) { - assert.equal(e, null); - assert.equal(problems.length, 377); - done(); - }); - }); - - it('should fail if not login', function(done) { - nock(config.URL_PROBLEMS).get('/').replyWithFile(200, './test/mock/problems.nologin.json.20161015'); - - client.getProblems(function(e, problems) { - assert.equal(e, 'session expired, please login again'); - done(); - }); - }); - }); // #getProblems - - describe('#getProblem', function() { - it('should ok', function(done) { - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .replyWithFile(200, './test/mock/find-the-difference.html.20170424'); - - client.getProblem(PROBLEM, function(e, problem) { - assert.equal(e, null); - assert.equal(problem.totalAC, 63380); - assert.equal(problem.totalSubmit, 123178); - assert.equal(problem.desc, - [ - '', - 'Given two strings s and t which consist of only lowercase letters.', - '', - 'String t is generated by random shuffling string s and then add one more letter at a random position.', - '', - 'Find the letter that was added in t.', - '', - 'Example:', - '', - 'Input:', - 's = "abcd"', - 't = "abcde"', - '', - 'Output:', - 'e', - '', - 'Explanation:', - "'e' is the letter that was added.", - '' - ].join('\r\n')); - - assert.equal(problem.templates.length, 7); - - assert.equal(problem.templates[0].value, 'cpp'); - assert.equal(problem.templates[0].text, 'C++'); - assert.equal(problem.templates[0].defaultCode, - [ - 'class Solution {', - 'public:', - ' char findTheDifference(string s, string t) {', - ' ', - ' }', - '};' - ].join('\r\n')); - - assert.equal(problem.templates[1].value, 'java'); - assert.equal(problem.templates[1].text, 'Java'); - assert.equal(problem.templates[1].defaultCode, - [ - 'public class Solution {', - ' public char findTheDifference(String s, String t) {', - ' ', - ' }', - '}' - ].join('\r\n')); - - assert.equal(problem.templates[2].value, 'python'); - assert.equal(problem.templates[2].text, 'Python'); - assert.equal(problem.templates[2].defaultCode, - [ - 'class Solution(object):', - ' def findTheDifference(self, s, t):', - ' """', - ' :type s: str', - ' :type t: str', - ' :rtype: str', - ' """', - ' ' - ].join('\r\n')); - - assert.equal(problem.templates[3].value, 'c'); - assert.equal(problem.templates[3].text, 'C'); - assert.equal(problem.templates[3].defaultCode, - [ - 'char findTheDifference(char* s, char* t) {', - ' ', - '}' - ].join('\r\n')); - - assert.equal(problem.templates[4].value, 'csharp'); - assert.equal(problem.templates[4].text, 'C#'); - assert.equal(problem.templates[4].defaultCode, - [ - 'public class Solution {', - ' public char FindTheDifference(string s, string t) {', - ' ', - ' }', - '}' - ].join('\r\n')); - - assert.equal(problem.templates[5].value, 'javascript'); - assert.equal(problem.templates[5].text, 'JavaScript'); - assert.equal(problem.templates[5].defaultCode, - [ - '/**', - ' * @param {string} s', - ' * @param {string} t', - ' * @return {character}', - ' */', - 'var findTheDifference = function(s, t) {', - ' ', - '};' - ].join('\r\n')); - - assert.equal(problem.templates[6].value, 'ruby'); - assert.equal(problem.templates[6].text, 'Ruby'); - assert.equal(problem.templates[6].defaultCode, - [ - '# @param {String} s', - '# @param {String} t', - '# @return {Character}', - 'def find_the_difference(s, t)', - ' ', - 'end' - ].join('\r\n')); - done(); - }); - }); - - it('should fail if no permission for locked', function(done) { - PROBLEM.locked = true; - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .replyWithFile(200, './test/mock/locked.html.20161015'); - - client.getProblem(PROBLEM, function(e, problem) { - assert.equal(e, 'failed to load locked problem!'); - done(); - }); - }); - - it('should fail if http error', function(done) { - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .replyWithError('unknown error!'); - - client.getProblem(PROBLEM, function(e, problem) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - }); // #getProblem - - describe('#testProblem', function() { - it('should ok', function(done) { - nock('https://leetcode.com') - .post('/problems/find-the-difference/interpret_solution/') - .reply(200, '{"interpret_expected_id": "id1", "interpret_id": "id2"}'); - - nock('https://leetcode.com') - .get('/submissions/detail/id1/check/') - .reply(200, '{"state": "SUCCESS"}'); - - nock('https://leetcode.com') - .get('/submissions/detail/id2/check/') - .reply(200, '{"state": "SUCCESS"}'); - - client.testProblem(PROBLEM, function(e, results) { - assert.equal(e, null); - assert.deepEqual(results, - [ - {name: 'Your', state: 'SUCCESS'}, - {name: 'Expected', state: 'SUCCESS'} - ]); - done(); - }); - }); - - it('should fail if http error', function(done) { - nock('https://leetcode.com') - .post('/problems/find-the-difference/interpret_solution/') - .replyWithError('unknown error!'); - - client.testProblem(PROBLEM, function(e, results) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - }); // #testProblem - - describe('#submitProblem', function() { - it('should ok', function(done) { - nock('https://leetcode.com') - .post('/problems/find-the-difference/submit/') - .reply(200, '{"submission_id": "id1"}'); - - nock('https://leetcode.com') - .get('/submissions/detail/id1/check/') - .reply(200, '{"state": "SUCCESS"}'); - - client.submitProblem(PROBLEM, function(e, results) { - assert.equal(e, null); - assert.deepEqual(results, [{name: 'Your', state: 'SUCCESS'}]); - done(); - }); - }); - - it('should ok after delay', function(done) { - this.timeout(5000); - - nock('https://leetcode.com') - .post('/problems/find-the-difference/submit/') - .reply(200, '{"error": "You run code too soon"}'); - nock('https://leetcode.com') - .post('/problems/find-the-difference/submit/') - .reply(200, '{"submission_id": "id1"}'); - - nock('https://leetcode.com') - .get('/submissions/detail/id1/check/') - .reply(200, '{"state": "STARTED"}'); - nock('https://leetcode.com') - .get('/submissions/detail/id1/check/') - .reply(200, '{"state": "SUCCESS"}'); - - client.submitProblem(PROBLEM, function(e, results) { - assert.equal(e, null); - assert.deepEqual(results, [{name: 'Your', state: 'SUCCESS'}]); - done(); - }); - }); - - it('should fail if server error', function(done) { - nock('https://leetcode.com') - .post('/problems/find-the-difference/submit/') - .reply(200, '{"error": "maybe internal error?"}'); - - client.submitProblem(PROBLEM, function(e, results) { - assert.equal(e, 'maybe internal error?'); - done(); - }); - }); - - it('should fail if server error in check result', function(done) { - nock('https://leetcode.com') - .post('/problems/find-the-difference/submit/') - .reply(200, '{"submission_id": "id1"}'); - - nock('https://leetcode.com') - .get('/submissions/detail/id1/check/') - .replyWithError('unknown error!'); - - client.submitProblem(PROBLEM, function(e, results) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - }); // #submitProblem - - describe('#starProblem', function() { - it('should star ok', function(done) { - nock('https://leetcode.com') - .post('/problems/favor/') - .reply(200, '{"is_favor": true}'); - - client.starProblem(PROBLEM, true, function(e, starred) { - assert.equal(e, null); - assert.equal(starred, true); - done(); - }); - }); - - it('should unstar ok', function(done) { - nock('https://leetcode.com') - .delete('/problems/favor/') - .reply(200, '{"is_favor": false}'); - - client.starProblem(PROBLEM, false, function(e, starred) { - assert.equal(e, null); - assert.equal(starred, false); - done(); - }); - }); - - it('should star fail if http error', function(done) { - nock('https://leetcode.com') - .post('/problems/favor/') - .replyWithError('unknown error!'); - - client.starProblem(PROBLEM, true, function(e, starred) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - }); // #starProblem - - describe('#getSubmissions', function() { - it('should ok', function(done) { - var problem = { - id: 1, - name: 'Two Sum', - key: 'two-sum', - link: 'https://leetcode.com/problems/two-sum', - locked: false - }; - - nock('https://leetcode.com') - .get('/api/submissions/two-sum') - .replyWithFile(200, './test/mock/two-sum.submissions.json.20170425'); - - client.getSubmissions(problem, function(e, submissions) { - assert.equal(e, null); - assert.equal(submissions.length, 20); - - assert.deepEqual(submissions[0], { - id: '95464136', - title: 'Two Sum', - 'is_pending': false, - lang: 'cpp', - time: '1 month, 3 weeks', - runtime: '12 ms', - url: '/submissions/detail/95464136/', - 'status_display': 'Accepted' - }); - - assert.deepEqual(submissions[1], { - id: '78502271', - title: 'Two Sum', - 'is_pending': false, - lang: 'cpp', - time: '6 months, 1 week', - runtime: '13 ms', - url: '/submissions/detail/78502271/', - 'status_display': 'Accepted' - }); - done(); - }); - }); - - it('should fail if http error', function(done) { - nock('https://leetcode.com') - .get('/api/submissions/find-the-difference') - .replyWithError('unknown error!'); - - client.getSubmissions(PROBLEM, function(e, submissions) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - }); // #getSubmissions - - describe('#getSubmission', function() { - var SUBMISSION; - - beforeEach(function() { - SUBMISSION = { - id: '73790064', - lang: 'cpp', - runtime: '9 ms', - path: '/submissions/detail/73790064/', - state: 'Accepted' - }; - }); - - it('should ok', function(done) { - nock('https://leetcode.com') - .get('/submissions/detail/73790064/') - .replyWithFile(200, './test/mock/two-sum.submission.73790064.html.20161006'); - - client.getSubmission(SUBMISSION, function(e, submission) { - assert.equal(e, null); - assert.deepEqual(submission.code, - [ - 'class Solution {', - 'public:', - ' vector twoSum(vector& nums, int target) {', - ' return res;', - ' }', - '};', - '' - ].join('\r\n')); - done(); - }); - }); - - it('should fail if http error', function(done) { - nock('https://leetcode.com') - .get('/submissions/detail/73790064/') - .replyWithError('unknown error!'); - - client.getSubmission(SUBMISSION, function(e, submission) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - - it('should fail if no matching submission', function(done) { - nock('https://leetcode.com') - .get('/submissions/detail/73790064/') - .replyWithFile(200, './test/mock/locked.html.20161015'); - - client.getSubmission(SUBMISSION, function(e, submission) { - assert.equal(e, null); - assert.equal(submission.code, null); - done(); - }); - }); - }); // #getSubmission - - describe('#login', function() { - it('should ok', function(done) { - nock(config.URL_LOGIN).get('/').reply(200, '', { - 'Set-Cookie': [ - 'csrftoken=LOGIN_CSRF_TOKEN; Max-Age=31449600; Path=/; secure' - ] - }); - - nock(config.URL_LOGIN).post('/').reply(302, '', { - 'Set-Cookie': [ - 'csrftoken=SESSION_CSRF_TOKEN; Max-Age=31449600; Path=/; secure', - 'LEETCODE_SESSION=SESSION_ID; Max-Age=31449600; Path=/; secure', - "messages='Successfully signed in as Eric.'; Max-Age=31449600; Path=/; secure" - ] - }); - - var user = {}; - client.login(user, function(e, user) { - assert.equal(e, null); - - assert.equal(user.loginCSRF, 'LOGIN_CSRF_TOKEN'); - assert.equal(user.sessionCSRF, 'SESSION_CSRF_TOKEN'); - assert.equal(user.sessionId, 'SESSION_ID'); - assert.equal(user.name, 'Eric'); - done(); - }); - }); - - it('should fail if http error', function(done) { - nock(config.URL_LOGIN).get('/').reply(200, '', { - 'Set-Cookie': [ - 'csrftoken=LOGIN_CSRF_TOKEN; Max-Age=31449600; Path=/; secure' - ] - }); - nock(config.URL_LOGIN).post('/').replyWithError('unknown error!'); - - var user = {}; - client.login(user, function(e, user) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - - it('should fail if http error, 2nd', function(done) { - nock(config.URL_LOGIN).get('/').replyWithError('unknown error!'); - - var user = {}; - client.login(user, function(e, user) { - assert.equal(e.message, 'unknown error!'); - done(); - }); - }); - }); // #login -}); - diff --git a/test/test_log.js b/test/test_log.js new file mode 100644 index 00000000..92e5ef4b --- /dev/null +++ b/test/test_log.js @@ -0,0 +1,109 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + +const chalk = require('../lib/chalk'); + +describe('log', function() { + let log; + let savedOutput; + let expected; + + before(function() { + chalk.init(); + }); + + beforeEach(function() { + log = rewire('../lib/log'); + savedOutput = log.output; + log.output = x => expected = x; + + log.init(); + expected = ''; + }); + + afterEach(function() { + log.output = savedOutput; + }); + + describe('#setLevel', function() { + it('should ok with known level', function() { + log.setLevel('TRACE'); + assert.deepEqual(log.level, log.levels.get('TRACE')); + log.setLevel('DEBUG'); + assert.deepEqual(log.level, log.levels.get('DEBUG')); + log.setLevel('INFO'); + assert.deepEqual(log.level, log.levels.get('INFO')); + log.setLevel('WARN'); + assert.deepEqual(log.level, log.levels.get('WARN')); + log.setLevel('ERROR'); + assert.deepEqual(log.level, log.levels.get('ERROR')); + }); + + it('should ok with unknown level', function() { + log.setLevel(''); + assert.deepEqual(log.level, log.levels.get('INFO')); + }); + }); // #setLevel + + describe('#isEnabled', function() { + it('should ok', function() { + log.setLevel('DEBUG'); + assert.equal(log.isEnabled('TRACE'), false); + assert.equal(log.isEnabled('DEBUG'), true); + assert.equal(log.isEnabled('INFO'), true); + assert.equal(log.isEnabled('WARN'), true); + assert.equal(log.isEnabled('ERROR'), true); + }); + }); // #isEnabled + + describe('#levels', function() { + it('should ok with log.trace', function() { + log.trace('some error'); + assert.equal(expected, ''); + + log.setLevel('TRACE'); + log.trace('some error'); + assert.equal(expected, chalk.gray('[TRACE] some error')); + }); + + it('should ok with log.debug', function() { + log.debug('some error'); + assert.equal(expected, ''); + + log.setLevel('DEBUG'); + log.debug('some error'); + assert.equal(expected, chalk.gray('[DEBUG] some error')); + }); + + it('should ok with log.info', function() { + log.info('some error'); + assert.equal(expected, 'some error'); + }); + + it('should ok with log.warn', function() { + log.warn('some error'); + assert.equal(expected, chalk.yellow('[WARN] some error')); + }); + + it('should ok with log.error', function() { + log.error('some error'); + assert.equal(expected, chalk.red('[ERROR] some error')); + }); + + it('should ok with log.fail', function() { + log.fail({msg: 'some error', statusCode: 500}); + assert.equal(expected, chalk.red('[ERROR] some error [code=500]')); + + log.fail('some error'); + assert.equal(expected, chalk.red('[ERROR] some error')); + }); + }); // #levels + + describe('#printf', function() { + it('should ok', function() { + log.printf('%s and %s and %%', 'string', 100); + assert.equal(expected, 'string and 100 and %'); + }); + }); // #printf +}); diff --git a/test/test_plugin.js b/test/test_plugin.js new file mode 100644 index 00000000..aa3a40be --- /dev/null +++ b/test/test_plugin.js @@ -0,0 +1,223 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const assert = require('chai').assert; +const rewire = require('rewire'); + +const chalk = require('../lib/chalk'); +const config = require('../lib/config'); +const log = require('../lib/log'); +const th = require('./helper'); + +const Plugin = rewire('../lib/plugin'); + +describe('plugin', function() { + let file; + let cache; + + const NOOP = () => {}; + + before(function() { + log.init(); + chalk.init(); + config.init(); + + file = rewire('../lib/file'); + cache = rewire('../lib/cache'); + Plugin.__set__('file', file); + Plugin.__set__('cache', cache); + }); + + beforeEach(function() { + th.clean(); + cache.get = NOOP; + }); + + describe('#Plugin.init', function() { + const p1 = new Plugin(0, 'leetcode', '2.0'); + const p2 = new Plugin(1, 'cache', '1.0'); + const p3 = new Plugin(2, 'retry', '3.0'); + const p4 = new Plugin(3, 'core', '4.0'); + + before(function() { + p1.init = p2.init = p3.init = p4.init = NOOP; + file.listCodeDir = function() { + return [ + {name: 'cache', data: p2, file: 'cache.js'}, + {name: 'leetcode', data: p1, file: 'leetcode.js'}, + {name: 'retry', data: p3, file: 'retry.js'}, + {name: 'bad', data: null} + ]; + }; + }); + + it('should init ok', function() { + cache.get = () => { + return {cache: true, leetcode: false, retry: true}; + }; + assert.deepEqual(Plugin.plugins, []); + + const res = Plugin.init(p4); + assert.equal(res, true); + assert.deepEqual(Plugin.plugins.length, 3); + + const names = Plugin.plugins.map(p => p.name); + assert.deepEqual(names, ['retry', 'cache', 'leetcode']); + + assert.equal(p4.next, p3); + assert.equal(p3.next, p2); + assert.equal(p2.next, null); + assert.equal(p1.next, null); + }); + + it('should find missing ok', function() { + cache.get = () => { + return {company: true, leetcode: false, solution: true}; + }; + + const res = Plugin.init(p4); + assert.equal(res, false); + assert.deepEqual(Plugin.plugins.length, 5); + + const names = Plugin.plugins.map(p => p.name); + assert.deepEqual(names, ['retry', 'cache', 'leetcode', 'company', 'solution']); + + assert.equal(p4.next, p3); + assert.equal(p3.next, p2); + assert.equal(p2.next, null); + assert.equal(p1.next, null); + }); + }); // #Plugin.init + + describe('#install', function() { + let expected; + + before(function() { + Plugin.__set__('cp', { + exec: function(cmd, opts, cb) { + expected = cmd; + return cb(); + } + }); + }); + + it('should install no deps ok', function(done) { + expected = ''; + const p = new Plugin(100, 'test', '2017.12.26', 'desc', []); + p.install(function() { + assert.equal(expected, ''); + done(); + }); + }); + + it('should install deps ok', function(done) { + const deps = ['a', 'b:linux', 'b:darwin', 'b:win32', 'c:bad', 'd']; + const p = new Plugin(100, 'test', '2017.12.26', 'desc', deps); + p.install(function() { + assert.equal(expected, 'npm install --save a b d'); + done(); + }); + }); + }); // #install + + describe('#Plugin.copy', function() { + const SRC = path.resolve(th.DIR, 'copy.src.js'); + const DST = path.resolve(th.DIR, 'copy.test.js'); + + before(function() { + file.pluginFile = () => DST; + }); + + it('should copy from http error', function(done) { + Plugin.copy('non-exists', function(e, fullpath) { + assert.equal(e, 'HTTP Error: 404'); + assert.equal(fs.existsSync(DST), false); + done(); + }); + }).timeout(5000); + + it('should copy from local ok', function(done) { + const data = [ + 'module.exports = {', + ' x: 123,', + ' install: function(cb) { cb(); }', + '};' + ]; + fs.writeFileSync(SRC, data.join('\n')); + + Plugin.copy(SRC, function(e, fullpath) { + assert.notExists(e); + assert.equal(fullpath, DST); + assert.equal(fs.existsSync(DST), true); + done(); + }); + }); + }); // #Plugin.copy + + describe('#Plugin.installMissings', function() { + const PLUGINS = [ + new Plugin(0, '0', 'missing'), + new Plugin(1, '1', '2018.01.01'), + new Plugin(2, '2', 'missing'), + ]; + let expected; + + beforeEach(function() { + expected = []; + file.pluginFile = x => th.DIR + x; + Plugin.install = (name, cb) => { + expected.push(name); + return cb(null, PLUGINS[+name]); + }; + }); + + it('should ok', function(done) { + Plugin.plugins = PLUGINS; + Plugin.installMissings(function(e) { + assert.notExists(e); + assert.deepEqual(expected, ['0', '2']); + done(); + }); + }); + }); // #Plugin.installMissings + + describe('#delete', function() { + it('should ok', function() { + file.pluginFile = x => th.DIR + x; + + const p = new Plugin(0, '0', '2018.01.01'); + p.file = '0.js'; + fs.writeFileSync('./tmp/0.js', ''); + + assert.equal(p.deleted, false); + assert.deepEqual(fs.readdirSync(th.DIR), ['0.js']); + p.delete(); + assert.equal(p.deleted, true); + assert.deepEqual(fs.readdirSync(th.DIR), []); + p.delete(); + assert.equal(p.deleted, true); + assert.deepEqual(fs.readdirSync(th.DIR), []); + }); + }); // #delete + + describe('#save', function() { + it('should ok', function() { + let data = {}; + cache.get = () => data; + cache.set = (k, x) => data = x; + + const p = new Plugin(0, '0', '2018.01.01'); + p.save(); + assert.deepEqual(data, {'0': true}); + + p.enabled = false; + p.save(); + assert.deepEqual(data, {'0': false}); + + p.deleted = true; + p.save(); + assert.deepEqual(data, {}); + }); + }); // #save +}); diff --git a/test/test_queue.js b/test/test_queue.js new file mode 100644 index 00000000..9f87d25d --- /dev/null +++ b/test/test_queue.js @@ -0,0 +1,58 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + + + +describe('queue', function() { + let Queue; + + beforeEach(function() { + Queue = rewire('../lib/queue'); + }); + + it('should ok', function(done) { + function doTask(x, q, cb) { + ++q.ctx.n; + q.ctx.sum += x; + return cb(); + } + + const ctx = {n: 0, sum: 0}; + const q = new Queue([], ctx, doTask); + + q.addTask(1); + q.addTask(2); + q.addTasks([3, 4, 5]); + + q.run(5, function(e, ctx) { + assert.notExists(e); + assert.equal(ctx.n, 5); + assert.equal(ctx.sum, 15); + done(); + }); + }); + + it('should ok in sequence', function(done) { + const config = {network: {}}; + Queue.__set__('config', config); + + function doTask(x, q, cb) { + if (!q.ctx.list) q.ctx.list = []; + q.ctx.list.push(x); + return cb(); + } + + const q = new Queue(null, null, doTask); + q.addTask(1); + q.addTasks([2, 3]); + q.addTasks([4]); + q.addTask(5); + + q.run(null, function(e, ctx) { + assert.notExists(e); + assert.deepEqual(ctx.list, [1, 2, 3, 4, 5]); + done(); + }); + }); +}); diff --git a/test/test_session.js b/test/test_session.js new file mode 100644 index 00000000..0638db2d --- /dev/null +++ b/test/test_session.js @@ -0,0 +1,52 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + +describe('session', function() { + let session; + let stats; + let now; + + beforeEach(function() { + stats = null; + const cache = { + get: (k) => stats, + set: (k, v) => stats = v + }; + const moment = () => { + return {format: () => now} + }; + + session = rewire('../lib/session'); + session.__set__('cache', cache); + session.__set__('moment', moment); + }); + + describe('#updateStat', function() { + it('should update number ok', function() { + now = '2017.12.13'; + session.updateStat('ac', 10); + assert.deepEqual(stats, {'2017.12.13': {ac: 10}}); + + session.updateStat('ac', 20); + assert.deepEqual(stats, {'2017.12.13': {ac: 30}}); + + now = '2017.12.14'; + session.updateStat('ac', 40); + assert.deepEqual(stats, { + '2017.12.13': {ac: 30}, + '2017.12.14': {ac: 40} + }); + }); + + it('should update set ok', function() { + now = '2017.12.13'; + session.updateStat('ac.set', 101); + assert.deepEqual(stats, {'2017.12.13': {'ac.set': [101]}}); + session.updateStat('ac.set', 100); + assert.deepEqual(stats, {'2017.12.13': {'ac.set': [101, 100]}}); + session.updateStat('ac.set', 101); + assert.deepEqual(stats, {'2017.12.13': {'ac.set': [101, 100]}}); + }); + }); // #updateStat +}); diff --git a/test/test_sprintf.js b/test/test_sprintf.js new file mode 100644 index 00000000..aed7cbc3 --- /dev/null +++ b/test/test_sprintf.js @@ -0,0 +1,33 @@ +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); + +const sprintf = require('../lib/sprintf'); + +describe('sprintf', function() { + it('should ok', function() { + assert.equal(sprintf('%%'), '%'); + assert.equal(sprintf('%s', 123), '123'); + assert.equal(sprintf('%6s', 123), ' 123'); + assert.equal(sprintf('%06s', 123), '000123'); + assert.equal(sprintf('%-6s', 123), '123 '); + assert.equal(sprintf('%=6s', 123), ' 123 '); + + assert.equal(sprintf('%4s,%=4s,%-4s', 123, 'xy', 3.1), ' 123, xy ,3.1 '); + }); + + it('should non-ascii ok', function() { + assert.equal(sprintf('%4s', '中'), ' 中'); + assert.equal(sprintf('%-4s', '中'), '中 '); + assert.equal(sprintf('%=4s', '中'), ' 中 '); + + assert.equal(sprintf('%=14s', '12你好34世界'), ' 12你好34世界 '); + }); + + it('should color ok', function() { + const chalk = rewire('../lib/chalk'); + chalk.init(); + + assert.equal(sprintf('%=3s', chalk.red('X')), ' ' + chalk.red('X') + ' '); + }); +});