├── .eslintrc ├── .gitignore ├── .npmrc ├── .travis.yml ├── CHECKLIST.md ├── README.md ├── bin └── manpm.js ├── images ├── manpm-screenshot.png └── search-section.png ├── package.json └── src ├── find-section-spec.js ├── find-section.js ├── get-readme-spec.js ├── get-readme.js ├── index.js ├── to-sections-spec.js ├── to-sections.js └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0, 4 | "indent": [ 5 | 2, 6 | 2 7 | ], 8 | "quotes": [ 9 | 2, 10 | "single" 11 | ], 12 | "linebreak-style": [ 13 | 2, 14 | "unix" 15 | ], 16 | "semi": [ 17 | 2, 18 | "always" 19 | ] 20 | }, 21 | "env": { 22 | "node": true, 23 | "browser": false 24 | }, 25 | "extends": "eslint:recommended" 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '0.12' 10 | - '4.3.0' 11 | - '5' 12 | before_install: 13 | - npm i -g npm@2.14.12 14 | before_script: 15 | - npm prune 16 | after_success: 17 | - npm run semantic-release 18 | branches: 19 | except: 20 | - "/^v\\d+\\.\\d+\\.\\d+$/" 21 | -------------------------------------------------------------------------------- /CHECKLIST.md: -------------------------------------------------------------------------------- 1 | - [x] watch the [egghead.io][egghead] series [How to Write an Open Source JavaScript Library][egghead series]; 2 | it is extremely useful for any NPM project (OSS or closed-sourced). 3 | 4 | - [x] start unit testing right away, [pick your unit testing framework][pick testing framework] 5 | 6 | - [x] start linting code to prevent obvious problems, like misspelled variable. 7 | [eslint][eslint], [jshint][jshint], [jscs][jscs] or all of them together 8 | [gulp-lint-everything][gulp-lint-everything] 9 | 10 | - [x] run linting and unit tests on each commit locally. [pre-git][pre-git], [ghooks][ghooks] 11 | 12 | - [x] validate commit message using [pre-git][pre-git] or [commitizen][commitizen] with [validate-commit-msg][validate-commit-msg]. This 13 | enables other tools, like intelligent release notes. 14 | 15 | - [x] show the project's GitHub open issues on demand or on commit using [git-issues][git-issues] 16 | 17 | - [x] setup continuous integration server, like [TravisCI][travis] or [CircleCI][circle] (or wait until you set up [semantic-release][semantic-release] which will set up [TravisCI][travis] for you). 18 | 19 | - [x] [add badges][badges] to the README to make broken unit tests or out of date dependencies visible 20 | * ci server badge 21 | * published NPM package info 22 | * production and dev dependencies being out of date 23 | * semantic release badge 24 | 25 | - [x] check module published size and white list only necessary files, [tutorial][module size] 26 | 27 | - [x] setup [semantic-release][semantic-release] to automate publishing 28 | and avoid breaking [semver][semver]. This is [important][semver important], 29 | but is currently [broken][broken semver] in too many projects. Even this checklist is using semver! 30 | 31 | - [x] avoid surprizes by using exact versions of the top level dependencies. 32 | Use [save-exact][save-exact] NPM setting and [exact-semver][exact-semver] to enforce it. 33 | 34 | - [x] setup a script to reliably update out of date dependencies using [next-update][next-update install] 35 | 36 | - [ ] catch missing or invalid `package.json` values using [grunt-nice-package][grunt-nice-package] 37 | or [fixpack][fixpack] 38 | 39 | - [x] write simple installation commands for your module 40 | 41 | - [x] write "quick intro" example showing the main feature of your module 42 | 43 | - [ ] add CONTRIBUTING.md file with clear guidelines how others can add new features or fix bugs 44 | in your module. [Atom editor][atom] and [lodash][lodash] have excellent examples to follow. 45 | 46 | [egghead]: https://egghead.io 47 | [egghead series]: https://egghead.io/series/how-to-write-an-open-source-javascript-library 48 | 49 | [pick testing framework]: http://glebbahmutov.com/blog/picking-javascript-testing-framework/ 50 | 51 | [eslint]: http://eslint.org/ 52 | [jshint]: http://jshint.com/docs/ 53 | [jscs]: http://jscs.info/ 54 | [gulp-lint-everything]: https://github.com/bahmutov/gulp-lint-everything 55 | 56 | [pre-git]: https://github.com/bahmutov/pre-git 57 | [ghooks]: https://www.npmjs.com/package/ghooks 58 | 59 | [commitizen]: https://www.npmjs.com/package/commitizen 60 | 61 | [validate-commit-msg]: https://www.npmjs.com/package/validate-commit-msg 62 | 63 | [git-issues]: https://www.npmjs.com/package/git-issues 64 | 65 | [travis]: https://travis-ci.org/ 66 | [circle]: https://circleci.com/ 67 | 68 | [badges]: http://glebbahmutov.com/blog/tightening-node-project/ 69 | 70 | [module size]: http://glebbahmutov.com/blog/smaller-published-NPM-modules/ 71 | 72 | [semantic-release]: https://github.com/semantic-release/semantic-release 73 | [semver]: http://semver.org/ 74 | [semver important]: https://medium.com/javascript-scene/software-versions-are-broken-3d2dc0da0783#.h96ppopx3 75 | [broken semver]: https://www.youtube.com/watch?v=tc2UgG5L7WM 76 | 77 | [save-exact]: https://docs.npmjs.com/misc/config#save-exact 78 | [exact-semver]: https://github.com/bahmutov/exact-semver 79 | 80 | [next-update install]: https://github.com/bahmutov/next-update#install 81 | 82 | [grunt-nice-package]: https://github.com/bahmutov/grunt-nice-package 83 | [fixpack]: https://github.com/henrikjoreteg/fixpack 84 | 85 | [atom]: https://github.com/atom/atom/blob/master/CONTRIBUTING.md 86 | [lodash]: https://github.com/lodash/lodash/blob/master/CONTRIBUTING.md 87 | 88 | Source: [npm-module-checklist](https://github.com/bahmutov/npm-module-checklist) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # manpm 2 | 3 | > Shows the relevant part of NPM module's README file right in your terminal 4 | 5 | [![NPM][manpm-icon] ][manpm-url] 6 | 7 | [![manpm](https://img.shields.io/badge/manpm-compatible-3399ff.svg)](https://github.com/bahmutov/manpm) 8 | [![alternate](https://img.shields.io/badge/manpm-%E2%9C%93-3399ff.svg)](https://github.com/bahmutov/manpm) 9 | [![CI][ci-badge] ][ci-url] 10 | [![semantic-release][semantic-image] ][semantic-url] 11 | [![npm checklist](https://img.shields.io/badge/%E2%98%B0-%E2%9C%93-brightgreen.svg)](https://github.com/bahmutov/npm-module-checklist#readme) 12 | [CHECKLIST.md](CHECKLIST.md) 13 | 14 | [manpm-icon]: https://nodei.co/npm/manpm.png?downloads=true 15 | [manpm-url]: https://npmjs.org/package/manpm 16 | [ci-badge]: https://travis-ci.org/bahmutov/manpm.png?branch=master 17 | [ci-url]: https://travis-ci.org/bahmutov/manpm 18 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 19 | [semantic-url]: https://github.com/semantic-release/semantic-release 20 | 21 | ## Badge 22 | 23 | If you module has API well-described in the README, and `manpm ` works well, 24 | consider adding the following `manpm compatible` badge to your README. Here is the markdown 25 | for both variants: words or unicode symbols 26 | 27 | ``` 28 | [![manpm](https://img.shields.io/badge/manpm-compatible-3399ff.svg)](https://github.com/bahmutov/manpm) 29 | [![alternate](https://img.shields.io/badge/manpm-%E2%9C%93-3399ff.svg)](https://github.com/bahmutov/manpm) 30 | ``` 31 | 32 | ## Install 33 | 34 | npm install -g manpm 35 | 36 | ## Show the entire README from a package or github repo 37 | 38 | You can give NPM package name (like `manpm`), GitHub user / repo pair (like `bahmutov/manpm`) or 39 | full GitHub url (like `https://github.com/bahmutov/object-fitter` or `git@github.com:bahmutov/object-fitter.git`). 40 | 41 | manpm 42 | 43 | For example `manpm email-regex` will render the README from 44 | [email-regex](https://www.npmjs.com/package/email-regex) package in you terminal 45 | 46 | ![manpm screenshot](images/manpm-screenshot.png) 47 | 48 | ## Show part of the readme 49 | 50 | manpm [optional search text inside README] 51 | 52 | If search text is provided, only a section of the README file with that text 53 | (if found) will be displayed. 54 | 55 | ![manpm search section](images/search-section.png) 56 | 57 | The following search features are implemented 58 | 59 | - [x] find exact match in the section heading text 60 | - [ ] fuzzy text match in the section heading text 61 | - [x] find exact match in the section body 62 | - [ ] fuzzy text match in the section body 63 | 64 | I am still looking for a library capable of fuzzy text search. 65 | Maybe [lunr](https://github.com/olivernn/lunr.js)? 66 | 67 | ## Show and search local README 68 | 69 | Sometimes you just want to find a section in the local README file, right in the current directory. 70 | 71 | manpm . [optional search text] 72 | 73 | ## Example: showing ES6 docs 74 | 75 | There is a great GitHub repo [ES6 Overview in 350 Bullet Points](https://github.com/bevacqua/es6) 76 | hosted at `https://github.com/bevacqua/es6`. Let us see how we can info for `WeakSets` 77 | 78 | ``` 79 | $ manpm bevacqua/es6 weaksets 80 | # WeakSets 81 | 82 | * WeakSet is sort of a cross-breed between Set and WeakMap 83 | * A WeakSet is a set that can't be iterated and doesn't have enumeration methods 84 | * WeakSet values must be reference types 85 | * WeakSet may be useful for a metadata table indicating whether a reference is actively in use or not 86 | * Read ES6 WeakSets in Depth (https://ponyfoo.com/articles/es6-weakmaps-sets-and-weaksets-in-depth#es6-weaksets) 87 | 88 | (back to table of contents) (#table-of-contents) 89 | ``` 90 | 91 | I added an alias to `manpm bevacqua/es6` command to my shell alias file for convenience 92 | 93 | $ echo 'alias es6-docs="manpm bevacqua/es6"' >> ~/.alias 94 | source ~/.alias 95 | es6-docs weaksets 96 | es6-docs array 97 | 98 | ## Example: showing ES6 features 99 | 100 | Another great succint overview of ES6 features is 101 | in [lukehoban/es6features](https://github.com/lukehoban/es6features). Let us add an alias 102 | 103 | echo 'alias es6-features="manpm lukehoban/es6features"' >> ~/.alias 104 | source ~/.alias 105 | 106 | Let us look up the binary notation in ES6 107 | 108 | es6-features binary 109 | ### Binary and Octal Literals 110 | Two new numeric literal forms are added for binary (b) and octal (o). 111 | 0b111110111 === 503 // true 112 | 0o767 === 503 // true 113 | 114 | ## Pipe through less | more 115 | 116 | You can pipe the output of `manpm` through "less" or "more" tools - but you will 117 | lose the Markdown highlighting. 118 | 119 | manpm | less 120 | manpm | more 121 | 122 | ## Advanced 123 | 124 | If there are problems and `manpm` is not working as expected, you can see the debug output. 125 | Just run the tool with `DEBUG=manpm manpm ...` environment setting. 126 | 127 | $ DEBUG=manpm manpm object-fitter 128 | manpm fetching README for package +0ms object-fitter 129 | 130 | Good testing commands: 131 | 132 | DEBUG=manpm npm run example-es6-docs 133 | 134 | ## Inspired by the following tools 135 | 136 | * [man-n](https://github.com/man-n/man-n) 137 | * [npm-man](https://github.com/eush77/npm-man) 138 | * [readme](https://www.npmjs.com/package/readme) 139 | 140 | I wanted something a little more API friendly, like finding and showing 141 | a README section that talked about a specific API method for example. 142 | 143 | ## More examples 144 | 145 | - 📺 Watch video [Type Placeholders Into The Form: manp and cypress-await example](https://youtu.be/Z4nDKbWMkJc) 146 | 147 | ### Small print 148 | 149 | Author: Gleb Bahmutov © 2015 150 | 151 | * [@bahmutov](https://twitter.com/bahmutov) 152 | * [glebbahmutov.com](http://glebbahmutov.com) 153 | * [blog](http://glebbahmutov.com/blog/) 154 | 155 | License: MIT - do anything with the code, but don't blame me if it does not work. 156 | 157 | Spread the word: tweet, star on github, etc. 158 | 159 | Support: if you find any problems with this module, email / tweet / 160 | [open issue](https://github.com/bahmutov/manpm/issues) on Github 161 | 162 | ## MIT License 163 | 164 | Copyright (c) 2015 Gleb Bahmutov 165 | 166 | Permission is hereby granted, free of charge, to any person 167 | obtaining a copy of this software and associated documentation 168 | files (the "Software"), to deal in the Software without 169 | restriction, including without limitation the rights to use, 170 | copy, modify, merge, publish, distribute, sublicense, and/or sell 171 | copies of the Software, and to permit persons to whom the 172 | Software is furnished to do so, subject to the following 173 | conditions: 174 | 175 | The above copyright notice and this permission notice shall be 176 | included in all copies or substantial portions of the Software. 177 | 178 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 179 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 180 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 181 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 182 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 183 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 184 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 185 | OTHER DEALINGS IN THE SOFTWARE. 186 | 187 | -------------------------------------------------------------------------------- /bin/manpm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('simple-bin-help')({ 4 | minArguments: 3, 5 | packagePath: __dirname + '/../package.json', 6 | help: 'USE: manpm [optional search text]' 7 | }); 8 | 9 | var name = process.argv[2]; 10 | var search = process.argv[3]; 11 | var maNpm = require('../src/index'); 12 | maNpm({ 13 | name: name, 14 | search: search 15 | }); 16 | -------------------------------------------------------------------------------- /images/manpm-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/manpm/71d3cc8be7a379f2c688737c080afc6aea644d0b/images/manpm-screenshot.png -------------------------------------------------------------------------------- /images/search-section.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/manpm/71d3cc8be7a379f2c688737c080afc6aea644d0b/images/search-section.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manpm", 3 | "description": "Shows the relevant part of NPM module's README file right in your terminal", 4 | "main": "./src/index.js", 5 | "version": "0.0.0-semantic-release", 6 | "bin": { 7 | "manpm": "./bin/manpm.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha src/*-spec.js", 11 | "pretest": "npm run lint", 12 | "lint": "eslint src/*.js bin/*.js", 13 | "commit": "git-issues && commit-wizard", 14 | "issues": "git-issues", 15 | "pkgfiles": "pkgfiles", 16 | "exact": "exact-semver", 17 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 18 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 19 | "next-update": "next-update -k true", 20 | "example-npm": "node bin/manpm.js chalk background", 21 | "test-search-in-text": "node bin/manpm.js chalk cmder", 22 | "example-github": "node bin/manpm.js https://github.com/bahmutov/object-fitter", 23 | "example-es6-docs": "node bin/manpm.js bevacqua/es6 literals", 24 | "test-hr": "DEBUG=manpm node bin/manpm.js hr", 25 | "ban": "ban" 26 | }, 27 | "files": [ 28 | "bin", 29 | "src/*.js", 30 | "!src/*-spec.js", 31 | "CHECKLIST.md" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/bahmutov/manpm.git" 36 | }, 37 | "keywords": [ 38 | "man", 39 | "manual", 40 | "help", 41 | "npm", 42 | "utility", 43 | "readme", 44 | "cli" 45 | ], 46 | "author": "Gleb Bahmutov ", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/bahmutov/manpm/issues" 50 | }, 51 | "homepage": "https://github.com/bahmutov/manpm#readme", 52 | "dependencies": { 53 | "@bahmutov/parse-github-repo-url": "0.1.0", 54 | "bluebird": "3.3.4", 55 | "check-more-types": "2.15.0", 56 | "debug": "2.2.0", 57 | "get-package-readme": "1.2.0", 58 | "github-readme": "1.0.0", 59 | "lazy-ass": "1.4.0", 60 | "lodash": "4.8.1", 61 | "marked": "0.3.5", 62 | "marked-terminal": "https://github.com/mikaelbr/marked-terminal.git#35f6ddac9d9f32b11e85ceb6bc5f42642f06e456", 63 | "marked-to-md": "1.0.1", 64 | "package-json": "2.3.2", 65 | "simple-bin-help": "1.6.0", 66 | "simple-get": "1.4.3", 67 | "update-notifier": "0.6.3" 68 | }, 69 | "preferGlobal": true, 70 | "release": { 71 | "verifyConditions": { 72 | "path": "condition-node-version", 73 | "node": "4.3.0", 74 | "verbose": true 75 | } 76 | }, 77 | "config": { 78 | "pre-git": { 79 | "commit-msg": "simple", 80 | "pre-commit": [ 81 | "npm run ban", 82 | "npm test", 83 | "npm run example-npm", 84 | "npm run example-github", 85 | "npm run example-es6-docs", 86 | "npm run test-search-in-text", 87 | "npm run test-hr" 88 | ], 89 | "pre-push": [ 90 | "npm run size" 91 | ], 92 | "post-commit": [], 93 | "post-merge": [] 94 | }, 95 | "next-update": { 96 | "skip": "simple-get" 97 | } 98 | }, 99 | "devDependencies": { 100 | "ban-sensitive-files": "1.8.2", 101 | "condition-node-version": "1.2.0", 102 | "eslint": "2.6.0", 103 | "exact-semver": "1.2.0", 104 | "git-issues": "1.2.0", 105 | "mocha": "2.4.5", 106 | "next-update": "1.2.2", 107 | "pre-git": "3.8.3", 108 | "quote": "0.4.0", 109 | "semantic-release": "6.2.1" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/find-section-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var quote = require('quote'); 4 | 5 | /* global describe, it */ 6 | 7 | // had to remove describe-it - it was conflicting with really-need 8 | /* 9 | var partJs = require('path').join(__dirname, 'find-section.js'); 10 | var marked = require('marked'); 11 | var describeIt = require('describe-it'); 12 | describeIt(partJs, 'findSectionByHeader(search, tokens)', function () { 13 | it('is a function', function () { 14 | la(check.fn(this.findSectionByHeader)); 15 | }); 16 | 17 | it('finds single section by text in the header', function () { 18 | var text = [ 19 | '# foo is awesome', 20 | 'bar', 21 | '# baz is worse', 22 | 'something else' 23 | ].join('\n'); 24 | var tokens = marked.lexer(text); 25 | var section = this.findSectionByHeader('foo', tokens); 26 | la(check.unemptyArray(section), section); 27 | la(section.length === 2, section); 28 | la(section[0].text === 'foo is awesome', section[0]); 29 | la(section[1].text === 'bar', section[1]); 30 | }); 31 | 32 | it('finds two adjoining sections by text in the header', function () { 33 | var text = [ 34 | '# foo is awesome', 35 | 'bar', 36 | '# baz is worse than foo', 37 | 'something else' 38 | ].join('\n'); 39 | var tokens = marked.lexer(text); 40 | var section = this.findSectionByHeader('foo', tokens); 41 | 42 | la(check.unemptyArray(section), section); 43 | la(section.length === 4, section); 44 | la(section[0].text === 'foo is awesome', section[0]); 45 | la(section[2].text === 'baz is worse than foo', section[2]); 46 | }); 47 | 48 | it('finds two separate sections by text in the header', function () { 49 | var text = [ 50 | '# foo is awesome', 51 | 'bar', 52 | 53 | '# something else in the middle', 54 | 'something else', 55 | 56 | '# baz is worse than foo', 57 | 'something else' 58 | ].join('\n'); 59 | var tokens = marked.lexer(text); 60 | var section = this.findSectionByHeader('foo', tokens); 61 | 62 | la(check.unemptyArray(section), section); 63 | la(section.length === 4, section); 64 | la(section[0].text === 'foo is awesome', section[0]); 65 | la(section[2].text === 'baz is worse than foo', section[2]); 66 | }); 67 | }); 68 | 69 | describeIt(partJs, 'var toTokens', function () { 70 | it('is a function', function () { 71 | la(check.fn(this.toTokens)); 72 | }); 73 | 74 | it('parses 2 paragraphs', function () { 75 | var text = ['# p1', 'foo', '# p2', 'bar'].join('\n'); 76 | var tokens = this.toTokens(text); 77 | la(check.array(tokens)); 78 | la(tokens.length === 4); 79 | 80 | la(tokens[0].type === 'heading', tokens); 81 | la(tokens[0].text === 'p1', tokens); 82 | 83 | la(tokens[1].type === 'paragraph', tokens); 84 | la(tokens[1].text === 'foo', tokens); 85 | 86 | la(tokens[2].type === 'heading', tokens); 87 | la(tokens[2].text === 'p2', tokens); 88 | 89 | la(tokens[3].type === 'paragraph', tokens); 90 | la(tokens[3].text === 'bar', tokens); 91 | }); 92 | }); 93 | */ 94 | 95 | describe('find section', function () { 96 | var find = require('./find-section'); 97 | 98 | it('is a function', function () { 99 | la(check.fn(find)); 100 | }); 101 | 102 | it('returns entire text without options', function () { 103 | var text = 'foo'; 104 | var section = find(undefined, text); 105 | la(text === section); 106 | }); 107 | 108 | it('returns entire text without search', function () { 109 | var text = 'foo'; 110 | var section = find({}, text); 111 | la(text === section); 112 | }); 113 | 114 | it('returns entire text if there are no sections', function () { 115 | var text = 'foo bar\nbaz 42'; 116 | var search = { text: 'baz' }; 117 | var found = find(search, text); 118 | la(found === text, 'found', found); 119 | }); 120 | 121 | it('finds first section after header with text', function () { 122 | var text = [ 123 | '# foo is awesome', 124 | 'bar', 125 | '# baz is worse', 126 | 'something else' 127 | ]; 128 | var search = { text: 'foo' }; 129 | var found = find(search, text.join('\n')); 130 | la(check.unemptyString(found)); 131 | var firstSection = text.slice(0, 2).join('\n'); 132 | la(found === firstSection, 'found\n' + quote(found), '\ninstead of\n' + quote(firstSection)); 133 | }); 134 | 135 | it('finds word inside the section', function () { 136 | var text = [ 137 | '# 1 is foo', 138 | 'foo is awesome', 139 | '# 2 is bar', 140 | 'bar is not as good' 141 | ]; 142 | var search = { text: 'awesome' }; 143 | var expected = text.slice(0, 2).join('\n'); 144 | var found = find(search, text.join('\n')); 145 | la(check.unemptyString(found), 'could not find', search); 146 | la(found === expected, 'found first section\n' + found); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /src/find-section.js: -------------------------------------------------------------------------------- 1 | var log = require('debug')('manpm'); 2 | var verbose = require('debug')('verbose'); 3 | 4 | var la = require('lazy-ass'); 5 | var check = require('check-more-types'); 6 | var marked = require('marked'); 7 | var mdRenderer = require('marked-to-md'); 8 | var renderer = mdRenderer(new marked.Renderer()); 9 | var parser = new marked.Parser({ renderer: renderer }); 10 | var toSections = require('./to-sections'); 11 | var _ = require('lodash'); 12 | 13 | var toTokens = marked.lexer.bind(marked); 14 | 15 | function fromTokens(tokens) { 16 | // parser.parse removes all items from tokens! 17 | var copy = _.clone(tokens, true); 18 | copy.links = _.clone(tokens.links, true); 19 | return parser.parse(copy); 20 | } 21 | 22 | // assumes search is lowercase text already 23 | function hasText(text, search) { 24 | la(check.string(text), 'missing text', text); 25 | la(check.unemptyString(search), 'missing search', search); 26 | var has = text.toLowerCase().indexOf(search) !== -1; 27 | return has; 28 | } 29 | 30 | // returns found tokens 31 | function findSectionByHeader(search, tokens) { 32 | la(check.unemptyString(search), 'missing search', search); 33 | la(check.array(tokens), 'missing tokens', tokens); 34 | 35 | search = search.toLowerCase(); 36 | 37 | var foundStart, foundEnd; 38 | 39 | var links = tokens.links; 40 | var foundTokens = []; 41 | 42 | tokens.forEach(function (token, k) { 43 | if (token.type !== 'heading' && !foundStart) { 44 | return; 45 | } 46 | if (token.type === 'heading') { 47 | var hasSearchText = token.text.toLowerCase().indexOf(search) !== -1; 48 | verbose('checking heading', k, token.text, 49 | 'has text?', hasSearchText, 50 | 'start', foundStart, 'end', foundEnd); 51 | 52 | if (check.not.defined(foundStart) && hasSearchText) { 53 | foundStart = k; 54 | return; 55 | } 56 | if (check.not.defined(foundStart) && !hasSearchText) { 57 | return; 58 | } 59 | if (!hasSearchText) { 60 | foundEnd = k; 61 | 62 | verbose('part from %d to %d', foundStart, foundEnd); 63 | var part = tokens.slice(foundStart, foundEnd); 64 | foundTokens = foundTokens.concat(part); 65 | foundStart = foundEnd = undefined; 66 | } 67 | } 68 | }); 69 | 70 | if (check.defined(foundStart) && check.not.defined(foundEnd)) { 71 | foundEnd = tokens.length; 72 | } 73 | if (check.defined(foundStart) && 74 | check.defined(foundEnd)) { 75 | verbose('slicing part at the end %d to %d', foundStart, foundEnd); 76 | var part = tokens.slice(foundStart, foundEnd); 77 | foundTokens = foundTokens.concat(part); 78 | } 79 | 80 | if (check.not.empty(foundTokens)) { 81 | foundTokens.links = links; 82 | } 83 | return foundTokens; 84 | } 85 | 86 | // returns found tokens 87 | function findSectionByText(search, tokens) { 88 | la(check.unemptyString(search), 'missing search', search); 89 | la(check.array(tokens), 'missing tokens', tokens); 90 | 91 | search = search.toLowerCase(); 92 | 93 | var sections = toSections(tokens); 94 | la(check.array(sections), 95 | 'could not find sections from tokens', tokens); 96 | 97 | var sectionsText = sections.map(fromTokens); 98 | 99 | var foundIndex = -1; 100 | sectionsText.some(function (sectionText, k) { 101 | if (hasText(sectionText, search)) { 102 | foundIndex = k; 103 | return true; 104 | } 105 | }); 106 | 107 | if (foundIndex !== -1) { 108 | return sections[foundIndex]; 109 | } 110 | } 111 | 112 | // if not found, returns entire text 113 | function findSection(options, md) { 114 | la(check.maybe.object(options), 'missing options', options); 115 | la(check.unemptyString(md), 'missing markdown', md); 116 | 117 | options = options || {}; 118 | 119 | var searchString = options.text || options.search; 120 | if (check.unemptyString(searchString)) { 121 | log('searching for markdown part that talks about', searchString); 122 | } 123 | if (!searchString) { 124 | return md; 125 | } 126 | 127 | var tokens = toTokens(md); 128 | la(check.array(tokens), 'could not parse markdown', md); 129 | 130 | var foundSectionByHeader = findSectionByHeader(searchString, tokens); 131 | if (check.unemptyArray(foundSectionByHeader)) { 132 | return fromTokens(foundSectionByHeader).trim(); 133 | } 134 | 135 | var foundSectionByText = findSectionByText(searchString, tokens); 136 | if (check.unemptyArray(foundSectionByText)) { 137 | return fromTokens(foundSectionByText).trim(); 138 | } 139 | 140 | console.log('Cannot find the search string "%s", showing entire document', 141 | searchString); 142 | return md; 143 | } 144 | 145 | module.exports = findSection; 146 | -------------------------------------------------------------------------------- /src/get-readme-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var utils = require('./utils'); 4 | 5 | /* global describe, it */ 6 | 7 | describe('maybeGithubRepoName(name)', function () { 8 | it('returns true for common repos', function () { 9 | var cases = [ 10 | 'foo/bar', 11 | 'bahmutov/manpm', 12 | 'bahmutov/describe-it', 13 | 'bevacqua/es6' 14 | ]; 15 | cases.forEach(function (c) { 16 | la(utils.maybeGithubRepoName(c), c); 17 | }); 18 | }); 19 | 20 | it('returns false for non repos', function () { 21 | la(!utils.maybeGithubRepoName('foo/bar/baz')); 22 | la(!utils.maybeGithubRepoName('git@github.com:bahmutov/object-fitter.git')); 23 | }); 24 | }); 25 | 26 | describe('maybeGithubRepoUrl(name)', function () { 27 | it('returns true for common repo urls', function () { 28 | var cases = [ 29 | 'https://github.com/bevacqua/es6', 30 | 'git@github.com:bevacqua/es6.git' 31 | ]; 32 | cases.forEach(function (c) { 33 | la(utils.maybeGithubRepoUrl(c), c); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('parseGithub(url)', function () { 39 | it('parses es6 repo url', function () { 40 | var url = 'https://github.com/bevacqua/es6'; 41 | var parsed = utils.parseGithub(url); 42 | la(check.object(parsed), parsed); 43 | la(parsed.user === 'bevacqua', 'invalid username', parsed); 44 | la(parsed.repo === 'es6', 'invalid repo', parsed); 45 | }); 46 | 47 | it('parses es6 .git repo url', function () { 48 | var url = 'git@github.com:bevacqua/es6.git'; 49 | var parsed = utils.parseGithub(url); 50 | la(check.object(parsed), parsed); 51 | la(parsed.user === 'bevacqua', 'invalid username', parsed); 52 | la(parsed.repo === 'es6', 'invalid repo', parsed); 53 | }); 54 | 55 | it('parses user/repo format', function () { 56 | var url = 'bevacqua/es6'; 57 | var parsed = utils.parseGithub(url); 58 | la(check.object(parsed), parsed); 59 | la(parsed.user === 'bevacqua', 'invalid username', parsed); 60 | la(parsed.repo === 'es6', 'invalid repo', parsed); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/get-readme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('debug')('manpm'); 4 | var la = require('lazy-ass'); 5 | var check = require('check-more-types'); 6 | var utils = require('./utils'); 7 | 8 | var Promise = require('bluebird'); 9 | var packageJson = require('package-json'); 10 | var simpleGet = require('simple-get'); 11 | 12 | function get(url) { 13 | return new Promise(function (resolve, reject) { 14 | log('getting from url', url); 15 | simpleGet.concat(url, function (err, data, res) { 16 | if (err) { 17 | log('simple get error from url', url, err); 18 | return reject(err); 19 | } 20 | if (res.statusCode !== 200) { 21 | log('while fetching from', url, 'got', res.statusCode); 22 | log(res); 23 | return reject(new Error('GET from ' + url + ' status ' + res.statusCode)); 24 | } 25 | return resolve(data); 26 | }); 27 | }); 28 | } 29 | 30 | var githubSchema = { 31 | user: check.unemptyString, 32 | repo: check.unemptyString 33 | }; 34 | var isValidGithubInfo = check.schema.bind(null, githubSchema); 35 | 36 | function toString(x) { 37 | return x.toString(); 38 | } 39 | 40 | function formGithubUrl(info, filename) { 41 | la(isValidGithubInfo(info), 'missing github info', info); 42 | la(check.unemptyString(filename), 'missing filename', filename); 43 | var fullUrl = 'https://raw.githubusercontent.com/' + info.user + 44 | '/' + info.repo + 45 | '/master/' + filename; 46 | return fullUrl; 47 | } 48 | 49 | function getReadmeFromGithub(name) { 50 | la(check.unemptyString(name), 'missing github info', name); 51 | log('getting readme directly from github for', name); 52 | var parsed = utils.parseGithub(name); 53 | la(isValidGithubInfo(parsed), parsed, 'from', name); 54 | 55 | var fullUrl = formGithubUrl(parsed, 'README.md'); 56 | la(check.unemptyString(fullUrl), 'missing url', fullUrl, 'from', parsed); 57 | log('fetching url from', fullUrl); 58 | 59 | return get(fullUrl) 60 | .catch(function () { 61 | // probably not found 62 | fullUrl = formGithubUrl(parsed, 'readme.md'); 63 | log('fetching url from', fullUrl); 64 | return get(fullUrl); 65 | }) 66 | .catch(function () { 67 | // probably not found 68 | fullUrl = formGithubUrl(parsed, 'readme.markdown'); 69 | log('fetching url from', fullUrl); 70 | return get(fullUrl); 71 | }) 72 | .then(toString); 73 | } 74 | 75 | function getLocalReadmeFile() { 76 | var fs = require('fs'); 77 | var join = require('path').join; 78 | var filename = join(process.cwd(), 'README.md'); 79 | return new Promise(function (resolve, reject) { 80 | if (!fs.existsSync(filename)) { 81 | return reject(new Error('Cannot find local file ' + filename)); 82 | } 83 | return resolve(fs.readFileSync(filename, 'utf8')); 84 | }); 85 | } 86 | 87 | function getReadme(name) { 88 | la(check.unemptyString(name), 'missing name'); 89 | 90 | if (name === '.') { 91 | log('fetching README in the current working folder'); 92 | return getLocalReadmeFile(); 93 | } 94 | 95 | if (utils.maybeGithubRepoName(name)) { 96 | log('fetching README for github repo', name); 97 | return getReadmeFromGithub(name); 98 | } 99 | if (utils.maybeGithubRepoUrl(name)) { 100 | log('fetching README for github url', name); 101 | return getReadmeFromGithub(name); 102 | } 103 | 104 | log('fetching README for NPM package', name); 105 | return packageJson(name, 'latest') 106 | .then(function (json) { 107 | log('repository', json.repository); 108 | if (!json.repository) { 109 | throw new Error('Cannot find repository for ' + name); 110 | } 111 | la(check.unemptyString(json.repository.url), 112 | 'missing url', json.repository); 113 | return getReadmeFromGithub(json.repository.url); 114 | }); 115 | } 116 | 117 | module.exports = getReadme; 118 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('debug')('manpm'); 4 | var la = require('lazy-ass'); 5 | var check = require('check-more-types'); 6 | var getReadme = require('./get-readme'); 7 | var findSection = require('./find-section'); 8 | la(check.fn(findSection), 'missing find section'); 9 | 10 | var marked = require('marked'); 11 | var TerminalMarkdown = require('marked-terminal'); 12 | marked.setOptions({ 13 | renderer: new TerminalMarkdown() 14 | }); 15 | 16 | function printMarkdown(md) { 17 | console.log(marked(md)); 18 | } 19 | 20 | function maNpm(options) { 21 | la(check.object(options), 'missing input options'); 22 | la(check.unemptyString(options.name), 'missing package name', options); 23 | 24 | return getReadme(options.name) 25 | .then(findSection.bind(null, options)) 26 | .then(printMarkdown) 27 | .catch(function (err) { 28 | console.error(err); 29 | log(err.stack); 30 | }); 31 | } 32 | 33 | module.exports = maNpm; 34 | 35 | if (!module.parent) { 36 | log('stand alone demo'); 37 | maNpm({ name: 'obind' }); 38 | } 39 | -------------------------------------------------------------------------------- /src/to-sections-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var marked = require('marked'); 4 | 5 | /* global describe, it */ 6 | 7 | var toTokens = marked.lexer.bind(marked); 8 | 9 | describe('to-sections', function () { 10 | var toSections = require('./to-sections'); 11 | 12 | var text = [ 13 | '# 1 is foo', 14 | 'foo is awesome', 15 | '# 2 is bar', 16 | 'bar is not as good' 17 | ].join('\n'); 18 | var tokens = toTokens(text); 19 | la(check.array(tokens)); 20 | 21 | it('is a function', function () { 22 | la(check.fn(toSections)); 23 | }); 24 | 25 | it('splits into 2 sections', function () { 26 | var sections = toSections(tokens); 27 | la(check.array(sections), sections); 28 | la(sections.length === 2, 'found 2 sections', sections); 29 | }); 30 | 31 | it('first section has 2 items', function () { 32 | var sections = toSections(tokens); 33 | var first = sections[0]; 34 | la(check.array(first), 'missing first section', first); 35 | la(first.length === 2, 'first has 2 tokens', first); 36 | }); 37 | 38 | it('first section has links', function () { 39 | var sections = toSections(tokens); 40 | var first = sections[0]; 41 | la(first.links, 'first section is missing links', first); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/to-sections.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | 4 | function toSections(tokens) { 5 | la(check.array(tokens), 'expected markdown tokens list', tokens); 6 | 7 | var foundSections = [], current; 8 | tokens.forEach(function (token) { 9 | if (token.type === 'heading') { 10 | if (current) { 11 | foundSections.push(current); 12 | } 13 | current = [token]; 14 | return; 15 | } else if (current) { 16 | current.push(token); 17 | } 18 | }); 19 | if (current) { 20 | foundSections.push(current); 21 | } 22 | 23 | foundSections.forEach(function (section) { 24 | section.links = tokens.links; 25 | }); 26 | return foundSections; 27 | } 28 | 29 | module.exports = toSections; 30 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var log = require('debug')('manpm'); 2 | var la = require('lazy-ass'); 3 | var check = require('check-more-types'); 4 | var parseGithubRepoUrl = require('@bahmutov/parse-github-repo-url'); 5 | 6 | // TODO move to kensho/check-more-types 7 | function maybeGithubRepoName(name) { 8 | var regular = /^[a-zA-Z0-9]+\/[a-zA-Z0-9\-\.]+$/; 9 | return regular.test(name); 10 | } 11 | 12 | function maybeGithubRepoUrl(name) { 13 | return Array.isArray(parseGithubRepoUrl(name)); 14 | } 15 | 16 | function parseGithub(url) { 17 | log('parsing github url', url); 18 | var parsed = parseGithubRepoUrl(url); 19 | la(check.array(parsed), 'could not parse github url', url); 20 | return { 21 | user: parsed[0], 22 | repo: parsed[1] 23 | }; 24 | } 25 | 26 | module.exports = { 27 | maybeGithubRepoName: maybeGithubRepoName, 28 | maybeGithubRepoUrl: maybeGithubRepoUrl, 29 | parseGithub: parseGithub 30 | }; 31 | --------------------------------------------------------------------------------