diff --git a/.dockerignore b/.dockerignore index ed775dc7..c34cc8cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +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 8cce4ef7..5ef04108 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { "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 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/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/docs/advanced.md b/docs/advanced.md index a7cc593b..42904e91 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -9,6 +9,7 @@ title: Advanced Topic * [Cache](#cache) * [Configuration](#configuration) * [Color Themes](#color-themes) +* [File Name](#file-name) * [Log Levels](#log-levels) * [Plugins](#plugins) @@ -91,6 +92,10 @@ The config file is saved in `~/.lc/config.json`, here is a full exmaple (include "enable": true, "theme": "default" }, + "file": { + "show": "${fid}.${slug}", + "submission": "${fid}.${slug}.${sid}.${ac}" + }, "icon": { "theme": "" }, @@ -107,6 +112,7 @@ Here are some useful settings: * `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)) @@ -167,6 +173,23 @@ Of course you can create your own themes if you like, look into `colors` folder "yellow": "#ffff00" } +# 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. diff --git a/docs/commands.md b/docs/commands.md index 6ff28658..1893dbc4 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -234,7 +234,9 @@ Display question details. With `-g`/`-l`/`-x`, the code template would be auto g * golang * java * javascript + * kotlin * mysql + * php * python * python3 * ruby @@ -465,7 +467,7 @@ Display version information. Short: $ leetcode version - 2.5.2 + 2.6.2 Verbose: @@ -475,7 +477,7 @@ Verbose: | | ___ ___| |_ ___ ___ __| | ___ | |/ _ \/ _ \ __|/ __|/ _ \ / _` |/ _ \ | | __/ __/ |_ (__| (_) | (_| | __/ - |_|\___|\___|\__|\___|\___/ \__,_|\___| CLI v2.5.2 + |_|\___|\___|\__|\___|\___/ \__,_|\___| CLI v2.6.2 [Environment] Node v8.1.4 diff --git a/docs/install.md b/docs/install.md index 5b8e0706..c2e20595 100644 --- a/docs/install.md +++ b/docs/install.md @@ -3,6 +3,12 @@ layout: default title: Installation --- +# All in One (beta) + +No need to install node.js. Now available on 64bits linux, mac, and windows. + +[Download](https://github.com/skygragon/leetcode-cli/releases) + # Prerequisites Install the latest LTS version of `node.js` (`npm` included): @@ -49,6 +55,11 @@ Similar with above, while you can introduce your own changes as you wish. $ 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 + ### 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. diff --git a/docs/releases.md b/docs/releases.md index e2bd6a12..77cce5eb 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,36 @@ 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` diff --git a/icons/ascii.json b/icons/ascii.json index 6660733f..1a7664f1 100644 --- a/icons/ascii.json +++ b/icons/ascii.json @@ -4,6 +4,7 @@ "like": "*", "unlike": " ", "lock": "$", + "nolock": " ", "empty": " ", "ac": "O", "notac": "X", diff --git a/icons/default.json b/icons/default.json index 42bdbecd..a5263a7f 100644 --- a/icons/default.json +++ b/icons/default.json @@ -4,6 +4,7 @@ "like": "★", "unlike": "☆", "lock": "🔒", + "nolock": " ", "empty": " ", "ac": "▣", "notac": "▤", diff --git a/icons/win7.json b/icons/win7.json index 7f032a83..0e79a481 100644 --- a/icons/win7.json +++ b/icons/win7.json @@ -4,6 +4,7 @@ "like": "♥", "unlike": " ", "lock": "$", + "nolock": " ", "empty": " ", "ac": "O", "notac": "X", diff --git a/lib/cache.js b/lib/cache.js index a6097c2a..42efad0b 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,42 +1,41 @@ 'use strict'; -var fs = require('fs'); var path = require('path'); -var h = require('./helper'); +var file = require('./file'); const cache = {}; cache.init = function() { - h.mkdir(h.getCacheDir()); + file.mkdir(file.cacheDir()); }; cache.get = function(k) { - const fullpath = h.getCacheFile(k); - if (!fs.existsSync(fullpath)) return null; + const fullpath = file.cacheFile(k); + if (!file.exist(fullpath)) return null; - return JSON.parse(fs.readFileSync(fullpath)); + return JSON.parse(file.data(fullpath)); }; cache.set = function(k, v) { - const 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) { - const fullpath = h.getCacheFile(k); - if (!fs.existsSync(fullpath)) return false; + const fullpath = file.cacheFile(k); + if (!file.exist(fullpath)) return false; - fs.unlinkSync(fullpath); + file.rm(fullpath); return true; }; cache.list = function() { - return fs.readdirSync(h.getCacheDir()) + return file.list(file.cacheDir()) .filter(x => path.extname(x) === '.json') .map(function(filename) { const k = path.basename(filename, '.json'); - const stat = fs.statSync(h.getCacheFile(k)); + const stat = file.stat(file.cacheFile(k)); return { name: k, size: stat.size, diff --git a/lib/chalk.js b/lib/chalk.js index 090ec573..ef78e9d8 100644 --- a/lib/chalk.js +++ b/lib/chalk.js @@ -3,6 +3,8 @@ var _ = require('underscore'); var style = require('ansi-styles'); var supportsColor = require('supports-color'); +var file = require('./file'); + const chalk = { enabled: supportsColor.stdout, use256: supportsColor.stdout && supportsColor.stdout.has256, @@ -54,8 +56,7 @@ chalk.wrap = function(pre, post) { const bgName = x => 'bg' + x[0].toUpperCase() + x.substr(1); chalk.init = function() { - const h = require('./helper'); - for (let f of h.getCodeDirData('colors')) { + for (let f of file.listCodeDir('colors')) { const theme = {}; const data = _.extendOwn({}, DEFAULT, f.data); for (let x of _.pairs(data)) { diff --git a/lib/cli.js b/lib/cli.js index 051bbee2..e59cf7f5 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -5,6 +5,7 @@ var chalk = require('./chalk'); var cache = require('./cache'); var config = require('./config'); var h = require('./helper'); +var file = require('./file'); var icon = require('./icon'); var log = require('./log'); var Plugin = require('./plugin'); @@ -52,6 +53,24 @@ 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() { @@ -73,22 +92,16 @@ cli.run = function() { }); config.init(); - cache.init(); initColor(); initIcon(); initLogLevel(); - - if (Plugin.init()) { - Plugin.save(); + initDir() + initPlugins(function(e) { + if (e) return log.fatal(e); + cache.init(); runCommand(); - } else { - Plugin.installMissings(function(e) { - if (e) return log.error(e); - Plugin.init(); - runCommand(); - }); - } + }); }; module.exports = cli; diff --git a/lib/commands/cache.js b/lib/commands/cache.js index 30bc24b4..154a3302 100644 --- a/lib/commands/cache.js +++ b/lib/commands/cache.js @@ -1,12 +1,12 @@ 'use strict'; var _ = require('underscore'); -var sprintf = require('sprintf-js').sprintf; var h = require('../helper'); var chalk = require('../chalk'); var log = require('../log'); var cache = require('../cache'); var session = require('../session'); +var sprintf = require('../sprintf'); const cmd = { command: 'cache [keyword]', @@ -56,8 +56,8 @@ cmd.handler = function(argv) { return x; }) .forEach(function(f) { - log.printf(' %s %8s %s ago', - chalk.green(sprintf('%-60s', f.name)), + log.printf(' %-60s %8s %s ago', + chalk.green(f.name), h.prettySize(f.size), h.prettyTime((Date.now() - f.mtime) / 1000)); }); diff --git a/lib/commands/config.js b/lib/commands/config.js index 8d684939..6a1fe364 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -2,7 +2,7 @@ var _ = require('underscore'); var nconf = require('nconf'); -var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); var config = require('../config'); var log = require('../log'); @@ -55,12 +55,12 @@ function loadConfig(showall) { } function saveConfig() { - require('fs').writeFileSync(h.getConfigFile(), prettyConfig(loadConfig(false))); + file.write(file.configFile(), prettyConfig(loadConfig(false))); } cmd.handler = function(argv) { session.argv = argv; - nconf.file('local', h.getConfigFile()); + nconf.file('local', file.configFile()); // show all if (argv.key.length === 0) @@ -75,14 +75,14 @@ cmd.handler = function(argv) { // delete if (argv.delete) { - if (v === undefined) return log.error('Key not found: ' + argv.key); + 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.error('Key not found: ' + argv.key); + if (v === undefined) return log.fatal('Key not found: ' + argv.key); return log.info(prettyConfig(v)); } diff --git a/lib/commands/list.js b/lib/commands/list.js index f3e69143..c010de86 100644 --- a/lib/commands/list.js +++ b/lib/commands/list.js @@ -1,6 +1,5 @@ 'use strict'; var _ = require('underscore'); -var sprintf = require('sprintf-js').sprintf; var h = require('../helper'); var chalk = require('../chalk'); @@ -67,14 +66,14 @@ cmd.handler = function(argv) { if (problem.locked) ++stat.locked; if (problem.starred) ++stat.starred; - log.printf('%s %s %s [%3d] %-60s %-6s (%.2f %%)', + log.printf('%s %s %s [%=4s] %-60s %-6s (%s %%)', (problem.starred ? chalk.yellow(icon.like) : icon.empty), - (problem.locked ? chalk.red(icon.lock) : icon.empty), + (problem.locked ? chalk.red(icon.lock) : icon.nolock), h.prettyState(problem.state), problem.fid, problem.name, - h.prettyLevel(sprintf('%-6s', problem.level)), - problem.percent); + h.prettyLevel(problem.level), + problem.percent.toFixed(2)); if (argv.extra) { let badges = [problem.category]; @@ -99,9 +98,9 @@ cmd.handler = function(argv) { if (argv.stat) { log.info(); - log.printf(' Listed: %-9d Locked: %-9d Starred: %-9d', problems.length, stat.locked, stat.starred); - log.printf(' Accept: %-9d Not-AC: %-9d Remain: %-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 6a5b9721..ca5a9cff 100644 --- a/lib/commands/plugin.js +++ b/lib/commands/plugin.js @@ -1,12 +1,11 @@ 'use strict'; -var sprintf = require('sprintf-js').sprintf; - var h = require('../helper'); var chalk = require('../chalk'); var config = require('../config'); var log = require('../log'); var Plugin = require('../plugin'); var session = require('../session'); +var sprintf = require('../sprintf'); const cmd = { command: 'plugin [name]', @@ -53,10 +52,10 @@ const cmd = { .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 GtiHub') - .example(chalk.yellow('leetcode plugin -i company'), 'Install company plugin from GtiHub') + .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 comapny plugin') + .example(chalk.yellow('leetcode plugin -e company'), 'Enable company plugin') .example(chalk.yellow('leetcode plugin -D company'), 'Delete company plugin'); } }; @@ -70,7 +69,6 @@ function print(plugins) { log.printf(' %s %-18s %-15s %s', h.prettyText('', p.enabled && !p.missing), p.name, p.ver, p.desc); - Plugin.save(); } cmd.handler = function(argv) { @@ -80,8 +78,10 @@ cmd.handler = function(argv) { const name = argv.name; if (argv.install) { - const cb = function(e) { - if (e) return log.error(e); + const cb = function(e, p) { + if (e) return log.fatal(e); + p.help(); + p.save(); Plugin.init(); print(); }; @@ -95,18 +95,18 @@ cmd.handler = function(argv) { } if (name) plugins = plugins.filter(x => x.name === name); - if (plugins.length === 0) return log.error('Plugin not found!'); + if (plugins.length === 0) return log.fatal('Plugin not found!'); const p = plugins[0]; if (p.missing && (argv.enable || argv.disable)) - return log.error('Plugin missing, install it first'); + return log.fatal('Plugin missing, install it first'); if (argv.enable) { - p.enable(true); + p.enabled = true; p.save(); print(); } else if (argv.disable) { - p.enable(false); + p.enabled = false; p.save(); print(); } else if (argv.delete) { diff --git a/lib/commands/session.js b/lib/commands/session.js index d348dea5..64d460d6 100644 --- a/lib/commands/session.js +++ b/lib/commands/session.js @@ -1,12 +1,12 @@ 'use strict'; var prompt = require('prompt'); -var sprintf = require('sprintf-js').sprintf; 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]', @@ -61,14 +61,14 @@ function printSessions(e, sessions) { if (s.total_submitted > 0) submissionRate = s.total_acs * 100 / s.total_submitted; - log.printf(' %s %8d %-26s %s (%6s %%) %s (%6s %%)', + log.printf(' %s %8s %-26s %6s (%6s %%) %6s (%6s %%)', s.is_active ? h.prettyState('ac') : ' ', s.id, s.name || 'Anonymous Session', - chalk.green(sprintf('%6s', s.ac_questions)), - sprintf('%.2f', questionRate), - chalk.green(sprintf('%6s', s.total_acs)), - sprintf('%.2f', submissionRate)); + chalk.green(s.ac_questions), + questionRate.toFixed(2), + chalk.green(s.total_acs), + submissionRate.toFixed(2)); } } diff --git a/lib/commands/show.js b/lib/commands/show.js index d930740f..7c66204a 100644 --- a/lib/commands/show.js +++ b/lib/commands/show.js @@ -1,11 +1,11 @@ 'use strict'; -var fs = require('fs'); 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'); @@ -75,20 +75,18 @@ const cmd = { function genFileName(problem, opts) { const path = require('path'); const params = [ - problem.fid, - problem.slug, + file.fmt(config.file.show, problem), '', h.langToExt(opts.lang) ]; - // try to use a new filename to avoid overwrite by mistake - let i = 0; - let name; - do { - name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.')); - params[2] = i++; - } while (fs.existsSync(name)); - return name; + // try new name to avoid overwrite by mistake + for (let i = 0; ; ++i) { + const name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.')); + if (!file.exist(name)) + return name; + params[1] = i; + } } function showProblem(problem, argv) { @@ -122,9 +120,9 @@ function showProblem(problem, argv) { let filename; if (argv.gen) { + file.mkdir(argv.outdir); filename = genFileName(problem, argv); - h.mkdir(argv.outdir); - fs.writeFileSync(filename, code); + file.write(filename, code); if (argv.editor !== undefined) { childProcess.spawn(argv.editor || config.code.editor, [filename], { @@ -139,7 +137,7 @@ function showProblem(problem, argv) { } } - log.printf('[%d] %s %s', problem.fid, problem.name, + 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)); @@ -152,7 +150,7 @@ function showProblem(problem, argv) { log.info(); log.printf('* %s', problem.category); - log.printf('* %s (%.2f%%)', h.prettyLevel(problem.level), problem.percent); + log.printf('* %s (%s%%)', h.prettyLevel(problem.level), problem.percent.toFixed(2)); if (filename) log.printf('* Source Code: %s', chalk.yellow.underline(filename)); diff --git a/lib/commands/star.js b/lib/commands/star.js index 991e10e4..3660432b 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -35,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.fid, 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 54f7a41a..772499c4 100644 --- a/lib/commands/stat.js +++ b/lib/commands/stat.js @@ -1,6 +1,5 @@ 'use strict'; var moment = require('moment'); -var sprintf = require('sprintf-js').sprintf; var _ = require('underscore'); var chalk = require('../chalk'); @@ -8,6 +7,7 @@ var icon = require('../icon'); var log = require('../log'); var core = require('../core'); var session = require('../session'); +var sprintf = require('../sprintf'); var h = require('../helper'); const cmd = { @@ -50,9 +50,9 @@ function printLine(key, done, all) { const n = 30; const percent = (all > 0) ? done / all : 0; const x = Math.ceil(n * percent); - log.printf(' %s\t%3d/%-3d (%6s %%) %s%s', + log.printf(' %s\t%3s/%-3s (%6s %%) %s%s', h.prettyLevel(key), done, all, - sprintf('%.2f', 100 * percent), + (100 * percent).toFixed(2), chalk.green('█'.repeat(x)), chalk.red('░'.repeat(n - x))); } @@ -96,7 +96,7 @@ function showGraph(problems) { if (groups > 5) groups = 5; const header = _.range(groups) - .map(x => sprintf('%4d%18d', x * 10 + 1, x * 10 + 10)) + .map(x => sprintf('%4s%18s', x * 10 + 1, x * 10 + 10)) .join(''); log.info(' ' + header); @@ -104,7 +104,8 @@ function showGraph(problems) { for (let problem of problems) graph[problem.fid] = ICONS[problem.state] || ICONS.none; - let line = [sprintf(' %03d', 0)]; + 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(' '); @@ -114,7 +115,7 @@ function showGraph(problems) { // time to start new row if (i % (10 * groups) === 0 || i === n) { log.info(line.join(' ')); - line = [sprintf(' %03d', i)]; + line = [sprintf(rowNumFormat, i)]; } } diff --git a/lib/commands/submission.js b/lib/commands/submission.js index ff570674..de0449a3 100644 --- a/lib/commands/submission.js +++ b/lib/commands/submission.js @@ -1,10 +1,12 @@ 'use strict'; -var fs = require('fs'); +var path = require('path'); -var sprintf = require('sprintf-js').sprintf; +var _ = require('underscore'); var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); +var config = require('../config'); var log = require('../log'); var Queue = require('../queue'); var core = require('../core'); @@ -60,7 +62,7 @@ function doTask(problem, queue, cb) { // - green: accepted, fresh download // - yellow: not ac-ed, fresh download // - white: existed already, skip download - log.printf('[%3d] %-60s %s', problem.fid, problem.name, + log.printf('[%=4s] %-60s %s', problem.fid, problem.name, (e ? chalk.red('ERROR: ' + (e.msg || e)) : msg)); if (cb) cb(e); } @@ -91,17 +93,15 @@ function exportSubmission(problem, argv, cb) { const submission = submissions.find(x => x.status_display === 'Accepted') || submissions[0]; submission.ac = (submission.status_display === 'Accepted'); - const f = sprintf('%s/%d.%s.%s.%s%s', - argv.outdir, - problem.fid, - problem.slug, - submission.id, - submission.ac ? 'ac' : 'notac', - h.langToExt(submission.lang)); + const data = _.extend({}, submission, problem); + data.sid = submission.id; + data.ac = submission.ac ? 'ac' : 'notac'; + const basename = file.fmt(config.file.submission, data); + const f = path.join(argv.outdir, basename + h.langToExt(submission.lang)); - h.mkdir(argv.outdir); + file.mkdir(argv.outdir); // skip the existing cached submissions - if (fs.existsSync(f)) + if (file.exist(f)) return cb(null, chalk.underline(f)); core.getSubmission(submission, function(e, submission) { @@ -112,7 +112,7 @@ function exportSubmission(problem, argv, cb) { code: submission.code, tpl: argv.extra ? 'detailed' : 'codeonly' }; - fs.writeFileSync(f, core.exportProblem(problem, opts)); + file.write(f, core.exportProblem(problem, opts)); cb(null, submission.ac ? chalk.green.underline(f) : chalk.yellow.underline(f)); }); diff --git a/lib/commands/submit.js b/lib/commands/submit.js index 297084f7..56f5ed04 100644 --- a/lib/commands/submit.js +++ b/lib/commands/submit.js @@ -1,8 +1,8 @@ 'use strict'; -var fs = require('fs'); var util = require('util'); var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); var log = require('../log'); var core = require('../core'); @@ -43,17 +43,16 @@ function printLine() { cmd.handler = function(argv) { session.argv = 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 - const 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); @@ -77,7 +76,7 @@ cmd.handler = function(argv) { let ratio = 0.0; for (let score of scores) { - if (parseFloat(score[0]) > myRuntime) + if (parseFloat(score[0]) >= myRuntime) ratio += parseFloat(score[1]); } diff --git a/lib/commands/test.js b/lib/commands/test.js index ff320548..21c4a4eb 100644 --- a/lib/commands/test.js +++ b/lib/commands/test.js @@ -1,8 +1,8 @@ 'use strict'; -var fs = require('fs'); var _ = require('underscore'); var h = require('../helper'); +var file = require('../file'); var chalk = require('../chalk'); var log = require('../log'); var core = require('../core'); @@ -53,14 +53,12 @@ function printResult(actual, expect, k) { } 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 - const 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) @@ -73,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); diff --git a/lib/commands/user.js b/lib/commands/user.js index af9ea4c2..4cd903cd 100644 --- a/lib/commands/user.js +++ b/lib/commands/user.js @@ -1,6 +1,5 @@ 'use strict'; var prompt = require('prompt'); -var sprintf = require('sprintf-js').sprintf; var h = require('../helper'); var config = require('../config'); @@ -8,6 +7,7 @@ var chalk = require('../chalk'); var log = require('../log'); var core = require('../core'); var session = require('../session'); +var sprintf = require('../sprintf'); const cmd = { command: 'user', @@ -65,9 +65,9 @@ cmd.handler = function(argv) { if (user) { log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host'))); log.info(chalk.gray('-'.repeat(60))); - log.printf(' %s %s %s', + log.printf(' %s %-20s %s', h.prettyText('', user.paid || false), - chalk.yellow(sprintf('%-20s', user.name)), + 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 684166a7..4ba16749 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('underscore'); +var file = require('../file'); var chalk = require('../chalk'); var icon = require('../icon'); var log = require('../log'); @@ -46,19 +47,18 @@ cmd.handler = function(argv) { '| | ___ ___| |_ ___ ___ __| | ___ ', '| |/ _ \\/ _ \\ __|/ __|/ _ \\ / _` |/ _ \\', '| | __/ __/ |_ (__| (_) | (_| | __/', - '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___| CLI v' + version + '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___| CLI ' + chalk.green('v' + version) ].join('\n'); log.info(logo); - const h = require('../helper'); 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.getAll(true), function(v, k) { diff --git a/lib/config.js b/lib/config.js index 1f53cce9..373b9f0f 100644 --- a/lib/config.js +++ b/lib/config.js @@ -2,7 +2,7 @@ var _ = require('underscore'); var nconf = require('nconf'); -var h = require('./helper'); +var file = require('./file'); const DEFAULT_CONFIG = { // usually you don't wanna change those @@ -22,9 +22,11 @@ const DEFAULT_CONFIG = { 'javascript', 'kotlin', 'mysql', + 'php', 'python', 'python3', 'ruby', + 'rust', 'scala', 'swift' ], @@ -55,6 +57,10 @@ const DEFAULT_CONFIG = { editor: 'vim', lang: 'cpp' }, + file: { + show: '${fid}.${slug}', + submission: '${fid}.${slug}.${sid}.${ac}' + }, color: { enable: true, theme: 'default' @@ -63,7 +69,8 @@ const DEFAULT_CONFIG = { theme: '' }, network: { - concurrency: 10 + concurrency: 10, + delay: 1 }, plugins: {} }; @@ -71,7 +78,7 @@ const DEFAULT_CONFIG = { function Config() {} Config.prototype.init = function() { - nconf.file('local', h.getConfigFile()) + nconf.file('local', file.configFile()) .add('global', {type: 'literal', store: DEFAULT_CONFIG}) .defaults({}); diff --git a/lib/core.js b/lib/core.js index a1356a62..74362f78 100644 --- a/lib/core.js +++ b/lib/core.js @@ -1,11 +1,11 @@ 'use strict'; -var path = require('path'); var util = require('util'); var _ = require('underscore'); var log = require('./log'); var h = require('./helper'); +var file = require('./file'); var Plugin = require('./plugin'); const core = new Plugin(99999999, 'core', '20170722', 'Plugins manager'); @@ -106,31 +106,26 @@ core.starProblem = function(problem, starred, cb) { }; core.exportProblem = function(problem, opts) { - // copy problem attrs thus we can render it in template - const input = _.extend({}, problem); + const data = _.extend({}, problem); - input.code = opts.code.replace(/\r\n/g, '\n'); - input.comment = h.langToCommentStyle(opts.lang); - input.percent = input.percent.toFixed(2); - input.testcase = util.inspect(input.testcase || ''); + // unify format before rendering + data.app = require('./config').app || 'leetcode'; + if (!data.fid) data.fid = data.id; + if (!data.lang) data.lang = opts.lang; + data.code = (opts.code || data.code || '').replace(/\r\n/g, '\n'); + data.comment = h.langToCommentStyle(data.lang); + data.percent = data.percent.toFixed(2); + data.testcase = util.inspect(data.testcase || ''); if (opts.tpl === 'detailed') { // NOTE: wordwrap internally uses '\n' as EOL, so here we have to // remove all '\r' in the raw string. - const desc = input.desc.replace(/\r\n/g, '\n').replace(/^ /mg, '⁠'); - const wrap = require('wordwrap')(79 - input.comment.line.length); - input.desc = wrap(desc).split('\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'); } - const tplfile = path.join(h.getCodeDir('templates'), opts.tpl + '.tpl'); - let output = _.template(h.getFileData(tplfile))(input); - - if (h.isWindows()) { - output = output.replace(/\n/g, '\r\n'); - } else { - output = output.replace(/\r\n/g, '\n'); - } - return 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 33b9e1f3..8806086e 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -1,11 +1,9 @@ 'use strict'; -var fs = require('fs'); -var path = require('path'); - var _ = require('underscore'); -var mkdirp = require('mkdirp'); var ora = require('ora'); +var file = require('./file'); + const UNITS_SIZE = [ {unit: 'B', name: 'Bytes', count: 1024}, {unit: 'K', name: 'KBytes', count: 1024}, @@ -40,10 +38,12 @@ const LANGS = [ {lang: 'java', ext: '.java', style: 'c'}, {lang: 'javascript', ext: '.js', style: 'c'}, {lang: 'kotlin', ext: '.kt', style: 'c'}, - {lang: 'mysql', ext: '.sql', style: '#'}, + {lang: 'mysql', ext: '.sql', style: '--'}, + {lang: 'php', ext: '.php', style: 'c'}, {lang: 'python', ext: '.py', style: '#'}, - {lang: 'python3', ext: '.python3.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'} ]; @@ -53,15 +53,11 @@ const h = {}; h.KEYS = { user: '../user', stat: '../stat', - plugins: '../plugins', + plugins: '../../plugins', problems: 'problems', problem: p => p.fid + '.' + p.slug + '.' + p.category }; -h.isWindows = function() { - return process.platform === 'win32'; -}; - h.prettyState = function(state) { switch (state) { case 'ac': return this.prettyText('', true); @@ -130,80 +126,16 @@ h.langToExt = function(lang) { }; h.extToLang = function(fullpath) { - // HACK: compatible with old ext - if (fullpath.endsWith('.py3')) return 'python3'; - - const res = _.chain(LANGS) - .filter(x => fullpath.endsWith(x.ext)) - .sortBy(x => -x.ext.length) - .value(); - return res.length ? res[0].lang : 'unknown'; + const res = LANGS.find(x => fullpath.endsWith(x.ext)); + return res ? res.lang : 'unknown'; }; h.langToCommentStyle = function(lang) { const res = LANGS.find(x => x.lang === lang); - return (res && res.style === '#') ? - {start: '#', line: '#', end: '#'} : - {start: '/*', line: ' *', end: ' */'}; -}; - -h.mkdir = function(fullpath) { - if (fs.existsSync(fullpath)) return; - mkdirp.sync(fullpath); -}; - -h.getCodeDirData = function(dir) { - dir = h.getCodeDir(dir); - return fs.readdirSync(dir).map(function(file) { - const fullpath = path.join(dir, file); - const ext = path.extname(file); - - const name = path.basename(file, ext); - let data = null; - - switch (ext) { - case '.js': data = require(fullpath); break; - case '.json': data = JSON.parse(h.getFileData(fullpath)); 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.getUserHomeDir = function() { - return process.env.HOME || process.env.USERPROFILE; -}; - -h.getHomeDir = function() { - return path.join(this.getUserHomeDir(), '.lc'); -}; - -h.getCacheDir = function() { - return path.join(this.getHomeDir(), 'cache'); -}; - -h.getCodeDir = function(dir) { - return path.join(__dirname, '..', dir || ''); -}; - -h.getCacheFile = function(k) { - return path.join(this.getCacheDir(), k + '.json'); -}; - -h.getConfigFile = function() { - return path.join(this.getHomeDir(), 'config.json'); -}; - -h.getPluginFile = function(name) { - return path.join(this.getCodeDir('lib/plugins'), path.basename(name)); + return (res && res.style === 'c') ? + {start: '/*', line: ' *', end: ' */'} : + {start: res.style, line: res.style, end: res.style}; }; h.readStdin = function(cb) { @@ -211,13 +143,13 @@ h.readStdin = function(cb) { const bufs = []; console.log('NOTE: to finish the input, press ' + - (this.isWindows() ? ' and ' : '')); + (file.isWindows() ? ' and ' : '')); stdin.on('readable', function() { 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); diff --git a/lib/icon.js b/lib/icon.js index af95137b..c147a792 100644 --- a/lib/icon.js +++ b/lib/icon.js @@ -1,7 +1,7 @@ 'use strict'; var _ = require('underscore'); -var h = require('./helper'); +var file = require('./file'); const icons = { yes: '✔', @@ -18,13 +18,13 @@ const icons = { }; icons.setTheme = function(name) { - const defaultName = h.isWindows() ? 'win7' : 'default'; + const defaultName = file.isWindows() ? 'win7' : 'default'; const theme = this.themes.get(name) || this.themes.get(defaultName) || {}; _.extendOwn(this, theme); }; icons.init = function() { - for (let f of h.getCodeDirData('icons')) + for (let f of file.listCodeDir('icons')) icons.themes.set(f.name, f.data); }; diff --git a/lib/log.js b/lib/log.js index cb8435de..394b356c 100644 --- a/lib/log.js +++ b/lib/log.js @@ -1,8 +1,8 @@ 'use strict'; var _ = require('underscore'); -var sprintf = require('sprintf-js').sprintf; var chalk = require('./chalk'); +var sprintf = require('./sprintf'); const log = { output: _.bind(console.log, console), @@ -25,7 +25,16 @@ log.isEnabled = function(name) { }; 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() { diff --git a/lib/plugin.js b/lib/plugin.js index 5f430ebf..bbd6da44 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -7,6 +7,7 @@ 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'); @@ -20,7 +21,8 @@ function Plugin(id, name, ver, desc, deps) { this.enabled = true; this.deleted = false; - this.missing = (ver === 'missing'); + this.missing = (this.ver === 'missing'); + this.builtin = (this.ver === 'default'); // only need deps for current platform this.deps = _.chain(deps || []) @@ -39,27 +41,11 @@ Plugin.prototype.setNext = function(next) { this.next = next; }; -Plugin.prototype.setFile = function(file) { - this.file = file; - this.enabled = (file[0] !== '.'); -}; - -Plugin.prototype.enable = function(enabled) { - if (this.enabled === enabled) return; - const newfile = enabled ? this.file.substr(1) : '.' + this.file; - try { - fs.renameSync(h.getPluginFile(this.file), h.getPluginFile(newfile)); - } catch(e) { - log.error(e.message); - } - this.setFile(newfile); -}; - Plugin.prototype.delete = function() { if (!this.missing) { try { - const fullpath = h.getPluginFile(this.file); - fs.unlinkSync(fullpath); + const fullpath = file.pluginFile(this.file); + file.rm(fullpath); } catch(e) { return log.error(e.message); } @@ -68,13 +54,13 @@ Plugin.prototype.delete = function() { }; Plugin.prototype.save = function() { - const data = cache.get(h.KEYS.plugins) || {}; + const stats = cache.get(h.KEYS.plugins) || {}; - if (this.deleted) delete data[this.name]; + if (this.deleted) delete stats[this.name]; else if (this.missing) return; - else data[this.name] = this.enabled; + else stats[this.name] = this.enabled; - cache.set(h.KEYS.plugins, data); + cache.set(h.KEYS.plugins, stats); }; Plugin.prototype.install = function(cb) { @@ -83,9 +69,9 @@ Plugin.prototype.install = function(cb) { const cmd = 'npm install --save ' + this.deps.join(' '); log.debug(cmd); const spin = h.spin(cmd); - cp.exec(cmd, {cwd: h.getCodeDir()}, function() { + cp.exec(cmd, {cwd: file.codeDir()}, function(e) { spin.stop(); - return cb(); + return cb(e); }); }; @@ -97,47 +83,62 @@ Plugin.init = function(head) { log.trace('initializing all plugins'); head = head || require('./core'); - // 1. check installed plugins - let plugins = []; - for (let f of h.getCodeDirData('lib/plugins')) { + 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; - - p.setFile(f.file); log.trace('found plugin: ' + p.name + '=' + p.ver); + + p.file = f.file; + p.enabled = stats[p.name]; + + if (!(p.name in stats)) { + if (p.builtin) { + log.trace('new builtin plugin, enable by default'); + p.enabled = true; + } else { + log.trace('new 3rd party plugin, disable by default'); + p.enabled = false; + } + } + installed.push(p); + } + // the one with bigger `id` comes first + installed = _.sortBy(installed, x => -x.id); + + // 2. init all in reversed order + for (let i = installed.length - 1; i >= 0; --i) { + const p = installed[i]; if (p.enabled) { p.init(); log.trace('inited plugin: ' + p.name); } else { log.trace('skipped plugin: ' + p.name); } - - plugins.push(p); } - // chain the plugins together - // the one has bigger `id` comes first - plugins = _.sortBy(plugins, x => -x.id); - + // 3. chain together + const plugins = installed.filter(x => x.enabled); let last = head; for (let p of plugins) { - if (!p.enabled) continue; last.setNext(p); last = p; } - // 2. check saved plugins + // 4. check missing plugins const missings = []; - const data = cache.get(h.KEYS.plugins) || {}; - for (let k of _.keys(data)) { - if (plugins.find(x => x.name === k)) continue; + for (let k of _.keys(stats)) { + if (installed.find(x => x.name === k)) continue; const p = new Plugin(-1, k, 'missing'); - p.enabled = data[k]; + p.enabled = stats[k]; missings.push(p); + log.trace('missing plugin:' + p.name); } - log.trace('missing plugins: ' + missings.length); - Plugin.plugins = plugins.concat(missings); + Plugin.plugins = installed.concat(missings); return missings.length === 0; }; @@ -146,23 +147,28 @@ Plugin.copy = function(src, cb) { if (path.extname(src) !== '.js') { src = config.sys.urls.plugin.replace('$name', src); } - const dst = h.getPluginFile(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) { - spin.stop(); - fs.unlinkSync(dst); - return cb(e); + dststream.emit('error', e); }); - const dststream = fs.createWriteStream(dst); + dststream.on('error', function(e) { + error = e; + dststream.end(); + }); dststream.on('close', function() { spin.stop(); - return cb(null, dst); + if (error) file.rm(dst); + return cb(error, dst); }); log.debug('copying from ' + src); @@ -176,6 +182,7 @@ Plugin.install = function(name, cb) { log.debug('copied to ' + fullpath); const p = require(fullpath); + p.file = path.basename(fullpath); p.install(function() { return cb(null, p); }); @@ -186,7 +193,7 @@ Plugin.installMissings = function(cb) { function doTask(plugin, queue, cb) { Plugin.install(plugin.name, function(e, p) { if (!e) { - p.enable(plugin.enabled); + p.enabled = plugin.enabled; p.save(); p.help(); } @@ -194,11 +201,11 @@ Plugin.installMissings = function(cb) { }); } - const plugins = Plugin.plugins.filter(x => x.missing); - if (plugins.length === 0) return cb(); + 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(plugins, {}, doTask); + const q = new Queue(missings, {}, doTask); q.run(1, cb); }; diff --git a/lib/plugins/cache.js b/lib/plugins/cache.js index efe8d231..677c6c84 100644 --- a/lib/plugins/cache.js +++ b/lib/plugins/cache.js @@ -9,11 +9,6 @@ var session = require('../session'); const plugin = new Plugin(50, 'cache', '', 'Plugin to provide local cache.'); -plugin.init = function() { - Plugin.prototype.init.call(this); - cache.init(); -}; - plugin.getProblems = function(cb) { const problems = cache.get(h.KEYS.problems); if (problems) { diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js index 186e00fd..24331ec6 100644 --- a/lib/plugins/leetcode.js +++ b/lib/plugins/leetcode.js @@ -8,6 +8,7 @@ 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'); @@ -19,24 +20,24 @@ const plugin = new Plugin(10, 'leetcode', '', 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) { +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) { const code = resp.statusCode; log.debug('http error: ' + code); @@ -48,6 +49,10 @@ function checkError(e, resp, expectedStatus) { } } return e; +}; + +plugin.init = function() { + config.app = 'leetcode'; } plugin.getProblems = function(cb) { @@ -75,11 +80,11 @@ plugin.getProblems = function(cb) { plugin.getCategoryProblems = function(category, cb) { log.debug('running leetcode.getCategoryProblems: ' + category); - const opts = makeOpts(config.sys.urls.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); const json = JSON.parse(body); @@ -118,7 +123,7 @@ plugin.getProblem = function(problem, cb) { const user = session.getUser(); if (problem.locked && !user.paid) return cb('failed to load locked problem!'); - const opts = makeOpts(config.sys.urls.graphql); + const opts = plugin.makeOpts(config.sys.urls.graphql); opts.headers.Origin = config.sys.urls.base; opts.headers.Referer = problem.link; @@ -144,7 +149,7 @@ plugin.getProblem = function(problem, cb) { const spin = h.spin('Downloading ' + problem.slug); request.post(opts, function(e, resp, body) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); const q = body.data.question; @@ -152,11 +157,12 @@ plugin.getProblem = function(problem, cb) { problem.totalAC = JSON.parse(q.stats).totalAccepted; problem.totalSubmit = JSON.parse(q.stats).totalSubmission; - if (!q.translatedContent) { - problem.desc = he.decode(cheerio.load(q.content).root().text()); - }else{ - problem.desc = he.decode(cheerio.load(q.translatedContent).root().text()); - } + + let content = q.translatedContent ? q.translatedContent : q.content; + // Replace with '^' as the power operator + content = content.replace(/<\/sup>/gm, '').replace(//gm, '^'); + problem.desc = he.decode(cheerio.load(content).root().text()); + problem.templates = JSON.parse(q.codeDefinition); problem.testcase = q.sampleTestCase; problem.testable = q.enableRunCode; @@ -173,20 +179,20 @@ function runCode(opts, problem, cb) { 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) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); if (body.error) { @@ -219,7 +225,7 @@ function verifyResult(task, queue, cb) { const spin = h.spin('Waiting for judge result'); request(opts, function(e, resp, body) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); let result = JSON.parse(body); @@ -255,7 +261,13 @@ function formatResult(result) { 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! @@ -268,7 +280,7 @@ function formatResult(result) { plugin.testProblem = function(problem, cb) { log.debug('running leetcode.testProblem'); - const opts = makeOpts(config.sys.urls.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) { @@ -287,7 +299,7 @@ plugin.testProblem = function(problem, cb) { plugin.submitProblem = function(problem, cb) { log.debug('running leetcode.submitProblem'); - const opts = makeOpts(config.sys.urls.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) { @@ -303,11 +315,11 @@ plugin.submitProblem = function(problem, cb) { plugin.getSubmissions = function(problem, cb) { log.debug('running leetcode.getSubmissions'); - const opts = makeOpts(config.sys.urls.submissions.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. @@ -321,16 +333,16 @@ plugin.getSubmissions = function(problem, cb) { plugin.getSubmission = function(submission, cb) { log.debug('running leetcode.getSubmission'); - const opts = makeOpts(config.sys.urls.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); 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); }); @@ -338,7 +350,7 @@ plugin.getSubmission = function(submission, cb) { plugin.starProblem = function(problem, starred, cb) { log.debug('running leetcode.starProblem'); - const opts = makeOpts(); + const opts = plugin.makeOpts(); opts.headers.Origin = config.sys.urls.base; opts.headers.Referer = problem.link; @@ -359,7 +371,7 @@ plugin.starProblem = function(problem, starred, cb) { } request(opts, function(e, resp, body) { - e = checkError(e, resp, 204); + e = plugin.checkError(e, resp, 204); if (e) return cb(e); cb(null, starred); @@ -368,12 +380,12 @@ plugin.starProblem = function(problem, starred, cb) { plugin.getFavorites = function(cb) { log.debug('running leetcode.getFavorites'); - const opts = makeOpts(config.sys.urls.favorites); + const opts = plugin.makeOpts(config.sys.urls.favorites); const spin = h.spin('Retrieving user favorites'); request(opts, function(e, resp, body) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); const favorites = JSON.parse(body); @@ -383,7 +395,7 @@ plugin.getFavorites = function(cb) { plugin.getUserInfo = function(cb) { log.debug('running leetcode.getUserInfo'); - const opts = makeOpts(config.sys.urls.graphql); + 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; @@ -402,7 +414,7 @@ plugin.getUserInfo = function(cb) { const spin = h.spin('Retrieving user profile'); request.post(opts, function(e, resp, body) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); const user = body.data.user; @@ -411,7 +423,7 @@ plugin.getUserInfo = function(cb) { }; function runSession(method, data, cb) { - const opts = makeOpts(config.sys.urls.session); + const opts = plugin.makeOpts(config.sys.urls.session); opts.json = true; opts.method = method; opts.body = data; @@ -419,7 +431,7 @@ function runSession(method, data, cb) { const spin = h.spin('Waiting session result'); request(opts, function(e, resp, body) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e && e.statusCode === 302) e = session.errors.EXPIRED; return e ? cb(e) : cb(null, body.sessions); @@ -454,7 +466,7 @@ plugin.signin = function(user, cb) { const spin = h.spin('Signing in leetcode.com'); request(config.sys.urls.login, function(e, resp, body) { spin.stop(); - e = checkError(e, resp, 200); + e = plugin.checkError(e, resp, 200); if (e) return cb(e); user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken'); 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 d61ef84a..a9af9f7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "leetcode-cli", - "version": "2.5.2", + "version": "2.6.2", "description": "A cli tool to enjoy leetcode!", "preferGlobal": "true", "engines": { @@ -11,7 +11,24 @@ }, "scripts": { "lint": "eslint lib/ test/", - "test": "npm run lint && nyc mocha test/** && nyc report --reporter=lcov" + "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", @@ -33,28 +50,28 @@ }, "homepage": "https://github.com/skygragon/leetcode-cli#readme", "dependencies": { - "ansi-styles": "3.2.0", + "ansi-styles": "3.2.1", "cheerio": "0.20.0", - "he": "1.1.1", + "he": "1.2.0", "mkdirp": "0.5.1", "moment": "^2.20.1", "nconf": "0.10.0", - "ora": "^1.3.0", + "ora": "3.0.0", "prompt": "1.0.0", - "request": "2.83.0", - "sprintf-js": "1.1.1", - "supports-color": "5.1.0", - "underscore": "1.8.3", + "request": "2.88.0", + "supports-color": "5.5.0", + "underscore": "1.9.1", "wordwrap": "1.0.0", - "yargs": "10.0.3" + "yargs": "12.0.4" }, "devDependencies": { - "chai": "4.1.2", - "eslint": "4.13.1", - "eslint-config-google": "0.9.1", - "mocha": "4.0.1", - "nock": "9.1.4", - "nyc": "11.3.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/templates/codeonly.tpl b/templates/codeonly.tpl index 38efb755..d8baa802 100644 --- a/templates/codeonly.tpl +++ b/templates/codeonly.tpl @@ -1 +1 @@ -<%= code %> +${code} diff --git a/templates/detailed.tpl b/templates/detailed.tpl index b8a762e2..cf501c0d 100644 --- a/templates/detailed.tpl +++ b/templates/detailed.tpl @@ -1,14 +1,16 @@ -<%= 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 %> -<% desc.forEach(function(x) { %><%= comment.line %> <%= x %> -<% }) %><%= comment.end %> -<%= code %> +${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 index 56ee5363..ca0f532e 100644 --- a/test/helper.js +++ b/test/helper.js @@ -8,8 +8,13 @@ const h = { h.clean = function() { if (!fs.existsSync(this.DIR)) fs.mkdirSync(this.DIR); - for (let f of fs.readdirSync(this.DIR)) - fs.unlinkSync(this.DIR + f); + 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/plugins/test_cache.js b/test/plugins/test_cache.js index a3980418..6b3114ca 100644 --- a/test/plugins/test_cache.js +++ b/test/plugins/test_cache.js @@ -3,6 +3,7 @@ const _ = require('underscore'); const assert = require('chai').assert; const rewire = require('rewire'); +const h = require('../../lib/helper'); const log = require('../../lib/log'); const config = require('../../lib/config'); const th = require('../helper'); @@ -11,7 +12,7 @@ describe('plugin:cache', function() { let plugin; let next; let cache; - let h; + let file; let session; const PROBLEMS = [ @@ -29,11 +30,11 @@ describe('plugin:cache', function() { th.clean(); next = {}; - h = rewire('../../lib/helper'); - h.getCacheDir = () => th.DIR; + file = rewire('../../lib/file'); + file.cacheDir = () => th.DIR; cache = rewire('../../lib/cache'); - cache.__set__('h', h); + cache.__set__('file', file); cache.init(); session = rewire('../../lib/session'); diff --git a/test/test_cache.js b/test/test_cache.js index b3fb669d..caba14c1 100644 --- a/test/test_cache.js +++ b/test/test_cache.js @@ -13,11 +13,11 @@ describe('cache', function() { beforeEach(function() { th.clean(); - const h = rewire('../lib/helper'); - h.getCacheDir = () => th.DIR; + const file = rewire('../lib/file'); + file.cacheDir = () => th.DIR; cache = rewire('../lib/cache'); - cache.__set__('h', h); + cache.__set__('file', file); cache.init(); }); diff --git a/test/test_config.js b/test/test_config.js index 6cfc0210..9ae828dd 100644 --- a/test/test_config.js +++ b/test/test_config.js @@ -12,11 +12,11 @@ describe('config', function() { beforeEach(function() { th.clean(); - const h = rewire('../lib/helper'); - h.getConfigFile = () => FILE; + const file = rewire('../lib/file'); + file.configFile = () => FILE; config = rewire('../lib/config'); - config.__set__('h', h); + config.__set__('file', file); }); function createConfigFile(data) { diff --git a/test/test_core.js b/test/test_core.js index c61031c0..0a436bb4 100644 --- a/test/test_core.js +++ b/test/test_core.js @@ -142,7 +142,17 @@ describe('core', function() { }); // #starProblem describe('#exportProblem', function() { + let file; + + beforeEach(function() { + file = rewire('../lib/file'); + file.init(); + core.__set__('file', file); + }); + it('should codeonly ok', function() { + file.isWindows = () => false; + const expected = [ '/**', ' * Definition for singly-linked list.', @@ -171,9 +181,7 @@ describe('core', function() { }); it('should codeonly ok in windows', function() { - const h = rewire('../lib/helper'); - h.isWindows = () => true; - core.__set__('h', h); + file.isWindows = () => true; const expected = [ '/**', @@ -203,8 +211,12 @@ describe('core', function() { }); 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', @@ -249,7 +261,11 @@ describe('core', function() { }); it('should detailed ok with ruby', function() { + file.isWindows = () => false; + const expected = [ + '#', + '# @lc app=leetcode id=2 lang=ruby', '#', '# [2] Add Two Numbers', '#', 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 ac47664e..143bda9e 100644 --- a/test/test_helper.js +++ b/test/test_helper.js @@ -1,6 +1,4 @@ 'use strict'; -const path = require('path'); - const assert = require('chai').assert; const rewire = require('rewire'); const _ = require('underscore'); @@ -130,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'), '.python3.py'); + assert.equal(h.langToExt('python3'), '.py'); assert.equal(h.langToExt('ruby'), '.rb'); + assert.equal(h.langToExt('rust'), '.rs'); assert.equal(h.langToExt('scala'), '.scala'); assert.equal(h.langToExt('swift'), '.swift'); - assert.equal(h.langToExt('rust'), '.raw'); }); }); // #langToExt @@ -148,13 +147,13 @@ 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('c:/Users/skygragon/file.python3.py'), '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 @@ -163,6 +162,7 @@ describe('helper', function() { it('should ok', function() { 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); @@ -171,60 +171,17 @@ 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() { - const HOME = path.join(__dirname, '..'); - - it('should ok', function() { - process.env.HOME = '/home/skygragon'; - - assert.equal(h.getUserHomeDir(), '/home/skygragon'); - assert.equal(h.getHomeDir(), '/home/skygragon/.lc'); - assert.equal(h.getCacheDir(), '/home/skygragon/.lc/cache'); - assert.equal(h.getCacheFile('xxx'), '/home/skygragon/.lc/cache/xxx.json'); - assert.equal(h.getConfigFile(), '/home/skygragon/.lc/config.json'); - assert.equal(h.getFilename('/home/skygragon/.lc/cache/xxx.json'), 'xxx'); - - process.env.HOME = ''; - process.env.USERPROFILE = 'C:\\Users\\skygragon'; - assert.equal(h.getUserHomeDir(), 'C:\\Users\\skygragon'); - }); - - it('should getCodeDir ok', function() { - assert.equal(h.getCodeDir(), HOME); - assert.equal(h.getCodeDir('.'), HOME); - assert.equal(h.getCodeDir('icons'), path.join(HOME, 'icons')); - assert.equal(h.getCodeDir('lib/plugins'), path.join(HOME, 'lib', 'plugins')); - }); - - it('should getCodeDirData ok', function() { - const files = h.getCodeDirData('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 getPluginFile ok', function() { - const expect = path.join(HOME, 'lib/plugins/cache.js'); - assert.equal(h.getPluginFile('cache.js'), expect); - assert.equal(h.getPluginFile('./cache.js'), expect); - assert.equal(h.getPluginFile('https://github.com/skygragon/cache.js'), expect); - }); - - it('should getFileData ok with missing file', function() { - assert.equal(h.getFileData('non-exist'), null); - }); - }); // #dirAndFiles - describe('#getSetCookieValue', function() { it('should ok', function() { const resp = { diff --git a/test/test_icon.js b/test/test_icon.js index 8408a2c9..5da832ab 100644 --- a/test/test_icon.js +++ b/test/test_icon.js @@ -4,11 +4,11 @@ const rewire = require('rewire'); describe('icon', function() { let icon; - let h; + let file; beforeEach(function() { - h = rewire('../lib/helper'); - h.getCodeDirData = function() { + file = rewire('../lib/file'); + file.listCodeDir = function() { return [ {name: 'mac', data: {yes: 'yes', no: 'no', lock: 'lock', like: 'like', unlike: 'unlike'}}, {name: 'win7', data: {yes: 'YES', no: 'NO', lock: 'LOCK', like: 'LIKE', unlike: 'UNLIKE'}} @@ -16,7 +16,7 @@ describe('icon', function() { }; icon = rewire('../lib/icon'); - icon.__set__('h', h); + icon.__set__('file', file); icon.init(); }); @@ -30,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, '✘'); @@ -40,7 +42,7 @@ describe('icon', function() { }); it('should ok with unknown theme on windows', function() { - h.isWindows = () => true; + file.isWindows = () => true; icon.setTheme('non-exist'); assert.equal(icon.yes, 'YES'); diff --git a/test/test_log.js b/test/test_log.js index 3fd58ff6..92e5ef4b 100644 --- a/test/test_log.js +++ b/test/test_log.js @@ -93,16 +93,16 @@ describe('log', function() { it('should ok with log.fail', function() { log.fail({msg: 'some error', statusCode: 500}); - assert.equal(expected, chalk.red('[ERROR] some error [500]')); + assert.equal(expected, chalk.red('[ERROR] some error [code=500]')); log.fail('some error'); - assert.equal(expected, 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 %d and %%', 'string', 100); + 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 d758b9b9..aa3a40be 100644 --- a/test/test_plugin.js +++ b/test/test_plugin.js @@ -13,7 +13,7 @@ const th = require('./helper'); const Plugin = rewire('../lib/plugin'); describe('plugin', function() { - let h; + let file; let cache; const NOOP = () => {}; @@ -23,9 +23,9 @@ describe('plugin', function() { chalk.init(); config.init(); - h = rewire('../lib/helper'); + file = rewire('../lib/file'); cache = rewire('../lib/cache'); - Plugin.__set__('h', h); + Plugin.__set__('file', file); Plugin.__set__('cache', cache); }); @@ -35,17 +35,17 @@ describe('plugin', function() { }); 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'); + const p1 = new Plugin(0, 'leetcode', '2.0'); + const p2 = new Plugin(1, 'cache', '1.0'); + const p3 = new Plugin(2, 'retry', '3.0'); + const p4 = new Plugin(3, 'core', '4.0'); before(function() { p1.init = p2.init = p3.init = p4.init = NOOP; - h.getCodeDirData = function() { + file.listCodeDir = function() { return [ {name: 'cache', data: p2, file: 'cache.js'}, - {name: 'leetcode', data: p1, file: '.leetcode.js'}, // disabled + {name: 'leetcode', data: p1, file: 'leetcode.js'}, {name: 'retry', data: p3, file: 'retry.js'}, {name: 'bad', data: null} ]; @@ -53,6 +53,9 @@ describe('plugin', function() { }); it('should init ok', function() { + cache.get = () => { + return {cache: true, leetcode: false, retry: true}; + }; assert.deepEqual(Plugin.plugins, []); const res = Plugin.init(p4); @@ -60,7 +63,7 @@ describe('plugin', function() { assert.deepEqual(Plugin.plugins.length, 3); const names = Plugin.plugins.map(p => p.name); - assert.deepEqual(names, ['Retry', 'Cache', 'Leetcode']); + assert.deepEqual(names, ['retry', 'cache', 'leetcode']); assert.equal(p4.next, p3); assert.equal(p3.next, p2); @@ -70,7 +73,7 @@ describe('plugin', function() { it('should find missing ok', function() { cache.get = () => { - return {company: true, solution: true}; + return {company: true, leetcode: false, solution: true}; }; const res = Plugin.init(p4); @@ -78,7 +81,7 @@ describe('plugin', function() { assert.deepEqual(Plugin.plugins.length, 5); const names = Plugin.plugins.map(p => p.name); - assert.deepEqual(names, ['Retry', 'Cache', 'Leetcode', 'company', 'solution']); + assert.deepEqual(names, ['retry', 'cache', 'leetcode', 'company', 'solution']); assert.equal(p4.next, p3); assert.equal(p3.next, p2); @@ -123,7 +126,7 @@ describe('plugin', function() { const DST = path.resolve(th.DIR, 'copy.test.js'); before(function() { - h.getPluginFile = () => DST; + file.pluginFile = () => DST; }); it('should copy from http error', function(done) { @@ -143,9 +146,9 @@ describe('plugin', function() { ]; fs.writeFileSync(SRC, data.join('\n')); - Plugin.install(SRC, function(e, p) { + Plugin.copy(SRC, function(e, fullpath) { assert.notExists(e); - assert.equal(p.x, 123); + assert.equal(fullpath, DST); assert.equal(fs.existsSync(DST), true); done(); }); @@ -162,7 +165,7 @@ describe('plugin', function() { beforeEach(function() { expected = []; - h.getPluginFile = x => th.DIR + x; + file.pluginFile = x => th.DIR + x; Plugin.install = (name, cb) => { expected.push(name); return cb(null, PLUGINS[+name]); @@ -179,36 +182,9 @@ describe('plugin', function() { }); }); // #Plugin.installMissings - describe('#enable', function() { - const FILE = path.resolve(th.DIR, 'leetcode.js'); - - before(function() { - h.getPluginFile = () => FILE; - }); - - it('should ok', function() { - const p = new Plugin(0, 'Leetcode', '2.0', ''); - assert.equal(p.enabled, true); - - p.setFile('.leetcode.js'); - fs.writeFileSync(FILE, ''); - assert.equal(p.enabled, false); - assert.equal(p.file, '.leetcode.js'); - p.enable(false); - assert.equal(p.enabled, false); - assert.equal(p.file, '.leetcode.js'); - p.enable(true); - assert.equal(p.enabled, true); - assert.equal(p.file, 'leetcode.js'); - p.enable(false); - assert.equal(p.enabled, false); - assert.equal(p.file, '.leetcode.js'); - }); - }); // #enable - describe('#delete', function() { it('should ok', function() { - h.getPluginFile = x => th.DIR + x; + file.pluginFile = x => th.DIR + x; const p = new Plugin(0, '0', '2018.01.01'); p.file = '0.js'; 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') + ' '); + }); +});