├── .eslintrc ├── .gitignore ├── .jscsrc ├── .npmrc ├── .travis.yml ├── README.md ├── bin └── update-markdown.js ├── example ├── input.md └── section.md ├── index.js ├── package.json └── spec └── replace-section-spec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [ 16 | 2, 17 | "always" 18 | ] 19 | }, 20 | "env": { 21 | "node": true, 22 | "browser": false 23 | }, 24 | "extends": "eslint:recommended" 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | modules-used.md 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "yandex", 3 | "validateIndentation": 2, 4 | "disallowMultipleVarDecl": { 5 | "allExcept": ["undefined"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | - "5" 6 | sudo: false 7 | branches: 8 | only: 9 | - master 10 | - /^fix-.*$/ 11 | - /^issue-.*$/ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # update-markdown 2 | > Updates part of existing markdown document 3 | 4 | [![update-markdown-icon](https://nodei.co/npm/update-markdown.png?downloads=true)](https://nodei.co/npm/update-markdown.png?downloads=true) 5 | 6 | [![Build status](https://travis-ci.org/bahmutov/update-markdown.svg?branch=master) ](https://travis-ci.org/bahmutov/update-markdown) 7 | [![dependencies](https://david-dm.org/bahmutov/update-markdown.svg) ](https://david-dm.org/bahmutov/update-markdown) 8 | [![devdependencies](https://david-dm.org/bahmutov/update-markdown/dev-status.svg) ](https://david-dm.org/bahmutov/update-markdown#info=devDependencies) 9 | 10 | ## install and use 11 | ``` 12 | npm install -g update-markdown 13 | ``` 14 | If a file `filename.md` exists with the following contents 15 | 16 | ``` 17 | # title 18 | some text 19 | ## foo 20 | this is foo 21 | ## bar 22 | this is bar 23 | ``` 24 | and another file `new.md` with new text for section `## foo` we can replace it using 25 | 26 | ``` 27 | cat new.md | um filename.md "## foo" 28 | ``` 29 | updating the `filename.md` to have 30 | 31 | ``` 32 | # title 33 | some text 34 | ## foo 35 | 36 | ## bar 37 | this is bar 38 | ``` 39 | You can also specify new content from another markdown file, same command as above will be 40 | 41 | ``` 42 | um filename.md "## foo" new.md 43 | ``` 44 | Want to see it in action? Check out the list of 3rd party modules below - we are generating it using 45 | the [modules-used](https://github.com/bahmutov/modules-used) tool in build script in the [package.json](package.json). 46 | 47 | ```json 48 | "scripts": { 49 | "3rd-party": "modules-used | um README.md '### 3rd party modules'" 50 | } 51 | ``` 52 | ### 3rd party modules 53 | 54 | - [bluebird](https://github.com/petkaantonov/bluebird) - Full featured Promises/A+ implementation with exceptionally good performance 55 | - [check-more-types](https://github.com/kensho/check-more-types) - Large collection of predicates 56 | - [debug](https://github.com/visionmedia/debug) - small debugging utility 57 | - [get-stdin-promise](https://github.com/metaraine/get-stdin-promise) - Return stdin as a promise 58 | - [lazy-ass](https://github.com/bahmutov/lazy-ass) - Lazy assertions without performance penalty 59 | - [lodash](https://lodash.com/) - The modern build of lodash modular utilities. 60 | - [marked](https://github.com/chjj/marked) - A markdown parser built for speed 61 | - [marked-to-md](https://github.com/partageit/marked-to-md) - Markdown renderer for marked 62 | 63 | ### Small print 64 | Author: Gleb Bahmutov © 2015 65 | 66 | 67 | - [@bahmutov](https://twitter.com/bahmutov) 68 | - [glebbahmutov.com](http://glebbahmutov.com) 69 | - [blog](http://glebbahmutov.com/blog/) 70 | 71 | License: MIT - do anything with the code, but don't blame me if it does not work. 72 | 73 | Spread the word: tweet, star on github, etc. 74 | 75 | Support: if you find any problems with this module, email / tweet / 76 | [open issue](https://github.com/bahmutov/update-markdown/issues) on Github 77 | 78 | ## MIT License 79 | Copyright (c) 2015 Gleb Bahmutov 80 | 81 | Permission is hereby granted, free of charge, to any person 82 | obtaining a copy of this software and associated documentation 83 | files (the "Software"), to deal in the Software without 84 | restriction, including without limitation the rights to use, 85 | copy, modify, merge, publish, distribute, sublicense, and/or sell 86 | copies of the Software, and to permit persons to whom the 87 | Software is furnished to do so, subject to the following 88 | conditions: 89 | 90 | The above copyright notice and this permission notice shall be 91 | included in all copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 94 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 95 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 96 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 97 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 98 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 99 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 100 | OTHER DEALINGS IN THE SOFTWARE. 101 | 102 | -------------------------------------------------------------------------------- /bin/update-markdown.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var log = require('debug')('um'); 4 | var Promise = require('bluebird'); 5 | var updateMarkdown = require('..'); 6 | 7 | /* eslint no-console:0 */ 8 | 9 | function getCliOptions(argv) { 10 | switch (argv.length) { 11 | case 2: 12 | console.error('um
'); 13 | console.error('for example: um some-file.md "## foo"'); 14 | console.error('updates section with title "## foo" inside file "some-filemd"'); 15 | process.exit(-1); 16 | break; 17 | case 4: 18 | // text comes from STDIN 19 | return { 20 | filename: process.argv[2], 21 | title: process.argv[3] 22 | }; 23 | case 5: 24 | return { 25 | filename: process.argv[2], 26 | title: process.argv[3], 27 | textFilename: process.argv[4] 28 | }; 29 | default: 30 | console.error('Do not know how to handle arguments'); 31 | console.error(argv.slice(2)); 32 | process.exit(-1); 33 | } 34 | } 35 | 36 | function getNewText(options) { 37 | var read = require('fs').readFileSync; 38 | if (!options.text && options.textFilename) { 39 | log('reading new contents from file %s', options.textFilename); 40 | return Promise.resolve(read(options.textFilename, 'utf-8')); 41 | } 42 | 43 | log('reading new contents from STDIN'); 44 | return Promise.resolve(require('get-stdin-promise')); 45 | } 46 | 47 | var options = getCliOptions(process.argv); 48 | log('cli options', options); 49 | 50 | getNewText(options) 51 | .then(function (text) { 52 | options.text = text; 53 | updateMarkdown(options); 54 | }) 55 | .done(); 56 | -------------------------------------------------------------------------------- /example/input.md: -------------------------------------------------------------------------------- 1 | # title 2 | some text with link to [update-markdown](https://github.com/bahmutov/update-markdown) 3 | 4 | ## bar 5 | this is bar 6 | 7 | ## foo 8 | new content 9 | is here 10 | 11 | -------------------------------------------------------------------------------- /example/section.md: -------------------------------------------------------------------------------- 1 | new content 2 | is here 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var log = require('debug')('um'); 2 | var la = require('lazy-ass'); 3 | var check = require('check-more-types'); 4 | var read = require('fs').readFileSync; 5 | var write = require('fs').writeFileSync; 6 | var _ = require('lodash'); 7 | 8 | var marked = require('marked'); 9 | var mdRenderer = require('marked-to-md'); 10 | var renderer = mdRenderer(new marked.Renderer()); 11 | var parser = new marked.Parser({renderer: renderer}); 12 | 13 | // '## bar' -> 'bar' 14 | function headerText(text) { 15 | la(check.unemptyString(text), 'missing text', text); 16 | var tokens = marked.lexer(text); 17 | la(tokens.length, 'missing tokens', tokens, 'from', text); 18 | return tokens[0].text; 19 | } 20 | 21 | function isHeading(token) { 22 | return token && token.type === 'heading'; 23 | } 24 | 25 | // returns indices, both could be -1 or the end could be -1 26 | function findSection(tokens, heading) { 27 | var text = headerText(heading); 28 | la(check.unemptyString(text), 'could not extract text from heading', heading); 29 | 30 | var headerIndex = _.findIndex(tokens, {type: 'heading', text: text}); 31 | if (headerIndex === -1) { 32 | throw new Error('Could not find header text ' + heading); 33 | } 34 | var stopIndex = _.findIndex(tokens.slice(headerIndex + 1), isHeading); 35 | if (stopIndex !== -1) { 36 | stopIndex = headerIndex + 1 + stopIndex; 37 | } 38 | log('replacing section %s between indices %d to %d', heading, headerIndex, stopIndex); 39 | 40 | return { 41 | from: headerIndex + 1, 42 | to: stopIndex 43 | }; 44 | } 45 | 46 | function hasStart(indices) { 47 | return indices.from !== -1; 48 | } 49 | 50 | function hasNoEnd(indices) { 51 | return indices.to === -1; 52 | } 53 | 54 | function updateTokens(tokens, newText, indices) { 55 | la(check.object(indices), 'missing indices'); 56 | la(hasStart(indices), 'no start index', indices); 57 | 58 | var newTokens = marked.lexer(newText); 59 | var links = _.assign({}, tokens.links, newTokens.links); 60 | 61 | if (hasNoEnd(indices)) { 62 | tokens.splice(indices.from, tokens.length); 63 | tokens = tokens.concat(newTokens); 64 | } else { 65 | la(indices.to >= indices.from, 'invalid indices', indices); 66 | tokens.splice.apply(tokens, 67 | [indices.from, indices.to - indices.from].concat(newTokens)); 68 | } 69 | tokens.links = links; 70 | 71 | return tokens; 72 | } 73 | 74 | function replaceSection(tokens, heading, newText) { 75 | la(check.unemptyString(newText), 'missing new text'); 76 | 77 | var indices = findSection(tokens, heading); 78 | la(check.object(indices), 'could not find section', heading); 79 | return updateTokens(tokens, newText, indices); 80 | } 81 | 82 | function updateMarkdownWith(title, markdownText, replacement) { 83 | la(check.unemptyString(title), 'missing section title'); 84 | la(check.unemptyString(markdownText), 'missing markdown text'); 85 | la(check.unemptyString(replacement), 'missing replacement text', replacement); 86 | 87 | var tokens = marked.lexer(markdownText); 88 | log('split source markdown into %d tokens', tokens.length); 89 | 90 | var updatedTokens = replaceSection(tokens, title, replacement); 91 | la(check.array(updatedTokens), 'could not update tokens', updatedTokens); 92 | 93 | var updatedText = parser.parse(updatedTokens); 94 | return updatedText; 95 | } 96 | 97 | function updatedContent(options) { 98 | 99 | la(check.unemptyString(options.filename), 'missing filename', options); 100 | var text = read(options.filename, 'utf-8'); 101 | la(check.unemptyString(text), 'empty text in file', options.filename, text); 102 | log('read file %s', options.filename); 103 | 104 | var updatedText = updateMarkdownWith(options.title, text, options.text); 105 | return updatedText; 106 | } 107 | 108 | function updateMarkdown(options) { 109 | log('update markdown options', options); 110 | la(check.object(options), 'missing options', options); 111 | 112 | var updatedText = updatedContent(options); 113 | log('writing updatedText markdown to %s', options.filename); 114 | 115 | write(options.filename, updatedText); 116 | return updatedText; 117 | } 118 | 119 | module.exports = updateMarkdown; 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "update-markdown", 3 | "version": "1.1.3", 4 | "description": "Updates part of existing markdown document", 5 | "main": "index.js", 6 | "bin": { 7 | "um": "./bin/update-markdown.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha spec/*-spec.js", 11 | "test-all": "npm test && npm run example && npm run cat-example && npm run 3rd-party", 12 | "style": "jscs *.js bin/*.js spec/*.js --fix", 13 | "example": "DEBUG=um ./bin/update-markdown.js example/input.md '## foo' example/section.md", 14 | "cat-example": "cat example/section.md | DEBUG=um ./bin/update-markdown.js example/input.md '## foo'", 15 | "3rd-party": "modules-used | ./bin/update-markdown.js README.md '### 3rd party modules'", 16 | "next-update": "next-update -k true", 17 | "commit": "git-issues && commit-wizard", 18 | "issues": "git-issues", 19 | "lint": "eslint bin/*.js index.js spec/*.js", 20 | "pkgfiles": "pkgfiles", 21 | "size": "tarball=\"$(npm pack .)\"; wc -c \"${tarball}\"; tar tvf \"${tarball}\"; rm \"${tarball}\";" 22 | }, 23 | "files": [ 24 | "bin", 25 | "index.js" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/bahmutov/update-markdown.git" 30 | }, 31 | "keywords": [ 32 | "markdown", 33 | "md", 34 | "update", 35 | "cli" 36 | ], 37 | "author": "Gleb Bahmutov ", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/bahmutov/update-markdown/issues" 41 | }, 42 | "homepage": "https://github.com/bahmutov/update-markdown#readme", 43 | "dependencies": { 44 | "bluebird": "3.0.5", 45 | "check-more-types": "2.2.0", 46 | "debug": "2.2.0", 47 | "get-stdin-promise": "0.1.1", 48 | "lazy-ass": "1.1.0", 49 | "lodash": "3.10.1", 50 | "marked": "0.3.5", 51 | "marked-to-md": "1.0.1" 52 | }, 53 | "devDependencies": { 54 | "describe-it": "1.7.0", 55 | "eslint": "1.10.1", 56 | "git-issues": "1.2.0", 57 | "jscs": "2.6.0", 58 | "mocha": "2.3.4", 59 | "modules-used": "1.2.0", 60 | "next-update": "0.9.5", 61 | "pkgfiles": "2.3.0", 62 | "pre-git": "1.3.1" 63 | }, 64 | "config": { 65 | "pre-git": { 66 | "commit-msg": "validate-commit-msg", 67 | "pre-commit": [ 68 | "npm run style", 69 | "npm run lint", 70 | "npm run example", 71 | "npm run cat-example", 72 | "npm run 3rd-party" 73 | ], 74 | "pre-push": [ 75 | "npm run size", 76 | "npm run pkgfiles" 77 | ], 78 | "post-commit": [ 79 | "npm version" 80 | ], 81 | "post-merge": [] 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /spec/replace-section-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | var describeIt = require('describe-it'); 4 | var _ = require('lodash'); 5 | var join = require('path').join; 6 | 7 | var filename = join(__dirname, '..', 'index.js'); 8 | 9 | /* global describe, it, beforeEach */ 10 | describeIt(filename, 'updateMarkdownWith(title, markdownText, replacement)', function () { 11 | 12 | it('is a function', function () { 13 | la(check.fn(this.updateMarkdownWith)); 14 | }); 15 | 16 | describe('without links', function () { 17 | var source = ['# foo', 18 | 'this is a section', 19 | '# bar', 20 | 'another section' 21 | ].join('\n'); 22 | 23 | var replacement = 'replaced second section'; 24 | 25 | it('updates a section of markdown text', function () { 26 | var updated = this.updateMarkdownWith('bar', source, replacement); 27 | la(check.unemptyString(updated)); 28 | la(updated.indexOf('another') === -1, 'removed second section', updated); 29 | la(updated.indexOf('replaced') > 0, 'found new text', updated); 30 | }); 31 | }); 32 | 33 | describe('with inlined links', function () { 34 | var source = ['# foo', 35 | 'this is a section', 36 | '# bar', 37 | 'another section' 38 | ].join('\n'); 39 | 40 | var replacement = 'replaced [second](https://github.com) section'; 41 | 42 | it('updates a section of markdown text', function () { 43 | var updated = this.updateMarkdownWith('bar', source, replacement); 44 | la(check.unemptyString(updated)); 45 | la(updated.indexOf('another') === -1, 'removed second section', updated); 46 | la(updated.indexOf('replaced') > 0, 'found new text', updated); 47 | }); 48 | 49 | it('brings links from new text', function () { 50 | la(source.indexOf('github.com') === -1, 51 | 'source does not have this link at first', source); 52 | 53 | var updated = this.updateMarkdownWith('bar', source, replacement); 54 | la(updated.indexOf('github.com') > 0, 'found new link', updated); 55 | }); 56 | }); 57 | 58 | describe('with separate links', function () { 59 | var source = ['# foo', 60 | 'this is a section', 61 | '# bar', 62 | 'another section' 63 | ].join('\n'); 64 | 65 | var replacement = ['replaced [second][second] section', 66 | '', 67 | '[second]: https://github.com' 68 | ].join('\n'); 69 | 70 | it('updates a section of markdown text', function () { 71 | var updated = this.updateMarkdownWith('bar', source, replacement); 72 | la(check.unemptyString(updated)); 73 | la(updated.indexOf('another') === -1, 'removed second section', updated); 74 | la(updated.indexOf('replaced') > 0, 'found new text', updated); 75 | la(updated.indexOf('second') > 0, 'found new link title', updated); 76 | }); 77 | 78 | it('brings links from new text', function () { 79 | la(source.indexOf('github.com') === -1, 80 | 'source does not have this link at first', source); 81 | 82 | var updated = this.updateMarkdownWith('bar', source, replacement); 83 | la(updated.indexOf('github.com') > 0, 'found new link\n' + updated); 84 | }); 85 | }); 86 | }); 87 | 88 | describeIt(filename, 'headerText(text)', function (codeExtract) { 89 | var headerText; 90 | beforeEach(function () { 91 | headerText = codeExtract(); 92 | la(check.fn(headerText)); 93 | }); 94 | 95 | it('extracts just the title', function () { 96 | var text = headerText('## bar'); 97 | la(text === 'bar', 'could not get bar', text); 98 | }); 99 | 100 | it('works with 3rd level', function () { 101 | var text = headerText('### foo bar - baz'); 102 | la(text === 'foo bar - baz', 'could not get text', text); 103 | }); 104 | }); 105 | 106 | describeIt(filename, 'replaceSection(tokens, heading, newText)', function (codeExtract) { 107 | var tokens, replaceSection; 108 | var newText = 'new text'; 109 | 110 | beforeEach(function () { 111 | replaceSection = codeExtract(); 112 | la(check.fn(replaceSection)); 113 | tokens = [ 114 | {type: 'heading', depth: 1, text: 'title'}, 115 | {type: 'paragraph', text: 'some text'}, 116 | {type: 'heading', depth: 2, text: 'foo'}, 117 | {type: 'paragraph', text: 'this is foo'}, 118 | {type: 'heading', depth: 2, text: 'bar'}, 119 | {type: 'paragraph', text: 'this is bar'} 120 | ]; 121 | }); 122 | 123 | function hasNewText(tokenList) { 124 | return _.findIndex(tokenList, {text: newText}) !== -1; 125 | } 126 | 127 | beforeEach(function () { 128 | la(!hasNewText(tokens)); 129 | }); 130 | 131 | it('replaces middle section with given heading', function () { 132 | var heading = 'foo'; 133 | var replaced = replaceSection(tokens, heading, newText); 134 | la(check.array(replaced), 'returns an array'); 135 | la(hasNewText(replaced), 'has updated tokens', replaced); 136 | }); 137 | 138 | it('replaces last section with given heading', function () { 139 | var heading = 'bar'; 140 | var replaced = replaceSection(tokens, heading, newText); 141 | la(check.array(replaced), 'returns an array'); 142 | la(hasNewText(replaced), 'has updated tokens', replaced); 143 | }); 144 | 145 | }); 146 | --------------------------------------------------------------------------------