我必须构建自己的 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}
}
我会将画布覆盖在图像上。然后我可以使用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})
})
}
SharpJS 可以将两个文件合成在一起,这就是我保存图像的方式。
sharp(opts.source).composite([{input: opts.overlay}]).toFile(new_asset)
我设置了全局热键并只在按键时观察。
# 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)
所有数据都存储在单例中。没有响应式的废话。
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()