Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Wikilink Syntax #6

Open
henriette-einstein opened this issue May 28, 2022 · 9 comments
Open

Support Wikilink Syntax #6

henriette-einstein opened this issue May 28, 2022 · 9 comments
Labels
enhancement New feature or request

Comments

@henriette-einstein
Copy link

I would like to use Nuxt Content to generate HTML form Obsidian files (see https://www.obsidian.md). Obsidian uses the Wikilink syntax to reference files.

e.g.
[[anotherfile]] references the file "anotherfile.md" in the current directory.

This syntax is (as far as I could see) not supported by nuxt content. I could do some preprocessing to convert that syntax to the standard markdown syntax. But that would not work if I use the content-directory directly as the root folder for the Obsidian vault.

Therefore it would be great if you could provide a remark plugin and a configuration example that handles that.

@Atinux
Copy link
Owner

Atinux commented May 30, 2022

I love the idea, but in order for the links to work we need to know the route associated by it, for this template we know that one page = one document but that may not be the case for all websites using Nuxt Content.

Have you tried https://github.com/landakram/remark-wiki-link ?

See https://content.nuxtjs.org/api/configuration#markdown

@Atinux Atinux added the enhancement New feature or request label May 30, 2022
@sig9
Copy link

sig9 commented May 30, 2022

@Atinux
Copy link
Owner

Atinux commented Jun 16, 2022

Should be fixed for the redirection thanks @sig9

@ManasMadrecha
Copy link

ManasMadrecha commented Aug 8, 2022

@henriette-einstein

For Obsidian wiki-links with Absolute paths, you should follow these steps

  1. Use remark-wikilinks plugin as suggested by @Atinux, or create a custom remark plugin that visits text node in tree and using simple regex, grab the href and alt text of the link, and create a new html type node. One benefit of using own plugin is to add custom classes to such node, which can be used in the next step to grab such internal links.
  2. Then, change the tagName of such links to md-a or whatever, and create that component in components/content folder.
  3. Inside that, accept props or use fall-back attributes, and using those, fetch the requisite article by transforming the string of href accordingly.
    • For example, if you are using Obsidian's Vault folder as source folder, it might have multiple sub-folders. Yet the absolute wiki-links inside the Obsidian Vault will be just name of the file, without any path info (unless a file with similar name exisit in some other sub-folder)
    • So, inside the nuxt app, when you use the obtained href to fetch the article, how will the Content Module know where to find it relative to the source folder?
    • So, for that, you can have simple logic that, if the href contains /, that article does not have a unique name, so Obsidian must have added entire path info as the href. And, if the href doesn't contain /, then, it's a unique article.
    • So, while fetching the article, inside the where query, you can use either path or slug respectively to match it with href and fetch the article.
    • After fetching it, use that article's _path in the Template as the href of the link.

This syntax is (as far as I could see) not supported by nuxt content. I could do some preprocessing to convert that syntax to the standard markdown syntax. But that would not work if I use the content-directory directly as the root folder for the Obsidian vault.

By these steps,

  1. you won't need to do any pre-processing. Your original markdown contents can remain as it is in Obsidian Vault.
  2. Also, you don't need to use the Vault as a sub-folder for the nuxt-app. Using multi-sources feature of Content Module, your Obsidian Vault can remain whenever you like in your PC (e.g., OneDrive), and your nuxt-app elsewhere in your dev folder.

@henriette-einstein
Copy link
Author

@ManasMadrecha

Thanks a lot for that information. I tried to implement a custom remark plugin as you described and configured it in Nuxt-config. The plugin is super-simple:

import { visit } from 'unist-util-visit'

// The RegEx to match WikiLinks
const wikiRegex = /\[\[.*?\]\]/g

export default function remarkWikilink () {
  function transformer (ast) {
    visit(ast, 'text', node => {
      if (wikiRegex.test(node.value)) {
        const newValue = node.value.replace(wikiRegex, match => {
          return ( 
           '<w-link to="">' +
            match.substring(1, match.length - 1) +
            '</w-link>'
          )
        })

        Object.assign(node, { type: 'html', value: newValue })
      }
    })
  }

  return transformer
}

The returned value is just a test here. However, the plugin is never invoked. Instead

# Wikilink Test

[[Link to Page|Link Text]]

is rendered as

<span><span>Link to Page|Link Text</span></span>

If I use a different REGEX and a different input text, I can see that the plugin is involved. The plugin configuration therefore seems to work correctly.

It looks like double square brackets are somehow treated before they arrive in the plugin.

I will try to fiddle with the remark-wikilinks plugin, even though I have not seen a possibility to change the resulting tag-name in this plugin.

@ManasMadrecha
Copy link

ManasMadrecha commented Aug 12, 2022

@henriette-einstein

Remark

Unless some other plugin is also installed that treats the [[ as start of link, it will remain as text only. So, some other plugin must be treating it first, before your plugin. Try your custom plugin, by removing remark-wikilink plugin, or even remark-mdc plugin (enabled by default).

Also, did you register it correctly inside nuxt.config.js, i.e., inside remark and not rehype?

Then, the [[... will just be normal text node. And since you are visiting text node inside your plugin, it will work fine.

EDIT: Hey, try converting your plugin to a rehype plugin, instead of remark, and then try. So, visit the text there, and don't replace the node. Rather in the end, instead of using type:html, use type:element, tagName: w-link, properties: {className: ['internal']} and Object.assign this to the node.

Rehype

You can change the tagName of links even with remark-wikilink plugin, by having a custom rehype plugin. I think that plugin adds some classes like internal to the a links, so inside your own rehype plugin, you can visit such element with tagName as a and with node.properties.className containing internal, and then inside the visitor function, just change the node.tagName to md-a.

@henriette-einstein
Copy link
Author

henriette-einstein commented Aug 12, 2022

@ManasMadrecha
Thanks for your support! I have a minimal nuxt.config.js:

export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  content: {
    markdown: {
      remarkPlugins: ['remark-wikilink']
    }
  }

})

I have not configured any other plugin, therefore the behavior must be included in Nuxt-Standard.

I have already found another solution using a Nitro plugin.

Nitro-Plugin

Create a file wikilinkPlugin.ts in the directory server/plugins. My initial version is

// The RegEx to match WikiLinks
const wikiREGEX = /\[\[.*?\]\]/g
// The Regex for the file part of a Wikilink
const fileREGEX = /(?<=\[\[).*?(?=(\]|\|))/;
// The Regex for the optional title part of a Wikilink
const titleREGEX = /(?<=\|).*(?=]])/;


function transformLinks (text: string): string {
    let theLinks = text.match(wikiREGEX);
    let linksFound = []

    if (theLinks) {
        for (var theLink of theLinks) {
            console.log(theLink)
            let theLinkRef = theLink.match(fileREGEX);
            if (theLinkRef) {
                let theTitle = theLink.match(titleREGEX);
                let newLink = `<m-a href="${theLinkRef[0]}">`+ theTitle?theTitle[0]:theLinkRef[0] + '</m-a>'
                linksFound.push(
                    {
                        'oldText': theLink,
                        'newText': newLink
                    }
                )
            }
        }    
    }
    for(var i of linksFound) {
        text = text.replace(i.oldText,i.newText)

    }
    return text;
}

export default defineNitroPlugin((nitroApp) => {
    nitroApp.hooks.hook('content:file:beforeParse', (file) => {
      if (file._id.endsWith('.md')) {
        file.body = transformLinks(file.body)
      }
    })
})

and configure the plugin in 'nuxt.config.js`.

import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  nitro: {
    plugins: ['~/server/plugins/wikilinkPlugin.ts']
  }
})

The code is still incomplete. Local links and transclusions are not handled to name only the problems I can see by now. But it may be a start.

For some reason I don't understand, the final HTML will contain additional whitespace I did not produce. This may be a more complicated issue.

<m-a href="Text"> Text </m-a>

instead of

<m-a href="Text">Text</m-a>

@ManasMadrecha
Copy link

ManasMadrecha commented Aug 12, 2022

@henriette-einstein OMG, you have complicated it so much 😂

Issue with mdc

Well, the issue lies with mdc.

So, this in markdown

image

becomes

With MDC enabled (by default)

image

With MDC disabled

image

Solution

So, previously your remark plugin was not working, because you were visiting text node. While because of mdc, in the ast, the Obsidian links or embeds no more remain inside a text node; each [ becomes a span. That's the way mdc works.

With it disabled, now, your remark plugin will work, and you don't need nitro-plugin.


Note: If you don't want to disable mdc, you will have to visit span and not text node.

Also, Note: You shouldn't use the Nitro Plugin for modifying the text into links, because in that, the body is entire contents of the file. I guess that will even modify the Regex Matches inside code nodes. Rather, using remark to visit only text or specific span element ensures Regex Matches are as intended.

My Solution

Here is the my code (with mdc disabled):

Note:

  1. This code also considers the attachments (embeds) in md files.
  2. With mdc enabled, will have to modify the Code drastically. So, better to disable mdc for the time being.

With my Code, the above markdown will become this

image

Note: After you create the actual components, these tags will be replaced with the whatever tags you use in the template of those components.

The Code

Inside, nuxt.config.js,

import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  modules: ['@nuxt/content'],
  content: {
    markdown: {
      remarkPlugins: ['remark-obsidian-links'],
      mdc: false
    },
  }
})

The next file is plugins/markdown/remark-obsidian-links/index.js

Code
import { visit } from 'unist-util-visit'

function transformer(tree, { data }) {

	visit(tree, 'text', (node, i, parent) => {
		const txt = node.value;

		// const EMBED_REGEX = /(!\[\[)([^\[\[]+)(\]\])/g
		// const LINK_REGEX = /(\[\[)([^\[\[]+)(\]\])/g

		const EMBED_REGEX = /(!\[\[)(.*?)(\]\])/g
		const LINK_REGEX = /(\[\[)(.*?)(\]\])/g

		let newEmbed = txt.replace(EMBED_REGEX, (_, m1, m2, m3) => {

			let src, title;
			src = title = m2;
			if (src.includes('|')) {
				let parts = src.split('|')
				src = parts[0];
				title = parts[1];
			}

			return `<md-embed src="${src}" data-title="${title}"></md-embed>`

			/* Now, inside `md-embed` component, considering `src ends with what, check which type of embed is it (e.g., image, PDF, audio, or even an MD file, and accordingly use element using `is`.)
		  
			EXAMPLES: 

			If Image, then `alt` will be title. Also, consider if using Obsidian's width feature, then using `^(\d+)` regex, use that as `width` of the image, and not alt.

			If audio, then use `audio` native element with `control`, and inside it, add child `source` element, whose src will be above `src`.
		  
			If it is an MD file, you can use `ContentDoc` component, and fetch the file accordingly to display it. Also, take care of `src` that has `#`, i.e., sections/headings.

			*/
		})

               // After the Text node's Embeds have been converted, use such modified Text Node for replacing its links.
		let newLink = newEmbed.replace(LINK_REGEX, (_, m1, m2, m3) => {

			let href, title;
			href = title = m2;
			if (href.includes('|')) {
				let parts = href.split('|')
				href = parts[0];
				title = parts[1];
			}

			return `<md-a href="${href}" data-title="${title}">${title}</md-a>`
			/* Now, inside `md-a` component, fetch the article based on `href` prop. 
			- If `href` contains `/`, then inside `where` query, use `path`, else use `slug`. 
			- Also, take care of the `source`, if you using Content Module's multi-source feature.
			*/

		})
		Object.assign(node, { type: 'html', value: newLink })

	})

}

export default function () {
	return transformer
}

The next file is plugins/markdown/remark-obsidian-links/package.json

{
    "name": "remark-obsidian-links",
    "version": "1.0.0",
    "type": "module",
    "main": "index.js",
    "author": {"name": "ManasMadrecha", "url": "https://manas.madrecha.com"},
    "dependencies": {
        "unist-util-visit": "4.1.0"
    }
}

Install it in app's package.json

  "dependencies": {
    "remark-obsidian-links": "file:./plugins/markdown/remark-obsidian-links"
  },
  "devDependencies": {
    "@nuxt/content": "^2.0.0",
    "nuxt": "^3.0.0-rc.4"
  }

@henriette-einstein
Copy link
Author

@ManasMadrecha Thanks a lot for your guidance and solutions! I'll now write and refine the components to see how that works.

After that, I'll try to implement a backlink component. Hope I don't have to bother you again then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants