FreshでObsidianのウィキリンクをパースする
作成日
更新日
前提
- FreshでObsidian Publishライクなブログを目指す
- FreshでSupabase上の記事を取得する
- FreshでSupabase上の画像を取得する
やること
- 記事のパース
- サーバーサイドでやっておくべき
- GPTにはクライアントサイド(
api
)でやれって言われたけど無視する
- GPTにはクライアントサイド(
- ウィキリンク
- 画像
- 外部リンクには特殊なクラスをつけたい
external
- サーバーサイドでやっておくべき
[[ウィキリンク]]
の形をパースする[[ウィキリンク|表示名]]
の形をパースする[[Notes/ウィキリンク]]
ウィキリンク
にパース
[[Notes/ウィキリンク|表示名]]
- これは
表示名
にパース
- これは
- リンク先が存在するかどうかを確認する必要がある
- もしない場合には
別名
の名前のみを表示するか<span class="wikilink">ウィキリンク</a>
みたいにノートがあることが外からわかるようにしておくか。 - 存在する場合はその記事へのリンクを付与する。また
internal
クラスをつける
- もしない場合には
![[画像名.png]]
の形をパースする![[画像名.webp|548x225]]
の形をパースする- Obsidian Image Converterでalignを決めていても特にソースコード上では見える形ではでてこない。

- この書き方では
|500x200
のサイズ指定は[]
で行う
- この書き方では

- caption がつけられる


alt
なしバージョン
画像についての調査
パースする
ObsidianRenderer
をつくる
utils/obsidianRenderer
import { Renderer } from "@deno/gfm";
import he from "npm:he@^1.2"; // エンコードライブラリ
import { PostCacheEntry } from "../types/postCacheEntry.ts";
export class ObsidianRenderer extends Renderer {
private postCache = new Map<string, PostCacheEntry>();
constructor(cache?: Map<string, PostCacheEntry>) {
super();
if (cache) {
this.postCache = cache;
}
}
override text(text: string): string {
// Obsidian画像構文: ![[filename.webp|300x200]]
text = text.replace(/\!\[\[([^\]|]+)(?:\|(\d+)x(\d+))?\]\]/g, (_match, filename: string, width?: string, height?: string) => {
const ext = filename.split(".").pop()?.toLowerCase();
const isMedia = ["mp4", "webm", "pdf"].includes(ext ?? "");
if (isMedia) {
return `<iframe src="/media/${filename}" width="100%" height="400" class="media-embed" title="${he.encode(filename)}"></iframe>`;
}
const imgSrc = `/api/getImage/${he.encode(filename)}`;
const sizeAttr = (width && height) ? ` width="${width}" height="${height}"` : "";
return `<a href="/images/${he.encode(filename)}"><img src="${imgSrc}" alt="${he.encode(filename)}"${sizeAttr} /></a>`;
});
// 通常の [[wikilink|label]] 構文(!無し)
text = text.replace(/(?<!\!)\[\[([^\]]+?)\]\]/g, (_match, content: string) => {
const [rawSlug, display] = content.split("|");
const slug = rawSlug.replace(/^.+\//, "");
const label = display || slug;
const entry = this.postCache.get(slug);
if (entry?.exists) {
return `<a href="/${entry.category}/${slug}" class="internal">${he.encode(label)}</a>`;
} else {
return `<span class="wikilink">${he.encode(label)}</span>`;
}
});
return text;
}
override image(src: string, title: string | null, alt: string): string {
// Obsidian形式: ![[image.webp|548x225]]
let file = src;
let width: string | undefined;
let height: string | undefined;
const match = src.match(/^(.+?)\|(\d+)x(\d+)$/);
if (match) {
[, file, width, height] = match;
}
const path = file.startsWith("http") ? file : `/images/${file}`;
const sizeAttr = width && height ? ` width="${width}" height="${height}"` : "";
const altAttr = file;
return `<img src="${path}" alt="${he.encode(altAttr)}"${sizeAttr}${title ? ` title="${he.encode(title)}"` : ""} />`;
}
override link(href: string, title: string | null, text: string): string {
const isExternal = /^https?:\/\//.test(href);
const classAttr = isExternal ? ' class="external"' : ' class="internal"';
const targetAttr = isExternal ? ' target="_blank" rel="noopener"' : '';
const titleAttr = title ? ` title="${he.encode(title)}"` : "";
return `<a href="${href}"${classAttr}${targetAttr}${titleAttr}>${he.encode(text)}</a>`;
}
}
この Renderer
を次のように使う。
libs/getPosts.ts
// Markdown から全 [[slug]] を抽出
const slugs = Array.from(
new Set(
[...data.content.matchAll(/\[\[([^\]|]+)(?:\|.*?)?\]\]/g)].map((m) =>
m[1].replace(/^.+\//, "")
),
),
);
// Supabase からウィキリンク上の記事が存在するかをチェック
const postMap = await getPostCache(slugs);
// MyRenderer に渡す
const renderer = new ObsidianRenderer(postMap);
// このとき allowedTags と allowedAttributes と allowedClassesを
// 書いておかないと消されちゃう
const html = await render(data.content, {
renderer: renderer,
allowIframes: true,
allowMath: true,
allowedTags: ["img", "video", "iframe", "figure", "figcaption", "math", "a"],
allowedAttributes: {
a: ["href", "class", "target", "rel", "title"],
img: ["src", "alt", "width", "height", "title"],
iframe: ["src", "width", "height"],
},
allowedClasses: {
a: ["external", "internal"],
div: ["highlight", "notranslate"],
},
});
Renderer内は同期的に処理するので、Supabase からウィキリンク上の記事が存在するかをチェックする処理は外部で行ってからrendererに渡す必要がある。
utils/postExists.ts
import { PostCacheEntry } from "../types/postCacheEntry.ts";
import { supabase } from "./supabase.ts";
/**
* Supabase 上に指定された slug を title に含む記事が存在するかチェック
* @param slug[] 検索したい部分文字列の配列(例: "Obsidian")
* @returns 存在すれば PostCacheEntryをMapで返す
*/
export async function getPostCache(slugs: string[]): Promise<Map<string, PostCacheEntry>> {
const { data, error } = await supabase
.from("articles")
.select("title, category")
.eq("private", false);
const result = new Map<string, PostCacheEntry>();
if (error || !data) return result;
for (const slug of slugs) {
const matched = data.find((row) => row.title === slug);
if (matched) {
result.set(slug, {
exists: true,
category: matched.category.name || "notes", // fallback
});
} else {
result.set(slug, {
exists: false,
category: "",
});
}
}
return result;
}
とりあえずコレでパースできた。 追加であるかもだけど。

公開日
更新日