Skip to content

Commit

Permalink
feat: strikethrough, superscript and subscript
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed May 19, 2024
1 parent f1ac2f9 commit 1bab51b
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 175 deletions.
11 changes: 11 additions & 0 deletions src/FormatConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ export const IS_CODE = 0b10000 as const
export const IS_SUBSCRIPT = 0b100000 as const
export const IS_SUPERSCRIPT = 0b1000000 as const
export const IS_HIGHLIGHT = 0b10000000 as const

export type FORMAT =
| typeof DEFAULT_FORMAT
| typeof IS_BOLD
| typeof IS_ITALIC
| typeof IS_STRIKETHROUGH
| typeof IS_UNDERLINE
| typeof IS_CODE
| typeof IS_SUBSCRIPT
| typeof IS_SUPERSCRIPT
| typeof IS_HIGHLIGHT
55 changes: 50 additions & 5 deletions src/examples/basics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ export function Bare() {
return (
<>
<button onClick={() => ref.current?.setMarkdown('new markdown')}>Set new markdown</button>
<button onClick={() => console.log(ref.current?.getMarkdown())}>Get markdown</button>
<button
onClick={() => {
console.log(ref.current?.getMarkdown())
}}
>
Get markdown
</button>
<MDXEditor autoFocus={true} ref={ref} markdown={helloMarkdown} onChange={console.log} />
</>
)
Expand All @@ -65,6 +71,22 @@ tag
)
}

export function MoreFormatting() {
const ref = React.useRef<MDXEditorMethods>(null)
return (
<>
<MDXEditor
autoFocus={true}
ref={ref}
markdown={`
~~scratch this~~ *and <sup>sup this</sup> and <sub>sub this</sub> all in italic*
`}
onChange={console.log}
/>
</>
)
}

export function FocusEmpty() {
const ref = React.useRef<MDXEditorMethods>(null)
return (
Expand Down Expand Up @@ -124,7 +146,13 @@ export function Jsx() {
return (
<>
<button onClick={() => ref.current?.setMarkdown('new markdown')}>Set new markdown</button>
<button onClick={() => console.log(ref.current?.getMarkdown())}>Get markdown</button>
<button
onClick={() => {
console.log(ref.current?.getMarkdown())
}}
>
Get markdown
</button>

<MDXEditor ref={ref} markdown={jsxMarkdown} onChange={console.log} plugins={[jsxPlugin({ jsxComponentDescriptors })]} />
</>
Expand Down Expand Up @@ -241,8 +269,19 @@ const PlainTextCodeEditorDescriptor: CodeBlockEditorDescriptor = {
Editor: (props) => {
const cb = useCodeBlockEditorContext()
return (
<div onKeyDown={(e) => e.nativeEvent.stopImmediatePropagation()}>
<textarea rows={3} cols={20} defaultValue={props.code} onChange={(e) => cb.setCode(e.target.value)} />
<div
onKeyDown={(e) => {
e.nativeEvent.stopImmediatePropagation()
}}
>
<textarea
rows={3}
cols={20}
defaultValue={props.code}
onChange={(e) => {
cb.setCode(e.target.value)
}}
/>
</div>
)
}
Expand Down Expand Up @@ -313,7 +352,13 @@ export function ConditionalRendering() {

return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Editor is {isOpen ? 'open' : 'closed'}</button>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Editor is {isOpen ? 'open' : 'closed'}
</button>
{isOpen && (
<MDXEditor
markdown="# Hello world"
Expand Down
3 changes: 3 additions & 0 deletions src/icons/strikethrough_s.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/icons/subscript.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/icons/superscript.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions src/importMarkdownToLexical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as Mdast from 'mdast'
import { fromMarkdown, type Options } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
import { ParseOptions } from 'micromark-util-types'
import { IS_BOLD, IS_CODE, IS_ITALIC, IS_UNDERLINE } from './FormatConstants'
import { FORMAT } from './FormatConstants'
import { JsxComponentDescriptor } from './plugins/jsx'
import { DirectiveDescriptor } from './plugins/directives'
import { CodeBlockEditorDescriptor } from './plugins/codeblock'
Expand Down Expand Up @@ -64,13 +64,13 @@ export interface MdastImportVisitor<UN extends Mdast.Nodes> {
* Adds formatting as a context for the current node and its children.
* This is necessary due to mdast treating formatting as a node, while lexical considering it an attribute of a node.
*/
addFormatting(format: typeof IS_BOLD | typeof IS_ITALIC | typeof IS_UNDERLINE | typeof IS_CODE, node?: Mdast.Parent | null): void
addFormatting(format: FORMAT, node?: Mdast.Parent | null): void

/**
* Removes formatting as a context for the current node and its children.
* This is necessary due to mdast treating formatting as a node, while lexical considering it an attribute of a node.
*/
removeFormatting(format: typeof IS_BOLD | typeof IS_ITALIC | typeof IS_UNDERLINE | typeof IS_CODE, node?: Mdast.Parent | null): void
removeFormatting(format: FORMAT, node?: Mdast.Parent | null): void
/**
* Access the current formatting context.
*/
Expand Down Expand Up @@ -189,7 +189,9 @@ export function importMdastTreeToLexical({ root, mdastRoot, visitors, ...descrip
if (!isParent(mdastNode)) {
throw new Error('Attempting to visit children of a non-parent')
}
mdastNode.children.forEach((child) => visit(child, lexicalParent, mdastNode))
mdastNode.children.forEach((child) => {
visit(child, lexicalParent, mdastNode)
})
}

function visit(mdastNode: Mdast.RootContent | Mdast.Root, lexicalParent: LexicalNode, mdastParent: Mdast.Parent | null) {
Expand Down
9 changes: 9 additions & 0 deletions src/plugins/core/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import rich_text from '@/icons/rich_text.svg?react'
import settings from '@/icons/settings.svg?react'
import table from '@/icons/table.svg?react'
import undo from '@/icons/undo.svg?react'
import strikeThrough from '@/icons/strikethrough_s.svg?react'
import superscript from '@/icons/superscript.svg?react'
import subscript from '@/icons/subscript.svg?react'

/**
* A type that represents the possible icon names that can be used with the {@link iconComponentFor$} cell.
Expand Down Expand Up @@ -90,6 +93,9 @@ export type IconKey =
| 'redo'
| 'rich_text'
| 'settings'
| 'strikeThrough'
| 'subscript'
| 'superscript'
| 'table'
| 'undo'

Expand Down Expand Up @@ -135,6 +141,9 @@ const IconMap: Record<IconKey, React.FC> = {
redo,
rich_text,
settings,
strikeThrough,
subscript,
superscript,
table,
undo
}
Expand Down
65 changes: 59 additions & 6 deletions src/plugins/core/LexicalTextVisitor.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { $isTextNode, TextNode } from 'lexical'
import * as Mdast from 'mdast'
import { IS_BOLD, IS_CODE, IS_ITALIC, IS_UNDERLINE } from '../../FormatConstants'
import { IS_BOLD, IS_CODE, IS_ITALIC, IS_STRIKETHROUGH, IS_SUBSCRIPT, IS_SUPERSCRIPT, IS_UNDERLINE } from '../../FormatConstants'
import { LexicalExportVisitor } from '../../exportMarkdownFromLexical'
import { type MdxJsxTextElement } from 'mdast-util-mdx-jsx'

export function isMdastText(mdastNode: Mdast.Nodes): mdastNode is Mdast.Text {
return mdastNode.type === 'text'
}

const JOINABLE_TAGS = ['u', 'span']
const JOINABLE_TAGS = ['u', 'span', 'sub', 'sup']

export const LexicalTextVisitor: LexicalExportVisitor<TextNode, Mdast.Text> = {
export const LexicalTextVisitor: LexicalExportVisitor<TextNode, Mdast.Text | Mdast.Html | MdxJsxTextElement> = {
shouldJoin: (prevNode, currentNode) => {
if (['text', 'emphasis', 'strong'].includes(prevNode.type)) {
return prevNode.type === currentNode.type
}

if (
prevNode.type === 'mdxJsxTextElement' &&
(currentNode as unknown as MdxJsxTextElement).type === 'mdxJsxTextElement' &&
JOINABLE_TAGS.includes((currentNode as unknown as MdxJsxTextElement).name as string)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
currentNode.type === 'mdxJsxTextElement' &&
JOINABLE_TAGS.includes((currentNode as unknown as MdxJsxTextElement).name!)
) {
const currentMdxNode: MdxJsxTextElement = currentNode as unknown as MdxJsxTextElement
return prevNode.name === currentMdxNode.name && JSON.stringify(prevNode.attributes) === JSON.stringify(currentMdxNode.attributes)
Expand Down Expand Up @@ -47,7 +48,7 @@ export const LexicalTextVisitor: LexicalExportVisitor<TextNode, Mdast.Text> = {
const prevFormat = $isTextNode(previousSibling) ? previousSibling.getFormat() : 0
const textContent = lexicalNode.getTextContent()
// if the node is only whitespace, ignore the format.
const format = lexicalNode.getFormat() ?? 0
const format = lexicalNode.getFormat()
const style = lexicalNode.getStyle()

let localParentNode = mdastParent
Expand All @@ -73,6 +74,7 @@ export const LexicalTextVisitor: LexicalExportVisitor<TextNode, Mdast.Text> = {
children: []
}) as Mdast.Parent
}

if (prevFormat & format & IS_UNDERLINE) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'mdxJsxTextElement',
Expand All @@ -82,6 +84,32 @@ export const LexicalTextVisitor: LexicalExportVisitor<TextNode, Mdast.Text> = {
}) as Mdast.Parent
}

if (prevFormat & format & IS_STRIKETHROUGH) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'delete',
children: []
}) as Mdast.Parent
}

if (prevFormat & format & IS_SUPERSCRIPT) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'mdxJsxTextElement',
name: 'sup',
children: [],
attributes: []
}) as Mdast.Parent
}

if (prevFormat & format & IS_SUBSCRIPT) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'mdxJsxTextElement',
name: 'sub',
children: [],
attributes: []
}) as Mdast.Parent
}

// repeat the same sequence as above for formatting introduced with this node
if (format & IS_ITALIC && !(prevFormat & IS_ITALIC)) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'emphasis',
Expand All @@ -105,6 +133,31 @@ export const LexicalTextVisitor: LexicalExportVisitor<TextNode, Mdast.Text> = {
}) as Mdast.Parent
}

if (format & IS_STRIKETHROUGH && !(prevFormat & IS_STRIKETHROUGH)) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'delete',
children: []
}) as Mdast.Parent
}

if (format & IS_SUPERSCRIPT && !(prevFormat & IS_SUPERSCRIPT)) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'mdxJsxTextElement',
name: 'sup',
children: [],
attributes: []
}) as Mdast.Parent
}

if (format & IS_SUBSCRIPT && !(prevFormat & IS_SUBSCRIPT)) {
localParentNode = actions.appendToParent(localParentNode, {
type: 'mdxJsxTextElement',
name: 'sub',
children: [],
attributes: []
}) as Mdast.Parent
}

if (format & IS_CODE) {
actions.appendToParent(localParentNode, {
type: 'inlineCode',
Expand Down
Loading

0 comments on commit 1bab51b

Please sign in to comment.