kanban / webapp /src /components /live-markdown-plugin /liveMarkdownPlugin.ts
Leon4gr45's picture
Upload folder using huggingface_hub
13555f3 verified
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
EditorState,
CharacterMetadata,
ContentState,
ContentBlock,
EditorChangeType,
DraftStyleMap,
} from 'draft-js'
import {EditorPlugin} from '@draft-js-plugins/editor'
import {Repeat, List} from 'immutable'
// Inline style handlers
import createBoldStyleStrategy from './inline-styles/boldStyleStrategy'
import createItalicStyleStrategy from './inline-styles/italicStyleStrategy'
import createStrikethroughStyleStrategy from './inline-styles/strikethroughStyleStrategy'
import createHeadingDelimiterStyleStrategy from './inline-styles/headingDelimiterStyleStrategy'
import createULDelimiterStyleStrategy from './inline-styles/ulDelimiterStyleStrategy'
import createOLDelimiterStyleStrategy from './inline-styles/olDelimiterStyleStrategy'
import createQuoteStyleStrategy from './inline-styles/quoteStyleStrategy'
import createInlineCodeStyleStrategy from './inline-styles/inlineCodeStyleStrategy'
// Block type handlers
import createCodeBlockStrategy from './block-types/codeBlockStrategy'
import createHeadingBlockStrategy from './block-types/headingBlockStrategy'
import {BlockStrategy, InlineStrategy} from './pluginStrategy'
export interface LiveMarkdownPluginConfig {
inlineStyleStrategies?: InlineStrategy[]
blockTypeStrategies?: BlockStrategy[]
}
function createLiveMarkdownPlugin(config: LiveMarkdownPluginConfig = {}): EditorPlugin {
const {
inlineStyleStrategies = [
createBoldStyleStrategy(),
createItalicStyleStrategy(),
createStrikethroughStyleStrategy(),
createHeadingDelimiterStyleStrategy(),
createULDelimiterStyleStrategy(),
createOLDelimiterStyleStrategy(),
createQuoteStyleStrategy(),
createInlineCodeStyleStrategy(),
],
blockTypeStrategies = [
createCodeBlockStrategy(),
createHeadingBlockStrategy(),
],
} = config
// Construct the editor style map from our inline style strategies
const customStyleMap: DraftStyleMap = {}
inlineStyleStrategies.forEach((styleStrategy) => {
if (styleStrategy.style && styleStrategy.styles) {
customStyleMap[styleStrategy.style] = styleStrategy.styles
}
if (styleStrategy.delimiterStyle && styleStrategy.delimiterStyles) {
customStyleMap[styleStrategy.delimiterStyle] =
styleStrategy.delimiterStyles
}
})
// Construct the block style fn
const blockStyleMap = blockTypeStrategies.reduce((map: Record<string, string>, blockStrategy) => {
map[blockStrategy.type] = blockStrategy.className
return map
}, {})
const blockStyleFn = (block: ContentBlock) => {
const blockType = block.getType()
return blockStyleMap[blockType]
}
return {
// We must handle the maintenance of block types and inline styles on changes.
// To make sure the code is efficient we only perform maintenance on content
// blocks that have been changed. We only perform maintenance for change types
// that result in actual text changes (ignore cursing through text, etc).
onChange: (editorState) => {
// if (editorState.getLastChangeType() === 'insert-fragment')
// return maintainWholeEditorState();
return maintainEditorState(
editorState,
blockTypeStrategies,
inlineStyleStrategies,
)
},
customStyleMap,
blockStyleFn,
}
}
// Takes an EditorState and returns a ContentState updated with block types and
// inline styles according to the provided strategies
// Takes a targeted approach that only updates the modified block/blocks
const maintainEditorState = (
editorState: EditorState,
blockTypeStrategies: BlockStrategy[],
inlineStyleStrategies: InlineStrategy[],
) => {
// Bypass maintenance if text was not changed
const lastChangeType = editorState.getLastChangeType()
const bypassOnChangeTypes = [
'adjust-depth',
'apply-entity',
'change-block-data',
'change-block-type',
'change-inline-style',
'maintain-markdown',
]
if (bypassOnChangeTypes.includes(lastChangeType)) {
return editorState
}
// Maintain block types then inline styles
// Order is important bc we want the inline style strategies to be able to
// look at block type to avoid unnecessary regex searching when possible
const contentState = editorState.getCurrentContent()
let newContentState = maintainBlockTypes(contentState, blockTypeStrategies)
newContentState = maintainInlineStyles(
newContentState,
editorState,
inlineStyleStrategies,
)
// Apply the updated content state
let newEditorState = editorState
if (contentState !== newContentState) {
newEditorState = EditorState.push(
editorState,
newContentState,
'maintain-markdown' as EditorChangeType,
)
}
newEditorState = EditorState.forceSelection(
newEditorState,
editorState.getSelection(),
)
return newEditorState
}
// Takes a ContentState and returns a ContentState with block types and inline styles
// applied or removed as necessary
const maintainBlockTypes = (contentState: ContentState, blockTypeStrategies: BlockStrategy[]) => {
return blockTypeStrategies.reduce((cs, blockTypeStrategy) => {
return blockTypeStrategy.mapBlockType(cs)
}, contentState)
}
// Takes a ContentState (and EditorState for getting the selection and change type)
// and returns a ContentState with inline styles applied or removed as necessary
const maintainInlineStyles = (
contentState: ContentState,
editorState: EditorState,
inlineStyleStrategies: InlineStrategy[],
): ContentState => {
const lastChangeType = editorState.getLastChangeType()
const selection = editorState.getSelection()
const blockKey = selection.getStartKey()
const block = contentState.getBlockForKey(blockKey)
const blockMap = contentState.getBlockMap()
let newBlockMap = blockMap
// If text has been pasted (potentially modifying/creating multiple blocks) or
// the editor is new we must maintain the styles for all content blocks
if (lastChangeType === 'insert-fragment' || !lastChangeType) {
blockMap.forEach((b, k) => {
if (!b || !k) {
return
}
const newBlock = mapInlineStyles(b, inlineStyleStrategies) as ContentBlock
newBlockMap = newBlockMap.set(k, newBlock)
})
} else {
const newBlock = mapInlineStyles(block, inlineStyleStrategies) as ContentBlock
newBlockMap = newBlockMap.set(blockKey, newBlock)
}
// If enter was pressed (or the block was otherwise split) we must maintain
// styles in the previous block as well
if (lastChangeType === 'split-block') {
const newPrevBlock = mapInlineStyles(
contentState.getBlockBefore(blockKey)!,
inlineStyleStrategies,
) as ContentBlock
newBlockMap = newBlockMap.set(
contentState.getKeyBefore(blockKey),
newPrevBlock,
)
}
const newContentState = contentState.merge({
blockMap: newBlockMap,
}) as ContentState
return newContentState
}
// Maps inline styles to the provided ContentBlock's CharacterMetadata list based
// on the plugin's inline style strategies
const mapInlineStyles = (block: ContentBlock, strategies: InlineStrategy[]) => {
// This will be called upon any change that has the potential to effect the styles
// of a content block.
// Find all of the ranges that should have styles applied to them (i.e. all bold,
// italic, or strikethrough delimited ranges of the block).
const blockText = block.getText()
// Create a list of empty CharacterMetadata to map styles to
// eslint-disable-next-line new-cap
let characterMetadataList = List(
// eslint-disable-next-line new-cap
Repeat(CharacterMetadata.create(), blockText.length),
)
// Evaluate block text with each style strategy and apply styles to matching
// ranges of text and delimiters
strategies.forEach((strategy) => {
const styleRanges = strategy.findStyleRanges(block)
const delimiterRanges = strategy.findDelimiterRanges ? strategy.findDelimiterRanges(block, styleRanges) : []
characterMetadataList = applyStyleRangesToCharacterMetadata(
strategy.style,
styleRanges,
characterMetadataList,
)
characterMetadataList = applyStyleRangesToCharacterMetadata(
strategy.delimiterStyle,
delimiterRanges,
characterMetadataList,
)
})
// Apply the list of CharacterMetadata to the content block
return block.set('characterList', characterMetadataList)
}
// Applies the provided style to the corresponding ranges of the character metadata
const applyStyleRangesToCharacterMetadata = (
style: string | undefined,
ranges: number[][],
characterMetadataList: List<CharacterMetadata>,
) => {
let styledCharacterMetadataList = characterMetadataList
if (!style) {
return styledCharacterMetadataList
}
ranges.forEach((range) => {
for (let i = range[0]; i <= range[1]; i++) {
const styled = CharacterMetadata.applyStyle(
characterMetadataList.get(i),
style,
)
styledCharacterMetadataList = styledCharacterMetadataList.set(i, styled)
}
})
return styledCharacterMetadataList
}
export default createLiveMarkdownPlugin