I Am Ven

Photo by Andy Hermawa on Unsplash

Porting Obsidian Block Links to MDX in Astro


Intro

In this article, I’ll go over how to customize a remark plugin and reference how to create a custom plugin.

Table of Contents

FYI

I am by no means an expert at Javascript. There might be some code smells or things that seem “obvious” to you. Sadly not all of us can find (or have the time to find) programming mentors. So I have to figure a lot of this out on my own.

I am always open to constructive criticism and helpful instructions on how to improve.

Now without further ado, I present my solution to this very frustrating problem.

The Problem

I have been looking for an easy way to create a blog that allows me to have full control over the customization.

Wordpress is slow, buggy, and paying for BlueHost was not ideal.

I found Obsidian, which allowed me to easily create notes using Markdown. I love the idea of markdown, and the ability to add in HTML was another step forward to realizing my blog needs.

The downside is that it’s very obtuse to get custom JS to run in Obsidian. Publish allows you to do so only if you have a custom domain. I imagine there is a reason for that, and I can always get a custom domain.

However, why settle for less when you can use a framework like Astro JS?

This gave me free reign to customize the website as I pleased while using the simplicity of Markdown for my blog posts.

The catch here was that MDX/Remark (which Astro uses for markdown processing) markdown and Obsidian markdown had some slight differences.

In Obsidian, you can use ^reference to create an anchor to a block on the page. A block is a paragraph, heading, etc. To create a link to that block you would use the syntax: [[^#reference]].

MDX/Remark did not seem to have that capability, and their list of plugins was devoid of something like that as well.

Queue my rabbit hole of trying to figure out how to create a custom plugin so that I could change ^reference to <a id="reference"></a> and [[^#reference]] to it’s appropriate HTML element (heading, etc.).

The Original Idea

Originally, I had thought that I needed to manipulate the “tree” (remark utilizes a library that converts markdown into a tree) directly with my own custom plugin.

I successfully was able to get an idea of where the ^ was in the tree and how to search for it using regex. However, converting it to HTML was beyond me.

I searched and searched and couldn’t find anything that directly said “this is how to do it”. I tried “reading the manual”, but there are 150+ plugins and only a handful of comments.

Then I found this article: https://www.gatsbyjs.com/tutorial/remark-plugin-tutorial/

So direct! I had found what I needed to finally make some progress on this problem.

Here’s what I came up with:

export function remarkBlockLink() {
  return function(tree:any,file:any){
    visit(tree, "text", node => {
      if (!isCustomDirective(node.value,carrot)) return
      let text = toString(node)
      const html = `
          <a id="${text}">
          </>
        `
        
      node.type = "html"
      node.children = undefined
      node.value = html
    })
    return tree;
  }
}

First, we’re returning a function that takes in the tree and file (I believe this is the data/frontmatter that Astro uses, I’m not 100% sure on that.).

The visit function walks through the tree, using the second attribute to search only for nodes that match that string.

Then the third argument is a callback that takes the node as an argument.

I then make sure that the value of the node has our ^ carrot in it.

Afterwards we create a string with the desired HTML (in this case, the anchor tag)

Using WikiLinks Plugin

Remark JS has a plugin that translates Wikilink style Markdown to page links.

So [[Blog Page]] would become <a href="#page/blog-page">Blog Page</a>.

The plugin allows you to change this functionality through the option hrefTemplate. The default for this option is: (permalink) => '#/page/${permalink}\'.

So, if we want to capture ^ and change it, then all we need to do is change this function.

Here’s what I came up with:


const carrot = new RegExp(/\^/)
const hashtag = new RegExp(/^#/)

const isCustomDirective = (data:string,exp:RegExp)=> {
  return data.match(exp)
}

  

export function blockLink(permalink:string){
  if (isCustomDirective(permalink,carrot) || isCustomDirective(permalink,hashtag)){
    return `${permalink}`
  }
  else {
    return `#/page/${permalink}`
  }
}

export function resolveLink(name:string){
  if (isCustomDirective(name,hashtag)){
    return [name.replace(/ /g,'-').toLowerCase()]
  }
  else {
    return [name.replace(/ /g, '_').toLowerCase()]
  }
}

The blockLink function is taking the permalink and checking for either the carrot ^ or a hashtag # using regex.

Normally, the WikiLink plugin would just replace the markdown with a link to a page. However, now we are instructing it to just add an anchor.

The function passed to hrefTemplate takes the permalink parsed by WikiLink and then spits out a formatted string. Either the anchor link or the relative page link.

The # was added in to catch [[#^ref|Page]], so that this would be turned into a link to the anchor.

<a href="#^ref">Page</a>

Next is the resolveLink function. WikiLinks allows another option, pageResolver, to resolve the link to something readable to the browser.

Similar to the blockLink function, we are filtering by our custom directives ^ and #. Then we replace any spaces with a - hyphen. Generally I’ve seen that any href or ids take out spaces (or special characters) and replace them with hyphens. Otherwise, if it’s just a page link, we use the syntax in the else statement (which I most likely will change in the near future).

The last bit of customization comes from the aliasDivider option for the WikiLinks plugin. I set this to | because the default for the plugin is :.

Conclusion

Hopefully this helps anyone who has questions about how to edit plugin functionality for Remark. I certainly had to take a few days (and some trial and error) to figure out how to get this done.