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 6ab3e64b..5ef04108 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,22 +1,37 @@ module.exports = { "env": { "browser": false, - "node": true, - "mocha": true + "es6": true, + "mocha": true, + "node": true }, - "extends": "google", + "extends": [ + "google", + "eslint:recommended" + ], "rules": { + "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": 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/.gitignore b/.gitignore index a833ee3d..c08d1d39 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +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 ba0299f3..258b5df8 100644 --- a/README.md +++ b/README.md @@ -1,45 +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. -* More [**PLUGINS**](https://skygragon.github.io/leetcode-cli/advanced#plugins) to enjoy extra useful features! + -## 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/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 index 76b7eac0..7577d79d 100644 --- a/colors/blue.json +++ b/colors/blue.json @@ -1,6 +1,10 @@ { - "gray": "#B0C4DE", - "green": "#66D9EF", - "red": "#AE81FF", - "yellow": "#87CEEB" + "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 c7b90db3..86e60471 100644 --- a/colors/dark.json +++ b/colors/dark.json @@ -1,6 +1,9 @@ { + "blue": "#000099", + "cyan": "#009999", "gray": "#455354", "green": "#009900", + "magenta": "#990099", "red": "#990000", "yellow": "#999900" } 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 index ac68279b..2fcc4378 100644 --- a/colors/orange.json +++ b/colors/orange.json @@ -1,6 +1,10 @@ { - "gray": "#C4BE89", - "green": "#E6DB74", - "red": "#ef5939", - "yellow": "#FD971F" + "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 497a5e43..762a8a90 100644 --- a/colors/pink.json +++ b/colors/pink.json @@ -1,6 +1,10 @@ { - "gray": "#BCA3A3", - "green": "#ff1493", - "red": "#dc143c", - "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 794e9625..42904e91 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -3,21 +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) +* [Color Themes](#color-themes) +* [File Name](#file-name) +* [Log Levels](#log-levels) * [Plugins](#plugins) -# Auto login +# 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!** @@ -25,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...** @@ -38,78 +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 - problems.json # problems list - 1.two-sum.algorithms.json # specific problem info + $ ls -a1 ~/.lc/cache/ + problems.json # cached questions list + 1.two-sum.algorithms.json # cached specific question -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. +**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, - "PLUGINS": {} - } + { + "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). -* `ICON_THEME` to set icon them used in output. -* `LANG` to set your default language used in coding. -* `USE_COLOR` to enable colorful output. -* `PLUGINS` to config each installed plugins, see [Plugins](#plugins). +* `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 + { + "plugins": { + "github": { + "repo": "https://github.com/skygragon/test", + "token": "abcdefghijklmnopqrstuvwxyz" + }, + "cpp.lint": { + "bin": "~/bin/cpplibt.py", + "flags": [] + } + } + } + +# 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: * `blue` -* `default` * `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", @@ -121,11 +173,28 @@ 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. - * Will print detailed HTTP requests/responses. + * Will print detailed HTTP requests/responses. # Plugins @@ -133,4 +202,4 @@ You can easily introduce more features by installing other plugins form third pa * [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. \ No newline at end of file +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 ede09bac..1893dbc4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -5,11 +5,13 @@ 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) @@ -18,170 +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] - leetcode list [keyword] + Examples: + leetcode cache Show all cache + leetcode cache 1 Show cache of question 1 - 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 Delete all cache + leetcode cache 1 -d Delete cache of question 1 # cache -Show cached problems. +Show local cached questions. -* `-d` to delete specific cached problem. -* `-a` to delete all cached problems. +* `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 - .user 816.00B 2 hours ago - problems 148.48K 2 hours ago - 1.two-sum.algorithms 2.52K 2 hours ago - ...... + $ 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 + ...... + +Delete cache of question 733: + + $ 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. + * `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. - * algorithms - * database - * shell -* `-s` to show statistic counters. + * 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%) + 🔒 [360] Sort Transformed Array Medium (41.0%) + 🔒 [325] Maximum Size Subarray Sum Equals k Medium (40.9%) # plugin -Display installed plugins. To install 3rd party plugins, please check the [Advanced Tips](https://skygragon.github.io/leetcode-cli/advanced#plugins). +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 existing plugin. -* `-e` to enable existing plugin. -* `-D` to delete existing 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 + $ leetcode plugin -i company -Install plugin from local file: +(Deprecated) Install plugin from local file: - $ leetcode plugin -i /company.js + $ 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 AUTO_LOGIN is on. - ✔ cache default Plugin to provide local cache. - ✔ leetcode default Plugin to talk with leetcode APIs. + $ 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 %) + # 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. -* `-t` to show code template. -* `-d` to show problem description. +* `-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) - * bash - * c - * cpp - * csharp - * golang - * java - * javascript - * mysql - * python - * python3 - * ruby - * scala - * swift -* Instead of index number, you can use name to select a problem. - * `leetcode show 1` - * `leetcode show "Two Sum"` - * `leetcode show two-sum` -* If index number/name not provided, a randomly problem will be displayed. - * `leetcode show` + * 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 + * algorithms * Easy (25.6%) * Total Accepted: 274880 * Total Submissions: 1074257 @@ -203,101 +282,122 @@ Display problem details. With `-g`/`-l`/`-x`, the code template could be auto ge Only show the code template: - $ leetcode show -t --no-desc 1 - class Solution { - public: - vector twoSum(vector& nums, int target) { + $ 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. -* `-t` to show statistics on given tag. E.g. - * algorithms - * database - * shell +* `-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. -* `-l` to specify the desired programming language. -* `-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 @@ -305,39 +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) - ✔ Your runtime beats 49.89 % of cpp submissions + ✔ 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) + * 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. + * 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 @@ -351,51 +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 - 2.0.3 + $ leetcode version + 2.6.2 Verbose: - $ leetcode version -v - _ _ _ - | | | | | | - | | ___ ___| |_ ___ ___ __| | ___ - | |/ _ \/ _ \ __|/ __|/ _ \ / _` |/ _ \ - | | __/ __/ |_ (__| (_) | (_| | __/ - |_|\___|\___|\__|\___|\___/ \__,_|\___| CLI v2.0.3 - - [Environment] - Cache: /Users/skygragon/.lc/ - Config: /Users/skygragon/.lcconfig - - [Configuration] - AUTO_LOGIN true - COLOR_THEME orange - ICON_THEME default - LANG cpp - MAX_WORKERS 10 - USE_COLOR true - - [Themes] - Colors blue,dark,default,orange,pink - Icons ascii,default,win7 - - [Plugins] - retry default - cache default - leetcode default + $ 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 f19b3c4f..c2e20595 100644 --- a/docs/install.md +++ b/docs/install.md @@ -3,43 +3,70 @@ layout: default title: Installation --- -# Prerequisites +# All in One (beta) + +No need to install node.js. Now available on 64bits linux, mac, and windows. -`node.js` (`npm` included) required, please follow the installation guide: +[Download](https://github.com/skygragon/leetcode-cli/releases) + +# Prerequisites -* https://nodejs.org/en/download/package-manager/ -* https://nodejs.org/en/download/ +Install the latest LTS version of `node.js` (`npm` included): -You might wanna choose the latest stable release on the list. +* [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 release, but not include the latest development version. +This will install the latest STABLE version, but not include the latest DEV version. $ npm install -g leetcode-cli + $ leetcode version + +In case Ubuntu failed due to **permission denied**, try following: + + $ 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 development version on GitHub. +This will install the latest DEV version from GitHub repo. $ npm install -g skygragon/leetcode-cli + $ leetcode version -### From local source +### From source -Similar with above, while you can introduce your changes as you like. +Similar with above, while you can introduce your own changes as you wish. $ git clone http://github.com/skygragon/leetcode-cli $ cd leetcode-cli && ./bin/install + $ leetcode version + +### From source (all-in-one) + + $ git clone http://github.com/skygragon/leetcode-cli + $ cd leetcode-cli && node ./bin/pkg -Then verify the result: +### 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 index 7a1261dc..1a7664f1 100644 --- a/icons/ascii.json +++ b/icons/ascii.json @@ -4,5 +4,9 @@ "like": "*", "unlike": " ", "lock": "$", - "none": " " + "nolock": " ", + "empty": " ", + "ac": "O", + "notac": "X", + "none": "o" } diff --git a/icons/default.json b/icons/default.json index 3bb81cec..a5263a7f 100644 --- a/icons/default.json +++ b/icons/default.json @@ -4,5 +4,9 @@ "like": "★", "unlike": "☆", "lock": "🔒", - "none": " " + "nolock": " ", + "empty": " ", + "ac": "▣", + "notac": "▤", + "none": "⬚" } diff --git a/icons/win7.json b/icons/win7.json index c79ba54a..0e79a481 100644 --- a/icons/win7.json +++ b/icons/win7.json @@ -3,5 +3,10 @@ "no": "×", "like": "♥", "unlike": " ", - "lock": "$" + "lock": "$", + "nolock": " ", + "empty": " ", + "ac": "O", + "notac": "X", + "none": "o" } diff --git a/lib/cache.js b/lib/cache.js index 3c91de0c..42efad0b 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,51 +1,41 @@ -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() { - var dir = h.getCacheDir(); - if (!fs.existsSync(dir)) fs.mkdirSync(dir); + 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() { - var dir = h.getCacheDir(); - if (!fs.existsSync(dir)) return []; - - return fs.readdirSync(dir) - .filter(function(filename) { - return path.extname(filename) === '.json'; - }) + return file.list(file.cacheDir()) + .filter(x => path.extname(x) === '.json') .map(function(filename) { - var k = path.basename(filename, '.json'); - var stat = fs.statSync(h.getCacheFile(k)); + const k = path.basename(filename, '.json'); + const stat = file.stat(file.cacheFile(k)); return { name: k, size: stat.size, diff --git a/lib/chalk.js b/lib/chalk.js index 9ca817d0..ef78e9d8 100644 --- a/lib/chalk.js +++ b/lib/chalk.js @@ -1,32 +1,42 @@ +'use strict'; var _ = require('underscore'); var style = require('ansi-styles'); var supportsColor = require('supports-color'); -var chalk = { - enabled: supportsColor, - use256: supportsColor && supportsColor.has256, - 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 = []; -var DEFAULT = { - black: "#000000", - blue: "#0000ff", - cyan: "#00ffff", - gray: "#999999", - green: "#00ff00", - magenta: "#ff00ff", - red: "#ff0000", - white: "#ffffff", - yellow: "#ffff00" +const DEFAULT = { + black: '#000000', + blue: '#0000ff', + cyan: '#00ffff', + gray: '#999999', + green: '#00ff00', + magenta: '#ff00ff', + red: '#ff0000', + white: '#ffffff', + yellow: '#ffff00' }; chalk.setTheme = function(name) { - var theme = this.themes[name] || this.themes.default || {}; - this.theme = _.extendOwn(DEFAULT, theme); + 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) { @@ -38,40 +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 h = require('./helper'); - _.each(h.getDirData(['colors']), function(f) { - chalk.themes[f.name] = _.mapObject(f.data, function(v, k) { - return chalk.use256 ? style.color.ansi256.hex(v) : style.color.ansi.hex(v); - }); - }); + 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); + + 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', 'gray', '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 }); - }); + } }; module.exports = chalk; diff --git a/lib/cli.js b/lib/cli.js index c58d5cc9..e59cf7f5 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,9 +1,11 @@ +'use strict'; var _ = require('underscore'); var chalk = require('./chalk'); +var cache = require('./cache'); var config = require('./config'); -var core = require('./core'); var h = require('./helper'); +var file = require('./file'); var icon = require('./icon'); var log = require('./log'); var Plugin = require('./plugin'); @@ -12,40 +14,35 @@ var Plugin = require('./plugin'); // global config < local config < cli params // Color is a tricky one so we manually handle it here. function initColor() { - // FIXME: delete this hack when supports-color handles it well. - if (process.env.TERM_PROGRAM === 'iTerm.app') chalk.use256 = true; - - chalk.enabled = config.USE_COLOR && chalk.enabled; + chalk.enabled = config.color.enable && chalk.enabled; chalk.init(); - chalk.setTheme(config.COLOR_THEME); + chalk.setTheme(config.color.theme); } function initIcon() { icon.init(); - icon.setTheme(config.ICON_THEME); + icon.setTheme(config.icon.theme); } function initLogLevel() { log.init(); - var level = 'INFO'; + 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') { - var request = require('request'); + const request = require('request'); request.debug = true; console.error = _.wrap(console.error, function(func) { - var args = _.toArray(arguments); + let args = Array.from(arguments); args.shift(); // FIXME: hack HTTP request log, hope no one else use it... if (args.length > 0 && args[0].indexOf('REQUEST ') === 0) { - args = args.map(function(arg) { - return h.printSafeHTTP(arg); - }); + args = args.map((x) => h.printSafeHTTP(x)); log.trace.apply(log, args); } else { log.info.apply(log, args); @@ -56,27 +53,55 @@ function initLogLevel() { log.setLevel(level); } +function initDir() { + file.init(); + file.mkdir(file.homeDir()) +} + +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 = {}; +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(); + }); + config.init(); initColor(); initIcon(); initLogLevel(); - - Plugin.init(core); - - process.stdout.on('error', function(e) { - if (e.code === 'EPIPE') process.exit(); + initDir() + initPlugins(function(e) { + if (e) return log.fatal(e); + cache.init(); + runCommand(); }); - - require('yargs') - .commandDir('commands') - .completion() - .help() - .strict() - .argv; }; module.exports = cli; diff --git a/lib/commands/cache.js b/lib/commands/cache.js index 80aef0dd..154a3302 100644 --- a/lib/commands/cache.js +++ b/lib/commands/cache.js @@ -1,3 +1,4 @@ +'use strict'; var _ = require('underscore'); var h = require('../helper'); @@ -5,46 +6,61 @@ var chalk = require('../chalk'); var log = require('../log'); var cache = require('../cache'); var session = require('../session'); +var sprintf = require('../sprintf'); -var cmd = { - command: 'cache', - desc: 'show cached problems', - builder: { - all: { - alias: 'a', - type: 'boolean', - describe: 'Delete all cached problems', - default: false - }, - delete: { - alias: 'd', - type: 'string', - describe: 'Delete specific cached problem' - } +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; - if (argv.delete === undefined) { - _.sortBy(cache.list(), function(f) { - var x = parseInt(f.name.split('.')[0], 10); - if (_.isNaN(x)) x = 0; + + 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('%-50s %8s %s ago', + log.printf(' %-60s %8s %s ago', chalk.green(f.name), h.prettySize(f.size), h.prettyTime((Date.now() - f.mtime) / 1000)); }); - } else if (argv.all) { - cache.list().forEach(function(f) { - if (f.name === '.user') return; - cache.del(f.name); - }); - } else { - cache.del(argv.delete); } }; 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 0c49f6ce..c010de86 100644 --- a/lib/commands/list.js +++ b/lib/commands/list.js @@ -1,3 +1,4 @@ +'use strict'; var _ = require('underscore'); var h = require('../helper'); @@ -7,136 +8,99 @@ var log = require('../log'); var core = require('../core'); 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' - }, - tag: { - alias: 't', - type: 'string', - default: '', - describe: 'Filter problems by tags' - } + 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) { session.argv = argv; - core.getProblems(function(e, problems) { + 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)); - }); - } - - if (argv.tag) { - var tag = argv.tag.toLowerCase(); - // TODO: fill company/tags in problems - problems = _.filter(problems, function(x) { - return x.category === tag || - (_.isArray(x.companies) && x.companies.indexOf(tag) !== -1) || - (_.isArray(x.tags) && x.tags.indexOf(tag) !== -1); - }); - } - - var word = String(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', 'starred', 'ac', 'notac', 'None', 'Easy', 'Medium', 'Hard'].forEach(function(x) { - stat[x] = 0; - }); - - problems = _.sortBy(problems, function(x) { - return -x.id; - }); + const stat = {}; + for (let x of ['locked', 'starred', 'ac', 'notac', 'None', 'Easy', 'Medium', 'Hard']) stat[x] = 0; - problems.forEach(function(problem) { + 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.printf('%s %s %s [%3d] %-60s %-6s (%.2f %%)', - (problem.starred ? chalk.yellow(icon.like) : icon.none), - (problem.locked ? chalk.red(icon.lock) : icon.none), + 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.id, + problem.fid, problem.name, h.prettyLevel(problem.level), - problem.percent); - }); + 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.printf(' All: %-9d Listed: %-9d', all, problems.length); - log.printf(' Locked: %-9d Starred: %-9d', stat.locked, stat.starred); - log.printf(' Accept: %-9d Not-AC: %-9d New: %-9d', stat.ac, stat.notac, stat.None); - log.printf(' Easy: %-9d Medium: %-9d Hard: %-9d', stat.Easy, stat.Medium, stat.Hard); + 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 index bdd6aaab..ca5a9cff 100644 --- a/lib/commands/plugin.js +++ b/lib/commands/plugin.js @@ -1,128 +1,123 @@ -var fs = require('fs'); -var path = require('path'); - -var request = require('request'); - +'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'); -var cmd = { +const cmd = { command: 'plugin [name]', - desc: 'show plugins', - builder: { - install: { - alias: 'i', - type: 'boolean', - describe: 'Install plugin', - default: false - }, - enable: { - alias: 'e', - type: 'boolean', - describe: 'Enable plugin', - default: false - }, - disable: { - alias: 'd', - type: 'boolean', - describe: 'Disable plugin', - default: false - }, - delete: { - alias: 'D', - type: 'boolean', - describe: 'Delete plugin', - default: false - } + 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'); } }; -var URL_PLUGIN = 'https://github.com/skygragon/leetcode-cli-plugins/raw/master/plugins/$name.js'; - -function install(src) { - // assume to be a raw plugin name if not js file. - if (path.extname(src) !== '.js') { - src = URL_PLUGIN.replace('$name', src); - } - - // copy to plugins folder - var dst = path.join(Plugin.dir, path.basename(src)); - var dststream = fs.createWriteStream(dst); - - log.debug('copying from ' + src); - var srcstream = src.startsWith('https://') ? request(src) : fs.createReadStream(src); - srcstream.on('response', function(resp) { - if (resp.statusCode !== 200) - srcstream.emit('error', 'HTTP Error: ' + resp.statusCode); - }); - srcstream.on('error', function(e) { - log.error(e); - fs.unlinkSync(dst); - }); - - srcstream.pipe(dststream); - dststream.on('end', function() { - log.debug('copied to ' + dst); +function print(plugins) { + log.info(chalk.gray(sprintf(' %6s %-18s %-15s %s', 'Active', 'Name', 'Version', 'Desc'))); + log.info(chalk.gray('-'.repeat(100))); - // install dependencies - var plugin = require(path.relative(__dirname, dst)); - if (plugin.deps.length === 0) return; - - var cmd = 'npm install --save ' + plugin.deps.join(' '); - log.debug(cmd); - require('child_process').execSync(cmd, { - cwd: path.resolve(__dirname, '../..') - }); - }); + 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; - var name = argv.name; - - if (argv.install) - return install(name); - - var plugins = Plugin.plugins; - if (name) { - plugins = plugins.filter(function(p) { - return p.name === name; - }); - } - if (!argv.enable && !argv.disable && !argv.delete) { - plugins.forEach(function(p) { - log.printf('%s %-18s %-15s %s', h.prettyText('', p.enabled), p.name, p.ver, p.desc); - }); + 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 (plugins.length === 0) return log.error('Plugin not found!'); + if (name) plugins = plugins.filter(x => x.name === name); + if (plugins.length === 0) return log.fatal('Plugin not found!'); - var plugin = plugins[0]; - var oldname = Plugin.fullpath(plugin.file); - var newname; + const p = plugins[0]; + if (p.missing && (argv.enable || argv.disable)) + return log.fatal('Plugin missing, install it first'); if (argv.enable) { - if (plugin.file[0] !== '.') return; - newname = Plugin.fullpath(plugin.file.substr(1)); - - fs.rename(oldname, newname, function(e) { - if (e) log.error(e.message); - }); + p.enabled = true; + p.save(); + print(); } else if (argv.disable) { - if (plugin.file[0] === '.') return; - newname = Plugin.fullpath('.' + plugin.file); - - fs.rename(oldname, newname, function(e) { - if (e) log.error(e.message); - }); + p.enabled = false; + p.save(); + print(); } else if (argv.delete) { - fs.unlink(oldname, function(e) { - if (e) log.error(e.message); - }); + p.delete(); + p.save(); + Plugin.init(); + print(); + } else if (argv.config) { + log.info(JSON.stringify(config.plugins[name] || {}, null, 2)); + } else { + print(plugins); } }; 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 8459270f..7c66204a 100644 --- a/lib/commands/show.js +++ b/lib/commands/show.js @@ -1,9 +1,11 @@ -var fs = require('fs'); +'use strict'; var util = require('util'); var _ = require('underscore'); +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'); @@ -11,101 +13,187 @@ var config = require('../config'); var core = require('../core'); var session = require('../session'); -var cmd = { +const cmd = { command: 'show [keyword]', - 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', - choices: config.LANGS - }, - extra: { - alias: 'x', - type: 'boolean', - default: false, - describe: 'Provide extra problem details in generated file' - }, - desc: { - alias: 'd', - type: 'boolean', - default: true, - describe: 'Show problem description' - }, - template: { - alias: 't', - type: 'boolean', - default: false, - describe: 'Show code template' - } + 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) { - session.argv = argv; - core.getProblem(argv.keyword, function(e, problem) { - if (e) return log.fail(e); - - var template = _.find(problem.templates, function(x) { - return x.value === argv.lang; - }); - if (!template && (argv.template || argv.gen)) - return log.fail('Unknown language "' + argv.lang + '"'); +function genFileName(problem, opts) { + const path = require('path'); + const params = [ + file.fmt(config.file.show, problem), + '', + h.langToExt(opts.lang) + ]; - var filename; - if (argv.gen) { - problem.code = template.defaultCode; + // 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; + } +} - // try to use a new filename to avoid overwrite by mistake - filename = problem.id + '.' + problem.slug + h.langToExt(argv.lang); - var i = 0; - while (fs.existsSync(filename)) - filename = problem.id + '.' + problem.slug + '.' + (i++) + h.langToExt(argv.lang); +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(' '); - core.exportProblem(problem, filename, !argv.extra); + 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; } - if (argv.desc) { - log.printf('[%d] %s %s', problem.id, problem.name, - (problem.starred ? chalk.yellow(icon.like) : icon.none)); - log.info(); - log.info(chalk.underline(problem.link)); - - log.info(); - log.printf('* %s', problem.category); - log.printf('* %s (%.2f%%)', h.prettyLevel(problem.level), problem.percent); - - 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); - - if (argv.template) { - log.info(); - log.info('Template:'); - log.info(); - } - } + 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.template) { - log.info(chalk.yellow(template.defaultCode)); + 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('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 a22360f2..3660432b 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -1,19 +1,29 @@ +'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'); } }; @@ -25,7 +35,7 @@ cmd.handler = function(argv) { core.starProblem(problem, !argv.delete, function(e, starred) { if (e) return log.fail(e); - log.printf('[%d] %s %s', problem.id, problem.name, + 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 1928bf15..772499c4 100644 --- a/lib/commands/stat.js +++ b/lib/commands/stat.js @@ -1,139 +1,224 @@ -var sprintf = require('sprintf-js').sprintf; +'use strict'; +var moment = require('moment'); var _ = require('underscore'); -var config = require('../config'); 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' - }, - tag: { - alias: 't', - type: 'string', - default: 'all', - describe: 'Show statistics on given tag', - choices: ['all'].concat(config.CATEGORIES) - } + 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) { - var n = 30; - var percent = (all > 0) ? done / all : 0; - var x = Math.ceil(n * percent); - log.printf(' %s\t%3d/%-3d (%.2f%%) %s%s', - h.prettyLevel(key), done, all, 100 * percent, - chalk.green(bar('█', x)), - chalk.red(bar('░', n - x))); + 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 showSummary(problems) { - var stats = { - easy: {all: 0, ac: 0}, - medium: {all: 0, ac: 0}, - hard: {all: 0, ac: 0} - }; - var statsNoLock = { +function showProgress(problems) { + const stats = { easy: {all: 0, ac: 0}, medium: {all: 0, ac: 0}, hard: {all: 0, ac: 0} }; - problems.forEach(function(problem) { - var level = problem.level.toLowerCase(); - var state = problem.state.toLowerCase(); + for (let problem of problems) { + const level = problem.level.toLowerCase(); + const state = problem.state.toLowerCase(); - if (!(level in stats)) return; + if (!(level in stats)) continue; ++stats[level].all; - if (!problem.locked) ++statsNoLock[level].all; - if (!(state in stats[level])) return; + if (!(state in stats[level])) continue; ++stats[level][state]; - if (!problem.locked) ++statsNoLock[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); - - log.info(); - log.info('Without Locked:'); - printLine('Easy', statsNoLock.easy.ac, statsNoLock.easy.all); - printLine('Medium', statsNoLock.medium.ac, statsNoLock.medium.all); - printLine('Hard', statsNoLock.hard.ac, statsNoLock.hard.all); } function showGraph(problems) { - var ac = chalk.green('█'); - var notac = chalk.enabled ? chalk.red('█') : 'X'; - var none = chalk.gray('░'); - - 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; - } - }); + 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); + + const graph = []; + for (let problem of problems) + graph[problem.fid] = ICONS[problem.state] || ICONS.none; + + 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.printf('%8d%9d%5d%8d%5d%8d%5d%8d%5d%8d', 1, 10, 11, 20, 21, 30, 31, 40, 41, 50); + line.push(graph[i] || ICONS.empty); - 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)]; + // time to start new row + if (i % (10 * groups) === 0 || i === n) { + log.info(line.join(' ')); + line = [sprintf(rowNumFormat, i)]; } } log.info(); log.printf('%7s%s%3s%s%3s%s', - ' ', ac + chalk.green(' Accepted'), - ' ', notac + chalk.red(' Not Accepted'), - ' ', none + ' Remaining'); + ' ', ICONS.ac + chalk.green(' Accepted'), + ' ', ICONS.notac + chalk.red(' Not Accepted'), + ' ', ICONS.none + ' Remaining'); +} + +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; + + 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'); + + 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.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) { session.argv = argv; - core.getProblems(function(e, problems) { + core.filterProblems(argv, function(e, problems) { if (e) return log.fail(e); - if (argv.tag !== 'all') { - problems = _.filter(problems, function(x) { - return x.category === argv.tag; - }); - } + if (!argv.lock) + problems = problems.filter(x => !x.locked); - if (argv.graph) { - showGraph(problems); - } else { - showSummary(problems); - } + 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 3e949ae7..de0449a3 100644 --- a/lib/commands/submission.js +++ b/lib/commands/submission.js @@ -1,137 +1,133 @@ -var fs = require('fs'); +'use strict'; +var path = require('path'); var _ = require('underscore'); -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 Queue = require('../queue'); var core = require('../core'); 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' - }, - lang: { - alias: 'l', - type: 'string', - default: 'all', - describe: 'Programming language used for previous submission' - } + 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.printf('[%3d] %-60s %s', problem.id, problem.name, - (e ? chalk.red('ERROR: ' + (e.msg || 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?'); // get obj list contain required filetype - var submissionInTargetType = _.filter(submissions, function(x) { - return argv.lang === 'all' || argv.lang === x.lang; - }); - if (submissionInTargetType.length === 0) { - return cb("No previous submission in required language."); - } - var submission = _.find(submissionInTargetType, function(x) { - return x.status_display === 'Accepted'; - }); - - var submissionState = submission === undefined ? 'notac' : 'ac'; + 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 || submissionInTargetType[0]; - - var filename = sprintf('%s/%d.%s.%s.%s%s', - argv.outdir, - problem.id, - problem.slug, - submission.id, - submissionState, - h.langToExt(submission.lang)); + const submission = submissions.find(x => x.status_display === 'Accepted') || submissions[0]; + submission.ac = (submission.status_display === 'Accepted'); - if (!fs.existsSync(argv.outdir)) { - fs.mkdirSync(argv.outdir); - } + 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) { session.argv = argv; - var doTask = _.partial(onTaskRun, 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; } @@ -141,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 19061d72..56f5ed04 100644 --- a/lib/commands/submit.js +++ b/lib/commands/submit.js @@ -1,77 +1,84 @@ -var fs = require('fs'); +'use strict'; var util = require('util'); -var _ = require('underscore'); - 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'); } }; function printResult(actual, k) { if (!actual.hasOwnProperty(k)) return; - var v = actual[k] || ''; - var lines = _.isArray(v) ? v : [v]; - lines.forEach(function(line) { + 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() { - var args = _.toArray(arguments); - var actual = args.shift(); - var line = util.format.apply(util, args); + 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) { session.argv = argv; + if (!file.exist(argv.filename)) + return log.fatal('File ' + argv.filename + ' not exist!'); - if (!fs.existsSync(argv.filename)) - return log.error('File ' + argv.filename + ' not exist!'); - - // 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]; + 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]; + const result = results[0]; printResult(result, 'state'); printLine(result, '%d/%d cases passed (%s)', result.passed, result.total, result.runtime); - // show beat ratio 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.'); - var lang = submission.distributionChart.lang; - var scores = submission.distributionChart.distribution; - var myRuntime = parseFloat(result.runtime); + const lang = submission.distributionChart.lang; + const scores = submission.distributionChart.distribution; + const myRuntime = parseFloat(result.runtime); - var ratio = 0.0; - scores.forEach(function(score) { - if (parseFloat(score[0]) > myRuntime) + 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); @@ -81,6 +88,7 @@ cmd.handler = function(argv) { printResult(result, 'testcase'); printResult(result, 'answer'); printResult(result, 'expected_answer'); + printResult(result, 'stdout'); } // update this problem status in local cache diff --git a/lib/commands/test.js b/lib/commands/test.js index 38e54df8..21c4a4eb 100644 --- a/lib/commands/test.js +++ b/lib/commands/test.js @@ -1,55 +1,64 @@ -var fs = require('fs'); +'use strict'; var _ = require('underscore'); 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: '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 printResult(actual, expect, k) { if (!actual.hasOwnProperty(k)) return; // HACk: leetcode still return 'Accepted' even the answer is wrong!! - var v = actual[k] || ''; + const v = actual[k] || ''; if (k === 'state' && v === 'Accepted') return; - var ok = actual.ok; + let ok = actual.ok; if (expect && !_.isEqual(actual[k], expect[k])) ok = false; - var lines = _.isArray(v) ? v : [v]; - lines.forEach(function(line) { + const lines = Array.isArray(v) ? v : [v]; + for (let line of lines) { if (k !== 'state') line = k + ': ' + line; log.info(' ' + h.prettyText(' ' + line, ok)); - }); + } } function runTest(argv) { - if (!fs.existsSync(argv.filename)) - return log.error('File ' + argv.filename + ' not exist!'); + if (!file.exist(argv.filename)) + return log.fatal('File ' + argv.filename + ' not exist!'); - // 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]; + 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); if (!problem.testable) @@ -62,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); @@ -69,7 +79,8 @@ 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].type)); diff --git a/lib/commands/user.js b/lib/commands/user.js index 17c63296..4cd903cd 100644 --- a/lib/commands/user.js +++ b/lib/commands/user.js @@ -1,32 +1,41 @@ +'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) { session.argv = argv; - var user = null; + let user = null; if (argv.login) { // login prompt.colors = false; @@ -40,7 +49,6 @@ 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)); }); }); @@ -54,9 +62,14 @@ cmd.handler = function(argv) { } else { // show current user user = session.getUser(); - if (user) - log.info('You are now login as', chalk.yellow(user.name)); - else + 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 ec4269c2..4ba16749 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -1,15 +1,21 @@ +'use strict'; var _ = require('underscore'); +var file = require('../file'); var chalk = require('../chalk'); var icon = require('../icon'); var log = require('../log'); var Plugin = require('../plugin'); var session = require('../session'); -var cmd = { +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'); } }; @@ -18,10 +24,10 @@ function printLine(k, v) { } function getVersion() { - var version = require('../../package.json').version; + let version = require('../../package.json').version; try { - var commit = require('../../.env.json').commit.short; + const commit = require('../../.env.json').commit.short; if (commit) version += '-' + commit; } catch (e) {} @@ -30,44 +36,42 @@ function getVersion() { cmd.handler = function(argv) { session.argv = argv; - var version = getVersion(); + 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.info(logo); - var h = require('../helper'); - var os = require('os'); - var config = require('../config'); + const os = require('os'); + const config = require('../config'); log.info('\n[Environment]'); printLine('Node', process.version); printLine('OS', os.platform() + ' ' + os.release()); - printLine('Cache', h.getCacheDir()); - printLine('Config', h.getConfigFile()); + printLine('Cache', file.cacheDir()); + printLine('Config', file.configFile()); log.info('\n[Configuration]'); - _.each(config.getUserConfig(), function(v, k) { - printLine(k, v); + _.each(config.getAll(true), function(v, k) { + if (k === 'plugins') return; + printLine(k, JSON.stringify(v)); }); log.info('\n[Themes]'); - printLine('Colors', _.keys(chalk.themes)); - printLine('Icons', _.keys(icon.themes)); + printLine('Colors', Array.from(chalk.themes.keys())); + printLine('Icons', Array.from(icon.themes.keys())); log.info('\n[Plugins]'); - _.each(Plugin.plugins, function(p, k) { - printLine(p.name, p.ver); - }); + 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 2bf4794b..373b9f0f 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,76 +1,105 @@ +'use strict'; var _ = require('underscore'); +var nconf = require('nconf'); -var h = require('./helper'); +var file = require('./file'); -// usually you don't wanna change those -var DEFAULT_SYS_CONFIG = { - URL_BASE: 'https://leetcode.com', - URL_LOGIN: 'https://leetcode.com/accounts/login/', - URL_PROBLEMS: 'https://leetcode.com/api/problems/$category/', - URL_PROBLEM: 'https://leetcode.com/problems/$slug', - URL_TEST: 'https://leetcode.com/problems/$slug/interpret_solution/', - URL_SUBMIT: 'https://leetcode.com/problems/$slug/submit/', - URL_SUBMISSIONS: 'https://leetcode.com/api/submissions/$slug', - URL_SUBMISSION: 'https://leetcode.com/submissions/detail/$id/', - URL_VERIFY: 'https://leetcode.com/submissions/detail/$id/check/', - URL_FAVORITES: 'https://leetcode.com/list/api/questions', - URL_FAVORITE_DELETE: 'https://leetcode.com/list/api/questions/$hash/$id', +const DEFAULT_CONFIG = { + // usually you don't wanna change those + 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' + } + }, - LANGS: [ - 'bash', - 'c', - 'cpp', - 'csharp', - 'golang', - 'java', - 'javascript', - 'mysql', - 'python', - 'python3', - 'ruby', - 'scala', - 'swift' - ], - - CATEGORIES: [ - 'algorithms', - 'database', - 'shell' - ], - - PLUGINS: {} -}; - -// but you will want change these -var DEFAULT_USER_CONFIG = { - AUTO_LOGIN: false, - COLOR_THEME: 'default', - ICON_THEME: '', - LANG: 'cpp', - MAX_WORKERS: 10, - USE_COLOR: true + // but you will want change these + 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() { - // check local config: ~/.lcconfig - var localConfig = JSON.parse(h.getFileData(h.getConfigFile())) || {}; - _.extendOwn(this, this.getDefaultConfig()); - _.extendOwn(this, localConfig); -}; + nconf.file('local', file.configFile()) + .add('global', {type: 'literal', store: DEFAULT_CONFIG}) + .defaults({}); -Config.prototype.getDefaultConfig = function() { - var cfg = {}; - _.extendOwn(cfg, DEFAULT_SYS_CONFIG); - _.extendOwn(cfg, DEFAULT_USER_CONFIG); - return cfg; + 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.getUserConfig = function() { - return _.pick(this, function(v, k) { - return k in DEFAULT_USER_CONFIG; - }); +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 ece0a4bd..74362f78 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,39 +1,96 @@ -var fs = require('fs'); -var path = require('path'); +'use strict'; var util = require('util'); var _ = require('underscore'); var log = require('./log'); var h = require('./helper'); +var file = require('./file'); var Plugin = require('./plugin'); -var session = require('./session'); -var core = new Plugin(99999999, 'core', '20170722', 'Plugins manager'); +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' + } +}; -core.getProblem = function(keyword, cb) { +function hasTag(o, tag) { + return Array.isArray(o) && o.some(x => x.indexOf(tag.toLowerCase()) >= 0); +} + +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) +}; + +core.filterProblems = function(opts, cb) { this.getProblems(function(e, problems) { if (e) return cb(e); - var problem; - keyword = Number(keyword) || keyword; + for (let q of (opts.query || '').split('')) { + const f = QUERY_HANDLERS[q]; + if (!f) continue; + problems = problems.filter(x => f(x, q)); + } - if (keyword === undefined) { - var user = session.getUser(); - // random select one that not AC-ed yet - problems = _.filter(problems, function(x) { - if (x.state === 'ac') return false; - if (!user.paid && x.locked) return false; - return true; - }); - if (problems.length > 0) - problem = problems[_.random(problems.length - 1)]; - } else { - problem = _.find(problems, function(x) { - return x.id === keyword || x.name === keyword || x.slug === keyword; + for (let t of (opts.tag || [])) { + problems = problems.filter(function(x) { + return x.category === t || + hasTag(x.companies, t) || + hasTag(x.tags, t); }); } + 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; + 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); }); @@ -48,40 +105,27 @@ core.starProblem = function(problem, starred, cb) { core.next.starProblem(problem, starred, cb); }; -core.exportProblem = function(problem, f, codeOnly) { - var output = ''; - problem.code = problem.code.replace(/\r\n/g, '\n'); - - if (codeOnly) { - output = problem.code; - } else { - var input = { - comment: 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 || ''); +core.exportProblem = function(problem, opts) { + const data = _.extend({}, problem); + // 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. - var desc = input.desc.replace(/\r\n/g, '\n') - .replace(/^ /mg, '⁠'); - - var wrap = require('wordwrap')(79 - input.comment.line.length); - input.desc = wrap(desc).split('\n'); - - var tpl = h.getFileData(path.resolve(__dirname, '../source.tpl')); - output = _.template(tpl)(input); - } - - if (h.isWindows()) { - output = output.replace(/\n/g, '\r\n'); - } else { - output = output.replace(/\r\n/g, '\n'); + 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); + 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 23a7ed9a..8806086e 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -1,16 +1,17 @@ -var fs = require('fs'); -var path = require('path'); - +'use strict'; var _ = require('underscore'); +var ora = require('ora'); + +var file = require('./file'); -var UNITS_SIZE = [ +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} ]; -var UNITS_TIME = [ +const UNITS_TIME = [ {unit: 's', name: 'seconds', count: 60}, {unit: 'm', name: 'minutes', count: 60}, {unit: 'h', name: 'hours', count: 24}, @@ -21,41 +22,40 @@ var UNITS_TIME = [ ]; function getUnit(units, v) { - for (var i = 0; i < units.length; ++i) { + 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; } } -var 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: 'mysql', ext: '.sql', style: '#'}, - {lang: 'python', ext: '.py', style: '#'}, - {lang: 'python3', ext: '.py3', style: '#'}, - {lang: 'ruby', ext: '.rb', style: '#'}, - {lang: 'scala', ext: '.scala', style: 'c'}, - {lang: 'swift', ext: '.swift', style: 'c'} +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'} ]; -var h = {}; +const h = {}; h.KEYS = { - user: '.user', + user: '../user', + stat: '../stat', + plugins: '../../plugins', problems: 'problems', - problem: function(p) { - return p.id + '.' + p.slug + '.' + p.category; - } -}; - -h.isWindows = function() { - return process.platform === 'win32'; + problem: p => p.fid + '.' + p.slug + '.' + p.category }; h.prettyState = function(state) { @@ -67,8 +67,8 @@ h.prettyState = function(state) { }; h.prettyText = function(text, yesNo) { - var chalk = require('./chalk'); - var icon = require('./icon'); + const chalk = require('./chalk'); + const icon = require('./icon'); switch (yesNo) { case true: return chalk.green(icon.yes + text); case false: return chalk.red(icon.no + text); @@ -77,22 +77,22 @@ h.prettyText = function(text, yesNo) { }; h.prettySize = function(n) { - var res = getUnit(UNITS_SIZE, n); + const res = getUnit(UNITS_SIZE, n); return res[0].toFixed(2) + res[1].unit; }; h.prettyTime = function(n) { - var res = getUnit(UNITS_TIME, n); + const res = getUnit(UNITS_TIME, n); return res[0].toFixed(0) + ' ' + res[1].name; }; h.prettyLevel = function(level) { - var chalk = require('./chalk'); - switch (level) { - case 'Easy': return chalk.green('Easy'); - case 'Medium': return chalk.yellow('Medium'); - case 'Hard': return chalk.red('Hard'); - default: return ' '; + 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; } }; @@ -121,92 +121,35 @@ h.statusToName = function(sc) { }; h.langToExt = function(lang) { - var res = _.find(LANGS, function(x) { - return x.lang === lang; - }); + const res = LANGS.find(x => x.lang === lang); return res ? res.ext : '.raw'; }; h.extToLang = function(fullpath) { - var ext = path.extname(fullpath); - var res = _.find(LANGS, function(x) { - return x.ext === ext; - }); + const res = LANGS.find(x => fullpath.endsWith(x.ext)); return res ? res.lang : 'unknown'; }; h.langToCommentStyle = function(lang) { - var res = _.find(LANGS, function(x) { - return x.lang === lang; - }); + const res = LANGS.find(x => x.lang === lang); - return (res && res.style === '#') ? - {start: '#', line: '#', end: '#'} : - {start: '/*', line: ' *', end: ' */'}; -}; - -h.getDirData = function(paths) { - paths.unshift('..'); - paths.unshift(__dirname); - var dir = path.join.apply(path, paths); - - return _.map(fs.readdirSync(dir), function(file) { - var fullpath = path.join(dir, file); - var ext = path.extname(file); - - var name = path.basename(file, ext); - var data = null; - - switch (ext) { - case '.js': - data = require(fullpath); - break; - case '.json': - data = JSON.parse(h.getFileData(fullpath)); - break; - default: - break; - } - return {name: name, data: data, file: file}; - }); -}; - -h.getFilename = function(fullpath) { - return path.basename(fullpath, path.extname(fullpath)); -}; - -h.getFileData = function(fullpath) { - return fs.existsSync(fullpath) ? fs.readFileSync(fullpath).toString() : null; -}; - -h.getHomeDir = function() { - return process.env.HOME || process.env.USERPROFILE; -}; - -h.getCacheDir = function() { - return path.join(this.getHomeDir(), '.lc'); -}; - -h.getCacheFile = function(k) { - return path.join(this.getCacheDir(), k + '.json'); -}; - -h.getConfigFile = function() { - return path.join(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 ' + - (this.isWindows() ? ' and ' : '')); + (file.isWindows() ? ' and ' : '')); stdin.on('readable', function() { - var data = stdin.read(); + const data = stdin.read(); if (data) { // windows doesn't treat ctrl-D as EOF - if (h.isWindows() && data.toString() === '\x04\r\n') { + if (file.isWindows() && data.toString() === '\x04\r\n') { stdin.emit('end'); } else { bufs.push(data); @@ -220,13 +163,13 @@ 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]; } } @@ -239,4 +182,28 @@ h.printSafeHTTP = function(msg) { .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 index 00217a73..c147a792 100644 --- a/lib/icon.js +++ b/lib/icon.js @@ -1,28 +1,31 @@ +'use strict'; var _ = require('underscore'); -var h = require('./helper'); +var file = require('./file'); -var icons = { +const icons = { yes: '✔', no: '✘', like: '★', unlike: '☆', lock: '🔒', - none: ' ', + empty: ' ', + ac: '▣', + notac: '▤', + none: '⬚', - themes: [] + themes: new Map() }; icons.setTheme = function(name) { - var defaultName = h.isWindows() ? 'win7' : 'default'; - var theme = this.themes[name] || this.themes[defaultName] || {}; + const defaultName = file.isWindows() ? 'win7' : 'default'; + const theme = this.themes.get(name) || this.themes.get(defaultName) || {}; _.extendOwn(this, theme); }; icons.init = function() { - _.each(h.getDirData(['icons']), function(f) { - icons.themes[f.name] = f.data; - }); + for (let f of file.listCodeDir('icons')) + icons.themes.set(f.name, f.data); }; module.exports = icons; diff --git a/lib/log.js b/lib/log.js index f7fd76c9..394b356c 100644 --- a/lib/log.js +++ b/lib/log.js @@ -1,55 +1,63 @@ +'use strict'; var _ = require('underscore'); -var sprintf = require('sprintf-js').sprintf; var chalk = require('./chalk'); +var sprintf = require('./sprintf'); -var log = { +const log = { output: _.bind(console.log, console), level: null, - levels: { - TRACE: {value: 0, color: 'gray'}, - DEBUG: {value: 1, color: 'gray'}, - INFO: {value: 2, color: ''}, - WARN: {value: 3, color: 'yellow'}, - ERROR: {value: 4, color: 'red'} - } + 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[name] || this.levels.INFO; + this.level = this.levels.get(name) || this.levels.get('INFO'); }; log.isEnabled = function(name) { - return this.level.value <= this.levels[name].value; + return this.level.value <= this.levels.get(name).value; }; log.fail = function(e) { - log.error(sprintf('%s [%d]', (e.msg || e), (e.statusCode || 0))); + 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, _.toArray(arguments))); + log.info(sprintf.apply(null, Array.from(arguments))); }; log.init = function() { this.setLevel('INFO'); - _.keys(this.levels).forEach(function(name) { + for (let name of this.levels.keys()) { log[name.toLowerCase()] = function() { - var level = log.levels[name]; + const level = log.levels.get(name); if (log.level.value > level.value) return; - var args = _.toArray(arguments); + const args = Array.from(arguments); if (name !== 'INFO') args.unshift('[' + name + ']'); - var s = args.map(function(arg) { - return arg.toString(); - }).join(' '); + 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 index 45dec672..bbd6da44 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -1,70 +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.desc = desc || ''; + this.enabled = true; - this.deps = deps || []; + 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.config = config.plugins[this.name] || {}; this.next = null; }; Plugin.prototype.setNext = function(next) { - this.next = this.__proto__ = 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) { - Plugin.dir = path.resolve(__dirname, '../lib/plugins/'); + log.trace('initializing all plugins'); + head = head || require('./core'); - var plugins = []; - h.getDirData(['lib', 'plugins']).forEach(function(f) { - var p = f.data; - if (!p) return; + 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; - if (f.name[0] === '.') p.enabled = false; + p.enabled = stats[p.name]; - log.trace('found plugin: ' + p.name + '=' + p.ver); + 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); } + } - plugins.push(p); + // 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); }); - // chain the plugins together - // the one has bigger `id` comes first - plugins = _.sortBy(plugins, function(p) { - return -p.id; + dststream.on('error', function(e) { + error = e; + dststream.end(); + }); + dststream.on('close', function() { + spin.stop(); + if (error) file.rm(dst); + return cb(error, dst); }); - var last = head; - plugins.forEach(function(p) { - if (!p.enabled) return; - last.setNext(p); - last = p; + 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.plugins = plugins; }; -Plugin.fullpath = function(filename) { - return path.join(Plugin.dir, filename); +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 index ae45710c..677c6c84 100644 --- a/lib/plugins/cache.js +++ b/lib/plugins/cache.js @@ -1,3 +1,4 @@ +'use strict'; var _ = require('underscore'); var cache = require('../cache'); @@ -6,15 +7,10 @@ var log = require('../log'); var Plugin = require('../plugin'); var session = require('../session'); -var plugin = new Plugin(20, 'cache', '', 'Plugin to provide local cache.'); - -plugin.init = function() { - Plugin.prototype.init.call(this); - cache.init(); -}; +const plugin = new Plugin(50, 'cache', '', 'Plugin to provide local cache.'); plugin.getProblems = function(cb) { - var problems = cache.get(h.KEYS.problems); + const problems = cache.get(h.KEYS.problems); if (problems) { log.debug('cache hit: problems.json'); return cb(null, problems); @@ -29,8 +25,8 @@ plugin.getProblems = function(cb) { }; plugin.getProblem = function(problem, cb) { - var k = h.KEYS.problem(problem); - var _problem = cache.get(k); + const k = h.KEYS.problem(problem); + const _problem = cache.get(k); if (_problem) { log.debug('cache hit: ' + k + '.json'); _.extendOwn(problem, _problem); @@ -49,17 +45,15 @@ 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. - var _problem = _.omit(problem, ['locked', 'state', 'starred']); + const _problem = _.omit(problem, ['locked', 'state', 'starred']); return cache.set(h.KEYS.problem(problem), _problem); }; plugin.updateProblem = function(problem, kv) { - var problems = cache.get(h.KEYS.problems); + const problems = cache.get(h.KEYS.problems); if (!problems) return false; - var _problem = _.find(problems, function(x) { - return x.id === problem.id; - }); + const _problem = problems.find(x => x.id === problem.id); if (!_problem) return false; _.extend(_problem, kv); @@ -79,7 +73,7 @@ plugin.logout = function(user, purge) { if (!user) user = session.getUser(); if (purge) session.deleteUser(); // NOTE: need invalidate any user related cache - cache.del(h.KEYS.problems); + session.deleteCodingSession(); return user; }; diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js index 1ef162e5..24331ec6 100644 --- a/lib/plugins/leetcode.js +++ b/lib/plugins/leetcode.js @@ -1,3 +1,4 @@ +'use strict'; var util = require('util'); var _ = require('underscore'); @@ -7,35 +8,38 @@ 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 Queue = require('../queue'); var session = require('../session'); -var plugin = new Plugin(10, 'leetcode', '', +const plugin = new Plugin(10, 'leetcode', '', 'Plugin to talk with leetcode APIs.'); +var spin; + // update options with user credentials -function signOpts(opts, user) { +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'; -} +}; -function makeOpts(url) { - var opts = {}; +plugin.makeOpts = function(url) { + const opts = {}; opts.url = url; opts.headers = {}; if (session.isLogin()) - signOpts(opts, session.getUser()); + plugin.signOpts(opts, session.getUser()); return opts; -} +}; -function checkError(e, resp, expectedStatus) { +plugin.checkError = function(e, resp, expectedStatus) { if (!e && resp && resp.statusCode !== expectedStatus) { - var code = resp.statusCode; + const code = resp.statusCode; log.debug('http error: ' + code); if (code === 403 || code === 401) { @@ -45,13 +49,16 @@ function checkError(e, resp, expectedStatus) { } } return e; +}; + +plugin.init = function() { + config.app = 'leetcode'; } plugin.getProblems = function(cb) { log.debug('running leetcode.getProblems'); - - var problems = []; - var doTask = function(category, taskDone) { + let problems = []; + const getCategory = function(category, queue, cb) { plugin.getCategoryProblems(category, function(e, _problems) { if (e) { log.debug(category + ': failed to getProblems: ' + e.msg); @@ -59,24 +66,28 @@ plugin.getProblems = function(cb) { log.debug(category + ': getProblems got ' + _problems.length + ' problems'); problems = problems.concat(_problems); } - return taskDone(e); + return cb(e); }); }; - queue.run(config.CATEGORIES, doTask, function(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); - var opts = makeOpts(config.URL_PROBLEMS.replace('$category', category)); + const opts = plugin.makeOpts(config.sys.urls.problems.replace('$category', category)); + spin.text = 'Downloading category ' + category; request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); - var json = JSON.parse(body); + const json = JSON.parse(body); // leetcode permits anonymous access to the problem list // while we require login first to make a better experience. @@ -85,21 +96,16 @@ plugin.getCategoryProblems = function(category, cb) { return cb(session.errors.EXPIRED); } - var user = session.getUser(); - user.paid = json.is_paid; - session.saveUser(user); - - var problems = json.stat_status_pairs - .filter(function(p) { - return !p.stat.question__hide; - }) + 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.URL_PROBLEM.replace('$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), @@ -114,45 +120,55 @@ plugin.getCategoryProblems = function(category, cb) { plugin.getProblem = function(problem, cb) { log.debug('running leetcode.getProblem'); - var opts = makeOpts(problem.link); + const user = session.getUser(); + if (problem.locked && !user.paid) return cb('failed to load locked problem!'); - request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); - if (e) return cb(e); + const opts = plugin.makeOpts(config.sys.urls.graphql); + opts.headers.Origin = config.sys.urls.base; + opts.headers.Referer = problem.link; - var $ = cheerio.load(body); - var spans = $('ul[class=side-bar-list] li[class=list-item] span'); + 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' + }; - problem.totalAC = $(spans[3]).text(); - problem.totalSubmit = $(spans[5]).text(); + 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); - // TODO: revisit this if later leetcode remove this element. - // Then we need parse the body to get the description. - problem.desc = $('meta[name="description"]').attr('content'); - problem.desc = he.decode(problem.desc); + const q = body.data.question; + if (!q) return cb('failed to load problem!'); - var pageData; - var r = /(var pageData[^;]+;)/m; - var re = body.match(r); - if (!re) { - var user = session.getUser(); - if (problem.locked && user.paid) { - e = session.errors.EXPIRED; - } else { - e = 'failed to load' + (problem.locked ? ' locked' : '') + ' problem!'; - } - return cb(e); - } + problem.totalAC = JSON.parse(q.stats).totalAccepted; + problem.totalSubmit = JSON.parse(q.stats).totalSubmission; - eval(re[1]); - problem.templates = pageData.codeDefinition; - problem.testcase = pageData.sampleTestCase; - problem.testable = pageData.enableRunCode; - problem.templateMeta = eval(pageData.metaData); + 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()); - r = /https:\/\/discuss.leetcode.com\/category\/(\d+)/; - re = body.match(r); - if (re) problem.discuss = re[1]; + 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); }); @@ -160,25 +176,27 @@ plugin.getProblem = function(problem, cb) { function runCode(opts, problem, cb) { opts.method = 'POST'; - opts.headers.Origin = config.URL_BASE; + opts.headers.Origin = config.sys.urls.base; opts.headers.Referer = problem.link; opts.json = true; - opts._delay = opts._delay || 1; // in seconds + opts._delay = opts._delay || config.network.delay || 1; // in seconds opts.body = opts.body || {}; _.extendOwn(opts.body, { - lang: h.extToLang(problem.file), + lang: problem.lang, question_id: parseInt(problem.id, 10), test_mode: false, - typed_code: h.getFileData(problem.file) + typed_code: file.data(problem.file) }); + const spin = h.spin('Sending code to judge'); request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); + spin.stop(); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); if (body.error) { - if (body.error.indexOf('too soon') < 0) + if (!body.error.includes('too soon')) return cb(body.error); // hit 'run code too soon' error, have to wait a bit @@ -188,7 +206,7 @@ function runCode(opts, problem, cb) { ++opts._delay; log.debug('Will retry after %d seconds...', opts._delay); - var reRun = _.partial(runCode, opts, problem, cb); + const reRun = _.partial(runCode, opts, problem, cb); return setTimeout(reRun, opts._delay * 1000); } @@ -199,30 +217,31 @@ function runCode(opts, problem, cb) { }); } -function verifyResult(opts, jobs, results, cb) { - if (jobs.length === 0) return cb(null, results); - +function verifyResult(task, queue, cb) { + const opts = queue.ctx.opts; opts.method = 'GET'; - opts.url = config.URL_VERIFY.replace('$id', jobs[0].id); + opts.url = config.sys.urls.verify.replace('$id', task.id); + const spin = h.spin('Waiting for judge result'); request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); + spin.stop(); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); - var result = JSON.parse(body); + let result = JSON.parse(body); if (result.state === 'SUCCESS') { result = formatResult(result); - _.extendOwn(result, jobs[0]); - results.push(result); - jobs.shift(); + _.extendOwn(result, task); + queue.ctx.results.push(result); + } else { + queue.addTask(task); } - - setImmediate(verifyResult, opts, jobs, results, cb); + return cb(); }); } function formatResult(result) { - var x = { + const x = { ok: result.run_success, answer: result.code_answer || '', runtime: result.status_runtime || '', @@ -233,17 +252,22 @@ function formatResult(result) { }; x.error = _.chain(result) - .pick(function(v, k) { - return /_error$/.test(k) && v.length > 0; - }) + .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 { - x.stdout = util.inspect((result.code_output || []).join('\n')); + 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! @@ -256,47 +280,52 @@ function formatResult(result) { plugin.testProblem = function(problem, cb) { log.debug('running leetcode.testProblem'); - var opts = makeOpts(config.URL_TEST.replace('$slug', problem.slug)); + 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); - var jobs = [ + const tasks = [ {type: 'Actual', id: task.interpret_id}, {type: 'Expected', id: task.interpret_expected_id} ]; - verifyResult(opts, jobs, [], cb); + 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'); - var opts = makeOpts(config.URL_SUBMIT.replace('$slug', problem.slug)); + 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); - var jobs = [{type: 'Actual', id: task.submission_id}]; - verifyResult(opts, jobs, [], cb); + 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'); - var opts = makeOpts(config.URL_SUBMISSIONS.replace('$slug', problem.slug)); - opts.headers.Referer = config.URL_PROBLEM.replace('$slug', problem.slug); + 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 = checkError(e, resp, 200); + 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. - var submissions = JSON.parse(body).submissions_dump; - _.each(submissions, function(submission) { + const submissions = JSON.parse(body).submissions_dump; + for (let submission of submissions) submission.id = _.last(_.compact(submission.url.split('/'))); - }); return cb(null, submissions); }); @@ -304,16 +333,16 @@ plugin.getSubmissions = function(problem, cb) { plugin.getSubmission = function(submission, cb) { log.debug('running leetcode.getSubmission'); - var opts = makeOpts(config.URL_SUBMISSION.replace('$id', submission.id)); + const opts = plugin.makeOpts(config.sys.urls.submission.replace('$id', submission.id)); request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); - var re = body.match(/submissionCode:\s('[^']*')/); + let re = body.match(/submissionCode:\s('[^']*')/); if (re) submission.code = eval(re[1]); - re = body.match(/distribution_formatted:\s('[^']+')/); + re = body.match(/runtimeDistributionFormatted:\s('[^']+')/); if (re) submission.distributionChart = JSON.parse(eval(re[1])); return cb(null, submission); }); @@ -321,13 +350,13 @@ plugin.getSubmission = function(submission, cb) { plugin.starProblem = function(problem, starred, cb) { log.debug('running leetcode.starProblem'); - var opts = makeOpts(); - opts.headers.Origin = config.URL_BASE; + const opts = plugin.makeOpts(); + opts.headers.Origin = config.sys.urls.base; opts.headers.Referer = problem.link; - var user = session.getUser(); + const user = session.getUser(); if (starred) { - opts.url = config.URL_FAVORITES; + opts.url = config.sys.urls.favorites; opts.method = 'POST'; opts.json = true; opts.body = { @@ -335,18 +364,14 @@ plugin.starProblem = function(problem, starred, cb) { question_id: problem.id }; } else { - opts.url = config.URL_FAVORITE_DELETE + opts.url = config.sys.urls.favorite_delete .replace('$hash', user.hash) .replace('$id', problem.id); opts.method = 'DELETE'; } - var req = request(opts, function(e, resp, body) { - // FIXME: not sure why we hit HPE_INVALID_CONSTANT error? - if (req && req.response && req.response.statusCode === 204) - return cb(null, starred); - - e = checkError(e, resp, 204); + request(opts, function(e, resp, body) { + e = plugin.checkError(e, resp, 204); if (e) return cb(e); cb(null, starred); @@ -355,30 +380,102 @@ plugin.starProblem = function(problem, starred, cb) { plugin.getFavorites = function(cb) { log.debug('running leetcode.getFavorites'); - var opts = makeOpts(config.URL_FAVORITES); + const opts = plugin.makeOpts(config.sys.urls.favorites); + const spin = h.spin('Retrieving user favorites'); request(opts, function(e, resp, body) { - e = checkError(e, resp, 200); + spin.stop(); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); - var favorites = JSON.parse(body); + const favorites = JSON.parse(body); return cb(null, favorites); }); }; -plugin.login = function(user, cb) { - log.debug('running leetcode.login'); - request(config.URL_LOGIN, function(e, resp, body) { - e = checkError(e, resp, 200); +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'); - var opts = { - url: config.URL_LOGIN, + const opts = { + url: config.sys.urls.login, headers: { - Origin: config.URL_BASE, - Referer: config.URL_LOGIN, + Origin: config.sys.urls.base, + Referer: config.sys.urls.login, Cookie: 'csrftoken=' + user.loginCSRF + ';' }, form: { @@ -393,23 +490,43 @@ plugin.login = function(user, cb) { 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]; session.saveUser(user); + return cb(null, user); + }); + }); +}; - plugin.getFavorites(function(e, favorites) { - if (e) return cb(e); - - // TODO: pick other useful values from favorites - var favorite = _.find(favorites.favorites.private_favorites, function(f) { - return f.name === 'Favorite'; - }); - user.hash = favorite.id_hash; +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); + } - return cb(null, user); - }); + 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 index eea1eb0b..5dcf35e0 100644 --- a/lib/plugins/retry.js +++ b/lib/plugins/retry.js @@ -1,23 +1,28 @@ -var _ = require('underscore'); - +'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 AUTO_LOGIN is on.'); + 'Plugin to retry last failed request if autologin.enable is on.'); -var count = {}; +const count = {}; function canRetry(e, name) { - return e && config.AUTO_LOGIN && (e === session.errors.EXPIRED) && count[name] < 1; + return config.autologin.enable && + (e === session.errors.EXPIRED) && + (count[name] || 0) < config.autologin.retry; } plugin.init = function() { - var names = [ + const names = [ + 'activateSession', + 'createSession', + 'deleteSession', 'getProblems', 'getProblem', + 'getSessions', 'getSubmissions', 'getSubmission', 'getFavorites', @@ -26,15 +31,15 @@ plugin.init = function() { 'starProblem' ]; - names.forEach(function(name) { + for (let name of names) { count[name] = 0; plugin[name] = function() { - var args = _.toArray(arguments); - var cb = args.pop(); + const args = Array.from(arguments); + const cb = args.pop(); - var _cb = function() { - var results = _.toArray(arguments); - var e = results[0]; + const _cb = function() { + const results = Array.from(arguments); + const e = results[0]; if (!canRetry(e, name)) { count[name] = 0; return cb.apply(null, results); @@ -47,10 +52,10 @@ plugin.init = function() { }); }; - var next = plugin.next; + const next = plugin.next; next[name].apply(next, args.concat(_cb)); }; - }); + } }; // leetcode.com is limiting one session alive in the same time, @@ -60,7 +65,7 @@ plugin.init = function() { plugin.relogin = function(cb) { log.debug('session expired, try to re-login...'); - var user = session.getUser(); + const user = session.getUser(); if (!user) { log.debug('relogin failed: no user found, please login again'); return cb(); diff --git a/lib/queue.js b/lib/queue.js index 26dcd35f..68c24ce3 100644 --- a/lib/queue.js +++ b/lib/queue.js @@ -1,38 +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 workerRun(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(ctx.error); + 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) { - if (e) ctx.error = 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. - setImmediate(workerRun, ctx); + setImmediate(function() { self.workerRun(); }); }); -} - -queue.run = function(tasks, doTask, cb) { - var ctx = { - tasks: _.clone(tasks), - doTask: doTask, - cb: cb, - workers: config.MAX_WORKERS || 1, - error: null - }; - - for (var i = 0; i < ctx.workers; ++i) { - setImmediate(workerRun, ctx); - } }; -module.exports = queue; +module.exports = Queue; diff --git a/lib/session.js b/lib/session.js index 3c087aaf..6e70ed90 100644 --- a/lib/session.js +++ b/lib/session.js @@ -1,10 +1,12 @@ +'use strict'; +var moment = require('moment'); var _ = require('underscore'); var cache = require('./cache'); var config = require('./config'); var h = require('./helper'); -var session = {}; +const session = {}; session.errors = { EXPIRED: { @@ -20,7 +22,7 @@ session.getUser = function() { 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. - var _user = _.omit(user, config.AUTO_LOGIN ? [] : ['pass']); + const _user = _.omit(user, config.autologin.enable ? [] : ['pass']); cache.set(h.KEYS.user, _user); }; @@ -28,8 +30,29 @@ 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 fc100584..a9af9f7c 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,34 @@ { "name": "leetcode-cli", - "version": "2.0.3", + "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", - "prompt": "^1.0.0", - "request": "^2.74.0", - "sprintf-js": "^1.0.3", - "supports-color": "^3.2.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 1d2ab8e6..00000000 --- a/source.tpl +++ /dev/null @@ -1,14 +0,0 @@ -<%= comment.start %> -<%= comment.line %> [<%= id %>] <%= 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 %> -<% _.each(desc, function(x) { %><%= comment.line %> <%= x %> -<% }) %><%= comment.end %> -<%= 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 e6e02726..c8a4d61f 100644 --- a/test/mock/add-two-numbers.20161015.json +++ b/test/mock/add-two-numbers.20161015.json @@ -1 +1 @@ -{"state":"ac","id":2,"category":"algorithms","name":"Add Two Numbers","key":"add-two-numbers","link":"https://leetcode.com/problems/add-two-numbers","locked":false,"percent":25.368142876074806,"level":"Medium","starred":true,"totalAC":"195263","totalSubmit":"769711","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};"} +{"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/find-the-difference.html.20170714 b/test/mock/find-the-difference.html.20170714 deleted file mode 100644 index 78b172be..00000000 --- a/test/mock/find-the-difference.html.20170714 +++ /dev/null @@ -1,1176 +0,0 @@ - - - - - - - Find the Difference - LeetCode - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -
- -
-
- -
-

- 389. Find the Difference -

-
- -
-
-
-
- Loading Question ... -
-
- - - -
-
- - -
- - -
- -
- - - - Discuss - - - - - - - - Pick One - -
-
- -
- -
-

-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.
-

-
- - -
-
-
- -
-
- - - - - - - - -
-
-
- Seen this question in a real interview before?   - - -
- Yes -
-
- No -
-
-
-
- When did you encounter this question?   - - - -
- last week -
- -
- last month -
- -
- last 3 month -
- -
- last 6 month -
- -
- more than 6 months -
- -
- other -
- -
-
- -
- Which company?   - - - -
- 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 -
- - - - - - -
-
-
-
-
-
- - -
- - -
- - - - - -
- -
-
- -
-
-
-
-
-
-
- -
- - - - - - -   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/plugins/test_cache.js b/test/plugins/test_cache.js index 68f881ba..6b3114ca 100644 --- a/test/plugins/test_cache.js +++ b/test/plugins/test_cache.js @@ -1,48 +1,51 @@ -var execSync = require('child_process').execSync; -var fs = require('fs'); +'use strict'; +const _ = require('underscore'); +const assert = require('chai').assert; +const rewire = require('rewire'); -var _ = require('underscore'); -var assert = require('chai').assert; -var rewire = require('rewire'); - -var log = require('../../lib/log'); -var config = require('../../lib/config'); - -var cache = rewire('../../lib/cache'); -var h = rewire('../../lib/helper'); -var session = rewire('../../lib/session'); -var plugin = rewire('../../lib/plugins/cache'); - -var HOME = './tmp'; +const h = require('../../lib/helper'); +const log = require('../../lib/log'); +const config = require('../../lib/config'); +const th = require('../helper'); describe('plugin:cache', function() { - var PROBLEMS = [ - {id: 0, name: 'name0', slug: 'slug0', starred: false, category: 'algorithms'}, - {id: 1, name: 'name1', slug: 'slug1', starred: true, category: 'algorithms'} + 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'} ]; - var PROBLEM = {id: 0, slug: 'slug0', category: 'algorithms'}; - - var NEXT = {}; + const PROBLEM = {id: 0, fid: 0, slug: 'slug0', category: 'algorithms'}; before(function() { log.init(); config.init(); - plugin.init(); + }); + + beforeEach(function() { + th.clean(); + next = {}; + + file = rewire('../../lib/file'); + file.cacheDir = () => th.DIR; - h.getHomeDir = function() { - return HOME; - }; + cache = rewire('../../lib/cache'); + cache.__set__('file', file); + cache.init(); - cache.__set__('h', h); + session = rewire('../../lib/session'); session.__set__('cache', cache); + + plugin = rewire('../../lib/plugins/cache'); plugin.__set__('cache', cache); plugin.__set__('session', session); - plugin.setNext(NEXT); - }); + plugin.init(); - beforeEach(function() { - execSync('rm -rf ' + HOME); - fs.mkdirSync(HOME); + plugin.setNext(next); }); describe('#getProblems', function() { @@ -58,10 +61,7 @@ describe('plugin:cache', function() { it('should getProblems w/o cache ok', function(done) { cache.del('problems'); - - NEXT.getProblems = function(cb) { - return cb(null, PROBLEMS); - }; + next.getProblems = cb => cb(null, PROBLEMS); plugin.getProblems(function(e, problems) { assert.equal(e, null); @@ -72,10 +72,7 @@ describe('plugin:cache', function() { it('should getProblems w/o cache fail if client error', function(done) { cache.del('problems'); - - NEXT.getProblems = function(cb) { - return cb('client getProblems error'); - }; + next.getProblems = cb => cb('client getProblems error'); plugin.getProblems(function(e, problems) { assert.equal(e, 'client getProblems error'); @@ -99,10 +96,7 @@ describe('plugin:cache', function() { it('should getProblem w/o cache ok', function(done) { cache.set('problems', PROBLEMS); cache.del('0.slug0.algorithms'); - - NEXT.getProblem = function(problem, cb) { - return cb(null, PROBLEMS[0]); - }; + next.getProblem = (problem, cb) => cb(null, PROBLEMS[0]); plugin.getProblem(_.clone(PROBLEM), function(e, problem) { assert.equal(e, null); @@ -114,10 +108,7 @@ describe('plugin:cache', function() { it('should getProblem fail if client error', function(done) { cache.set('problems', PROBLEMS); cache.del('0.slug0.algorithms'); - - NEXT.getProblem = function(problem, cb) { - return cb('client getProblem error'); - }; + next.getProblem = (problem, cb) => cb('client getProblem error'); plugin.getProblem(_.clone(PROBLEM), function(e, problem) { assert.equal(e, 'client getProblem error'); @@ -130,14 +121,14 @@ describe('plugin:cache', function() { it('should ok', function() { cache.del('0.slug0.algorithms'); - var problem = _.clone(PROBLEMS[0]); + const problem = _.clone(PROBLEMS[0]); problem.locked = true; problem.state = 'ac'; - var ret = plugin.saveProblem(problem); + const ret = plugin.saveProblem(problem); assert.equal(ret, true); assert.deepEqual(cache.get('0.slug0.algorithms'), - {id: 0, slug: 'slug0', name: 'name0', category: 'algorithms'}); + {id: 0, fid: 0, slug: 'slug0', name: 'name0', category: 'algorithms'}); }); }); // #saveProblem @@ -145,15 +136,15 @@ describe('plugin:cache', function() { it('should updateProblem ok', function(done) { cache.set('problems', PROBLEMS); - var kv = {value: 'value00'}; - var ret = plugin.updateProblem(PROBLEMS[0], kv); + 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, name: 'name0', slug: 'slug0', value: 'value00', starred: false, category: 'algorithms'}, - {id: 1, name: 'name1', slug: 'slug1', starred: true, category: 'algorithms'} + {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(); }); @@ -161,31 +152,29 @@ describe('plugin:cache', function() { it('should updateProblem fail if no problems found', function() { cache.del('problems'); - var ret = plugin.updateProblem(PROBLEMS[0], {}); + const ret = plugin.updateProblem(PROBLEMS[0], {}); assert.equal(ret, false); }); it('should updateProblem fail if unknown problem', function() { cache.set('problems', [PROBLEMS[1]]); - var ret = plugin.updateProblem(PROBLEMS[0], {}); + const ret = plugin.updateProblem(PROBLEMS[0], {}); assert.equal(ret, false); }); }); // #updateProblem describe('#user', function() { - var USER = {name: 'test-user', pass: 'password'}; - var USER_SAFE = {name: 'test-user'}; + const USER = {name: 'test-user', pass: 'password'}; + const USER_SAFE = {name: 'test-user'}; it('should login ok', function(done) { - config.AUTO_LOGIN = true; + config.autologin.enable = true; // before login - cache.del('.user'); + cache.del(h.KEYS.user); assert.equal(session.getUser(), null); assert.equal(session.isLogin(), false); - NEXT.login = function(user, cb) { - return cb(null, user); - }; + next.login = (user, cb) => cb(null, user); plugin.login(USER, function(e, user) { assert.equal(e, null); @@ -199,12 +188,10 @@ describe('plugin:cache', function() { }); it('should login ok w/ auto login', function(done) { - config.AUTO_LOGIN = false; - cache.del('.user'); + config.autologin.enable = false; + cache.del(h.KEYS.user); - NEXT.login = function(user, cb) { - return cb(null, user); - }; + next.login = (user, cb) => cb(null, user); plugin.login(USER, function(e, user) { assert.equal(e, null); @@ -216,9 +203,7 @@ describe('plugin:cache', function() { }); it('should login fail if client login error', function(done) { - NEXT.login = function(user, cb) { - return cb('client login error'); - }; + next.login = (user, cb) => cb('client login error'); plugin.login(USER, function(e, user) { assert.equal(e, 'client login error'); @@ -228,7 +213,7 @@ describe('plugin:cache', function() { it('should logout ok', function(done) { // before logout - cache.set('.user', USER); + cache.set(h.KEYS.user, USER); assert.deepEqual(session.getUser(), USER); assert.equal(session.isLogin(), true); @@ -238,5 +223,18 @@ describe('plugin:cache', function() { 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 index ce29ccab..ef099b9b 100644 --- a/test/plugins/test_leetcode.js +++ b/test/plugins/test_leetcode.js @@ -1,17 +1,19 @@ -var _ = require('underscore'); -var assert = require('chai').assert; -var nock = require('nock'); -var rewire = require('rewire'); +'use strict'; +const _ = require('underscore'); +const assert = require('chai').assert; +const nock = require('nock'); +const rewire = require('rewire'); -var config = require('../../lib/config'); -var log = require('../../lib/log'); +const config = require('../../lib/config'); +const chalk = require('../../lib/chalk'); +const log = require('../../lib/log'); -var plugin = rewire('../../lib/plugins/leetcode'); -var session = rewire('../../lib/session'); +const plugin = rewire('../../lib/plugins/leetcode'); +const session = rewire('../../lib/session'); describe('plugin:leetcode', function() { - var USER = {hash: 'abcdef'}; - var PROBLEM = { + const USER = {hash: 'abcdef'}; + const PROBLEM = { id: 389, name: 'Find the Difference', slug: 'find-the-difference', @@ -19,7 +21,7 @@ describe('plugin:leetcode', function() { locked: false, file: '/dev/null' }; - var SUBMISSION = { + const SUBMISSION = { id: '73790064', lang: 'cpp', runtime: '9 ms', @@ -30,12 +32,11 @@ describe('plugin:leetcode', function() { before(function() { log.init(); config.init(); + chalk.init(); plugin.init(); - session.getUser = function() { - return USER; - }; - session.saveUser = function() {}; + session.getUser = () => USER; + session.saveUser = () => {}; plugin.__set__('session', session); }); @@ -43,8 +44,7 @@ describe('plugin:leetcode', function() { it('should ok', function(done) { nock('https://leetcode.com') .get('/accounts/login/') - .reply(200, '', { - 'Set-Cookie': [ + .reply(200, '', { 'Set-Cookie': [ 'csrftoken=LOGIN_CSRF_TOKEN; Max-Age=31449600; Path=/; secure' ]}); @@ -53,13 +53,17 @@ describe('plugin:leetcode', function() { .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" + 'LEETCODE_SESSION=SESSION_ID; Max-Age=31449600; Path=/; secure' ]}); nock('https://leetcode.com') .get('/list/api/questions') - .reply(200, JSON.stringify({favorites: {private_favorites: [{id_hash: 'abcdef', name: 'Favorite'}]}})); + .reply(200, JSON.stringify({ + user_name: 'Eric', + favorites: { + private_favorites: [{id_hash: 'abcdef', name: 'Favorite'}] + } + })); plugin.login({}, function(e, user) { assert.equal(e, null); @@ -158,7 +162,7 @@ describe('plugin:leetcode', function() { }); it('should fail if not login', function(done) { - config.AUTO_LOGIN = false; + config.autologin.enable = false; nock('https://leetcode.com') .get('/api/problems/algorithms/') .replyWithFile(200, './test/mock/problems.nologin.json.20161015'); @@ -171,15 +175,19 @@ describe('plugin:leetcode', function() { }); // #getCategoryProblems describe('#getProblem', function() { + beforeEach(function() { + PROBLEM.locked = false; + }); + it('should ok', function(done) { nock('https://leetcode.com') - .get('/problems/find-the-difference') - .replyWithFile(200, './test/mock/find-the-difference.html.20170714'); + .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, '73.2K'); - assert.equal(problem.totalSubmit, '142K'); + assert.equal(problem.totalAC, '89.7K'); + assert.equal(problem.totalSubmit, '175.7K'); assert.equal(problem.desc, [ '', @@ -203,7 +211,7 @@ describe('plugin:leetcode', function() { '' ].join('\r\n')); - assert.equal(problem.templates.length, 11); + assert.equal(problem.templates.length, 12); assert.equal(problem.templates[0].value, 'cpp'); assert.equal(problem.templates[0].text, 'C++'); @@ -221,7 +229,7 @@ describe('plugin:leetcode', function() { assert.equal(problem.templates[1].text, 'Java'); assert.equal(problem.templates[1].defaultCode, [ - 'public class Solution {', + 'class Solution {', ' public char findTheDifference(String s, String t) {', ' ', ' }', @@ -333,15 +341,23 @@ describe('plugin:leetcode', function() { '}' ].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; - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .replyWithFile(200, './test/mock/locked.html.20161015'); plugin.getProblem(PROBLEM, function(e, problem) { assert.equal(e, 'failed to load locked problem!'); @@ -350,9 +366,7 @@ describe('plugin:leetcode', function() { }); it('should fail if session expired', function(done) { - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .reply(403); + nock('https://leetcode.com').post('/graphql').reply(403); plugin.getProblem(PROBLEM, function(e, problem) { assert.equal(e, session.errors.EXPIRED); @@ -361,9 +375,7 @@ describe('plugin:leetcode', function() { }); it('should fail if http error', function(done) { - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .reply(500); + nock('https://leetcode.com').post('/graphql').reply(500); plugin.getProblem(PROBLEM, function(e, problem) { assert.deepEqual(e, {msg: 'http error', statusCode: 500}); @@ -372,9 +384,7 @@ describe('plugin:leetcode', function() { }); it('should fail if unknown error', function(done) { - nock('https://leetcode.com') - .get('/problems/find-the-difference') - .replyWithError('unknown error!'); + nock('https://leetcode.com').post('/graphql').replyWithError('unknown error!'); plugin.getProblem(PROBLEM, function(e, problem) { assert.equal(e.message, 'unknown error!'); @@ -438,8 +448,6 @@ describe('plugin:leetcode', function() { }); 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"}'); @@ -460,7 +468,7 @@ describe('plugin:leetcode', function() { assert.equal(results[0].ok, true); done(); }); - }); + }).timeout(5000); it('should fail if server error', function(done) { nock('https://leetcode.com') @@ -528,7 +536,7 @@ describe('plugin:leetcode', function() { describe('#getSubmissions', function() { it('should ok', function(done) { - var problem = { + const problem = { id: 1, name: 'Two Sum', slug: 'two-sum', @@ -636,7 +644,7 @@ describe('plugin:leetcode', function() { plugin.getFavorites(function(e, favorites) { assert.equal(e, null); - var my = favorites.favorites.private_favorites; + const my = favorites.favorites.private_favorites; assert.equal(my.length, 1); assert.equal(my[0].name, 'Favorite'); assert.equal(my[0].id_hash, 'abcdefg'); @@ -644,4 +652,68 @@ describe('plugin:leetcode', function() { }); }); }); // #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 index 4a249ada..dbdb060c 100644 --- a/test/plugins/test_retry.js +++ b/test/plugins/test_retry.js @@ -1,25 +1,24 @@ -var assert = require('chai').assert; -var rewire = require('rewire'); +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); -var log = require('../../lib/log'); +const log = require('../../lib/log'); -var config = rewire('../../lib/config'); -var session = rewire('../../lib/session'); -var plugin = rewire('../../lib/plugins/retry'); +const config = rewire('../../lib/config'); +const session = rewire('../../lib/session'); +const plugin = rewire('../../lib/plugins/retry'); describe('plugin:retry', function() { - var USER = {}; - var NEXT = {}; - var PROBLEMS = [{id: 0, name: 'name0'}]; + const USER = {}; + const NEXT = {}; + const PROBLEMS = [{id: 0, name: 'name0'}]; before(function() { log.init(); config.init(); plugin.init(); - session.getUser = function() { - return USER; - }; + session.getUser = () => USER; plugin.__set__('config', config); plugin.__set__('session', session); @@ -27,10 +26,8 @@ describe('plugin:retry', function() { }); it('should fail if auto login disabled', function(done) { - config.AUTO_LOGIN = false; - NEXT.getProblems = function(cb) { - return cb(session.errors.EXPIRED); - }; + config.autologin.enable = false; + NEXT.getProblems = cb => cb(session.errors.EXPIRED); plugin.getProblems(function(e, problems) { assert.equal(e, session.errors.EXPIRED); @@ -38,53 +35,60 @@ describe('plugin:retry', function() { }); }); - it('should retry if session expired', function(done) { - config.AUTO_LOGIN = true; + it('should retry ok if finally ok', function(done) { + config.autologin.enable = true; + config.autologin.retry = 3; - var n = 0; + let n = 0; NEXT.getProblems = function(cb) { - ++n; - if (n === 1) return cb(session.errors.EXPIRED); - return cb(null, PROBLEMS); - }; - - NEXT.login = function(user, cb) { - return cb(null, user); + return ++n <= 3 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS); }; + NEXT.login = (user, cb) => cb(null, user); plugin.getProblems(function(e, problems) { - assert.equal(e, null); + assert.notExists(e); assert.equal(problems, PROBLEMS); done(); }); }); - it('should fail if user expired locally', function(done) { - config.AUTO_LOGIN = true; + it('should retry fail if always failed', function(done) { + config.autologin.enable = true; + config.autologin.retry = 2; - var n = 0; + let n = 0; NEXT.getProblems = function(cb) { - ++n; - if (n === 1) return cb(session.errors.EXPIRED); - return cb(null, PROBLEMS); + return ++n <= 3 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS); }; + NEXT.login = (user, cb) => { + return n == 1 ? cb(null, user) : cb('login failed'); + } - session.getUser = function() { - return null; + 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.equal(e, null); + assert.notExists(e); assert.equal(problems, PROBLEMS); done(); }); }); it('should fail if other errors', function(done) { - config.AUTO_LOGIN = true; - NEXT.getProblems = function(cb) { - return cb('unknown error'); - }; + config.autologin.enable = true; + NEXT.getProblems = cb => cb('unknown error'); plugin.getProblems(function(e, problems) { assert.equal(e, 'unknown error'); diff --git a/test/test_cache.js b/test/test_cache.js index 4e3405f9..caba14c1 100644 --- a/test/test_cache.js +++ b/test/test_cache.js @@ -1,60 +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; - before(function() { - var cachedir = './tmp'; - execSync('rm -rf ' + cachedir); + const K = '.test'; + const V = {test: 'data'}; - h.getCacheDir = function() { - return cachedir; - }; - cache.__set__('h', h); - }); + beforeEach(function() { + th.clean(); - it('should ok when not cached', function() { - cache.del(k); + const file = rewire('../lib/file'); + file.cacheDir = () => th.DIR; - assert.equal(cache.get(k), null); - assert.equal(cache.del(k), false); + cache = rewire('../lib/cache'); + cache.__set__('file', file); + cache.init(); }); - it('should ok when cached', function() { - assert.equal(cache.set(k, v), true); + it('should get ok when not cached', function() { + cache.del(K); + assert.equal(cache.get(K), null); + assert.equal(cache.del(K), false); + }); - assert.deepEqual(cache.get(k), v); - assert.equal(cache.del(k), true); + 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 list ok when no cached', function() { - var items = cache.list(); + const items = cache.list(); assert.equal(items.length, 0); }); it('should list ok when cached', function() { - assert.equal(cache.set(k, v), true); - - var items = cache.list(); + 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); - }); - - it('should list ok when cache dir not exist', function() { - h.getCacheDir = function() { - return '/not-exist-dir'; - }; - - var items = cache.list(); - assert.equal(items.length, 0); + 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 3b345ceb..9ae828dd 100644 --- a/test/test_config.js +++ b/test/test_config.js @@ -1,51 +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); + }); + + function createConfigFile(data) { + const fs = require('fs'); + fs.writeFileSync(FILE, JSON.stringify(data)); + } - var config = rewire('../lib/config'); - config.__set__('h', h); + it('should ok w/o local config', function() { + const DEFAULT_CONFIG = config.__get__('DEFAULT_CONFIG'); config.init(); - var expect = config.getDefaultConfig(); - var actual = _.extendOwn({}, config); // remove 'init' function + let actual = config.getAll(); + let expect = DEFAULT_CONFIG; assert.deepEqual(actual, expect); - expect = config.getUserConfig(); - actual = config.__get__('DEFAULT_USER_CONFIG'); + actual = config.getAll(true); + expect = _.omit(expect, 'sys'); assert.deepEqual(actual, expect); }); it('should ok w/ local config', function() { - var localConfig = { - AUTO_LOGIN: false, - LANG: 'ruby', - USE_COLOR: false - }; - - var h = rewire('../lib/helper'); - h.getFileData = function() { - return JSON.stringify(localConfig); - }; - - var config = rewire('../lib/config'); - config.__set__('h', h); + createConfigFile({ + autologin: {enable: false}, + code: {lang: 'ruby'}, + color: {enable: false} + }); config.init(); - var expect = config.getDefaultConfig(); - var actual = _.extendOwn({}, config); // remove 'init' function - _.extendOwn(expect, localConfig); - assert.deepEqual(actual, expect); + assert.equal(config.autologin.enable, false); + assert.equal(config.code.lang, 'ruby'); + assert.equal(config.color.enable, false); + assert.equal(config.code.editor, 'vim'); + }); - expect = config.getUserConfig(); - actual = config.__get__('DEFAULT_USER_CONFIG'); - _.extendOwn(actual, localConfig); - assert.deepEqual(actual, expect); + it('should remove legacy keys', function() { + createConfigFile({ + USE_COLOR: true, + code: {lang: 'ruby'} + }); + config.init(); + + 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 fdf36da7..0a436bb4 100644 --- a/test/test_core.js +++ b/test/test_core.js @@ -1,69 +1,140 @@ -var fs = require('fs'); - -var _ = require('underscore'); -var assert = require('chai').assert; -var rewire = require('rewire'); - -var log = require('../lib/log'); - -var session = rewire('../lib/session'); -var plugin = rewire('../lib/core'); +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); describe('core', function() { - var PROBLEMS = [ - {id: 0, name: 'name0', slug: 'slug0', starred: false, category: 'algorithms'}, - {id: 1, name: 'name1', slug: 'slug1', starred: true, category: 'algorithms'} + 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' + } ]; - var USER = {}; - var NEXT = {}; before(function() { + const log = require('../lib/log'); log.init(); - - session.getUser = function() { - return USER; - }; - - plugin.__set__('session', session); - plugin.setNext(NEXT); }); beforeEach(function() { - NEXT.getProblems = function(cb) { - return cb(null, PROBLEMS); - }; - NEXT.getProblem = function(problem, cb) { - return cb(null, problem); - }; + next = {}; + next.getProblems = cb => cb(null, PROBLEMS); + next.getProblem = (p, cb) => cb(null, p); + + core = rewire('../lib/core'); + core.setNext(next); }); + 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(); + }); + } + }); + + 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(); + }); + } + }); + + 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 + describe('#starProblem', function() { - it('should starProblem ok', function(done) { - NEXT.starProblem = function(problem, starred, cb) { - return cb(null, starred); - }; + it('should ok', function(done) { + next.starProblem = (p, starred, cb) => cb(null, starred); assert.equal(PROBLEMS[0].starred, false); - plugin.starProblem(PROBLEMS[0], true, function(e, starred) { - assert.equal(e, null); + core.starProblem(PROBLEMS[0], true, function(e, starred) { + assert.notExists(e); assert.equal(starred, true); done(); }); }); - it('should starProblem ok if already starred', function(done) { + it('should ok if already starred', function(done) { assert.equal(PROBLEMS[1].starred, true); - plugin.starProblem(PROBLEMS[1], true, function(e, starred) { - assert.equal(e, null); + core.starProblem(PROBLEMS[1], true, function(e, starred) { + assert.notExists(e); assert.equal(starred, true); done(); }); }); - it('should starProblem ok if already unstarred', function(done) { + it('should ok if already unstarred', function(done) { assert.equal(PROBLEMS[0].starred, false); - plugin.starProblem(PROBLEMS[0], false, function(e, starred) { - assert.equal(e, null); + core.starProblem(PROBLEMS[0], false, function(e, starred) { + assert.notExists(e); assert.equal(starred, false); done(); }); @@ -71,35 +142,81 @@ describe('core', function() { }); // #starProblem describe('#exportProblem', function() { - function injectVerify(expected, done) { - plugin.__set__('fs', { - writeFileSync: function(f, data) { - assert.equal(data, expected); - done(); - }, - readFileSync: fs.readFileSync - }); - } + let file; + + beforeEach(function() { + file = rewire('../lib/file'); + file.init(); + core.__set__('file', file); + }); - it('should ok w/ code only', function(done) { - var expected = [ + 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'); - injectVerify(expected, done); + 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 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'); - var problem = require('./mock/add-two-numbers.20161015.json'); - plugin.exportProblem(problem, 'test.cpp', true); + 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 ok w/ detailed comments', function(done) { - var expected = [ + 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', @@ -117,23 +234,38 @@ describe('core', function() { ' * 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'); - injectVerify(expected, done); - - var problem = require('./mock/add-two-numbers.20161015.json'); - plugin.exportProblem(problem, 'test.cpp', false); + 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); }); - it('should ok w/ detailed comments, 2nd', function(done) { - var expected = [ + it('should detailed ok with ruby', function() { + file.isWindows = () => false; + + const expected = [ + '#', + '# @lc app=leetcode id=2 lang=ruby', '#', '# [2] Add Two Numbers', '#', @@ -170,74 +302,62 @@ describe('core', function() { '' ].join('\n'); - injectVerify(expected, done); - - var problem = require('./mock/add-two-numbers.20161015.json'); + const problem = require('./mock/add-two-numbers.20161015.json'); problem.testcase = null; - problem.code = _.find(problem.templates, function(template) { - return template.value === 'ruby'; - }).defaultCode; - plugin.exportProblem(problem, 'test.rb', false); + const opts = { + lang: 'ruby', + code: problem.templates[6].defaultCode, + tpl: 'detailed' + }; + assert.equal(core.exportProblem(problem, opts), expected); }); }); // #exportProblem describe('#getProblem', function() { - it('should getProblem by id ok', function(done) { - plugin.getProblem(0, function(e, problem) { - assert.equal(e, null); + it('should get by id ok', function(done) { + core.getProblem(0, function(e, problem) { + assert.notExists(e); assert.deepEqual(problem, PROBLEMS[0]); done(); }); }); - it('should getProblem by key ok', function(done) { - plugin.getProblem('slug0', function(e, problem) { - assert.equal(e, null); + it('should get by key ok', function(done) { + core.getProblem('slug0', function(e, problem) { + assert.notExists(e); assert.deepEqual(problem, PROBLEMS[0]); done(); }); }); - it('should getProblem error if not found', function(done) { - plugin.getProblem(3, function(e, problem) { + it('should fail if not found', function(done) { + core.getProblem(3, function(e, problem) { assert.equal(e, 'Problem not found!'); done(); }); }); - it('should getProblem fail if client error', function(done) { - NEXT.getProblem = function(problem, cb) { - return cb('client getProblem error'); - }; + it('should fail if client error', function(done) { + next.getProblem = (problem, cb) => cb('client getProblem error'); - plugin.getProblem(0, function(e, problem) { + core.getProblem(0, function(e, problem) { assert.equal(e, 'client getProblem error'); done(); }); }); - it('should getProblem random ok', function(done) { - NEXT.getProblems = function(cb) { - return cb(null, [ - {id: 0, state: 'ac', locked: false}, - {id: 1, state: 'none', locked: true}, - {id: 2, state: 'none', locked: false} - ]); - }; - - plugin.getProblem(undefined, function(e, problem) { - assert.equal(e, null); - assert.equal(problem.id, 2); + 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(); }); }); - it('should getProblem fail if getProblems error', function(done) { - NEXT.getProblems = function(cb) { - return cb('getProblems error'); - }; + it('should fail if getProblems error', function(done) { + next.getProblems = cb => cb('getProblems error'); - plugin.getProblem(0, function(e, problem) { + core.getProblem(0, function(e, problem) { assert.equal(e, 'getProblems error'); done(); }); 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 7ca3bef2..143bda9e 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -1,11 +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'); - -chalk.init(); +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; @@ -46,6 +56,20 @@ 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'); @@ -104,12 +128,13 @@ describe('helper', function() { 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'), '.py3'); + 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 @@ -122,20 +147,22 @@ describe('helper', function() { 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('c:/Users/skygragon/file.py3'), 'python3'); 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('~/leetcode/../file.sql'), 'mysql'); assert.equal(h.extToLang('/home/skygragon/file.dat'), 'unknown'); }); }); // #extToLang describe('#langToCommentStyle', function() { it('should ok', function() { - var C_STYLE = {start: '/*', line: ' *', end: ' */'}; - var RUBY_STYLE = {start: '#', line: '#', end: '#'}; + 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); @@ -144,48 +171,26 @@ describe('helper', function() { 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'), RUBY_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'); - }); - - it('should getDirData ok', function() { - var files = h.getDirData(['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'); - }); - }); // #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: {} }; @@ -198,14 +203,14 @@ describe('helper', function() { describe('#printSafeHTTP', function() { it('should hide sensitive info', function() { - var raw = [ + const raw = [ "Cookie: 'xxxxxx'", "'X-CSRFToken': 'yyyyyy'", "'set-cookie': ['zzzzzz']" ].join('\r\n'); - var hide = [ - "Cookie: ", + const hide = [ + 'Cookie: ', "'X-CSRFToken': ", "'set-cookie': " ].join('\r\n'); @@ -216,8 +221,8 @@ describe('helper', function() { 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); @@ -242,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 index d9d1175e..5da832ab 100644 --- a/test/test_icon.js +++ b/test/test_icon.js @@ -1,37 +1,28 @@ -var assert = require('chai').assert; -var rewire = require('rewire'); - -var h = rewire('../lib/helper'); +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); describe('icon', function() { - var icon = null; + let icon; + let file; - before(function() { - h.getDirData = function() { + beforeEach(function() { + file = rewire('../lib/file'); + file.listCodeDir = function() { return [ - { - name: 'word', - data: { - yes: 'yes', - no: 'no', - lock: 'lock', - like: 'like', - unlike: 'unlike' - } - } + {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'}} ]; }; - }); - beforeEach(function() { icon = rewire('../lib/icon'); - icon.__set__('h', h); + icon.__set__('file', file); icon.init(); }); describe('#setTheme', function() { it('should ok with known theme', function() { - icon.setTheme('word'); + icon.setTheme('mac'); assert.equal(icon.yes, 'yes'); assert.equal(icon.no, 'no'); assert.equal(icon.lock, 'lock'); @@ -39,7 +30,9 @@ describe('icon', function() { assert.equal(icon.unlike, 'unlike'); }); - it('should ok with unknown theme', function() { + it('should ok with unknown theme on linux', function() { + file.isWindows = () => false; + icon.setTheme('non-exist'); assert.equal(icon.yes, '✔'); assert.equal(icon.no, '✘'); @@ -47,5 +40,16 @@ describe('icon', function() { 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_log.js b/test/test_log.js index 0684cb0e..92e5ef4b 100644 --- a/test/test_log.js +++ b/test/test_log.js @@ -1,48 +1,50 @@ -var assert = require('chai').assert; +'use strict'; +const assert = require('chai').assert; +const rewire = require('rewire'); -var chalk = require('../lib/chalk'); -var log = require('../lib/log'); +const chalk = require('../lib/chalk'); describe('log', function() { - var _output = null; - var result = ''; + let log; + let savedOutput; + let expected; before(function() { chalk.init(); - _output = log.output; - log.output = function(s) { - result = s; - }; - }); - - after(function() { - log.output = _output; }); beforeEach(function() { + log = rewire('../lib/log'); + savedOutput = log.output; + log.output = x => expected = x; + log.init(); - result = ''; + 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.TRACE); + assert.deepEqual(log.level, log.levels.get('TRACE')); log.setLevel('DEBUG'); - assert.deepEqual(log.level, log.levels.DEBUG); + assert.deepEqual(log.level, log.levels.get('DEBUG')); log.setLevel('INFO'); - assert.deepEqual(log.level, log.levels.INFO); + assert.deepEqual(log.level, log.levels.get('INFO')); log.setLevel('WARN'); - assert.deepEqual(log.level, log.levels.WARN); + assert.deepEqual(log.level, log.levels.get('WARN')); log.setLevel('ERROR'); - assert.deepEqual(log.level, log.levels.ERROR); + assert.deepEqual(log.level, log.levels.get('ERROR')); }); it('should ok with unknown level', function() { log.setLevel(''); - assert.deepEqual(log.level, log.levels.INFO); + assert.deepEqual(log.level, log.levels.get('INFO')); }); - }); + }); // #setLevel describe('#isEnabled', function() { it('should ok', function() { @@ -53,48 +55,55 @@ describe('log', function() { 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(result, ''); + assert.equal(expected, ''); log.setLevel('TRACE'); log.trace('some error'); - assert.equal(result, chalk.gray('[TRACE] some error')); + assert.equal(expected, chalk.gray('[TRACE] some error')); }); it('should ok with log.debug', function() { log.debug('some error'); - assert.equal(result, ''); + assert.equal(expected, ''); log.setLevel('DEBUG'); log.debug('some error'); - assert.equal(result, chalk.gray('[DEBUG] some error')); + assert.equal(expected, chalk.gray('[DEBUG] some error')); }); it('should ok with log.info', function() { log.info('some error'); - assert.equal(result, 'some error'); + assert.equal(expected, 'some error'); }); it('should ok with log.warn', function() { log.warn('some error'); - assert.equal(result, chalk.yellow('[WARN] some error')); + assert.equal(expected, chalk.yellow('[WARN] some error')); }); it('should ok with log.error', function() { log.error('some error'); - assert.equal(result, chalk.red('[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(result, chalk.red('[ERROR] some error [500]')); + assert.equal(expected, chalk.red('[ERROR] some error [code=500]')); log.fail('some error'); - assert.equal(result, chalk.red('[ERROR] some error [0]')); + 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 index a98ece26..aa3a40be 100644 --- a/test/test_plugin.js +++ b/test/test_plugin.js @@ -1,49 +1,223 @@ -var assert = require('chai').assert; -var rewire = require('rewire'); +'use strict'; +const fs = require('fs'); +const path = require('path'); -var log = require('../lib/log'); -var Plugin = rewire('../lib/plugin'); -var h = rewire('../lib/helper'); +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() { - var leetcode = new Plugin(0, 'Leetcode', '2.0', ''); - var cache = new Plugin(1, 'Cache', '1.0', ''); - var retry = new Plugin(2, 'Retry', '3.0', ''); - var core = new Plugin(3, 'Core', '4.0', ''); + let file; + let cache; + + const NOOP = () => {}; before(function() { log.init(); + chalk.init(); + config.init(); - var noop = function() {}; - cache.init = noop; - leetcode.init = noop; - retry.init = noop; - core.init = noop; - - h.getDirData = function() { - return [ - {name: 'cache', data: cache}, - {name: 'leetcode', data: leetcode}, - {name: 'retry', data: retry}, - {name: 'bad', data: null} - ]; - }; - Plugin.__set__('h', h); + file = rewire('../lib/file'); + cache = rewire('../lib/cache'); + Plugin.__set__('file', file); + Plugin.__set__('cache', cache); + }); + + beforeEach(function() { + th.clean(); + cache.get = NOOP; }); - it('should init ok', function() { - assert.deepEqual(Plugin.plugins, []); - Plugin.init(core); - assert.deepEqual(Plugin.plugins.length, 3); + 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'); - var names = Plugin.plugins.map(function(p) { - return p.name; + 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} + ]; + }; }); - assert.deepEqual(names, ['Retry', 'Cache', 'Leetcode']); - assert.equal(core.next, retry); - assert.equal(retry.next, cache); - assert.equal(cache.next, leetcode); - assert.equal(leetcode.next, 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') + ' '); + }); +});