├── .npmrc
├── .gitignore
├── scripts
├── doctest
├── prepublish
└── test
├── .config
├── .eslintrc.json
├── .circleci
└── config.yml
├── CONTRIBUTING.md
├── examples
├── fp.md
└── fp.js
├── package.json
├── LICENSE
├── README.md
└── bin
└── transcribe
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /npm-debug.log
3 |
--------------------------------------------------------------------------------
/scripts/doctest:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euf -o pipefail
3 |
4 | node_modules/.bin/doctest --module commonjs --prefix . -- examples/fp.js
5 |
--------------------------------------------------------------------------------
/.config:
--------------------------------------------------------------------------------
1 | repo-owner = davidchambers
2 | repo-name = transcribe
3 | author-name = David Chambers
4 | source-files = bin/transcribe examples/fp.js
5 | readme-source-files =
6 | module-type = commonjs
7 |
--------------------------------------------------------------------------------
/scripts/prepublish:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euf -o pipefail
3 |
4 | node_modules/.bin/sanctuary-prepublish "$@"
5 |
6 | bin/transcribe \
7 | --url "https://github.com/davidchambers/transcribe/blob/v$VERSION/{filename}#L{line}" \
8 | -- examples/fp.js >examples/fp.md
9 | git add examples/fp.md
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["./node_modules/sanctuary-style/eslint.json"],
4 | "env": {"node": true},
5 | "overrides": [
6 | {
7 | "files": ["README.md"],
8 | "rules": {
9 | "no-unused-vars": ["error", {"varsIgnorePattern": "^map$"}]
10 | }
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | build:
5 | docker:
6 | - image: circleci/node:14
7 | environment:
8 | NPM_CONFIG_COLOR: false
9 | NPM_CONFIG_LOGLEVEL: warn
10 | NPM_CONFIG_PROGRESS: false
11 | steps:
12 | - checkout
13 | - run: npm install
14 | - run: npm test
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 1. Update local main branch:
4 |
5 | $ git checkout main
6 | $ git pull upstream main
7 |
8 | 2. Create feature branch:
9 |
10 | $ git checkout -b feature-x
11 |
12 | 3. Make one or more atomic commits, and ensure that each commit has a
13 | descriptive commit message. Commit messages should be line wrapped
14 | at 72 characters.
15 |
16 | 4. Run `npm test`, and address any errors. Preferably, fix commits in place
17 | using `git rebase` or `git commit --amend` to make the changes easier to
18 | review.
19 |
20 | 5. Push:
21 |
22 | $ git push origin feature-x
23 |
24 | 6. Open a pull request.
25 |
--------------------------------------------------------------------------------
/examples/fp.md:
--------------------------------------------------------------------------------
1 | ### `map :: (a -> b) -> Array a -> Array b`
2 |
3 | Transforms a list of elements of type `a` into a list of elements
4 | of type `b` using the provided function of type `a -> b`.
5 |
6 | ```javascript
7 | > map (String) ([1, 2, 3, 4, 5])
8 | ['1', '2', '3', '4', '5']
9 | ```
10 |
11 | ### `filter :: (a -> Boolean) -> Array a -> Array a`
12 |
13 | Returns the list of elements which satisfy the provided predicate.
14 |
15 | ```javascript
16 | > filter (function(n) { return n % 2 === 0; }) ([1, 2, 3, 4, 5])
17 | [2, 4]
18 | ```
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transcribe",
3 | "version": "1.1.2",
4 | "description": "Generate Markdown documentation from code comments",
5 | "license": "MIT",
6 | "homepage": "https://github.com/davidchambers/transcribe",
7 | "bugs": "https://github.com/davidchambers/transcribe/issues",
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/davidchambers/transcribe.git"
11 | },
12 | "bin": "./bin/transcribe",
13 | "files": [
14 | "/LICENSE",
15 | "/README.md",
16 | "/bin/transcribe",
17 | "/package.json"
18 | ],
19 | "engines": {
20 | "node": ">=6.0.0"
21 | },
22 | "dependencies": {
23 | "commander": "2.8.x",
24 | "sanctuary": "3.1.0"
25 | },
26 | "devDependencies": {
27 | "sanctuary-scripts": "6.0.x"
28 | },
29 | "scripts": {
30 | "doctest": "sanctuary-doctest",
31 | "lint": "sanctuary-lint",
32 | "release": "sanctuary-release",
33 | "test": "npm run lint && sanctuary-test && npm run doctest"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/fp.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //# map :: (a -> b) -> Array a -> Array b
4 | //.
5 | //. Transforms a list of elements of type `a` into a list of elements
6 | //. of type `b` using the provided function of type `a -> b`.
7 | //.
8 | //. ```javascript
9 | //. > map (String) ([1, 2, 3, 4, 5])
10 | //. ['1', '2', '3', '4', '5']
11 | //. ```
12 | const map = f => xs => {
13 | const output = [];
14 | for (let idx = 0; idx < xs.length; idx += 1) {
15 | output.push (f (xs[idx]));
16 | }
17 | return output;
18 | };
19 | exports.map = map;
20 |
21 | //# filter :: (a -> Boolean) -> Array a -> Array a
22 | //.
23 | //. Returns the list of elements which satisfy the provided predicate.
24 | //.
25 | //. ```javascript
26 | //. > filter (n => n % 2 === 0) ([1, 2, 3, 4, 5])
27 | //. [2, 4]
28 | //. ```
29 | const filter = pred => xs => {
30 | const output = [];
31 | for (let idx = 0; idx < xs.length; idx += 1) {
32 | if (pred (xs[idx])) {
33 | output.push (xs[idx]);
34 | }
35 | }
36 | return output;
37 | };
38 | exports.filter = filter;
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 David Chambers
2 | Copyright (c) 2019 Plaid Technologies, Inc.
3 |
4 | Permission is hereby granted, free of charge, to any person
5 | obtaining a copy of this software and associated documentation
6 | files (the "Software"), to deal in the Software without
7 | restriction, including without limitation the rights to use,
8 | copy, modify, merge, publish, distribute, sublicense, and/or
9 | sell copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 | OTHER DEALINGS IN THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/scripts/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euf -o pipefail
3 |
4 | rst=$'\x1B[0m'
5 | red=$'\x1B[0;31m'
6 | grn=$'\x1B[0;32m'
7 | cyn=$'\x1B[0;36m'
8 | rev=$'\x1B[7m'
9 |
10 | function highlight {
11 | local output
12 | output="${1}${2}${rst}"
13 | output="${output//$'\n'/"${rev}\\n${rst}${1}"$'\n'}"
14 | output="${output//$'\xE2\x81\xA0'/"${rev}${rst}${1}"}"
15 | printf '%s' "$output"
16 | }
17 |
18 | function compare {
19 | if [[ "$3" == "$2" ]] ; then
20 | printf '%s\u2714%s %s\n' "$grn" "$rst" "$1"
21 | else
22 | printf '%s\u2718%s %s\n' "$red" "$rst" "$1"
23 | printf '%s\n' "$cyn---------------------------------- expected ----------------------------------$rst"
24 | highlight "$grn" "$2"
25 | printf '%s\n' "$cyn----------------------------------- actual -----------------------------------$rst"
26 | highlight "$red" "$3"
27 | printf '%s\n' "$cyn------------------------------------------------------------------------------$rst"
28 | exit 1
29 | fi
30 | }
31 |
32 | set +e
33 | IFS='' read -r -d '' unix <<'EOF'
34 | //# identity :: a -> a
35 | //.
36 | //. The identity function. Returns its argument.
37 | //.
38 | //. ```javascript
39 | //. > identity (1)
40 | //. 1
41 | //. ```
42 | function identity(x) {
43 | return x;
44 | }
45 | exports.identity = identity;
46 | EOF
47 | IFS='' read -r -d '' dos <<'EOF'
48 | //# identity :: a -> a
49 | //.
50 | //. The identity function. Returns its argument.
51 | //.
52 | //. ```javascript
53 | //. > identity (1)
54 | //. 1
55 | //. ```
56 | function identity(x) {
57 | return x;
58 | }
59 | exports.identity = identity;
60 | EOF
61 | IFS='' read -r -d '' expected <<'EOF'
62 | ### `identity :: a -> a`
63 |
64 | The identity function. Returns its argument.
65 |
66 | ```javascript
67 | > identity (1)
68 | 1
69 | ```
70 | EOF
71 | set -e
72 |
73 | # Append an underscore to preserve trailing newlines, then strip it.
74 | actual="$(bin/transcribe --url XXX -- <(printf '%s' "$unix") && printf _)"
75 | compare 'Unix line endings' "$expected" "${actual%_}"
76 |
77 | actual="$(bin/transcribe --url XXX -- <(printf '%s' "$dos") && printf _)"
78 | compare 'Windows line endings' "$expected" "${actual%_}"
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Transcribe
2 |
3 | Transcribe is a simple program which generates Markdown documentation from code
4 | comments.
5 |
6 | The general idea is that each "export" should be accompanied by a "docstring".
7 | The first line of the "docstring" should be a Haskell-inspired type signature
8 | in the form ` :: `.
9 |
10 | ```javascript
11 | //# map :: (a -> b) -> Array a -> Array b
12 | //.
13 | //. Transforms a list of elements of type `a` into a list of elements
14 | //. of type `b` using the provided function of type `a -> b`.
15 | //.
16 | //. ```javascript
17 | //. > map (String) ([1, 2, 3, 4, 5])
18 | //. ['1', '2', '3', '4', '5']
19 | //. ```
20 | const map = f => xs => {
21 | const output = [];
22 | for (let idx = 0; idx < xs.length; idx += 1) {
23 | output.push (f (xs[idx]));
24 | }
25 | return output;
26 | };
27 | ```
28 |
29 | The __`--heading-prefix`__ option specifies which lines in the source files
30 | contain type signatures to become headings in the output. The default value
31 | is `//#`; specify a different value if using a different comment style. For
32 | example:
33 |
34 | --heading-prefix '#%'
35 |
36 | The __`--prefix`__ option specifies which lines in the source files should
37 | appear in the output along with the lines prefixed with ``.
38 | The default value is `//.`; specify a different value if using a different
39 | comment style. For example:
40 |
41 | --prefix '#.'
42 |
43 | Each line beginning with zero or more whitespace characters followed by the
44 | prefix is included in the output, sans prefix and leading whitespace. The `.`
45 | in the default prefix makes it possible to be selective about which comments
46 | are included in the output: comments such as `// Should never get here!` will
47 | be ignored.
48 |
49 | The __`--url`__ option specifies a template for generating links to specific
50 | lines of source code on GitHub or another code-hosting site. The value should
51 | include `{filename}` and `{line}` placeholders to be replaced with the filename
52 | and line number of each of the signature lines. For example:
53 |
54 | --url 'https://github.com/plaid/sanctuary/blob/v0.4.0/{filename}#L{line}'
55 |
56 | Avoid pointing to a moving target: include a tag name or commit hash rather
57 | than a branch name such as `main`.
58 |
59 | The __`--heading-level`__ option specifies the heading level, an integer in
60 | range \[1, 6\]. The default value is `3`, which corresponds to an ``
61 | element in HTML. Specify a different value if desired. For example:
62 |
63 | --heading-level 4
64 |
65 | The __`--insert-into`__ option specifies the name of a file into which
66 | Transcribe will insert the generated output. By default, Transcribe writes to
67 | stdout. However, if `--insert-into` is provided, Transcribe will insert the
68 | output in the specified file between two special tags: `` and
69 | ``. For example:
70 |
71 | --insert-into README.md
72 |
73 | The options should be followed by one or more filenames. The filenames may
74 | be separated from the options by `--`. Files are processed in the order in
75 | which they are specified.
76 |
77 | Here's a complete example:
78 |
79 | $ transcribe \
80 | > --url 'https://github.com/plaid/example/blob/v1.2.3/{filename}#L{line}' \
81 | > -- examples/fp.js
82 | ### `map :: (a -> b) -> Array a -> Array b`
83 |
84 | Transforms a list of elements of type `a` into a list of elements
85 | of type `b` using the provided function of type `a -> b`.
86 |
87 | ```javascript
88 | > map (String) ([1, 2, 3, 4, 5])
89 | ['1', '2', '3', '4', '5']
90 | ```
91 |
92 | ### `filter :: (a -> Boolean) -> Array a -> Array a`
93 |
94 | Returns the list of elements which satisfy the provided predicate.
95 |
96 | ```javascript
97 | > filter (n => n % 2 === 0) ([1, 2, 3, 4, 5])
98 | [2, 4]
99 | ```
100 |
101 | By default, the output is written to stdout. One could redirect it to a file to
102 | generate lightweight API documentation:
103 |
104 | $ printf '\n## API\n\n' >>README.md
105 | $ transcribe \
106 | > --url 'https://github.com/plaid/example/blob/v1.2.3/{filename}#L{line}' \
107 | > -- examples/fp.js >>README.md
108 |
109 | Reading from stdin is not currently supported.
110 |
111 | One could also insert the output into an existing file by providing the
112 | `--insert-into` option:
113 |
114 | $ transcribe \
115 | > --url 'https://github.com/plaid/example/blob/v1.2.3/{filename}#L{line}' \
116 | > --insert-into README.md
117 | > -- examples/fp.js
118 |
--------------------------------------------------------------------------------
/bin/transcribe:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const fs = require ('fs');
6 |
7 | const program = require ('commander');
8 | const {create, env} = require ('sanctuary');
9 |
10 | const pkg = require ('../package.json');
11 |
12 |
13 | const {
14 | Just,
15 | Nothing,
16 | Pair,
17 | alt,
18 | append,
19 | array,
20 | compose: B,
21 | flip,
22 | fromMaybe,
23 | join,
24 | joinWith,
25 | lines,
26 | map,
27 | pair,
28 | pipe,
29 | reduce,
30 | snd,
31 | splitOn,
32 | stripPrefix,
33 | unfoldr,
34 | unlines,
35 | } = create ({checkTypes: false, env});
36 |
37 | const map2 = B (map) (map);
38 | const map3 = B (map) (map2);
39 | const map4 = B (map) (map3);
40 |
41 | // replace :: (String | RegExp) -> String -> String -> String
42 | const replace = patt => repl => s => s.replace (patt, repl);
43 |
44 | // esc :: String -> String
45 | const esc = pipe ([
46 | replace (/&/g) ('&'),
47 | replace (/ String
52 | const nbsp = replace (/[ ]/g) ('\u00A0');
53 |
54 | // controlWrapping :: String -> String
55 | const controlWrapping = pipe ([
56 | splitOn (' :: '),
57 | map (splitOn (' => ')),
58 | map2 (s => join (unfoldr (array (Nothing)
59 | (p => array (Just (Pair ([p, '']) ([])))
60 | (q => B (Just)
61 | (Pair ([p, nbsp (q)])))))
62 | (s.split (/([(][^()]+[)])/)))),
63 | map3 (splitOn (' -> ')),
64 | map4 (nbsp),
65 | map3 (joinWith (' -> ')),
66 | map2 (joinWith ('')),
67 | map (joinWith (' => ')),
68 | joinWith (' :: '),
69 | ]);
70 |
71 | // formatSignature :: Options -> String -> Number -> String -> String
72 | const formatSignature = options => filename => num => signature =>
73 | '#'.repeat (options.headingLevel) + ' ' +
74 | '' +
78 | '`' + controlWrapping (signature) + '`' +
79 | '';
80 |
81 | // parseLine :: Options -> String -> Number -> String -> String
82 | const parseLine = options => filename => num => pipe ([
83 | replace (/^\s+/) (''),
84 | s => alt (map (replace (/^[ ]/) (''))
85 | (stripPrefix (options.prefix) (s)))
86 | (map (B (formatSignature (options) (filename) (num))
87 | (replace (/^[ ]/) ('')))
88 | (stripPrefix (options.headingPrefix) (s))),
89 | fromMaybe (''),
90 | ]);
91 |
92 | // parseFile :: Options -> String -> String
93 | const parseFile = options => filename =>
94 | unlines (snd (reduce (flip (line =>
95 | pair (num => B (Pair (num + 1))
96 | (append (parseLine (options)
97 | (filename)
98 | (num)
99 | (line))))))
100 | (Pair (1) ([]))
101 | (lines (fs.readFileSync (filename, 'utf8')))));
102 |
103 | // transcribe :: Options -> Array String -> String
104 | const transcribe = options => pipe ([
105 | map (parseFile (options)),
106 | joinWith ('\n\n'),
107 | replace (/\n{3,}/g) ('\n\n'),
108 | replace (/^\n+/) (''),
109 | replace (/\n+$/) ('\n'),
110 | ]);
111 |
112 | program
113 | .version (pkg.version)
114 | .usage ('[options] ')
115 | .description (pkg.description)
116 | .option ('--heading-level ', 'heading level in range [1, 6] (default: 3)')
117 | .option ('--heading-prefix ', 'prefix for heading lines (default: "//#")')
118 | .option ('--insert-into ',
119 | 'name of a file into which Transcribe will insert generated output')
120 | .option ('--prefix ', 'prefix for non-heading lines (default: "//.")')
121 | .option ('--url ', 'source URL with {filename} and {line} placeholders')
122 | .parse (process.argv);
123 |
124 | let valid = true;
125 |
126 | if (!(program.headingLevel == null || /^[1-6]$/.test (program.headingLevel))) {
127 | process.stderr.write ('Invalid --heading-level\n');
128 | valid = false;
129 | }
130 |
131 | if (program.url == null) {
132 | process.stderr.write ('No --url template specified\n');
133 | valid = false;
134 | }
135 |
136 | if (!valid) {
137 | process.exit (1);
138 | }
139 |
140 | // defaultTo :: a -> a? -> a
141 | const defaultTo = x => y => y == null ? x : y;
142 |
143 | // output :: String
144 | const output =
145 | transcribe ({headingLevel: Number (defaultTo ('3') (program.headingLevel)),
146 | headingPrefix: defaultTo ('//#') (program.headingPrefix),
147 | prefix: defaultTo ('//.') (program.prefix),
148 | url: program.url})
149 | (program.args);
150 |
151 | if (program.insertInto == null) {
152 | process.stdout.write (output);
153 | } else {
154 | // Read the file, insert the output, and write to the file again
155 | fs.writeFileSync (
156 | program.insertInto,
157 | replace (/()[\s\S]*?()/)
158 | ('$1\n\n' + output + '\n$2')
159 | (fs.readFileSync (program.insertInto, 'utf8'))
160 | );
161 | }
162 |
--------------------------------------------------------------------------------