├── 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 | [](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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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 = '';
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('[](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('[](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('[](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('[](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('[](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 | '[](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('[](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('[](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 | '[](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 | '[](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 | [](https://gitspo.com/mentions/gajus/gitdown)
2 | [](https://www.npmjs.org/package/gitdown)
3 | [](https://travis-ci.org/brettz9/gitdown)
4 | [](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 | [](https://www.npmjs.org/package/gitdown)
572 | [](https://travis-ci.org/brettz9/gitdown)
573 | [](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 |
--------------------------------------------------------------------------------