起因
之前选择hugo作为博客的页面生成器,用obsidian来编辑,体验不错,但是也有些问题,比如在obsidian插件shell commands实现hugo博客编辑实时预览这篇文章中实现的效果中,obsidian的双链[[]]
不受支持,预览时候不会转换成链接,就只显示原来的文本
我想要实现的效果:
- 实时预览时候obsidian的双链可以转换成指向文章的链接,同时原本的markdown文件不被修改,方便继续在obsidian内编辑
- 在部署时候,obsidian的双链可以转换成指向文章的链接,在cf的部署服务上执行,这里由于markdown文件不在本地了,可以先修改markdown原文件再用hugo生成页面
过程记录
首先想到的是hugo会不会有预处理markdown的功能,或者插件能实现,但是搜了一圈资料,无解,而且是老早就有人提过的问题,一直没有很好的解决
之后想通过修改hugo源码,在渲染页面之前对读取的markdown字符串加一个预处理,为此翻了半天hugo源码,改出了第一版的满足我要求的hugo,但是这里还是有些问题的,首先我对hugo的源码并没有全面的了解,会不会引入新bug不可知,其次在cloudflare的页面生成环境里面又要拉一次我的改版hugo,很麻烦,并且之后hugo每次更新我都要重新拉下来检查修改编译,很麻烦
我没找到obsidian的双链信息存储的位置,markdown内也只有[[文章名]]
,并且只有在不同路径有同名文章时候才需要指定路径,猜测是把所有文章的标题用来匹配,而没有存储链接关系,理论上是可行的,所以后面把双链转换成网页的链接也从这个思路出发,先获取所有文章名和对应的地址,再根据[[文章名]]
进行匹配
现在的解决方案是:
注意并不支持标题里有特殊符号,这里只对空格进行了处理,实际因为没去仔细研究hugo的转换规则只做简单处理,如果未来有需要会去看hugo源码的转换规则修改油猴脚本和convert转换程序
本地实时预览时候,在油猴脚本加一段代码,实时修改html实现双链文本转链接,具体实现如下:
// ==UserScript==
// @name obsidian link convert
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match http://localhost:5678/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=undefined.localhost
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
document.addEventListener('DOMContentLoaded',() => {
async function getsitemapLinks(sitemapUrl) {
const response = await fetch(sitemapUrl);
const data = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'application/xml');
const urls = Array.from(doc.querySelectorAll('url loc')).map(loc => loc.textContent);
return urls;
}
(async () => {
const sitemapUrl = 'http://localhost:5678/sitemap.xml';
const links = await getsitemapLinks(sitemapUrl);
let pages=links.map(e=>decodeURI(e.split('/').at(-2)))
const map = pages.reduce((acc, key, index) => {
acc.set(key, links[index]);
return acc;
}, new Map());
function replaceBracketsWithLinks(element = document.body) {
const regex = /\[\[(.*?)\]\]/g;
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && regex.test(node.textContent)) {
const span = document.createElement('span');
span.innerHTML = node.textContent.replace(regex, (match, text) => {
const urlText = text.replace(/ /g, "-");
const link = map.get(urlText);
return `<a href="${link}">${text}</a>`;
});
node.parentNode.replaceChild(span, node);
} else if (node.nodeType === Node.ELEMENT_NODE && node.nodeName !== 'CODE' && node.nodeName !== 'PRE') {
replaceBracketsWithLinks(node);
}
});
}
replaceBracketsWithLinks()
})();
})
})();
主要的逻辑是:获取sitemap里面的所有链接,提取最后两个/
之间的文本作为文章名,再把页面中的所有[[文章名]]
替换成a标签,链接指向之前从sitemap获取的该名称对应的链接
实际效果是可以完成我的需求的,修改效果没问题,但是还是有些问题,在开启MathJax时候会把[[]]
转换成数学公式,我一般是把MathJax关掉的,所以影响不大,如果你有需求,还是要自己探索
cloudflare部署时候,改一下发布执行的命令,在前面加个预处理程序,把所有markdown内的双链都修改成hugo支持的链接,我写了一个go程序来实现:
package main
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
)
func processFileContent(content string, names []string, paths []string) string {
re := regexp.MustCompile(`\[\[(.*?)\]\]`)
return re.ReplaceAllStringFunc(content, func(match string) string {
name := match[2 : len(match)-2]
index := indexOf(names, name)
if index >= 0 {
path := paths[index]
path = strings.ReplaceAll(path, "\\", "/")
path = strings.TrimSuffix(path, ".md")
contentIndex := strings.Index(path, "content")
if contentIndex >= 0 {
path = path[contentIndex+len("content"):]
}
path = strings.ReplaceAll(path, " ", "-")
return fmt.Sprintf("[%s](%s)", name, path)
}
return match
})
}
func indexOf(slice []string, value string) int {
for i, v := range slice {
if v == value {
return i
}
}
return -1
}
func main() {
var paths []string
var names []string
filepath.WalkDir("content", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(d.Name(), ".md") {
paths = append(paths, path)
names = append(names, strings.TrimSuffix(d.Name(), ".md"))
}
return nil
})
for _, path := range paths {
contentBytes, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
content := string(contentBytes)
newContent := processFileContent(content, names, paths)
err = ioutil.WriteFile(path, []byte(newContent), os.ModePerm)
if err != nil {
panic(err)
}
}
}
主要逻辑是:遍历content文件夹,获取所有的markdown文件的名称和路径,再按照名称和路径的映射关系,把所有markdown内的[[文章名]]
替换成匹配到的文章的hugo可以识别的链接
编译一份linux的可执行文件convert,放到博客的的根目录,再用git上传到github,cloudflare拉取仓库时候就可以获取到转换用的程序,把发布命令修改成chmod 755 convert && ./convert && hugo
,就能先执行转换程序再使用hugo发布