8

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.

6
  • Did you get any answer? Commented Aug 28, 2022 at 16:35
  • @MohanKrishnaSai No. Commented Aug 31, 2022 at 23:25
  • 1
    Are you flexible to any solution or must using slate.js? Commented Sep 2, 2022 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. Commented Sep 3, 2022 at 4: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. Commented Sep 3, 2022 at 8:55

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

4 Comments

For your recommendation, how to hide and show the asterisks as in my question's image?
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.
to your question: both editors can handle images or pretty much anything else when extended properly stackoverflow.com/questions/59239118/…
to your question: if you select a specific word instead of hover, you can get the value like this github.com/ianstormtaylor/slate/issues/551. from there you can get the blocks property (as demonstrated here docs.slatejs.org/v/v0.47/walkthroughs/…) and apply your logic (such as - append value with proper markdown characters)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.