[com.sagevisuals/readmoi "4"]
com.sagevisuals/readmoi {:mvn/version "4"}
(require '[readmoi.core :refer [generate-all]])
Software documentation should have lots of examples. But it's kinda a pain to write html or markdown containing
code examples. Write some code in the editor, evaluate it, copy and paste it into the ReadMe, back-and-forth. And if the software changes, the examples
may no longer be accurate. It sure would be nice if we could write (+ 1 2)
directly into the ReadMe, and the document would automatically
insert 3
immediately afterwards. And if we ever decide to redefine +
, re-generating the document would update all the
results.
Developing Clojure is a pleasure because we're writing the code while standing inside the code itself. Markdown and html don't provide that. Plus, my editor is already set up for lisp code structural editing, and I am hesitant to give it up.
Hiccup is a wonderful utility that consumes Clojure code and outputs html. All the benefits of Clojure transfer to authoring html. Code editors can sling around lisp forms with abandon. We have the whole Clojure universe at our disposal. And best of all, we can evaluate code examples, right there in the document itself.
But, GitHub ReadMe documents are generated from markdown files, not hiccup. The ReadMoi library generates html and markdown ReadMe files — with up-to-date, evaluated code examples — from hiccup source.
The resulting ReadMe document is structured exactly as you see here: a Clojars badge, navigation links, one or more html <section>
s (Intro, Usage, Glossary, etc.) containing evaluated code examples, a
license statement, and a footer with copyright and compilation metadata.
We write our document, one .clj
file per section. Each section file contains hiccup/html forms with text liberally sprinkled with code
examples. Then, we create an options file that tells ReadMoi which section files to load. The options file also contains various…options (see
below). Finally, we tell ReadMoi to generate the ReadMe, one markdown file and one html file. Generating the ReadMe
files involves processing the hiccup forms, during which the code examples are evaluated and the returned values are inserted immediately next to the
Clojure form.
The following steps assume a Leiningen project.clj
file in the project's root directory.
Complete the setup.
Write our ReadMe sections. The format of each section's file is…
[:section#❬:section-href❭
❬hiccup content❭]
:section-href
is the value found in options map. ReadMoi automatically generates navigation links based on those hyperlink references
it finds in that map.
Also, we can show people how to use the software with the following pattern.
[:pre [:code (print-form-then-eval "(+ 1 2)")]]
…which gets rendered as…
(+ 1 2) ;; => 3
Don't bother inserting the return value. Every time we generate the document, the code is re-evaluated. We can re-write our code examples and quickly see how they'll appear in the document. Also, the code examples stay synchronized as the codebase changes.
Note: Any definitions (def
, defn
, etc.) will bind a value to a symbol in that namespace, which is useful and typically
what we'd want, but can on occasion, be inconvenient.
The pretty-printing is delegated to zprint
, which has a million and one options. print-form-then-eval
provides about four
knobs to tweak the line-breaking, which is good enough for most examples in a ReadMe document. See the api documentation for details.
Copy readmoi_options.edn
to our
project's resources/
directory.
The readmoi_options.edn
file assigns all the required information and declares our preferences for optional values. The map contains
the following required keys:
:sections
Vector containing one map for each section of the ReadMe. Each section map — having a one-to-one correspondence with one
.clj
section files — has the following keys:
:section-name
The section title (string). Required.
:section-href
Hyperlink reference, internal or external (string). Required.
:section-skip-load?
Indicates whether to load section contents from file (boolean). Set to true
if external link.
For example, the API documentation is on another webpage, so there's no additional section file required to generate the
ReadMe document. It's merely a hyperlink to an external webpage. On the other hand, the Usage section is part of this
document, so we do need to load the source text to generate the document.
The following are optional keys:
:clojars-badge?
Boolean that governs whether to display a Clojars badge. Information used to generate the badge is inferred from
project.clj
file. Default nil
.
:copyright-holder
String that appears in copyright statement at page footer. Default nil
.
:fn-map-additions
Special :fn-map
directives governing how zprint pretty-printer will format a function expression. Defaults to {}
.
:license-hiccup
Hiccup/html forms to replace the default license (MIT license) section.
:project-description
Alternative project description (string) to use in preference to the project description supplied by
defproject
in the project.clj
file.
:project-name-formatted
Alternative project name (string) to use in preference to the project name supplied by
defproject
in the project.clj
file.
:UUID
Version 4 Universally Unique Identifier. Suggestion: eval-and-replace
(random-uuid)
. Default nil
.
:readme-html-directory
Alternative output html directory (string). Include trailing '/'.
Defaults to 'doc/'.
:readme-html-filename
Alternative output html filename (string). Defaults to
'readme.html'.
:readme-markdown-directory
Alternative output markdown directory (string). Include trailing `/`. Defaults to '' (i.e.,
project's root directory).
:readme-markdown-filename
Alternative output markdown filename (string). Defaults to 'README.md'.
:sections-directory
Alternative directory to find sections hiccup .clj
files. Include trailing '/'. Default
resources/readme_sections/
.
:separator
String separating the s-expression and the evaluated result. Defaults to
' => '
.
:wrap-at
Column wrap base condition for print-form-then-eval
and prettyfy
. Defaults to 80
.
:tidy-html?
Indent and wrap html and markdown files. Defaults to nil
. Setting this
option to true
may be desirable minimize the version control 'diff' from one commit to the next. Note that the tidy-ing
procedure may insert line-breaks at an undesireable spot, e.g., within an in-text [:code ...]
block. To keep the block on one
line, use a Unicode U+0A00
non-breaking space. An html non-breaking space entity,
, gets rendered
literally.
Generate the html and markdown files. We could evaluate…
(generate-all (read-string (slurp "project.clj"))
(load-file "resources/readmoi_options.edn"))
…in whatever namespace we loaded generate-all
. Or, we could copy resources/readmoi_generator.clj
and evaluate all
forms in the namespace (cider command C-c C-k
). Some day, I'll make this a command line tool or a
Leiningen plugin.
ReadMoi produces two files. The first is a 'markdown' file that's actually plain old html, abusing
the fact that html passes through the markdown converter. By default, this markdown file is written to the
project's root directory where GitHub can find and display the ReadMe. We don't need a dedicated markdown converter to view this file;
copy it to a GitHub gist and it'll display similarly to when we view it on GitHub. The second file, by
default written to the resources/
directory, is a proper html document with a
<head>
, etc., that is viewable in any browser. We may want to copy over the css file for some minimal styling.
If a section's .clj
file won't load, check the options map in readmoi_options.edn
. The
:section-name
must correspond to the section's filename.
If a navigation link doesn't work as expected, check that the html section element id in the section's
.clj
file matches the :section-href
in the options map in readmoi_options.edn
.
If print-form-then-eval
doesn't behave as
you'd like, try adjusting the width-fn
and width-output
parameters first. Then if that doesn't suit, try supplying
a function-specific formatting directive in the :fn-map-additions
value of the options map in readmoi_options.edn
.
The zprint
pretty-printer has an astronomical amount of settings, but in the end, it just tries to do what its author thinks looks best.
Almost all the time it works great. My advice: don't chase perfection, just get it looking pretty good and spend the extra time on editing your
prose.
Here is some example hiccup/html that might live in a section file named super.clj
in the project's
resources/readme_sections/
directory.
[:section#super
[:h3 "Super Awesome Stuff"]
[:p "Here's how to use " [:code "inc"] "."]
[:pre [:code (print-form-then-eval "(inc 99)")]]]
Notice that we didn't include the 100
yielded by evaluating (inc 99)
. During hiccup processing,
print-form-then-eval
will do that for us, including inserting a separator.
Hiccup extracts id attributes from the thing following an html element's #
. In this example, the
section element's id is #super
.
We include this entry into the :sections
map of the options file.
{:sections [{:section-name "Super Awesome Stuff"
:section-href "super"]}
Notice that the :section-href
value in the options map matches the hiccup html element's id
attribute. That matching allows the navigation link at the top of the ReadMe to correctly link to the proper section somewhere later in the ReadMe.
After running generate-all
, that combination of hiccup/html and options would be rendered in the final ReadMe like this.
Super Awesome Stuff
Here's how to use
inc
.(inc 99) ;; => 100
ReadMoi consulted the options file, learned that there was a section called 'super', loaded the contents of the super.clj
file,
processed the hiccup/html contents of the file — which involved evaluating (inc 99)
and then inserting ;; => 100
— and
wrote the ReadMe files.
Speculoos: A data validation library.
fn-in
: A data structure handling library.
Term 1 definition...
Term 2 definition with internal link.
This program and the accompanying materials are made available under the terms of the MIT License.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
Ctrl+k | Jump to recent docs |
← | Move to previous article |
→ | Move to next article |
Ctrl+/ | Jump to the search field |