simian 0.1.1

A command-line tool for exploring and implementing Machine Learning algorithms in Rust.
Documentation
import { nanoid } from 'nanoid'
import { Editor, Element, Transforms } from 'slate'

import { isInsideElement } from '@/ui/editor/utils'

import { elementAddon } from '../base'
import { isInsideMark } from '../text/utils'

import { HeadingContext, HeadingContextValue } from './context'
import { Heading } from './element'
import { slashCommands } from './slash'
import { HeadingAddon } from './types'

/**
 * The render function.
 */
const render: HeadingAddon['render'] = ({ element, ...props }) => {
  if (element.type == 'heading') {
    return <Heading {...props} element={element} />
  }

  return null
}

/**
 * The context provider component.
 */
const ContextProvider: HeadingAddon['ContextProvider'] = ({
  addon,
  children,
}) => (
  <HeadingContext.Provider
    value={{
      emptyPlaceholder: addon.emptyPlaceholder,
    }}
  >
    {children}
  </HeadingContext.Provider>
)

/**
 * Handle insert break to create a paragraph if possible.
 */
const insertBreak: HeadingAddon['insertBreak'] = ({
  editor,
  insertBreak,
  selection,
}) => {
  if (selection && editor.hasAddon('paragraph')) {
    const entry = Editor.above(editor, {
      match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
    })

    if (entry) {
      const [block, path] = entry

      if (Element.isElement(block) && block.type === 'heading') {
        const isAtEnd = Editor.isEnd(editor, selection.anchor, path)

        if (isAtEnd) {
          Transforms.insertNodes(editor, {
            id: nanoid(),
            type: 'paragraph',
            children: [{ text: '' }],
          })
        } else {
          insertBreak()
          Transforms.setNodes(
            editor,
            { type: 'paragraph' },
            { match: (n) => Element.isElement(n) && Editor.isBlock(editor, n) },
          )
        }

        return true
      }
    }
  }

  return false
}

/**
 * Handle insert text.
 */
const insertText: HeadingAddon['insertText'] = (
  { editor, selection },
  text,
) => {
  if (text === ' ' && selection?.isCollapsed) {
    const blockEntry = Editor.above(editor, {
      match: (n) => Element.isElement(n) && Editor.isBlock(editor, n),
    })

    if (blockEntry) {
      const [, path] = blockEntry
      const start = Editor.start(editor, path)
      const range = { anchor: selection.anchor, focus: start }
      const beforeText = Editor.string(editor, range)

      // ────── HEADINGS ──────
      const headingMap: { [key: string]: number } = {
        '#': 2, // h1 is the title of the page itself
        '##': 3,
        '###': 4,
      }
      if (headingMap[beforeText]) {
        // Skip detection if we are inside some special elements.
        if (
          isInsideElement(editor, [
            'code-block',
            // "latex-block",
            // "latex-inline",
          ]) ||
          isInsideMark(editor, 'code')
        ) {
          return false
        }

        Transforms.delete(editor, { at: range })
        Transforms.setNodes(
          editor,
          { type: 'heading', level: headingMap[beforeText] },
          { match: (n) => Element.isElement(n) },
        )
        return true // break following execution
      }
    }
  }

  return false
}

/**
 * The addon builder.
 *
 * @param params - Initial set of params.
 */
export function heading(params?: HeadingContextValue): HeadingAddon {
  return elementAddon({
    emptyPlaceholder: (level, t) =>
      `${t.title('@editor.heading')} ${level - 1}`,
    ...params,
    id: 'heading',
    render,
    insertBreak,
    insertText,
    slashCommands,

    ContextProvider,
  })
}

export * from './schema'
export * from './types'