├── .circleci └── config.yml ├── .config ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── transcribe ├── examples ├── fp.js └── fp.md ├── package.json └── scripts ├── doctest ├── prepublish └── test /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------