概要
GitBucket用のプラグイン GitBucket Markdown Enhanced Pluginを開発しています。
GitBucket Markdown Enhanced Plugin は、GitBucket 標準のマークダウンレンダリングエンジンを置き換えるプラグインです。
目標は、Visual Studio Code の Markdown Preview Enhanced 向けの markdown ファイルを軽易に Web で共有できる環境です。
これまで数式の描画は、flexmark-java の GitLab Flavoured Markdown という拡張機能により、実現していました。
ところが、この拡張機能の記法は、Markdown Preview Enhanced が採用している記法と異なっていました。今回は、これまでの記法に加え、Markdown Preview Enhanced が採用している記法に対応した際の記録です。
前回の記事
GitLab Flavoured Markdown と Markdown Preview Enhanced での数式記法の違い
GitLab Flavoured Markdown の記法
-
$``$($`と`$の間に KaTeX の記法で書く) -
```mathで始まるコードブロック
- インライン記法
$...$\(...\)
- ブロック記法
$$...$$\[...\]-
```mathで始まるコードブロック
```math で始まるコードブロックは GitLab Flavoured Markdown でも対応していますが、その他は対応してません。
Markdown Preview Enhanced の記法に対応したコミット
KaTeX syntax adapted to MPE · yasumichi/gitbucket-markdown-enhanced@623d786
別記事に書いていますが、このコードには問題があったため、最終的なコードとは異なっていることをご了承ください。
AST 用ノードの作成
src/main/scala/io/github/yasumichi/gme/InlineKaTeX.scala で AST1 用のノードを作成しました。
package io.github.yasumichi.gme
import com.vladsch.flexmark.util.ast.Node
import com.vladsch.flexmark.util.sequence.BasedSequence
/**
* InlineKatex class
*
* AST node that holds the inline KaTeX syntax of flexmark-java.
*
* `$...$` is represented as InlineKatex node.
* An instance is created by the InlineKatexInlineParserExtension class.
*
* @param text
* @param source
*/
class InlineKatex(val text: BasedSequence, val source: BasedSequence) extends Node {
override def getSegments: Array[BasedSequence] = Array(source)
}
インラインパーサーの作成
次に上で作成したノードに格納する部分を担うインラインパーサーを作成しました。
src/main/scala/io/github/yasumichi/gme/InlineKatexInlineParserExtension.scala
package io.github.yasumichi.gme
import com.vladsch.flexmark.util.ast.Node
import com.vladsch.flexmark.util.sequence.BasedSequence
import com.vladsch.flexmark.parser.{
InlineParser,
InlineParserExtension,
InlineParserExtensionFactory,
LightInlineParser
}
import org.slf4j.LoggerFactory
/**
* InlineKatexInlineParserExtension class
*
* Inline parser extension that parses the inline KaTeX syntax.
*
* It recognizes the following patterns:
* - `\[...\]`
* - `$$...$$`
* - `$...$`
* - `\(...\)`
*/
class InlineKatexInlineParserExtension extends InlineParserExtension {
private val logger = LoggerFactory.getLogger(classOf[InlineKatexInlineParserExtension])
override def finalizeDocument(inlineParser: InlineParser): Unit = {}
override def finalizeBlock(inlineParser: InlineParser): Unit = {}
override def parse(inlineParser: LightInlineParser): Boolean = {
val patterns = List("""\\\[(.+?)\\\]""", """\$\$(.+?)\$\$""", """\$(.+?)\$""", """\\\((.+?)\\\)""")
logger.debug("Input: " + inlineParser.getInput().toString())
for (patternText <- patterns) {
logger.debug(s"Trying pattern: $patternText")
val matches = inlineParser.matchWithGroups(java.util.regex.Pattern.compile(patternText))
if (matches != null) {
logger.debug(s"Matched pattern: $patternText with content: ${matches(1)}")
inlineParser.flushTextNode()
val katexText = matches(1)
inlineParser.getBlock.appendChild(new InlineKatex(katexText, matches(0)))
return true
}
}
false
}
}
object InlineKatexInlineParserExtension {
class Factory() extends InlineParserExtensionFactory {
override def getCharacters: CharSequence = "$\\"
override def apply(inlineParser: LightInlineParser): InlineParserExtension = new InlineKatexInlineParserExtension()
override def getAfterDependents: java.util.Set[Class[_]] = null
override def getBeforeDependents: java.util.Set[Class[_]] = null
override def affectsGlobalScope(): Boolean = false
}
}
インラインパーサーを拡張機能に組み込む
上記で作成したインラインパーサーを拡張機能に組み込みます。関連部分のみ抜粋します。
src/main/scala/io/github/yasumichi/gme/MarkdownEnhancedExtention.scala
class MarkdownEnhancedExtention extends ParserExtension with HtmlRendererExtension {
/**
* Extend the parser with custom inline parser extension
*
* @param parserBuilder The parser builder to extend
*/
override def extend(parserBuilder: Parser.Builder): Unit = {
parserBuilder.customInlineParserExtensionFactory(new MarkInlineParserExtension.Factory())
parserBuilder.customInlineParserExtensionFactory(new InlineUriInlineParserExtension.Factory())
parserBuilder.customInlineParserExtensionFactory(new InlineKatexInlineParserExtension.Factory()) // ここを追加
}
InlineKatex ノードを HTML に変換する部分
InlineKatex ノードを HTML に変換する部分を実装します。
src/main/scala/io/github/yasumichi/gme/MarkdownEnhancedNodeRenderer.scala
まず、既存の MarkdownEnhancedNodeRenderer で InlineKatex ノードを処理する意思表示と処理するメソッドを登録します。
override def getNodeRenderingHandlers(): util.Set[NodeRenderingHandler[_ <: Object]] = {
(中略)
set.add(
new NodeRenderingHandler[InlineKatex](
classOf[InlineKatex],
this.renderInlineKatex
)
)
実際の処理を行う renderInlineKatex メソッドを実装します。
private def renderInlineKatex(
node: InlineKatex,
context: NodeRendererContext,
html: HtmlWriter
): Unit = {
if (node.source.startsWith("$$") || node.source.startsWith("\\[")) {
// Display math
html
.withAttr()
.attr("class", "katex")
.tag("div")
html.text(node.text.toString())
html.tag("/div")
} else {
// Inline math
html
.withAttr()
.attr("class", "katex")
.tag("span")
html.text(node.text.toString())
html.tag("/span")
}
}
参考リンク
- 新Markdown導入の背景とその実装について | 株式会社ヌーラボ(Nulab inc.)
- Math Typesetting Markdown Preview Enhanced のマニュアル
- 抽象構文木 - Wikipedia
- 構文解析 - 抽象構文木(AST)を理解する|Goで作る静的解析ツール開発入門
- たった1行から始めるPythonのAST(抽象構文木)入門 #Python - Qiita
- ast --- Abstract syntax trees — Python 3.14.2 ドキュメント
- 「コンパイラ実習」2025 年度課題 © 関西学院大学