├── tests ├── fixtures │ ├── foo.txt │ ├── .gitignore │ ├── include_test_weight_10.txt │ ├── badge │ │ ├── bower.json │ │ └── package.json │ └── filesize.txt ├── dummy_git │ ├── HEAD │ ├── refs │ │ └── .gitignore │ ├── objects │ │ └── .gitignore │ └── config ├── dummy_git_untracked_head │ ├── refs │ │ └── .gitignore │ ├── objects │ │ └── .gitignore │ ├── HEAD │ └── config ├── .eslintrc.json ├── helpers │ ├── date.js │ ├── include.js │ ├── gitinfo.js │ ├── variable.js │ ├── filesize.js │ ├── contents.js │ └── badge.js ├── locator.js ├── parser.js └── gitdown.js ├── .github └── FUNDING.yml ├── .npmignore ├── src ├── helpers │ ├── test.js │ ├── deadlink.js │ ├── date.js │ ├── variable.js │ ├── include.js │ ├── gitinfo.js │ ├── filesize.js │ ├── contents.js │ └── badge.js ├── index.js ├── locator.js ├── bin │ └── index.js ├── parser.js └── gitdown.js ├── .editorconfig ├── .gitignore ├── .babelrc.json ├── .travis.yml ├── .eslintrc.json ├── .README ├── helpers │ ├── include.md │ ├── deadlink.md │ ├── date.md │ ├── filesize.md │ ├── contents.md │ ├── variable.md │ ├── heading-nesting.md │ ├── anchor.md │ ├── gitinfo.md │ └── badge.md ├── usage.md └── README.md ├── LICENSE ├── package.json └── README.md /tests/fixtures/foo.txt: -------------------------------------------------------------------------------- 1 | bar -------------------------------------------------------------------------------- /tests/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | write.txt -------------------------------------------------------------------------------- /tests/dummy_git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /tests/dummy_git/refs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gajus 2 | patreon: gajus 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /tests/dummy_git/objects/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixtures/include_test_weight_10.txt: -------------------------------------------------------------------------------- 1 | {"gitdown": "test"} -------------------------------------------------------------------------------- /tests/dummy_git_untracked_head/refs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixtures/badge/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitdown" 3 | } -------------------------------------------------------------------------------- /tests/fixtures/badge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitdown" 3 | } -------------------------------------------------------------------------------- /tests/dummy_git_untracked_head/objects/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/dummy_git_untracked_head/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/some-untracked-branch 2 | -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "parserOptions": { 6 | "sourceType": "module" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/test.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const test = {}; 4 | test.compile = _.constant('test'); 5 | 6 | test.weight = 10; 7 | 8 | export default test; 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Gitdown from './gitdown.js'; 2 | import Parser from './parser.js'; 3 | 4 | Gitdown.Parser = Parser; 5 | 6 | export { 7 | default, 8 | } from './gitdown.js'; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /tests/fixtures/filesize.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. -------------------------------------------------------------------------------- /src/helpers/deadlink.js: -------------------------------------------------------------------------------- 1 | const deadlink = {}; 2 | 3 | deadlink.compile = () => { 4 | throw new Error('This helper cannot be called from the context of the markdown document.'); 5 | }; 6 | 7 | deadlink.weight = 100; 8 | 9 | export default deadlink; 10 | -------------------------------------------------------------------------------- /src/helpers/date.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | const date = {}; 4 | date.compile = (config = {}) => { 5 | config.format = config.format || 'X'; 6 | 7 | return moment().format(config.format); 8 | }; 9 | 10 | date.weight = 10; 11 | 12 | export default date; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | package-lock.json 5 | pnpm-lock.yaml 6 | *.log 7 | .* 8 | !.babelrc.json 9 | !.editorconfig 10 | !.eslintignore 11 | !.eslintrc.json 12 | !.flowconfig 13 | !.gitignore 14 | !.npmignore 15 | !.travis.yml 16 | !.README 17 | !.ncurc.js 18 | -------------------------------------------------------------------------------- /tests/dummy_git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | [remote "origin"] 9 | url = git@github.com:foo/bar.git 10 | fetch = +refs/heads/*:refs/remotes/origin/* 11 | [branch "master"] 12 | remote = origin 13 | merge = refs/heads/master 14 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | "istanbul" 6 | ] 7 | } 8 | }, 9 | "plugins": [ 10 | "@babel/transform-flow-strip-types" 11 | ], 12 | "presets": [ 13 | [ 14 | "@babel/env", 15 | { 16 | "targets": { 17 | "node": "10" 18 | } 19 | } 20 | ] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/dummy_git_untracked_head/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | [remote "origin"] 9 | url = git@github.com:foo/bar.git 10 | fetch = +refs/heads/*:refs/remotes/origin/* 11 | [branch "master"] 12 | remote = origin 13 | merge = refs/heads/master 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 14 5 | - 12 6 | - 10 7 | script: 8 | - npm run lint 9 | - npm run test 10 | - nyc --silent npm run test 11 | - nyc report --reporter=text-lcov | coveralls 12 | - nyc check-coverage --lines 60 13 | after_success: 14 | - NODE_ENV=production npm run build 15 | - semantic-release 16 | notifications: 17 | email: false 18 | sudo: false 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "requireConfigFile": false 6 | }, 7 | "env": { 8 | "node": true 9 | }, 10 | "rules": { 11 | "default-param-last": 0, 12 | "filenames/match-exported": 0, 13 | "import/no-commonjs": 0, 14 | "import/no-dynamic-require": 0, 15 | "import/unambiguous": 0, 16 | "import/extensions": 0, 17 | "no-use-extend-native/no-use-extend-native": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.README/helpers/include.md: -------------------------------------------------------------------------------- 1 | ### Include File 2 | 3 | 4 | ```json 5 | {"gitdown": "include"} 6 | 7 | ``` 8 | 9 | 10 | Includes the contents of the file to the document. 11 | 12 | The included file can have Gitdown JSON hooks. 13 | 14 | #### Example 15 | 16 | See source code of [./.README/README.md](https://github.com/gajus/gitdown/blob/master/.README/README.md). 17 | 18 | #### JSON Configuration 19 | 20 | | Name | Description | Default | 21 | | --- | --- | --- | 22 | | `file` | Path to the file. The path is relative to the root of the repository. | N/A | 23 | -------------------------------------------------------------------------------- /src/helpers/variable.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | const variable = {}; 4 | variable.compile = (config = {}, context) => { 5 | const scope = context.gitdown.getConfig().variable.scope; 6 | 7 | if (!config.name) { 8 | throw new Error('config.name must be provided.'); 9 | } 10 | 11 | const magicUndefined = 'undefined-' + Math.random(); 12 | const value = _.get(scope, config.name, magicUndefined); 13 | 14 | if (value === magicUndefined) { 15 | throw new Error('config.name "' + config.name + '" does not resolve to a defined value.'); 16 | } 17 | 18 | return value; 19 | }; 20 | 21 | variable.weight = 10; 22 | 23 | export default variable; 24 | -------------------------------------------------------------------------------- /.README/helpers/deadlink.md: -------------------------------------------------------------------------------- 1 | ### Find Dead URLs and Fragment Identifiers 2 | 3 | Uses [Deadlink](https://github.com/gajus/deadlink) to iterate through all of the URLs in the resulting document. Throws an error if either of the URLs is resolved with an HTTP status other than 200 or fragment identifier (anchor) is not found. 4 | 5 | #### Parser Configuration 6 | 7 | | Name | Description | Default | 8 | | --- | --- | --- | 9 | | `deadlink.findDeadURLs` | Find dead URLs. | `false` | 10 | | `deadlink.findDeadFragmentIdentifiers` | Find dead fragment identifiers. | `false` | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/helpers/date.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | 5 | const importFresh = (moduleName) => { 6 | return import(`${moduleName}?${Date.now()}`); 7 | }; 8 | 9 | describe('Parser.helpers.date', () => { 10 | let helper; 11 | 12 | beforeEach(async () => { 13 | helper = (await importFresh('../../src/helpers/date.js')).default; 14 | }); 15 | it('returns current UNIX timestamp', () => { 16 | expect(helper.compile()).to.equal(String(Math.floor(Date.now() / 1_000))); 17 | }); 18 | it('uses format parameter to adjust the format', () => { 19 | expect(helper.compile({ 20 | format: 'YYYY', 21 | })).to.equal(String(new Date().getUTCFullYear())); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /.README/helpers/date.md: -------------------------------------------------------------------------------- 1 | ### Print Date 2 | 3 | 4 | ```json 5 | {"gitdown": "date"} 6 | 7 | ``` 8 | 9 | 10 | Prints a string formatted according to the given [moment format](http://momentjs.com/docs/#/displaying/format/) string using the current time. 11 | 12 | #### Example 13 | 14 | 15 | ```json 16 | {"gitdown": "date"} 17 | {"gitdown": "date", "format": "YYYY"} 18 | 19 | ``` 20 | 21 | 22 | Generates: 23 | 24 | ```markdown 25 | 1563038327 26 | 2019 27 | 28 | ``` 29 | 30 | #### JSON Configuration 31 | 32 | | Name | Description | Default | 33 | | --- | --- | --- | 34 | | `format` | [Moment format](http://momentjs.com/docs/#/displaying/format/). | `X` (UNIX timestamp) | 35 | -------------------------------------------------------------------------------- /src/helpers/include.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const include = {}; 5 | 6 | /** 7 | * @typedef config 8 | * @property {string} file Path to a file. 9 | */ 10 | 11 | /** 12 | * @param {config} config 13 | * @param {object} context 14 | */ 15 | include.compile = (config = {}, context) => { 16 | if (!config.file) { 17 | throw new Error('config.file must be provided.'); 18 | } 19 | 20 | config.file = path.resolve(context.gitdown.getConfig().baseDirectory, config.file); 21 | 22 | if (!fs.existsSync(config.file)) { 23 | // eslint-disable-next-line no-console 24 | console.log('config.file', config.file); 25 | 26 | throw new Error('Input file does not exist.'); 27 | } 28 | 29 | return fs.readFileSync(config.file, { 30 | encoding: 'utf8', 31 | }); 32 | }; 33 | 34 | include.weight = 20; 35 | 36 | export default include; 37 | -------------------------------------------------------------------------------- /.README/helpers/filesize.md: -------------------------------------------------------------------------------- 1 | ### Get File Size 2 | 3 | 4 | ```json 5 | {"gitdown": "filesize"} 6 | 7 | ``` 8 | 9 | 10 | Returns file size formatted in human friendly format. 11 | 12 | #### Example 13 | 14 | 15 | ```json 16 | {"gitdown": "filesize", "file": "src/gitdown.js"} 17 | {"gitdown": "filesize", "file": "src/gitdown.js", "gzip": true} 18 | 19 | ``` 20 | 21 | 22 | Generates: 23 | 24 | ```markdown 25 | {"gitdown": "filesize", "file": "src/gitdown.js"} 26 | {"gitdown": "filesize", "file": "src/gitdown.js", "gzip": true} 27 | 28 | ``` 29 | 30 | #### JSON Configuration 31 | 32 | | Name | Description | Default | 33 | | --- | --- | --- | 34 | | `file` | Path to the file. The path is relative to the root of the repository. | N/A | 35 | | `gzip` | A boolean value indicating whether to gzip the file first. | `false` | 36 | -------------------------------------------------------------------------------- /.README/helpers/contents.md: -------------------------------------------------------------------------------- 1 | ### Generate Table of Contents 2 | 3 | 4 | ```json 5 | {"gitdown": "contents"} 6 | 7 | ``` 8 | 9 | 10 | Generates table of contents. 11 | 12 | The table of contents is generated using [markdown-contents](https://github.com/gajus/markdown-contents). 13 | 14 | #### Example 15 | 16 | 17 | ```json 18 | {"gitdown": "contents", "maxLevel": 4, "rootId": "features"} 19 | 20 | ``` 21 | 22 | 23 | ```markdown 24 | {"gitdown": "contents", "maxLevel": 4, "rootId": "features"} 25 | 26 | ``` 27 | 28 | #### JSON Configuration 29 | 30 | | Name | Description | Default | 31 | | --- | --- | --- | 32 | | `maxLevel` | The maximum heading level after which headings are excluded. | 3 | 33 | | `rootId` | ID of the root heading. Provide it when you need table of contents for a specific section of the document. Throws an error if element with the said ID does not exist in the document. | N/A | 34 | -------------------------------------------------------------------------------- /tests/locator.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | import fs from 'fs'; 5 | import Path from 'path'; 6 | import { 7 | fileURLToPath, 8 | } from 'url'; 9 | 10 | const dirname = Path.dirname(fileURLToPath(import.meta.url)); 11 | const importFresh = (moduleName) => { 12 | return import(`${moduleName}?${Date.now()}`); 13 | }; 14 | 15 | xdescribe('Locator', () => { 16 | let Locator; 17 | 18 | beforeEach(async () => { 19 | Locator = (await importFresh('../src/locator.js')).Parser; 20 | }); 21 | describe('.gitPath()', () => { 22 | it('returns absolute path to the .git/ directory', () => { 23 | expect(Locator.gitPath()).to.equal(fs.realpathSync(Path.resolve(dirname, './../.git'))); 24 | }); 25 | }); 26 | describe('.repositoryPath()', () => { 27 | it('returns absolute path to the parent of the _getGitPath() directory', () => { 28 | expect(Locator.repositoryPath()).to.equal(fs.realpathSync(Locator.gitPath() + '/..')); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /.README/helpers/variable.md: -------------------------------------------------------------------------------- 1 | ### Variables 2 | 3 | 4 | ```json 5 | {"gitdown": "variable"} 6 | 7 | ``` 8 | 9 | 10 | Prints the value of a property defined under a parser `variable.scope` configuration property. Throws an error if property is not set. 11 | 12 | #### Example 13 | 14 | 15 | ```js 16 | const gitdown = Gitdown( 17 | '{"gitdown": "variable", "name": "name.first"}' + 18 | '{"gitdown": "variable", "name": "name.last"}' 19 | ); 20 | 21 | gitdown.setConfig({ 22 | variable: { 23 | scope: { 24 | name: { 25 | first: "Gajus", 26 | last: "Kuizinas" 27 | } 28 | } 29 | } 30 | }); 31 | 32 | ``` 33 | 34 | 35 | #### JSON Configuration 36 | 37 | | Name | Description | Default | 38 | | --- | --- | --- | 39 | | `name` | Name of the property defined under a parser `variable.scope` configuration property. | N/A | 40 | 41 | #### Parser Configuration 42 | 43 | | Name | Description | Default | 44 | | --- | --- | --- | 45 | | `variable.scope` | Variable scope object. | `{}` | 46 | -------------------------------------------------------------------------------- /.README/helpers/heading-nesting.md: -------------------------------------------------------------------------------- 1 | ### Heading Nesting 2 | 3 | Github markdown processor generates heading ID based on the text of the heading. 4 | 5 | The conflicting IDs are solved with a numerical suffix, e.g. 6 | 7 | ```markdown 8 | # Foo 9 | ## Something 10 | # Bar 11 | ## Something 12 | 13 | ``` 14 | 15 | ```html 16 |

Foo

17 |

Something

18 |

Bar

19 |

Something

20 | 21 | ``` 22 | 23 | The problem with this approach is that it makes the order of the content important. 24 | 25 | Gitdown will nest the headings using parent heading names to ensure uniqueness, e.g. 26 | 27 | ```markdown 28 | # Foo 29 | ## Something 30 | # Bar 31 | ## Something 32 | 33 | ``` 34 | 35 | ```html 36 |

Foo

37 |

Something

38 |

Bar

39 |

Something

40 | 41 | ``` 42 | 43 | #### Parser Configuration 44 | 45 | | Name | Description | Default | 46 | | --- | --- | --- | 47 | | `headingNesting.enabled` | Boolean flag indicating whether to nest headings. | `true` | 48 | -------------------------------------------------------------------------------- /src/helpers/gitinfo.js: -------------------------------------------------------------------------------- 1 | import createGitinfo from 'gitinfo'; 2 | import _ from 'lodash'; 3 | 4 | const gitinfo = {}; 5 | gitinfo.compile = (config, context) => { 6 | const parserConfig = context.gitdown.getConfig().gitinfo; 7 | const gitInfo = createGitinfo.default({ 8 | gitPath: parserConfig.gitPath, 9 | ...parserConfig.defaultBranchName && { 10 | defaultBranchName: parserConfig.defaultBranchName, 11 | }, 12 | }); 13 | 14 | const methodMap = { 15 | branch: 'getBranchName', 16 | name: 'getName', 17 | url: 'getGithubUrl', 18 | username: 'getUsername', 19 | }; 20 | 21 | if (!config.name) { 22 | throw new Error('config.name must be provided.'); 23 | } 24 | 25 | if (!methodMap[config.name]) { 26 | throw new Error('Unexpected config.name value ("' + config.name + '").'); 27 | } 28 | 29 | if (!_.isFunction(gitInfo[methodMap[config.name]])) { 30 | throw new TypeError('Gitinfo module does not provide function "' + config.name + '".'); 31 | } 32 | 33 | return gitInfo[methodMap[config.name]](); 34 | }; 35 | 36 | gitinfo.weight = 10; 37 | 38 | export default gitinfo; 39 | -------------------------------------------------------------------------------- /src/locator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable canonical/filename-match-exported */ 2 | import fs from 'fs'; 3 | import Path from 'path'; 4 | import { 5 | fileURLToPath, 6 | } from 'url'; 7 | 8 | const theDirname = Path.dirname(fileURLToPath(import.meta.url)); 9 | 10 | const Locator = {}; 11 | 12 | /** 13 | * Returns path to the .git directory. 14 | * 15 | * @returns {string} 16 | */ 17 | Locator.gitPath = () => { 18 | let dirname; 19 | let gitpath; 20 | 21 | dirname = theDirname; 22 | 23 | do { 24 | if (fs.existsSync(dirname + '/.git')) { 25 | gitpath = dirname + '/.git'; 26 | 27 | break; 28 | } 29 | 30 | dirname = fs.realpathSync(dirname + '/..'); 31 | } while (fs.existsSync(dirname) && dirname !== '/'); 32 | 33 | if (!gitpath) { 34 | throw new Error('.git path cannot be located.'); 35 | } 36 | 37 | return gitpath; 38 | }; 39 | 40 | /** 41 | * Returns the parent path of the .git path. 42 | * 43 | * @returns {string} Path to the repository. 44 | */ 45 | Locator.repositoryPath = () => { 46 | return fs.realpathSync(Path.resolve(Locator.gitPath(), '..')); 47 | }; 48 | 49 | export default Locator; 50 | -------------------------------------------------------------------------------- /tests/helpers/include.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | import path from 'path'; 5 | import { 6 | fileURLToPath, 7 | } from 'url'; 8 | 9 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 10 | const importFresh = (moduleName) => { 11 | return import(`${moduleName}?${Date.now()}`); 12 | }; 13 | 14 | describe('Parser.helpers.include', () => { 15 | let helper; 16 | 17 | beforeEach(async () => { 18 | helper = (await importFresh('./../../src/helpers/include.js')).default; 19 | }); 20 | it('is rejected with an error when config.file is not provided', () => { 21 | expect(() => { 22 | helper.compile(); 23 | }).to.throw(Error, 'config.file must be provided.'); 24 | }); 25 | it('is rejected with an error when file is not found', () => { 26 | const context = { 27 | gitdown: { 28 | getConfig: () => { 29 | return { 30 | baseDirectory: dirname, 31 | }; 32 | }, 33 | }, 34 | }; 35 | 36 | expect(() => { 37 | helper.compile({ 38 | file: path.join(dirname, './does-not-exist'), 39 | }, context); 40 | }).to.throw(Error, 'Input file does not exist.'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /.README/helpers/anchor.md: -------------------------------------------------------------------------------- 1 | ### Reference an Anchor in the Repository 2 | 3 | > This feature is under development. 4 | > Please suggest ideas https://github.com/gajus/gitdown/issues 5 | 6 | 7 | ```json 8 | {"gitdown": "anchor"} 9 | 10 | ``` 11 | 12 | 13 | Generates a Github URL to the line in the source code with the anchor documentation tag of the same name. 14 | 15 | Place a documentation tag `@gitdownanchor ` anywhere in the code base, e.g. 16 | 17 | ```js 18 | /** 19 | * @gitdownanchor my-anchor-name 20 | */ 21 | 22 | ``` 23 | 24 | Then reference the tag in the Gitdown document: 25 | 26 | 27 | ``` 28 | Refer to [foo]({"gitdown": "anchor", "name": "my-anchor-name"}). 29 | 30 | ``` 31 | 32 | 33 | The anchor name must match `/^[a-z]+[a-z0-9\-_:\.]*$/i`. 34 | 35 | Gitdown will throw an error if the anchor is not found. 36 | 37 | #### JSON Configuration 38 | 39 | | Name | Description | Default | 40 | | --- | --- | --- | 41 | | `name` | Anchor name. | N/A | 42 | 43 | #### Parser Configuration 44 | 45 | | Name | Description | Default | 46 | | --- | --- | --- | 47 | | `anchor.exclude` | Array of paths to exclude. | `['./dist/*']` | 48 | -------------------------------------------------------------------------------- /tests/helpers/gitinfo.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | import Path from 'path'; 5 | import { 6 | fileURLToPath, 7 | } from 'url'; 8 | 9 | const dirname = Path.dirname(fileURLToPath(import.meta.url)); 10 | 11 | const importFresh = (moduleName) => { 12 | return import(`${moduleName}?${Date.now()}`); 13 | }; 14 | 15 | describe('Parser.helpers.gitinfo', () => { 16 | let context; 17 | let helper; 18 | 19 | beforeEach(async () => { 20 | helper = (await importFresh('../../src/helpers/gitinfo.js')).default; 21 | context = { 22 | gitdown: { 23 | getConfig: () => { 24 | return { 25 | gitinfo: { 26 | gitPath: dirname, 27 | }, 28 | }; 29 | }, 30 | }, 31 | }; 32 | }); 33 | it('throws an error if config.name is not provided', () => { 34 | expect(() => { 35 | helper.compile({}, context); 36 | }).to.throw(Error, 'config.name must be provided.'); 37 | }); 38 | it('throws an error if unsupported config.name property is provided', () => { 39 | expect(() => { 40 | helper.compile({ 41 | name: 'foo', 42 | }, context); 43 | }).to.throw(Error, 'Unexpected config.name value ("foo").'); 44 | }); 45 | it.skip('calls gitinfo method of the same name', () => { 46 | expect(helper.compile({ 47 | name: 'name', 48 | }, context)).to.equal('gitdown'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /.README/helpers/gitinfo.md: -------------------------------------------------------------------------------- 1 | ### Gitinfo 2 | 3 | 4 | ```json 5 | {"gitdown": "gitinfo"} 6 | 7 | ``` 8 | 9 | 10 | [Gitinfo](https://github.com/gajus/gitinfo) gets info about the local GitHub repository. 11 | 12 | #### Example 13 | 14 | 15 | ```json 16 | {"gitdown": "gitinfo", "name": "username"} 17 | {"gitdown": "gitinfo", "name": "name"} 18 | {"gitdown": "gitinfo", "name": "url"} 19 | {"gitdown": "gitinfo", "name": "branch"} 20 | 21 | ``` 22 | 23 | 24 | ``` 25 | {"gitdown": "gitinfo", "name": "username"} 26 | {"gitdown": "gitinfo", "name": "name"} 27 | {"gitdown": "gitinfo", "name": "url"} 28 | {"gitdown": "gitinfo", "name": "branch"} 29 | 30 | ``` 31 | 32 | #### Supported Properties 33 | 34 | |Name|Description| 35 | |---|---| 36 | |`username`|Username of the repository author.| 37 | |`name`|Repository name.| 38 | |`url`|Repository URL.| 39 | |`branch`|Current branch name.| 40 | 41 | #### JSON Configuration 42 | 43 | |Name|Description|Default| 44 | |---|---|---| 45 | |`name`|Name of the property.|N/A| 46 | 47 | #### Parser Configuration 48 | 49 | |Name|Description|Default| 50 | |---|---|---| 51 | |`gitinfo.defaultBranchName`|Default branch to use when the current branch name cannot be resolved.|N/A| 52 | |`gitinfo.gitPath`|Path to the `.git/` directory or a descendant. | `__dirname` of the script constructing an instance of `Gitdown`.| 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /tests/helpers/variable.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | 5 | const importFresh = (moduleName) => { 6 | return import(`${moduleName}?${Date.now()}`); 7 | }; 8 | 9 | describe('Parser.helpers.variable', () => { 10 | let context; 11 | let helper; 12 | 13 | beforeEach(async () => { 14 | helper = (await importFresh('../../src/helpers/variable.js')).default; 15 | context = { 16 | gitdown: { 17 | getConfig: () => { 18 | return { 19 | variable: { 20 | scope: {}, 21 | }, 22 | }; 23 | }, 24 | }, 25 | }; 26 | }); 27 | it('throws an error if variable name is not given', () => { 28 | expect(() => { 29 | helper.compile({}, context); 30 | }).to.throw(Error, 'config.name must be provided.'); 31 | }); 32 | it('throws an error if variable does not resolve to a defined value', () => { 33 | expect(() => { 34 | helper.compile({ 35 | name: 'a.b.c', 36 | }, context); 37 | }).to.throw(Error, 'config.name "a.b.c" does not resolve to a defined value.'); 38 | }); 39 | it('returns the resolved value', () => { 40 | context = { 41 | gitdown: { 42 | getConfig: () => { 43 | return { 44 | variable: { 45 | scope: { 46 | foo: { 47 | bar: { 48 | baz: 'quux', 49 | }, 50 | }, 51 | }, 52 | }, 53 | }; 54 | }, 55 | }, 56 | }; 57 | 58 | expect(helper.compile({ 59 | name: 'foo.bar.baz', 60 | }, context)).to.equal('quux'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /.README/helpers/badge.md: -------------------------------------------------------------------------------- 1 | ### Generate Badges 2 | 3 | 4 | ```json 5 | {"gitdown": "badge"} 6 | 7 | ``` 8 | 9 | 10 | Gitdown generates markdown for badges using the environment variables, e.g. if it is an NPM badge, Gitdown will lookup the package name from `package.json`. 11 | 12 | Badges are generated using http://shields.io/. 13 | 14 | #### Supported Services 15 | 16 | | Name | Description | 17 | | --- | --- | 18 | | `npm-version` | [NPM](https://www.npmjs.org/) package version. | 19 | | `bower-version` | [Bower](http://bower.io/) package version. | 20 | | `travis` | State of the [Travis](https://travis-ci.org/) build. | 21 | | `david` | [David](https://david-dm.org/) state of the dependencies. | 22 | | `david-dev` | [David](https://david-dm.org/) state of the development dependencies. | 23 | | `waffle` | Issues ready on [Waffle](https://waffle.io/) board. | 24 | | `gitter` | Join [Gitter](https://gitter.im/) chat. | 25 | | `coveralls` | [Coveralls](https://coveralls.io/). | 26 | | `codeclimate-gpa` | [Code Climate](https://codeclimate.com/) GPA. | 27 | | `codeclimate-coverage` | [Code Climate](https://codeclimate.com/) test coverage. | 28 | | `appveyor` | [AppVeyor](http://www.appveyor.com/) status. | 29 | 30 | What service are you missing? [Raise an issue](https://github.com/gajus/gitdown/issues). 31 | 32 | #### Example 33 | 34 | 35 | ```json 36 | {"gitdown": "badge", "name": "npm-version"} 37 | {"gitdown": "badge", "name": "travis"} 38 | {"gitdown": "badge", "name": "david"} 39 | 40 | ``` 41 | 42 | 43 | Generates: 44 | 45 | ```markdown 46 | {"gitdown": "badge", "name": "npm-version"} 47 | {"gitdown": "badge", "name": "travis"} 48 | {"gitdown": "badge", "name": "david"} 49 | 50 | ``` 51 | 52 | #### JSON Configuration 53 | 54 | | Name | Description | Default | 55 | | --- | --- | --- | 56 | | `name` | Name of the service. | N/A | 57 | -------------------------------------------------------------------------------- /src/helpers/filesize.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import { 3 | filesize as filesizer, 4 | } from 'filesize'; 5 | import fs from 'fs'; 6 | import zlib from 'zlib'; 7 | 8 | const filesize = {}; 9 | 10 | filesize.compile = async (config = {}) => { 11 | config.gzip = config.gzip || false; 12 | 13 | if (!config.file) { 14 | throw new Error('config.file must be provided.'); 15 | } 16 | 17 | const fileSize = await filesize.file(config.file, config.gzip); 18 | 19 | return filesize.format(fileSize); 20 | }; 21 | 22 | /** 23 | * Calculates size of a file. If gzip parameter is true, 24 | * calculates the gzipped size of a file. 25 | * 26 | * @private 27 | * @param {string} file 28 | * @param {boolean} gzip 29 | */ 30 | filesize.file = (file, gzip) => { 31 | return new Promise((resolve, reject) => { 32 | if (!fs.existsSync(file)) { 33 | // eslint-disable-next-line no-console 34 | console.log('file', file); 35 | 36 | reject(new Error('Input file does not exist.')); 37 | 38 | return; 39 | } 40 | 41 | if (gzip) { 42 | fs.readFile(file, (readFileError, buf) => { 43 | if (readFileError) { 44 | throw new Error(readFileError); 45 | } 46 | 47 | zlib.gzip(buf, (zlibError, data) => { 48 | if (zlibError) { 49 | throw new Error(zlibError); 50 | } 51 | 52 | resolve(data.length); 53 | }); 54 | }); 55 | } else { 56 | fs.stat(file, (statError, data) => { 57 | if (statError) { 58 | throw new Error(statError); 59 | } 60 | 61 | resolve(data.size); 62 | }); 63 | } 64 | }); 65 | }; 66 | 67 | /** 68 | * Formats size in bytes to a human friendly format. 69 | * 70 | * @private 71 | * @param {number} bytes 72 | */ 73 | filesize.format = (bytes) => { 74 | return filesizer(bytes); 75 | }; 76 | 77 | filesize.weight = 10; 78 | 79 | export default filesize; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "bin": "./src/bin/index.js", 8 | "type": "module", 9 | "dependencies": { 10 | "bluebird": "^3.7.2", 11 | "deadlink": "^1.1.3", 12 | "filesize": "^10.1.4", 13 | "get-urls": "^12.1.0", 14 | "gitinfo": "^2.4.0", 15 | "glob": "^10.4.4", 16 | "jsonfile": "^6.1.0", 17 | "lodash": "^4.17.21", 18 | "markdown-contents": "^1.0.11", 19 | "marked": "^13.0.2", 20 | "moment": "^2.30.1", 21 | "stack-trace": "^0.0.10", 22 | "yargs": "^17.7.2" 23 | }, 24 | "description": "Github markdown preprocessor.", 25 | "devDependencies": { 26 | "@babel/plugin-transform-flow-strip-types": "^7.24.7", 27 | "@babel/preset-env": "^7.24.7", 28 | "c8": "^10.1.2", 29 | "chai": "^5.1.1", 30 | "chai-as-promised": "^8.0.0", 31 | "coveralls": "^3.1.1", 32 | "eslint": "~8.57.0", 33 | "eslint-config-canonical": "~42.0.0", 34 | "husky": "^9.0.11", 35 | "mocha": "^10.6.0", 36 | "nock": "^13.5.4", 37 | "semantic-release": "^24.0.0", 38 | "sinon": "^18.0.0" 39 | }, 40 | "keywords": [ 41 | "github", 42 | "markdown", 43 | "table of contents", 44 | "toc", 45 | "include", 46 | "variable", 47 | "transclusion" 48 | ], 49 | "license": "BSD-3-Clause", 50 | "main": "./src/index.js", 51 | "name": "gitdown", 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/gajus/gitdown" 55 | }, 56 | "engines": { 57 | "node": ">=18" 58 | }, 59 | "scripts": { 60 | "binary": "./src/bin/index.js", 61 | "prepare": "husky", 62 | "create-readme": "./src/bin/index.js ./.README/README.md --output-file ./README.md", 63 | "lint": "eslint --report-unused-disable-directives ./src ./tests", 64 | "test": "c8 mocha \"./tests/**/*.js\" --timeout 9000" 65 | }, 66 | "version": "4.1.1" 67 | } 68 | -------------------------------------------------------------------------------- /src/helpers/contents.js: -------------------------------------------------------------------------------- 1 | import MarkdownContents from 'markdown-contents'; 2 | 3 | const contents = {}; 4 | contents.compile = (config = {}, context) => { 5 | let tree; 6 | 7 | config.maxLevel = config.maxLevel || 3; 8 | 9 | if (context.gitdown.getConfig().headingNesting.enabled) { 10 | tree = MarkdownContents(context.markdown).tree(); 11 | tree = contents.nestIds(tree); 12 | } else { 13 | const articles = MarkdownContents(context.markdown).articles(); 14 | 15 | tree = MarkdownContents.tree(articles, true, []); 16 | } 17 | 18 | if (config.rootId) { 19 | tree = contents.findRoot(tree, config.rootId).descendants; 20 | } 21 | 22 | tree = contents.maxLevel(tree, config.maxLevel); 23 | 24 | return MarkdownContents.treeToMarkdown(tree); 25 | }; 26 | 27 | /** 28 | * Removes tree descendants with level greater than maxLevel. 29 | * 30 | * @private 31 | */ 32 | contents.maxLevel = (tree, maxLevel = 1) => { 33 | return tree.filter((article) => { 34 | if (article.level > maxLevel) { 35 | return false; 36 | } else { 37 | article.descendants = contents.maxLevel(article.descendants, maxLevel); 38 | 39 | return true; 40 | } 41 | }); 42 | }; 43 | 44 | /** 45 | * @private 46 | */ 47 | contents.findRoot = (tree, rootId, notFirst) => { 48 | let found; 49 | let index; 50 | 51 | index = tree.length; 52 | 53 | while (index--) { 54 | if (tree[index].id === rootId) { 55 | found = tree[index]; 56 | 57 | break; 58 | } else { 59 | found = contents.findRoot(tree[index].descendants, rootId, true); 60 | } 61 | } 62 | 63 | if (!notFirst && !found) { 64 | throw new Error('Heading does not exist with rootId ("' + rootId + '").'); 65 | } 66 | 67 | return found; 68 | }; 69 | 70 | /** 71 | * @private 72 | */ 73 | contents.nestIds = (tree, parentIds = []) => { 74 | for (const article of tree) { 75 | const ids = parentIds.concat([ 76 | article.id, 77 | ]); 78 | 79 | article.id = ids.join('-'); 80 | 81 | contents.nestIds(article.descendants, ids); 82 | } 83 | 84 | return tree; 85 | }; 86 | 87 | contents.weight = 100; 88 | 89 | export default contents; 90 | -------------------------------------------------------------------------------- /tests/helpers/filesize.js: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import Path from 'path'; 4 | import { 5 | fileURLToPath, 6 | } from 'url'; 7 | 8 | const dirname = Path.dirname(fileURLToPath(import.meta.url)); 9 | const importFresh = (moduleName) => { 10 | return import(`${moduleName}?${Date.now()}`); 11 | }; 12 | 13 | const { 14 | expect, 15 | } = chai; 16 | 17 | chai.use(chaiAsPromised); 18 | 19 | describe('Parser.helpers.filesize', () => { 20 | let helper; 21 | 22 | beforeEach(async () => { 23 | helper = (await importFresh('./../../src/helpers/filesize.js')).default; 24 | }); 25 | it('is rejected with an error when config.file is not provided', () => { 26 | const result = helper.compile(); 27 | 28 | return expect(result).to.rejectedWith(Error, 'config.file must be provided.'); 29 | }); 30 | it('is rejected with an error when file is not found', () => { 31 | const result = helper.compile({ 32 | file: '/does-not-exist', 33 | }); 34 | 35 | return expect(result).to.rejectedWith(Error, 'Input file does not exist.'); 36 | }); 37 | 38 | it('returns formatted file size', () => { 39 | const result = helper.compile({ 40 | file: Path.resolve(dirname, './../fixtures/filesize.txt'), 41 | }); 42 | 43 | return expect(result).to.eventually.equal('191 B'); 44 | }); 45 | it('returns gziped formatted file size', () => { 46 | const result = helper.compile({ 47 | file: Path.resolve(dirname, './../fixtures/filesize.txt'), 48 | gzip: true, 49 | }); 50 | 51 | return expect(result).to.eventually.equal('145 B'); 52 | }); 53 | 54 | describe('.file(filename)', () => { 55 | it('throws an error if file is not found', () => { 56 | const result = helper.file(Path.resolve(dirname, './does-not-exist')); 57 | 58 | return expect(result).rejectedWith(Error, 'Input file does not exist.'); 59 | }); 60 | it('returns the file size in bytes', () => { 61 | const result = helper.file(Path.resolve(dirname, './../fixtures/filesize.txt')); 62 | 63 | return expect(result).eventually.equal(191); 64 | }); 65 | }); 66 | describe('.file(filename, true)', () => { 67 | it('returns gziped file size in bytes', () => { 68 | const result = helper.file(Path.resolve(dirname, './../fixtures/filesize.txt'), true); 69 | 70 | return expect(result).eventually.equal(145); 71 | }); 72 | }); 73 | describe('.format(size)', () => { 74 | it('returns file size as a human readable string', () => { 75 | const result = helper.format(1_024); 76 | 77 | expect(result).to.equal('1.02 kB'); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /.README/usage.md: -------------------------------------------------------------------------------- 1 | ## Command Line Usage 2 | 3 | ```sh 4 | npm install gitdown -g 5 | gitdown ./.README/README.md --output-file ./README.md 6 | 7 | ``` 8 | 9 | ## API Usage 10 | 11 | ```js 12 | import Gitdown from 'gitdown'; 13 | 14 | // Read the markdown file written using the Gitdown extended markdown. 15 | // File name is not important. 16 | // Having all of the Gitdown markdown files under ./.README/ path is the recommended convention. 17 | const gitdown = await Gitdown.readFile('./.README/README.md'); 18 | 19 | // If you have the subject in a string, call the constructor itself: 20 | // gitdown = await Gitdown.read('literal string'); 21 | 22 | // Get config. 23 | gitdown.getConfig() 24 | 25 | // Set config. 26 | gitdown.setConfig({ 27 | gitinfo: { 28 | gitPath: __dirname 29 | } 30 | }) 31 | 32 | // Output the markdown file. 33 | // All of the file system operations are relative to the root of the repository. 34 | gitdown.writeFile('README.md'); 35 | ``` 36 | 37 | ### Logging 38 | 39 | Gitdown is using `console` object to log messages. You can set your own logger: 40 | 41 | ```js 42 | gitdown.setLogger({ 43 | info: () => {}, 44 | warn: () => {} 45 | }); 46 | 47 | ``` 48 | 49 | The logger is used to inform about [Dead URLs and Fragment Identifiers](#find-dead-urls-and-fragment-identifiers). 50 | 51 | ## Syntax 52 | 53 | Gitdown extends markdown syntax using JSON: 54 | 55 | 56 | ```json 57 | {"gitdown": "helper name", "parameter name": "parameter value"} 58 | 59 | ``` 60 | 61 | 62 | The JSON object must have a `gitdown` property that identifies the helper you intend to execute. The rest is a regular JSON string, where each property is a named configuration property of the helper that you are referring to. 63 | 64 | JSON that does not start with a "gitdown" property will remain untouched. 65 | 66 | ### Ignoring Sections of the Document 67 | 68 | Use HTML comment tags to ignore sections of the document: 69 | 70 | ```html 71 | Gitdown JSON will be interpolated. 72 | <!-- gitdown: off --> 73 | Gitdown JSON will not be interpolated. 74 | <!-- gitdown: on --> 75 | Gitdown JSON will be interpolated. 76 | 77 | ``` 78 | 79 | ### Register a Custom Helper 80 | 81 | ```js 82 | gitdown.registerHelper('my-helper-name', { 83 | /** 84 | * @var {Number} Weight determines the processing order of the helper function. Default: 10. 85 | */ 86 | weight: 10, 87 | /** 88 | * @param {Object} config JSON configuration. 89 | * @return {mixed|Promise} 90 | */ 91 | compile: (config) => { 92 | return 'foo: ' + config.foo; 93 | } 94 | }); 95 | ``` 96 | 97 | 98 | ```json 99 | {"gitdown": "my-helper-name", "foo": "bar"} 100 | 101 | ``` 102 | 103 | 104 | Produces: 105 | 106 | ```markdown 107 | foo: bar 108 | 109 | ``` 110 | -------------------------------------------------------------------------------- /.README/README.md: -------------------------------------------------------------------------------- 1 | [![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/gitdown?style=flat-square)](https://gitspo.com/mentions/gajus/gitdown) 2 | {"gitdown": "badge", "name": "npm-version"} 3 | {"gitdown": "badge", "name": "travis"} 4 | {"gitdown": "badge", "name": "david"} 5 | 6 | Gitdown adds [additional functionality](#features) (generating table of contents, including documents, using variables, etc.) to [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). 7 | 8 | ## Cheat Sheet 9 | 10 | 11 | ```js 12 | // Generate table of contents 13 | {"gitdown": "contents"} 14 | {"gitdown": "contents", "maxLevel": 4} 15 | {"gitdown": "contents", "rootId": "features"} 16 | 17 | // Use a custom defined variable 18 | {"gitdown": "variable", "name": "nameOfTheVariable"} 19 | 20 | // Include file 21 | {"gitdown": "include", "file": "./LICENSE.md"} 22 | 23 | // Get file size 24 | {"gitdown": "filesize", "file": "./src/gitdown.js"} 25 | {"gitdown": "filesize", "file": "./src/gitdown.js", "gzip": true} 26 | 27 | // Generate badges 28 | {"gitdown": "badge", "name": "npm-version"} 29 | {"gitdown": "badge", "name": "bower-version"} 30 | {"gitdown": "badge", "name": "travis"} 31 | {"gitdown": "badge", "name": "david"} 32 | {"gitdown": "badge", "name": "david-dev"} 33 | {"gitdown": "badge", "name": "waffle"} 34 | 35 | // Print date 36 | {"gitdown": "date", "format": "YYYY"} 37 | 38 | ``` 39 | 40 | 41 | ## Contents 42 | 43 | {"gitdown": "contents", "maxDepth": 2} 44 | 45 | {"gitdown": "include", "file": "./usage.md"} 46 | 47 | a 48 | 49 | ## Features 50 | 51 | {"gitdown": "include", "file": "./helpers/contents.md"} 52 | {"gitdown": "include", "file": "./helpers/heading-nesting.md"} 53 | {"gitdown": "include", "file": "./helpers/deadlink.md"} 54 | {"gitdown": "include", "file": "./helpers/anchor.md"} 55 | {"gitdown": "include", "file": "./helpers/variable.md"} 56 | {"gitdown": "include", "file": "./helpers/include.md"} 57 | {"gitdown": "include", "file": "./helpers/filesize.md"} 58 | {"gitdown": "include", "file": "./helpers/badge.md"} 59 | {"gitdown": "include", "file": "./helpers/date.md"} 60 | {"gitdown": "include", "file": "./helpers/gitinfo.md"} 61 | 62 | ## Recipes 63 | 64 | ### Automating Gitdown 65 | 66 | Use [Husky](https://www.npmjs.com/package/husky) to check if user generated README.md before committing his changes. 67 | 68 | ```json 69 | "husky": { 70 | "hooks": { 71 | "pre-commit": "npm run lint && npm run test && npm run build", 72 | "pre-push": "gitdown ./.README/README.md --output-file ./README.md --check", 73 | } 74 | } 75 | 76 | ``` 77 | 78 | `--check` attributes makes Gitdown check if the target file differes from the source template. If the file differs then the program exits with an error code and message: 79 | 80 | > Gitdown destination file does not represent the current state of the template. 81 | 82 | Do not automate generating and committing documentation: automating commits will result in a noisy commit log. 83 | -------------------------------------------------------------------------------- /src/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from 'fs'; 4 | import _ from 'lodash'; 5 | import path from 'path'; 6 | import yargs from 'yargs'; 7 | import { 8 | hideBin, 9 | } from 'yargs/helpers'; 10 | 11 | const argv = yargs(hideBin(process.argv)) 12 | .usage('Usage: $0 [options]') 13 | .demand(1, 1, 'Gitdown program must be executed with exactly one non-option argument.') 14 | .options({ 15 | check: { 16 | default: false, 17 | description: 'Checks if the destination file represents the current ' + 18 | 'state of the template. Terminates program with exit status 1 if ' + 19 | 'generating a new document would result in changes of the target ' + 20 | 'document. Terminates program with exit status 0 otherwise (without ' + 21 | 'writng to the destination).', 22 | type: 'boolean', 23 | }, 24 | force: { 25 | default: false, 26 | describe: 'Write to file with different extension than ".md".', 27 | type: 'boolean', 28 | }, 29 | 'output-file': { 30 | demand: true, 31 | describe: 'Path to the output file.', 32 | type: 'string', 33 | }, 34 | }) 35 | .example('$0 ./.README/README.md --output-file ./README.md') 36 | .example('$0 ./.README/README.md --output-file ./README.txt --force') 37 | .wrap(null) 38 | .check((sargv) => { 39 | if (!_.startsWith(sargv._[0], './')) { 40 | throw new Error('Input file path must be a relative path. It must start with "./", e.g. "./README".'); 41 | } 42 | 43 | if (!_.startsWith(sargv.outputFile, './')) { 44 | throw new Error('Output file path must be a relative path. It must start with "./", e.g. "./README".'); 45 | } 46 | 47 | const inputFile = path.resolve(process.cwd(), sargv._[0]); 48 | 49 | try { 50 | fs.accessSync(inputFile, fs.constants.W_OK); 51 | } catch { 52 | // eslint-disable-next-line no-console 53 | console.log('inputFile', inputFile); 54 | 55 | throw new Error('Input file does not exist or cannot be written by the calling process.'); 56 | } 57 | 58 | const outputFile = sargv.outputFile; 59 | const outputFileExtension = path.extname(outputFile).toLowerCase(); 60 | const outputFileExists = path.resolve(process.cwd(), outputFile); 61 | 62 | if (outputFileExists && outputFileExists === inputFile) { 63 | throw new Error('Output file cannot overwrite the input file.'); 64 | } 65 | 66 | if (outputFileExtension !== '.md' && !sargv.force) { 67 | throw new Error('Cannot write into a file with an extension different than ".md". Use --force option.'); 68 | } 69 | 70 | return true; 71 | }) 72 | .strict() 73 | .argv; 74 | 75 | const main = async () => { 76 | const inputFile = argv._[0]; 77 | const outputFile = argv.outputFile; 78 | 79 | const resolvedInputFile = path.resolve(process.cwd(), inputFile); 80 | const resolvedOutputFile = path.resolve(process.cwd(), outputFile); 81 | 82 | // eslint-disable-next-line import/no-useless-path-segments 83 | const Gitdown = (await import('../index.js')).default; 84 | 85 | const gitdown = await Gitdown.readFile(resolvedInputFile); 86 | 87 | if (argv.check) { 88 | const generatedMarkdown = await gitdown.get(); 89 | 90 | if (fs.readFileSync(resolvedOutputFile, 'utf8') === generatedMarkdown) { 91 | return; 92 | } else { 93 | // eslint-disable-next-line no-console 94 | console.error('Gitdown destination file does not represent the current state of the template.'); 95 | 96 | process.exit(1); 97 | } 98 | 99 | return; 100 | } 101 | 102 | gitdown.writeFile(resolvedOutputFile); 103 | }; 104 | 105 | await main(); 106 | -------------------------------------------------------------------------------- /tests/parser.js: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import chaiAsPromised from 'chai-as-promised'; 3 | import Path from 'path'; 4 | import { 5 | spy, 6 | } from 'sinon'; 7 | import { 8 | fileURLToPath, 9 | } from 'url'; 10 | 11 | const dirname = Path.dirname(fileURLToPath(import.meta.url)); 12 | 13 | const importFresh = (moduleName) => { 14 | return import(`${moduleName}?${Date.now()}`); 15 | }; 16 | 17 | chai.use(chaiAsPromised); 18 | 19 | const expect = chai.expect; 20 | 21 | describe('Gitdown.Parser', () => { 22 | let Parser; 23 | let parser; 24 | let spyValue; 25 | 26 | beforeEach(async () => { 27 | Parser = (await importFresh('./../src/index.js')).default.Parser; 28 | parser = await Parser(); 29 | }); 30 | afterEach(() => { 31 | if (spyValue) { 32 | spyValue.restore(); 33 | } 34 | }); 35 | it('returns the input content', async () => { 36 | const state = await parser.play('foo'); 37 | 38 | expect(state.markdown).to.equal('foo'); 39 | }); 40 | it('ignores content starting with a HTML comment tag', async () => { 41 | const state = await parser.play('{"gitdown": "test"}{"gitdown": "test"}'); 42 | 43 | expect(state.markdown).to.equal('test{"gitdown": "test"}'); 44 | }); 45 | it('ignores content between and HTML comment tags', async () => { 46 | const state = await parser.play('{"gitdown": "test"}{"gitdown": "test"}'); 47 | 48 | expect(state.markdown).to.equal('{"gitdown": "test"}{"gitdown": "test"}'); 49 | }); 50 | it('interprets JSON starting with \'{"gitdown"\' and ending with \'}\'', async () => { 51 | const state = await parser.play('{"gitdown": "test"}{"gitdown": "test"}'); 52 | 53 | expect(state.markdown).to.equal('testtest'); 54 | }); 55 | it('throws an error if invalid Gitdown JSON hook is encountered', () => { 56 | const statePromise = parser.play('{"gitdown": invalid}'); 57 | 58 | return expect(statePromise).to.be.rejectedWith(Error, 'Invalid Gitdown JSON ("{"gitdown": invalid}").'); 59 | }); 60 | it('invokes a helper function with the markdown', async () => { 61 | spyValue = spy(parser.helpers().test, 'compile'); 62 | 63 | await parser.play('{"gitdown": "test", "foo": "bar"}'); 64 | 65 | expect(spyValue.calledWith({ 66 | foo: 'bar', 67 | })).to.be.equal(true); 68 | }); 69 | it('throws an error if an unknown helper is invoked', () => { 70 | const statePromise = parser.play('{"gitdown": "does-not-exist"}'); 71 | 72 | return expect(statePromise).to.be.rejectedWith(Error, 'Unknown helper "does-not-exist".'); 73 | }); 74 | it('descends to the helper with the lowest weight after each iteration', async () => { 75 | parser = await Parser({ 76 | getConfig: () => { 77 | return { 78 | baseDirectory: dirname, 79 | }; 80 | }, 81 | }); 82 | 83 | // Helper "include" is weight 20 84 | // Helper "test" is weight 10 85 | const state = await parser.play('{"gitdown": "include", "file": "./fixtures/include_test_weight_10.txt"}'); 86 | 87 | expect(state.markdown).to.equal('test'); 88 | }); 89 | }); 90 | 91 | describe('Parser.helpers', async () => { 92 | const glob = await import('glob'); 93 | const path = await import('path'); 94 | 95 | for (const helperName of glob.sync('./../src/helpers/*.js')) { 96 | const helper = await import(helperName); 97 | 98 | describe(path.basename(helperName, '.js'), () => { 99 | it('has compile method', () => { 100 | expect(helper).to.have.property('compile'); 101 | }); 102 | it('has weight property', () => { 103 | expect(helper).to.have.property('weight'); 104 | }); 105 | }); 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /tests/helpers/contents.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | 5 | const importFresh = (moduleName) => { 6 | return import(`${moduleName}?${Date.now()}`); 7 | }; 8 | 9 | describe('Parser.helpers.contents', () => { 10 | let helper; 11 | 12 | beforeEach(async () => { 13 | helper = (await importFresh('../../src/helpers/contents.js')).default; 14 | }); 15 | describe('when heading nesting is disabled', () => { 16 | let context; 17 | 18 | beforeEach(() => { 19 | context = { 20 | gitdown: { 21 | getConfig: () => { 22 | return { 23 | headingNesting: { 24 | enabled: false, 25 | }, 26 | }; 27 | }, 28 | }, 29 | markdown: '', 30 | }; 31 | }); 32 | 33 | it('generates table of contents for a markdown document', () => { 34 | context.markdown = '\n# a\n## b\n##c '; 35 | 36 | const contents = helper.compile({}, context); 37 | 38 | expect(contents).to.equal('* [a](#a)\n * [b](#b)\n * [c](#c)\n'); 39 | }); 40 | it('generates table of contents with a maxLevel', () => { 41 | context.markdown = '\n# a\n## b\n###c'; 42 | 43 | const contents = helper.compile({ 44 | maxLevel: 2, 45 | }, context); 46 | 47 | expect(contents).to.equal('* [a](#a)\n * [b](#b)\n'); 48 | }); 49 | it('generates unique IDs using incrementing index', () => { 50 | context.markdown = '\n# a\n## b\n## b'; 51 | 52 | const contents = helper.compile({}, context); 53 | 54 | expect(contents).to.equal('* [a](#a)\n * [b](#b)\n * [b](#b-1)\n'); 55 | }); 56 | }); 57 | describe('when heading nesting is enabled', () => { 58 | let context; 59 | 60 | beforeEach(() => { 61 | context = { 62 | gitdown: { 63 | getConfig: () => { 64 | return { 65 | headingNesting: { 66 | enabled: true, 67 | }, 68 | }; 69 | }, 70 | }, 71 | markdown: '', 72 | }; 73 | }); 74 | it('generates unique IDs using parent IDs', () => { 75 | context.markdown = '\n# a\n## b\n# c\n## d'; 76 | 77 | const contents = helper.compile({}, context); 78 | 79 | expect(contents).to.equal('* [a](#a)\n * [b](#a-b)\n* [c](#c)\n * [d](#c-d)\n'); 80 | }); 81 | }); 82 | describe('.maxLevel()', () => { 83 | it('removes nodes with level equal to maxLevel', () => { 84 | const tree = [ 85 | { 86 | descendants: [ 87 | { 88 | descendants: [ 89 | { 90 | descendants: [], 91 | level: 3, 92 | }, 93 | ], 94 | level: 2, 95 | }, 96 | ], 97 | level: 1, 98 | }, 99 | ]; 100 | 101 | const treeAfterMaxDepth = [ 102 | { 103 | descendants: [ 104 | { 105 | descendants: [], 106 | level: 2, 107 | }, 108 | ], 109 | level: 1, 110 | }, 111 | ]; 112 | 113 | expect(helper.maxLevel(tree, 2)).to.deep.equal(treeAfterMaxDepth); 114 | }); 115 | }); 116 | describe('.findRoot()', () => { 117 | it('find the object with ID', () => { 118 | const tree = [ 119 | { 120 | descendants: [ 121 | { 122 | descendants: [ 123 | { 124 | descendants: [], 125 | id: 'baz', 126 | }, 127 | ], 128 | id: 'bar', 129 | }, 130 | ], 131 | id: 'foo', 132 | }, 133 | ]; 134 | 135 | expect(helper.findRoot(tree, 'bar')).to.equal(tree[0].descendants[0]); 136 | }); 137 | it('throws an error if article with ID cannot be found', () => { 138 | expect(() => { 139 | helper.findRoot({}, 'bar'); 140 | }).to.throw(Error, 'Heading does not exist with rootId ("bar").'); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable canonical/filename-match-exported */ 2 | /* eslint-disable canonical/no-use-extend-native */ 3 | import Locator from './locator.js'; 4 | import Promise from 'bluebird'; 5 | import { 6 | glob, 7 | } from 'glob'; 8 | import _ from 'lodash'; 9 | import Path from 'path'; 10 | import { 11 | fileURLToPath, 12 | } from 'url'; 13 | 14 | const dirname = Path.dirname(fileURLToPath(import.meta.url)); 15 | 16 | /** 17 | * Parser is responsible for matching all of the instances of the Gitdown JSON and invoking 18 | * the associated operator functions. Operator functions are invoked in the order of the weight 19 | * associated with each function. Each operator function is passed the markdown document in 20 | * its present state (with alterations as a result of the preceding operator functions) and the 21 | * config from the JSON. This process is repeated until all commands have been executed and 22 | * parsing the document does not result in alteration of its state, i.e. there are no more Gitdown 23 | * JSON hooks that could have been generated by either of the preceding operator functions. 24 | * 25 | * @param {Gitdown} gitdown 26 | * @returns {Parser} 27 | */ 28 | const Parser = async (gitdown) => { 29 | let bindingIndex; 30 | 31 | bindingIndex = 0; 32 | 33 | const helpers = {}; 34 | const parser = {}; 35 | 36 | /** 37 | * Iterates markdown parsing and execution of the parsed commands until all of the 38 | * commands have been executed and the document does not no longer change after parsing it. 39 | * 40 | * @param {string} markdown 41 | * @param {Array} commands 42 | * @returns {Promise} Promise is resolved with the state object. 43 | */ 44 | parser.play = (markdown, commands = []) => { 45 | return Promise 46 | .try(async () => { 47 | const state = parser.parse(markdown, commands); 48 | const actState = await parser.execute(state); 49 | 50 | actState.commands 51 | .filter((command) => { 52 | return !command.executed; 53 | }); 54 | 55 | if (actState.done) { 56 | return actState; 57 | } else { 58 | return parser.play(actState.markdown, actState.commands); 59 | } 60 | }); 61 | }; 62 | 63 | /** 64 | * Parses the markdown for Gitdown JSON. Replaces the said JSON with placeholders for 65 | * the output of the command defined in the JSON. 66 | * 67 | * @see http://stackoverflow.com/questions/26910402/regex-to-match-json-in-a-document/26910403 68 | * @param {string} inputMarkdown 69 | * @param {Array} commands 70 | */ 71 | parser.parse = (inputMarkdown, commands) => { 72 | let outputMarkdown; 73 | 74 | const ignoreSection = []; 75 | 76 | outputMarkdown = inputMarkdown; 77 | 78 | // console.log('\n\n\n\ninput markdown:\n\n', markdown); 79 | 80 | // @see http://regex101.com/r/zO0eV6/2 81 | // console.log('markdown (before)', markdown); 82 | 83 | // /[\s\S]/ is an equivalent of /./m 84 | outputMarkdown = outputMarkdown.replaceAll(/[\S\s]*?(?:$|)/gu, (match) => { 85 | ignoreSection.push(match); 86 | 87 | return '⊂⊂I:' + ignoreSection.length + '⊃⊃'; 88 | }); 89 | 90 | outputMarkdown = outputMarkdown.replaceAll(/(\{"gitdown"[^}]+\})/gu, (match) => { 91 | let command; 92 | 93 | try { 94 | command = JSON.parse(match); 95 | } catch { 96 | throw new Error('Invalid Gitdown JSON ("' + match + '").'); 97 | } 98 | 99 | const name = command.gitdown; 100 | const config = { 101 | ...command, 102 | }; 103 | 104 | delete config.gitdown; 105 | 106 | bindingIndex++; 107 | 108 | if (!helpers[name]) { 109 | throw new Error('Unknown helper "' + name + '".'); 110 | } 111 | 112 | commands.push({ 113 | bindingIndex, 114 | config, 115 | executed: false, 116 | helper: helpers[name], 117 | name, 118 | }); 119 | 120 | return '⊂⊂C:' + bindingIndex + '⊃⊃'; 121 | }); 122 | 123 | outputMarkdown = outputMarkdown.replaceAll(/⊂⊂I:(\d+)⊃⊃/gu, (match, p1) => { 124 | return ignoreSection[Number.parseInt(p1, 10) - 1]; 125 | }); 126 | 127 | return { 128 | commands, 129 | markdown: outputMarkdown, 130 | }; 131 | }; 132 | 133 | /** 134 | * Execute all of the commands sharing the lowest common weight against 135 | * the current state of the markdown document. 136 | * 137 | * @param {object} state 138 | * @returns {Promise} Promise resolves to a state after all of the commands have been resolved. 139 | */ 140 | parser.execute = async (state) => { 141 | const notExecutedCommands = state.commands.filter((command) => { 142 | return !command.executed; 143 | }); 144 | 145 | if (!notExecutedCommands.length) { 146 | state.done = true; 147 | 148 | return state; 149 | } 150 | 151 | // Find the lowest weight among all of the not executed commands. 152 | const lowestWeight = _.minBy(notExecutedCommands, 'helper.weight').helper.weight; 153 | 154 | // Find all commands with the lowest weight. 155 | const lowestWeightCommands = _.filter(notExecutedCommands, (command) => { 156 | return command.helper.weight === lowestWeight; 157 | }); 158 | 159 | // Execute each command and update markdown binding. 160 | await Promise 161 | .resolve(lowestWeightCommands) 162 | .each(async (command) => { 163 | const context = { 164 | gitdown, 165 | locator: Locator, 166 | markdown: state.markdown, 167 | parser, 168 | }; 169 | 170 | const value = await Promise.resolve(command.helper.compile(command.config, context)); 171 | 172 | state.markdown = state.markdown.replace('⊂⊂C:' + command.bindingIndex + '⊃⊃', () => { 173 | return value; 174 | }); 175 | 176 | command.executed = true; 177 | }); 178 | 179 | return state; 180 | }; 181 | 182 | /* eslint-disable require-atomic-updates -- Safe */ 183 | /** 184 | * Load in-built helpers. 185 | * 186 | * @private 187 | */ 188 | parser.loadHelpers = async () => { 189 | /* eslint-enable require-atomic-updates -- Safe */ 190 | for (const helper of glob.sync(Path.resolve(dirname, './helpers/*.js'))) { 191 | parser.registerHelper(Path.basename(helper, '.js'), (await import(helper)).default); 192 | } 193 | }; 194 | 195 | /** 196 | * @param {string} name 197 | * @param {object} helper 198 | */ 199 | parser.registerHelper = (name, helper = {}) => { 200 | if (helpers[name]) { 201 | throw new Error('There is already a helper with a name "' + name + '".'); 202 | } 203 | 204 | if (_.isUndefined(helper.weight)) { 205 | helper.weight = 10; 206 | } 207 | 208 | if (_.isUndefined(helper.compile)) { 209 | throw new TypeError('Helper object must defined "compile" property.'); 210 | } 211 | 212 | helpers[name] = helper; 213 | }; 214 | 215 | /** 216 | * @returns {object} 217 | */ 218 | parser.helpers = () => { 219 | return helpers; 220 | }; 221 | 222 | await parser.loadHelpers(); 223 | 224 | return parser; 225 | }; 226 | 227 | export default Parser; 228 | -------------------------------------------------------------------------------- /src/helpers/badge.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import jsonfile from 'jsonfile'; 3 | 4 | const badge = {}; 5 | badge.compile = (config = {}, context) => { 6 | const services = {}; 7 | 8 | if (!config.name) { 9 | throw new Error('config.name must be provided.'); 10 | } 11 | 12 | const badgeStyle = 'style=flat-square'; 13 | 14 | services['npm-version'] = () => { 15 | let pkg; 16 | 17 | pkg = context.locator.repositoryPath() + '/package.json'; 18 | 19 | if (!fs.existsSync(pkg)) { 20 | throw new Error('./package.json is not found.'); 21 | } 22 | 23 | pkg = jsonfile.readFileSync(pkg); 24 | 25 | const theBadge = '![NPM version](http://img.shields.io/npm/v/' + pkg.name + '.svg?' + badgeStyle + ')'; 26 | 27 | return '[' + theBadge + '](https://www.npmjs.org/package/' + pkg.name + ')'; 28 | }; 29 | 30 | services['bower-version'] = () => { 31 | let bower; 32 | 33 | bower = context.locator.repositoryPath() + '/bower.json'; 34 | 35 | if (!fs.existsSync(bower)) { 36 | throw new Error('./bower.json is not found.'); 37 | } 38 | 39 | bower = jsonfile.readFileSync(bower); 40 | 41 | const theBadge = '![Bower version](http://img.shields.io/bower/v/' + bower.name + '.svg?' + badgeStyle + ')'; 42 | 43 | return '[' + theBadge + '](http://bower.io/search/?q=' + bower.name + ')'; 44 | }; 45 | 46 | /** 47 | * @see https://github.com/gajus/gitdown/issues/10 48 | */ 49 | services.david = () => { 50 | const gitinfo = context.parser.helpers().gitinfo; 51 | const repository = gitinfo.compile({ 52 | name: 'username', 53 | }, context) + '/' + gitinfo.compile({ 54 | name: 'name', 55 | }, context); 56 | const theBadge = '![Dependency Status](https://img.shields.io/david/' + repository + '.svg?' + badgeStyle + ')'; 57 | 58 | return '[' + theBadge + '](https://david-dm.org/' + repository + ')'; 59 | }; 60 | 61 | /** 62 | * @see https://github.com/gajus/gitdown/issues/10 63 | */ 64 | services['david-dev'] = () => { 65 | const gitinfo = context.parser.helpers().gitinfo; 66 | const repository = gitinfo.compile({ 67 | name: 'username', 68 | }, context) + '/' + gitinfo.compile({ 69 | name: 'name', 70 | }, context); 71 | const theBadge = '![Development Dependency Status](https://img.shields.io/david/dev/' + repository + '.svg?' + badgeStyle + ')'; 72 | 73 | return '[' + theBadge + '](https://david-dm.org/' + repository + '#info=devDependencies)'; 74 | }; 75 | 76 | /** 77 | * @see https://github.com/gajus/gitdown/issues/12 78 | */ 79 | services.gitter = () => { 80 | const gitinfo = context.parser.helpers().gitinfo; 81 | const repository = gitinfo.compile({ 82 | name: 'username', 83 | }, context) + '/' + gitinfo.compile({ 84 | name: 'name', 85 | }, context); 86 | const theBadge = '![Gitter chat](https://img.shields.io/gitter/room/' + repository + '.svg?' + badgeStyle + ')'; 87 | 88 | return '[' + theBadge + '](https://gitter.im/' + repository + ')'; 89 | }; 90 | 91 | /** 92 | * @see https://github.com/gajus/gitdown/issues/13 93 | */ 94 | services.coveralls = () => { 95 | const gitinfo = context.parser.helpers().gitinfo; 96 | const repository = gitinfo.compile({ 97 | name: 'username', 98 | }, context) + '/' + gitinfo.compile({ 99 | name: 'name', 100 | }, context); 101 | const branch = gitinfo.compile({ 102 | name: 'branch', 103 | }, context); 104 | const theBadge = '![Coverage Status](https://img.shields.io/coveralls/' + repository + '/' + branch + '.svg?' + badgeStyle + ')'; 105 | 106 | return '[' + theBadge + '](https://coveralls.io/r/' + repository + '?branch=' + branch + ')'; 107 | }; 108 | 109 | /** 110 | * @see https://github.com/gajus/gitdown/issues/33 111 | */ 112 | services.circleci = () => { 113 | const gitinfo = context.parser.helpers().gitinfo; 114 | const repository = gitinfo.compile({ 115 | name: 'username', 116 | }, context) + '/' + gitinfo.compile({ 117 | name: 'name', 118 | }, context); 119 | const branch = gitinfo.compile({ 120 | name: 'branch', 121 | }, context); 122 | const theBadge = '![Circle CI](https://img.shields.io/circleci/project/' + repository + '/circleci/' + branch + '.svg?' + badgeStyle + ')'; 123 | 124 | return '[' + theBadge + '](https://circleci.com/gh/' + repository + '?branch=' + branch + ')'; 125 | }; 126 | 127 | /** 128 | * @todo Link does not include travis branch. 129 | */ 130 | services.travis = () => { 131 | const rep = {}; 132 | const gitinfo = context.parser.helpers().gitinfo; 133 | const repository = gitinfo.compile({ 134 | name: 'username', 135 | }, context) + '/' + gitinfo.compile({ 136 | name: 'name', 137 | }, context); 138 | 139 | rep.branch = gitinfo.compile({ 140 | name: 'branch', 141 | }, context); 142 | 143 | const theBadge = '![Travis build status](http://img.shields.io/travis/' + repository + '/' + rep.branch + '.svg?' + badgeStyle + ')'; 144 | 145 | return '[' + theBadge + '](https://travis-ci.org/' + repository + ')'; 146 | }; 147 | 148 | services.waffle = () => { 149 | const gitinfo = context.parser.helpers().gitinfo; 150 | const repository = gitinfo.compile({ 151 | name: 'username', 152 | }, context) + '/' + gitinfo.compile({ 153 | name: 'name', 154 | }, context); 155 | const theBadge = '![Stories in Ready](https://badge.waffle.io/' + repository + '.svg?label=ready&title=Ready)'; 156 | 157 | return '[' + theBadge + '](https://waffle.io/' + repository + ')'; 158 | }; 159 | 160 | /** 161 | * @see https://github.com/gajus/gitdown/issues/16 162 | */ 163 | services['codeclimate-gpa'] = () => { 164 | const gitinfo = context.parser.helpers().gitinfo; 165 | const repository = 'github/' + gitinfo.compile({ 166 | name: 'username', 167 | }, context) + '/' + gitinfo.compile({ 168 | name: 'name', 169 | }, context); 170 | const theBadge = '![Code Climate GPA](https://img.shields.io/codeclimate/' + repository + '.svg?' + badgeStyle + ')'; 171 | 172 | return '[' + theBadge + '](https://codeclimate.com/' + repository + ')'; 173 | }; 174 | 175 | services['codeclimate-coverage'] = () => { 176 | const gitinfo = context.parser.helpers().gitinfo; 177 | const repository = 'github/' + gitinfo.compile({ 178 | name: 'username', 179 | }, context) + '/' + gitinfo.compile({ 180 | name: 'name', 181 | }, context); 182 | const theBadge = '![Code Climate Coverage](https://img.shields.io/codeclimate/coverage/' + repository + '.svg?' + badgeStyle + ')'; 183 | 184 | return '[' + theBadge + '](https://codeclimate.com/' + repository + ')'; 185 | }; 186 | 187 | /** 188 | * @see https://github.com/gajus/gitdown/issues/35 189 | */ 190 | services.appveyor = () => { 191 | const gitinfo = context.parser.helpers().gitinfo; 192 | const username = gitinfo.compile({ 193 | name: 'username', 194 | }, context); 195 | const name = gitinfo.compile({ 196 | name: 'name', 197 | }, context); 198 | const branch = gitinfo.compile({ 199 | name: 'branch', 200 | }, context); 201 | const repository = username + '/' + name; 202 | const theBadge = '![AppVeyor build status](https://img.shields.io/appveyor/ci/' + repository + '/' + branch + '.svg?' + badgeStyle + ')'; 203 | 204 | return '[' + theBadge + '](https://ci.appveyor.com/project/' + repository + '/branch/' + branch + ')'; 205 | }; 206 | 207 | if (!services[config.name]) { 208 | throw new Error('config.name "' + config.name + '" is unknown service.'); 209 | } 210 | 211 | return services[config.name](); 212 | }; 213 | 214 | badge.weight = 10; 215 | 216 | export default badge; 217 | -------------------------------------------------------------------------------- /tests/helpers/badge.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | import Path from 'path'; 5 | import { 6 | fileURLToPath, 7 | } from 'url'; 8 | 9 | const dirname = Path.dirname(fileURLToPath(import.meta.url)); 10 | const importFresh = (moduleName) => { 11 | return import(`${moduleName}?${Date.now()}`); 12 | }; 13 | 14 | describe('Parser.helpers.badge', () => { 15 | let gitinfoContext; 16 | let helper; 17 | 18 | beforeEach(async () => { 19 | helper = (await importFresh('../../src/helpers/badge.js')).default; 20 | gitinfoContext = { 21 | parser: { 22 | helpers: () => { 23 | return { 24 | gitinfo: { 25 | compile (config) { 26 | if (config.name === 'username') { 27 | return 'a'; 28 | } else if (config.name === 'name') { 29 | return 'b'; 30 | } else if (config.name === 'branch') { 31 | return 'c'; 32 | } 33 | 34 | throw new Error('Invalid config.'); 35 | }, 36 | }, 37 | }; 38 | }, 39 | }, 40 | }; 41 | }); 42 | it('throws an error when config.name is not provided', () => { 43 | expect(() => { 44 | helper.compile(); 45 | }).to.throw(Error, 'config.name must be provided.'); 46 | }); 47 | it('throws an error if unknown config.name is provided', () => { 48 | expect(() => { 49 | helper.compile({ 50 | name: 'foo', 51 | }); 52 | }).to.throw(Error, 'config.name "foo" is unknown service.'); 53 | }); 54 | describe('services', () => { 55 | describe('npm-version', () => { 56 | it('throws an error if package.json is not found in the root of the repository', () => { 57 | const context = { 58 | locator: { 59 | repositoryPath: () => { 60 | return dirname; 61 | }, 62 | }, 63 | }; 64 | 65 | expect(() => { 66 | helper.compile({ 67 | name: 'npm-version', 68 | }, context); 69 | }).to.throw(Error, './package.json is not found.'); 70 | }); 71 | it('returns markdown for the NPM badge', () => { 72 | const context = { 73 | locator: { 74 | repositoryPath: () => { 75 | return Path.resolve(dirname, './../fixtures/badge'); 76 | }, 77 | }, 78 | }; 79 | 80 | const badge = helper.compile({ 81 | name: 'npm-version', 82 | }, context); 83 | 84 | expect(badge).to.equal('[![NPM version](http://img.shields.io/npm/v/gitdown.svg?style=flat-square)](https://www.npmjs.org/package/gitdown)'); 85 | }); 86 | }); 87 | describe('bower-version', () => { 88 | it('throws an error if bower.json is not found in the root of the repository', () => { 89 | const context = { 90 | locator: { 91 | repositoryPath: () => { 92 | return dirname; 93 | }, 94 | }, 95 | }; 96 | 97 | expect(() => { 98 | helper.compile({ 99 | name: 'bower-version', 100 | }, context); 101 | }).to.throw(Error, './bower.json is not found.'); 102 | }); 103 | it('returns markdown for the Bower badge', () => { 104 | const context = { 105 | locator: { 106 | repositoryPath: () => { 107 | return Path.resolve(dirname, './../fixtures/badge'); 108 | }, 109 | }, 110 | }; 111 | 112 | const badge = helper.compile({ 113 | name: 'bower-version', 114 | }, context); 115 | 116 | expect(badge).to.equal('[![Bower version](http://img.shields.io/bower/v/gitdown.svg?style=flat-square)](http://bower.io/search/?q=gitdown)'); 117 | }); 118 | }); 119 | describe('coveralls', () => { 120 | it('returns markdown for the coveralls badge', () => { 121 | const badge = helper.compile({ 122 | name: 'coveralls', 123 | }, gitinfoContext); 124 | 125 | expect(badge).to.equal('[![Coverage Status](https://img.shields.io/coveralls/a/b/c.svg?style=flat-square)](https://coveralls.io/r/a/b?branch=c)'); 126 | }); 127 | }); 128 | describe('gitter', () => { 129 | it('returns markdown for the gitter badge', () => { 130 | const badge = helper.compile({ 131 | name: 'gitter', 132 | }, gitinfoContext); 133 | 134 | expect(badge).to.equal('[![Gitter chat](https://img.shields.io/gitter/room/a/b.svg?style=flat-square)](https://gitter.im/a/b)'); 135 | }); 136 | }); 137 | describe('david', () => { 138 | it('returns markdown for the david badge', () => { 139 | const badge = helper.compile({ 140 | name: 'david', 141 | }, gitinfoContext); 142 | 143 | expect(badge).to.equal('[![Dependency Status](https://img.shields.io/david/a/b.svg?style=flat-square)](https://david-dm.org/a/b)'); 144 | }); 145 | }); 146 | describe('david-dev', () => { 147 | it('returns markdown for the david badge', () => { 148 | const badge = helper.compile({ 149 | name: 'david-dev', 150 | }, gitinfoContext); 151 | 152 | expect(badge).to.equal( 153 | '[![Development Dependency Status](' + 154 | 'https://img.shields.io/david/dev/a/b.svg?style=flat-square' + 155 | ')](https://david-dm.org/a/b#info=devDependencies)', 156 | ); 157 | }); 158 | }); 159 | describe('travis', () => { 160 | it('returns markdown for the travis badge', () => { 161 | const badge = helper.compile({ 162 | name: 'travis', 163 | }, gitinfoContext); 164 | 165 | expect(badge).to.equal('[![Travis build status](http://img.shields.io/travis/a/b/c.svg?style=flat-square)](https://travis-ci.org/a/b)'); 166 | }); 167 | }); 168 | describe('waffle', () => { 169 | it('returns markdown for the waffle badge', () => { 170 | const badge = helper.compile({ 171 | name: 'waffle', 172 | }, gitinfoContext); 173 | 174 | expect(badge).to.equal('[![Stories in Ready](https://badge.waffle.io/a/b.svg?label=ready&title=Ready)](https://waffle.io/a/b)'); 175 | }); 176 | }); 177 | 178 | describe('codeclimate', () => { 179 | it('returns markdown for the codeclimate gpa badge', () => { 180 | const badge = helper.compile({ 181 | name: 'codeclimate-gpa', 182 | }, gitinfoContext); 183 | 184 | expect(badge).to.equal( 185 | '[![Code Climate GPA]' + 186 | '(https://img.shields.io/codeclimate/github/a/b.svg?style=flat-square' + 187 | ')](https://codeclimate.com/github/a/b)', 188 | ); 189 | }); 190 | it('returns markdown for the codeclimate coverage badge', () => { 191 | const badge = helper.compile({ 192 | name: 'codeclimate-coverage', 193 | }, gitinfoContext); 194 | 195 | expect(badge).to.equal( 196 | '[![Code Climate Coverage](' + 197 | 'https://img.shields.io/codeclimate/coverage/github/a/b.svg?style=flat-square' + 198 | ')](https://codeclimate.com/github/a/b)', 199 | ); 200 | }); 201 | }); 202 | 203 | describe('appveyor', () => { 204 | it('returns markdown for the AppVeyor badge', () => { 205 | const badge = helper.compile({ 206 | name: 'appveyor', 207 | }, gitinfoContext); 208 | 209 | expect(badge).to.equal( 210 | '[![AppVeyor build status](' + 211 | 'https://img.shields.io/appveyor/ci/a/b/c.svg?style=flat-square' + 212 | ')](https://ci.appveyor.com/project/a/b/branch/c)', 213 | ); 214 | }); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /tests/gitdown.js: -------------------------------------------------------------------------------- 1 | import { 2 | expect, 3 | } from 'chai'; 4 | import fs from 'fs'; 5 | import nock from 'nock'; 6 | import path from 'path'; 7 | import { 8 | spy, 9 | } from 'sinon'; 10 | import { 11 | fileURLToPath, 12 | } from 'url'; 13 | 14 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 15 | const importFresh = (moduleName) => { 16 | return import(`${moduleName}?${Date.now()}`); 17 | }; 18 | 19 | describe('Gitdown', () => { 20 | let Gitdown; 21 | 22 | beforeEach(async () => { 23 | Gitdown = (await importFresh('../src/index.js')).default; 24 | }); 25 | describe('.readFile()', () => { 26 | it('calls Gitdown.read() using the contents of the file', async () => { 27 | const gitdown = await Gitdown.readFile(path.resolve(dirname, './fixtures/foo.txt')); 28 | 29 | gitdown.setConfig({ 30 | gitinfo: { 31 | gitPath: path.resolve(dirname, './dummy_git/'), 32 | }, 33 | }); 34 | 35 | const response = await gitdown.get(); 36 | 37 | expect(response).to.equal('bar'); 38 | }); 39 | }); 40 | 41 | describe('prefixRelativeUrls', () => { 42 | it('replaces relative links', () => { 43 | expect(Gitdown.prefixRelativeUrls('A [relative](#link) test')).to.equal('A [relative](#user-content-link) test'); 44 | }); 45 | }); 46 | 47 | describe('.nestHeadingIds()', () => { 48 | it('replaces heading markup with HTML', () => { 49 | expect( 50 | Gitdown.nestHeadingIds('# Foo\n# Bar'), 51 | ).to.equal( 52 | '\n\n# Foo\n\n\n# Bar', 53 | ); 54 | }); 55 | it('nests heading ids', () => { 56 | expect( 57 | Gitdown.nestHeadingIds('# Foo\n## Bar'), 58 | ).to.equal( 59 | '\n\n# Foo\n\n\n## Bar', 60 | ); 61 | }); 62 | }); 63 | describe('.nestHeadingIds.iterateTree()', () => { 64 | it('iterates through each leaf of tree', () => { 65 | const result = []; 66 | 67 | const tree = [ 68 | { 69 | descendants: [ 70 | { 71 | descendants: [], 72 | id: 'b', 73 | }, 74 | { 75 | descendants: [], 76 | id: 'c', 77 | }, 78 | ], 79 | id: 'a', 80 | }, 81 | { 82 | descendants: [], 83 | id: 'd', 84 | }, 85 | ]; 86 | 87 | Gitdown.nestHeadingIds.iterateTree(tree, (index, leaf) => { 88 | result.push(index + '-' + leaf.id); 89 | }); 90 | 91 | expect(result).to.deep.equal([ 92 | '1-a', 93 | '2-b', 94 | '3-c', 95 | '4-d', 96 | ]); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('Gitdown.read()', () => { 102 | let Gitdown; 103 | 104 | beforeEach(async () => { 105 | Gitdown = (await importFresh('../src/index.js')).default; 106 | }); 107 | describe('.get()', () => { 108 | it('is using Parser to produce the response', async () => { 109 | const gitdown = await Gitdown.read('{"gitdown": "test"}', { 110 | gitPath: path.resolve(dirname, './dummy_git/'), 111 | }); 112 | 113 | const response = await gitdown.get(); 114 | 115 | expect(response).to.equal('test'); 116 | }); 117 | it('removes all gitdown specific HTML comments', async () => { 118 | const gitdown = await Gitdown.read('abc', { 119 | gitPath: path.resolve(dirname, './dummy_git/'), 120 | }); 121 | 122 | const response = await gitdown.get(); 123 | 124 | expect(response).to.equal('abc'); 125 | }); 126 | it('does not fail when HEAD is untracked', async () => { 127 | const gitdown = await Gitdown.read('{"gitdown": "gitinfo", "name": "name"}', { 128 | defaultBranchName: 'master', 129 | gitPath: path.resolve(dirname, './dummy_git_untracked_head/'), 130 | }); 131 | 132 | const response = await gitdown.get(); 133 | 134 | expect(response).to.equal('bar'); 135 | }); 136 | }); 137 | describe('.writeFile()', () => { 138 | it('writes the output of .get() to a file', async () => { 139 | const fileName = path.resolve(dirname, './fixtures/write.txt'); 140 | const randomString = String(Math.random()); 141 | const gitdown = await Gitdown.read(randomString, { 142 | gitPath: path.resolve(dirname, './dummy_git/'), 143 | }); 144 | 145 | await gitdown.writeFile(fileName); 146 | 147 | expect(fs.readFileSync(fileName, { 148 | encoding: 'utf8', 149 | })).to.equal(randomString); 150 | }); 151 | }); 152 | describe('.registerHelper()', () => { 153 | it('throws an error if registering a helper using name of an existing helper', async () => { 154 | const gitdown = await Gitdown.read('', { 155 | gitPath: path.resolve(dirname, './dummy_git/'), 156 | }); 157 | 158 | expect(() => { 159 | gitdown.registerHelper('test'); 160 | }).to.throw(Error, 'There is already a helper with a name "test".'); 161 | }); 162 | it('throws an error if registering a helper object without compile property', async () => { 163 | const gitdown = await Gitdown.read('', { 164 | gitPath: path.resolve(dirname, './dummy_git/'), 165 | }); 166 | 167 | expect(() => { 168 | gitdown.registerHelper('new-helper'); 169 | }).to.throw(Error, 'Helper object must defined "compile" property.'); 170 | }); 171 | it('registers a new helper', async () => { 172 | const gitdown = await Gitdown.read('{"gitdown": "new-helper", "testProp": "foo"}', { 173 | gitPath: path.resolve(dirname, './dummy_git/'), 174 | }); 175 | 176 | gitdown.registerHelper('new-helper', { 177 | compile (config) { 178 | return 'Test prop: ' + config.testProp; 179 | }, 180 | }); 181 | 182 | const markdown = await gitdown.get(); 183 | 184 | expect(markdown).to.equal('Test prop: foo'); 185 | }); 186 | }); 187 | xdescribe('.setConfig()', () => { 188 | let defaultConfiguration; 189 | 190 | beforeEach(() => { 191 | defaultConfiguration = { 192 | deadlink: { 193 | findDeadFragmentIdentifiers: false, 194 | findDeadURLs: false, 195 | }, 196 | gitinfo: { 197 | gitPath: dirname, 198 | }, 199 | headingNesting: { 200 | enabled: true, 201 | }, 202 | variable: { 203 | scope: {}, 204 | }, 205 | }; 206 | }); 207 | it('returns the current configuration', async () => { 208 | const gitdown = await Gitdown.read(''); 209 | const config = gitdown.config; 210 | 211 | expect(config).to.deep.equal(defaultConfiguration); 212 | }); 213 | it('sets a configuration', async () => { 214 | const gitdown = await Gitdown.read(''); 215 | 216 | gitdown.config = defaultConfiguration; 217 | 218 | expect(defaultConfiguration).to.equal(gitdown.config); 219 | }); 220 | }); 221 | describe.skip('.resolveURLs()', () => { 222 | let gitdown; 223 | let logger; 224 | let nocks; 225 | 226 | beforeEach(async () => { 227 | gitdown = await Gitdown.read('http://foo.com/ http://foo.com/#ok http://bar.com/ http://bar.com/#not-ok', { 228 | gitPath: path.resolve(dirname, './dummy_git/'), 229 | }); 230 | 231 | logger = { 232 | info () {}, 233 | warn () {}, 234 | }; 235 | 236 | gitdown.setLogger(logger); 237 | 238 | logger = gitdown.getLogger(); 239 | 240 | nocks = {}; 241 | nocks.foo = nock('http://foo.com').get('/').reply(200, '
', { 242 | 'content-type': 'text/html', 243 | }); 244 | nocks.bar = nock('http://bar.com').get('/').reply(404); 245 | }); 246 | 247 | afterEach(() => { 248 | nock.cleanAll(); 249 | }); 250 | 251 | it('it does not resolve URLs when config.deadlink.findDeadURLs is false', async () => { 252 | gitdown.setConfig({ 253 | deadlink: { 254 | findDeadFragmentIdentifiers: false, 255 | findDeadURLs: false, 256 | }, 257 | }); 258 | 259 | await gitdown.get(); 260 | 261 | expect(nocks.foo.isDone()).to.equal(false); 262 | }); 263 | it('it does resolve URLs when config.deadlink.findDeadURLs is true', async () => { 264 | gitdown.setConfig({ 265 | deadlink: { 266 | findDeadFragmentIdentifiers: false, 267 | findDeadURLs: true, 268 | }, 269 | }); 270 | 271 | await gitdown.get(); 272 | 273 | expect(nocks.foo.isDone()).to.equal(true); 274 | }); 275 | it('logs successful URL resolution using logger.info', async () => { 276 | const spyValue = spy(logger, 'info'); 277 | 278 | gitdown.setConfig({ 279 | deadlink: { 280 | findDeadFragmentIdentifiers: false, 281 | findDeadURLs: true, 282 | }, 283 | }); 284 | 285 | await gitdown.get(); 286 | 287 | expect(spyValue.calledWith('Resolved URL:', 'http://foo.com/')).to.equal(true); 288 | }); 289 | it('logs successful URL and fragment identifier resolution using logger.info', async () => { 290 | const spyValue = spy(logger, 'info'); 291 | 292 | gitdown.setConfig({ 293 | deadlink: { 294 | findDeadFragmentIdentifiers: true, 295 | findDeadURLs: true, 296 | }, 297 | }); 298 | 299 | await gitdown.get(); 300 | 301 | expect(spyValue.calledWith('Resolved fragment identifier:', 'http://foo.com/#ok')).to.equal(true); 302 | }); 303 | it('logs unsuccessful URL resolution using logger.warn', async () => { 304 | const spyValue = spy(logger, 'warn'); 305 | 306 | gitdown.setConfig({ 307 | deadlink: { 308 | findDeadFragmentIdentifiers: true, 309 | findDeadURLs: true, 310 | }, 311 | }); 312 | 313 | await gitdown.get(); 314 | 315 | expect(spyValue.calledWith('Unresolved URL:', 'http://bar.com/')).to.equal(true); 316 | }); 317 | it('logs unsuccessful fragment identifier resolution using logger.warn', async () => { 318 | const spyValue = spy(logger, 'warn'); 319 | 320 | gitdown.setConfig({ 321 | deadlink: { 322 | findDeadFragmentIdentifiers: true, 323 | findDeadURLs: true, 324 | }, 325 | }); 326 | 327 | await gitdown.get(); 328 | 329 | expect(spyValue.calledWith('Unresolved fragment identifier:', 'http://bar.com/#not-ok')).to.equal(true); 330 | }); 331 | }); 332 | }); 333 | -------------------------------------------------------------------------------- /src/gitdown.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable canonical/filename-match-exported */ 2 | import contents from './helpers/contents.js'; 3 | import gitinfo from './helpers/gitinfo.js'; 4 | import Parser from './parser.js'; 5 | import Promise from 'bluebird'; 6 | import Deadlink from 'deadlink'; 7 | import fs from 'fs'; 8 | import getUrls from 'get-urls'; 9 | import _ from 'lodash'; 10 | import MarkdownContents from 'markdown-contents'; 11 | import { 12 | marked, 13 | } from 'marked'; 14 | import path from 'path'; 15 | import StackTrace from 'stack-trace'; 16 | import { 17 | fileURLToPath, 18 | } from 'url'; 19 | 20 | const dirname = path.dirname(fileURLToPath(import.meta.url)); 21 | 22 | const Gitdown = {}; 23 | 24 | /** 25 | * @param {string} input Gitdown flavored markdown. 26 | * @param {object} [gitInfo] 27 | */ 28 | Gitdown.read = async (input, gitInfo) => { 29 | let instanceConfig; 30 | let instanceLogger; 31 | 32 | instanceConfig = {}; 33 | 34 | const gitdown = {}; 35 | const parser = await Parser(gitdown); 36 | 37 | /** 38 | * Process template. 39 | * 40 | * @returns {Promise} 41 | */ 42 | gitdown.get = async () => { 43 | const state = await parser.play(input); 44 | 45 | let markdown; 46 | markdown = state.markdown; 47 | 48 | if (gitdown.getConfig().headingNesting.enabled) { 49 | markdown = Gitdown.nestHeadingIds(markdown); 50 | } 51 | 52 | markdown = Gitdown.prefixRelativeUrls(markdown); 53 | 54 | // await gitdown.resolveURLs(markdown); // Disabling until may remove 55 | 56 | return markdown.replaceAll(//gu, ''); 57 | }; 58 | 59 | /** 60 | * Write processed template to a file. 61 | * 62 | * @param {string} fileName 63 | * @returns {Promise} 64 | */ 65 | gitdown.writeFile = async (fileName) => { 66 | const outputString = await gitdown.get(); 67 | 68 | return fs.writeFileSync(fileName, outputString); 69 | }; 70 | 71 | /** 72 | * @param {string} name 73 | * @param {object} helper 74 | */ 75 | gitdown.registerHelper = (name, helper) => { 76 | parser.registerHelper(name, helper); 77 | }; 78 | 79 | /** 80 | * Returns the first directory in the callstack that is not this directory. 81 | * 82 | * @private 83 | * @returns {string} Path to the directory where Gitdown was invoked. 84 | */ 85 | gitdown.executionContext = () => { 86 | let index; 87 | 88 | const stackTrace = StackTrace.get(); 89 | const stackTraceLength = stackTrace.length; 90 | 91 | index = 0; 92 | 93 | while (index++ < stackTraceLength) { 94 | const stackDirectory = path.dirname(stackTrace[index].getFileName()); 95 | 96 | if (dirname !== stackDirectory) { 97 | return stackDirectory; 98 | } 99 | } 100 | 101 | throw new Error('Execution context cannot be determined.'); 102 | }; 103 | 104 | /** 105 | * @private 106 | * @param {string} markdown 107 | */ 108 | gitdown.resolveURLs = (markdown) => { 109 | let promises; 110 | let urls; 111 | 112 | const repositoryURL = gitinfo.compile({ 113 | name: 'url', 114 | }, { 115 | gitdown, 116 | }) + '/tree/' + gitinfo.compile({ 117 | name: 'branch', 118 | }, { 119 | gitdown, 120 | }); 121 | const deadlink = Deadlink(); 122 | 123 | urls = Array.from(getUrls(markdown)); 124 | 125 | urls = urls.map((url) => { 126 | let resolvedUrl; 127 | 128 | // @todo What if it isn't /README.md? 129 | // @todo Test. 130 | if (_.startsWith(url, '#')) { 131 | // Github is using JavaScript to resolve anchor tags under #uses-content- ID. 132 | resolvedUrl = repositoryURL + '#user-content-' + url.slice(1); 133 | } else { 134 | resolvedUrl = url; 135 | } 136 | 137 | return resolvedUrl; 138 | }); 139 | 140 | if (!urls.length || !gitdown.getConfig().deadlink.findDeadURLs) { 141 | return Promise.resolve([]); 142 | } 143 | 144 | if (gitdown.getConfig().deadlink.findDeadFragmentIdentifiers) { 145 | promises = deadlink.resolve(urls); 146 | } else { 147 | promises = deadlink.resolveURLs(urls); 148 | } 149 | 150 | gitdown.getLogger().info('Resolving URLs', urls); 151 | 152 | return Promise 153 | .all(promises) 154 | .each((Resolution) => { 155 | if (Resolution.error && Resolution.fragmentIdentifier && !(Resolution.error instanceof Deadlink.URLResolution && !Resolution.error.error)) { 156 | // Ignore the fragment identifier error if resource resolution failed. 157 | gitdown.getLogger().warn('Unresolved fragment identifier:', Resolution.url); 158 | } else if (Resolution.error && !Resolution.fragmentIdentifier) { 159 | gitdown.getLogger().warn('Unresolved URL:', Resolution.url); 160 | } else if (Resolution.fragmentIdentifier) { 161 | gitdown.getLogger().info('Resolved fragment identifier:', Resolution.url); 162 | } else if (!Resolution.fragmentIdentifier) { 163 | gitdown.getLogger().info('Resolved URL:', Resolution.url); 164 | } 165 | }); 166 | }; 167 | 168 | /** 169 | * @param {object} logger 170 | */ 171 | gitdown.setLogger = (logger) => { 172 | if (!logger.info) { 173 | throw new Error('Logger must implement logger.info function.'); 174 | } 175 | 176 | if (!logger.warn) { 177 | throw new Error('Logger must implement logger.warn function.'); 178 | } 179 | 180 | instanceLogger = { 181 | info: logger.info, 182 | warn: logger.warn, 183 | }; 184 | }; 185 | 186 | /** 187 | * @returns {object} 188 | */ 189 | gitdown.getLogger = () => { 190 | return instanceLogger; 191 | }; 192 | 193 | /** 194 | * @typedef {object} config 195 | * @property {} 196 | */ 197 | 198 | /** 199 | * @param {object} config 200 | * @returns {undefined} 201 | */ 202 | gitdown.setConfig = (config) => { 203 | if (!_.isPlainObject(config)) { 204 | throw new TypeError('config must be a plain object.'); 205 | } 206 | 207 | if (config.variable && !_.isObject(config.variable.scope)) { 208 | throw new Error('config.variable.scope must be set and must be an object.'); 209 | } 210 | 211 | if (config.deadlink && !_.isBoolean(config.deadlink.findDeadURLs)) { 212 | throw new Error('config.deadlink.findDeadURLs must be set and must be a boolean value'); 213 | } 214 | 215 | if (config.deadlink && !_.isBoolean(config.deadlink.findDeadFragmentIdentifiers)) { 216 | throw new Error('config.deadlink.findDeadFragmentIdentifiers must be set and must be a boolean value'); 217 | } 218 | 219 | if (config.gitinfo && !fs.realpathSync(config.gitinfo.gitPath)) { 220 | throw new Error('config.gitinfo.gitPath must be set and must resolve an existing file path.'); 221 | } 222 | 223 | instanceConfig = _.defaultsDeep(config, instanceConfig); 224 | }; 225 | 226 | /** 227 | * @returns {object} 228 | */ 229 | gitdown.getConfig = () => { 230 | return instanceConfig; 231 | }; 232 | 233 | gitdown.setConfig({ 234 | baseDirectory: process.cwd(), 235 | deadlink: { 236 | findDeadFragmentIdentifiers: false, 237 | findDeadURLs: false, 238 | }, 239 | gitinfo: gitInfo || { 240 | gitPath: gitdown.executionContext(), 241 | }, 242 | headingNesting: { 243 | enabled: true, 244 | }, 245 | variable: { 246 | scope: {}, 247 | }, 248 | 249 | }); 250 | 251 | return gitdown; 252 | }; 253 | 254 | /** 255 | * Read input from a file. 256 | * 257 | * @param {string} fileName 258 | * @returns {Gitdown} 259 | */ 260 | Gitdown.readFile = async (fileName) => { 261 | if (!path.isAbsolute(fileName)) { 262 | throw new Error('fileName must be an absolute path.'); 263 | } 264 | 265 | const input = fs.readFileSync(fileName, { 266 | encoding: 'utf8', 267 | }); 268 | 269 | const directoryName = path.dirname(fileName); 270 | const gitdown = await Gitdown.read(input, { 271 | gitPath: directoryName, 272 | }); 273 | gitdown.setConfig({ 274 | baseDirectory: directoryName, 275 | }); 276 | 277 | return gitdown; 278 | }; 279 | 280 | /** 281 | * Prefixes "user-content-" to each Markdown internal link. 282 | * 283 | * @private 284 | * @param {string} inputMarkdown 285 | * @returns {string} 286 | */ 287 | Gitdown.prefixRelativeUrls = (inputMarkdown) => { 288 | return inputMarkdown.replaceAll(/\[(.*?)\]\(#(.*?)\)/gum, (match, text, anchor) => { 289 | return `[${text}](#user-content-${anchor})`; 290 | }); 291 | }; 292 | 293 | /** 294 | * Iterates through each heading in the document (defined using markdown) 295 | * and prefixes heading ID using parent heading ID. 296 | * 297 | * @private 298 | * @param {string} inputMarkdown 299 | * @returns {string} 300 | */ 301 | Gitdown.nestHeadingIds = (inputMarkdown) => { 302 | let outputMarkdown; 303 | 304 | const articles = []; 305 | const codeblocks = []; 306 | 307 | outputMarkdown = inputMarkdown; 308 | 309 | outputMarkdown = outputMarkdown.replaceAll(/^```[\S\s]*?\n```/gum, (match) => { 310 | codeblocks.push(match); 311 | 312 | return '⊂⊂⊂C:' + codeblocks.length + '⊃⊃⊃'; 313 | }); 314 | 315 | outputMarkdown = outputMarkdown.replaceAll(/^(#+)(.*$)/gum, (match, level, name) => { 316 | let normalizedName; 317 | 318 | const normalizedLevel = level.length; 319 | 320 | normalizedName = name.trim(); 321 | 322 | articles.push({ 323 | // `foo bar` 324 | // -foo-bar- 325 | // foo-bar 326 | id: _.trim(normalizedName.toLowerCase().replaceAll(/\W+/gu, '-'), '-'), 327 | level: normalizedLevel, 328 | name: normalizedName, 329 | }); 330 | 331 | // `test` 332 | normalizedName = _.trim(marked(normalizedName)); 333 | 334 | //

test

335 | normalizedName = normalizedName.slice(3, -4); 336 | 337 | // test 338 | 339 | return ` 340 | 341 | ${_.repeat('#', normalizedLevel)} ${normalizedName}`; 342 | }); 343 | 344 | outputMarkdown = outputMarkdown.replaceAll(/^⊂⊂⊂C:(\d+)⊃⊃⊃/gum, () => { 345 | return codeblocks.shift(); 346 | }); 347 | 348 | const tree = contents.nestIds(MarkdownContents.tree(articles)); 349 | 350 | Gitdown.nestHeadingIds.iterateTree(tree, (index, article) => { 351 | outputMarkdown = outputMarkdown.replaceAll(new RegExp('⊂⊂⊂H:' + index + '⊃⊃⊃', 'gu'), article.id); 352 | }); 353 | 354 | return outputMarkdown; 355 | }; 356 | 357 | /** 358 | * @private 359 | * @param {Array} tree 360 | * @param {Function} callback 361 | * @param {number} index 362 | */ 363 | Gitdown.nestHeadingIds.iterateTree = (tree, callback, index = 1) => { 364 | let nextIndex; 365 | 366 | nextIndex = index; 367 | 368 | for (const article of tree) { 369 | callback(nextIndex++, article); 370 | 371 | if (article.descendants) { 372 | nextIndex = Gitdown.nestHeadingIds.iterateTree(article.descendants, callback, nextIndex); 373 | } 374 | } 375 | 376 | return nextIndex; 377 | }; 378 | 379 | export default Gitdown; 380 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/gitdown?style=flat-square)](https://gitspo.com/mentions/gajus/gitdown) 2 | [![NPM version](http://img.shields.io/npm/v/gitdown.svg?style=flat-square)](https://www.npmjs.org/package/gitdown) 3 | [![Travis build status](http://img.shields.io/travis/brettz9/gitdown/update-readme.svg?style=flat-square)](https://travis-ci.org/brettz9/gitdown) 4 | [![Dependency Status](https://img.shields.io/david/brettz9/gitdown.svg?style=flat-square)](https://david-dm.org/brettz9/gitdown) 5 | 6 | Gitdown adds [additional functionality](#user-content-features) (generating table of contents, including documents, using variables, etc.) to [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). 7 | 8 | 9 | 10 | ## Cheat Sheet 11 | 12 | 13 | ```js 14 | // Generate table of contents 15 | {"gitdown": "contents"} 16 | {"gitdown": "contents", "maxLevel": 4} 17 | {"gitdown": "contents", "rootId": "features"} 18 | 19 | // Use a custom defined variable 20 | {"gitdown": "variable", "name": "nameOfTheVariable"} 21 | 22 | // Include file 23 | {"gitdown": "include", "file": "./LICENSE.md"} 24 | 25 | // Get file size 26 | {"gitdown": "filesize", "file": "./src/gitdown.js"} 27 | {"gitdown": "filesize", "file": "./src/gitdown.js", "gzip": true} 28 | 29 | // Generate badges 30 | {"gitdown": "badge", "name": "npm-version"} 31 | {"gitdown": "badge", "name": "bower-version"} 32 | {"gitdown": "badge", "name": "travis"} 33 | {"gitdown": "badge", "name": "david"} 34 | {"gitdown": "badge", "name": "david-dev"} 35 | {"gitdown": "badge", "name": "waffle"} 36 | 37 | // Print date 38 | {"gitdown": "date", "format": "YYYY"} 39 | 40 | ``` 41 | 42 | 43 | 44 | 45 | ## Contents 46 | 47 | * [Cheat Sheet](#user-content-cheat-sheet) 48 | * [Contents](#user-content-contents) 49 | * [Command Line Usage](#user-content-command-line-usage) 50 | * [API Usage](#user-content-api-usage) 51 | * [Logging](#user-content-api-usage-logging) 52 | * [Syntax](#user-content-syntax) 53 | * [Ignoring Sections of the Document](#user-content-syntax-ignoring-sections-of-the-document) 54 | * [Register a Custom Helper](#user-content-syntax-register-a-custom-helper) 55 | * [Features](#user-content-features) 56 | * [Generate Table of Contents](#user-content-features-generate-table-of-contents) 57 | * [Heading Nesting](#user-content-features-heading-nesting) 58 | * [Find Dead URLs and Fragment Identifiers](#user-content-features-find-dead-urls-and-fragment-identifiers) 59 | * [Reference an Anchor in the Repository](#user-content-features-reference-an-anchor-in-the-repository) 60 | * [Variables](#user-content-features-variables) 61 | * [Include File](#user-content-features-include-file) 62 | * [Get File Size](#user-content-features-get-file-size) 63 | * [Generate Badges](#user-content-features-generate-badges) 64 | * [Print Date](#user-content-features-print-date) 65 | * [Gitinfo](#user-content-features-gitinfo) 66 | * [Recipes](#user-content-recipes) 67 | * [Automating Gitdown](#user-content-recipes-automating-gitdown) 68 | 69 | 70 | 71 | 72 | ## Command Line Usage 73 | 74 | ```sh 75 | npm install gitdown -g 76 | gitdown ./.README/README.md --output-file ./README.md 77 | 78 | ``` 79 | 80 | 81 | 82 | ## API Usage 83 | 84 | ```js 85 | import Gitdown from 'gitdown'; 86 | 87 | // Read the markdown file written using the Gitdown extended markdown. 88 | // File name is not important. 89 | // Having all of the Gitdown markdown files under ./.README/ path is the recommended convention. 90 | const gitdown = await Gitdown.readFile('./.README/README.md'); 91 | 92 | // If you have the subject in a string, call the constructor itself: 93 | // gitdown = await Gitdown.read('literal string'); 94 | 95 | // Get config. 96 | gitdown.getConfig() 97 | 98 | // Set config. 99 | gitdown.setConfig({ 100 | gitinfo: { 101 | gitPath: __dirname 102 | } 103 | }) 104 | 105 | // Output the markdown file. 106 | // All of the file system operations are relative to the root of the repository. 107 | gitdown.writeFile('README.md'); 108 | ``` 109 | 110 | 111 | 112 | ### Logging 113 | 114 | Gitdown is using `console` object to log messages. You can set your own logger: 115 | 116 | ```js 117 | gitdown.setLogger({ 118 | info: () => {}, 119 | warn: () => {} 120 | }); 121 | 122 | ``` 123 | 124 | The logger is used to inform about [Dead URLs and Fragment Identifiers](#user-content-find-dead-urls-and-fragment-identifiers). 125 | 126 | 127 | 128 | ## Syntax 129 | 130 | Gitdown extends markdown syntax using JSON: 131 | 132 | 133 | ```json 134 | {"gitdown": "helper name", "parameter name": "parameter value"} 135 | 136 | ``` 137 | 138 | 139 | The JSON object must have a `gitdown` property that identifies the helper you intend to execute. The rest is a regular JSON string, where each property is a named configuration property of the helper that you are referring to. 140 | 141 | JSON that does not start with a "gitdown" property will remain untouched. 142 | 143 | 144 | 145 | ### Ignoring Sections of the Document 146 | 147 | Use HTML comment tags to ignore sections of the document: 148 | 149 | ```html 150 | Gitdown JSON will be interpolated. 151 | <!-- gitdown: off --> 152 | Gitdown JSON will not be interpolated. 153 | <!-- gitdown: on --> 154 | Gitdown JSON will be interpolated. 155 | 156 | ``` 157 | 158 | 159 | 160 | ### Register a Custom Helper 161 | 162 | ```js 163 | gitdown.registerHelper('my-helper-name', { 164 | /** 165 | * @var {Number} Weight determines the processing order of the helper function. Default: 10. 166 | */ 167 | weight: 10, 168 | /** 169 | * @param {Object} config JSON configuration. 170 | * @return {mixed|Promise} 171 | */ 172 | compile: (config) => { 173 | return 'foo: ' + config.foo; 174 | } 175 | }); 176 | ``` 177 | 178 | 179 | ```json 180 | {"gitdown": "my-helper-name", "foo": "bar"} 181 | 182 | ``` 183 | 184 | 185 | Produces: 186 | 187 | ```markdown 188 | foo: bar 189 | 190 | ``` 191 | 192 | 193 | a 194 | 195 | 196 | 197 | ## Features 198 | 199 | 200 | 201 | ### Generate Table of Contents 202 | 203 | 204 | ```json 205 | {"gitdown": "contents"} 206 | 207 | ``` 208 | 209 | 210 | Generates table of contents. 211 | 212 | The table of contents is generated using [markdown-contents](https://github.com/gajus/markdown-contents). 213 | 214 | 215 | 216 | #### Example 217 | 218 | 219 | ```json 220 | {"gitdown": "contents", "maxLevel": 4, "rootId": "features"} 221 | 222 | ``` 223 | 224 | 225 | ```markdown 226 | * [Generate Table of Contents](#user-content-features-generate-table-of-contents) 227 | * [Example](#user-content-features-generate-table-of-contents-example) 228 | * [JSON Configuration](#user-content-features-generate-table-of-contents-json-configuration) 229 | * [Heading Nesting](#user-content-features-heading-nesting) 230 | * [Parser Configuration](#user-content-features-heading-nesting-parser-configuration) 231 | * [Find Dead URLs and Fragment Identifiers](#user-content-features-find-dead-urls-and-fragment-identifiers) 232 | * [Parser Configuration](#user-content-features-find-dead-urls-and-fragment-identifiers-parser-configuration-1) 233 | * [Reference an Anchor in the Repository](#user-content-features-reference-an-anchor-in-the-repository) 234 | * [JSON Configuration](#user-content-features-reference-an-anchor-in-the-repository-json-configuration-1) 235 | * [Parser Configuration](#user-content-features-reference-an-anchor-in-the-repository-parser-configuration-2) 236 | * [Variables](#user-content-features-variables) 237 | * [Example](#user-content-features-variables-example-1) 238 | * [JSON Configuration](#user-content-features-variables-json-configuration-2) 239 | * [Parser Configuration](#user-content-features-variables-parser-configuration-3) 240 | * [Include File](#user-content-features-include-file) 241 | * [Example](#user-content-features-include-file-example-2) 242 | * [JSON Configuration](#user-content-features-include-file-json-configuration-3) 243 | * [Get File Size](#user-content-features-get-file-size) 244 | * [Example](#user-content-features-get-file-size-example-3) 245 | * [JSON Configuration](#user-content-features-get-file-size-json-configuration-4) 246 | * [Generate Badges](#user-content-features-generate-badges) 247 | * [Supported Services](#user-content-features-generate-badges-supported-services) 248 | * [Example](#user-content-features-generate-badges-example-4) 249 | * [JSON Configuration](#user-content-features-generate-badges-json-configuration-5) 250 | * [Print Date](#user-content-features-print-date) 251 | * [Example](#user-content-features-print-date-example-5) 252 | * [JSON Configuration](#user-content-features-print-date-json-configuration-6) 253 | * [Gitinfo](#user-content-features-gitinfo) 254 | * [Example](#user-content-features-gitinfo-example-6) 255 | * [Supported Properties](#user-content-features-gitinfo-supported-properties) 256 | * [JSON Configuration](#user-content-features-gitinfo-json-configuration-7) 257 | * [Parser Configuration](#user-content-features-gitinfo-parser-configuration-4) 258 | 259 | 260 | ``` 261 | 262 | 263 | 264 | #### JSON Configuration 265 | 266 | | Name | Description | Default | 267 | | --- | --- | --- | 268 | | `maxLevel` | The maximum heading level after which headings are excluded. | 3 | 269 | | `rootId` | ID of the root heading. Provide it when you need table of contents for a specific section of the document. Throws an error if element with the said ID does not exist in the document. | N/A | 270 | 271 | 272 | 273 | ### Heading Nesting 274 | 275 | Github markdown processor generates heading ID based on the text of the heading. 276 | 277 | The conflicting IDs are solved with a numerical suffix, e.g. 278 | 279 | ```markdown 280 | # Foo 281 | ## Something 282 | # Bar 283 | ## Something 284 | 285 | ``` 286 | 287 | ```html 288 |

Foo

289 |

Something

290 |

Bar

291 |

Something

292 | 293 | ``` 294 | 295 | The problem with this approach is that it makes the order of the content important. 296 | 297 | Gitdown will nest the headings using parent heading names to ensure uniqueness, e.g. 298 | 299 | ```markdown 300 | # Foo 301 | ## Something 302 | # Bar 303 | ## Something 304 | 305 | ``` 306 | 307 | ```html 308 |

Foo

309 |

Something

310 |

Bar

311 |

Something

312 | 313 | ``` 314 | 315 | 316 | 317 | #### Parser Configuration 318 | 319 | | Name | Description | Default | 320 | | --- | --- | --- | 321 | | `headingNesting.enabled` | Boolean flag indicating whether to nest headings. | `true` | 322 | 323 | 324 | 325 | ### Find Dead URLs and Fragment Identifiers 326 | 327 | Uses [Deadlink](https://github.com/gajus/deadlink) to iterate through all of the URLs in the resulting document. Throws an error if either of the URLs is resolved with an HTTP status other than 200 or fragment identifier (anchor) is not found. 328 | 329 | 330 | 331 | #### Parser Configuration 332 | 333 | | Name | Description | Default | 334 | | --- | --- | --- | 335 | | `deadlink.findDeadURLs` | Find dead URLs. | `false` | 336 | | `deadlink.findDeadFragmentIdentifiers` | Find dead fragment identifiers. | `false` | 337 | 338 | 339 | 340 | 341 | 342 | 343 | ### Reference an Anchor in the Repository 344 | 345 | > This feature is under development. 346 | > Please suggest ideas https://github.com/gajus/gitdown/issues 347 | 348 | 349 | ```json 350 | {"gitdown": "anchor"} 351 | 352 | ``` 353 | 354 | 355 | Generates a Github URL to the line in the source code with the anchor documentation tag of the same name. 356 | 357 | Place a documentation tag `@gitdownanchor ` anywhere in the code base, e.g. 358 | 359 | ```js 360 | /** 361 | * @gitdownanchor my-anchor-name 362 | */ 363 | 364 | ``` 365 | 366 | Then reference the tag in the Gitdown document: 367 | 368 | 369 | ``` 370 | Refer to [foo]({"gitdown": "anchor", "name": "my-anchor-name"}). 371 | 372 | ``` 373 | 374 | 375 | The anchor name must match `/^[a-z]+[a-z0-9\-_:\.]*$/i`. 376 | 377 | Gitdown will throw an error if the anchor is not found. 378 | 379 | 380 | 381 | #### JSON Configuration 382 | 383 | | Name | Description | Default | 384 | | --- | --- | --- | 385 | | `name` | Anchor name. | N/A | 386 | 387 | 388 | 389 | #### Parser Configuration 390 | 391 | | Name | Description | Default | 392 | | --- | --- | --- | 393 | | `anchor.exclude` | Array of paths to exclude. | `['./dist/*']` | 394 | 395 | 396 | 397 | ### Variables 398 | 399 | 400 | ```json 401 | {"gitdown": "variable"} 402 | 403 | ``` 404 | 405 | 406 | Prints the value of a property defined under a parser `variable.scope` configuration property. Throws an error if property is not set. 407 | 408 | 409 | 410 | #### Example 411 | 412 | 413 | ```js 414 | const gitdown = Gitdown( 415 | '{"gitdown": "variable", "name": "name.first"}' + 416 | '{"gitdown": "variable", "name": "name.last"}' 417 | ); 418 | 419 | gitdown.setConfig({ 420 | variable: { 421 | scope: { 422 | name: { 423 | first: "Gajus", 424 | last: "Kuizinas" 425 | } 426 | } 427 | } 428 | }); 429 | 430 | ``` 431 | 432 | 433 | 434 | 435 | #### JSON Configuration 436 | 437 | | Name | Description | Default | 438 | | --- | --- | --- | 439 | | `name` | Name of the property defined under a parser `variable.scope` configuration property. | N/A | 440 | 441 | 442 | 443 | #### Parser Configuration 444 | 445 | | Name | Description | Default | 446 | | --- | --- | --- | 447 | | `variable.scope` | Variable scope object. | `{}` | 448 | 449 | 450 | 451 | ### Include File 452 | 453 | 454 | ```json 455 | {"gitdown": "include"} 456 | 457 | ``` 458 | 459 | 460 | Includes the contents of the file to the document. 461 | 462 | The included file can have Gitdown JSON hooks. 463 | 464 | 465 | 466 | #### Example 467 | 468 | See source code of [./.README/README.md](https://github.com/gajus/gitdown/blob/master/.README/README.md). 469 | 470 | 471 | 472 | #### JSON Configuration 473 | 474 | | Name | Description | Default | 475 | | --- | --- | --- | 476 | | `file` | Path to the file. The path is relative to the root of the repository. | N/A | 477 | 478 | 479 | 480 | ### Get File Size 481 | 482 | 483 | ```json 484 | {"gitdown": "filesize"} 485 | 486 | ``` 487 | 488 | 489 | Returns file size formatted in human friendly format. 490 | 491 | 492 | 493 | #### Example 494 | 495 | 496 | ```json 497 | {"gitdown": "filesize", "file": "src/gitdown.js"} 498 | {"gitdown": "filesize", "file": "src/gitdown.js", "gzip": true} 499 | 500 | ``` 501 | 502 | 503 | Generates: 504 | 505 | ```markdown 506 | 9.3 kB 507 | 2.8 kB 508 | 509 | ``` 510 | 511 | 512 | 513 | #### JSON Configuration 514 | 515 | | Name | Description | Default | 516 | | --- | --- | --- | 517 | | `file` | Path to the file. The path is relative to the root of the repository. | N/A | 518 | | `gzip` | A boolean value indicating whether to gzip the file first. | `false` | 519 | 520 | 521 | 522 | ### Generate Badges 523 | 524 | 525 | ```json 526 | {"gitdown": "badge"} 527 | 528 | ``` 529 | 530 | 531 | Gitdown generates markdown for badges using the environment variables, e.g. if it is an NPM badge, Gitdown will lookup the package name from `package.json`. 532 | 533 | Badges are generated using http://shields.io/. 534 | 535 | 536 | 537 | #### Supported Services 538 | 539 | | Name | Description | 540 | | --- | --- | 541 | | `npm-version` | [NPM](https://www.npmjs.org/) package version. | 542 | | `bower-version` | [Bower](http://bower.io/) package version. | 543 | | `travis` | State of the [Travis](https://travis-ci.org/) build. | 544 | | `david` | [David](https://david-dm.org/) state of the dependencies. | 545 | | `david-dev` | [David](https://david-dm.org/) state of the development dependencies. | 546 | | `waffle` | Issues ready on [Waffle](https://waffle.io/) board. | 547 | | `gitter` | Join [Gitter](https://gitter.im/) chat. | 548 | | `coveralls` | [Coveralls](https://coveralls.io/). | 549 | | `codeclimate-gpa` | [Code Climate](https://codeclimate.com/) GPA. | 550 | | `codeclimate-coverage` | [Code Climate](https://codeclimate.com/) test coverage. | 551 | | `appveyor` | [AppVeyor](http://www.appveyor.com/) status. | 552 | 553 | What service are you missing? [Raise an issue](https://github.com/gajus/gitdown/issues). 554 | 555 | 556 | 557 | #### Example 558 | 559 | 560 | ```json 561 | {"gitdown": "badge", "name": "npm-version"} 562 | {"gitdown": "badge", "name": "travis"} 563 | {"gitdown": "badge", "name": "david"} 564 | 565 | ``` 566 | 567 | 568 | Generates: 569 | 570 | ```markdown 571 | [![NPM version](http://img.shields.io/npm/v/gitdown.svg?style=flat-square)](https://www.npmjs.org/package/gitdown) 572 | [![Travis build status](http://img.shields.io/travis/brettz9/gitdown/update-readme.svg?style=flat-square)](https://travis-ci.org/brettz9/gitdown) 573 | [![Dependency Status](https://img.shields.io/david/brettz9/gitdown.svg?style=flat-square)](https://david-dm.org/brettz9/gitdown) 574 | 575 | ``` 576 | 577 | 578 | 579 | #### JSON Configuration 580 | 581 | | Name | Description | Default | 582 | | --- | --- | --- | 583 | | `name` | Name of the service. | N/A | 584 | 585 | 586 | 587 | ### Print Date 588 | 589 | 590 | ```json 591 | {"gitdown": "date"} 592 | 593 | ``` 594 | 595 | 596 | Prints a string formatted according to the given [moment format](http://momentjs.com/docs/#/displaying/format/) string using the current time. 597 | 598 | 599 | 600 | #### Example 601 | 602 | 603 | ```json 604 | {"gitdown": "date"} 605 | {"gitdown": "date", "format": "YYYY"} 606 | 607 | ``` 608 | 609 | 610 | Generates: 611 | 612 | ```markdown 613 | 1563038327 614 | 2019 615 | 616 | ``` 617 | 618 | 619 | 620 | #### JSON Configuration 621 | 622 | | Name | Description | Default | 623 | | --- | --- | --- | 624 | | `format` | [Moment format](http://momentjs.com/docs/#/displaying/format/). | `X` (UNIX timestamp) | 625 | 626 | 627 | 628 | ### Gitinfo 629 | 630 | 631 | ```json 632 | {"gitdown": "gitinfo"} 633 | 634 | ``` 635 | 636 | 637 | [Gitinfo](https://github.com/gajus/gitinfo) gets info about the local GitHub repository. 638 | 639 | 640 | 641 | #### Example 642 | 643 | 644 | ```json 645 | {"gitdown": "gitinfo", "name": "username"} 646 | {"gitdown": "gitinfo", "name": "name"} 647 | {"gitdown": "gitinfo", "name": "url"} 648 | {"gitdown": "gitinfo", "name": "branch"} 649 | 650 | ``` 651 | 652 | 653 | ``` 654 | brettz9 655 | gitdown 656 | https://github.com/brettz9/gitdown 657 | update-readme 658 | 659 | ``` 660 | 661 | 662 | 663 | #### Supported Properties 664 | 665 | |Name|Description| 666 | |---|---| 667 | |`username`|Username of the repository author.| 668 | |`name`|Repository name.| 669 | |`url`|Repository URL.| 670 | |`branch`|Current branch name.| 671 | 672 | 673 | 674 | #### JSON Configuration 675 | 676 | |Name|Description|Default| 677 | |---|---|---| 678 | |`name`|Name of the property.|N/A| 679 | 680 | 681 | 682 | #### Parser Configuration 683 | 684 | |Name|Description|Default| 685 | |---|---|---| 686 | |`gitinfo.defaultBranchName`|Default branch to use when the current branch name cannot be resolved.|N/A| 687 | |`gitinfo.gitPath`|Path to the `.git/` directory or a descendant. | `__dirname` of the script constructing an instance of `Gitdown`.| 688 | 689 | 690 | 691 | 692 | ## Recipes 693 | 694 | 695 | 696 | ### Automating Gitdown 697 | 698 | Use [Husky](https://www.npmjs.com/package/husky) to check if user generated README.md before committing his changes. 699 | 700 | ```json 701 | "husky": { 702 | "hooks": { 703 | "pre-commit": "npm run lint && npm run test && npm run build", 704 | "pre-push": "gitdown ./.README/README.md --output-file ./README.md --check", 705 | } 706 | } 707 | 708 | ``` 709 | 710 | `--check` attributes makes Gitdown check if the target file differes from the source template. If the file differs then the program exits with an error code and message: 711 | 712 | > Gitdown destination file does not represent the current state of the template. 713 | 714 | Do not automate generating and committing documentation: automating commits will result in a noisy commit log. 715 | --------------------------------------------------------------------------------