content-wind
content-wind copied to clipboard
Support Wikilink Syntax
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.
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
See https://content.nuxtjs.org/api/configuration#markdown
@Atinux side-note: I noticed that https://content.nuxtjs.org/api/configuration#markdown does a 302 redirect to https://content.nuxtjs.org/api/v1/getting-started/configuration#markdown with a "Page not found" message.
Should be fixed for the redirection thanks @sig9
@henriette-einstein
For Obsidian wiki-links with Absolute paths, you should follow these steps
- Use remark-wikilinks plugin as suggested by @Atinux, or create a custom remark plugin that
visit
stext
node intree
and using simple regex, grab thehref
andalt text
of the link, and create a newhtml
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. - Then, change the tagName of such links to
md-a
or whatever, and create that component incomponents/content
folder. - 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 thehref
. And, if thehref
doesn't contain/
, then, it's a unique article. - So, while fetching the article, inside the
where
query, you can use eitherpath
orslug
respectively to match it withhref
and fetch the article. - After fetching it, use that article's
_path
in the Template as thehref
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,
- you won't need to do any pre-processing. Your original markdown contents can remain as it is in Obsidian Vault.
- 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.
@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.
@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
.
@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>
@henriette-einstein OMG, you have complicated it so much 😂
Issue with mdc
Well, the issue lies with mdc
.
So, this in markdown
becomes
With MDC enabled (by default)
With MDC disabled
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:
- This code also considers the attachments (embeds) in
md
files. - With
mdc
enabled, will have to modify the Code drastically. So, better to disablemdc
for the time being.
With my Code, the above markdown will become this
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"
}
@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.