Writing Jekyll Plugins
- Testing Jekyll Plugins â Small Technology Notes from Jan 26, 2022, 6:51am
It took me more than eighteen months and more than four hundred posts, here on Entropy Arbitrage, but I finally backed myself into a corner with the blog.
I should note that, if you want to skip the storytelling and background, and just get to the concepts, Iâll meet you at Jekyll Plugins. If you want to skip the talk about structure and just want a simple example like I wanted when I started, weâll see each other at The Final Code. And if youâre not interested in working with Jekyll, you can probably skip the post without missing anything.
Anyway, Iâve never quite been happy with how quotes looked in posts, so letâs start the story there.
The style did the job, but itâs soâŚbrutal, where just about everything else is relatively smooth. So, I decided to finally change it, using a shadow and some curves, instead of spending week after week saying âyeah, that could be betterâ like Iâve been doing.
âŚAnd that is when I realized that, when converting from a format with minimal semantic structureâlike Markdown, which only headings, quotes, and tablesâto a format with slightly more semantic structure (HTML), we sometimes hack our way into adding semantic structure in less-than-elegant ways. The big example for this blog has been the âpull quotesâ that I (increasingly rarely) use in posts to call out the key ideas of a section. Because I only saw this as pure syntax, I identified pull quotes as nested quotes, creating the styles based on that assumption.
It was always an ugly solution, but it was better than the most obvious alternative, which was to hand-code the HTML for the pull quotes and drop that unreadable mess into the Markdown file. I already do this for some things, like embedding audio and video from off-site, so it wouldnât be completely out of character.
Technical Debt
Because my pull quotes were quotes-inside-of-quotes, trying to change the appearance of quotes made a mess of those. The browser tried to render a quote inside a quote, exactly what I asked it to do, but not what I wanted, and basically useless.
That was annoying, but fine. This is what we call technical debt. My expedient solution saved me time then, with the expectation that the time would need to be âpaid backâ at some point in the future. That future was Monday, and that meant finally learning how Jekyllâthe software that generates this blogâworks, on some level.
If youâll pardon the digression, the term is awkward, but âdebtâ is a great metaphor for so many aspects of life. Deciding to take âshortcutsâ to get something doneâspeeding to get to work on time, saying something unkind to a friend to get out of a conversation, skipping an event where youâre expected, ignoring a health problem, or anything elseâyouâre implicitly accepting that you may need to pay that time back with interest. You might get pulled over immediately. The health condition might need more-aggressive treatment. You might need to work to regain your friendâs trust. And maybe, occasionally, your creditor forgets about you.
Even looking at things on a societal level, we have many problems today that are a result of making an expedient choice in the distant pastâthink of colonialism or slaveryâand often outright refusing to pay that âsocietal technical debtâ whenever the bill comes due again.
Alternatives
Back to styling quotes.
I already knew most of what I needed to know about Jekyll from working with it, really. Before Jekyll converts the Markdown files into HTML to be posted, it expands its own âinline tags,â plus any templates written in a simple language from Shopify called Liquid. Iâve used the templates before, letting me include the word count for a post that doesnât need me to update it or even count, or listing all posts with a certain tag. Iâve also used Jekyllâs inline tags, for example referring to earlier posts by calling post_url
, so that I donât need to know the actual URL for a post.
Reviewing the Liquid documentation, templates werenât going to help me. Instead, I needed a new tagâI call it pull
âwhich will generate the HTML for the pull quote.
Jekyll Plugins
After poking around for a whileâI donât know why this isnât just linked somewhere obvious on the Jekyll websiteâI found enough information to build a Jekyll plugin.
Structure
The bare minimum to know follows.
- We write the plugins in Ruby.
- Plugins sit in the
_plugins
folder. - When serving the âdevelopmentâ version of the blog locally, Jekyll isnât interested in changes to pluginsâthough it definitely recognizes the changes and triggers a rebuildâso its server needs to be restarted to test a new idea.
- A plugin class has two piecesâan
initialize()
method to handle any setup, and arender()
method to return the generated HTMLâand then must get attached to an inline tag name.
And thatâs basically everything, so letâs break down how the new pull quotes work, at least as of yesterday, when I checked everything in.
Initialization
All things considered, this is the boring part.
def initialize(tag_name, text, parse_context)
super
@text = text
end
Like I said, itâs boring. It calls super
, the initialization code that this is otherwise replacing for the class, and stores the input stringâthe text of the pull quote, in this caseâin a global variable for use elsewhere in the object.
Iâm admittedly not sure what parse_context
is, even after poking at it and searching, so Iâm ignoring it until I realize that I need it for something. You might say that Iâm refinancing my technical debt, rather than just paying it. Maybe itâll turn out to be the solution to letting me render Markdown from inside a tag. Or maybe it wonât.
Rendering
Modern HTML has an aside
tag, meant for content that isnât relevant to the main body of the page. That seems like the best bet, here. Not only does that describe most of the semantics of a pull quote, it has also been supported by every major browser since 2015, with most supporting it since 2011. If anybody is still using a browser that old, theyâre doing it on purpose, so Iâm not really concerned about themâŚ
So, for the first pass, our rendering is simple.
def render(context)
return "<aside class=\"pull-quote\">#{@text}</aside>"
end
I take the text and stuff it into an aside
, and the CSS class .pull-quote
gets all my old, broken âblockquote blockquote
â pull quote styling.
The context
parameter appears to hold information about the blog posts. Specifically, context.environments.first['page']
is a hash table that includes previous
/next
elements, the front-matter elements (title, date, and so forth), most other obvious metadata, and a content
element. So, donât try to print it, since itâll try to render the content inside the post, which will recursively work until it crashes the process. In other words, the entire blog is accessible from the plugin, and that includes modifying elements, though I honestly canât imagine many ways to make that useful.
Based on this Stack Overflow answer, it looks like context
can also give me access to the Markdown-to-HTML converter, which would let me convert my pull quotes. Maybe Iâll look at that some other time.
Register
Once the class has been definedâinitialization and renderingâwe finally register the tag name as attached to the code.
Liquid::Template.register_tag('pull', PullInlineTag)
I assume that part is self-explanatory.
Power-Mad
That could be the entire story, but it isnât. One unfortunate problem is that, as I hinted at in the Initialization section, we render the input text
literally. Because of that, I need to clear the various Markdown bits from the quotes. And the times that Iâve used a pull quote in order toâmore technical debt from avoiding semanticsâshove a small image over to the right of the page now just show the Markdown string pointing to the image, so that wonât do.
Easily enough, in the latter case, I can just replace those fake pull quotes with a different tag, that Iâll unimaginatively but cryptically call imgr
, for âimage to the right.â That has the advantage that the dark red border will no longer be associated with those small images.
I wonât include that code, because itâs boring after seeing pull
, except that an image takes multiple parameters, not just a single string. Thereâs the path to the image, the âalt
â text that shows when the browser canât display the image, and the âtitle
â text that shows up as a âtooltipâ when you hover over the image with the mouse. Normally, I set both texts to be the same, out of laziness, but I usually want alt
to be descriptive to the extent that itâs relevant to the postâin case the image isnât displayedâand either the same description or any smart-ass remark that I might occasionally come up with goes in alt
.
However, Liquidâs inline tags do only take one blob of textâunless I missed something importantâso I can just delimit the string with something that isnât likely to be used in a pull quote or image caption, like the pipe (|
) character, and call split '|'
on the string to return the components.
As long as Iâm doing that for imgr
, though, I can do the same thing for pull
, because thereâs one additional problem with the old pull quotes: They always show up to the right of the page. In older posts where I used pull quotes more frequently, the consistency is acceptable, but I can do better, now. In this case, I can add an optional part of the input text that chooses whether to put the pull quote on the left- or right-hand side. So, letâs revisit the rendering code.
def render(context)
text, place = params @text
return "<aside class=\"pull-quote pull-quote-#{place}\">" \
"#{text}</aside>"
end
def params(input)
parts = input.split '|'
return parts[0].strip, parts.length > 1 ? parts[1].strip : 'right'
end
The params()
method separates out the text
to render and the place
, the side to drop the pull quote. In this case, if I donât provide a position, the conditional code will default to 'right'
, which represents the behavior of the old pull quotes. The default saves me the trouble of going back through all the old posts a second time, to specify that everything should go on the right.
I can then hack out the side-dependent parts of my .pull-quote
style, and create .pull-quote-right
with those bits and .pull-quote-left
with their opposites. And with that taken care of, I can now drop pull quotes on either side of the page, whenever I please, so that I can break up the visual layout more carefully. Or I can put them on both sides, in the case of this paragraph, which I already regret trying.
Iâve been driven wild with power, I guess. Mwah-ha-etc.
Anyway, I had to go through my old posts looking for nested quotes, to replace them with either a pull
or imgr
tag, and now I can stop wasting time with the hacked-together pull quotes.
Where Was I?
Oh, right, I did all this so that I can change the style of the normal quotes. And that is now possible, like so.
Iâve been driven wild with power, I guess.
Me, a few paragraphs up
I like the new styleâitâs much clearer that theyâre to be read as offset from the main textâso the work was probably worth it. Itâs not perfect, mind you, but the new look is still better than the old look.
Iâm using a heading for the credit, for example, which is definitely another legitimate semantic taboo, especially since the heading comes after the quote. So, I should probably eventually add a citation as a tag, generating a line with style that matches those headings; HTML has a cite
element that would be ideal for it. I could also add a quote-with-citation tag, but that seems excessive.
The style also somehow interacts badly with the imgr
tag. Maybe the pull quotes and normal quotes always had a conflict, and I just never noticed, because of the stark appearances and similar natures. Conveniently, the situation has only come up when I decided to add illustrations to a Twitter roundup post. Barely anybody ever refers to the old editions but meâI post the links for a reason, after allâas far as I can tell, so itâs not a high priority item to fix. Like I said, Iâm refinancing some of this debt, not paying it off completely.
And itâs not a problem so much as a possible enhancement, but Iâd like to find a way to push the pull quotes further down the height of the paragraph, so that they arenât always sitting at the top. Thereâs probably some obvious CSS to do it, but itâll take some research.
Regardless, notice how the new style for quotes does not affect the style for the pull quotes. They donât have shadows, and the curvature of the box is less. Before pull quotes had their own semantic meaning, the pull quotes were placing empty quote boxes, in addition to overly styled pull quote boxes.
Note that I was tempted to fully unleash the semantic meaning of a pull quote, by structuring the HTML as follows.
<aside>
<blockquote>
The quoted text.
</blockquote>
</aside>
Thatâs what a pull quote semantically is, after all, a quote meant as an aside. However, I quickly realized that was probably going to cause me more conflicts as I work to undo the existing quote styles. I may revisit this, however, if I decide to start using the aside
tag for other purposes.
The Final Code
If you want to play with this idea and donât feel like wandering around the Entropy Arbitrage Code repository to find it, hereâs the final version of the plugin.
class PullInlineTag < Liquid::Tag
def initialize(tag_name, text, parse_context)
super
@text = text
end
def render(context)
text, place = params @text
return "<aside class=\"pull-quote pull-quote-#{place}\">" \
"#{text}</aside>"
end
def params(input)
parts = input.split '|'
return parts[0].strip, parts.length > 1 ? parts[1].strip : 'right'
end
end
Liquid::Template.register_tag('pull', PullInlineTag)
About the only piece that I didnât already mention is that the plugin is a subclass of Liquid::Tag
.
Other Possibilities
Now that I have this process down, I can potentially take a look at other tags that have crossed my mind.
For example, readers might notice that, when I link to Twitter, I add Font Awesomeâs Twitter icon to the link. Likewise, I have started trying to remember to indicate the articleâs license at the other end of a link, if itâs not compatible with the blog. It might be nice to have a warn_url
tag that lets me specify Twitter, non-commercial links, or anything else that might come up in the future, saving me the need to track down the code for the icons every time I need them.
Similarly, Iâve considered adding my âJCâ icon to internal links, though that might be too cluttered. More useful whenever I embed video or a book from the Internet Archive, I need to look up the syntax and rescale everything, whereas a plugin can do that automatically.
It probably doesnât fit with the flow of my posts, where links are just inline, but given that the plugins are written in Ruby, I could easily have one download each given URL to extract a title and preview. Bicker does something like this, though I believe that it relies on a gem. A typical use for this that might fit well with Mondayâs developer diary posts would be a github
and/or gitlab
tag that generates a link and preview, so that I donât need to constantly twist my writing to fit the name of the project into each update.
I donât do anything with data visualization, right now, but if I do in the future, it wouldnât be hard to write a plugin to generate a vector image of a bar, line, or pie graph, given some input points. Likewise, I donât think that Iâd use such a feature, but as the audience slowly grows, generating an HTML form and adding some back-end code elsewhere would make it possible to run polls.
A possible alternative to or expansion of the original pull quote idea comes to mind, too. Itâs not terrible, but the current (and prior) process is a little clumsy, in that I need to find a quote to pull out, copy it into the tag, and remember to update it, if the source paragraph ever changes. It might be possible to wrap the entire paragraph in a Liquid::Block
tag, and add delimiters to the paragraph to mark the text to be pulled out, so that it only gets typed once. On the other hand, that wouldnât allow for editing.
It doesnât come up often, but I occasionally want to post some code that includes a Liquid tag. Unfortunately, Liquid processes first, meaning that, if I want to explain that âI can insert the page title by typing Writing Jekyll Plugins
ââI didnât type the title, thereâI unfortunately get the title, instead of page.title
inside double-braces ({ { ... } }
, minus the spaces); it gets transformed before Markdown does. The problem where the contents donât get processed could be useful, here, by letting the Liquid code pass through.
In other words, the ability to create new tags should make it possible to do pretty much anything, but do it in a way thatâs semantically appropriate and doesnât make the Markdown versions of the post significantly less readable.
This work seems to have created one minor problem, though: Now that Iâve added what I would call âtrivialâ plugins, it has nearly doubled the time that Jekyll takes to process and generate the updated files. Iâll need to see whatâs going on with that and how to speed it up, at some point. Itâs not exactly a disaster, but itâs a nuisance that Iâd rather not deal withâŚ
Anyway, for the handful of people still coming to or working with Jekyll, I hope that this gives you a start on creating plugins. I may pick up some of the ideas I listed and run with them, or this might be plenty for me. But Iâm at least glad that I wonâtâonce I handle the citation tagâtry to fake my way through semantic organization after this.
I should also mentionâsince Iâm talking about the blogâthat Jekyll has been a shockingly good experience, especially once I automated some of the more tedious or intricate tasks away. Every once in a while, it looks like I get the date in the URL wrong, but I have no plans of replacing it with another static site generator or blogging platform anytime in the foreseeable future.
Credits: The header image is If a door know needs a label⌠well its function is not clear by Alan Levine, made available under the terms of the Creative Commons Attribution 2.0 Generic license.
By commenting, you agree to follow the blog's Code of Conduct and that your comment is released under the same license as the rest of the blog. Or do you not like comments sections? Continue the conversation in the #entropy-arbitrage chatroom on Matrix…
Tags: programming blog techtips ruby