我必须构建自己的 Markdown 编辑器,因为对我来说没有哪个工具足够快。

2025-05-27

我必须构建自己的 Markdown 编辑器,因为对我来说没有哪个工具足够快。

TL;DR

我创建了一个名为Fast Author的开源Markdown 编辑器,以提高我创建涉及大量屏幕截图的技术教程文章时的效率。

👉 https://github.com/ExamProCo/fast-author

替代文本

PS 我在这个编辑器中写了这篇文章。

痛点

我一直在编写 AWS 认证开发人员课程的新版本,并在 freeCodeCamp 上免费发布该课程,并且我已经完成了所有讲座视频的录制,只剩下后续内容

跟随练习(有些人可能称之为实验室)是我制作的视频,您可以跟随我获得 AWS 的实践技能。

然而,制作这些视频一直是我制作的瓶颈,因为我必须在过程中发现问题并回溯,这可能导致我重新录制 3-4 个视频片段。

因此,对我来说,以更容易修改的书面形式对它们进行指导是有意义的。

无论如何我都必须创建书面版本,因为在我的付费平台上,我们提供书面版本作为免费观看视频的补充。

我在这里感受到了巨大的痛苦,因为开发助理需要大量的实践,这些后续工作比任何其他认证都需要更多的关注和复杂性。

现有编辑器和我的用例

市面上有很多 Markdown 编辑器,但没有一个是为高级用户设计的,也没有一个针对我的用例进行优化的,它们是:

我用什么来构建它?花了多长时间?

我花了3天时间就完成了构建。2天开发,1天与我的联合创始人Bayko一起进行QA。今天我正在按照预期用途使用它,我已经知道我的生产力将提高500%。

替代文本

电子

我已经制作了一款名为 Swap-N-Pop 的开源视频游戏,因此这就像回顾我之前所做的事情一样简单。

Coffeescript

如果我有多个合作者,Typescript 会是更好的选择,但我想尽快完成这项工作,而 Coffeescript 的速度无与伦比。

这与 Swap-N-Pop 的路径相同,当我需要测试代码和更多协作时,我将其从 Coffeescript 转换为 Typescript。

MithrilJS

我本来打算使用 Svelte,但我想完成这个,所以我只是回过头去利用 Mithril,在那里我已经解决了很多 javascript *难题,而且我不想再花两天时间进行开发。

SharpJS

我不喜欢使用 ImageMagick,所以选择了安装起来更容易的 SharpJS。但是,使用 Electron 时,我们遇到了不少麻烦。我必须找到 Electron 和 SharpJS 的正确版本。

编辑

  • 编辑器应该使用等宽字体来轻松对齐将在代码元素中呈现的文本。
  • 应该快速切换到发布者预览模式
  • 设计应针对并排预览进行优化
  • 需要热键来为 Highting、underling 和将文本标记为红色自定义标签。

图像

  • 应该能够将图像拖入编辑器
  • 应该能够快速编辑图像以调整大小、裁剪、添加边框以及绘制矩形和标记
  • 应将原始图像存储在项目中以供将来参考或修改

预览和导出

  • 应该能够加载自定义 css 以供发布者预览,这样我就可以看到它在 DEV、Medium、freeCodeCamp、HashNode 等平台上的效果
  • 应按出现的顺序重命名文件,因为它们在导出时被移动

新增奖励

由于这是一个 Electron 应用程序,我应该能够添加我的 Grammarly 扩展来更好地提高我的写作水平。

分散注意力或拖延的项目?

到目前为止,绕道还是值得的。如果我去另一家公司工作,然后提出可以尝试在几天内开发一个工具,以节省几周的时间,他们可能不会让我这么做,因为大多数人会认为这会分散我的注意力。

我本可以在创建这个课程的几天内完成我的课程,但是人们很容易只关注短期目标,而从长远来看,知道何时投入时间是一项需要大量尝试才能克服的技能。

有趣的代码

我认为我提取了一些有趣的代码:

我借用了在线函数来获取 Canvas 的相对坐标。



function relMouseCoords(event){
    var totalOffsetX = 0;
    var totalOffsetY = 0;
    var canvasX = 0;
    var canvasY = 0;
    var currentElement = this;

    do{
        totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft;
        totalOffsetY += currentElement.offsetTop - currentElement.scrollTop;
    }
    while(currentElement = currentElement.offsetParent)

    canvasX = event.pageX - totalOffsetX;
    canvasY = event.pageY - totalOffsetY;

    return {x:canvasX, y:canvasY}
}


Enter fullscreen mode Exit fullscreen mode

我会将画布覆盖在图像上。然后我可以使用toDataURL()和 替换字符串的开头来将画布捕获为图像replace(/^data:image\/png;base64,/, "")



function save(){
  console.log('saving')
  let path = "/tmp/save-drawing-overlay.png"
  const el = document.getElementById('draw')
  fs.writeFile(path, el.toDataURL().replace(/^data:image\/png;base64,/, ""), 'base64', function(err){
    console.log(err)
    ipc.send('sharp-draw',{overlay: path, source: asset.path})
  })
}


Enter fullscreen mode Exit fullscreen mode

SharpJS 可以将两个文件合成在一起,这就是我保存图像的方式。



sharp(opts.source).composite([{input: opts.overlay}]).toFile(new_asset)


Enter fullscreen mode Exit fullscreen mode

我设置了全局热键并只在按键时观察。



# global hotkeys
document.addEventListener 'keydown', (e)=>
  meta =
  if os.platform() is 'darwin'
    'Meta'
  else
    'Control'
  Data.meta(true)  if e.key is meta
  Data.shift(true) if e.key is 'Shift'
  if Data.meta()
    if e.key is 'f'
      ipc.send('toggle-fullscreen')
    else if e.key is 'p'
      Data.publisher_preview !Data.publisher_preview()
      m.redraw(true)
    else if e.key is 'n'
      ipc.send('prompt-new')
    else if e.key is 's' && Data.shift()
      Data.splitview !Data.splitview()
      m.redraw(true)
    else if e.key is 'w' && Data.shift()
      Data.line_wrap !Data.line_wrap()
      m.redraw(true)
document.addEventListener 'keyup', (e)=>
  Data.meta(false)
  Data.shift(false)


Enter fullscreen mode Exit fullscreen mode

所有数据都存储在单例中。没有响应式的废话。



import stream from 'mithril/stream'

class Data
  constructor:->
    # The root directory where all the markdown files are stored
    # eg. ~/fast-author/
    @home = stream('')

    # When the current file was last saved
    @last_saved = stream('')

    # the file that shows selecte in the right hand column
    @active_file = stream(null)

    # files that appear in the right hand column
    @files  = stream([])

    # assets that appear in the right hand column
    # assets only for the current markdown file that is active
    @assets  = stream([])

    # The currently selected image in the markdown to apply editing
    @active_asset = stream null

    # the contents of the markdown file
    @document = stream('')

    # whether the meta key is being held eg. Command on Mac
    @meta = stream(false)

    # whether the shift key is behind held
    @shift = stream(false)

    # whether to wrap or not wrap lines in textarea
    @line_wrap = stream(false)
    #
    # whether to split the view (show both editor or preview, or just editor)
    @splitview = stream(true)

    # when true will hide editor and center preview.
    @publisher_preview = stream(false)

    # the start and end select for markdown textarea
    @selectionStart = stream false
    @selectionEnd   = stream false

    # current selections for infobar
    @_selectionStart = stream 0
    @_selectionEnd   = stream 0
  markdown_path:(name)=>
    path = "#{@home()}/#{name}/index.md"
    console.log path
    path
  # select can be loss after certain updates to textarea.
  # This ensures our old selection remains
  keep_selection:=>
    @selectionStart @_selectionStart()
    @selectionEnd @_selectionEnd()
  get_asset:=>
    asset = null
    for a in @assets()
      if a.path is @active_asset().replace('file://','')
        asset = a
        break
    asset
export default new Data()



Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/exampro/i-had-to-build-my-own-markdown-editor-because-no-tool-was-fast-enough-for-me-3b3o
PREV
Laravel Greatest Trick Revealed: Magic Methods What is a magic method ? How Laravel uses magic methods
NEXT
免费 3 小时 Azure Fundamentals (AZ-900) 认证课程(100 多个视频!)😱