boondoc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +345 -0
  6. data/exe/boondoc +6 -0
  7. data/lib/boondoc/assets.rb +112 -0
  8. data/lib/boondoc/cli.rb +99 -0
  9. data/lib/boondoc/config.rb +201 -0
  10. data/lib/boondoc/containment.rb +40 -0
  11. data/lib/boondoc/context.rb +48 -0
  12. data/lib/boondoc/core_constants.rb +23 -0
  13. data/lib/boondoc/docstring_parser.rb +119 -0
  14. data/lib/boondoc/filter.rb +34 -0
  15. data/lib/boondoc/frontend/comments.rb +67 -0
  16. data/lib/boondoc/frontend/prism_parser.rb +40 -0
  17. data/lib/boondoc/frontend/visitor.rb +359 -0
  18. data/lib/boondoc/generator.rb +116 -0
  19. data/lib/boondoc/linker.rb +175 -0
  20. data/lib/boondoc/links.rb +78 -0
  21. data/lib/boondoc/markdown.rb +90 -0
  22. data/lib/boondoc/model/code_object.rb +165 -0
  23. data/lib/boondoc/model/docstring.rb +70 -0
  24. data/lib/boondoc/model/registry.rb +150 -0
  25. data/lib/boondoc/path_map.rb +108 -0
  26. data/lib/boondoc/preview.rb +81 -0
  27. data/lib/boondoc/rake_task.rb +39 -0
  28. data/lib/boondoc/references.rb +59 -0
  29. data/lib/boondoc/renderer/base.rb +50 -0
  30. data/lib/boondoc/renderer/docstring.rb +140 -0
  31. data/lib/boondoc/renderer/index.rb +49 -0
  32. data/lib/boondoc/renderer/method_section.rb +84 -0
  33. data/lib/boondoc/renderer/namespace_page.rb +165 -0
  34. data/lib/boondoc/reporter.rb +36 -0
  35. data/lib/boondoc/stitcher.rb +89 -0
  36. data/lib/boondoc/type_parser.rb +78 -0
  37. data/lib/boondoc/version.rb +5 -0
  38. data/lib/boondoc/writer.rb +163 -0
  39. data/lib/boondoc/yard_bridge.rb +113 -0
  40. data/lib/boondoc/yardopts.rb +165 -0
  41. data/lib/boondoc.rb +48 -0
  42. metadata +116 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f854aff827f84fc734634d7e01d80908ff4f347532ab20cce7996771876b9f1f
4
+ data.tar.gz: 3ebdb2551d145c72e70639cad96e5f31366fbe98e21db115da1a8964f9b8d2a2
5
+ SHA512:
6
+ metadata.gz: e7cdde7b2696fa3bca5cf2b8dcb7825ea28ffe4c2f69db5fa41444330fa7d6910b5ec683f11ab21106cca39c2978fe247dcd2b701d4d27f15753bbb6fb912427
7
+ data.tar.gz: d78c6cca549633e526d9f979d59d0bc653fb4339c23f57a51370ef8b57c14e296c77ab3f8f803c58818e356bc188b07d5f80294449a1179a178b401e7a064878
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-10-25
4
+
5
+ ### ✨ Features
6
+ - **Markdown docs from YARD comments:** Prism-based parsing of YARD-style comments, with one markdown page per class or module.
7
+ - **Stitched README and extra files**, with reference links between pages.
8
+ - **YARD bridge (optional):** `--yard-macros` hands YARD only the files that contain macros or directives, and warns when a project has none.
9
+ - **Assets:** the `assets` option (`--assets`, or `--asset` in `.yardopts`) copies files such as images into the output, and `copy_linked_assets` also copies the files that pages embed. Links to a copied file, in the README, stitched files, and doc comments, point at the copy. A file that is no longer copied is removed like a stale page.
10
+ - **`.yardopts` support:** reads the source globs, the extra files after `-` and `--files`, `--readme`, `--title`, `--private`, `--protected`, and `--exclude`. `.boondoc.yml` and the command line win over it, key by key, and `--no-yardopts` (or `yardopts: false`) skips it. `--output-dir` is never used, and other options are listed as ignored.
11
+ - **`boondoc serve`** warns when `--host` is not a loopback address. Links to source files 404 under `serve`, because they point outside the output directory.
12
+ - **Case-safe file names:** classes and modules such as `Guard::CLI` and `Guard::Cli` now get their kind in the file name (`CLI.class.md`, `Cli.module.md`) instead of overwriting each other on case-insensitive filesystems.
13
+
14
+ ### 🔒 Security
15
+ - `{include:file:...}`, `readme`, and `files` only read files inside the project, and Boondoc never writes or deletes through a symlink inside the output directory, or outside it.
16
+ - The output directory must be inside the project and may not be, or sit below, a symlink. `--allow-outside-output` (command line and Rake options only, never `.boondoc.yml`) allows an output directory outside the project; that directory is not checked for symlinks.
17
+ - Ruby files that resolve outside the project (an absolute glob, `../`, or a symlink) are skipped with a warning, so `--strict` fails.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "Boondoc" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["joesharp13@gmail.com"](mailto:"joesharp13@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Joe Sharp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,345 @@
1
+ # Boondoc
2
+
3
+ Markdown-only API documentation for Ruby.
4
+
5
+ Boondoc reads the YARD-style comments you already write and produces plain
6
+ GitHub-flavored markdown: one file per class or module, an index, and working
7
+ relative links between them. There is no HTML and no site to deploy or host. The
8
+ output renders directly in a repository's `docs/` folder.
9
+
10
+ Because everything is markdown from start to finish, your README, changelog
11
+ and guides are stitched in as they are, and `{Foo#bar}` references inside them
12
+ become real links.
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ bundle add boondoc --group development
18
+ ```
19
+
20
+ or
21
+
22
+ ```sh
23
+ gem install boondoc
24
+ ```
25
+
26
+ Boondoc needs Ruby 3.3 or newer.
27
+
28
+ ## Usage
29
+
30
+ ```sh
31
+ boondoc # document lib/**/*.rb into docs/
32
+ boondoc 'app/**/*.rb' lib -o docs/api # choose the sources and the output
33
+ boondoc --files CHANGELOG.md 'guides/*.md'
34
+ ```
35
+
36
+ | Option | Meaning |
37
+ | --- | --- |
38
+ | `-o`, `--output DIR` | Output directory. Default `docs`. It must be inside the project, see `--allow-outside-output`. |
39
+ | `--index NAME` | Name of the index page. Default `README.md`, which GitHub shows when browsing the folder. |
40
+ | `--readme FILE` | File used as the top of the index page. Default `README.md` if present. |
41
+ | `--skip-readme` | Leave the README out of the index page. |
42
+ | `--files a.md b/*.md` | Extra files to stitch in. |
43
+ | `--assets FILES` | Files, directories, or globs to copy into the output, such as images. `FROM:DIR` copies into `DIR` instead. See [Assets](#assets). |
44
+ | `--copy-linked-assets` | Also copy the files that pages embed, such as images in the README and in doc comments. |
45
+ | `--exclude GLOBS` | Ruby files to leave out. |
46
+ | `--title TITLE` | Index title when there is no README. |
47
+ | `--private`, `--protected` | Include those methods (and `@private` objects). |
48
+ | `--yard-macros` | Expand `@!macro`, `@!method`, and `@!attribute` with the yard gem. See below. |
49
+ | `--no-yardopts` | Do not read `.yardopts`. See [Reading `.yardopts`](#reading-yardopts). |
50
+ | `--strict` | Exit non-zero on any warning, such as a link that does not resolve. |
51
+ | `--force` | Overwrite files in the output directory that Boondoc did not write. |
52
+ | `--allow-outside-output` | Let the output directory be outside the project, for docs kept outside the repository. This is a security risk if you do not trust the project, see [Where Boondoc reads and writes](#where-boondoc-reads-and-writes). Command line and Rake options only, never `.boondoc.yml`. |
53
+ | `-c`, `--config FILE` | Config file. Default `.boondoc.yml`. |
54
+ | `-q`, `--quiet` | Only print warnings and errors. |
55
+
56
+ The same settings can live in `.boondoc.yml`:
57
+
58
+ ```yaml
59
+ sources:
60
+ - lib/**/*.rb
61
+ output: docs
62
+ files:
63
+ - CHANGELOG.md
64
+ - guides/*.md
65
+ strict: true
66
+ ```
67
+
68
+ ### Rake
69
+
70
+ ```ruby
71
+ require 'boondoc/rake_task'
72
+
73
+ Boondoc::RakeTask.new(:docs) do |t|
74
+ t.options = { files: ['CHANGELOG.md'], strict: true }
75
+ end
76
+ ```
77
+
78
+ ## What gets written
79
+
80
+ ```
81
+ docs/README.md your README, then an index of the API and the stitched files
82
+ docs/Foo.md one page per class or module
83
+ docs/Foo/Bar.md nested namespaces mirror the directory structure
84
+ docs/top-level.md top-level methods and constants, if there are any
85
+ docs/files/CHANGELOG.md stitched files
86
+ docs/.boondoc-manifest the list of files boondoc wrote
87
+ ```
88
+
89
+ Classes and modules whose names differ only by case, such as `Guard::CLI` and
90
+ `Guard::Cli`, would share a file on case-insensitive filesystems. Boondoc adds
91
+ the kind to both names (`Guard/CLI.class.md` and `Guard/Cli.module.md`, and
92
+ `Guard/Cli.module/` for the children of the module), so the output is the same
93
+ on every platform. Two of the same kind get a number as well (`Cli.class-2.md`).
94
+ Names that don't collide are left alone.
95
+
96
+ On every run Boondoc removes pages it wrote earlier that no longer correspond
97
+ to anything, and it refuses to overwrite a file it did not write unless you
98
+ pass `--force`. Files you add to the output directory by hand are left alone.
99
+
100
+ ### Where Boondoc reads and writes
101
+
102
+ Boondoc limits what a project can make it read or write, because the project
103
+ (its comments, symlinks, and `.boondoc.yml`) may come from someone else, such as
104
+ a pull request that CI builds docs for.
105
+
106
+ - The output directory must be inside the project and must not be, or sit
107
+ below, a symlink. `.boondoc.yml` cannot move it outside.
108
+ - Ruby files that resolve outside the project, through an absolute path,
109
+ `../` or a symlink, are skipped with a warning, and `--strict` turns the
110
+ warning into a failure. A README or extra file outside the project is an
111
+ error. Run Boondoc from the directory that contains everything you want
112
+ documented.
113
+ - `{include:file:...}`, `readme`, and `files` only read files inside the project.
114
+ - Boondoc never writes or deletes through a symlink inside the output
115
+ directory, or outside it.
116
+
117
+ `--allow-outside-output` (on the command line, or `allow_outside_output: true`
118
+ in Rake options) lifts the first rule, for docs that are kept outside the
119
+ repository. An output directory outside the project is not checked for
120
+ symlinks. It is a security risk if you do not trust the project: its
121
+ `.boondoc.yml` then chooses where files are written and, with `force: true`,
122
+ which files are overwritten. Only pass it for projects you trust, and a
123
+ `.boondoc.yml` that sets it is rejected.
124
+
125
+ Each method gets an explicit anchor (`<a id="method-i-name"></a>`), because
126
+ GitHub's heading anchors cannot tell `save`, `save?`, and `save!` apart. That
127
+ anchor is the only HTML in the output.
128
+
129
+ ## Viewing the docs
130
+
131
+ The output is plain markdown with relative links, so anything that renders
132
+ markdown and follows relative links can browse it.
133
+
134
+ **GitHub.** Commit the output directory and open it on github.com. GitHub
135
+ renders the folder's `README.md` under the file list, which is why the index
136
+ page has that name by default, and the links between pages and to method
137
+ anchors work as they are.
138
+
139
+ **VS Code.** Open `docs/README.md` and run *Markdown: Open Preview*
140
+ (`Cmd+Shift+V`, or `Ctrl+Shift+V` on Windows and Linux). Links in the preview
141
+ open the other pages, and no extension is needed.
142
+
143
+ **In a browser, with `boondoc serve`.** This starts a local preview styled
144
+ like github.com:
145
+
146
+ ```sh
147
+ boondoc serve # serves docs/ at http://127.0.0.1:3333
148
+ boondoc serve -o docs/api --port 4000
149
+ ```
150
+
151
+ It runs the
152
+ [gh-markdown-preview](https://github.com/yusukebe/gh-markdown-preview)
153
+ extension of the [GitHub CLI](https://cli.github.com), so `gh` has to be
154
+ installed. If the extension is missing, Boondoc asks before installing it.
155
+ `serve` reads the output directory from `-o` or `.boondoc.yml`, and it does
156
+ not generate anything: run `boondoc` first.
157
+
158
+ Links to source files (`../../lib/...`) point outside the output directory, so
159
+ they 404 under `serve`. They work on GitHub and in VS Code.
160
+
161
+ Serve only docs you trust, and keep the default host. The extension returns any
162
+ file the current user can read through its `/__/md?path=` address, sends the
163
+ markdown it renders to GitHub's markdown API, and does not check the `Host`
164
+ header. Boondoc warns when `--host` is not a loopback address.
165
+
166
+ To run the extension yourself, start it from inside the output directory:
167
+
168
+ ```sh
169
+ gh extension install yusukebe/gh-markdown-preview
170
+ cd docs && gh markdown-preview --host 127.0.0.1
171
+ ```
172
+
173
+ It always serves files relative to the directory it was started in, so
174
+ `gh markdown-preview docs` from the project root shows the index page but
175
+ every link on it leads to a missing page.
176
+
177
+ Other previewers work if they resolve links relative to the file being viewed.
178
+ Some editor plugins render only a single file and do not follow links at all.
179
+
180
+ ## Stitching and links
181
+
182
+ Anywhere in a docstring, a stitched file, or your README:
183
+
184
+ | Write | Get |
185
+ | --- | --- |
186
+ | `{Foo::Bar}` | a link to that class |
187
+ | `{Foo#bar}`, `{Foo.bar}`, `{#bar}` | a link to that method |
188
+ | `{Foo#bar the bar method}` | the same link with your own text |
189
+ | `{file:CHANGELOG.md}` | a link to a stitched file |
190
+ | `{https://example.com Example}` | an ordinary link |
191
+ | `{include:file:snippets/setup.md}` | the contents of that file, inline |
192
+ | `{include:Foo}` | the docstring of `Foo`, inline |
193
+ | `\{Foo}` | a literal `{Foo}` |
194
+
195
+ References are resolved the way Ruby resolves constants, from the namespace
196
+ they are written in outwards, and methods are also found on superclasses and
197
+ mixins. Fenced code, indented code, and inline code are never touched, and
198
+ braces that do not look like a reference (`{key: value}`) pass through.
199
+
200
+ Relative links and images in stitched files are rewritten so they still point
201
+ at the same thing from their new location.
202
+
203
+ ### Assets
204
+
205
+ Images and other files that pages need are copied into the output, next to the
206
+ pages, so the docs still work when the output directory is published on its
207
+ own. There are two ways to choose them:
208
+
209
+ - `assets` (or `--assets`, or `--asset` in `.yardopts`) names files, directories,
210
+ or globs. A directory is copied with everything under it. `images:media` copies
211
+ into `media/` in the output instead of keeping the project path. Hidden files,
212
+ symlinks that leave the project, and the output directory itself are skipped.
213
+ - `copy_linked_assets: true` (or `--copy-linked-assets`) also copies the files
214
+ that pages *embed*: images, `<img>`, `<source srcset>`, `<video poster>`, and
215
+ `![alt][label]` references. A plain link such as `[the source](lib/foo.rb)`
216
+ still points back into the project, so it is never copied.
217
+
218
+ Copied files keep their project path, or the `:DIR` you gave them. Links to them
219
+ are rewritten to point at the copy, in the README, other stitched files, and doc
220
+ comments, which Boondoc rewrites per page so an image in `Foo::Bar` still resolves
221
+ from `docs/Foo/Bar.md`. Files are copied byte for byte, and a copy that is already
222
+ up to date is not touched.
223
+
224
+ ## Supported tags
225
+
226
+ `@param`, `@option`, `@return`, `@raise`, `@yield`, `@yieldparam`, `@yieldreturn`,
227
+ `@example`, `@overload`, `@see`, `@note`, `@todo`, `@deprecated`, `@abstract`,
228
+ `@since`, `@version`, `@author`, `@api`, `@private`, plus `(see Foo#bar)`
229
+ references. Any other tag is rendered as a labelled line rather than dropped.
230
+
231
+ Docstrings are treated as markdown and passed through unchanged. RDoc markup
232
+ is not converted.
233
+
234
+ ## Compared with YARD and RDoc
235
+
236
+ Boondoc reads the same comments YARD does and aims at one thing YARD and RDoc
237
+ do not do: documentation that is markdown all the way through.
238
+
239
+ | | Boondoc | YARD | RDoc |
240
+ | --- | --- | --- | --- |
241
+ | Output | Markdown files | HTML site, `yri` | HTML site, `ri` |
242
+ | Viewing | GitHub, any markdown previewer | Browser, `yard server` | Browser, `ri` |
243
+ | Comment syntax | YARD tags (`@param`, `@return`, ...) | YARD tags | Prose and directives (`:call-seq:`, `:nodoc:`) |
244
+ | Comment markup | Markdown, passed through | Markdown, RDoc, and others, converted to HTML | RDoc or Markdown, converted to HTML |
245
+ | Ruby parser | [Prism](https://github.com/ruby/prism) | Its own | Prism (as of RDoc 8.1) |
246
+ | Macros and directives | Through YARD, with `--yard-macros` | Yes | No |
247
+ | Plugins and templates | No | Yes | Yes |
248
+ | C extensions | No | Yes | Yes |
249
+ | Ships with Ruby | No | No | Yes |
250
+
251
+ **What is the same as YARD.** The tags, the `[Types]` syntax, references in
252
+ braces, `(see Other#method)`, `@overload`, `@private`, and `@api private`, and
253
+ stitching in extra files. A codebase documented for YARD needs no changes.
254
+ Boondoc understands classes, modules, methods, attributes, constants,
255
+ visibility in all its forms, `module_function`, aliases, mixins, `Struct.new`,
256
+ `Data.define` and `Class.new`.
257
+
258
+ **What is different from YARD.** There is no HTML, no server, no `.yardoc`
259
+ database, no `yri`, no `yard stats`, and no template system. Boondoc does not
260
+ load YARD plugins, and reads only part of `.yardopts` (see
261
+ [below](#reading-yardopts)). Docstrings are treated as markdown and
262
+ are not converted from RDoc markup or Textile. Macros and directives need the
263
+ bridge described below.
264
+
265
+ **How it relates to RDoc.** RDoc is the tool that ships with Ruby. It does
266
+ not use YARD's tags, so comments written for RDoc carry less structure. Boondoc
267
+ will still document such a codebase: every class and method gets a page and a
268
+ signature, comments appear as written, and `:nodoc:` is honoured. RDoc markup
269
+ such as `+code+` or `=== Heading` is left as it is rather than converted.
270
+
271
+ ### Reading `.yardopts`
272
+
273
+ If the project has a `.yardopts`, Boondoc uses these options from it, so a
274
+ codebase set up for YARD needs no extra configuration:
275
+
276
+ | In `.yardopts` | Used as |
277
+ | --- | --- |
278
+ | source globs and directories | `sources` |
279
+ | files after `-`, and `--files a,b` | `files` |
280
+ | `-r`, `--readme`, `--main` | `readme` |
281
+ | `--asset FROM[:DIR]` | `assets` |
282
+ | `--title` | `title` |
283
+ | `--private`, `--protected` | `private`, `protected` |
284
+ | `--exclude REGEXP` | Ruby files whose path matches the regular expression are left out, as in YARD |
285
+
286
+ `.boondoc.yml` and the command line win over `.yardopts`, one option at a time:
287
+ a `files` list in `.boondoc.yml` replaces the one from `.yardopts` instead of
288
+ adding to it. Pass `--no-yardopts`, or set `yardopts: false`, to skip the file.
289
+
290
+ Boondoc does not use `--output-dir`. In a `.yardopts` it names YARD's HTML
291
+ directory, which is usually ignored by git, while Boondoc's output is meant to be
292
+ committed. Following it could send the docs somewhere that is never published,
293
+ so Boondoc keeps writing to `output` (default `docs`) and prints a note. Set
294
+ `output` in `.boondoc.yml`, or pass `-o`, to choose the directory. Every other
295
+ option it does not use, such as `--markup`, `--plugin`, or `--template`, is listed
296
+ in a note too, as are C source globs, since Boondoc documents Ruby only. The notes
297
+ are not warnings, so `--strict` does not fail on them, and `--quiet` hides them.
298
+ `.document` files are not read.
299
+
300
+ `.yardopts` comes from the project, so it has the same limits as `.boondoc.yml`:
301
+ its paths must be inside the project, and it cannot set the output directory.
302
+
303
+ ### Macros and directives
304
+
305
+ Boondoc does not expand YARD macros and directives (`@!macro`, `@!method`,
306
+ `@!attribute`) on its own. If your code relies on them, add the yard gem and
307
+ pass `--yard-macros`:
308
+
309
+ ```ruby
310
+ gem 'yard', group: :development
311
+ ```
312
+
313
+ Prism remains the source of truth. YARD is run as a second pass and only
314
+ contributes the objects that directives create and the expanded docstrings of
315
+ comments that use a macro.
316
+
317
+ ### Performance
318
+
319
+ <picture>
320
+ <source media="(prefers-color-scheme: dark)" srcset="images/benchmark-dark.svg">
321
+ <img alt="Bar charts of the benchmark table in docs/Performance.md, one panel per codebase: Boondoc is the fastest tool on every codebase" src="images/benchmark-light.svg">
322
+ </picture>
323
+
324
+ The full table and how it was measured are in [docs/Performance.md](docs/Performance.md).
325
+
326
+ ## Development
327
+
328
+ ```sh
329
+ bin/setup
330
+ bundle exec rake # specs and RuboCop
331
+ bundle exec boondoc # regenerate docs/ with boondoc itself
332
+ bundle exec guard # RuboCop, specs and a benchmark check on every change
333
+ ```
334
+
335
+ The rendering specs compare against the files in
336
+ `spec/fixtures/sample/expected`. After an intended change to the output,
337
+ regenerate them with `UPDATE_GOLDEN=1 bundle exec rspec spec/boondoc/generator_spec.rb`
338
+ and review the diff.
339
+
340
+ New to the codebase? The [contributor familiarity guide](docs/CONTRIBUTING.md)
341
+ walks through the pipeline and where to start for common changes.
342
+
343
+ ## License
344
+
345
+ Available as open source under the terms of the [MIT License](LICENSE.txt).
data/exe/boondoc ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'boondoc'
5
+
6
+ Boondoc::CLI.start_with_default(ARGV)
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Boondoc
6
+ # The files that are copied into the output directory next to the pages: images, video, PDFs,
7
+ # and anything else a page links to or embeds.
8
+ #
9
+ # Two kinds are copied. The ones named by the `assets` option are always copied. Each entry is a
10
+ # file, a directory (everything under it), or a glob, optionally followed by `:DIR` to copy into
11
+ # that directory of the output instead of keeping the project path. Hidden files, symlinks that
12
+ # leave the project, and anything in the output directory are skipped. With `copy_linked_assets`
13
+ # the files that pages embed are copied too, as they are found while the pages are rendered, so
14
+ # {#files} is only complete once rendering has finished.
15
+ class Assets
16
+ # @param config [Config]
17
+ # @raise [Error] when a named file is outside the project, or two files would be copied to the same path
18
+ def initialize(config)
19
+ @config = config
20
+ @named = Array(config.assets).each_with_object({}) { |entry, map| add(map, entry) }
21
+ @by_source = @named.invert
22
+ @linked = {}
23
+ end
24
+
25
+ # Where a file will be in the output directory, when it is copied there.
26
+ #
27
+ # @param absolute [String] absolute path of a file in the project
28
+ # @param embedded [Boolean] whether the page embeds the file, as an image does
29
+ # @return [String, nil] the path relative to the output directory, or nil when the file is not copied
30
+ def output_for(absolute, embedded: false)
31
+ @by_source[absolute] || (link(absolute) if embedded && @config.copy_linked_assets)
32
+ end
33
+
34
+ # Finds the copy of a path written in a page. A path is first read relative to the output
35
+ # directory, as YARD's `--asset` paths are, and then relative to the project.
36
+ #
37
+ # @param path [String] a path without any fragment or query
38
+ # @param embedded [Boolean] whether the page embeds the file rather than linking to it
39
+ # @return [String, nil] the path relative to the output directory, or nil when nothing is copied
40
+ def copy_for(path, embedded: false)
41
+ output = Pathname.new(path).cleanpath.to_s
42
+ return output if @named.key?(output)
43
+
44
+ output_for(File.expand_path(path, @config.root), embedded: embedded)
45
+ end
46
+
47
+ # @return [Hash{String => String}] absolute source paths, keyed by the path relative to
48
+ # the output directory that each is copied to
49
+ def files
50
+ @named.merge(@linked)
51
+ end
52
+
53
+ private
54
+
55
+ def add(map, entry)
56
+ from, directory = entry.split(':', 2)
57
+ Dir.glob(from, base: @config.root).each do |match|
58
+ sources(match).each do |source, within|
59
+ output = directory ? output_path(directory, within) : source
60
+ absolute = File.expand_path(source, @config.root)
61
+ clash = map.fetch(output, absolute) != absolute
62
+ raise Error, "#{output} would be copied from both #{map[output]} and #{absolute}" if clash
63
+
64
+ map[output] = absolute
65
+ end
66
+ end
67
+ end
68
+
69
+ # @return [Array<Array(String, String)>] each file under a match, as its path in the project and
70
+ # its path relative to the match, which a `:DIR` destination keeps
71
+ def sources(match)
72
+ full = File.expand_path(match, @config.root)
73
+ raise Error, "#{match} is outside the project (#{@config.root})" unless Containment.inside?(full, @config.root)
74
+ return [[relative(full), File.basename(full)]] if File.file?(full)
75
+
76
+ Dir.glob('**/*', base: full).select { |path| named_file?(File.join(full, path)) }
77
+ .map { |path| [relative(File.join(full, path)), path] }
78
+ end
79
+
80
+ def named_file?(full)
81
+ inside = full.delete_prefix(File.join(@config.root, ''))
82
+ File.file?(full) && inside.split('/').none? { |part| part.start_with?('.') } &&
83
+ Containment.inside?(full, @config.root) && !full.start_with?(File.join(@config.output_dir, ''))
84
+ end
85
+
86
+ def output_path(directory, within)
87
+ path = Pathname.new(File.join(directory, within)).cleanpath.to_s
88
+ raise Error, "asset destination #{directory} is outside the output directory" if path.start_with?('..', '/')
89
+
90
+ path
91
+ end
92
+
93
+ def relative(full)
94
+ Pathname.new(full).relative_path_from(Pathname.new(@config.root)).to_s
95
+ end
96
+
97
+ # Remembers an embedded file that is worth copying and returns where it goes.
98
+ def link(absolute)
99
+ root = File.join(@config.root, '')
100
+ return nil unless absolute.start_with?(root) && copyable?(absolute)
101
+
102
+ output = absolute.delete_prefix(root)
103
+ @linked[output] = absolute
104
+ output
105
+ end
106
+
107
+ def copyable?(absolute)
108
+ File.file?(absolute) && !Stitcher::MARKDOWN.match?(absolute) &&
109
+ !absolute.start_with?(File.join(@config.output_dir, '')) && Containment.inside?(absolute, @config.root)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Boondoc
6
+ # The `boondoc` command.
7
+ class CLI < Thor
8
+ COMMANDS = %w[generate serve version help].freeze
9
+ COMMAND_FLAGS = %w[-h --help -v --version].freeze
10
+
11
+ # Runs the CLI, treating a bare `boondoc [GLOBS]` as `boondoc generate [GLOBS]`.
12
+ #
13
+ # @param argv [Array<String>]
14
+ # @return [void]
15
+ def self.start_with_default(argv)
16
+ known = COMMANDS.include?(argv.first) || COMMAND_FLAGS.include?(argv.first)
17
+ start(known ? argv : ['generate', *argv])
18
+ end
19
+
20
+ # @return [Boolean] exit non-zero when a command fails
21
+ def self.exit_on_failure?
22
+ true
23
+ end
24
+
25
+ desc 'generate [GLOBS]', 'Write markdown documentation (default command)'
26
+ long_desc <<~DESC
27
+ Parses the Ruby files matching GLOBS (default: lib/**/*.rb) and writes one markdown
28
+ file per class or module, plus an index, into the output directory.
29
+
30
+ Options can also be set in .boondoc.yml using the same names with underscores
31
+ (sources, exclude, output, index, readme, files, assets, copy_linked_assets, title, private,
32
+ protected, yard_macros, strict, yardopts). allow_outside_output can only be given on the command line.
33
+
34
+ The output directory must be inside the project, and not reached through a symlink.
35
+ Sources outside the project are skipped with a warning.
36
+ DESC
37
+ option :output, aliases: '-o', type: :string, desc: 'Output directory (default: docs)'
38
+ option :index, type: :string, desc: 'Name of the index page (default: README.md)'
39
+ option :readme, type: :string, desc: 'File to use as the index page (default: README.md if present)'
40
+ option :skip_readme, type: :boolean, desc: 'Do not include a README in the index page'
41
+ option :files, type: :array, desc: 'Extra markdown files or globs to stitch in'
42
+ option :assets, type: :array, desc: 'Files, directories, or globs to copy into the output (FROM or FROM:DIR)'
43
+ option :copy_linked_assets, type: :boolean,
44
+ desc: 'Also copy the files that pages embed, such as images in the README'
45
+ option :exclude, type: :array, desc: 'Ruby files or globs to leave out'
46
+ option :title, type: :string, desc: 'Title for the index page when there is no README'
47
+ option :private, type: :boolean, desc: 'Include private methods and @private objects'
48
+ option :protected, type: :boolean, desc: 'Include protected methods'
49
+ option :yard_macros, type: :boolean, desc: 'Expand @!macro, @!method, and @!attribute using the yard gem'
50
+ option :yardopts, type: :boolean, desc: 'Read .yardopts (default; --no-yardopts to skip it)'
51
+ option :strict, type: :boolean, desc: 'Exit non-zero if there are warnings'
52
+ option :force, type: :boolean, desc: 'Overwrite files in the output directory that boondoc did not write'
53
+ option :allow_outside_output, type: :boolean,
54
+ desc: 'Allow an output directory outside the project (a security risk if the ' \
55
+ 'project is not trusted; for docs kept outside the repository)'
56
+ option :config, aliases: '-c', type: :string, desc: 'Config file (default: .boondoc.yml)'
57
+ option :quiet, aliases: '-q', type: :boolean, desc: 'Only print warnings and errors'
58
+ def generate(*globs)
59
+ Generator.new(Config.load(overrides(globs))).run
60
+ rescue Error => e
61
+ raise Thor::Error, "boondoc: #{e.message}"
62
+ end
63
+
64
+ desc 'serve', 'Preview the generated docs in a browser with gh markdown-preview'
65
+ long_desc <<~DESC
66
+ Starts the gh markdown-preview extension of the GitHub CLI from inside the output
67
+ directory, so the links between pages work. Offers to install the extension when
68
+ it is missing. Run `boondoc` first to generate the docs.
69
+ DESC
70
+ option :output, aliases: '-o', type: :string, desc: 'Directory holding the docs (default: docs)'
71
+ option :allow_outside_output, type: :boolean, desc: 'Allow a docs directory outside the project'
72
+ option :host, type: :string, default: Preview::DEFAULT_HOST, desc: 'Address to bind'
73
+ option :port, aliases: '-p', type: :numeric, desc: 'Port to listen on (default: 3333)'
74
+ option :config, aliases: '-c', type: :string, desc: 'Config file (default: .boondoc.yml)'
75
+ def serve
76
+ config = Config.load(options.slice('output', 'config', 'allow_outside_output').transform_keys(&:to_sym))
77
+ Preview.new(config.output_dir, host: options[:host], port: options[:port]) do
78
+ yes?("The gh markdown-preview extension is not installed. Install #{Preview::EXTENSION} now? [y/N]")
79
+ end.start
80
+ rescue Error => e
81
+ raise Thor::Error, "boondoc: #{e.message}"
82
+ end
83
+
84
+ desc 'version', 'Print the boondoc version'
85
+ def version
86
+ puts Boondoc::VERSION
87
+ end
88
+ map %w[-v --version] => :version
89
+
90
+ private
91
+
92
+ def overrides(globs)
93
+ settings = options.to_h.transform_keys(&:to_sym)
94
+ settings[:readme] = false if settings.delete(:skip_readme)
95
+ settings[:sources] = globs unless globs.empty?
96
+ settings
97
+ end
98
+ end
99
+ end