6

I'm writing a markdown text editor using slate.js. I'm trying to implement the following live-rendering effect (from Typora):

Live Markdown rendering

As you can see,

  1. When I'm typing, the text is turning to bold automatically.
  2. When I hit the space key, the four asterisks disappeared, only the text itself is visible.
  3. When I focus the cursor back to the text, the asterisks shows up again (so I can modify them).

I've already implemented the first item thanks to the example of MarkdownPreview, here is the code of it (take from the slate repository):

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

My question is, how can I implement the second and third items? I've been thinking about it for a long time, but didn't find any good way to achieve them.

Searene
  • 25,920
  • 39
  • 129
  • 186
  • Did you get any answer? – Mohan Krishna Sai Aug 28 '22 at 16:35
  • @MohanKrishnaSai No. – Searene Aug 31 '22 at 23:25
  • 1
    Are you flexible to any solution or must using slate.js? – Atikur Rabbi Sep 02 '22 at 19:02
  • So the question is basically "can someone implement this for me for free?". You really need to put in some work yourself or at least show that you've made an attempt. And no, copy-pasting that example from github does not count. You can come back when you have a more specific question. – zoran404 Sep 03 '22 at 04:59
  • @AtikurRabbi Currently I'm using `slate.js` as my text editor's framework because it's flexible and powerful. But if you have better solutions to implement a real-time text editor like Typora or obsidian, I'm happy to know. – Searene Sep 03 '22 at 08:55
  • @Searene Did you manage to pull this off? – kyw Apr 19 '23 at 09:19

1 Answers1

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:

Using slate.js commands (read this doc for the general concept + useful examples). This approach will let slate handle the load but requires you to deep-dive into that project as well. Note that you can register custom commands and queries to suit your need without breaking your work pipeline (read this)

The insane way:

Try to override slate by using custom events on-top of rendered elements. This can be tricky but achievable if you love to play with projects internals and inject values on the fly. Not very recommended though

My Recommendation

Create a custom command that will apply the markdown style you want (for instance: bold)

Use slate handler for tracking the space key hit and use your command

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()
  }
}

I know - this example will apply bold style to all text but - if you combine it with a range selection you will get the style you need for the relevant area in you editor (again - docs for the rescue)

From this point - applying a specific selection via key press or adding the markdown characters by clicking it (using an event + query + command) is only a matter of time

ymz
  • 6,602
  • 1
  • 20
  • 39
  • For your recommendation, how to hide and show the asterisks as in my question's image? – Searene Sep 03 '22 at 09:03
  • Thanks for recommending Monaco Editor, I didn't know it before. It's a good candidate for a code editor, but is it a good candidate for a real-time text editor? In the text editor, I want to show the header with a larger font size, I even want to display images and tables as well, VSCode doesn't seem to be able to do it, I doubt if Monaco Editor can. – Searene Sep 03 '22 at 09:06
  • to your question: both editors can handle images or pretty much anything else when extended properly https://stackoverflow.com/questions/59239118/display-an-image-in-monaco-editor-hoverprovider – ymz Sep 03 '22 at 09:36
  • **to your question:** if you select a specific word instead of hover, you can get the value like this https://github.com/ianstormtaylor/slate/issues/551. from there you can get the `blocks` property (as demonstrated here https://docs.slatejs.org/v/v0.47/walkthroughs/applying-custom-formatting) and apply your logic (such as - append value with proper markdown characters) – ymz Sep 03 '22 at 09:45