使用 slate.js 实现实时 Markdown 渲染

Sea*_*ene 6 javascript markdown reactjs slatejs

我正在使用slate.js编写一个 Markdown 文本编辑器。我正在尝试实现以下实时渲染效果(来自Typora):

实时 Markdown 渲染

如你看到的,

  1. 当我打字时,文本会自动变为粗体。
  2. 当我按空格键时,四个星号消失,只有文本本身可见。
  3. 当我将光标重新聚焦到文本上时,星号再次出现(这样我就可以修改它们)。

感谢MarkdownPreview的示例,我已经实现了第一项,这是它的代码(取自slate 存储库):

import Prism from 'prismjs'
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor, Descendant } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'

// eslint-disable-next-line
;Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore

const MarkdownPreviewExample = () => {
  const renderLeaf = useCallback(props => <Leaf {...props} />, [])
  const editor = useMemo(() => withHistory(withReact(createEditor())), [])
  const decorate = useCallback(([node, path]) => {
    const ranges = []

    if (!Text.isText(node)) {
      return ranges
    }

    const getLength = token => {
      if (typeof token === 'string') {
        return token.length
      } else if (typeof token.content === 'string') {
        return token.content.length
      } else {
        return token.content.reduce((l, t) => l + getLength(t), 0)
      }
    }

    const tokens = Prism.tokenize(node.text, Prism.languages.markdown)
    let start = 0

    for (const token of tokens) {
      const length = getLength(token)
      const end = start + length

      if (typeof token !== 'string') {
        ranges.push({
          [token.type]: true,
          anchor: { path, offset: start },
          focus: { path, offset: end },
        })
      }

      start = end
    }

    return ranges
  }, [])

  return (
    <Slate editor={editor} value={initialValue}>
      <Editable
        decorate={decorate}
        renderLeaf={renderLeaf}
        placeholder="Write some markdown..."
      />
    </Slate>
  )
}

const Leaf = ({ attributes, children, leaf }) => {
  return (
    <span
      {...attributes}
      className={css`
        font-weight: ${leaf.bold && 'bold'};
        font-style: ${leaf.italic && 'italic'};
        text-decoration: ${leaf.underlined && 'underline'};
        ${leaf.title &&
          css`
            display: inline-block;
            font-weight: bold;
            font-size: 20px;
            margin: 20px 0 10px 0;
          `}
        ${leaf.list &&
          css`
            padding-left: 10px;
            font-size: 20px;
            line-height: 10px;
          `}
        ${leaf.hr &&
          css`
            display: block;
            text-align: center;
            border-bottom: 2px solid #ddd;
          `}
        ${leaf.blockquote &&
          css`
            display: inline-block;
            border-left: 2px solid #ddd;
            padding-left: 10px;
            color: #aaa;
            font-style: italic;
          `}
        ${leaf.code &&
          css`
            font-family: monospace;
            background-color: #eee;
            padding: 3px;
          `}
      `}
    >
      {children}
    </span>
  )
}

const initialValue: Descendant[] = [
  {
    type: 'paragraph',
    children: [
      {
        text:
          'Slate is flexible enough to add **decorations** that can format text based on its content. For example, this editor has **Markdown** preview decorations on it, to make it _dead_ simple to make an editor with built-in Markdown previewing.',
      },
    ],
  },
  {
    type: 'paragraph',
    children: [{ text: '## Try it out!' }],
  },
  {
    type: 'paragraph',
    children: [{ text: 'Try it out for yourself!' }],
  },
]

export default MarkdownPreviewExample
Run Code Online (Sandbox Code Playgroud)

我的问题是,第二项和第三项如何实现?我想了很久,却没有找到什么好的方法来实现。

ymz*_*ymz 0

The good news:

  • The original text is saved unharmed (somewhere in your application)
  • Slate.js is rendering a new element for each style. thanks to the live demonstration link you've posted it is clear that the DOM is saving the state of your markdown (which suggest that we can add manipulations on-top of slate if required)

The bad news:

  • Implementing an editor is a headache. Using an existing one is not always as intuitive and easy as we expected. That's why I love the Monaco editor project so much (a free editable editor that gives me the experience of VS code for free? yes please!)

What we can do?

the easy way:

Use Monaco editor in our project. From my experience - it is better in terms of coding and formatting technical texts (and maybe this is the engine behind VS code)

The not-so-easy way:

We can make a simpler implementation - a panel with the raw text and another panel for preview (by the way - this is exactly what happens when you edit markdown files in VS code anyways). That way we can always render the view to reflect our most recent changes. For starters - you can use this project to do it and add tweaks to it so suit your demands

The hard way:

使用slate.js commands(阅读本文档以了解一般概念+有用的示例)。这种方法将让 slate 处理负载,但也需要您深入研究该项目。请注意,您可以注册自定义命令和查询以满足您的需要,而不会破坏您的工作管道(阅读此内容

疯狂的方式

尝试通过在渲染元素之上使用自定义事件来覆盖 slate。如果您喜欢研究项目内部并动态注入值,这可能很棘手,但可以实现。虽然不是很推荐

我的推荐

创建一个自定义命令来应用markdown style您想要的(例如:bold

使用 slate handler 来跟踪space击键并使用您的命令

function onKeyDown(event, editor, next) {
  if (event.key == 'Enter') {
    // TODO: add markdown style 
    editor.applyMarkdownBold() // applyMarkdownBold is a made-up name for your custom command
  } else {
    return next()
  }
}
Run Code Online (Sandbox Code Playgroud)

我知道 - 这个示例会将粗体样式应用于所有文本,但是 - 如果您将其与 a 结合使用,range selection您将获得编辑器中相关区域所需的样式(再次 -救援文档)

从这一点开始 - 通过按键应用特定选择或通过单击添加 Markdown 字符(使用事件 + 查询 + 命令)只是时间问题