概要
Java の Markdown パーサーライブラリ flexmark-java を利用して、Markdown を HTML に変換する際、リンク生成時にリンク先 URL をカスタマイズする例です。
発端は、自作の GitBucket Markdown Enhanced Plugin において、リポジトリトップ (README.md) からの相対リンクがうまく機能しないバグでした。
GitBucket Markdown Enhanced Plugin は、GitBucket 標準のマークダウンレンダリングエンジンを置き換えるプラグインです。
Scala で開発している関係上、Java ではなく Scala での例です。Java で同様のことをしたい場合は、参考リンクを参照してください。
前回の記事
バグの説明
GitBucket において、リポジトリトップの URL は、以下のとおりです。(Tomcat 等に Deploy している場合)
http://host:port/gitbucket/User/Repository
通常、リポジトリトップには、README.md の内容が表示されますが、同じ階層の test.md へのリンクを [アンカー](test.md)と書いた場合、そのまま相対リンクを利用するとリンククリック時に次の URL へ遷移し、 Not Found になってしまいます。
http://host:port/gitbucket/User/Repository/test.md
なぜなら、test.md を表示するためには、以下の URL を指定する必要があるからです。
http://host:port/gitbucket/User/Repository/blob/main/test.md
ところが、README.md 自体を blob モードで表示しているときは、URL が次のようになり、相対リンクが機能します。
http://host:port/gitbucket/User/Repository/blob/main/README.md
また、ブランチ branch を表示時は、リポジトリトップの URL は次のとおりです。
http://host:port/gitbucket/User/Repository/tree/branch
この時、blob モードで README.md を表示する URL は次のとおりです。
http://host:port/gitbucket/User/Repository/blob/branch/README.md
リンク URL をカスタマイズできないか検索してみた結果
次のページを見つけました。
com.vladsch.flexmark.html.LinkResolver インターフェースを継承し、resolveLink() メソッドを override し、com.vladsch.flexmark.html.HtmlRenderer.Builder で関連付けを行えば良いようです。
作成する LinkResolver にリクエスト時の情報を渡すための修正
既に作成している MarkdownEnhancedRenderer に修正を加えます。
flexmark-java にオプションを渡すための DataKey を準備します。
object MarkdownEnhancedRenderer {
val BASE_URL = new DataKey[String]("BASE_URL", "")
val CURRENT_PATH = new DataKey[String]("CURRENT_PATH", "")
}
toHtml() メソッドでのオプション設定のところで以下を追加しました。
options.set(MarkdownEnhancedRenderer.BASE_URL, context.baseUrl)
options.set(MarkdownEnhancedRenderer.CURRENT_PATH, context.currentPath)
http://host:port/gitbucket/User/Repository というリクエスト URL で処理されている場合、以下のような値が渡されます。
| DataKey | 渡している変数 | 値 |
|---|---|---|
| BASE_URL | context.baseUrl | http://host:port/gitbucket |
| CURRENT_PATH | context.currentPath | /User/Repository |
LinkResolver を継承した MarkdownEnhancedLinkResolver の作成
com.vladsch.flexmark.html.LinkResolver インターフェースを継承した MarkdownEnhancedLinkResolver を新規作成しました。
package io.github.yasumichi.gme
import com.vladsch.flexmark.html.LinkResolver
import com.vladsch.flexmark.html.LinkResolverFactory
import com.vladsch.flexmark.html.renderer.{LinkStatus, LinkResolverBasicContext, ResolvedLink}
import com.vladsch.flexmark.util.ast.Node
import java.{util => ju}
class MarkdownEnhancedLinkResolver extends LinkResolver {
override def resolveLink(node: Node, context: LinkResolverBasicContext, link: ResolvedLink): ResolvedLink = {
val url = link.getUrl()
if (url.contains("://")) {
link.withStatus(LinkStatus.VALID).withUrl(url)
}
else if (url.startsWith("/")) {
link.withStatus(LinkStatus.VALID).withUrl(url)
} else {
val baseUrl = MarkdownEnhancedRenderer.BASE_URL.get(context.getOptions())
val currentPath = MarkdownEnhancedRenderer.CURRENT_PATH.get(context.getOptions())
val pathElems = currentPath.split("/")
if (pathElems.length > 3 && pathElems(3).equals("blob")) {
link.withStatus(LinkStatus.VALID).withUrl(url)
} else if(pathElems.length > 3 && pathElems(3).equals("tree")) {
pathElems(3) = "blob"
link.withStatus(LinkStatus.VALID).withUrl(baseUrl + pathElems.mkString("/") + "/" + url)
} else {
link.withStatus(LinkStatus.VALID).withUrl(baseUrl + currentPath + "/blob/main/" + url)
}
}
}
}
resolveLink の処理は以下の通り。
- URL に
://が含まれる場合- 絶対 URL とみなして、そのまま URL を返す。
- それ以外の場合
- currentPath の第4階層が
blobの場合- そのまま相対パスを返す 。
- currentPath の第4階層が
treeの場合- 相対パスを blob アドレスに変換して返す。
- 上記以外の場合
- baseUrl、currentPath、
/blob/main/、相対パスを結合して返す。
- baseUrl、currentPath、
- currentPath の第4階層が
作成した MarkdownEnhancedLinkResolver を利用できるように Factory クラスも用意しておきます。
object MarkdownEnhancedLinkResolver {
class Factory() extends LinkResolverFactory {
override def getAfterDependents(): ju.Set[Class[_ <: Object]] = {null}
override def getBeforeDependents(): ju.Set[Class[_ <: Object]] = {null}
override def affectsGlobalScope(): Boolean = {false}
override def apply(context: LinkResolverBasicContext): LinkResolver = {
new MarkdownEnhancedLinkResolver()
}
}
}
MarkdownEnhancedExtention で作成した MarkdownEnhancedLinkResolver が使われるようにする
既に作成済みの MarkdownEnhancedExtention の extend() メソッドに以下を追加しました。
htmlRendererBuilder.linkResolverFactory((new MarkdownEnhancedLinkResolver.Factory()))
最終的なコミット
最終的なコミットは、次のリンクで確認できます。
fix process relative link · yasumichi/gitbucket-markdown-enhanced@33424bc
参考リンク
- Prepend image links with custom URL #8 - vsch/flexmark-java
- flexmark-java/flexmark-docx-converter/src/main/java/com/vladsch/flexmark/docx/converter/internal/DocxLinkResolver.java at master · vsch/flexmark-java
- flexmark-java/flexmark-java-samples/src/com/vladsch/flexmark/java/samples/CustomLinkResolverSample.java at bcfe84a3ab6d23d04adce3e5a0bae45c6b791d14 · vsch/flexmark-java
- gitbucket/src/main/scala/gitbucket/core/view/Markdown.scala at master · gitbucket/gitbucket (本家 GitBucket では、リンク URL の処理を GitBucketMarkedRenderer クラスの fixUrl メソッドで行っている。)
- 文字列の配列(リスト)を特定の文字列で連結するには (implode / join / mkString) | hydroculのメモ