Genaver, pronounced JEN-a-ver, is short for GENerate A VERsion. (It is not a variation of the names Jennifer or Guinevere, nor is it a reference to the drink genever, which is pronounced "jeh-NEE-ver" anyway.)
But what does it generate a version of? The originally intended use-case is to generate versions of a presentation done in reveal.js or some such similar system. It could also be used to generate a version of any arbitrary HTML file, such as a children's story where you want to substitute in the child's name. Please let me know if you use it for anything other than presentations. (For that matter, feel free to let me know if you use it at all!)
I do presentations at conferences, often reusing the same presentation for different conferences. Not only might the conferences have different timeslot lengths, but also these are usually software development conferences, sometimes focusing on a particular programming language. So, I might want to use code examples in Elixir at one conference, Ruby at another, and so on, all to illustrate the same concept in the same presentation. Furthermore, they are in many different countries, speaking many different languages, and I usuallly do my introduction in the local language. If that's not English, I include slides with the local version written, and "subtitles" in English.
Previously I had been doing my presentations in Keynote (the Mac equivalent of PowerPoint), and creating a new file for each conference. Just like PowerPoint, Keynote produces fairly large binary files, making it difficult to compare two versions, and cumbersome to combine, say, the Spanish intro from one with the Elixir code of another, to make a version that fits in a different length than either. It also made it difficult to hunt down some turn of phrase that I had used a while back, but had to remove because the next version would be shorter.
So, I wanted to use a textual format (such as, but not limited to, reveal.js), and devise some way to maintain just one "primary" file, containing all the introductions, code versions, and details I could include. The idea was that then I could devise a system whereby I could somehow generate from that a version containing only the "universal" parts, the one specific desired introduction and set of code, and not the bits of verbiage somehow "tagged" as "only include this if I've got at least X minutes to fill", where X is greater than some limit I would give the system.
Initially I thought I would have to write a fairly complex system to parse that primary file and some form of tagging. Later it occurred to me that I could also commit a gross abuse of the C preprocessor. I was discussing this idea with someone, who said he had been having great success in similar things with a certain text processing library from Python. He was using that because he wanted to step away from using JavaScript to process text.
That was my lightbulb moment! Duh! I've already got the presentation in HTML, and manipulating presentation of HTML is exactly what JavaScript was originally intended for, is the overwhelmingly common tool for, and is actually very good at! So, rather than continue pursuing the idea of creating a system that would generate a new file that would contain only the desired items, I decided to try writing some JavaScript that would hide the elements that are not desired.
He and I discussed that a bit further, but he didn't seem to quite understand what I meant or why it would suit my purposes better. So, I decided that rather than writing further prose to explain, it was time to try coding up a Proof of Concept, including a demo HTML file calling it. That turned out to be rather easy, with about 20 lines of JS, even with some redundancy. So I cleaned up the code a bit, started thinking about tests (coming eventually!) and more features, wrote up this README, and here we are!
(Later I also discovered that
reveal.js works by changing the display
property,
thus clobbering the hiding that Genaver does,
so now it actually removes parts.)
Before you can make it transform your presentation (or whatever), your JS engine has to know about it. So, before the call to it, you must include the line:
<script src="genaver.js"></script>
(adjusting the URL if needed, for wherever you're getting it from).
After that, ideally at the bottom of your HTML file, you set your options and include a line like:
<script type="text/javascript">generate_version(options);</script>
(where options
may be a variable or a literal).
Options are how you tell Genaver exactly what you want it to show.
There are two types of options: filters and constants.
Each option is a string,
consisting of a tag, a comparator, and a value,
optionally separated by any number of spaces.
The tags can only use
letters (though they may be upper or lower case),
digits,
and hyphens.
For filters, the tags must not start with var-
,
as that is how to designate a constant option.
They also should not start with not-if
,
as that is used to invert string match filtering
(as explained later).
For filters, the comparator can be =
, !=
, <
, <=
, >
, or >=
,
while for constants, it must be =
.
The value may consist of any characters.
There are two ways to pass options to Genaver: directly via the JavaScript call, and in the URL.
Filtering was the original use-case, for things like "make a version with the intro in Portuguese and the code in Elixir, fitting in 30 minutes". I expect that this will be the vast majority of the usage.
There are two types of values.
Which type the value is ass-u-me'd to be,
depends on the comparator used.
For =
or !=
,
that implies that the value is a string,
while for <
, <=
, >
, or >=
,
that implies that it is a number.
Strings will look for an exact literal string match with the value, currently case-sensitively. (The tags are case-insensitive, due to how HTML works.) Numbers will be interpreted as floating point, even though I expect the vast majority of use, perhaps even all of it, to be integers.
No other comparators are currently allowed,
though I have had the idea to
make the current string comparators case-insensitive,
and tack on an extra =
to make it sensitive.
Genaver operates on HTML attributes,
using the data-
convention.
So, for instance,
to designate some element
(be it a div, span, section, or whatever)
as being in, say, Portuguese,
you can add the attribute
data-human-lang="portuguese"
.
If you tell Genaver the option human-lang=portuguese
,
then any elements tagged with, for instance,
data-human-lang="japanese"
will be hidden,
while those with
data-human-lang="portuguese"
will remain to be shown.
(Unless of course they're hidden for some other reason!)
Similarly, to designate bits of code as being in Elixir,
you can tag them with
data-code-lang="elixir"
,
and have them be the ones shown, by using
code-lang=elixir
.
Conversely, you can tag elements with
data-not-if-$KEY="$VALUE"
,
such as data-not-if-human-language="portuguese"
,
to hide elements if the value does match what was given,
such as if the human-language is Portuguese.
To give a concrete example:
I give presentations in many different countries,
and always do a brief introduction.
If the local language is not English,
I do the intro in the local language,
with slides for each few words,
with the words in the local language above,
a relevant picture in the middle,
and an English translation below.
So, each of those slide elements
is tagged data-not-if-language="english"
.
So, if I tell Genaver that this presentation is in English,
those slides are not shown.
Constants are an additional feature,
used for things like
building up the PDF slides URL
from the conference abbreviation and year,
such as constructing
www.my-slidestorage.tld/ThisTalk-Whatever-2034.pdf
from the conference abbreviation "Whatever" and the year "2034".
With this sort of mechanism,
you don't need to change that URL
nor construct it from
a horrible mess of conditional values.
For constants,
the tag must start with var-
and the comparator must be =
.
The value may contain any kind of characters,
but will be interpreted as text, not HTML.
In the presentation HTML,
any element tagged with
the attribute data-gv-var
,
and having the value match the rest of the tag,
will have its InnerText
set to the corresponding value.
For instance,
if you tell Genaver
var-conf-name = WhateverConf
,
then any element with the attribute data-gv-var="conf-name"
will have its InnerText
set to WhateverConf.
You can pass Genaver options in JS as a list of strings. For instance,
['human-lang = french',
'comp-lang != ruby']
tells Genaver that you want only the elements that
either have no data-human-lang
attribute
(so they're "universal"),
or that specify data-human-lang="french"
,
and
do not specify data-comp-lang="ruby"
.
Anything tagged, for instance,
data-human-lang="japanese"
will be hidden,
and elements tagged, for instance, data-comp-lang="python"
will be shown (unless hidden for other reasons).
To specify these options in JS,
use at the bottom of your file
a script
element as shown above.
You may either set the options
variable to some value, like so:
const options = [
'human-lang = japanese',
'not-human-lang= english',
'prog-lang = ruby',
'min-time <= 30',
'max-time >= 30',
'var-city = Kyoto',
'var-conf = Something',
'var-year = 2022'
];
or use such a literal in place of the variable. Note that each option may have zero, one, or many spaces between the comparator and the other pieces.
Options specified in the URL have the exact same syntax (including that spaces may be included or not), and will override choices passed in the JS. Simply put them after the URL like you would any other URL params, AND tack gv- onto the front. That is, add a ? to the URL, then all the options separated with ampersands (&s), with gv- on the front of each one. For instance:
my-talk.html?gv-human-lang=french&gv-prog-lang=elixir&gv-min_time<40&gv-var-city=Paris
(It is perfectly okay that there is no =
in min_time<40
.
When there is one,
Genaver reconstructs the spec from the two halves the browser hands it,
and then subjects it to
exactly the same parsing as for JS-passed strings.)
Long story short, Genaver looks for elements with the relevant attribute. For each one, it looks at the value's relation to the desired value. (Any element that does not even have the attribute is left alone.) From there it depends if it is filtering or using a constant.
If the relationship of the element's attribute value, to the value desired, is the inverse of the relationship desired, then the element is hidden. In other words, if you wanted equality (whether string or numeric) and you found inequality, or vice-versa, then it's hidden. If you wanted it numerically equal or greater, and you found something less, then it's hidden, and similarly for other opposing pairs of comparison.
When you want inequality, it's rather simple. Genaver can simply look up all the elements with the undesired value for that attribute, and remove them. For anything else, it looks up those that have the desired attribute, loops through them, does the desired comparison, and removes the element if the comparison is false.
Here again it's rather simple.
Again, since we know the exact value we want,
it can scoop them up in one query and set their InnerText
.