这几天捣鼓博客的时候发现 Quartz 不原生支持 Excalidraw,一个解决方法是用 Obsidian 的 Quartz Syncer 插件,不过我尝试了一下之后发现这个插件不支持先同步到本地 git 仓库再推送到远程,而是直接用 Github token 来做同步,笔记多了之后速度很慢。

思来想去还是直接写两个脚本最简单,于是就有了这篇博客,供参考。

同步到本地仓库

因为后面还有可能会调一调 quartz.config.ts 或者 quartz.layout.ts 之类的,所以我还是在本地保留了一个仓库。因此每次同步时要先同步到本地仓库再推送。

首先写一个 sync.sh

#!/bin/bash
 
OBSIDIAN_PATH="/mnt/e/Obsidian Vault/Obsidian Vault"
QUARTZ_CONTENT_PATH="./content"
 
echo "Strat to sync..." >&2
 
mkdir -p "$QUARTZ_CONTENT_PATH"
 
rsync -av --delete \
    --exclude='.git' \
    --exclude='.obsidian' \
    --exclude='.trash' \
    --exclude='Scripts/' \
    --exclude='*.canvas' \
    "$OBSIDIAN_PATH/" "$QUARTZ_CONTENT_PATH"
 
echo "Sync finished!
" >&2
 
echo "Differences:" >&2
git diff --name-only >&2

为了能直接在 Obsidian 界面内同步,我安装了 Shell Command 插件,这个插件可以让你在 Obsidian 内运行自定义的命令。(这里我将 stdout 的输出忽略了,因此提示信息我都重定向到了 stderr

推送到远程仓库

再写一个 deploy.sh

#!/bin/bash
 
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
 
echo "Processing Excalidraw replacement..." >&2
 
python3 replace_excalidraw.py >&2
 
echo "Bulding and pushing to Github..." >&2
npx quartz sync
 
echo "Successfully deployed!" >&2

直接调用 npx quartz sync 就行了,会自动推送到远程仓库。这里记得设置一下 NVM 的环境变量,不然不能直接在 Shell Command 中运行。

替换 Excalidraw 绘图文件为 SVG

这里的 replace_excalidraw.py 是为了将笔记中的 Excalidraw 绘图文件的引用转为显示 SVG 的 HTML,Quartz 可以直接支持笔记中对图片的引用,但是因为我想支持博客中深色/浅色主题下显示不同的图片,所以这里直接转成了 HTML:

import os
import re
 
# 配置 Quartz 的内容目录
CONTENT_DIR = "/home/evan/WORK/quartz/content"
 
def process_excalidraw_images(content_dir):
    pattern = re.compile(r'!\[\[(.*?)(\.excalidraw)(.*?)\]\]')
 
    for root, dirs, files in os.walk(content_dir):
        for file in files:
            if file.endswith(".md"):
                file_path = os.path.join(root, file)
                
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                
                # 检查是否存在 excalidraw 引用
                if '.excalidraw' in content:
                    # 替换逻辑
                    
                    def replace_func(match):
                        base_name = match.group(1) # e.g., "attachments/drawing"
                        ext = match.group(2)       # ".excalidraw"
                        alias = match.group(3)     # e.g., "|500" or ""
                        
                        # 构建导出图片的文件名 (需与你的 Excalidraw 插件设置一致)
                        # 注意:Quartz 处理图片链接通常需要 URL 编码或相对路径,
                        # 这里假设图片和笔记在同一层级或 Quartz 能自动解析 filename
                        
                        # 构造 HTML 块
                        # class="image-switch" 用于 CSS 控制
                        # 注意处理 alias (如调整宽度)
                        
                        style = ""
                        if "|" in alias:
                            width = alias.split("|")[-1]
                            if width.isdigit():
                                style = f'style="width:{width}px"'
 
                        # 生成 HTML
                        # 注意:Quartz 4 通常能解析 Markdown 中的 HTML <img> 标签
                        html_block = (
                            f'<div class="image-switch" {style}>'
                            f'<img class="light-img" src="{base_name}.excalidraw.light.svg" alt="{base_name}" />'
                            f'<img class="dark-img" src="{base_name}.excalidraw.dark.svg" alt="{base_name}" />'
                            f'</div>'
                        )
                        return html_block
 
                    new_content = pattern.sub(replace_func, content)
                    
                    # 写回文件
                    with open(file_path, 'w', encoding='utf-8') as f:
                        f.write(new_content)
                    print(f"Processed Excalidraw in: {file}")
 
if __name__ == "__main__":
    process_excalidraw_images(CONTENT_DIR)

(看到这么详细的注释就知道是 AI 写的了)

接着记得在 Obsidian 的 Excalidraw 插件中的嵌入到 Markdow 文档中的绘图 - 导出 - 导出设置中开启自动导出 SVG 副本和同时导出深色和浅色主题图片。

最后在 Obsidian 中设置好调用两个脚本的命令和快捷键就可以使用了,平时在 Obsidian 中正常用 Excalidraw 绘图,推送到仓库时会自动将对 .excalidraw.md 后缀文件的引用替换为显示对应 SVG 的 HTML,并且支持随博客显示的深色/浅色主题更换绘图的深色/浅色主题。