Programmer. (Many Interests: Philosophy & Mathematics)

Home

Extending jekyll markdown syntax using Liquid

Introduction

If you've ever used tools like Mediawiki or Tiddlywiki or Gollum you probably are familiar with the nifty little markup syntax to specify links i.e., of the form [​[​Link to some file]​]​, but it can be a pain in the backside to accomplish the same with the markdown syntax without making changes to the markdown parser that you are using. Although I am pretty comfortable writing in markdown now after having adopted jekyll as my go-to SSG for my personal website, I was a little troubled with having to type out the much verbose alternative i.e. [Link to some file](Url of the file) offered by markdown. And since I have no idea how to change the kramdown engine—the markdown parser that I am currently using for parsing my markdown files in jekyll—I put out this question in search for a plugin, and luckily got a neat solution by Joost van der Schee using just a hundred odd lines of liquid code (including comments).

The code below is a little different from what Joost suggested, but most of the change is just due to addition of support for external links that I wanted.

Usage:

Note: You can still use the old syntax alongside the new one as the new syntax is written with liquid and doesn't interfere with kramdown's markdown parsing.

General SyntaxOriginal Syntax
- Internal Link: [​[Link to some file]​]​​
- External Link: [​[Title|URL]​]

  • Internal links: [​[​Some Link]]
  • External links: [​[​Some Link::https://address]]

Examples

Example of an internal link that points to a valid post or page, that is, a page with the title (not url) mentioned in the brackets.

  • Example of a good internal link: [​[​The Value of Philosophy]]
  • Rendered Text: The Value of PhilosophyThe Value of Philosophy
    Synopsis of Russell's View

    In the last chapter of this classic — "The problems of Philosophy" — Russell discusses the plight of the prejudice laden society by showing how the material-driven 'practical' men who in their quest to find the most arithmetized utilitarian system remain completely oblivious to the necessity of providing food for the mind. He argues that it is in this — the goods of the mind — lie the value of philosophy which he says is as much important as the goods of the body i...

Example of an internal link that do not point to a valid post or page, that is, a page with the title (not url) mentioned in the brackets.

Note: The yellow highlight shows that there does not exist a page with that title in the project folders.

Example of an external link that points to an url.

  • Example of a good external link: [​[Twitter::https://twitter.com]]
  • Rendered Text: Twitter

Note: Since handling broken external links will require either manipulating headers using JS or using server-side code for link monitoring, we will not be handling external broken links

Code

The code has been heavily commented to make it more accessible, but here is a gist of what it does just in case:

We split the entire file by using double square brackets and put the split tokens into an array we call the content_array.

...
...
{% assign link_open_delimiter = '[​[' %}
{% assign content_array = page.content | split:link_open_delimiter %}
...
...

Example: If a page has "lorem ipsum varum [​[dorum]] borum [​[morum]] gerum" as its content, the resultant content_array will contain the following: ["lorem ipsum varum", "dorum]] borum", "morum]] gerum"]

We loop through the content array and do the same thing for the closing tag so as to be able to get the title separately

...
...
{% assign link_close_delimiter = ']]' %}
{% for item in content_array %}
	{% assign itemparts = item | split:link_close_delimiter %}
	{% assign internal_link = itemparts[0] %}
	{% assign external_link = itemparts[0] | split:external_link_delimiter %}
...
...

Now itemparts[0] has the part inside the square brackets, that is, the title in the case of internal link and both title as well as the url in the case of external link, since the entire external link i.e., of the form [​[Title::URL]] is contained within the brackets.

Once we have the title of the internal links, all that remains in the parsing section is to look through all the files and get the data corresponding to the pages with those titles. Which is done using the Where filter

...
...
{% assign result_posts = site.posts | where: 'title',itemparts[0] %}
...
...

The above code snippet stores all the data pertaining to a particular page in result_posts like its yaml matters in index 0 and its contents in index 1. So, now we can easily fetch the url of the page by using result_post[0].url.

We are pretty much done with our quest to getting the wiki-like syntax in jekyll, Hurray! Come on, already Celebrate!

The only thing that is left now is to be able to create anchor tags with proper title and proper url when generating html. For that we need to store all our urls and links in a separate array so that we can easily loop through them and just create a string with anchor tag and append the corresponding url and title to it.

...
...
{% assign internal_url_array = internal_urls | split:link_joiner_delimiter %}
{% assign internal_link_array = internal_links | split:link_joiner_delimiter %}  
                                       
{% assign replaced_content = page.content %}
                   
{% for title in internal_link_array %}
	{% assign url = internal_url_array[forloop.index0] %}
                   
	{% if url == nil %}
		{% assign link_text = '<a style="background-color:#ffffc4;" href="' | append: 'javascript:void(0)' | append: '">' | append: title | append: '</a>' %}
	{% elsif url == empty %}
		{% assign link_text = '<a style="background-color:#ffffc4;" href="' | append: 'javascript:void(0)' | append: '">' | append: title | append: '</a>' %}
	{% else %}
		{% assign link_text = '<a  href="' | append: url | append: '">' | append: title | append: '</a>' %}
	{% endif %}
                   
	{% assign bracket_link = link_open_delimiter| append: title | append: link_close_delimiter %}
	{% assign replaced_content = replaced_content | replace: bracket_link,link_text %}
{% endfor %}
...
...
Repeat the same for external links
...
...

Here we maintain four arrays two for links/titles and two for urls; and once we have them all, we just loop though the corresponding title/link array and append the url to the anchor string that we have created. And finally to display the resultant anchor tag in our generated html, we have to replace the [​[Title::URL]]/[​[Some Random Link]] in markdown with the newly created anchor tag.

That is what is done in this step:

...
...    
{% assign link_text = '<a  href="' | append: url | append: '">' | append: title | append: '</a>' %}
                   
{% assign bracket_link = link_open_delimiter| append: title | append: link_close_delimiter %}
                           
{% assign replaced_content = replaced_content | replace: bracket_link,link_text %}
...
...

Note: The complete code is shown below and also has comments in the appropriate places to make the code more accesible. Please refer to it. (Also scroll below to see how to use/troubleshoot the code once you've copied it your project).

Complete Code

{% comment %} Liquid Code to Parse Wiki-like link Syntax {% endcomment %}

{% comment %} 
Line1: Get content from the current page into the variable content_array by splitting it using '[​[' as a delimiter, If a page has "lorem ipsum varum [​[dorum]] borum [​[morum]] gerum" as its content, the resultant content_array will contain the following: ["lorem ipsum varum", "dorum]] borum", "morum]] gerum"]  
{% endcomment %}
                   
{% assign link_open_delimiter = '[​[' %}
{% assign link_close_delimiter = ']]' %}
{% assign content_array = page.content | split:link_open_delimiter %}
{% assign external_link_delimiter = '::' %}
                   
{% comment %} 
The use of this weird looking symbol i.e., "$@" is to ensure that we do not fall victim to parsing error due to use of common delimiters like commas(,) in user-code.
{% endcomment %}
                   
{% assign link_joiner_delimiter = '$@' %}
{% for item in content_array %}
	{% if forloop.index > 1 %}
                   
		{% comment %} 
		Same as the first comment, but for closing brackets  
		{% endcomment %}
                   
		{% assign itemparts = item | split:link_close_delimiter %}
                   
		{% comment %} 
		itempart[0] contains the link in the case of internal link and both link as well as url in case of external link  
		{% endcomment %}
                   
		{% assign internal_link = itemparts[0] %}
		{% assign external_link = itemparts[0] | split:external_link_delimiter %}
                   
		{% comment %} 
		external_link[1] will be zero only when it is an internal link as we are splitting itemparts by :: in the previous line 
		{% endcomment %}
                   
		{% if external_link[1] == nil %}
			{% comment %} 
			result_collection will contain the yaml matter of the page with title present in itemparts[0] i.e., title mentioned in double brackets in the current page as a link. 
Note: you must replace the collection_name here with your own collection name.
			{% endcomment %}
                   
			{% assign result_collection_name = site.collection_name | where: 'title',itemparts[0] %}
			{% assign result_posts = site.posts | where: 'title',itemparts[0] %}
			{% assign internal_links = internal_links | append: link_joiner_delimiter | append: internal_link %}
			{% assign internal_urls = internal_urls | append: link_joiner_delimiter | append: result_collection_name[0].url | append: result_posts[0].url %}
		{% else %}
			{% assign external_links = external_links | append: link_joiner_delimiter | append: external_link[0] %}
			{% assign external_urls = external_urls | append: link_joiner_delimiter | append: external_link[1] %}
		{% endif %}
	{% endif %}
{% endfor %}
                                       
{% comment %}
Store all links and urls in separate arrays
{% endcomment %}
                   
{% assign internal_url_array = internal_urls | split:link_joiner_delimiter %}
{% assign internal_link_array = internal_links | split:link_joiner_delimiter %}
{% assign external_url_array = external_urls | split:link_joiner_delimiter %}
{% assign external_link_array = external_links | split:link_joiner_delimiter %}
                   
{% comment %}
Store the content to changed in the replaced_content variable
{% endcomment %}       
                                       
{% assign replaced_content = page.content %}
                   
{% for title in internal_link_array %}
	{% assign url = internal_url_array[forloop.index0] %}
                   
	{% if url == nil %}
		{% assign link_text = '<a style="background-color:#ffffc4;" href="' | append: 'javascript:void(0)' | append: '">' | append: title | append: '</a>' %}
	{% elsif url == empty %}
		{% assign link_text = '<a style="background-color:#ffffc4;" href="' | append: 'javascript:void(0)' | append: '">' | append: title | append: '</a>' %}
	{% else %}
		{% assign link_text = '<a  href="' | append: url | append: '">' | append: title | append: '</a>' %}
	{% endif %}
                   
	{% assign bracket_link = link_open_delimiter| append: title | append: link_close_delimiter %}
	{% assign replaced_content = replaced_content | replace: bracket_link,link_text %}
{% endfor %}
                   
{% for title in external_link_array %}
	{% assign url = external_url_array[forloop.index0] %}
                   
	{% assign link_text = '<a href="' | append: url | append: '">' | append: title | append: '</a>' %}
                   
	{% comment %}
	Since the external link is of the form [​[Title::URL]], while replacing we have to append the "::URL" part to the title, which is done here using "append: external_link_delimiter"
	{% endcomment %}  
                   
	{% assign bracket_link = link_open_delimiter | append: title | append: external_link_delimiter | append: url | append: link_close_delimiter %}
	{% assign replaced_content = replaced_content | replace: bracket_link,link_text %}
{% endfor %}
{{ replaced_content | markdownify }} 

How to use the code in your project

All you have to do to be able to use the code above in your own project is just copy the snippet as it is and paste it in the place where you usually display your content — {​{​content}}, that is, instead of {​{​content}} now it will be that liquid code snippet that you just copied, which will replace your usual content will {​{replaced_​content}} as can be seen in the last line of the code above.

Note: If the code does not work out-of-the-box, do not be worried. It works, I use it all over my site, even this page and all the examples in the page are powered by that code.

Here are some troubleshooting steps if it does not work out of the box.

  1. You might not have changed the site.collection_name in the above code.
    1. If you do not use collections, just remove all the statements that use collection_name.
    2. If you do use collections, just change the collection_name to your custom collection name.
  2. There might be stray characters introduced due to copy-paste.
    1. Try to delete all the occurences of "[​[", "]]", "::", "$@" in the above code once you have copied them and just re-type them.

A Trivia for people who are interested:

If you observed keenly, you'd have noticed that this page makes extensive use of "[​[" and "]]" for expository purposes without getting converted to a link. All thanks to zero-width space. Also yes, you will have to deal with this issue too if you wished to use "[​[" in your makdown files. 😅

P.S. please do let me know if you still have any questions or issues with getting it to work in the comments section below: