OrgReadme-fy: README.org from your library's functions and tests (Emacs package)
Below you find the latest version of (1) the package's README and (2) its main source file.
For the git repository and issues tracker, see the project's page on sr.ht.
For more packages, see Software.
README.org
Overview
OrgReadme-fy helps you create a README.org
for your Emacs projects. Features:
- It can generate for you a
readme-template.org
so you don't have to start from scratch. This file will, upon creation, fetch some metadata from your package file (the main .el). It will have a skeleton of common sections. - It can generate for you a summary table and/or descriptive subtrees that feed readme-template.org to create the final
README.org
. This table and/or subtrees, in turn, are automatically generated from your package file and an optional examples.el file, providing a list of functions, their signature, their docstring, and examples of usage.
This file has been created with it.
Is OrgReadme-fy for you?
More so in proportion to how many of the following questions you can answer ‘yes’ to:
- Are you going to write a README for your Emacs project?
- Would you prefer to use Org as the markup language for it?
(Note that org-export can be used to easily convert it to Markdown. I see no need to do that, however.) - Would you like to summarize some (or even all) functions from your project in a table having the first line of their docstring and, optionally, their signature?
- Would you like to describe some (or even all) functions from your project — showing their signature and their full docstring?
- Would you like to show usage examples of these functions by having them fetched directly from your Exemplify-ERT–based tests and displayed in a palatable, readable format?
Background
The idea for this package came from seeing how clean Dash's ERT tests looked, then understanding what was going on by looking at dev/dash-defs.el
. I saw that all the functions' descriptions and examples in Dash's README.md
were being automatically generated from dash.el
and dev/examples.el
.
I found that interesting. Much later, when writing xht
(an extensive hash tables library for Emacs), I slightly adapted it so that I could more easily and cleanly write my own ERT tests for it.
It worked quite well. My xht/dev/examples.el
ended up with more than 9000 lines: I personally wrote more than 1800 such tests for it in this format.
I realized that dash-defs.el
was doing two different things that were closely related but fundamentally independent:
- Providing macros for writing cleaner ERT tests that look like, and double as, examples.
- Providing functions for automatically generating documentation from these examples and from functions' docstrings.
To build on and further develop these ideas, I wrote two packages: Exemplify-ERT and OrgReadme-fy.
Exemplify-ERT and OrgReadme-fy
These two packages are independent from, but complementary to, each other:
- Exemplify-ERT is about making it more pleasant to write ERT tests for your project. You can use it just for that, even if you don't plan to ever write a README.
- OrgReadme-fy is about bootstrapping a
README.org
from what you already have. If you do have anexamples.el
created with Exemplify-ERT, it will use that to fetch your examples. If you don't, that's fine: you can use it just for generating the initial skeleton and (more importantly) for automatically producing tables and subtrees with the information that is already there in your package's functions and docstrings.
If you use them together for your library, the effect will be that:
- Your
dev/examples.el
file will have readable examples that double as ERT tests. - Your elisp file will have code (defuns, defvars, defcustoms) plus its documentation.
- Your
README.org
will use both this documentation and the examples fromdev/examples.el
.
Expanding the above a bit:
- The elisp file will be the single source of truth for documentation about the functions and commands. This documentation will be seen in context, either inside (as doc strings) or before (as
orgreadme-fy-describe
strings) the functions' code. - This very same documentation can be automatically translated to your
README.org
. But here, instead of the functions' code, you'll have examples of usage of the functions, coming straight from yourdev/examples.el
file. - Your
dev/examples.el
file doubles as ERT tests and examples of usage. Because of the clean syntax they use, these examples are imported to theREADME.org
exactly as they are. - Hierarchical headers in the elisp file (";;;+") will be automatically translated as hierarchical headings for the
Commands
andFunctions
headings of yourREADME.org
. - Your
README.org
is generated from areadme-template.org
. This file has the code blocks that import both the documentation and the examples.
- You can configure these code blocks to your liking: what to exclude, at what depth the headings start, whether to include summary tables, where to insert them, spacing, etc.
- You write there as well the contents of all other headings that you want to include in your README — everything that doesn't seem fit to be in the elisp file (as is the case of the
Overview
andBackground
headings you're reading right now).
- You can configure these code blocks to your liking: what to exclude, at what depth the headings start, whether to include summary tables, where to insert them, spacing, etc.
Differences from Dash's implementation
In OrgReadme-fy some things are different.
Org is the target format
While dash-defs creates a Markdown document, OrgReadme-fy aims at Org.
If desired, the org file can be easily exported to .md with something like:
(org-export-to-file 'md "README.md")
Likewise for Texinfo, which dash-defs also targets:
(org-export-to-file 'texinfo "your-package-name.texi")
(In the latter case, however, the source template would need to be a bit different because of documentation licensing, specifying Info node, etc. If you're interested in implementing this for future versions, see TODO.org
for a preliminary idea of how it could be done from a single, org-and-texi-exportable readme-template.org
by using hooks and selective exporting.)
All examples are fetched
Some time ago I wanted to learn more ways to use -let
. So I checked Dash' README.md
.
Three examples there:
(-let (([a (b c) d] [1 (2 3) 4])) (list a b c d)) ;; => (1 2 3 4) (-let [(a b c . d) (list 1 2 3 4 5 6)] (list a b c d)) ;; => (1 2 3 (4 5 6)) (-let [(&plist :foo foo :bar bar) (list :baz 3 :foo 1 :qux 4 :bar 2)] (list foo bar)) ;; => (1 2)
Ah, nice. But the documentation says much more about what it can do that isn't covered by these examples. So I wondered if there would be a test file for dash, and this was when I first opened its dev/examples.el.
Then I looked for -let
, and... 147 examples! Now, if after that I still didn't get it, I'd be a lost case. So I looked into them, and its syntax became clearer.
At a later time, when writing tests/examples for xht
, I decided I wanted to give visibility to all my examples. Formal docstrings are useful, but examples are a great way of learning, and I think we underestimate that. So why leave them in a file that few are likely to open? The kinds of usage distinction that drives the writing of slightly different tests is often precisely the kind of distinction a person learning the library wants to know about to be able to predict the outcomes of the function.
So let's show them all, because skipping extra ones when you "got it" is much easier than fabricating extra ones when you haven't got it. I still haven't heard of any person going "Ugh, that library, I hate it — too many examples!"
(As a bonus, coding was easier: no parsing to select examples — just bring them all.)
Examples are used in their exact format
Examples sometimes need to use larger, nested expressions that would be much less readable if crammed in one line. The function from dash-defs.el
that parses the examples treat them as sexps, and the formatting is lost. As a result, all of Dash's README's examples get limited to a single line. This is not a problem for the majority of them, which are short. But have a look at, say these longer two:
(--tree-map-nodes (eq (car-safe it) 'add-mode) (-concat it (list :mode 'emacs-lisp-mode)) '(with-mode emacs-lisp-mode (foo bar) (add-mode a b) (baz (add-mode c d)))) ;; => (with-mode emacs-lisp-mode (foo bar) (add-mode a b :mode emacs-lisp-mode) (baz (add-mode c d :mode emacs-lisp-mode)))
and
(--tree-reduce (cond ((stringp it) (concat it " " acc)) (t (let ((sn (symbol-name it))) (concat "<" sn ">" acc "</" sn ">")))) '(body (p "some words") (div "more" (b "bold") "words"))) ;; => "<body><p>some words</p> <div>more <b>bold</b> words</div></body>"
Wouldn't you rather, instead, read them like this?
(--tree-map-nodes (eq (car-safe it) 'add-mode) ; pred (-concat it (list :mode 'emacs-lisp-mode)) ; form '(with-mode ; tree emacs-lisp-mode (foo bar) (add-mode a b) (baz (add-mode c d)))) => '(with-mode emacs-lisp-mode (foo bar) (add-mode a b :mode emacs-lisp-mode) (baz (add-mode c d :mode emacs-lisp-mode)))
and
(--tree-reduce (cond ((stringp it) (concat it " " acc)) (t (let ((sn (symbol-name it))) (concat "<" sn ">" acc "</" sn ">")))) '(body (p "some words") (div "more" (b "bold") "words"))) => "<body><p>some words</p> <div>more <b>bold</b> words</div></body>"
So I decided that examples.el
should be able to accommodate as much space, indentation, and formatting as the person writing the tests wanted, and that this formatting would be exactly transposed to the README. And since we're fetching all the examples anyway, any amount of ;;-commenting may be now added around the examples and preserved.
Structure and contents of documentation happen in the main library file, not in examples.el
With dash-defs.el
, the examples are organized into groups. This is directly reflected in the README structure, which is limited to two levels: group and its functions. Example:
** Unfolding *** -iterate (fun init n) *** -unfold (fun seed)
That's a good step towards organization. I felt, however, that allowing for arbitrary levels of grouping would be better. But how to?
I decided that all the structure and organization should come from, and be dictated by, the library file. So no need for grouping in examples.el
: the README should follow the structure of yourlibrary.el
.
I don't know about you, but I use Outshine when coding Elisp. It makes all those leading ;;;; of your Elisp file behave like org trees — with hierarchy, easy folding and unfolding with the press of a TAB, different coloring depending on level, easy shifting of "subtrees" around, promotion/demotion, narrowing, etc.
As a result, the hierarchical organization of my code grows organically. And this already-existing structure could be transposed to the README, couldn't it? So I decided to leave all defexamples
(now modified and called exemplify-ert
) at top level in examples.el
by removing the def-example-groups
.
But then, where do the string contents of these last ones would go? Why, to yourlibrary.el
. I came up with orgreadme-fy-describe, which basically lets you add docstrings to your headings. Its use looks like this:
;;;;; Advanced deflangelization (orgreadme-fy-describe "These are the main functions for non-trivial deflangelization when the simple ones fail. There are three types: | Function | Explanation | |---------------------------+-----------------------------------| | deflangelize-with-fooing | To be used with non-trivial-foos. | | deflangelize-with-barring | To be used with non-trivial-bars. | | deflangelize-try-both | To be used when all else fails. | Try starting with the fooing one.") (defun deflangelize-with-fooing (foo &rest flanges) "Deflangelize FLANGES by using FOO. Try FOO in FLANGES and see if it works." (when flanges (ignore-errors (deflangelize flanges 'foo foo))))
Which in org becomes the below (and may then also become html):
*** Advanced deflangelization These are the main functions for non-trivial deflangelization when the simple ones fail. There are three types: | Function | Explanation | |---------------------------+-----------------------------------| | deflangelize-with-fooing | To be used with non-trivial-foos. | | deflangelize-with-barring | To be used with non-trivial-bars. | | deflangelize-try-both | To be used when all else fails. | Try starting with the fooing one. **** deflangelize-with-fooing (foo &rest flanges) Deflangelize FLANGES by using FOO. Try FOO in FLANGES and see if it works.
So the documentation moves from dev/examples.el
to yourlibrary.el
, giving context to the actual code.
Installation
See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.
Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el
file.
(use-package orgreadme-fy :commands (orgreadme-fy-make-new-readme-template orgreadme-fy-make-new-readme orgreadme-fy-see-news orgreadme-fy-see-readme orgreadme-fy-see-library-readme))
To have all functions available, including orgreadme-fy-libfile->orgstr
(which is used to generate the strings), simply use this instead:
(use-package orgreadme-fy :demand t)
or
(require 'orgreadme-fy)
Usage
The main functions are:
Function | Summary |
---|---|
orgreadme-fy-describe | Describe with string STR the contents under this heading. |
orgreadme-fy-make-new-readme-template | Create a readme-template.org for your project. |
orgreadme-fy-make-new-readme | Create a README.org for your project. |
orgreadme-fy-libfile->orgstr | Given LIBFILE, OPS, and EXMPFILE, return README-ready org string. |
orgreadme-fy-libfile->orgbuf | Paste result of ‘orgreadme-fy-libfile->orgstr’ in an org buffer. |
orgreadme-fy-see-library-readme | Open library LIBNAME's README.org file. |
orgreadme-fy-see-readme | Open orgreadme-fy's README.org file. |
They are described in more detail below.
The table above was automatically generated by OrgReadme-fy, as was all of the following heading "Functions".
In the readme-template.org
, the code that generated the table went into an org-src block that looks like this:
#+begin_src emacs-lisp :exports results :results raw (orgreadme-fy-libfile->orgstr "orgreadme-fy.el" (list :output-type 'table :include-pred (lambda (fun) (s-match (rx symbol-start (| "orgreadme-fy-describe" "orgreadme-fy-make-new-readme-template" "orgreadme-fy-make-new-readme" "orgreadme-fy-libfile->orgstr" "orgreadme-fy-libfile->orgbuf" "orgreadme-fy-see-readme" "orgreadme-fy-see-library-readme") symbol-end) fun)))) #+end_src
So when readme-template.org
is exported to README.org
, the block is evaluated and replaced by its results.
Now more details.
We'll show almost everything here with this:
#+begin_src emacs-lisp :exports results :results raw (orgreadme-fy-libfile->orgstr "orgreadme-fy.el" (list :root-level 1 :output-type 'describe :only-with-examples nil :exclude-pred (lambda (fun) (s-match "--" fun)))) #+end_src
which would exclude only one or two double-dashed internal functions.
IMPORTANT: only the seven or so functions above will be of your interest as a user of the library.
The inclusion of everything below is simply to exemplify how OrgReadme-fy works — by applying the library to itself!
So there we go!
Functions
The describe-heading macro
orgreadme-fy-describe (str)
Describe with string STR the contents under this heading.
Think of it as a docstring for the subheadings of your elisp files.
A heading is a line-beginning comment that has 3+ semicolons. Each
heading may have as many orgreadme-fy-describe calls as you want,
although as a general rule a single one should suffice and is
preferable. (When more than one is present under a heading when
orgreadme-fy-libfile->orgbuf is called on your file, their
strings will be concatenated, separated from each other by an empty
line.)
You can use this in your programs to describe what each
section/heading of your program's file is about. You don't need to
install or require orgreadme-fy for that: just copy this macro to
some place in your program before the first call to it. Then rename
it using your library prefix.
For example: yourlibrary-describe
or yourlibrary--describe
.
You'll need to pass :describe-fun #'<that-chosen-name> as a pair of
the OPS argument when calling orgreadme-fy-libfile->orgstr.
Feel free to strip away all of this docstring beyond the first two
lines (or even all of it) if that suits you and you'd rather have
it short. See xht--describe
for an example.
If you ever want to create org documentation from your file, it
will be ready. And if you don't, I'd say that using it will be more
manageable than using comments, and will make it more readable to
others. It will also help distinguish between documentation-like
descriptions (using this) and small, incidental, code-local
commenting (using regular comments).
Finally, remember that you can create org tables inside STR when
you call this macro — in fact, inside any function's docstring as
well — with the function orgtbl-mode
, which is a minor mode that
works outside org. This can come handy to summarize properties of
functions under the heading you're describing.
(orgreadme-fy-describe "Add docstrings to your Emacs Lisp \"subtrees\".") => nil
Create your README
We have two steps here:
- Generate a readme-template.org that is specific to your project.
This is done once, with orgreadme-fy-make-new-readme-template.
This file will have calls for the insertion of other pieces —
pieces such as functions' documentations and examples (generated
by orgreadme-fy-libfile->orgstr). - Generate the README.org — the one people will read. This will be
done from readme-template.org with
orgreadme-fy-make-new-readme.
The first is better called interactively from your library.el, so
that it guesses paths, etc.
For the second, you can either:
Open readme-template.org and: M-x orgreadme-fy-make-new-readme
This is already there at the top of the template, though.
Just C-x C-e it.
From the command line, cd to your project folder and run:
./make-readme.sh
This script is placed on your project folder by
orgreadme-fy-make-new-readme-template.
It runs Emacs in batch mode, generates README.org, and exits.
orgreadme-fy-make-new-readme-template (&optional libfile templates-template-file)
Create a readme-template.org for your project.
Run it interactively and confirm filenames and directory.
LIBFILE is the project's main .el (the package file), from which
readme-template.org will have metadata extracted to fill out some
information for you already.
TEMPLATES-TEMPLATE-FILE is the template-generating template file;
the orgreadme-fy-template's template file. The default will be
suggested — a file provided with this package.
Eventually, you may want to copy this default and adapt it to your
needs — to however your README files are likely to be structured,
which will likely differ from my humbly suggested structure. Do so.
Save it somewhere and then pass this path to
orgreadme-fy-templates-template-file
. Your template's template
will then be the one suggested when you create a
readme-template.org.
orgreadme-fy-make-new-readme (&optional readme-template-org readme-org)
Create a README.org for your project.
This command should be called after you have created a
readme-template with orgreadme-fy-make-new-readme-template.
If filename README-TEMPLATE-ORG is provided, use it. Otherwise it's
assumed that the current buffer is the source template file.
If filename README-ORG is provided, use it.
Otherwise default to "README.org".
The generated README's buffer is then marked as read-only, to help
you remember that changes should be made upstream instead (to your
readme-template), lest a future conversion overwrites them.
Generate org strings from your library and its examples
These are the main functions to be used for generating org strings. These
strings will be inserted in your final README.org with the help of org src
blocks calling orgreadme-fy-libfile->orgstr.
orgreadme-fy-libfile->orgstr (libfile &optional ops exmpfile excl-list)
Given LIBFILE, OPS, and EXMPFILE, return README-ready org string.
Your library file must have a heading ";;; Code:".
OPS is a plist. It allows you to select the sort of org string that
you want and some further refinements. You can then call this
function repeatedly with separate parameters to generate separate
org string outputs, which may then be seamlessly included in
different parts of your README.org.
Here are the possibilities:
Key | Default value | Possible values |
---|---|---|
:output-type | 'describe | describe, table |
:output-subtype | 'functions | functions, aliases¹ |
:only-non-empty-headings | t | t or nil |
:only-with-examples | t | t or nil |
:describe-fun | #'orgreadme-fy-describe | fun name or string |
:make-links | t | t or nil |
:signatures-column | nil | t or nil |
:inter-heading-spacing | 0 | natnump |
:root-level | 2 | natnump |
:include-pred | nil (include all) | any regexp |
:exclude-pred | nil (exclude none) | any regexp |
¹ only when 'table, as 'describe can only be 'functions. So if you set
:output-subtype to 'aliases under 'describe it will be ignored, and
'describe will fall back to 'functions.
What is what:
Key | Explanation |
---|---|
:output-type | Full documentation or summary table |
:output-subtype | When 'table, output functions or aliases? |
:only-non-empty-headings | Whether to exclude empty headings |
:only-with-examples | Whether to exclude functions without examples |
:describe-fun | Name of heading-describing function |
:make-links | Whether to make links for functions |
:signatures-column | Whether to add a signatures column |
:inter-heading-spacing | Number of blank lines between headings |
:root-level | Number of asterisks at root subtrees |
:include-pred | Function/aliases names to include (predicate) |
:exclude-pred | Function/aliases names to exclude (predicate) |
So :root-level is the level (number of asterisks) to be assigned to
the root subtree(s) generated when :output-type (see below) is
'describe. (It has no influence when :output-type is 'table.)
When :root-level is nil, 0, 2, or not a natural number it defaults,
unchanged, to 2. If it's 1, all subtrees are promoted. If greater
than 2, all subtrees are demoted accordingly.
If you don't want to convert any links wrapped in `' or ‘’ from
your docstrings to point to customids, set :make-links to nil.
Inclusion is processed before exclusion.
One example of :exclude-pred would be:
(lambda (fun) (s-match "--" fun))
to exclude showing internal functions in the documentation
(unless you use "--" to denote anaphoric macros — then not).
Another example is a :include-pred of
(lambda (fun) (commandp fun))
to only include functions that are commands — that is, interactive.
And :describe-fun is to allow LIBFILE to have the
orgreadme-fy-describe macro under a different name. See
orgreadme-fy-describe for more information.
Not all options apply to the different output types.
When not applicable (N/A), passing the option has no effect.
describe | table | table | |
---|---|---|---|
Key | functions | functions | aliases |
:only-non-empty-headings | ✓ | N/A | N/A |
:inter-heading-spacing | ✓ | N/A | N/A |
:only-with-examples | ✓ | N/A | N/A |
:describe-fun | ✓ | N/A | N/A |
:signatures-column | N/A | ✓ | N/A |
:root-level | ✓ | N/A | N/A |
:make-links | ✓ | ✓ | N/A |
:include-pred | ✓ | ✓ | ✓ |
:exclude-pred | ✓ | ✓ | ✓ |
The plist is updated in this order:
orgreadme-fy-ops-plist-default
, orgreadme-fy-ops-plist-user
,
and OPS. So OPS has higher precedence.
If EXMPFILE is nil, use orgreadme-fy-examples-rel-path
.
EXCL-LIST is a list of callables to exclude besides those of
orgreadme-fy-top-callables-exclusions-list-default
and
orgreadme-fy-top-callables-exclusions-list-user
.
orgreadme-fy-libfile->orgbuf (libfile &optional ops exmpfile excl-list)
Paste result of orgreadme-fy-libfile->orgstr in an org buffer.
This way you can visualize the results of the insertion files that
you can generate with orgreadme-fy-libfile->orgstr.
LIBFILE is a library file.
For details about OPS, EXMPFILE, EXCL-LIST, see orgreadme-fy-libfile->orgstr.
;; All these evaluate to nil, but that's not the point! ;; These are examples you can try on this very library file ;; and check the different results. (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el") => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" '(:inter-heading-spacing 0)) => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" (list :root-level 5 :inter-heading-spacing 0)) => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" '(:output-type table)) => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" '(:output-type table :output-subtype aliases)) => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" (list :output-type 'table :exclude-pred (lambda (fun) (s-match "--" fun)))) => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" (list :output-type 'table ;; This is the describe-fun for this package only, and used as ;; default if omitted. But each package should borrow the function ;; and rename it with the package's prefix — so when applying ;; [[#orgreadme-fy-libfile->orgbuf][orgreadme-fy-libfile->orgbuf]] or [[#orgreadme-fy-libfile->orgstr][orgreadme-fy-libfile->orgstr]] ;; to another package, a :describe-fun line should be passed. :describe-fun #'orgreadme-fy-describe :exclude-pred (lambda (fun) (s-match "--" fun)) :include-pred (lambda (fun) (s-match "heading" fun)))) => nil (orgreadme-fy-libfile->orgbuf "../orgreadme-fy.el" '(:output-type table :signatures-column t)) => nil
Generate hash tables
Functions that create hash tables using your library or examples file.
orgreadme-fy-lines-ht (file)
Given a FILE, create hash table with all its lines.
Keys are 0-started line numbers. Values are line contents as string.
orgreadme-fy-headings-ht (lines-ht)
Given a LINES-HT, return a ht with only lines of elisp headings.
Headings match this regex "^;;;;* ".
orgreadme-fy-headings-ranges-ht (headings-ht &optional file)
Return ht with the same keys, and values being 1- next key.
This is the range this heading, stored in HEADINGS-HT, covers.
Useful for lookup of descriptions and functions, since if their
line numbers fall inside this range they are under this heading.
If optional argument FILE is given, use its number of lines as the
limit for last range. Otherwise use 1000000.
(orgreadme-fy-headings-ranges-ht (h* 10 "a" 23 "c" 133 "d" 424 "e")) H=> (h* 10 22 23 132 133 423 424 1000000)
orgreadme-fy-descs-ht (libfile &optional describe-fun)
Given a LIBFILE, return a ht with orgreadme-fy-describe strings.
The keys of the table will be the 0-started line-numbers where
calls to that macro are found in the LIBFILE, and the associated
values will be the strings passed to them.
In other words, this function recovers the strings from all calls to
orgreadme-fy-describe and their position in the file.
DESCRIBE-FUN is the name of the describe function used. It could
happen that LIBFILE used the macro but renamed it (for example, to
its same namespace), so this option allow for looking up different
names. If nil, it defaults to orgreadme-fy-describe.
DESCRIBE-FUN can be passed as string or symbol, both are ok. So if
your library is named foo and you decided to copy the
orgreadme-fy-describe macro into it and rename it to
foo-describe, you could pass this argument as either
#'foo-describe, 'foo-describe or "foo-describe".
The application of 1-
to line numbers happens because
h<-lines
(which see), used by orgreadme-fy-lines-ht, assigns an
index of 0 to the first line read, and we want the keys of the
generated hash tables to be comparable and refer to the same line
positions.
orgreadme-fy-aliases-ht (libfile)
Given a LIBFILE, return a ht with from-to aliases pairs.
The keys of the table will be the 0-started line-numbers where
calls to defalias
are found in the LIBFILE.
orgreadme-fy-funs-ht (lines-ht &optional excl-list)
Given a LINES-HT, return a ht with only lines of top‑level defs.
Values are cons of deftype and callable (that is, the functions and
macro symbols passed to the deftype). For example, this could become:
'("defun" . "orgreadme-fy-funs-ht")
EXCL-LIST is a list of callables to exclude besides those of
orgreadme-fy-top-callables-exclusions-list-default
and
orgreadme-fy-top-callables-exclusions-list-user
.
orgreadme-fy-examples-ht (exmpfile)
Given an EXMPFILE, return a ht with exemplify-ert
sexp string.
Function name is what comes after "(exemplify-ert ". This will be
the key.
The value will be the whole sexp as string. No change in formatting
happens: treated as string, not sexp — so it preserves indentations
and alignments from your examples file.
orgreadme-fy-range-lookup (curkey range-ht some-ht)
Return values that fall within desirable range.
Return a list of values whose keys fall in the range between CURKEY
and its lookup on RANGE-HT for SOME-HT.
Generate org pieces
Functions that generate org-mode strings, such as org headings or org
blocks from your examples.
orgreadme-fy-fun->org-src-block (fun examples-ht)
Given FUN and EXAMPLES-HT, return org src block as string.
FUN is passed as a string.
The first line and the closing parenthesis of the exemplify-ert sexp
are replaced by the block's beg and end.
(orgreadme-fy-fun->org-src-block "orgreadme-fy-headings-ranges-ht" (orgreadme-fy-examples-ht "examples.el")) => "\n#+name: examples-orgreadme-fy-headings-ranges-ht\n#+begin_src\ emacs-lisp :exports code :eval no (orgreadme-fy-headings-ranges-ht (h* 10 \"a\" 23 \"c\" 133 \"d\" 424 \"e\")) H=> (h* 10 22 23 132 133 423 424 1000000)\n#+end_src\n"
orgreadme-fy-heading-elisp->org (str)
Convert an elisp heading string STR into an org heading.
Generate parts of functions' documentation
Functions that generate parts of a function's documentation, such as their
signature, first line, and rest of its description.
orgreadme-fy-fun->documentation (fun)
Given FUN, return its documentation.
(orgreadme-fy-fun->documentation #'orgreadme-fy-fun->org-src-block) => "Given FUN and EXAMPLES-HT, return org src block as string. FUN is passed as a string. The first line and the closing parenthesis of the exemplify-ert sexp are replaced by the block's beg and end. (fn FUN EXAMPLES-HT)"
orgreadme-fy-fun->docstring (fun)
Given FUN, return its docstring, trimmed.
(orgreadme-fy-fun->docstring #'orgreadme-fy-fun->org-src-block) => "Given FUN and EXAMPLES-HT, return org src block as string. FUN is passed as a string. The first line and the closing parenthesis of the exemplify-ert sexp are replaced by the block's beg and end."
orgreadme-fy-fun->1stline (fun)
Given FUN, return first line of its docstring.
(orgreadme-fy-fun->1stline #'orgreadme-fy-fun->org-src-block) => "Given FUN and EXAMPLES-HT, return org src block as string."
orgreadme-fy-fun->middledoc (fun)
Given FUN, return the middle of its docstring, trimmed.
(orgreadme-fy-fun->middledoc #'orgreadme-fy-fun->org-src-block) => "FUN is passed as a string. The first line and the closing parenthesis of the exemplify-ert sexp are replaced by the block's beg and end."
orgreadme-fy-fun->signature (fun)
Given FUN, return its signature.
If the docstring has (fn ...) usage, use that.
Otherwise, use the one found in symbol-function
.
(orgreadme-fy-fun->signature #'orgreadme-fy-fun->org-src-block) => "(fun examples-ht)" (orgreadme-fy-fun->signature #'orgreadme-fy-see-readme) => "(&optional heading narrow)" (orgreadme-fy-fun->signature #'xht-do-convert-dwim) => "()" (orgreadme-fy-fun->signature #'h*) => "(key-1 value-1 key-2 value-2 ...)" (orgreadme-fy-fun->signature #'h-new) => "(&optional sz ts we rs rt)" (orgreadme-fy-fun->signature #'h-sel) => "(fun table)" (orgreadme-fy-fun->signature #'h-each) => "(table fun)" (orgreadme-fy-fun->signature #'h-lunzip) => "(table &optional reverse)" (orgreadme-fy-fun->signature #'h--each) => "(table &rest body)" (orgreadme-fy-fun->signature #'->>) => "(x &optional form &rest more)"
orgreadme-fy-fun->signature-from-symbol-function (fun)
Given FUN, return signature as it shows in symbol-function
.
So if the docstring has (fn ...) usage, don't use it.
orgreadme-fy-deal-with-escapes (string)
Deal with \= in STRING.
\= quotes the following character and is discarded; thus, \=\= puts \=
into the output, \=\[ puts \[ into the output, and \=` puts ` into the
output.
Return the original STRING if no substitutions are made.
Otherwise, return a new string.
This function has been adapted from substitute-command-keys
,
trimming it down to only the part related to escaping.
(orgreadme-fy-deal-with-escapes "This \(here) is \\='escaped.") => "This (here) is 'escaped." (orgreadme-fy-deal-with-escapes "No escapes.") => "No escapes." (orgreadme-fy-deal-with-escapes "") => ""
Generate parts of user options' documentation
Functions that generate parts of a user option's documentation,
such as their first line, and the rest of its description.
These functions were added later and are still unused. For the
moment, automatic documentation of user options is unavailable, so
orgreadme-fy-top-callables-exclusions-list-default
has
defcustom
as member.
User options would need to be treated a bit differently from
functions: no signatures, no exemplify-ert
, different way of
fetching the docstring, shouldn't mix with functions in the 'table
option of :output-type in orgreadme-fy-libfile->orgstr, etc. So
it seems that including them would add a bit of complexity to the
current code.
Given the above, and given that it seems likely that most people
won't be too interested in documenting their defcustom
variables
in their README, I haven't implemented it. In any case, with this
future possibility in mind, I leave these functions here.
orgreadme-fy-cusvar->documentation (cusvar)
Given custom variable CUSVAR, return its documentation.
orgreadme-fy-cusvar->docstring (cusvar)
Given custom variable CUSVAR, return its docstring, trimmed.
orgreadme-fy-cusvar->1stline (cusvar)
Given custom variable CUSVAR, return 1st line of its docstring.
orgreadme-fy-cusvar->middledoc (cusvar)
Given custom variable CUSVAR, return the middle of its docstring.
orgreadme-fy-cusvar->signature (cusvar)
Given custom variable CUSVAR, return an empty string.
Unlike functions, there are no signatures.
Buffer replacements and adjustments
Functions that make replacements and adjustments in the current buffer
Used mostly for post-processing generated org files.
README.org post-processing
orgreadme-fy-post-process-readme-org ()
Post-process current buffer, assumed to be README.org.
orgreadme-fy-align-tables-in-buffer ()
Align all org tables in current buffer.
orgreadme-fy-make-prop-nil-in-buffer ()
Find #+:options: prop:t in this buffer and change it to nil.
Your readme-template.org should have prop:t so that property drawers
are exported to README.org. They have customids, needed for anchors
and internal linking.
However, we want to turn this off in README.org, or they'll end up
exported to HTML as tiny code blocks having CUSTOMID: something.
orgreadme-fy-restore-params-of-org-src-blocks-in-buffer ()
Find elisp source blocks and restore its parameters.
They were added by orgreadme-fy-fun->org-src-block, but lost when
exported — so restore them. While we're on it, downcase them all.
orgreadme-fy-downcase-properties-in-buffer ()
Downcase ^#+OPTIONS, ^#+AUTHOR and the like in the buffer.
libfile->orgstr generation post-processing
orgreadme-fy-remove-empty-headings ()
Remove empty headings in buffer.
orgreadme-fy-delete-subtree ()
Delete subtree at point.
orgreadme-fy-adjust-root-level (&optional root-level)
Adjust to ROOT-LEVEL the root level of subtrees in buffer.
Current root level is assumed to be 2.
orgreadme-fy-promote-all-subtrees-in-buffer (&optional n)
Promote all subtrees in buffer by N levels.
If nil, N defaults to 1.
orgreadme-fy-demote-all-subtrees-in-buffer (&optional n)
Demote all subtrees in buffer by N levels.
If nil, N defaults to 1.
orgreadme-fy-make-links-in-buffer ()
Replace `'- and ‘’-wrapped links with org links to anchors.
Only make links if target anchors actually exist. Otherwise, replace the quotes
with equal signs to make it monospace.
Also replace Info links.
orgreadme-fy-fetch-custom-ids-in-buffer ()
Make hash-table-as-set of all custom-id properties in buffer.
Accessing READMEs
Quick access to a library's README
Commands to quickly access a library's README.
orgreadme-fy-see-library-readme (&optional libname)
Open library LIBNAME's README.org file.
Load LIBNAME, if not yet loaded, then search for README.org in the
directory from which it has been loaded.
If not found, look for README.md, README.markdown, README.rst,
README.txt, and README. The first one found, if any, is opened
read-only.
If called interactively, library name will be asked.
orgreadme-fy-locate-library-readme (libname)
Locate library LIBNAME's README file.
Load LIBNAME, if not yet loaded, then search for README.org in the
directory from which it has been loaded.
If not found, look for README.md, README.markdown, README.rst,
README.txt, and README. Return the filename if found, nil
otherwise.
orgreadme-fy-list-loaded-libraries ()
List all loaded libraries.
orgreadme-fy-libname->libfile (libname)
Given LIBNAME, load library and return filename it loaded from.
See OrgReadme-Fy's README
A command to quickly open the README.org of this very library.
orgreadme-fy-see-readme (&optional heading narrow)
Open orgreadme-fy's README.org file.
Search for the file in orgreadme-fy.el's directory.
If found, open it read-only.
If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.
If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found.
Examples:
(orgreadme-fy-see-readme "News" t)
(orgreadme-fy-see-readme "Installation")
See OrgReadme-Fy's News
orgreadme-fy-see-news ()
See the News in orgreadme-fy's README.org file.
Contributing
See my page Software for information about how to contribute to any of my Emacs packages.
News
1.1.1
OrgReadme-Fy News
This version fixes a dependency issue.
Fixed
Dependency on Emacs 29 has been fixed: back to Emacs 25
orgreadme-fy-align-tables-in-buffer was using a couple of functions that depended on Emacs 29.1.
This has been changed to go back to depending on Emacs 25.1.
1.1.0
OrgReadme-Fy News
This version brings a new function and some bugfixes.
New functions brought by this version
orgreadme-fy-deal-with-escapes
can deal with escapes in documentation.
Changes
Made root-level argument optional in orgreadme-fy-adjust-root-level
Updated orgreadme-fy-top-callables-exclusions-list-default
Fixed
Fixed issue where escapes weren't being dealt with by orgreadme-fy-fun->documentation
Fixed orgreadme-fy-align-tables-in-buffer, which wasn't aligning in newer Emacs versions
Adjustments in docstrings, most of which quoting
1.0.0
OrgReadme-Fy Breaking News
To help preserve separation of namespaces, the prefixes of OrgReadme-Fy's functions and variables have all changed from ‘ormfy-’ to ‘orgreadme-fy-’.
There's nothing to do if:
- this is your first install of OrgReadme-Fy or
- you haven't customized any variables and
- you have no files currently using any of the package's functions
If, however, you have added anywhere (for example, in your init.el) settings that use the old prefix, simply update them to use the new prefix. Just replace ‘ormfy-’ with ‘orgreadme-fy-’.
You may also want to have a look at customizations you may have made in OrgReadme-Fy group. Use either of:
- M-x customize-group RET orgreadme-fy RET
- (customize-group "orgreadme-fy") ;<--C-x C-e
Finally, if you're generating READMEs with the library, make sure that the function used in the template is ‘orgreadme-fy-libfile->orgstr’ (with the new prefix).
Note: The old symbols have for now been marked as obsolete and aliased to the new ones to avoid any sudden breakage. However, these aliases may be removed without further notice. So if any of the adjustments above apply to you, better do them already, before new releases.
New commands brought by this version
orgreadme-fy-see-news
to open README.org, narrow into the News heading, skip to the latest, recenter, and show branches.
Changes in commands
orgreadme-fy-see-readme (&optional heading narrow)
may now take two optional arguments when called non-interactively.
License
This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.
Therefore, license and copyright information can be found in:
- each file's comment header, or
- an adjacent file of the same name with the additional extension
.license
, or - the
.reuse/dep5
file
The full text of the licenses can be found in the LICENSES
subdirectory.
orgreadme-fy.el
Structure
;;; orgreadme-fy.el --- README.org from your library's functions and tests -*- lexical-binding: t -*- ;;; Commentary: ;;;; For all the details, please do see the README ;;; Acknowledgments: ;;; Code: ;;;; Libraries ;;;; Obsolete symbols ;;;; Package metadata ;;;; Customizable variables ;;;; Functions ;;;;; The describe-heading macro ;;;;; Create your README ;;;;; Generate org strings from your library and its examples ;;;;; Generate hash tables ;;;;; Generate org pieces ;;;;; Generate parts of functions' documentation ;;;;; Generate parts of user options' documentation ;;;;; Buffer replacements and adjustments ;;;;;; README.org post-processing ;;;;;; libfile->orgstr generation post-processing ;;;;; Accessing READMEs ;;;;;; Quick access to a library's README ;;;;;; See OrgReadme-Fy's README ;;;;;; See OrgReadme-Fy's News ;;;; Wrapping up ;;; orgreadme-fy.el ends here
Contents
;;; orgreadme-fy.el --- README.org from your library's functions and tests -*- lexical-binding: t -*- ;; SPDX-FileCopyrightText: © flandrew <https://keyoxide.org/191F5B3E212EF1E515C19918AF32B9A5C1CCCB2D> ;; SPDX-License-Identifier: GPL-3.0-or-later ;;--------------------------------------------------------------------------- ;; Author: flandrew ;; Created: 2022-04-02 ;; Version: 1.1.1 ;; Homepage: <https://flandrew.srht.site/listful/software.html> ;; Keywords: maint, docs, lisp ;; Package-Requires: ((emacs "25.1") (dash "2.14") (s "1.12") (f "0.20") (xht "1.0.5")) ;;--------------------------------------------------------------------------- ;; This file is part of OrgReadme-fy, which is NOT part of GNU Emacs. ;; This program is free software: you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by the Free ;; Software Foundation, either version 3 of the License, or (at your option) ;; any later version. ;; ;; This program is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or ;; FITNESS FOR A PARTICULAR PURPOSE. For more details, see the full license at ;; either LICENSES/GPL-3.0-or-later.txt or <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; OrgReadme-fy helps you create a README.org for your Emacs projects. ;; Features: ;; ;; - It can generate for you a readme-template.org so you don't have to start ;; from scratch. This file will, upon creation, fetch some metadata from ;; your package file (the main .el). It will have a skeleton of common ;; sections. ;; ;; - It can generate for you a summary table and/or descriptive subtrees that ;; feed readme-template.org to create the final README.org. This table ;; and/or subtrees, in turn, are automatically generated from your package ;; file and an optional examples.el file, providing a list of functions, ;; their signature, their docstring, and examples of usage. ;; ;; It's independent from, but goes hand-in-hand with, Exemplify-ERT. ;; ;;;; For all the details, please do see the README ;; ;; Open it easily with any of the below: ;; (find-file-read-only "README.org") <--- C-x C-e here¹, or ;; (find-file-read-only-other-frame "README.org") <--- C-x C-e here¹, or ;; (find-file-read-only-other-window "README.org") <--- C-x C-e here¹ ;; ;; or from any buffer: ;; M-x orgreadme-fy-see-readme ;; ;; or read it online: ;; <https://flandrew.srht.site/listful/sw-emacs-orgreadme-fy.html> ;; ;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Acknowledgments: ;; ;; The idea for this library came after seeing how clean Dash's ERT tests ;; looked in Dash's dev/examples.el, and that these examples were being ;; used to generate Dash's README.md. ;; ;; After using the examples structure myself while writing XHT, I decided to ;; further develop the idea. The result was two independent packages that go ;; well together: Exemplify-ERT and OrgReadme-fy. ;; ;; My thanks to Magnar Sveen and the other Dash contributors. ;; ;; ------------------------------------------------------------------------ ;; ;; Function ‘orgreadme-fy-list-loaded-libraries’ was adapted from apropos.el's ;; ‘apropos-library’. ;; ;; About apropos.el: ;; SPDX-FileCopyrightText: © Free Software Foundation, Inc. ;; SPDX-License-Identifier: GPL-3.0-or-later ;; Authors: Joe Wells, Daniel Pfeiffer ;; ;; ------------------------------------------------------------------------ ;; ;; Function ‘orgreadme-fy-deal-with-escapes’ was adapted from help.el's ;; ‘substitute-command-keys’. ;; ;; About help.el: ;; SPDX-FileCopyrightText: © Free Software Foundation, Inc. ;; SPDX-License-Identifier: GPL-3.0-or-later ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Code: ;;;; Libraries (require 'pcase) (require 'dash) (require 'xht) ; <---also by the author of this package (require 'rx) (require 'f) (require 's) (require 'ox) ; ‘org-export-to-file’ (require 'org) ; ‘org-mark-subtree’, ‘org-at-heading-p’, ;; ‘org-map-region’, ‘org-entry-get’ (require 'subr-x) ; ‘if-let’ (require 'lisp-mnt) ; ‘lm-summary’, ‘lm-version’, ‘lm-homepage’ ;;;; Obsolete symbols (let ((new 'orgreadme-fy) (old 'ormfy) (ver "1.0")) (mapc (lambda (sym) (define-obsolete-variable-alias (intern (format "%s-%s" old sym)) (intern (format "%s-%s" new sym)) ver)) '(downcase-properties post-process-readme-org-user function-word readme-template-org-rel-path summary-word examples-rel-path signature-word templates-template-file-rel-path alias-from-word templates-template-file alias-to-word top-callables-exclusions-list-default ops-plist-default top-callables-exclusions-list-user ops-plist-user)) (mapc (lambda (sym) (define-obsolete-function-alias (intern (format "%s-%s" old sym)) (intern (format "%s-%s" new sym)) ver)) '(describe make-new-readme-template see-readme lines-ht make-new-readme see-library-readme headings-ht libfile->orgstr libname->libfile headings-ranges-ht libfile->orgbuf list-loaded-libraries descs-ht fun->documentation cusvar->documentation aliases-ht fun->docstring cusvar->docstring funs-ht fun->1stline cusvar->1stline examples-ht fun->middledoc cusvar->middledoc range-lookup fun->signature cusvar->signature fun->signature-from-symbol-function make-links-in-buffer fun->org-src-block align-tables-in-buffer heading-elisp->org make-prop-nil-in-buffer post-process-readme-org downcase-properties-in-buffer remove-empty-headings promote-all-subtrees-in-buffer delete-subtree demote-all-subtrees-in-buffer adjust-root-level fetch-custom-ids-in-buffer -obj-sym-str-or-error restore-params-of-org-src-blocks-in-buffer))) ;;;; Package metadata (defconst orgreadme-fy--name "OrgReadme-Fy") (defconst orgreadme-fy--dot-el (format "%s.el" (file-name-sans-extension (eval-and-compile (or load-file-name buffer-file-name))))) (defconst orgreadme-fy--readme-org (expand-file-name "README.org" (file-name-directory orgreadme-fy--dot-el))) (defconst orgreadme-fy--summary (lm-summary orgreadme-fy--dot-el)) (defconst orgreadme-fy--version (lm-version orgreadme-fy--dot-el)) (defconst orgreadme-fy--homepage (lm-homepage orgreadme-fy--dot-el)) ;;;; Customizable variables (defgroup orgreadme-fy nil (format "%s." orgreadme-fy--summary) :group 'docs :group 'lisp :link '(emacs-library-link :tag "Lisp file" "orgreadme-fy.el") :link `(file-link :tag "README.org" ,orgreadme-fy--readme-org) :link `(url-link :tag "Homepage" ,orgreadme-fy--homepage)) (defcustom orgreadme-fy-downcase-properties t "Downcase property words such as #+AUTHOR in README.org?" :type 'boolean :group 'orgreadme-fy) (defcustom orgreadme-fy-post-process-readme-org-user #'ignore "Additional function to call on README.org after post-processing. This will be run after ‘orgreadme-fy-post-process-readme-org’, which see. The default value here does nothing." :type 'function :group 'orgreadme-fy) (defcustom orgreadme-fy-function-word "Function" "Label of Functions column when :output-type is \\='table." :type 'string :group 'orgreadme-fy) (defcustom orgreadme-fy-summary-word "Summary" "Label of Summary column when :output-type is \\='table." :type 'string :group 'orgreadme-fy) (defcustom orgreadme-fy-signature-word "Signature" "Label of Signature column when :output-type is \\='table." :type 'string :group 'orgreadme-fy) (defcustom orgreadme-fy-alias-from-word "This function…" "Label of Alias from column when :output-subtype is \\='aliases." :type 'string :group 'orgreadme-fy) (defcustom orgreadme-fy-alias-to-word "is an alias to…" "Label of Alias to column when :output-subtype is \\='aliases." :type 'string :group 'orgreadme-fy) (defcustom orgreadme-fy-readme-template-org-rel-path "readme-template.org" "Name of the template file for .org." :type 'string :group 'orgreadme-fy) (defcustom orgreadme-fy-examples-rel-path "dev/examples.el" "Where to look for the examples file if no path is passed. This is relative to the location of the library file." :type 'string :group 'orgreadme-fy) (defvar orgreadme-fy-templates-template-file-rel-path "templates/readme-templates-template.org" "Relative location of the default template's template file.") (defcustom orgreadme-fy-templates-template-file "" "The path to the readme-template.org's file-generating template. You can copy the default file from the templates subdir, modify it, save it, and then pass this file's path to this variable. See the docstring of ‘orgreadme-fy-new-readme’ for more information." :type 'file :group 'orgreadme-fy) (defvar orgreadme-fy-top-callables-exclusions-list-default '(declare-function dolist mapc -each --each and or when unless if while setq setq-default setq-local -setq -let -let* -if-let -if-let* -when-let -when-let* let let* if-let if-let* when-let when-let* while-let and-let* provide require autoload warn message define-obsolete-function-alias make-obsolete define-obsolete-variable-alias make-obsolete-variable defalias defconst defcustom defgroup defvar defvaralias eval eval-after-load eval-and-compile eval-when-compile with-eval-after-load gv-define-setter put orgreadme-fy-describe) "Default list of functions and macro symbols to exclude. When found being executed at the top level, will be excluded from consideration and not documented. Note that the ‘orgreadme-fy-describe’ macro is in this exclusion list because it receives no function as argument. Its string argument is retrieved separately and specially.") (defcustom orgreadme-fy-top-callables-exclusions-list-user nil "A list of additional functions and macro symbols to exclude. When found being executed at the top level, they will be excluded from consideration and not documented. Add here whatever is not about the functions and macros which you wish to describe and exemplify in the README." :type '(repeat symbol) :group 'orgreadme-fy) (defvar orgreadme-fy-ops-plist-default (list :only-non-empty-headings t :inter-heading-spacing 1 :only-with-examples t :signatures-column nil :output-type 'describe :output-subtype 'functions :describe-fun 'orgreadme-fy-describe :include-pred nil :exclude-pred nil :make-links t :root-level 2) "Default property list of options to use when generating the org string. Any item of this setting can be overridden when set with different values in ‘orgreadme-fy-ops-plist-user’.") (defcustom orgreadme-fy-ops-plist-user nil "Property list of options to use when generating the org string. Values in ‘orgreadme-fy-ops-plist-user’ are overridden by those set by this variable." :type 'plist :group 'orgreadme-fy) ;;;; Functions ;;;;; The describe-heading macro ;;;###autoload (defmacro orgreadme-fy-describe (str) "Describe with string STR the contents under this heading. Think of it as a docstring for the subheadings of your elisp files. A heading is a line-beginning comment that has 3+ semicolons. Each heading may have as many ‘orgreadme-fy-describe’ calls as you want, although as a general rule a single one should suffice and is preferable. (When more than one is present under a heading when ‘orgreadme-fy-libfile->orgbuf’ is called on your file, their strings will be concatenated, separated from each other by an empty line.) You can use this in your programs to describe what each section/heading of your program's file is about. You don't need to install or require orgreadme-fy for that: just copy this macro to some place in your program before the first call to it. Then rename it using your library prefix. For example: ‘yourlibrary-describe’ or ‘yourlibrary--describe’. You'll need to pass :describe-fun #'<that-chosen-name> as a pair of the OPS argument when calling ‘orgreadme-fy-libfile->orgstr’. Feel free to strip away all of this docstring beyond the first two lines (or even all of it) if that suits you and you'd rather have it short. See ‘xht--describe’ for an example. If you ever want to create org documentation from your file, it will be ready. And if you don't, I'd say that using it will be more manageable than using comments, and will make it more readable to others. It will also help distinguish between documentation-like descriptions (using this) and small, incidental, code-local commenting (using regular comments). Finally, remember that you can create org tables inside STR when you call this macro — in fact, inside *any* function's docstring as well — with the function ‘orgtbl-mode’, which is a minor mode that works outside org. This can come handy to summarize properties of functions under the heading you're describing." (declare (indent 0)) (unless (stringp str) (user-error "The 'describe macro' must receive a string"))) ;; ^ Why not read the line number of the declaration to feed a hash table in a ;; global variable? Because it'd be unnecessarily complex and also fragile ;; (since line numbers could be spurious if called from another function or ;; depending on whether interpreted or compiled, etc). ;; ;; This dumb-looking solution here works better and has no dependencies. ;; Note that the macro itself does nothing with the string, which is ;; completely ignored (but otherwise easily readable by whomever goes ;; through your .el). The strings are recovered later, when creating your ;; README, by ‘orgreadme-fy-descs-ht’, which is called by ;; ‘orgreadme-fy-libfile->orgstr’, which is called by ;; ‘orgreadme-fy-libfile->orgbuf’. ;;;;; Create your README (orgreadme-fy-describe "We have two steps here: 1. Generate a readme-template.org that is specific to your project. This is done once, with ‘orgreadme-fy-make-new-readme-template’. This file will have calls for the insertion of other pieces — pieces such as functions' documentations and examples (generated by ‘orgreadme-fy-libfile->orgstr’). 2. Generate the README.org — the one people will read. This will be done from readme-template.org with ‘orgreadme-fy-make-new-readme’. The first is better called interactively from your library.el, so that it guesses paths, etc. For the second, you can either: 1. Open readme-template.org and: M-x orgreadme-fy-make-new-readme This is already there at the top of the template, though. Just C-x C-e it. 2. From the command line, cd to your project folder and run: ./make-readme.sh This script is placed on your project folder by ‘orgreadme-fy-make-new-readme-template’. It runs Emacs in batch mode, generates README.org, and exits.") ;;;###autoload (defun orgreadme-fy-make-new-readme-template (&optional libfile templates-template-file) "Create a readme-template.org for your project. Run it interactively and confirm filenames and directory. LIBFILE is the project's main .el (the package file), from which readme-template.org will have metadata extracted to fill out some information for you already. TEMPLATES-TEMPLATE-FILE is the template-generating template file; the orgreadme-fy-template's template file. The default will be suggested — a file provided with this package. Eventually, you may want to copy this default and adapt it to your needs — to however your README files are likely to be structured, which will likely differ from my humbly suggested structure. Do so. Save it somewhere and then pass this path to ‘orgreadme-fy-templates-template-file’. Your template's template will then be the one suggested when you create a readme-template.org." (interactive) (unless libfile (setq libfile (read-file-name "Your library file (the .el): " nil nil nil (let ((el (-> (f-base default-directory) (concat ".el")))) (when (f-exists? (f-expand el default-directory)) el)) nil))) (let* ((root (f-parent libfile)) (pack (f-base root)) (this (orgreadme-fy-libname->libfile 'orgreadme-fy)) ;; make-readme.sh (mkrdt (->> this f-parent (f-expand "templates/make-readme.sh"))) (mkrd (f-expand "make-readme.sh" root)) ;; readme-template.org (rtorg (f-expand orgreadme-fy-readme-template-org-rel-path root))) ;; Make make-readme.sh (unless (f-exists? mkrd) (copy-file mkrdt mkrd) (set-file-modes mkrd #o744)) ;; Make readme-template.org (if (not (or (not (f-exists? rtorg)) (and (y-or-n-p (s-lex-format "File ${rtorg} exists. OVERWRITE?")) (y-or-n-p (s-lex-format "Will overwrite ${rtorg}. Are you sure?"))))) (message "Ok, nothing done") (--> (or templates-template-file (read-file-name "Template's template file to use:\n" "/" nil nil (or (s-presence orgreadme-fy-templates-template-file) (->> this f-parent (f-expand orgreadme-fy-templates-template-file-rel-path))) nil)) f-read (s-replace-all `(("$PACKAGE" . ,pack) ("$DESCRIPTION" . ,(--> libfile lm-summary)) ("$KEYWORDS" . ,(--> libfile lm-keywords)) ("$NAME" . ,(--> libfile lm-authors (-map #'car it) (s-join ", " it))) ("$YEAR" . ,(->> (decode-time) (nth 5) (format "%s")))) it) (s-replace-regexp (concat "#.title:[ \t]+\\(" pack "\\)[ \t]*$") (s-titleize pack) it nil nil 1) ;; "or" because f-write returns nil with success, error otherwise. (or (f-write it 'utf-8 rtorg) (find-file rtorg)))))) ;;;###autoload (defun orgreadme-fy-make-new-readme (&optional readme-template-org readme-org) "Create a README.org for your project. This command should be called after you have created a readme-template with ‘orgreadme-fy-make-new-readme-template’. If filename README-TEMPLATE-ORG is provided, use it. Otherwise it's assumed that the current buffer is the source template file. If filename README-ORG is provided, use it. Otherwise default to \"README.org\". The generated README's buffer is then marked as read-only, to help you remember that changes should be made upstream instead (to your readme-template), lest a future conversion overwrites them." (interactive) (when readme-template-org (find-file readme-template-org)) (unless readme-org (setq readme-org "README.org")) (unless (eq major-mode 'org-mode) (user-error "Not an org file: %s. Can't make %s" readme-template-org readme-org)) (let ((was-already-open-p (find-buffer-visiting readme-org)) (pr (make-progress-reporter (format "Creating %s..." readme-org)))) (when was-already-open-p (pop-to-buffer was-already-open-p) (save-buffer) (kill-buffer was-already-open-p)) (org-export-to-file 'org readme-org) (find-file readme-org) (orgreadme-fy-post-process-readme-org) (when orgreadme-fy-post-process-readme-org-user (funcall orgreadme-fy-post-process-readme-org-user)) (save-buffer) (read-only-mode 1) (progress-reporter-done pr))) ;;;;; Generate org strings from your library and its examples (orgreadme-fy-describe "These are the main functions to be used for generating org strings. These strings will be inserted in your final README.org with the help of org src blocks calling ‘orgreadme-fy-libfile->orgstr’.") ;;;###autoload (defun orgreadme-fy-libfile->orgstr (libfile &optional ops exmpfile excl-list) "Given LIBFILE, OPS, and EXMPFILE, return README-ready org string. Your library file must have a heading \";;; Code:\". OPS is a plist. It allows you to select the sort of org string that you want and some further refinements. You can then call this function repeatedly with separate parameters to generate separate org string outputs, which may then be seamlessly included in different parts of your README.org. Here are the possibilities: | Key | Default value | Possible values | |--------------------------+-------------------------+---------------------| | :output-type | \\='describe | describe, table | | :output-subtype | \\='functions | functions, aliases¹ | | :only-non-empty-headings | t | t or nil | | :only-with-examples | t | t or nil | | :describe-fun | #\\='orgreadme-fy-describe | fun name or string | | :make-links | t | t or nil | | :signatures-column | nil | t or nil | | :inter-heading-spacing | 0 | natnump | | :root-level | 2 | natnump | | :include-pred | nil (include all) | any regexp | | :exclude-pred | nil (exclude none) | any regexp | ¹ only when \\='table, as \\='describe can only be \\='functions. So if you set :output-subtype to \\='aliases under \\='describe it will be ignored, and \\='describe will fall back to \\='functions. What is what: | Key | Explanation | |--------------------------+-----------------------------------------------| | :output-type | Full documentation or summary table | | :output-subtype | When \\='table, output functions or aliases? | | :only-non-empty-headings | Whether to exclude empty headings | | :only-with-examples | Whether to exclude functions without examples | | :describe-fun | Name of heading-describing function | | :make-links | Whether to make links for functions | | :signatures-column | Whether to add a signatures column | | :inter-heading-spacing | Number of blank lines between headings | | :root-level | Number of asterisks at root subtrees | | :include-pred | Function/aliases names to include (predicate) | | :exclude-pred | Function/aliases names to exclude (predicate) | So :root-level is the level (number of asterisks) to be assigned to the root subtree(s) generated when :output-type (see below) is \\='describe. (It has no influence when :output-type is \\='table.) When :root-level is nil, 0, 2, or not a natural number it defaults, unchanged, to 2. If it's 1, all subtrees are promoted. If greater than 2, all subtrees are demoted accordingly. If you don't want to convert any links wrapped in \\=`\\=' or \\=‘\\=’ from your docstrings to point to custom_ids, set :make-links to nil. Inclusion is processed before exclusion. One example of :exclude-pred would be: #+begin_src emacs-lisp (lambda (fun) (s-match \"--\" fun)) #+end_src to exclude showing internal functions in the documentation \(unless you use \"--\" to denote anaphoric macros — then not). Another example is a :include-pred of #+begin_src emacs-lisp (lambda (fun) (commandp fun)) #+end_src to only include functions that are commands — that is, interactive. And :describe-fun is to allow LIBFILE to have the ‘orgreadme-fy-describe’ macro under a different name. See ‘orgreadme-fy-describe’ for more information. Not all options apply to the different output types. When not applicable (N/A), passing the option has no effect. | | describe | table | table | |--------------------------+------------+------------+----------| | Key | functions | functions | aliases | |--------------------------+------------+------------+----------| | :only-non-empty-headings | ✓ | N/A | N/A | | :inter-heading-spacing | ✓ | N/A | N/A | | :only-with-examples | ✓ | N/A | N/A | | :describe-fun | ✓ | N/A | N/A | | :signatures-column | N/A | ✓ | N/A | | :root-level | ✓ | N/A | N/A | | :make-links | ✓ | ✓ | N/A | | :include-pred | ✓ | ✓ | ✓ | | :exclude-pred | ✓ | ✓ | ✓ | The plist is updated in this order: ‘orgreadme-fy-ops-plist-default’, ‘orgreadme-fy-ops-plist-user’, and OPS. So OPS has higher precedence. If EXMPFILE is nil, use ‘orgreadme-fy-examples-rel-path’. EXCL-LIST is a list of callables to exclude besides those of ‘orgreadme-fy-top-callables-exclusions-list-default’ and ‘orgreadme-fy-top-callables-exclusions-list-user’." (unless exmpfile (setq exmpfile (f-expand orgreadme-fy-examples-rel-path (f-parent libfile)))) (unless (f-exists? exmpfile) (warn "orgreadme-fy-library->subtree: Couldn't find %s" exmpfile)) (unless (f-exists? libfile) (user-error "‘orgreadme-fy-library’->subtree: Couldn't find %s" libfile)) ;; Library must be loaded so we can fetch docstrings and signatures: (-> libfile f-long f-no-ext load) (setq ops (h-mix (h<-plist orgreadme-fy-ops-plist-default) (h<-plist orgreadme-fy-ops-plist-user) (h<-plist ops))) (h-let ops ;; Some more error handling (unless (natnump .inter-heading-spacing) (error ":inter-heading-spacing must be a natural number")) (unless (booleanp .make-links) (error ":make-links must be a boolean")) (unless (booleanp .signatures-column) (error ":signatures-column must be a boolean")) (unless (booleanp .only-non-empty-headings) (error ":only-non-empty-headings must be a boolean")) (unless (booleanp .only-with-examples) (error ":only-with-examples must be a boolean")) (unless (memq .output-type '(table describe)) (error "Invalid :output-type option")) ;; Handle the case where :describe-fun isn't ‘orgreadme-fy-describe’, such ;; as when one borrows the macro and rename it with the package's prefix. (setq excl-list (-uniq (cons .describe-fun excl-list))) (let* ((lines-ht (orgreadme-fy-lines-ht libfile)) (headings-ht (orgreadme-fy-headings-ht lines-ht)) (head-rng-ht (orgreadme-fy-headings-ranges-ht headings-ht libfile)) ;; Further trim headings-ht: only those under "Code:" (code-line (car (h--first (s-prefix? ";;; Code:" value t) lines-ht))) (headcode-ht (h--sel (and (> key code-line) (not (s-match "^;;; .*" value))) headings-ht)) examples-ht descs-ht funs-ht aliases-ht hline heading level descs funs-to-ht funs) (with-temp-buffer (let ((standard-output (current-buffer))) (pcase .output-type ('table (setq hline "|----------+------------+-----------|\n") (princ "| ") (pcase .output-subtype ('functions (setq funs-ht (orgreadme-fy-funs-ht lines-ht excl-list)) (princ orgreadme-fy-function-word) (princ " | ") (princ orgreadme-fy-summary-word) (when .signatures-column (princ " | ") (princ orgreadme-fy-signature-word))) ('aliases (setq aliases-ht (orgreadme-fy-aliases-ht libfile)) (princ orgreadme-fy-alias-from-word) (princ " | ") (princ orgreadme-fy-alias-to-word))) (princ " |\n") (h--each headcode-ht (pcase .output-subtype ('functions (setq funs (->> (orgreadme-fy-range-lookup key head-rng-ht funs-ht) (-map #'cdr)))) ('aliases ;; That's a ht of key:val = alias-from:alias-to ;; aliases-ht holds cons-cells as values, and ;; ‘orgreadme-fy-range-lookup’ makes that an alist (setq funs-to-ht (->> (orgreadme-fy-range-lookup key head-rng-ht aliases-ht) h<-alist) funs (h-lkeys funs-to-ht)))) (when .include-pred (setq funs (--filter (funcall .include-pred it) funs))) (when .exclude-pred (setq funs (-difference funs (--filter (funcall .exclude-pred it) funs)))) ;; These work on each heading (inside ‘h--each’) ;; Make table (princ hline) (dolist (fun funs) (pcase .output-subtype ('functions (let ((1st (orgreadme-fy-fun->1stline fun)) (funli (if .make-links (format "[[#%s][%s]]" fun fun) fun))) (mapc #'princ `("| " ,funli " | " ,1st " |")) (when .signatures-column (princ (->> (orgreadme-fy-fun->signature fun) read (cons 'fn) (format " %s |")))))) ('aliases (let ((to (h-get funs-to-ht fun))) (mapc #'princ `("| " ,fun " | " ,to " |"))))) (terpri))) ;; These work globally (outside and after ‘h--each’) ;; Remove repeated hlines, align. (goto-char (point-min)) (while (search-forward hline nil t) (while (looking-at (regexp-opt (list hline))) (replace-match "" nil t))) (orgreadme-fy-align-tables-in-buffer)) ('describe (setq examples-ht (orgreadme-fy-examples-ht exmpfile) descs-ht (orgreadme-fy-descs-ht libfile .describe-fun) funs-ht (orgreadme-fy-funs-ht lines-ht excl-list)) (h--each headcode-ht (setq heading (orgreadme-fy-heading-elisp->org value) level (->> heading (s-split " ") car length) descs (orgreadme-fy-range-lookup key head-rng-ht descs-ht) funs (->> (orgreadme-fy-range-lookup key head-rng-ht funs-ht) (-map #'cdr))) (when .include-pred (setq funs (--filter (funcall .include-pred it) funs))) (when .exclude-pred (setq funs (-difference funs (--filter (funcall .exclude-pred it) funs)))) ;; These work on each heading (inside ‘h--each’) ;; Heading proper (princ heading) (terpri) ;; Heading description (dolist (desc descs) ;; Add an empty line before each description — unless you ;; chose tight spacing, in which case it'd look a bit odd. (when (/= 0 .inter-heading-spacing) (terpri)) (princ desc) (terpri)) (dotimes (_i .inter-heading-spacing) (terpri)) ;; Functions' signature, docstring, examples (dolist (fun funs) (let ((src-block (orgreadme-fy-fun->org-src-block fun examples-ht)) signature 1stline middledoc asterisks) (if (and .only-with-examples (s-blank? src-block)) (ignore) (setq signature (orgreadme-fy-fun->signature fun) 1stline (orgreadme-fy-fun->1stline fun) middledoc (orgreadme-fy-fun->middledoc fun) asterisks (s-repeat (1+ level) "*")) (princ asterisks) (princ " ") (princ fun) (princ " ") (princ "=") (princ signature) (princ "=") (terpri) ;; Custom IDs (princ ":PROPERTIES:\n:CUSTOM_ID: ") (princ fun) (princ "\n:END:\n") (unless (s-blank? 1stline) ;; Add an empty line after function's headline — unless ;; you chose tight spacing, in which case it'd look a ;; bit odd. (when (/= 0 .inter-heading-spacing) (terpri)) (princ 1stline) (terpri)) (unless (s-blank? middledoc) (terpri) (princ middledoc) (terpri)) (princ src-block) (dotimes (_i .inter-heading-spacing) (terpri)))))) ;; These work globally (outside and after ‘h--each’) ;; Remove empty headings (when .only-non-empty-headings (orgreadme-fy-remove-empty-headings)) ;; Adjust level (orgreadme-fy-adjust-root-level .root-level) ;; Create links (when .make-links (orgreadme-fy-make-links-in-buffer))))) ;; Get the string, adjusting empty lines at the top. (--> (buffer-substring-no-properties (point-min) (point-max)) (s-trim it) (pcase .output-type ('table (identity it)) ('describe (s-prepend (s-repeat .inter-heading-spacing "\n") it)))))))) ;;;###autoload (defun orgreadme-fy-libfile->orgbuf (libfile &optional ops exmpfile excl-list) "Paste result of ‘orgreadme-fy-libfile->orgstr’ in an org buffer. This way you can visualize the results of the insertion files that you can generate with ‘orgreadme-fy-libfile->orgstr’. LIBFILE is a library file. For details about OPS, EXMPFILE, EXCL-LIST, see ‘orgreadme-fy-libfile->orgstr’." (let ((buffer (get-buffer-create "*orgreadme-fy results*"))) (when (pop-to-buffer buffer) (erase-buffer) (ignore-errors (org-mode)) (toggle-truncate-lines 1) (save-excursion (insert (orgreadme-fy-libfile->orgstr libfile ops exmpfile excl-list)))))) ;;;;; Generate hash tables (orgreadme-fy-describe "Functions that create hash tables using your library or examples file.") (defun orgreadme-fy-lines-ht (file) "Given a FILE, create hash table with all its lines. Keys are 0-started line numbers. Values are line contents as string." (-> file f-read h<-lines)) (defun orgreadme-fy-headings-ht (lines-ht) "Given a LINES-HT, return a ht with only lines of elisp headings. Headings match this regex \"^;;;;* \"." (h--sel (s-match "^;;;;* " value) lines-ht)) (defun orgreadme-fy-headings-ranges-ht (headings-ht &optional file) "Return ht with the same keys, and values being 1- next key. This is the range this heading, stored in HEADINGS-HT, covers. Useful for lookup of descriptions and functions, since if their line numbers fall inside this range they are under this heading. If optional argument FILE is given, use its number of lines as the limit for last range. Otherwise use 1000000." (let* ((keys-hd (h-lkeys headings-ht)) (vals-hd (-snoc (-map #'1- (cdr keys-hd)) (if file (with-temp-buffer (insert-file-contents file) (goto-char (point-max)) (line-number-at-pos)) 1000000)))) ;; Just a giant number ^ beyond the num lines of libfile. ;; This simplifies dealing with the last heading case. (h-zip-lists keys-hd vals-hd))) (defun orgreadme-fy-descs-ht (libfile &optional describe-fun) "Given a LIBFILE, return a ht with ‘orgreadme-fy-describe’ strings. The keys of the table will be the 0-started line-numbers where calls to that macro are found in the LIBFILE, and the associated values will be the strings passed to them. In other words, this function recovers the strings from all calls to ‘orgreadme-fy-describe’ and their position in the file. DESCRIBE-FUN is the name of the describe function used. It could happen that LIBFILE used the macro but renamed it (for example, to its same namespace), so this option allow for looking up different names. If nil, it defaults to ‘orgreadme-fy-describe’. DESCRIBE-FUN can be passed as string or symbol, both are ok. So if your library is named foo and you decided to copy the ‘orgreadme-fy-describe’ macro into it and rename it to foo-describe, you could pass this argument as either #'foo-describe, \\='foo-describe or \"foo-describe\". The application of ‘1-’ to line numbers happens because ‘h<-lines’ (which see), used by ‘orgreadme-fy-lines-ht’, assigns an index of 0 to the first line read, and we want the keys of the generated hash tables to be comparable and refer to the same line positions." (unless describe-fun (setq describe-fun #'orgreadme-fy-describe)) (let ((res (h-new)) ;; Q: Is there a better way than eval to pass a variable to rx? ;; A: Apparently these are equivalent: ;; (eval `(rx (: bol..... <=> (rx-form `(: bol..... (rx (eval `(rx (: bol (* blank) "(" (| ,(format "%s" describe-fun)) (+ (any " \t\n"))))))) (with-temp-buffer (insert-file-contents libfile) (with-syntax-table emacs-lisp-mode-syntax-table (while (re-search-forward rx nil t) (h-put! res (1- (line-number-at-pos)) (if (looking-at "\"") (read (buffer-substring-no-properties (point) (progn (forward-sexp) (point)))) (error "‘orgreadme-fy-descs-ht’: %s %s" describe-fun "not followed by string")))) res)))) (defun orgreadme-fy-aliases-ht (libfile) "Given a LIBFILE, return a ht with from-to aliases pairs. The keys of the table will be the 0-started line-numbers where calls to ‘defalias’ are found in the LIBFILE." (let ((result (h-new))) (with-temp-buffer (insert-file-contents libfile) (with-syntax-table emacs-lisp-mode-syntax-table (while (re-search-forward (rx (: bol (* blank) ?\( (| "defalias") symbol-end (+ (in " \t")) (? "#") (? "'") (group (* (| (syntax word) (syntax symbol) (: ?\\ nonl)))) (+ (in " \t")) (? "#") (? "'") (group (* (| (syntax word) (syntax symbol) (: ?\\ nonl)))))) nil t) (h-put! result (1- (line-number-at-pos)) (cons (match-string 1) (match-string 2)))) result)))) (defun orgreadme-fy-funs-ht (lines-ht &optional excl-list) "Given a LINES-HT, return a ht with only lines of top‑level defs. Values are cons of deftype and callable (that is, the functions and macro symbols passed to the deftype). For example, this could become: #+begin_src emacs-lisp \\='(\"defun\" . \"orgreadme-fy-funs-ht\") #+end_src EXCL-LIST is a list of callables to exclude besides those of ‘orgreadme-fy-top-callables-exclusions-list-default’ and ‘orgreadme-fy-top-callables-exclusions-list-user’." (->> lines-ht (h--sel (s-starts-with? "(" value)) (h--rej (member (->> value (s-chop-prefix "(") (s-replace-regexp " .*" "") intern) (-concat excl-list orgreadme-fy-top-callables-exclusions-list-user orgreadme-fy-top-callables-exclusions-list-default))) (h--hmap key (->> value (s-chop-prefix "(") (s-split " ") (-take 2) (apply #'cons))))) (defun orgreadme-fy-examples-ht (exmpfile) "Given an EXMPFILE, return a ht with ‘exemplify-ert’ sexp string. Function name is what comes after \"(exemplify-ert \". This will be the key. The value will be the whole sexp as string. No change in formatting happens: treated as string, not sexp — so it preserves indentations and alignments from your examples file." (let ((result (h-new))) (if (not (f-exists? exmpfile)) result (with-temp-buffer (insert-file-contents exmpfile) (with-syntax-table emacs-lisp-mode-syntax-table (while (re-search-forward (rx (: bol (* blank) "(" (| "exemplify-ert") (+ (any " \t\n")) (group (+ (not (any " \t\n")))))) nil t) (beginning-of-line) (h-put! result (match-string 1) (buffer-substring-no-properties (point) (progn (forward-sexp) (point))))) result))))) (defun orgreadme-fy-range-lookup (curkey range-ht some-ht) "Return values that fall within desirable range. Return a list of values whose keys fall in the range between CURKEY and its lookup on RANGE-HT for SOME-HT." (->> some-ht (h--sel (and (>= key curkey) (<= key (h-get range-ht curkey)))) h-lvalues)) ;;;;; Generate org pieces (orgreadme-fy-describe "Functions that generate org-mode strings, such as org headings or org blocks from your examples.") (defun orgreadme-fy-fun->org-src-block (fun examples-ht) "Given FUN and EXAMPLES-HT, return org src block as string. FUN is passed as a string. The first line and the closing parenthesis of the exemplify-ert sexp are replaced by the block's beg and end." (if-let ((exemplify-ert-str (h-get examples-ht fun))) (->> exemplify-ert-str (s-replace-regexp "\\`.*\n+" (concat "\n" "#+name: examples-" fun "\n" "#+begin_src emacs-lisp " ":exports code :eval no\n")) ;; It seems that the :exports etc params here ^ are lost when ;; exported to README.org — so we'll post-process it as well. (s-replace-regexp "\n*)\\'" "\n#+end_src\n")) "")) (defun orgreadme-fy-heading-elisp->org (str) "Convert an elisp heading string STR into an org heading." (--> str (s-replace-regexp "^;;" "" it) (s-split " " it) (s-join " " (cons (s-replace ";" "*" (car it)) (cdr it))))) ;;;;; Generate parts of functions' documentation (orgreadme-fy-describe "Functions that generate parts of a function's documentation, such as their signature, first line, and rest of its description.") (defun orgreadme-fy-fun->documentation (fun) "Given FUN, return its documentation." (let ((text-quoting-style 'grave)) (if-let ((doc (with-demoted-errors "Error: %S" (documentation (orgreadme-fy--obj-sym-str-or-error fun) t)))) (orgreadme-fy-deal-with-escapes doc) ""))) (defun orgreadme-fy-fun->docstring (fun) "Given FUN, return its docstring, trimmed." (if-let ((doc (orgreadme-fy-fun->documentation fun))) (->> doc (s-replace-regexp "\n+(fn.*\\'" "") s-trim) "")) (defun orgreadme-fy-fun->1stline (fun) "Given FUN, return first line of its docstring." (->> (orgreadme-fy-fun->documentation fun) s-lines car)) (defun orgreadme-fy-fun->middledoc (fun) "Given FUN, return the middle of its docstring, trimmed." (->> (orgreadme-fy-fun->docstring fun) s-lines cdr (s-join "\n") s-trim-left)) (defun orgreadme-fy-fun->signature (fun) "Given FUN, return its signature. If the docstring has (fn ...) usage, use that. Otherwise, use the one found in ‘symbol-function’." (-if-let* ((doc (orgreadme-fy-fun->documentation fun)) (match (s-match "(fn.*\\'" doc))) (->> match car downcase read cdr (format "%s")) (-if-let* ((sig (orgreadme-fy-fun->signature-from-symbol-function fun)) (out (format "%s" sig))) (pcase out ("0" "()") (_ out)) "()"))) (defun orgreadme-fy-fun->signature-from-symbol-function (fun) "Given FUN, return signature as it shows in ‘symbol-function’. So if the docstring has (fn ...) usage, don't use it." (let ((foo (symbol-function (h-as-symbol fun)))) ;; macro (and (eq 'cons (type-of foo)) (eq 'macro (car foo)) (setq foo (cdr foo))) ;; function (pcase (type-of foo) ;; byte-compiled ('compiled-function (aref foo 0)) ;; regular ('cons (cadr foo))))) (defun orgreadme-fy-deal-with-escapes (string) "Deal with \\\\== in STRING. \\\\== quotes the following character and is discarded; thus, \\\\==\\\\== puts \\\\== into the output, \\\\==\\[ puts \\[ into the output, and \\\\==\\=` puts \\=` into the output. Return the original STRING if no substitutions are made. Otherwise, return a new string. This function has been adapted from ‘substitute-command-keys’, trimming it down to only the part related to escaping." (when (not (null string)) (let ((inhibit-modification-hooks t)) (with-temp-buffer (insert string) (goto-char (point-min)) (while (< (point) (point-max)) (let ((orig-point (point))) (cond ;; 1. Handle all sequences starting with "\" ((= (following-char) ?\\) (ignore-errors (forward-char 1)) (cond ;; 1A. Ignore \= at end of string. ((and (= (+ (point) 1) (point-max)) (= (following-char) ?=)) (forward-char 1)) ;; 1B. \= quotes the next character; thus, to put in \[ ;; without its special meaning, use \=\[. ((= (following-char) ?=) (goto-char orig-point) (delete-char 2) (ignore-errors (forward-char 1))))) ;; 2. Nothing to do -- next character. (:otherwise (forward-char 1))))) (buffer-string))))) (defun orgreadme-fy--obj-sym-str-or-error (obj) "If OBJ isn't passed as either string or symbol, signal error. Otherwise return OBJ as symbol." (cond ((symbolp obj) obj) ((stringp obj) (intern obj)) (t (user-error "%s is neither symbol nor string" obj)))) ;;;;; Generate parts of user options' documentation (orgreadme-fy-describe "Functions that generate parts of a user option's documentation, such as their first line, and the rest of its description. These functions were added later and are still unused. For the moment, automatic documentation of user options is unavailable, so ‘orgreadme-fy-top-callables-exclusions-list-default’ has ‘defcustom’ as member. User options would need to be treated a bit differently from functions: no signatures, no ‘exemplify-ert’, different way of fetching the docstring, shouldn't mix with functions in the 'table option of :output-type in ‘orgreadme-fy-libfile->orgstr’, etc. So it seems that including them would add a bit of complexity to the current code. Given the above, and given that it seems likely that most people won't be too interested in documenting their ‘defcustom’ variables in their README, I haven't implemented it. In any case, with this future possibility in mind, I leave these functions here.") (defun orgreadme-fy-cusvar->documentation (cusvar) "Given custom variable CUSVAR, return its documentation." (setq cusvar (orgreadme-fy--obj-sym-str-or-error cusvar)) (let ((text-quoting-style 'grave)) (if (and (boundp cusvar) (custom-variable-p cusvar)) (get cusvar 'variable-documentation) ""))) (defun orgreadme-fy-cusvar->docstring (cusvar) "Given custom variable CUSVAR, return its docstring, trimmed." (s-trim (orgreadme-fy-cusvar->documentation cusvar))) (defun orgreadme-fy-cusvar->1stline (cusvar) "Given custom variable CUSVAR, return 1st line of its docstring." (->> (orgreadme-fy-cusvar->documentation cusvar) s-lines car)) (defun orgreadme-fy-cusvar->middledoc (cusvar) "Given custom variable CUSVAR, return the middle of its docstring." (->> (orgreadme-fy-cusvar->docstring cusvar) s-lines cdr (s-join "\n") s-trim-left)) (defun orgreadme-fy-cusvar->signature (cusvar) "Given custom variable CUSVAR, return an empty string. Unlike functions, there are no signatures." (ignore cusvar) "") ;;;;; Buffer replacements and adjustments (orgreadme-fy-describe "Functions that make replacements and adjustments in the current buffer Used mostly for post-processing generated org files.") ;;;;;; README.org post-processing (defun orgreadme-fy-post-process-readme-org () "Post-process current buffer, assumed to be README.org." (when orgreadme-fy-downcase-properties (orgreadme-fy-downcase-properties-in-buffer)) (orgreadme-fy-restore-params-of-org-src-blocks-in-buffer) ;; See function's docstring to see why: (orgreadme-fy-make-prop-nil-in-buffer) ;; For some reason org-export messes up with the already-aligned tables ;; when they have links. It seems that org-export re-aligns them with ;; link-visibility on. The easiest way that I've found to fix this is to ;; simply visit the file and realign all tables again while keeping link ;; visibility off: (orgreadme-fy-align-tables-in-buffer)) (defun orgreadme-fy-align-tables-in-buffer () "Align all org tables in current buffer." (interactive) (ignore-errors (org-mode)) (when (equal major-mode 'org-mode) (progn (add-to-invisibility-spec '(org-link)) (org-restart-font-lock) (if (boundp 'org-link-descriptive) (setq org-link-descriptive t) (with-no-warnings ; Obsolete since Org 9.3: (setq org-descriptive-links t)))) (outline-show-all) ;; Alternative to the below: ;; (goto-char 3) ;; (call-interactively #'org-ctrl-c-ctrl-c) (org-table-map-tables #'org-table-align t) (outline-hide-sublevels 1))) (defun orgreadme-fy-make-prop-nil-in-buffer () "Find #+:options: prop:t in this buffer and change it to nil. Your readme-template.org should have prop:t so that property drawers are exported to README.org. They have custom_ids, needed for anchors and internal linking. However, we want to turn this off in README.org, or they'll end up exported to HTML as tiny code blocks having CUSTOM_ID: something." (interactive) (save-excursion (goto-char (point-min)) (let ((case-fold-search t)) (when (re-search-forward "^#\\+options: *.*\\(prop:t\\)" nil t 1) (replace-match "prop:nil" nil nil nil 1))))) ;; ^ Other ways to fix this? Maybe some way to say in the template itself: ;; if exporting to org, then prop:t, else prop:nil? (defun orgreadme-fy-restore-params-of-org-src-blocks-in-buffer () "Find elisp source blocks and restore its parameters. They were added by ‘orgreadme-fy-fun->org-src-block’, but lost when exported — so restore them. While we're on it, downcase them all." (interactive) (save-excursion (goto-char (point-min)) (let ((case-fold-search t)) (while (re-search-forward "^#\\+begin_src emacs-lisp.*$" nil t) (replace-match "#+begin_src emacs-lisp :exports code :eval no") ;; Make casing consistent with the above: (forward-line -1) (when (looking-at "#\\+NAME:") (downcase-region (point) (+ 6 (point)))) (when (re-search-forward "^#\\+END_SRC" nil t) (replace-match "#+end_src" t)))))) (defun orgreadme-fy-downcase-properties-in-buffer () "Downcase ^#+OPTIONS, ^#+AUTHOR and the like in the buffer." (interactive) (save-excursion (goto-char (point-min)) (while (re-search-forward (rx (: bol ?# ?+ (group (| "OPTIONS" "TOC" "TITLE" "AUTHOR" "DESCRIPTION" "KEYWORDS" "LANGUAGE" "RESULTS" "BEGIN_EXAMPLE" "END_EXAMPLE" "BEGIN_QUOTE" "END_QUOTE" "BEGIN_SRC" "END_SRC")) ?:)) nil t) (replace-match (downcase (match-string 1)) t nil nil 1)))) ;;;;;; libfile->orgstr generation post-processing (defun orgreadme-fy-remove-empty-headings () "Remove empty headings in buffer." (interactive) (let ((do-again t) count) ;; It may be nested with no contents at many levels, so we redo until ;; there's no more empty node. (while do-again (goto-char (point-max)) (setq count 0) (while (re-search-backward org-outline-regexp-bol nil t) (--> (cadr (org-element-headline-parser (point-max))) (unless (plist-get it :contents-begin) (orgreadme-fy-delete-subtree) (setq count (1+ count))))) (when (= count 0) (setq do-again nil))))) (defun orgreadme-fy-delete-subtree () "Delete subtree at point." (interactive) (atomic-change-group (when (org-mark-subtree) (delete-region (region-beginning) (region-end))))) (defun orgreadme-fy-adjust-root-level (&optional root-level) "Adjust to ROOT-LEVEL the root level of subtrees in buffer. Current root level is assumed to be 2." (interactive) (cond ((or (not root-level) (not (natnump root-level)) (= 2 root-level) (= 0 root-level)) (ignore)) ((= root-level 1) (orgreadme-fy-promote-all-subtrees-in-buffer 1)) (t (goto-char (point-min)) (orgreadme-fy-demote-all-subtrees-in-buffer (- root-level 2))))) (defun orgreadme-fy-promote-all-subtrees-in-buffer (&optional n) "Promote all subtrees in buffer by N levels. If nil, N defaults to 1." (interactive) (goto-char (point-min)) (while (not (eobp)) (when (org-at-heading-p) (delete-char (or n 1))) (forward-line))) (defun orgreadme-fy-demote-all-subtrees-in-buffer (&optional n) "Demote all subtrees in buffer by N levels. If nil, N defaults to 1." (interactive) (goto-char (point-min)) (while (not (eobp)) (when (org-at-heading-p) (insert (s-repeat (or n 1) "*"))) (forward-line))) (defun orgreadme-fy-make-links-in-buffer () "Replace `'- and ‘’-wrapped links with org links to anchors. Only make links if target anchors actually exist. Otherwise, replace the quotes with equal signs to make it monospace. Also replace Info links." (interactive) ;; Info (goto-char (point-min)) (while (re-search-forward "Info anchor [‘`](\\([^ ]+\\)) *\\([^’']+\\)[’']" nil t) (let* ((m1 (match-string 1)) (m2 (match-string 2)) (ma (format "info:%s#%s" m1 (s-replace " " "%20" m2))) (mb (format "(%s) %s" m1 m2))) (replace-match (format "[[%s][%s]]" ma mb)))) ;; Regular (goto-char (point-min)) (let ((cid-ht (orgreadme-fy-fetch-custom-ids-in-buffer)) m) (while (re-search-forward "[‘`]\\([^’' ]+\\)[’']" nil t) (setq m (match-string 1)) (if (h-get cid-ht m) (replace-match (format "[[#%s][%s]]" m m)) (replace-match (format "=%s=" m)))))) (defun orgreadme-fy-fetch-custom-ids-in-buffer () "Make hash-table-as-set of all custom-id properties in buffer." (interactive) (let ((cid-ht (h-new))) (goto-char (point-min)) (org-map-region ;; (lambda () (h-mix! cid-ht ;; (h-as-set<-lines ;; (org-entry-get nil "custom_id")))) ;; The below is the same but more straightforward: (lambda () (h-put! cid-ht (org-entry-get nil "custom_id") t)) (point-min) (point-max)) ;; remove nil key if it happened and return the table (h-rem cid-ht nil))) ;;;;; Accessing READMEs ;;;;;; Quick access to a library's README (orgreadme-fy-describe "Commands to quickly access a library's README.") ;;;###autoload (defun orgreadme-fy-see-library-readme (&optional libname) "Open library LIBNAME's README.org file. Load LIBNAME, if not yet loaded, then search for README.org in the directory from which it has been loaded. If not found, look for README.md, README.markdown, README.rst, README.txt, and README. The first one found, if any, is opened read-only. If called interactively, library name will be asked." (interactive) (unless libname (setq libname (completing-read "Library name: " (orgreadme-fy-list-loaded-libraries) nil t))) (--> (orgreadme-fy-locate-library-readme libname) (if (not it) (message "Couldn't find a README file for %s" libname) (let ((pr (make-progress-reporter (format "Opening %s ... " (abbreviate-file-name it))))) (find-file-read-only it) (progress-reporter-done pr))))) (defun orgreadme-fy-locate-library-readme (libname) "Locate library LIBNAME's README file. Load LIBNAME, if not yet loaded, then search for README.org in the directory from which it has been loaded. If not found, look for README.md, README.markdown, README.rst, README.txt, and README. Return the filename if found, nil otherwise." (--> (orgreadme-fy-libname->libfile libname) (expand-file-name "README" (file-name-directory it)) (let* ((exts '(".org" ".md" ".markdown" ".rst" ".txt" "")) (all (-map (lambda (ext) (format "%s%s" it ext)) exts))) (car (-filter #'file-exists-p all))))) (defun orgreadme-fy-list-loaded-libraries () "List all loaded libraries." (let ((libs (delq nil (-map #'car load-history)))) (nconc (delq nil (-map (lambda (l) (setq l (file-name-nondirectory l)) (while (not (equal (setq l (file-name-sans-extension l)) l))) l) libs)) libs))) (defun orgreadme-fy-libname->libfile (libname) "Given LIBNAME, load library and return filename it loaded from." (require (if (symbolp libname) libname (intern libname))) (--> load-history (-map #'car it) (car (-filter (lambda (lib) (when (string-match (format "/%s.el[cn]*$" libname) lib) lib)) it)))) ;;;;;; See OrgReadme-Fy's README (orgreadme-fy-describe "A command to quickly open the README.org of this very library.") ;;;###autoload (defun orgreadme-fy-see-readme (&optional heading narrow) "Open orgreadme-fy's README.org file. Search for the file in orgreadme-fy.el's directory. If found, open it read-only. If optional argument HEADING is passed, try to navigate to the heading after opening it. HEADING should be a string. If optional argument NARROW is non-nil, narrow to that heading. This argument has no effect if HEADING is nil or not found. Examples: (orgreadme-fy-see-readme \"News\" t) (orgreadme-fy-see-readme \"Installation\")" (interactive) (let ((readme orgreadme-fy--readme-org)) (if (file-exists-p readme) (let ((pr (make-progress-reporter (format "Opening %s ... " (abbreviate-file-name readme))))) (find-file-read-only readme) (when heading (orgreadme-fy--goto-org-heading heading narrow)) (progress-reporter-done pr)) (message "Couldn't find %s's README.org" orgreadme-fy--name)))) (defun orgreadme-fy--goto-org-heading (heading &optional narrow) "Navigate to org HEADING and optionally NARROW to it." (let* ((hrx (format "^[*]+ %s" heading)) (pos (save-match-data (save-excursion (save-restriction (widen) (goto-char (point-max)) (re-search-backward hrx nil t 1)))))) (when pos (widen) (goto-char pos) (if (and narrow (fboundp 'org-narrow-to-subtree)) (org-narrow-to-subtree) (recenter-top-bottom 1)) (when (fboundp 'outline-show-subtree) (outline-show-subtree))))) ;;;;;; See OrgReadme-Fy's News ;;;###autoload (defun orgreadme-fy-see-news () "See the News in orgreadme-fy's README.org file." (interactive) (orgreadme-fy-see-readme "News" 'narrow) (let ((cmds '(outline-hide-subtree outline-show-children outline-next-heading outline-show-subtree))) (and (fboundp (nth 0 cmds)) (fboundp (nth 1 cmds)) (fboundp (nth 2 cmds)) (fboundp (nth 3 cmds)) (mapc #'funcall cmds)))) ;;;; Wrapping up (provide 'orgreadme-fy) ;; Local Variables: ;; coding: utf-8 ;; indent-tabs-mode: nil ;; sentence-end-double-space: nil ;; outline-regexp: ";;;;* " ;; End: ;;; orgreadme-fy.el ends here