Generating cheatsheets from another site

A site that already publishes code examples and explanatory sections holds everything a cheatsheet needs; node tools/cheatsheet-site.mjs <site> is what reads it.

It crawls the tree for elements carrying data-cheatsheet, collects what each one wraps, and writes a cheatsheet page per folder of the site that marked any. The output folder is resolved before anything is written - cheatsheets/ under the site root, then cheatsheets/cheatsheets/, then cheatsheets/cheatsheets_0/ and upward until a name is free - so a site that already has one of those keeps it. The script installs nothing, because it runs against somebody else's checkout: every import is a Node builtin or a path inside this repository.

A folder is a subject, so a folder is a cheatsheet

guide/install.html and guide/usage.html are written as guide.html, headed for the folder, with a section per page in the order the site reads them - each keeping the heading it asked for. A page at the site root has no folder to be named for and is written under its own name. Neither the name nor the markup depends on how many pages a folder holds, so the day a second page arrives is not the day the first page's output changes its name. Two folders of one name under different parents, or a root page named for a folder beside it, still want one file in a flat folder: that is reported and the later one is not written, rather than one being overwritten in silence.

The attribute method

The marker's value chooses the page's heading. data-cheatsheet="title" takes it from the page's own <title> and data-cheatsheet="page" from its file name; marked without a value, a page takes its title with its file name behind it. A value naming neither is reported with the page it was written on rather than quietly handed the default, as is an explicit title on a page that has none - the default has a documented fallback and the ask does not.

A heading read off a name is read as prose - somePageName.html is headed Some Page Name, and parseHTMLString.html is Parse HTML String - while a <title> is left exactly as it was written, because a file name is written for a filesystem and a title was written to be read.

The comment method

A page with more than one thing to say marks its material with a comment instead.

<!-- {% cheatsheet: [heading: page, group: page, name: splice] %} -->

It applies to the element that follows it, with nothing but whitespace in between, and carries as many properties as it needs: heading names the source the heading is read from, group says whether the page joins its folder's cheatsheet or is one of its own, and name says what file the cheatsheet is written as. A cheatsheet comment is still a comment, so a marker written inside one is still not a marker.

The two methods side by side

The attributeThe comment
Written on the element wrapping the material on the line in front of that element
Says one thing: where the heading is read from three: the heading source, whether the page joins its folder's cheatsheet, and the file the cheatsheet is written as
To learn nothing - an attribute and one of two values a bracketed list, three property names, and the values each takes
To get wrong a value naming no heading source that, a property the vocabulary does not hold, a property written with no value, one written twice, and a list that is not a list
Reported with the page the page and the line

The trade-off is the first two rows against the next two. An attribute carries one value and there is nothing to get right beyond spelling it, which is why it is the method to reach for first: a page that only wants its material collected says so and is done. A comment carries as much as the page has to say, and the price is a syntax - a list, a vocabulary and a value per property - with five more ways to write it wrong. So the question is not which method is better but how much the page has to say: one thing, and the attribute is the whole answer; a second or a third, and the comment is the only place to put it, because an attribute has nowhere to put a second value.

Nothing is lost by starting with the attribute and moving later. The two feed one collector, a page may use either, a site may use both, and a page changes method by having a comment written in front of the element it already marked.

What happens when they disagree

Where the two speak about one heading the comment wins, because it was written about the material rather than left on an element. The disagreement is reported either way, as is a property the vocabulary does not hold or a value it does not take, with the page and the line it was written on.

The menu, and the page it points at

The third marking is the only one written into a page the site owner keeps rather than read out of it. data-cheatsheet-menu says where the menu goes, and its value says which of the two:

<div data-cheatsheet-menu="page"></div>       a link to the page carrying every cheatsheet
<div data-cheatsheet-menu="dropdown"></div>   an entry per page in the generated folder

Every cheatsheet is also concatenated into all-cheatsheets.html in the generated folder, composed by the same function that writes the individual pages - each cheatsheet is a section of it, with the pages inside a folder's cheatsheet one heading level below, so the two cannot show the same material differently. It is a page of that folder like any other, so it claims its name before any cheatsheet does: a site whose own page wants that name is told rather than written over.

A link is relative to the page it is written into, not to the folder the tool resolved, so a marked page two folders down gets ../../cheatsheets/arrays.html. An entry's text is the prose form of the generated file's name - base64Utils.html reads Base 64 Utils - rather than the cheatsheet's heading, which may have been read off a <title> and be a sentence. The dropdown is a <details> with no class, no style and no script, so it works with JavaScript off and a site that wants its own control has markup to replace.

A menu element is never written over. A fill is wrapped in <!-- cheatsheet-site:menu --> and its closing pair, which is how a run tells its own work from a site owner's: an element carrying that signature is filled again, and one carrying anything else is left exactly as it was. The signature is what makes the refusal possible at all - a menu element is not empty on the second run either, so a refusal that read emptiness would fill once and never again.

Five things are refused rather than written: a value naming neither menu or no value at all, since an attribute carries no default here and guessing writes markup into somebody else's page; an element also carrying data-cheatsheet, or one inside material that is, because filling it would replace the content just read out of it; an element holding such material, or holding a second menu element, since the first would leave the site with no copy of that material at all and the second cut the page's markup apart; an element holding plain content, which is a hand written menu nobody could get back; and a menu on a site that marked no material, which has nothing to list. Each is reported with the page and the line, and the plain content case names both ways out - empty the element, or keep the markup and drop the attribute.

What a generated page carries

A render of a cheatsheet document, and the material it was read from. The collected blocks become a document of the three models, that document is composed through the output surface, and the composed sheet is embedded at the top of the page - so a media file of the same cheatsheet is another render of the same object rather than a second code path that happens to agree today.

<h1>Acme Reference</h1>
<div class="cheatsheet" role="img" aria-label="Acme Reference cheat sheet">
  <svg id="cheatsheet-site-index" viewBox="0 0 3600 3600">...</svg>
</div>
<main data-cheatsheet-block="section">...the site's own markup...</main>

The markup stays because the reading is lossy and a sheet is a picture of text: a table becomes rows, a chart's <script> contributes nothing, and a <pre> listing loses its indentation. A reader who wants the table rather than a picture of it has nowhere else to get it, so the figure is the reading and the markup underneath is the record. A heading, a caption and a table's header cell keep their role as the comment rows a label is set in; material that reads as no text at all - a page of nothing but an <img> - gets no figure rather than a sheet with an invented row on it.

Every fragment is named, because the master page holds one per cheatsheet and a composition carries id="cheatsheet" on its root. The name is the generated file's stem, which the flat output folder has already made unique, slugged the way the surface slugs a render's file name - a stem is a site owner's own file or folder name, and an id may hold no whitespace.

The browser route, for a site with no build step

The scanner reads files, so a marker a script wrote at run time is invisible to it. api/cheatsheet/site.mjs is the other half, and it needs no checkout, no Node and nothing installed - a page that can run a module can build its own cheatsheet section from the elements it already has:

<script type="module">
  import { cheatsheetSection } from "./api/cheatsheet/index.mjs";

  const { element, findings } = cheatsheetSection(document);
  if (element) document.querySelector("#cheatsheet").append(element);
  for (const finding of findings) console.warn(finding);
</script>

It reads the same two markings, because the attribute names, the heading sources and the element lists are declared there and the scripted tool imports them: one vocabulary with two readers rather than two that agree until one is edited. The section it builds is the shape the scripted tool writes - a heading, the composed figure, and a <main> per block - so a site can move between the routes without its stylesheet changing. The material is cloned rather than moved, and the section is handed back rather than inserted, since where it goes is the page's decision.

Two things it does not do. There is no generated folder, so a data-cheatsheet-menu on the page is reported rather than filled: a menu pointing at pages nobody wrote is worse than no menu. And nothing is written to a file and nothing is fetched - it reaches the document it was handed and the modules beside it, which is what lets it load from a page with no bundler.

Reading a run before trusting it

A run can be read before it is trusted and run twice without moving anything. --dry-run reports the folder it resolved and the pages it would write, and touches nothing. A real run records the name it resolved in cheatsheets/.../cheatsheet-site.json, and a later run reads that rather than resolving a second section beside the first - which is what it would otherwise do, because run one's output is what makes cheatsheets/ taken for run two.

The manifest carries no timestamp, so a second run over an unchanged site writes the same bytes and the generated pages can be committed and diffed. It records its own shape, and a run reads every shape the tool has written: the folder is read off where the file sits rather than out of the file, so an older manifest answers that as well as a current one and is rewritten on the way past.

There is a site to run it against, committed. fixtures/site/ is a small placeholder site - half tutorial, half tool documentation - carrying every page shape the tooling has to handle: prose sections, code examples, a table, and a chart whose script holds a </div> in a string, which is the case that proves a script is an opaque span rather than markup. Read its README for what is in it and what is deliberately not yet.

node tools/cheatsheet-site.mjs fixtures/site --dry-run

Why the collector is a scanner

Having no parser available is what makes the collector a scanner, and the limits of scanning are listed in that file's own header rather than left to be discovered. A start tag is walked rather than searched, so a > inside an attribute value is not read as the end of one: <div title="a>b" data-cheatsheet> is legal HTML, is one element, and is collected as one.

Part of the cheatsheets documentation. The repository is on GitHub, and the example page it documents is here.

Contents