-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2513 from microsoft/u/juliaroldi/markdown-plugin-…
…port [Part 1] Port Markdown feature
- Loading branch information
Showing
10 changed files
with
909 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
packages/roosterjs-content-model-plugins/lib/markdown/MarkdownPlugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import { setFormat } from './utils/setFormat'; | ||
import type { | ||
ContentChangedEvent, | ||
EditorInputEvent, | ||
EditorPlugin, | ||
IEditor, | ||
KeyDownEvent, | ||
PluginEvent, | ||
} from 'roosterjs-content-model-types'; | ||
|
||
/** | ||
* Options for Markdown plugin | ||
*/ | ||
export interface MarkdownOptions { | ||
strikethrough?: boolean; | ||
bold?: boolean; | ||
italic?: boolean; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
const DefaultOptions: Required<MarkdownOptions> = { | ||
strikethrough: false, | ||
bold: false, | ||
italic: false, | ||
}; | ||
|
||
/** | ||
* Markdown plugin handles markdown formatting, such as transforming * characters into bold text. | ||
*/ | ||
export class MarkdownPlugin implements EditorPlugin { | ||
private editor: IEditor | null = null; | ||
private shouldBold = false; | ||
private shouldItalic = false; | ||
private shouldStrikethrough = false; | ||
private lastKeyTyped: string | null = null; | ||
|
||
/** | ||
* @param options An optional parameter that takes in an object of type MarkdownOptions, which includes the following properties: | ||
* - strikethrough: If true text between ~ will receive strikethrough format. Defaults to true. | ||
* - bold: If true text between * will receive bold format. Defaults to true. | ||
* - italic: If true text between _ will receive italic format. Defaults to true. | ||
*/ | ||
constructor(private options: MarkdownOptions = DefaultOptions) {} | ||
|
||
/** | ||
* Get name of this plugin | ||
*/ | ||
getName() { | ||
return 'Markdown'; | ||
} | ||
|
||
/** | ||
* The first method that editor will call to a plugin when editor is initializing. | ||
* It will pass in the editor instance, plugin should take this chance to save the | ||
* editor reference so that it can call to any editor method or format API later. | ||
* @param editor The editor object | ||
*/ | ||
initialize(editor: IEditor) { | ||
this.editor = editor; | ||
} | ||
|
||
/** | ||
* The last method that editor will call to a plugin before it is disposed. | ||
* Plugin can take this chance to clear the reference to editor. After this method is | ||
* called, plugin should not call to any editor method since it will result in error. | ||
*/ | ||
dispose() { | ||
this.editor = null; | ||
this.shouldBold = false; | ||
this.shouldItalic = false; | ||
this.shouldStrikethrough = false; | ||
this.lastKeyTyped = null; | ||
} | ||
|
||
/** | ||
* Core method for a plugin. Once an event happens in editor, editor will call this | ||
* method of each plugin to handle the event as long as the event is not handled | ||
* exclusively by another plugin. | ||
* @param event The event to handle: | ||
*/ | ||
onPluginEvent(event: PluginEvent) { | ||
if (this.editor) { | ||
switch (event.eventType) { | ||
case 'input': | ||
this.handleEditorInputEvent(this.editor, event); | ||
break; | ||
case 'keyDown': | ||
this.handleBackspaceEvent(event); | ||
this.handleKeyDownEvent(event); | ||
break; | ||
case 'contentChanged': | ||
this.handleContentChangedEvent(event); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private handleEditorInputEvent(editor: IEditor, event: EditorInputEvent) { | ||
const rawEvent = event.rawEvent; | ||
const selection = editor.getDOMSelection(); | ||
if ( | ||
selection && | ||
selection.type == 'range' && | ||
selection.range.collapsed && | ||
rawEvent.inputType == 'insertText' | ||
) { | ||
switch (rawEvent.data) { | ||
case '*': | ||
if (this.options.bold) { | ||
if (this.shouldBold) { | ||
setFormat(editor, '*', { fontWeight: 'bold' }); | ||
this.shouldBold = false; | ||
} else { | ||
this.shouldBold = true; | ||
} | ||
} | ||
|
||
break; | ||
case '~': | ||
if (this.options.strikethrough) { | ||
if (this.shouldStrikethrough) { | ||
setFormat(editor, '~', { strikethrough: true }); | ||
this.shouldStrikethrough = false; | ||
} else { | ||
this.shouldStrikethrough = true; | ||
} | ||
} | ||
break; | ||
case '_': | ||
if (this.options.italic) { | ||
if (this.shouldItalic) { | ||
setFormat(editor, '_', { italic: true }); | ||
this.shouldItalic = false; | ||
} else { | ||
this.shouldItalic = true; | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private handleKeyDownEvent(event: KeyDownEvent) { | ||
const rawEvent = event.rawEvent; | ||
if (!event.handledByEditFeature && !rawEvent.defaultPrevented) { | ||
switch (rawEvent.key) { | ||
case 'Enter': | ||
this.shouldBold = false; | ||
this.shouldItalic = false; | ||
this.shouldStrikethrough = false; | ||
this.lastKeyTyped = null; | ||
break; | ||
case ' ': | ||
if (this.lastKeyTyped === '*' && this.shouldBold) { | ||
this.shouldBold = false; | ||
} else if (this.lastKeyTyped === '~' && this.shouldStrikethrough) { | ||
this.shouldStrikethrough = false; | ||
} else if (this.lastKeyTyped === '_' && this.shouldItalic) { | ||
this.shouldItalic = false; | ||
} | ||
this.lastKeyTyped = null; | ||
break; | ||
default: | ||
this.lastKeyTyped = rawEvent.key; | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private handleBackspaceEvent(event: KeyDownEvent) { | ||
if (!event.handledByEditFeature && event.rawEvent.key === 'Backspace') { | ||
if (this.lastKeyTyped === '*' && this.shouldBold) { | ||
this.shouldBold = false; | ||
} else if (this.lastKeyTyped === '~' && this.shouldStrikethrough) { | ||
this.shouldStrikethrough = false; | ||
} else if (this.lastKeyTyped === '_' && this.shouldItalic) { | ||
this.shouldItalic = false; | ||
} | ||
this.lastKeyTyped = null; | ||
} | ||
} | ||
|
||
private handleContentChangedEvent(event: ContentChangedEvent) { | ||
if (event.source == 'Format') { | ||
this.shouldBold = false; | ||
this.shouldItalic = false; | ||
this.shouldStrikethrough = false; | ||
} | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
packages/roosterjs-content-model-plugins/lib/markdown/utils/setFormat.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; | ||
import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; | ||
|
||
import type { ContentModelSegmentFormat, IEditor } from 'roosterjs-content-model-types'; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function setFormat(editor: IEditor, character: string, format: ContentModelSegmentFormat) { | ||
editor.formatContentModel((model, context) => { | ||
const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( | ||
model, | ||
false /*includeFormatHolder*/ | ||
); | ||
|
||
if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) { | ||
const marker = selectedSegmentsAndParagraphs[0][0]; | ||
context.newPendingFormat = { | ||
...marker.format, | ||
strikethrough: !!marker.format.strikethrough, | ||
italic: !!marker.format.italic, | ||
fontWeight: marker.format?.fontWeight ? 'bold' : undefined, | ||
}; | ||
|
||
const paragraph = selectedSegmentsAndParagraphs[0][1]; | ||
if (marker.segmentType == 'SelectionMarker') { | ||
const markerIndex = paragraph.segments.indexOf(marker); | ||
if (markerIndex > 0 && paragraph.segments[markerIndex - 1]) { | ||
const segmentBeforeMarker = paragraph.segments[markerIndex - 1]; | ||
|
||
if ( | ||
segmentBeforeMarker.segmentType == 'Text' && | ||
segmentBeforeMarker.text[segmentBeforeMarker.text.length - 1] == character | ||
) { | ||
const textBeforeMarker = segmentBeforeMarker.text.slice(0, -1); | ||
if (textBeforeMarker.indexOf(character) > -1) { | ||
const lastCharIndex = segmentBeforeMarker.text.length; | ||
const firstCharIndex = segmentBeforeMarker.text | ||
.substring(0, lastCharIndex - 1) | ||
.lastIndexOf(character); | ||
|
||
const formattedText = splitTextSegment( | ||
segmentBeforeMarker, | ||
paragraph, | ||
firstCharIndex, | ||
lastCharIndex | ||
); | ||
|
||
formattedText.text = formattedText.text | ||
.replace(character, '') | ||
.slice(0, -1); | ||
formattedText.format = { | ||
...formattedText.format, | ||
...format, | ||
}; | ||
|
||
context.canUndoByBackspace = true; | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return false; | ||
}); | ||
} |
Oops, something went wrong.