0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

flexmark-java でリンク生成時に URL をカスタマイズする

Last updated at Posted at 2025-11-15

概要

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/、相対パスを結合して返す。

作成した 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

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?