diff --git a/demo/index.tsx b/demo/index.tsx index c1d80d36..76ed0dae 100644 --- a/demo/index.tsx +++ b/demo/index.tsx @@ -10,11 +10,17 @@ import { import { useTextAreaMarkdownEditor } from "../src/hooks/use-markdown-editor"; import { faBold, faItalic } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { bold, italic } from "../src"; export type DemoProps = {}; export const Demo: React.FunctionComponent = props => { - const { ref, commandController } = useTextAreaMarkdownEditor(); + const { ref, commandController } = useTextAreaMarkdownEditor({ + commandMap: { + bold: bold, + italic: italic + } + }); return ( diff --git a/src/commands/command-controller.ts b/src/commands/command-controller.ts index 2324c71b..06948abf 100644 --- a/src/commands/command-controller.ts +++ b/src/commands/command-controller.ts @@ -1,8 +1,6 @@ import { extractKeyActivatedCommands } from "./command-utils"; -import * as React from "react"; import { TextController } from "../types/CommandOptions"; import { Command, CommandContext, CommandMap } from "./command"; -import { getDefaultCommandMap } from "./markdown-commands/defaults"; export class CommandController { private readonly textController: TextController; @@ -18,7 +16,7 @@ export class CommandController { constructor(textController: TextController, commandMap: CommandMap) { this.textController = textController; - this.commandMap = { ...getDefaultCommandMap(), ...(commandMap || {}) }; + this.commandMap = commandMap; this.keyActivatedCommands = extractKeyActivatedCommands(commandMap); } diff --git a/src/commands/markdown-commands/boldCommand.tsx b/src/commands/markdown-commands/bold.tsx similarity index 81% rename from src/commands/markdown-commands/boldCommand.tsx rename to src/commands/markdown-commands/bold.tsx index 07278e6e..26db20d8 100644 --- a/src/commands/markdown-commands/boldCommand.tsx +++ b/src/commands/markdown-commands/bold.tsx @@ -1,11 +1,11 @@ import * as React from "react"; import { Command } from "../command"; -import { selectWord } from "../../util/MarkdownUtil"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const boldCommand: Command = { +export const bold: Command = { execute: ({ initialState, textApi }) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); diff --git a/src/commands/markdown-commands/codeCommand.tsx b/src/commands/markdown-commands/code.tsx similarity index 80% rename from src/commands/markdown-commands/codeCommand.tsx rename to src/commands/markdown-commands/code.tsx index 74788595..d7235165 100644 --- a/src/commands/markdown-commands/codeCommand.tsx +++ b/src/commands/markdown-commands/code.tsx @@ -1,15 +1,11 @@ import * as React from "react"; -import { - getBreaksNeededForEmptyLineAfter, - getBreaksNeededForEmptyLineBefore, - selectWord -} from "../../util/MarkdownUtil"; import { Command } from "../command"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const codeCommand: Command = { +export const code: Command = { execute: async ({ textApi, initialState }) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); @@ -30,13 +26,13 @@ export const codeCommand: Command = { return; } - const breaksBeforeCount = getBreaksNeededForEmptyLineBefore( + const breaksBeforeCount = markdownHelpers.getBreaksNeededForEmptyLineBefore( state1.text, state1.selection.start ); const breaksBefore = Array(breaksBeforeCount + 1).join("\n"); - const breaksAfterCount = getBreaksNeededForEmptyLineAfter( + const breaksAfterCount = markdownHelpers.getBreaksNeededForEmptyLineAfter( state1.text, state1.selection.end ); diff --git a/src/commands/markdown-commands/defaults.ts b/src/commands/markdown-commands/defaults.ts deleted file mode 100644 index 3dd562ef..00000000 --- a/src/commands/markdown-commands/defaults.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommandMap } from "../command"; -import { headerCommand } from "./headerCommand"; -import { boldCommand } from "./boldCommand"; -import { italicCommand } from "./italicCommand"; -import { strikeThroughCommand } from "./strikeThroughCommand"; -import { linkCommand } from "./linkCommand"; -import { quoteCommand } from "./quoteCommand"; -import { codeCommand } from "./codeCommand"; -import { - checkedListCommand, - orderedListCommand, - unorderedListCommand -} from "./listCommands"; -import { imageCommand } from "./imageCommand"; - -export function getDefaultCommandMap(): CommandMap { - return { - header: headerCommand, - bold: boldCommand, - italic: italicCommand, - strikethrough: strikeThroughCommand, - link: linkCommand, - quote: quoteCommand, - code: codeCommand, - image: imageCommand, - "unordered-list": unorderedListCommand, - "ordered-list": orderedListCommand, - "checked-list": checkedListCommand - }; -} diff --git a/src/commands/markdown-commands/headerCommand.tsx b/src/commands/markdown-commands/header.tsx similarity index 84% rename from src/commands/markdown-commands/headerCommand.tsx rename to src/commands/markdown-commands/header.tsx index 10f0e7be..6ab8b52d 100644 --- a/src/commands/markdown-commands/headerCommand.tsx +++ b/src/commands/markdown-commands/header.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import { Command } from "../command"; -import { selectWord } from "../../util/MarkdownUtil"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; import { TextController, TextState } from "../../types/CommandOptions"; function setHeader( @@ -9,7 +9,7 @@ function setHeader( prefix: string ) { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); @@ -23,7 +23,7 @@ function setHeader( }); } -export const headerCommand: Command = { +export const header: Command = { execute: ({ initialState, textApi }) => { setHeader(initialState, textApi, "### "); } diff --git a/src/commands/markdown-commands/imageCommand.tsx b/src/commands/markdown-commands/image.tsx similarity index 84% rename from src/commands/markdown-commands/imageCommand.tsx rename to src/commands/markdown-commands/image.tsx index 6c8321e3..9a795bfa 100644 --- a/src/commands/markdown-commands/imageCommand.tsx +++ b/src/commands/markdown-commands/image.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import { Command } from "../command"; -import { selectWord } from "../../util/MarkdownUtil"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const imageCommand: Command = { +export const image: Command = { execute: ({ initialState, textApi }) => { // Replaces the current selection with the whole word selected const state1 = textApi.setSelectionRange( - selectWord({ + markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }) diff --git a/src/commands/markdown-commands/italicCommand.tsx b/src/commands/markdown-commands/italic.tsx similarity index 81% rename from src/commands/markdown-commands/italicCommand.tsx rename to src/commands/markdown-commands/italic.tsx index 6db1e2dc..bdc9817f 100644 --- a/src/commands/markdown-commands/italicCommand.tsx +++ b/src/commands/markdown-commands/italic.tsx @@ -1,11 +1,11 @@ import * as React from "react"; -import { selectWord } from "../../util/MarkdownUtil"; import { Command } from "../command"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const italicCommand: Command = { +export const italic: Command = { execute: ({ initialState, textApi }) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); diff --git a/src/commands/markdown-commands/linkCommand.tsx b/src/commands/markdown-commands/link.tsx similarity index 81% rename from src/commands/markdown-commands/linkCommand.tsx rename to src/commands/markdown-commands/link.tsx index edb763c8..c74bd071 100644 --- a/src/commands/markdown-commands/linkCommand.tsx +++ b/src/commands/markdown-commands/link.tsx @@ -1,11 +1,11 @@ import * as React from "react"; -import { selectWord } from "../../util/MarkdownUtil"; import { Command } from "../command"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const linkCommand: Command = { +export const link: Command = { execute: ({ initialState, textApi }) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); diff --git a/src/commands/markdown-commands/listCommands.tsx b/src/commands/markdown-commands/list.tsx similarity index 90% rename from src/commands/markdown-commands/listCommands.tsx rename to src/commands/markdown-commands/list.tsx index dad8dec1..b69e1e7f 100644 --- a/src/commands/markdown-commands/listCommands.tsx +++ b/src/commands/markdown-commands/list.tsx @@ -1,11 +1,7 @@ import * as React from "react"; -import { - getBreaksNeededForEmptyLineAfter, - getBreaksNeededForEmptyLineBefore, - selectWord -} from "../../util/MarkdownUtil"; import { TextController, TextState } from "../../types/CommandOptions"; import { Command } from "../command"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; export type AlterLineFunction = (line: string, index: number) => string; @@ -42,19 +38,19 @@ export const makeList = ( insertBefore: string | AlterLineFunction ) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: state0.text, selection: state0.selection }); const state1 = api.setSelectionRange(newSelectionRange); - const breaksBeforeCount = getBreaksNeededForEmptyLineBefore( + const breaksBeforeCount = markdownHelpers.getBreaksNeededForEmptyLineBefore( state1.text, state1.selection.start ); const breaksBefore = Array(breaksBeforeCount + 1).join("\n"); - const breaksAfterCount = getBreaksNeededForEmptyLineAfter( + const breaksAfterCount = markdownHelpers.getBreaksNeededForEmptyLineAfter( state1.text, state1.selection.end ); diff --git a/src/commands/markdown-commands/quoteCommand.tsx b/src/commands/markdown-commands/quote.tsx similarity index 74% rename from src/commands/markdown-commands/quoteCommand.tsx rename to src/commands/markdown-commands/quote.tsx index 7a158733..7dd4c7b5 100644 --- a/src/commands/markdown-commands/quoteCommand.tsx +++ b/src/commands/markdown-commands/quote.tsx @@ -1,27 +1,23 @@ import * as React from "react"; -import { - getBreaksNeededForEmptyLineAfter, - getBreaksNeededForEmptyLineBefore, - selectWord -} from "../../util/MarkdownUtil"; import { Command } from "../command"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const quoteCommand: Command = { +export const quote: Command = { execute: ({ initialState, textApi }) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); const state1 = textApi.setSelectionRange(newSelectionRange); - const breaksBeforeCount = getBreaksNeededForEmptyLineBefore( + const breaksBeforeCount = markdownHelpers.getBreaksNeededForEmptyLineBefore( state1.text, state1.selection.start ); const breaksBefore = Array(breaksBeforeCount + 1).join("\n"); - const breaksAfterCount = getBreaksNeededForEmptyLineAfter( + const breaksAfterCount = markdownHelpers.getBreaksNeededForEmptyLineAfter( state1.text, state1.selection.end ); diff --git a/src/commands/markdown-commands/strikeThroughCommand.tsx b/src/commands/markdown-commands/strikethrough.tsx similarity index 80% rename from src/commands/markdown-commands/strikeThroughCommand.tsx rename to src/commands/markdown-commands/strikethrough.tsx index 0f4a25aa..39adf3f9 100644 --- a/src/commands/markdown-commands/strikeThroughCommand.tsx +++ b/src/commands/markdown-commands/strikethrough.tsx @@ -1,11 +1,11 @@ import * as React from "react"; -import { selectWord } from "../../util/MarkdownUtil"; import { Command } from "../command"; +import { markdownHelpers } from "../../helpers/markdown-helpers"; -export const strikeThroughCommand: Command = { +export const strikethrough: Command = { execute: ({ initialState, textApi }) => { // Adjust the selection to encompass the whole word if the caret is inside one - const newSelectionRange = selectWord({ + const newSelectionRange = markdownHelpers.selectWord({ text: initialState.text, selection: initialState.selection }); diff --git a/src/helpers/markdown-helpers.ts b/src/helpers/markdown-helpers.ts new file mode 100644 index 00000000..ff42cada --- /dev/null +++ b/src/helpers/markdown-helpers.ts @@ -0,0 +1,113 @@ +import { TextSection } from "../types/TextSection"; +import { SelectionRange } from "../types/SelectionRange"; + +/** + * A list of helpers for manipulating markdown text. + * These helpers do not interface with a textarea. For that, see + */ +export const markdownHelpers = { + getSurroundingWord: function(text: string, position: number): SelectionRange { + if (!text) throw Error("Argument 'text' should be truthy"); + + const isWordDelimiter = (c: string) => c === " " || c.charCodeAt(0) === 10; + + // leftIndex is initialized to 0 because if selection is 0, it won't even enter the iteration + let start = 0; + // rightIndex is initialized to text.length because if selection is equal to text.length it won't even enter the interation + let end = text.length; + + // iterate to the left + for (let i = position; i - 1 > -1; i--) { + if (isWordDelimiter(text[i - 1])) { + start = i; + break; + } + } + + // iterate to the right + for (let i = position; i < text.length; i++) { + if (isWordDelimiter(text[i])) { + end = i; + break; + } + } + + return { start, end }; + }, + + /** + * If the cursor is inside a word and (selection.start === selection.end) + * returns a new Selection where the whole word is selected + * @param text + * @param selection + */ + selectWord: function({ text, selection }: TextSection): SelectionRange { + if (text && text.length && selection.start === selection.end) { + // the user is pointing to a word + return this.getSurroundingWord(text, selection.start); + } + return selection; + }, + + /** + * Gets the number of line-breaks that would have to be inserted before the given 'startPosition' + * to make sure there's an empty line between 'startPosition' and the previous text + */ + getBreaksNeededForEmptyLineBefore: function( + text = "", + startPosition: number + ): number { + if (startPosition === 0) return 0; + + // rules: + // - If we're in the first line, no breaks are needed + // - Otherwise there must be 2 breaks before the previous character. Depending on how many breaks exist already, we + // may need to insert 0, 1 or 2 breaks + + let neededBreaks = 2; + let isInFirstLine = true; + for (let i = startPosition - 1; i >= 0 && neededBreaks >= 0; i--) { + switch (text.charCodeAt(i)) { + case 32: // blank space + continue; + case 10: // line break + neededBreaks--; + isInFirstLine = false; + break; + default: + return neededBreaks; + } + } + return isInFirstLine ? 0 : neededBreaks; + }, + + /** + * Gets the number of line-breaks that would have to be inserted after the given 'startPosition' + * to make sure there's an empty line between 'startPosition' and the next text + */ + getBreaksNeededForEmptyLineAfter(text = "", startPosition: number): number { + if (startPosition === text.length - 1) return 0; + + // rules: + // - If we're in the first line, no breaks are needed + // - Otherwise there must be 2 breaks before the previous character. Depending on how many breaks exist already, we + // may need to insert 0, 1 or 2 breaks + + let neededBreaks = 2; + let isInLastLine = true; + for (let i = startPosition; i < text.length && neededBreaks >= 0; i++) { + switch (text.charCodeAt(i)) { + case 32: + continue; + case 10: { + neededBreaks--; + isInLastLine = false; + break; + } + default: + return neededBreaks; + } + } + return isInLastLine ? 0 : neededBreaks; + } +}; diff --git a/src/hooks/use-markdown-editor.ts b/src/hooks/use-markdown-editor.ts index 0babff42..d62e1984 100644 --- a/src/hooks/use-markdown-editor.ts +++ b/src/hooks/use-markdown-editor.ts @@ -1,8 +1,8 @@ import React, { useMemo, useRef } from "react"; import { CommandController } from "../commands/command-controller"; -import { getDefaultCommandMap } from "../commands/markdown-commands/defaults"; import { TextAreaTextController } from "../text/textarea-text-controller"; import { TextController } from "../types/CommandOptions"; +import { CommandMap } from "../commands/command"; export type UseTextAreaMarkdownEditorResult = { ref: React.RefObject; @@ -10,7 +10,13 @@ export type UseTextAreaMarkdownEditorResult = { commandController: CommandController; }; -export function useTextAreaMarkdownEditor(): UseTextAreaMarkdownEditorResult { +export type UseTextAreaMarkdownEditorOptions = { + commandMap: CommandMap; +}; + +export function useTextAreaMarkdownEditor( + options: UseTextAreaMarkdownEditorOptions +): UseTextAreaMarkdownEditorResult { const textAreaRef = useRef(null); const textController = useMemo(() => { @@ -18,7 +24,7 @@ export function useTextAreaMarkdownEditor(): UseTextAreaMarkdownEditorResult { }, [textAreaRef]); const commandController = useMemo( - () => new CommandController(textController, getDefaultCommandMap()), + () => new CommandController(textController, options.commandMap), [textAreaRef] ); diff --git a/src/index.ts b/src/index.ts index a958f9da..5fd467ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,38 @@ // Command controller // Individual commands -import { headerCommand } from "./commands/markdown-commands/headerCommand"; -import { boldCommand } from "./commands/markdown-commands/boldCommand"; -import { italicCommand } from "./commands/markdown-commands/italicCommand"; -import { strikeThroughCommand } from "./commands/markdown-commands/strikeThroughCommand"; -import { linkCommand } from "./commands/markdown-commands/linkCommand"; -import { quoteCommand } from "./commands/markdown-commands/quoteCommand"; -import { codeCommand } from "./commands/markdown-commands/codeCommand"; +import { header } from "./commands/markdown-commands/header"; +import { bold } from "./commands/markdown-commands/bold"; +import { italic } from "./commands/markdown-commands/italic"; +import { strikethrough } from "./commands/markdown-commands/strikethrough"; +import { link } from "./commands/markdown-commands/link"; +import { quote } from "./commands/markdown-commands/quote"; +import { code } from "./commands/markdown-commands/code"; import { checkedListCommand, orderedListCommand, unorderedListCommand -} from "./commands/markdown-commands/listCommands"; -import { imageCommand } from "./commands/markdown-commands/imageCommand"; +} from "./commands/markdown-commands/list"; +import { image } from "./commands/markdown-commands/image"; import { CommandController } from "./commands/command-controller"; -import { TextController } from "./types/CommandOptions"; +import type { TextController } from "./types/CommandOptions"; import { TextAreaTextController } from "./text/textarea-text-controller"; +import { markdownHelpers } from "./helpers/markdown-helpers"; export { + markdownHelpers, CommandController, TextController, TextAreaTextController, - headerCommand, - boldCommand, - italicCommand, - strikeThroughCommand, - linkCommand, - quoteCommand, - codeCommand, + header, + bold, + italic, + strikethrough, + link, + quote, + code, checkedListCommand, orderedListCommand, unorderedListCommand, - imageCommand + image }; diff --git a/src/text/textarea-text-controller.ts b/src/text/textarea-text-controller.ts index b1bedb59..bcb48e4f 100644 --- a/src/text/textarea-text-controller.ts +++ b/src/text/textarea-text-controller.ts @@ -1,6 +1,6 @@ import { TextController, TextState } from "../types/CommandOptions"; import * as React from "react"; -import { Selection } from "../types/Selection"; +import { SelectionRange } from "../types/SelectionRange"; export class TextAreaTextController implements TextController { textAreaRef: React.RefObject; @@ -18,7 +18,7 @@ export class TextAreaTextController implements TextController { return getStateFromTextArea(textArea); } - setSelectionRange(selection: Selection): TextState { + setSelectionRange(selection: SelectionRange): TextState { const textArea = this.textAreaRef.current; if (!textArea) { throw new Error("TextAreaRef is not set"); diff --git a/src/types/CommandOptions.ts b/src/types/CommandOptions.ts index 0bca3796..0bdad96c 100644 --- a/src/types/CommandOptions.ts +++ b/src/types/CommandOptions.ts @@ -1,4 +1,4 @@ -import { Selection } from "./Selection"; +import { SelectionRange } from "./SelectionRange"; /** * The state of the text of the whole editor @@ -15,7 +15,7 @@ export interface TextState { /** * The section of the text that is selected */ - selection: Selection; + selection: SelectionRange; } export interface TextController { @@ -30,7 +30,7 @@ export interface TextController { * Selects the specified text range * @param selection */ - setSelectionRange(selection: Selection): TextState; + setSelectionRange(selection: SelectionRange): TextState; /** * Get the current text state */ diff --git a/src/types/FunctionTypes.ts b/src/types/FunctionTypes.ts deleted file mode 100644 index 462a53fa..00000000 --- a/src/types/FunctionTypes.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from "react"; - -export type GenerateMarkdownPreview = ( - markdown: string -) => Promise; - -/** - * If the command returns true for a given KeyboardEvent, - * the command is executed - */ -export type HandleKeyCommand = ( - e: React.KeyboardEvent -) => boolean; diff --git a/src/types/Selection.ts b/src/types/SelectionRange.ts similarity index 50% rename from src/types/Selection.ts rename to src/types/SelectionRange.ts index 79f6b7a7..8815cacf 100644 --- a/src/types/Selection.ts +++ b/src/types/SelectionRange.ts @@ -1,4 +1,4 @@ -export interface Selection { +export interface SelectionRange { start: number; end: number; } diff --git a/src/types/TextSection.ts b/src/types/TextSection.ts index b1d345d8..5768e440 100644 --- a/src/types/TextSection.ts +++ b/src/types/TextSection.ts @@ -1,6 +1,6 @@ -import { Selection } from "./Selection"; +import { SelectionRange } from "./SelectionRange"; export interface TextSection { text: string; - selection: Selection; + selection: SelectionRange; } diff --git a/src/util/MarkdownUtil.ts b/src/util/MarkdownUtil.ts deleted file mode 100644 index a38c999a..00000000 --- a/src/util/MarkdownUtil.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { TextSection } from "../types/TextSection"; -import { Selection } from "../types/Selection"; - -export function getSurroundingWord(text: string, position: number): Selection { - if (!text) throw Error("Argument 'text' should be truthy"); - - const isWordDelimiter = (c: string) => c === " " || c.charCodeAt(0) === 10; - - // leftIndex is initialized to 0 because if selection is 0, it won't even enter the iteration - let start = 0; - // rightIndex is initialized to text.length because if selection is equal to text.length it won't even enter the interation - let end = text.length; - - // iterate to the left - for (let i = position; i - 1 > -1; i--) { - if (isWordDelimiter(text[i - 1])) { - start = i; - break; - } - } - - // iterate to the right - for (let i = position; i < text.length; i++) { - if (isWordDelimiter(text[i])) { - end = i; - break; - } - } - - return { start, end }; -} - -/** - * If the cursor is inside a word and (selection.start === selection.end) - * returns a new Selection where the whole word is selected - * @param text - * @param selection - */ -export function selectWord({ text, selection }: TextSection): Selection { - if (text && text.length && selection.start === selection.end) { - // the user is pointing to a word - return getSurroundingWord(text, selection.start); - } - return selection; -} - -/** - * Gets the number of line-breaks that would have to be inserted before the given 'startPosition' - * to make sure there's an empty line between 'startPosition' and the previous text - */ -export function getBreaksNeededForEmptyLineBefore( - text = "", - startPosition: number -): number { - if (startPosition === 0) return 0; - - // rules: - // - If we're in the first line, no breaks are needed - // - Otherwise there must be 2 breaks before the previous character. Depending on how many breaks exist already, we - // may need to insert 0, 1 or 2 breaks - - let neededBreaks = 2; - let isInFirstLine = true; - for (let i = startPosition - 1; i >= 0 && neededBreaks >= 0; i--) { - switch (text.charCodeAt(i)) { - case 32: // blank space - continue; - case 10: // line break - neededBreaks--; - isInFirstLine = false; - break; - default: - return neededBreaks; - } - } - return isInFirstLine ? 0 : neededBreaks; -} - -/** - * Gets the number of line-breaks that would have to be inserted after the given 'startPosition' - * to make sure there's an empty line between 'startPosition' and the next text - */ -export function getBreaksNeededForEmptyLineAfter( - text = "", - startPosition: number -): number { - if (startPosition === text.length - 1) return 0; - - // rules: - // - If we're in the first line, no breaks are needed - // - Otherwise there must be 2 breaks before the previous character. Depending on how many breaks exist already, we - // may need to insert 0, 1 or 2 breaks - - let neededBreaks = 2; - let isInLastLine = true; - for (let i = startPosition; i < text.length && neededBreaks >= 0; i++) { - switch (text.charCodeAt(i)) { - case 32: - continue; - case 10: { - neededBreaks--; - isInLastLine = false; - break; - } - default: - return neededBreaks; - } - } - return isInLastLine ? 0 : neededBreaks; -}