可编辑的浮窗与可导出的 Markdown Mindmap 导图 - 开发讨论 - Obsidian 中文论坛

插件是如何适配 Obsidian 的 PDF 导出功能的:

1. 主要实现逻辑

mmBlock.js 中可以看到关键实现:

module.exports = (plg, ob)=> {
  const md2htmlText = require('./getText/md2htmlText.js')(app, ob)
  const { genMM } = require('./genMM.js')(plg.app, ob)
  const mmBlock = async (source, el, ctx)=> {
    const fmRgx = new RegExp(String.raw`---\nmarkmap:\n  height: (\d+)\n---\n`, '')
    let height = 400
    const md = source.replace(fmRgx, (m, p1)=> { height = p1; return '' })
    el.style.height = `${height}px`
    const text = await md2htmlText(md, ctx.sourcePath)
    if (ctx.el.parentNode?.className == 'print') {
      await genMM(el, text, ctx.sourcePath, height)
      el.style.height = 'fit-content'
    }
    else setTimeout(async ()=> {
      await genMM(el, text, ctx.sourcePath)
    }, 100)
  }
  plg.registerMarkdownCodeBlockProcessor('markmap', mmBlock)
}

主要步骤:

  1. 检测打印环境:
if (ctx.el.parentNode?.className == 'print') {

通过检查父节点的 className 是否为 ‘print’ 来判断是否处于 PDF 导出环境

  1. 特殊处理导出场景:
await genMM(el, text, ctx.sourcePath, height)
el.style.height = 'fit-content'

2. 导出图片处理

genMM.js 中:

const genMM = async (wrapper, htmlText, sourcePath, printHeight)=> {
  wrapper.empty()
  const svg = wrapper.createSvg('svg')
  const lib = new Transformer(), { root } = lib.transform(htmlText)
  const mm = Markmap.create(svg, mmJson.opts, root)
  funcBtns(svg, sourcePath); await mm.fit()
  if (printHeight) {
    await mm.fit()
    // seems markmap@0.18 requires calling fit() again before exporting a PDF
    svg.replaceWith(await svg2img(svg))
  }
  else wrapper.append(customBar(mm))
}

当检测到是打印环境时(printHeight 参数存在):

  1. 再次调用 mm.fit() 确保思维导图尺寸正确
  2. 将 SVG 转换为图片并替换原 SVG 元素

3. SVG 转图片处理

svg2img.js 中:

const svg2img = async (svg)=> {
  // 计算尺寸和缩放比例
  const { width: w1, height: h1 } = svg.getBoundingClientRect()
  const { width: w2, height: h2, x, y } = svg.getBBox()
  , scale = 2 * 1e-2 * Math.max(w2, h2)
  , ratio = h2 / w2
  let width, height
  
  // 保持宽高比
  if (h1 / w1 < ratio) {
    height = h1 * scale
    width = height / ratio
  } else {
    width = w1 * scale
    height = width * ratio
  }
  
  // 添加边距
  const margin = h2 / 75
 
  // 克隆 SVG 并添加样式
  const svgClone = svg.cloneNode(!0)
  svgClone.prepend(Object.assign(
    document.createElement('style'), 
    {textContent: rule}
  ))
  
  // 处理嵌入图片
  await encodeImg2B64(svgClone)
 
  // 转换为 base64 图片
  const svgStr = unescape(encodeURI(
    new XMLSerializer().serializeToString(svgClone)
  ))
  const img = await b64b.loadNewImg(
    `data:image/svg+xml;base64,${btoa(svgStr)}`
  )
  const b64 = b64b.getByDraw(
    width, height, scale, img, 
    [-x + margin, -y + margin]
  )
  img.src = b64
  return img
}

主要步骤:

  1. 计算适当的尺寸和缩放比例
  2. 克隆 SVG 并添加必要的样式
  3. 处理嵌入的图片(转为 base64)
  4. 将 SVG 转换为 base64 格式的图片
  5. 创建新的图片元素并返回

4. 嵌入图片处理

const encodeImg2B64 = async (svg)=> {
  const imgs = svg.querySelectorAll('img')
  for (const img of imgs) {
    const b64 = await b64b.getByUrl(img.src)
    if (img.src.startsWith('blob:')) {
      const newImg = await b64b.loadNewImg(b64)
      newImg.style.cssText = img.style.cssText
      newImg.style.width = '100%'
      img.replaceWith(newImg)
    }
    else img.src = b64
  }
}

确保所有嵌入的图片都被转换为 base64 格式,以便在 PDF 中正确显示。

5. 样式处理

module.exports = `
svg.markmap {
  --markmap-text-color: #222;
  --markmap-highlight-bg: rgba(255, 208, 0, 0.4);
  background-color: #fff;
}
.markmap-foreign.markmap-foreign {
  & code {white-space: pre-wrap;}
  & .copy-code-button {display: none;}
}
`

为导出的 PDF 设置特定的样式,确保在打印时有良好的显示效果。

这样的设计确保了:

  1. 思维导图可以正确地导出到 PDF
  2. 保持了图片质量和清晰度
  3. 维持了正确的宽高比
  4. 处理了嵌入图片的兼容性
  5. 提供了适合打印的样式

解决translate(NaN,NaN)

可参考codeblock render blank · Issue #4 · PlayerMiller109/obsidian-markmap-fileviews · GitHub

Ob 的工作方式:以阅读模式打开时,不是在切换时才渲染编辑模式,而是与阅读模式同时。

如何得出这个判断:阅读模式的 ctxpromises[0];编辑模式相反。

此时编辑模式的 div 状态为 display: none

→ 初始化 markmap 时容器不可见

→ 容器的 width/height 被计算为 0,坐标计算为 NaN

When opened in reading mode, the editing mode is rendered simultaneously with the reading mode, rather than when switching to.

How to arrive at this judgment: In reading mode, the ctx has promises[0]; vice versa.

At this time, the state of the editing mode div is display: none.

→ The container is invisible when initializing the markmap.

→ The width/height of the container is calculated as 0, and the coordinates are calculated as NaN.

初步已解决:不再报错。

Preliminarily solved: Do not call mm.fit() for the editing mode when you open in reading mode. No longer reports an error.

进一步方案:找到切换模式的检测。

Further solution: Find the detection for mode switching.

  • 目前 Ob 没有提供直接的方法实现。no direct method currently
  • 急用可以考虑定时器监测。can use a timer for monitoring if urgent
  • 我倾向于等待寻找更合适的方法。I tend to wait and look for a more suitable method.