Using Antora with Prism.js
Using Antora with the Prism.js syntax highlighter
- Why use the Antora website builder?
- Antora
- Antora installation
- Installing the Antora Collector Extension
- Setting the project directory as a Git repository
- Creating a working directory structure for the project
- Starting files for the project
- Navigation files
- Main files that include the prebuilt HTML files
- Directory structure for module1 and module2 so far
- Prebuilding the HTML files
- Setting up Antora to use prebuilt HTML files
- Fixing up the process.
- Modifying the Antora header
- Putting in a table of contents
- Adjusting the width of the main content section
- Making headings look like Asciidoctor Headings
- Adjusting the spacing for pages that include a prebuilt HTML file
- Embedding images
- Scaling images on hover
- Removing Edit this Page from links on top
- GitHub repository
Why use the Antora website builder?
When I began documenting things that I learned about plain vanilla JavaScript, I did this because my experience showed me that documenting things was the best way for me to learn. I encourage you to consider doing the same type of thing. By documenting what you learn, you are forced to think about how you might explain what you learned to others. This often leads to a more fundamental understanding of the concepts you are dealing with. In addition, documenting what you are learning gives you a good way to start up again on some project that got interrupted by other things that became a higher priority. I found it was much easier to get back to a project, if I had some documentation, than if I had to start over from scratch.
Once I decided that I would try to share some of the things I learned, I thought about using a website builder that would make the intrasite navigation easier to use, and easier to maintain. For most of the courses I taught, I had stuck with navigation that had all the internal site links at the top of the page. This limited the amount of internal links and was not useful for displaying sub links. In addition, that style of navigation would involve more maintenance when you add and edit links. I did some searches on the Web for some website builders. I came up with three possible choices: Antora, Hugo and Jekyl. I decided to go with Antora as it is based on using Asciidoc documents and Antora is noted to be useful for documentation pages. I had been making simple web standalone web pages for my documentation with Asciidoc more recently (after I retired), so this also made Antora my choice.
Antora
Antora has extensive documentation online and a good way to get using it is by starting with this Antora Docs - Quickstart. Eventually, you will want to dig into more of their extensive documentation at this site.
Using Prism.js with Asciidoctor
Once I began to use Antora, I found that I did not care for the built-in syntax highlighter for code, highlight.js. I have used the Prism.js source highlighter for many years, and I particularly liked the support Prism.js has for line numbering and line highlighting. I found those features useful when talking about what the code is supposed to do. To use Prism.js, I would need to create a custom syntax highlighter adapter. Such an adapter is shown in the Asciidoctor documentation and can be found at this page. I was able to modify this example to get Prism.js to work with Asciidoctor. However, Antora is JavaScript based, and defaults to using Asciidoctor.js.
I attempted to modify my Visual Studio (VS) Code configuration so that the Ruby version of Asciidoctor would be used instead of the JavaScript version. But, this only applies to using the VS Code editor. So, while I did modify my settings.json file for VS Code by adding the following, this just removed warnings shown in that editor.
{
"asciidoc.preview.asciidoctorAttributes": {
"use_asciidoctor_js": false,
"source-highlighter": "highlight.js"
}
}
This did not result in Antora using the Ruby version of Asciidoctor. I then rewrote the custom syntax highlighter adapter in JavaScript instead of Ruby. Unfortunately, this did not work. My guess as to why this did not work is that Antora has a lot of Node package dependencies. Somehow, based on the versions of those dependencies that are pulled in when installing Antora, there are conflicts. In particular, I kept getting error messages related to there being more than one version of the Opal package. So, this would cause errors stating that Opal.queue is undefined.
At that point, I decided to try to make use of the Antora Collector Extension, as the documentation for Antora implied that this would allow the use of prebuilt HTML files. The idea to use this was to prebuild the HTML files using the Ruby version of Asciidoctor. Then, the prebuilt HTML files could be copied into the appropriate directories so that Antora could include those files. In that way, the syntax highlighting performed by Prism.js would still be intact. What follows are the main steps to carry out this process. I will also include some of the things that I had to figure out along the way to develop the details of this process. I include the workarounds so that you can see my thought process in solving the problems I encountered.
Antora installation
Let’s start by going over the installation of Antora. These instructions come from the Antora Docs - Quickstart guide.
Make sure that you have a fairly up to date version of Node.js and npm installed. If you are on a Debian-based Linux system, instructions to install Node.js and npm can be found here.
The recommendation at Antora’s site is to install Antora locally. So, I will create a directory specifically for a demonstration project.
Making a project directory
For the purposes of this guide, I will refer to the project directory by this path /path/to/demo_process_site. I chose this name as this project is to demonstrate the process of using Antora with Prism.js. So, if you were trying to follow along on your own computer, substitute using your own absolute path. For example, if you were doing this on a Linux computer and decided to make your project directory /home/username/Documents/demo_process_site, then in your mind subsitute /home/username/Documents/ for /path/to/.
Here are the steps I followed to make the project directory and install Antora within that project directory.
$ mkdir /path/to/demo_process_site
$ cd /path/to/demo_process_site
node -e "fs.writeFileSync('package.json', '{}')"
$ npm install -D -E antora
Line 1 creates the project directory. Line 2 changes into that project directory. Line 3 will create an empty package.json file. Line 4 installs antora, with the -D meaning that antora will be a development dependency (i.e. only used in a build process). The -E means to use an exact version of antora. That will keep you locked into that version of antora for your project.
If you displayed your package.json file at this point, it would look like this:
{
"devDependencies": {
"antora": "3.1.10"
}
}
Installing the Antora Collector Extension
Since we will also be using the Antora Collector Extension (see Antora Collector Extension latest Documentation), let’s install that as well.
$ cd /path/to/test_website
$ npm install @antora/collector-extension
If you looked at package.json at this point, you would see the following:
{
"devDependencies": {
"antora": "3.1.10"
},
"dependencies": {
"@antora/collector-extension": "^1.0.1"
}
}
Setting the project directory as a Git repository
Antora expects your website to be built from a Git repository. You can simply have your Git repository local, and that is what I will demonstrate here.
In case you don’t have git installed, here is how it is installed on a Debian-based Linux machine:
$ sudo apt install git
Once git has been installed, you can do the following:
$ cd /path/to/test_website
$ git init
$ git config --global user.email "yourusername"
$ git config --global user.name "yourname"
$ git commit --allow-empty -m init
If you already had git installed, you can skip steps 3 and 4, as those only need to be run once for your system.
Setting up a .gitignore file
In the project directory you can create a .gitignore file which lets git know which files and directories to leave untracked. So, a good simple start to this file would be:
/build/
/node_modules
Creating a working directory structure for the project
I will start with a minimal amount of directories within my project directory. Then, I will add more directories as dictated by making the project work or work better. So, the main subdirectory for the project directory will be the modules subdirectory. The modules subdirectory can contain several directories. The modules directory will be the directory that houses all of the pages of your website. For the purpose of this example, I will use three subdirectories inside my modules directory. I will have a directory called ROOT for the top level content, and directories called module1 and module2 for pages associated with for example modules in a course. The ROOT directory name must be in all capital letters. The other directories should start with a letter and use lower-case letters. Here is a start to creating the directory structure.
$ cd /path/to/demo_process_site
$ mkdir -p modules/ROOT
$ mkdir -p modules/module1
$ mkdir -p modules/module2
Within each of these directories, there should be a pages directory. Those directories will contain Asciidoc documents that are used for the pages of your website. Let’s create those pages directories.
$ cd /path/to/demo_process_site
$ mkdir -p modules/ROOT/pages
$ mkdir -p modules/module1/pages
$ mkdir -p modules/module2/pages
Suppose you want to display only the top level directories in a given directory. You could use the following command. Note, that this is run from the parent directory from my project directory.
$ tree demo_process_site -d -L 1
demo_process_site
├── modules
└── node_modules
The -d means to display directories only (no files). The -L 1 means to only show up to level 1 directories.
Let’s look at the tree including up to level 2 directories.
$ tree demo_process_site -d -L 2
demo_process_site
├── modules
│ ├── module1
│ ├── module2
│ └── ROOT
└── node_modules
├── abort-controller
├── @antora
├── antora
├── argparse
├── @asciidoctor
├── asciidoctor-opal-runtime
├── async-lock
├── atomic-sleep
├── available-typed-arrays
├── b4a
├── balanced-match
├── bare-events
├── base64-js
├── brace-expansion
├── braces
├── buffer
├── buffer-crc32
├── cache-directory
├── call-bind
├── call-bind-apply-helpers
├── call-bound
├── clean-git-ref
├── clone
├── colorette
├── commander
├── concat-map
├── convict
├── crc-32
├── dateformat
├── decompress-response
├── define-data-property
├── diff3
├── dunder-proto
├── end-of-stream
├── es-define-property
├── es-errors
├── es-object-atoms
├── events
├── event-target-shim
├── fast-copy
├── fast-fifo
├── fast-glob
├── fastq
├── fast-redact
├── fast-safe-stringify
├── fill-range
├── for-each
├── fs.realpath
├── function-bind
├── get-intrinsic
├── get-proto
├── glob
├── glob-parent
├── gopd
├── handlebars
├── hasown
├── has-property-descriptors
├── has-symbols
├── has-tostringtag
├── help-me
├── hpagent
├── @iarna
├── ieee754
├── ignore
├── inflight
├── inherits
├── isarray
├── is-callable
├── is-extglob
├── is-glob
├── is-number
├── isomorphic-git
├── is-typed-array
├── joycon
├── json5
├── js-yaml
├── lodash.clonedeep
├── math-intrinsics
├── merge2
├── micromatch
├── mime-db
├── mime-types
├── mimic-response
├── minimatch
├── minimist
├── minimisted
├── multi-progress
├── neo-async
├── @nodelib
├── once
├── on-exit-leak-free
├── pako
├── path-is-absolute
├── pend
├── picomatch
├── pify
├── pino
├── pino-abstract-transport
├── pino-pretty
├── pino-std-serializers
├── possible-typed-array-names
├── process
├── process-warning
├── progress
├── pump
├── queue-microtask
├── quick-format-unescaped
├── readable-stream
├── real-require
├── remove-trailing-separator
├── replace-ext
├── require-from-string
├── reusify
├── run-parallel
├── safe-buffer
├── safe-stable-stringify
├── secure-json-parse
├── set-function-length
├── sha.js
├── should-proxy
├── simple-concat
├── simple-get
├── sonic-boom
├── source-map
├── split2
├── streamx
├── string_decoder
├── strip-json-comments
├── teex
├── text-decoder
├── thread-stream
├── to-buffer
├── to-regex-range
├── typed-array-buffer
├── uglify-js
├── unxhr
├── util-deprecate
├── vinyl
├── which-typed-array
├── wordwrap
├── wrappy
├── xdg-basedir
├── yargs-parser
├── yauzl
└── yazl
Quite a mess. This is because antora has so many dependencies that the number of directories inside node_modules is quite large. We can fix this by ignoring directories with the tree command.
$ tree demo_process_site -d -L 2 -I "node_modules"
demo_process_site
└── modules
├── module1
├── module2
└── ROOT
The -I "dir_pattern" will cause tree to Ignore any directories that match that pattern. So, we can view all the directories inside modules directory in the following manner:
$ tree demo_process_site -d -L 3 -I "node_modules"
demo_process_site
└── modules
├── module1
│ └── pages
├── module2
│ └── pages
└── ROOT
└── pages
The module1 and module2 directories will be holding pages that are not at the top level. These directories will be holding the main content for the website. Since these are pages that will include source code, I want the pages to use prebuilt HTML, so that I can make use of the Prism.js syntax highlighter. So, I will need several additional directories (besides pages) inside of both module1 and module2. The next set of commands will create those additional directories,.
$ cd /path/to/demo_process_worksite
$ cd modules/module1
$ mkdir examples images prebuilt
$ cd ../module2
$ mkdir examples images prebuilt
To show the new directories, I use the tree command again from the parent directory of my demo_process_site project directory.
$ tree demo_process_site/ -d -L 3 -I "node_modules"
demo_process_site/
└── modules
├── module1
│ ├── examples
│ ├── images
│ ├── pages
│ └── prebuilt
├── module2
│ ├── examples
│ ├── images
│ ├── pages
│ └── prebuilt
└── ROOT
└── pages
So, if you have more than two modules, you would need the additional modules to have the same subdirectories as module1 or module2. The directory names modules, examples, images and pages are predefined directory names that have special meanings in Antora. The directory name ROOT is a special predefined directory name that means that it contains top level content (like a landing page)
Other names like module1, module2 and prebuilt are not predefined directory names. I just chose those names because they suited the kind of contents that was going to be placed in them. When you choose your own directory names, start with a letter, use lower-case letters and don’t use special characters.
Predefined directory names
The name modules must always be used to house the pages that form the contents of your website. The examples directory will be used to hold copies of prebuilt HTML files that will be included from pages in the pages directory. The images directory will contain subdirectories for all the pages in the prebuilt directory. The images in those subdirectories are used to build the HTML files that will reside in the prebuilt directory. I found that the simplest way to use images without worrying too much about links to the images, was to make use of embedded images.
Starting files for the project
There are several files that I used to start off the project. For module1 and module2, I will place some files inside the prebuilt subdirectory. Those will be Asciidoc (.adoc) files that will be used to create the prebuilt HTML files. I also created a file index.adoc inside ROOT/pages, as this will become the landing page, or home page. To support the files in the prebuilt directories, I will also create subdirectories inside the corresponding images directory. I will create these files and subdirectories with the following commands:
$ cd /path/to/demo_process_site
$ touch modules/ROOT/pages/index.adoc
$ touch modules/module1/prebuilt/for_loops_pre.adoc
$ touch modules/module1/prebuilt/while_loops_pre.adoc
$ touch modules/module2/prebuilt/for_loops_pre.adoc
$ touch modules/module2/prebuilt/while_loops_pre.adoc
$ mkdir modules/module1/images/for_loops
$ mkdir modules/module1/images/while_loops
$ mkdir modules/module2/images/for_loops
$ mkdir modules/module2/images/while_loops
Line 2 creates index.adoc, which will be the landing page. Lines 3 and 4 create files that will become pages for module1. Lines 5 and 6 will create files that will become pages for module2. Lines 7 and 8 will created subdirectories for the images associated with the for_loop_pre.adoc file for module1 and the while_loop_pre.adoc file for module1, respectively. Lines 9 and 10 do the same thing for the module2 directory.
After running these commands, this is what my directory structure looks like when running tree from the parent directory of my project directory:
$ tree demo_process_site -L 4 -I "node_modules|package*.json"
demo_process_site
└── modules
├── module1
│ ├── examples
│ ├── images
│ │ ├── for_loops
│ │ └── while_loops
│ ├── pages
│ └── prebuilt
│ ├── for_loops_pre.adoc
│ └── while_loops_pre.adoc
├── module2
│ ├── examples
│ ├── images
│ │ ├── for_loops
│ │ └── while_loops
│ ├── pages
│ └── prebuilt
│ ├── for_loops_pre.adoc
│ └── while_loops_pre.adoc
└── ROOT
└── pages
└── index.adoc
Note that I no longer use the -d switch so files are displayed also. I also show files/directories out to level 4. Although it may not be obvious at this point, I use the tree command so that if you choose to create the files and directories in a different way than the command line commands I used, you can check if your file/directory structure is correct.
Files for module1
Here is the source code for the file for_loops_pre.adoc that goes in the module1 directory
= For loops in Python
[.normal]
== Simple for loops in Python
Here are some examples of some simple for loops written in Python. One of the most common uses of for loops, is to iterate over some array-like object. In Python, such an object would be a *list*. Here is a very simple example of such a for loop iterating over a list of strings.
=== For each style of for loop
[source,python,linenums]
----
mylist = ['apple', 'banana', 'cherry', 'grapes', 'peach']
for fruit in mylist:
print(fruit)
----
When run at the console, this would be the output:
[source,console]
apple
banana
cherry
grapes
peach
This style of for loop is often called a *for each* style, as it is basically saying:
For each fruit in mylist
print fruit
=== Index-based for loop
Python also has a for loop style that is similar to index-based for loops common to many other programming languages. Here is an example using the index-based style.
[source,python,linenums]
----
mylist = ['apple', 'banana', 'cherry', 'grapes', 'peach']
for i in range(0, len(mylist)):
print(mylist[i])
----
Here *i* is an index that is used to select a single element from the list. The *range()* function goes from the first argument (0) up until but not including the size of the list (len(mylist) returns the size of the list). Although the index-based for loop is more work to implement, it is also more powerful as it is well suited to handling different elements in the list in a different manner. The *for each* style of for loop is best suited for cases where you want to treat each element in the same way.
=== An accumulator example
For loops are often used in the *accumulator pattern*. This pattern is useful for calculating things like totals and averages of a list. Suppose we only want to get the average of numbers in a list, where the number is greater than 35. Here is an example of how you can do this:
[source,python,linenums]
----
nums = [30,40,50,40,50,30]
total = 0
count = 0
for num in nums:
if (num > 35):
total += num
count += 1
average = total/count
print("average:", average)
----
Here, *total* and *count* are called *accumulator* variables. That is because they are used to accumulate a set of results. The *accumulator* pattern is marked by setting an accumulator variable to 0 before a for loop. Then inside the for loop the value of that accumulator is updated by adding the next value. In this particular example, we also accumulate the *count*, because the average should only be based on the numbers that are greater than 35. So, you could not get the correct average by dividing by the size of *nums*.
The result of running this program would be:
[source,console]
average: 45.0
Here is the source code for the file while_loops_pre.adoc for the module1 directory:
= While loops in Python
[.normal]
== Simple while loops in Python
For loops are often used to iterate over array-like objects like lists. So, the number of repeats (number of times the for loop runs), is set by the size of the list. While loops are repetition statements that will repeat a variable number of times, depending on the actual run time events. Here is an example of a while loop that will run until the user decides to end the input:
[source,python,linenums]
----
names = []
done = False
while (not done):
name = input("Enter a name or 'done' if done: ")
name = name.strip().capitalize()
if (name == 'Done'):
done = True
else:
names.append(name)
print("names:", names)
----
The while loop will run until the test condition (*not done*) is no longer true. So, when the variable *done* becomes True, the while loop will end. Note that we use a selection statement on lines 16-19 to handle the input name. We use *if* with an *else* clause so that the *names* list does not include the name "Done". Here is a sample run of this program:
[source,console]
Enter a name or 'done' if done: aaron
Enter a name or 'done' if done: Bob
Enter a name or 'done' if done: Joe
Enter a name or 'done' if done: Heather
Enter a name or 'done' if done: done
names: ['Aaron', 'Bob', 'Joe', 'Heather']
Note that the *capitalize()* function in Python capitalizes just the first letter of the word it is operating on.
[sidebar]
Python did not have Boolean variables at first. So, like the *C* programming language, you could use any non-zero number for true and zero for false. You could use things like empty lists for false too. But, in version 2.3, Python added true Boolean variables. They are: *True* and *False*. Note that these variables must start with the capital letters as shown.
=== Using break
Here is a program that does essentially the same thing as the previous program. But, it makes use of the *break* statement. The *break* statement is used to immediately terminate the loop that directly contains the *break* statement. This is the code:
[source,python,linenums]
----
names = []
while (True):
name = input("Enter a name or leave blank when done: ")
name = name.strip().capitalize()
if (name == ""):
break
else:
names.append(name)
print("names:", names)
----
Here on line 2, the while loop's test condition is always True, so this loop will run indefinitely. So, on line 5 and 6, we test to see if the user has entered a blank name and if so, the *break* statement on line 6 ends the while loop. If you run the program, you will see the same kind of output as the previous program:
[source,console]
Enter a name or leave blank when done: alice
Enter a name or leave blank when done: Douglas
Enter a name or leave blank when done: rambo
Enter a name or leave blank when done:
names: ['Alice', 'Douglas', 'Rambo']
Here is the source code for the file for_loops_pre.adoc in the module2 directory:
= For loops in JavaScript
[.normal]
== Simple for loops in JavaScript
Here are some examples of some simple for loops in JavaScript. As with other programming languages, for loops in JavaScript are useful for iterating through arrays or array-like objects. Here are some exaamples.
=== JavaScript's for each style of for loop
Here is a simple example of a "*for each*" style for loop in JavaScript:
[source,javascript,linenums]
----
let nums = [1, 3, 8, 12, 15];
for (let num of nums) {
console.log(num);
}
----
The *for (let item of array)* pattern is the JavaScript way of doing a *for each* style for loop. Take special not of the keyword *of*. This is how you iterate over all the objects in the array.
[sidebar]
In JavaScript, there is also a *for (let key in object)* pattern. Note the use of *in*, instead of *of*. This pattern is used to iterate over all the *keys* (properties) of an object. This is different from the *for (let item of array)* pattern which iterates over all the *values* in the array.
If you run the above program, this will produce the following output:
[source,console]
1
3
8
12
15
=== Index-based for loop in JavaScript
As with most other programming languages, JavaScript has an index-based style of for loop too. Here is an example:
[source,javascript,linenums]
----
let nums = [1, 3, 8, 12, 15];
for (let i = 0; i < nums.length; i++) {
console.log(nums[i]);
}
----
The index-based for loop, makes it easier to treat different elements in the array differently, but is a little more work to set up than the "*for each*" style. The output produced in this case is:
[source,console]
1
3
8
12
15
Let's rewrite the code, to only print out the elements of the array which have an odd index.
[source,javascript,linenums]
----
let nums = [1, 3, 8, 12, 15];
for (let i = 0; i < nums.length; i++) {
if ((i%2) == 1) {
console.log(nums[i]);
}
}
----
On lines 4-6, we have a selection statement that only prints out the element if the index (*i*) is an odd number. So, the output when this program is run would be:
[source,console]
3
12
That is because the index is odd for those elements: +
nums[1] is 3 +
nums[3] is 12
=== Iterating over keys of an object
Here is an example of using a for loop to iterate over all the keys of an object:
[source,javascript,linenums]
----
let student = {}
student.name = 'Jane Doe';
student.major = 'ICS';
student.gpa = 3.8;
console.log('Student Jane Doe:');
for (let key in student) {
console.log(`${key}: ${student[key]}`);
}
----
Lines 1-4 create an object named *student* and define some properties for that *student*. Line 6 prints a little header. The for loop on lines 7-9 iterate over all the keys in *student* and display the *key* and the key's *value*. Here is the output from running this program.
[source,console]
Student Jane Doe:
name: Jane Doe
major: ICS
gpa: 3.8
Here is the source code for the file while_loops_pre.adoc in the module2 directory:
= While loops in JavaScript
[.normal]
== Simple while loops in JavaScript
Here are some simple examples of while loops in JavaScript. Like while loops in other programming languages, while loops in JavaScript are repetition statements that repeat until some condition is met. The number of repetitions for a while loop depend on the code that the while loop is running within. So, they are useful when you don't know how many repetitions will be needed while writing the code.
=== While loops that run until something is used up
Here is program that uses while loops that would work irrespective of the size of the arrays that are being used:
[source,javascript,linenums]
----
let names = ['Alice', 'Bob', 'Carol']; // assume the contents is determined by something else
let names2 = [];
while (names.length > 0) {
names2.push(names.pop());
}
while (names2.length > 0) {
console.log(names2.shift());
}
----
On line 1, we hard-coded the *names* array. But, assume that we don't know what the *names* array looks like. The while loop on lines 4-6 will work for any array called *names*. What it does, is fill an array called *names2* that will have the same elements as *names* but in reverse order. The while loop on lines 8-10, will just print out the contents of *names2*. As you can see from the output below, the order of the elements is reversed from the original hard-coded array.
[source,console]
Carol
Bob
Alice
=== Making use of an iterator
While loops can make use of an iterator. In this next program, we will use the *makeIterator()* function that will create an iterator out of the passed array. An iterator is an object that will return the next element in the object by calling the *next()* function. That *next()* function will return false, when all the elements have been processed.
[source,javascript,linenums]
----
function makeIterator(array) {
let index = 0;
return {
next: function() {
if (index < array.length) {
return array[index++];
}
else {
return false;
}
}
}
}
const myArray = ['Alice', 'Carol', 'David'];
const myIterator = makeIterator(myArray);
let num;
while ((num = myIterator.next())) {
console.log(num);
}
----
Lines 1-14 define the *makeIterator()* function. This creates an *iterator* from the passed array. On line 2, the variable *index* is declared and set to 0. When *index* is incremented (on line 7), this makes it so that the selection statement that starts on line 6 can control the number of times that *next()* will return a value from the array. When the array has been exhausted, the *next()* function will return false (line 10).
Line 16 defines a hard-coded array, called *myArray*. Line 17 calls the *makeIterator()* function to create the iterator called *myIterator*. Line 58 defines the variable, *num*, to hold the value returned by *myIterator.next()*. Lines 18-20 define the while loop that will run, as long as the statement *num = myIterator.next()* is true. Line 19 just prints out the value from the array. The output from running this program is:
[source,console]
Alice
Carol
David
[sidebar]
The *for (let item of array)* pattern uses a built-in iterator similar to this iterator to process each item of the array, in the order of the items of the array. Hopefully, you can see that this pattern makes it simpler to iterate over an array, as opposed to having to define your own iterator.
Here is the source code for the file index.adoc that is in the ROOT directory.
= Home page
[.normal]
== Click on links on left to navigate site
Navigation files
Since we have two modules (module1 and module2), we will use two navigation files. These should provide navigation links to all the files inside of the respective modules.
Here is the navigation file for module1
* xref:module1:for_loops1.adoc[For Loops]
* xref:module1:while_loops1.adoc[While Loops]
The first line (starting with a dot (.)) is a text title that will go above the links. The second and third line are the links to the files that will be inside the pages (/path/to/demo_process_site/modules/module1/pages) directory. Note that these files (which have not yet been created) will not have the _pre as part of the filename and will simply include the prebuilt files (that do contain _pre as part of the filename).
Here is the navigation file for module2
* xref:module2:for_loops2.adoc[For Loops]
* xref:module2:while_loops2.adoc[While Loops]
Main files that include the prebuilt HTML files
Now, let’s create the main files that will go into the respective pages directories, that will simply include the prebuilt HTML files. We need a corresponding file for each of the HTML files in the prebuilt directories. The following shows the contents of those files:
= For loops in Python
\++++
include::example$/for_loops1_pre.html[]
\++++
= While loops in Python
\++++
include::example$/while_loops1_pre.html[]
\++++
= For loops in JavaScript
\++++
include::example$/for_loops2_pre.html[]
\++++
= While loops in JavaScript
\++++
include::example$/while_loops2_pre.html[]
\++++
Directory structure for module1 and module2 so far
Now would be a good time to look at the file/directory structure for module1 and module2
$ cd /path/to/demo_process_site
$ tree modules -P "*.adoc|*.html" -I "node_modules" --prune
modules
├── module1
│ ├── nav1.adoc
│ ├── pages
│ │ ├── for_loops.adoc
│ │ └── while_loops.adoc
│ └── prebuilt
│ ├── for_loops_pre.adoc
│ └── while_loops_pre.adoc
├── module2
│ ├── nav2.adoc
│ ├── pages
│ │ ├── for_loops.adoc
│ │ └── while_loops.adoc
│ └── prebuilt
│ ├── for_loops_pre.adoc
│ └── while_loops_pre.adoc
└── ROOT
└── pages
└── index.adoc
Note that there is a navigation file for each module, and that for both modules, there is a corresponding file in the pages directory with each file in the prebuilt directory.
Prebuilding the HTML files
This is where we will use asciidoctor and the Prism.js syntax highlighter to prebuild the HTML files. I started with this file, downloaded from this Asciidoctor Docs - Custom syntax highlighter:
class PrismSyntaxHighlighter < Asciidoctor::SyntaxHighlighter::Base
register_for 'prism'
def format node, lang, opts
opts[:transform] = proc do |pre, code|
code['class'] = %(language-#{lang}) if lang
end
super
end
def docinfo? location
location == :footer
end
def docinfo location, doc, opts
base_url = doc.attr 'prismdir', %(#{opts[:cdn_base_url]}/prism/1.15.0)
slash = opts[:self_closing_tag_slash]
unless (theme_name = doc.attr 'prism-style', 'prism') == 'prism'
theme_name = %(prism-#{theme_name})
end
%(<link rel="stylesheet" href="#{base_url}/themes/#{theme_name}.min.css"#{slash}>
<script src="#{base_url}/prism.min.js"></script>
<script src="#{base_url}/components/prism-ruby.min.js"></script>)
end
end
This provided a good starting point for using Prism.js. But it did not support line numbers, line highlighting and changing the start of the line numbers. So, I had to add some code to get those things to work. After all, the main reason for using Prism.js was to add support for those things. It took me a lot of trial and error to make the following changes. I may add a page explaining how I went about doing that, as it could be an interesting topic for those of you who are interested in how the Ruby programming language deals with properties of objects. But, to save time and space here, this is my final code:
class PrismSyntaxHighlighter < Asciidoctor::SyntaxHighlighter::Base
register_for 'prism'
def format node, lang, opts
opts[:transform] = proc do |pre, code|
code['class'] = %(language-#{lang}) if lang
attributes = node.attributes
if attributes.key?('linenums')
pre['class'] += ' line-numbers'
end
if attributes.key?('data-line')
pre['data-line'] = %(#{attributes['data-line']})
end
if attributes.key?('data-start')
pre['data-start'] = %(#{attributes['data-start']})
end
end
super
end
def docinfo? location
location == :footer
end
def docinfo location, doc, opts
base_url = doc.attr 'prismdir', %(#{opts[:cdn_base_url]}/prism/1.15.0)
slash = opts[:self_closing_tag_slash]
unless (theme_name = doc.attr 'prism-style', 'prism') == 'prism'
theme_name = %(prism-#{theme_name})
end
end
end
The added lines are lines 8-11, 13-15 and 17-19. I also removed lines between lines 34 and 35, as these did not seem to affect the output.
Prebuilding the HTML files.
Now, that I have the custom syntax highlighter created, I worked on the steps needed to prebuild the HTML files using asciidoctor and this custom syntax highlighter. Initially, the CSS files for asciidoctor.css and for prism.css will be placed in the prebuilt directories. These files will have to be placed in a different location once we start the antora build. The same thing applies to the JavaScript file, prism.js.
Asciidoctor makes use of what they call docinfo files. This is a way to place CSS and JavaScript file links into the built HTML file. Here is the Asciidoctor Docs - docinfo documentation that provides details on how to make use of these files. Just be aware, that those files will need to be modified when antora is used to complete the build process. For now, I will set these files up, so that they allow me to test the prebuilding stage.
Creating a docinfo_files directory
I created a docinfo_files directory right inside the /path/to/demo_process_site directory. The docinfo_files directory will contain two files: docinfo.html and docinfo-footer.html. Asciidoctor will insert the contents of docinfo.html into the <head> section of the prebuilt HTML file. Asciidoctor will insert the contents of docinfo-footer.html into the <div id="footer"> section of the prebuilt HTML file. So, the starting contents for these files will be:
<link rel="stylesheet" href="./prism.css">
<script src="./prism.js">
At this point, this assumes that prism.css and prism.js are located in the prebuilt directories.
Downloading the Prism.js files
I went to this site to set up and download the JavaScript and CSS files for Prism.js. I selected the following programming languages and plugins:
Prism
Themes:
Default
Languages:
Markup (html, xml, svg)
CSS
C-like
JavaScript
AsciiDoc
C, C#, C++
Java
JSON + Web App Manifest
MongoDB
PHP
Python
React JSX
Ruby
Shell session
SQL
TypeScript
XML doc
YAML
Plugins:
Line Highlight
Line Numbers
Remove initial line feed
Normalize Whitespace
Match braces
Of course, you should adjust the choices according to your needs. I copied prism.css into both of my prebuilt directories in the modules, and I also copied prism.js into those prebuilt directories as well.
Modifying the header of the _pre.adoc files
To make use of Prism.js and the docinfo files, I needed to change the header section of all of the _pre.adoc files. So, for example, this is how the file for_loops_pre.adoc in the module1 directory was changed.
= For loops in Python
:source-highlighter: prism
:docinfo: shared-head, shared-footer
:docinfodir: /path/to/demo_process_site/docinfo_files
[.normal]
== Simple for loops in Python
Here are some examples of some simple for loops written in Python. One of the most common uses of for loops, is to iterate over some array-like object. In Python, such an object would be a *list*. Here is a very simple example of such a for loop iterating over a list of strings.
=== For each style of for loop
// rest of file is unchanged
Lines 2, 3 and 4 are the new lines. The rest of the document is unchanged. Line 2 will specify that prism will be the source-highligther. Line 3 specifies that a docinfo.html file will be used for the shared <head> sections of all the files built in this way and that docinfo-footer.html will be used for the <div id="footer"> sections of those prebuilt files. Line 4, specifies the full path to the directory that contains the docinfo.html and docinfo-footer.html files. Make sure that you supply the full path starting from the systems root directory (/).
Those same three lines were added to the other _pre.adoc files in the prebuilt directories. To show that this is formatting the source correctly, here is a screen shot showing for_loops_pre.html in the modules1/prebuilt directory:
As you can see, the line numbers are showing up as they should.
Asciidoctor command to prebuild HTML files
Here are the asciidoctor commands to prebuild the HTML files
$ cd /path/to/demo_process_site
$ asciidoctor -r prism-syntax-highlighter.rb -a linkcss -a source-highlighter=prism modules/module1/prebuilt/*.adoc
$ asciidoctor -r prism-syntax-highlighter.rb -a linkcss -a source-highlighter=prism modules/module2/prebuilt/*.adoc
The -r CLI option is to require a library. In this case, this is the Ruby script that defines the custom source highlighter. The -a linkcss will create the file asciidoctor.css in the output directory, instead of adding all of those styles into internal CSS contents inside the <head> section of the prebuilt HTML. This simplifies the HTML file and makes it easier to see the HTML markup. The -a source-highlighter=prism attribute sets the source-highlighter to prism.
Here is a tree view showing the files inside of modules after running those asciidoctor commands:
$ cd /path/to/demo_process_site
$ tree modules -P "*.html|*.adoc|*.css|*.js" -I node_modules --prune
modules
├── module1
│ ├── nav1.adoc
│ ├── pages
│ │ ├── for_loops.adoc
│ │ └── while_loops.adoc
│ └── prebuilt
│ ├── asciidoctor.css
│ ├── for_loops_pre.adoc
│ ├── for_loops_pre.html
│ ├── prism.css
│ ├── prism.js
│ ├── while_loops_pre.adoc
│ └── while_loops_pre.html
├── module2
│ ├── nav2.adoc
│ ├── pages
│ │ ├── for_loops.adoc
│ │ └── while_loops.adoc
│ └── prebuilt
│ ├── asciidoctor.css
│ ├── for_loops_pre.adoc
│ ├── for_loops_pre.html
│ ├── prism.css
│ ├── prism.js
│ ├── while_loops_pre.adoc
│ └── while_loops_pre.html
└── ROOT
└── pages
└── index.adoc
So, each prebuilt directory contains prism.css and prism.js, as well as .html files for each .adoc file inside of the module1 and module2 directories.
Setting up Antora to use prebuilt HTML files
The Antora Collector Extension was something I installed earlier. In case you didn’t install it, this is the set of commands needed:
$ cd /path/to/demo_process_site
$ npm install @antora/collector-extension
Configuring Antora with antora.yml and antora-playbook.yml
Basic instructions on setting up the antora.yml file can be found here: What’s antora.yml.
Here is the contents of the antora.yml file:
name: demo-process-site
version: ~
title: Demonstration of Antora with Prism
nav:
- modules/module1/nav1.adoc
- modules/module2/nav2.adoc
ext:
collector:
clean:
dir: build
scan:
- dir: build/generated
file: '**/*.adoc'
- dir: ./modules/module1/prebuilt/
into: ./modules/module1/examples/
- dir: ./modules/module2/prebuilt/
into: ./modules/modle2/examples/
On line 1, the name is specified. The name is used at the start of the URL, so it should not contain any spaces or special characters. By convention, using all lower-case letters is recommended.
On line 2, we set the version as ~. This means that this component is unversioned and so a version number will not be part of the URL.
Line 3 specifies the title which will also be used for the site specification in the antora-playbook.yml file. This should be the string that is the description of your website.
Lines 4-6 define the navigation structure for the site. Lines 5 and 6 refer to the navigation files we created earlier. By placing the link to nav1.adoc above the link to nav2.adoc, the links in those files will be displayed in that order. That is, the module1 pages will be displayed above the module2 pages.
Lines 8-18 are added to help run the Antora Collector Extension. The collector property under the ext (extension) is used to configure what the collector process will entail. So on lines 10 and 11, the process starts off by cleaning the build directory. Lines 12-18 configure the scan process. On line 15-16 this makes the files in modules/module1/prebuilt available in modules/module1/examples. Lines 17-18 makes the files in modules/module2/prebuilt available in modules/module2/examples. We have to manually copy the files to make this happen, and that will be part of the make_all.sh script that will control the entire process. That file will be shown after the antora-playbook.yml configuration file shown next:
Information on setting up the antora-playbook.yml file can be found at: Antora Docs - Setup Playbook
site:
title: Demonstration of Antora with Prism
start_page: demo-process-site::index.adoc
content:
sources:
- url: .
branches: HEAD
ui:
bundle:
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
snapshot: true
antora:
extensions:
- '@antora/collector-extension'
Lines 1-3 set up the site property. Line 2 shows the title as defined in the antora.yml file. Line 3 sets the landing page (home page) for the site. Note that this starts with the name of the site followed by the name of the landing page (index.adoc).
Lines 5-8 set up the content property. The sources property of the content has a url value of . (current directory). This is relative to the location of antora-playbook.yml. Since antora-playbook.yml is located in the project directory (/path/to/demo_process_site), the current directory is the main project directory. Since this is a Git repository, line 8 specifies that the Git version should be the latest version or HEAD.
Lines 10-13 set up the ui property. This is the property that configures the user interface that the website is using. Lines 11-12 make it so that the Antora default UI will be used for this sites user interface. On line 13, the snapshot value of true means that Antora will download the UI bundle whenever fetch is activated in the playbook, or fetch is activated from the CLI when running the antora command.
Lines 15-17 set the Antora Collector Extension to be one of the extensions called by Antora. Later, we will add another extension that can edit already built HTML files. That will be shown later on.
Creating a build script called make_all.sh
Since there are several steps involved in our overall build process, I created a simple shell script that executes these steps in the proper order. Here is the file called make_all.sh that I used:
#!/bin/bash
asciidoctor -r prism-syntax-highlighter.rb -a linkcss -a source-highlighter=prism modules/module1/prebuilt/*.adoc
asciidoctor -r prism-syntax-highlighter.rb -a linkcss -a source-highlighter=prism modules/module2/prebuilt/*.adoc
cp modules/module1/prebuilt/*.html modules/module1/examples/
cp modules/module2/prebuilt/*.html modules/module2/examples/
npx antora --log-level=debug antora-playbook.yml --stacktrace
Lines 3 and 4 call asciidoctor to prebuild the .adoc files into HTML files. Lines 6 and 7 copy those prebuilt HTML files into the corresponding examples directories. Finally, line 9 runs antora. The --log-level=debug will display debugging messages that can be useful in case there are errors in running antora. The playbook file is specified as antora-playbook.yml. The --stacktrace will display a stack trace in case errors are encountered running antora. Although the debug and stacktrace are not completely necessary, they are a good idea and can be very helpful when trying to figure out how to get some part of the process of running antora to work.
Fixing up the process.
Although the build succeeds, the pages are not displayed correctly. Here is an example screen shot of the file for_loops.adoc in module1:
As you can see, the navigational links on the left are working okay, but the source code is not highlighted correctly. By viewing the Page Source (CTRL-U in the browser), you can see that the file has incorrect links to prism.css and asciidoctor.css. The file is also has the wrong URL for the src attribute in the <script> element for prism.js. If you think about it, this makes sense as none of those three files exists in the build.
Getting the CSS and JavaScript files into the build
We need to "supplement" our UI with some additional files, namely prism.css, asciidoctor.css and prism.js. We start by modifying antora-playbook.yml to include a supplemental_files property that has a value set to a new directory that I will call supplemental-ui.
So, here is the new version of antora-playbook.yml:
site:
title: Demonstration of Antora with Prism
start_page: demo-process-site::index.adoc
content:
sources:
- url: .
branches: HEAD
ui:
bundle:
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
snapshot: true
supplemental_files: ./supplemental-ui
antora:
extensions:
- '@antora/collector-extension'
The new line, line 14, specifies that a directory called supplemental-ui inside the main project directory will contain the additional CSS and JavaScript files that we need to be copied into the build. The supplemental-ui directory will contain several subdirectories. The two subdirectories we will start with are called css and js. We copy prism.css and asciidoctor.css into the css subdirectory and copy prism.js into the js subdirectory.
The following commands use tree to show what this looks like:
$ cd /path/to/demo_process_site
$ tree -P "*.css|*.js" -I "node_modules|build|test_files" --prune
.
├── modules
│ ├── module1
│ │ └── prebuilt
│ │ ├── asciidoctor.css
│ │ ├── prism.css
│ │ └── prism.js
│ └── module2
│ └── prebuilt
│ ├── asciidoctor.css
│ ├── prism.css
│ └── prism.js
└── supplemental-ui
├── css
│ ├── asciidoctor.css
│ └── prism.css
└── js
└── prism.js
Note that I had to ignore the folder "test_files" because I put some test source code there to help me to create some of the pages for the the modules.
Now, I can run make_all.sh again. Using some console commands including tree you can see that antora has copied the CSS and JavaScript files into the build directory.
$ cd /path/to/demo_process_site
$ tree build -P "*.css|*.js|*.html" --prune
build
└── site
├── _
│ ├── css
│ │ ├── asciidoctor.css
│ │ ├── prism.css
│ │ └── site.css
│ └── js
│ ├── prism.js
│ ├── site.js
│ └── vendor
│ └── highlight.js
├── demo-process-site
│ ├── index.html
│ ├── module1
│ │ ├── for_loops.html
│ │ └── while_loops.html
│ └── module2
│ ├── for_loops.html
│ └── while_loops.html
└── index.html
So, you can see that inside of *build/site* that there is a subdirectory named *_* that has the subdirectories *css* and *js* that contain the files copied in because they are in the *supplemental-ui* subdirectories. This *tree* view also shows us that the correct path to a file like *prism.css* should be:
<link rel="stylesheet" href="../../_/css/prism.css">
fixing the docinfo.html and docinfo-footer.html files
So, docinfo.html and docinfo-footer.html needs to be edited to make use of the new path that works for the antora build.
<link rel="stylesheet" href="../../_/css/prism.css">
<script src="../../_/js/prism.js"></script>
With those changes, we can run make_all.sh again. Now, some of the formatting is correct. Here is a screen shot showing the for_loops.adoc from module1:
We can see the syntax highlighting, but it looks like the line numbers are missing. In the browser, you can right-click on the part near where the line numbers should show up and select Inspect in the drop down menu. This will show the following in the Elements tab:
You can see that for a <span> element with class="line-numbers-rows" that this has the line numbers, but something is covering it up. If you noticed, the link to asciidoctor.css has not yet been fixed. So, this means that something in site.css (from the Antora UI) is causing the problem.
So, I opened up the file /path/to/demo_process_site/build/site/_/css/site.css in the VS Code editor. I installed the CSS Formatter extension by Martin Aeschlimann to format the CSS file to make it easier to edit. Since I did not know specifically which lines were the source of the problem, I modified line 2555 so it was no longer a block comment and then commented out the bottom half of the CSS file (from line 1221 on). After doing this and saving site.css, I noticed that the line numbers became visible. So, I knew the lines causing the problem were between line 1221 and the end of the file. The last line of the file is 2731, so the halfway point between 1221 and 2731 would be about 1976. So, I commented out from line 1221 to 1977 and saved site.css again. The line numbers were still visible, so now the lines of code that cause the problem are between lines 1221 and 1997. The halfway point would be about 1609. So, I tried commenting out between lines 1221 and 1610. This made the line number disappear, so that means the range to test is from line 1610 to 1997.
The halfway point for that range is about 1803, so I tested the range 1610 to 1802. This turned out to be the correct range as the line numbers showed up for this range. The halfway point for this new range is about 1706. I tested commenting out between line 1610 and line 1708, but this caused the line numbers to disappear again. So, the lines must be between line 1708 and 1802. The halfway point for that is about 1755. So, I commented out between 1708 and 1752. That made the line numbers covered up, so the new range was 1752 to 1802. The halfway point for that is about 1777. So, I commented out between line 1752 and line 1767. Luckily, this turned out to be the range where the problem lies. This narrowed things down considerably.
As can be seen from the screen shot below, the actual lines turned out to be line 1760 and line 1761:
making changes to site.css permanent
Since site.css is generated by the antora process, we need to copy the edited site.css file into the directory /path/to/demo_process_site/supplemental-ui/css. Then that copy of site.css will be copied back into the build.
Now, running make_all.sh will show the source code with line numbers.
Modifying the Antora header
Now that I have Prism.js working, I could start to modify the general appearance of the web pages. Antora places a header at the top of each web page that looks like this:
Note how the header contains Home, Products, Services and a Download button. I want to remove those parts of the header. To do this, I started by going to GitLab Antora UI default and getting the link to clone this repository. Then I ran the following commands:
$ cd /path/to/demo_process_site
$ git clone https://gitlab.com/antora/antora-ui-default.git
This creates the directory antora-ui-default inside my main project folder. I used the following commands including tree to find the .hbs (handlebars) file that controls the header contents.
$ cd /path/to/demo_process_site
$ tree antora-ui-default/src -P "*.hbs" --prune
antora-ui-default/src
├── layouts
│ ├── 404.hbs
│ └── default.hbs
└── partials
├── article-404.hbs
├── article.hbs
├── body.hbs
├── breadcrumbs.hbs
├── edit-this-page.hbs
├── footer-content.hbs
├── footer.hbs
├── footer-scripts.hbs
├── header-content.hbs
├── header.hbs
├── header-scripts.hbs
├── head.hbs
├── head-icons.hbs
├── head-info.hbs
├── head-meta.hbs
├── head-prelude.hbs
├── head-scripts.hbs
├── head-styles.hbs
├── head-title.hbs
├── main.hbs
├── nav-explore.hbs
├── nav.hbs
├── nav-menu.hbs
├── nav-toggle.hbs
├── nav-tree.hbs
├── page-versions.hbs
├── pagination.hbs
├── toc.hbs
└── toolbar.hbs
The file that I am looking for is /path/to/demo_process_site/antora-ui-default/src/partials/header-content.hbs. I want to edit this file. I do this by making a new subdirectory inside /path/to/demo_process_site/supplemental-ui called partials. I then copy the header-content.hbs into that partials directory.
By placing this copy of header-content.hbs into supplemental-ui/partials, this will add this edited file into the build. So, we are making use of the supplemental-ui directory again. Here is the contents of the header-content.hbs file before any changes:
<header class="header">
<nav class="navbar">
<div class="navbar-brand">
<a class="navbar-item" href="{{{or site.url siteRootPath}}}">{{site.title}}</a>
{{#if env.SITE_SEARCH_PROVIDER}}
<div class="navbar-item search hide-for-print">
<div id="search-field" class="field">
<input id="search-input" type="text" placeholder="Search the docs"{{#if page.home}} autofocus{{/if}}>
</div>
</div>
{{/if}}
<button class="navbar-burger" aria-controls="topbar-nav" aria-expanded="false" aria-label="Toggle main menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
<div id="topbar-nav" class="navbar-menu">
<div class="navbar-end">
<a class="navbar-item" href="#">Home</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Products</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="#">Product A</a>
<a class="navbar-item" href="#">Product B</a>
<a class="navbar-item" href="#">Product C</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Services</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="#">Service A</a>
<a class="navbar-item" href="#">Service B</a>
<a class="navbar-item" href="#">Service C</a>
</div>
</div>
<div class="navbar-item">
<span class="control">
<a class="button is-primary" href="#">Download</a>
</span>
</div>
</div>
</div>
</nav>
</header>
If you look, the lines adding those elements that I want to remove are on lines 20-35 and lines 37-41. So, I will comment out those lines (rather than delete them) using block comments for handlebars files.
Here is the modified file:
<header class="header">
<nav class="navbar">
<div class="navbar-brand">
<a class="navbar-item" href="{{{or site.url siteRootPath}}}">{{site.title}}</a>
{{#if env.SITE_SEARCH_PROVIDER}}
<div class="navbar-item search hide-for-print">
<div id="search-field" class="field">
<input id="search-input" type="text" placeholder="Search the docs"{{#if page.home}} autofocus{{/if}}>
</div>
</div>
{{/if}}
<button class="navbar-burger" aria-controls="topbar-nav" aria-expanded="false" aria-label="Toggle main menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
<div id="topbar-nav" class="navbar-menu">
<div class="navbar-end">
{{!-- <a class="navbar-item" href="#">Home</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Products</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="#">Product A</a>
<a class="navbar-item" href="#">Product B</a>
<a class="navbar-item" href="#">Product C</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link" href="#">Services</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="#">Service A</a>
<a class="navbar-item" href="#">Service B</a>
<a class="navbar-item" href="#">Service C</a>
</div> --}}
</div>
{{!-- <div class="navbar-item">
<span class="control">
<a class="button is-primary" href="#">Download</a>
</span>
</div> --}}
</div>
</div>
</nav>
</header>
If you rebuild the site using make_all.sh, the header has been modified:
Putting in a table of contents
If you noticed on the pages on my site, there is a right sidebar that shows the table of contents for a given page. This is a very useful feature for navigation within that page. For long pages, this is actually essential. So, the next thing I tried to add in was this table of contents feature. By default Antora uses such a table of contents. But, this feature is lost because we are prebuilding the HTML files. So, we will use asciidoctor to put the table of contents in. Then, we will need to manipulate the already built HTML files, so that we can make use of the table of contents that asciidoctor creates.
You may have already noticed that the asciidoctor.css file is not referenced properly by the HTML files in build/site. The link for asciidoctor.css is actually this:
<link rel="stylesheet" href="./asciidoctor.css">
This is placed into the prebuilt HTML files by asciidoctor when creating the prebuilt HTML files. I will try to solve both of these problems at the same time, because they can both be fixed by post-processing the prebuilt HTML files in build/site. This kind of post-processing is something that can be done with an Antora extension.
I started with the example here: Antora Docs - Antora Extensions - Report unlisted pages example. The example there shows how to use the contentCatalog to manipulate pages that are in the website. In addition, I looked at this reference: Antora Docs - Antora Extensions - Generate report of all pages for the events that are triggered once all the pages have actually been built.
To add to the JavaScript code outlined in the first link in the previous paragraph, I made use of two Node.js packages: cheerio and fs. Here is some documentation on cheerio. Here is the fs library documentation. To install cheerio and fs, you can run the following commands:
$ cd /path/to/demo_process_site
$ npm install cheerio
$ npm install fs
Here is a start to the JavaScript function that we will use to post-process the HTML files that are already built.
"use_strict";
function register() {
this.once('sitePublished', ({ contentCatalog }) => {
contentCatalog.getComponents().forEach(({ versions }) => {
versions.forEach(({ name: component, version, url }) => {
const pages = contentCatalog
.findBy({ component, version, family: 'page' });
for (const page of pages) {
console.log(`path: ${page.out.path}`);
}
});
});
});
}
module.exports = { register };
Lines 3-16 define the register() function. The register() function is exported on line 18. Lines 5-14 will wait for the sitePublished event and run the unnamed function that is passed the contentCatalog. Line 6 will get all the components of the contentCatalog and for each version (we only have one version) will obtain pages on line 7 which is a reference to all the pages for the version. Then, line 9-11 define a for loop that will process each page of pages. Line 10 simply displays the text 'path: ' followed by the value of the variable page.out.path.
To run this extension, we need to modify the antora-playbook.yml file. Here is the modified version of antora-playbook.yml:
site:
title: Demonstration of Antora with Prism
start_page: demo-process-site::index.adoc
content:
sources:
- url: .
branches: HEAD
ui:
bundle:
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
snapshot: true
supplemental_files: ./supplemental-ui
antora:
extensions:
- '@antora/collector-extension'
- ./post-process-html.js
Line 19 is the new line, and the line simply runs the script post-process-html.js. If we run make_all.sh, this is the output on the console:
$ ./make_all.sh
Site generation complete!
Open file:///home/vern/Documents/demo_process_site/build/site/index.html in a browser to view your site.
path: demo-process-site/index.html
path: demo-process-site/module1/for_loops1.html
path: demo-process-site/module1/while_loops1.html
path: demo-process-site/module2/for_loops.html
path: demo-process-site/module2/while_loops.html
As you can see, all of our built HTML files have been found. These are the files that we want to edit.
Modifying the asciidoctor command
We need to modify the call of asciidoctor so that asciidoctor generates a table of contents. So, this is the new version of make_all.sh that I used:
#!/bin/bash
asciidoctor -r ./prism-syntax-highlighter.rb -a toc=right -a linkcss -a source-highlighter=prism modules/module1/prebuilt/*.adoc
asciidoctor -r ./prism-syntax-highlighter.rb -a toc=right -a linkcss -a source-highlighter=prism modules/module2/prebuilt/*.adoc
cp modules/module1/prebuilt/*.html modules/module1/examples/
cp modules/module2/prebuilt/*.html modules/module2/examples/
npx antora --log-level=debug antora-playbook.yml --stacktrace
Lines 3 and 4 have been modified by adding the additional attribute -a toc=right. This is supposed to build a table of contents on the right. If we just run make_all.sh, this is a screen shot of what one of the pages looks like in a browser:
The table of contents has been added, but it is on the top of the page, not in the right sidebar. By looking at the page source of some pages generated by asciidoctor, and then opening /path/to/demo_process_site/build/site/demo-process-site/module1/for_loops1.html, I was able to find the following things shown in the next screen shots.
Line 10 should have been this:
<body class="article toc2 toc-right">
And, the link to asciidoctor.css on line 106 shown next, is wrong:
The correct href for the link should be as shown next:
<link rel="stylesheet" href="../../_/css/asciidoctor.css">
So, what we want to do is modify post-process-html.js so that it uses cheerio and fs to make these changes programmatically. Here is the new version of the code for post-process-html.js:
"use_strict";
const cheerio = require('cheerio');
const fs = require('fs');
function register() {
this.once('sitePublished', ({ contentCatalog }) => {
contentCatalog.getComponents().forEach(({ versions }) => {
versions.forEach(({ name: component, version, url }) => {
const pages = contentCatalog
.findBy({ component, version, family: 'page' });
for (const page of pages) {
//console.log(`path: ${page.out.path}`);
let path = './build/site/' + page.out.path;
const $ = cheerio.load(fs.readFileSync(path));
let body = $(`body[class="article"]`);
body.attr('class', 'article toc2 toc-right');
let link = $(`link[href="./asciidoctor.css"]`);
link.attr('href', '../../_/css/asciidoctor.css');
fs.writeFile(path, $.html(), function(err) {
if (err) {
throw err;
}
console.log(`${path} saved`);
});
}
});
});
});
}
module.exports = { register };
Lines 3 and 4 allow the use of cheerio and fs for this function. Line 13 comments out the console.log() statement that just printed out all the pages that were part of the contentCatalog. Line 14 creates the variable path that is the actual path to the files inside the build. Line 15 uses fs.readFileSync(path) to read in the file and then loads this file into cheerio for processing. Line 15 also assigns $ as the reference to the HTML document loaded into cheerio. Line 16 uses $ to select any <body> elements with the class="article" attribute. Line 17 replaces the value of class with 'article toc2 toc-right', as this is needed to put the table of contents into the right sidebar. Line 18 uses $ to select any <link> elements with href="./asciidoctor.css". Line 19 replaces the href value with '../../_/css/asciidoctor.css', the correct path to this CSS file in the build.
Lines 20-25 attempts to overwrite the file pointed to by path with $.html(). That is the contents of the HTML DOM that cheerio has just modified. If there is no error, line 24 will print out the name of the file that has been saved.
Here is what the console looks like when make_all.sh is run:
$ cd /path/to/demo_process_site
$ ./make_all.sh
Site generation complete!
Open file:///home/vern/Documents/demo_process_site/build/site/index.html in a browser to view your site.
./build/site/demo-process-site/module2/while_loops.html saved
./build/site/demo-process-site/index.html saved
./build/site/demo-process-site/module1/for_loops1.html saved
./build/site/demo-process-site/module1/while_loops1.html saved
./build/site/demo-process-site/module2/for_loops.html saved
Since, there are no errors, the HTML files were post-processed. Here is a screenshot of for_loops1.html (the for loops in Python page):
Adjusting the width of the main content section
If you look at the following screen shot of for_loop1.html, you can see that there is unused space on the right side of the main content section:
To fix this, I edited the site.css file found in build/site//css. The following screen shot shows the changes I made to /path/to/demo_process_site/build/site//css/site.css:
As you can see, I increased the values of max-width by 14rem. The original values are shown commented out to the right of the new values. Here is a screen shot of for_loops1.html:
So, the main content section can use more of the available window width. Anytime you make changes to a CSS file in the build, you need to copy the file to /path/to/demo_process_site/supplemental-ui/css. That way when you rebuild the pages, the edited copy will be used.
Making headings look like Asciidoctor Headings
It turns out that site.css (from Antora) overwrites some other things that affect the appearance of the headings on the web pages. The headings use a higher font-weight and are all colored black. So, I edited /path/to/demo_process_site/build/site/_/css/site.css again. Here is a screen shot that shows the changes I made:
So, lines 954 and 955 are commented out to prevent black color for all headings, and the font-weight is also commented out.
Here line 1002 is commented out to prevent the heavier font-weight.
I saved the site.css file and made a copy of it for the /path/to/demo_process_site/supplemental-ui/css folder.
Here is what for_loops1.html looks like when rebuilt:
Adjusting the spacing for pages that include a prebuilt HTML file
If you look at the pages as they are now, you can see two main titles at the top. One is from the prebuilt HTML that is included, and one is from the page that is doing the include. Here is what the for_loops1.html looks like right now:
Note also that the top title is partly obscured. There may be better fixes than this, but I opted for a simple change to the site.css file in build/site/_/css. The following screen shot shows the change I made:
As you can see, line 969 changed the margin-top to -2.5rem instead of +2.5rem. After making this change, copying the edited CSS file to /path/to/demo_process_site/supplemental-ui/css, here is *for_loops1.html" in the browser with that change:
Embedding images
To include the images without having to change the image link for the prebuilt files versus the files in build/site, I opted to embed the images. This is done by adding to the header section of the Asciidoc pages:
As an example, here is for_loops1_pre.adoc with the modified header and some images.
= For loops in Python
:source-highlighter: prism
:docinfo: shared-head, shared-footer
:docinfodir: /home/vern/Documents/demo_process_site/docinfo_files
:data-uri:
[.normal]
== Simple for loops in Python
Here are some examples of some simple for loops written in Python. One of the most common uses of for loops, is to iterate over some array-like object. In Python, such an object would be a *list*. Here is a very simple example of such a for loop iterating over a list of strings.
=== For each style of for loop
[source,python,linenums]
----
mylist = ['apple', 'banana', 'cherry', 'grape', 'peach']
for fruit in mylist:
print(fruit)
----
When run at the console, this would be the output:
[source,console]
apple
banana
cherry
grape
peach
Here is a diagram representing *mylist* that shows the indices and values for that list:
[.thumbnail]
image:../images/for_loops1/mylist.png[]
Note that the index for each element is an integer that starts at zero and increases by one. So, to access 'apple', you could refer to this as:
This is only a partial listing, to save space. Line 5 is the additional line in the header that will make the images embedded. Line 34 declares the image as a thumbnail (to use a border) and line 35 shows how to link to an image. The path is the correct one for the /path/to/demo_process_site/modules/module1/prebuilt folder as there is an images folder with a folder called for_loops1. Here is what the directory structure looks like:
$ cd /path/to/demo_process_site
$ tree -P "*.png|*.adoc|*.html" -I "node_modules|build|antora-ui-default" --prune
.
├── docinfo_files
│ ├── docinfo-footer.html
│ └── docinfo.html
└── modules
├── module1
│ ├── examples
│ │ ├── for_loops1_pre.html
│ │ └── while_loops1_pre.html
│ ├── images
│ │ ├── for_loops1
│ │ │ └── mylist.png
│ │ └── while_loops1
│ │ └── capitalize.png
│ ├── nav1.adoc
│ ├── pages
│ │ ├── for_loops1.adoc
│ │ └── while_loops1.adoc
│ └── prebuilt
│ ├── for_loops1_pre.adoc
│ ├── for_loops1_pre.html
│ ├── while_loops1_pre.adoc
│ └── while_loops1_pre.html
├── module2
│ ├── examples
│ │ ├── for_loops_pre.html
│ │ └── while_loops_pre.html
│ ├── images
│ │ ├── for_loops2
│ │ │ ├── nums.png
│ │ │ └── student.json.png
│ │ └── while_loops2
│ │ ├── iterator2.png
│ │ ├── iterator.png
│ │ ├── pop1.png
│ │ ├── pop2.png
│ │ ├── pop3.png
│ │ └── shift.png
│ ├── nav2.adoc
│ ├── pages
│ │ ├── for_loops.adoc
│ │ └── while_loops.adoc
│ └── prebuilt
│ ├── for_loops_pre.adoc
│ ├── for_loops_pre.html
│ ├── while_loops_pre.adoc
│ └── while_loops_pre.html
└── ROOT
└── pages
└── index.adoc
The key thing is that the relative paths for the prebuild is all I need to worry about, because the images will be embedded for the final build.
Scaling images on hover
Some of the images have details that are hard to see, depending on the zoom level of the browser. So, I dealt with this by making changes to /path/to/demo_process_site/build/site/_/css/site.css. The following screen shot shows these changes.
The lines 2732-2737 are added to the end of the site.css file, to automatically scale the image by 25% if the mouse is hovered over the image. As stated before, I had to copy the /path/to/demo_process_site/build/site/_/css/site.css to /path/to/demo_process_site/supplemental-ui/css to make the changes permanent.
Removing Edit this Page from links on top
One other thing that I removed was the Edit this Page link from the links on the top of this page. Here is a screen shot showing the link I am talking about.
To do this, you can copy the file /path/to/demo_process_site/antora-ui-default/src/partials/edit-this-page.hbs into the /path/to/demo_process_site/supplemental-ui/partials. Then, edit the file inside the supplemental-ui/partials location. In this case we want to comment out the entire contents of this file using {{!-- comment --}}.
{{!--
{{#if (and page.fileUri (not env.CI))}}
<div class="edit-this-page"><a href="{{page.fileUri}}">Edit this Page</a></div>
{{else if (and page.editUrl (or env.FORCE_SHOW_EDIT_PAGE_LINK (not page.origin.private)))}}
<div class="edit-this-page"><a href="{{page.editUrl}}">Edit this Page</a></div>
{{/if}}
--}}
If you rebuild the site using make_all.sh, you will see that the resulting pages no longer have the Edit this Page link.
GitHub repository
If you want to get access to the file associated with this guide, here is the link to the GitHub repository for this guide.