script.js$(() => {
$.getScript("https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.14.0/mermaid.min.js")
.done((script, textStatus) => {
mermaid.mermaidAPI.initialize({ startOnLoad: false })
const mermaidViewer = new MermaidViewer()
mermaidViewer.onScrapboxPageChanged()
scrapbox.on("page:changed", () => mermaidViewer.onScrapboxPageChanged())
scrapbox.on("lines:changed", () => mermaidViewer.onScrapboxLinesChanged())
})
.fail((jqxhr, settings, exception) => {
console.error(exception)
})
const MermaidViewer = function() {
const DEFAULT_SHOW_CODE = false
this.recentMermaidCodes = new Map()
this.codeViewStatusRepository = new MermaidCodeViewStatusRepository()
this.onScrapboxLinesChanged = function() {
if (scrapbox.Page.lines) {
this.updateDiagrams()
}
}
this.onScrapboxPageChanged = function() {
if (scrapbox.Page.lines) {
this.updateDiagrams()
this.setAllCodeViewStatus(DEFAULT_SHOW_CODE)
}
}
// すべてのコードブロックの表示ステータスを変更
// 引数: value 表示ステータス (true|false)
this.setAllCodeViewStatus = function(value) {
for (const [id, code] of this.recentMermaidCodes) {
code.setCodeViewStatus(value)
}
}
// 変更があればダイアグラムを更新
this.updateDiagrams = function() {
const newCodes = this.findMermaidCodes()
const diff = MermaidViewerUtils.diffMermaidCodes(this.recentMermaidCodes, newCodes)
for (const item of diff) {
if (item.op === "delete") {
item.code.deleteDiagram()
} else {
item.code.updateDiagram()
}
}
this.recentMermaidCodes = newCodes
}
// mermaidコードをページ内から検索
// 戻り値: Map型
// キー: コードブロックのID(最初の行ID)
// 値: MermaidCode
this.findMermaidCodes = function() {
const result = new Map()
var text, filename, id, lastLineId, lineIds
for (const line of scrapbox.Page.lines) {
if (line.codeBlock && line.codeBlock.lang === "mermaid") {
if (line.codeBlock.start) {
text = ""
id = line.id
lineIds = new Set()
} else {
text += "\n" + line.text
}
lineIds.add(line.id)
if (line.codeBlock.end) {
lastLineId = line.id
text = text.trim()
result.set(id, new MermaidCode(id, text, lastLineId, lineIds, this.codeViewStatusRepository))
}
}
}
return result
}
}
const MermaidCode = function(id, text, lastLineId, lineIds, codeViewStatusRepository) {
const MERMAID_SVG_ID_PREFIX = "mermaid-"
this.id = id
this.text = text
this.lastLineId = lastLineId
this.lineIds = lineIds
this.codeViewStatusRepository = codeViewStatusRepository
this.svgId = MERMAID_SVG_ID_PREFIX + id
// mermaidダイアグラムを更新
this.updateDiagram = function() {
try {
const svg = mermaid.mermaidAPI.render(this.svgId, this.text)
$("#" + this.svgId).remove()
$("#L" + this.lastLineId).after(svg)
} catch (e) {
console.error(e)
$("#L" + this.lastLineId).after($("#" + this.svgId))
}
$("#" + this.svgId)
.on("click", () => this.onSvgClicked())
.css("cursor","pointer")
}
// mermaidダイアグラムを削除
this.deleteDiagram = function() {
$("#" + this.svgId).remove()
}
// mermaidダイアグラム(SVG)がクリックされたときのイベントハンドラ
// コードブロックの表示ステータスを変更
this.onSvgClicked = function() {
this.codeViewStatusRepository.changeStatus(this.id)
this.applyCodeView()
}
// コードブロックの表示ステータスを適用
this.applyCodeView = function() {
const status = this.codeViewStatusRepository.getStatus(this.id)
for (const lineId of this.lineIds) {
if (status) {
$("#L" + lineId).show(100)
} else {
$("#L" + lineId).hide(100)
}
}
}
// コードブロックの表示ステータスを変更
this.setCodeViewStatus = function(value) {
this.codeViewStatusRepository.setStatus(this.id, value)
this.applyCodeView()
}
}
const MermaidCodeViewStatusRepository = function() {
this.status = new Map()
this.defaultValue = true
this.changeStatus = function(id) {
const old = this.status.has(id) ? this.status.get(id) : this.defaultValue
this.status.set(id, !old)
}
this.getStatus = function(id) {
return this.status.has(id) ? this.status.get(id) : this.defaultValue
}
this.setStatus = function(id, value) {
this.status.set(id, value)
}
}
const MermaidViewerUtils = {}
// 2つのMap型に格納されたコードの差分を返す
// 引数: oldMap 古い値(Map型)
// 引数: newMap 新しい値(Map型)
MermaidViewerUtils.diffMermaidCodes = function(oldMap, newMap) {
const result = []
const intersection = new Set()
for (const [key, val] of newMap) {
if (!oldMap.has(key)) {
result.push({ op: "new", key: key, code: newMap.get(key) })
} else {
intersection.add(key)
}
}
for (const [key, val] of oldMap) {
if (!newMap.has(key)) {
result.push({ op: "delete", key: key, code: oldMap.get(key) })
intersection.delete(key)
}
}
for (const key of intersection) {
const oldVal = oldMap.get(key)
const newVal = newMap.get(key)
if (oldVal.text !== newVal.text) {
result.push({ op: "changed", key: key, code: newMap.get(key) })
}
}
return result;
}
})