@ -14,7 +14,13 @@ export function textareaInsertText(textarea, value) {
triggerEditorContentChanged ( textarea ) ;
}
function handleIndentSelection ( textarea , e ) {
type TextareaValueSelection = {
value : string ;
selStart : number ;
selEnd : number ;
}
function handleIndentSelection ( textarea : HTMLTextAreaElement , e ) {
const selStart = textarea . selectionStart ;
const selEnd = textarea . selectionEnd ;
if ( selEnd === selStart ) return ; // do not process when no selection
@ -56,53 +62,125 @@ function handleIndentSelection(textarea, e) {
triggerEditorContentChanged ( textarea ) ;
}
function handleNewline ( textarea : HTMLTextAreaElement , e : Event ) {
const selStart = textarea . selectionStart ;
const selEnd = textarea . selectionEnd ;
if ( selEnd !== selStart ) return ; // do not process when there is a selection
type MarkdownHandleIndentionResult = {
handled : boolean ;
valueSelection? : TextareaValueSelection ;
}
type TextLinesBuffer = {
lines : string [ ] ;
lengthBeforePosLine : number ;
posLineIndex : number ;
inlinePos : number
}
export function textareaSplitLines ( value : string , pos : number ) : TextLinesBuffer {
const lines = value . split ( '\n' ) ;
let lengthBeforePosLine = 0 , inlinePos = 0 , posLineIndex = 0 ;
for ( ; posLineIndex < lines . length ; posLineIndex ++ ) {
const lineLength = lines [ posLineIndex ] . length + 1 ;
if ( lengthBeforePosLine + lineLength > pos ) {
inlinePos = pos - lengthBeforePosLine ;
break ;
}
lengthBeforePosLine += lineLength ;
}
return { lines , lengthBeforePosLine , posLineIndex , inlinePos } ;
}
function markdownReformatListNumbers ( linesBuf : TextLinesBuffer , indention : string ) {
const reDeeperIndention = new RegExp ( ` ^ ${ indention } \\ s+ ` ) ;
const reSameLevel = new RegExp ( ` ^ ${ indention } ([0-9]+) \\ . ` ) ;
let firstLineIdx : number ;
for ( firstLineIdx = linesBuf . posLineIndex - 1 ; firstLineIdx >= 0 ; firstLineIdx -- ) {
const line = linesBuf . lines [ firstLineIdx ] ;
if ( ! reDeeperIndention . test ( line ) && ! reSameLevel . test ( line ) ) break ;
}
firstLineIdx ++ ;
let num = 1 ;
for ( let i = firstLineIdx ; i < linesBuf . lines . length ; i ++ ) {
const oldLine = linesBuf . lines [ i ] ;
const sameLevel = reSameLevel . test ( oldLine ) ;
if ( ! sameLevel && ! reDeeperIndention . test ( oldLine ) ) break ;
if ( sameLevel ) {
const newLine = ` ${ indention } ${ num } . ${ oldLine . replace ( reSameLevel , '' ) } ` ;
linesBuf . lines [ i ] = newLine ;
num ++ ;
if ( linesBuf . posLineIndex === i ) {
// need to correct the cursor inline position if the line length changes
linesBuf . inlinePos += newLine . length - oldLine . length ;
linesBuf . inlinePos = Math . max ( 0 , linesBuf . inlinePos ) ;
linesBuf . inlinePos = Math . min ( newLine . length , linesBuf . inlinePos ) ;
}
}
}
recalculateLengthBeforeLine ( linesBuf ) ;
}
function recalculateLengthBeforeLine ( linesBuf : TextLinesBuffer ) {
linesBuf . lengthBeforePosLine = 0 ;
for ( let i = 0 ; i < linesBuf . posLineIndex ; i ++ ) {
linesBuf . lengthBeforePosLine += linesBuf . lines [ i ] . length + 1 ;
}
}
const value = textarea . value ;
export function markdownHandleIndention ( tvs : TextareaValueSelection ) : MarkdownHandleIndentionResult {
const unhandled : MarkdownHandleIndentionResult = { handled : false } ;
if ( tvs . selEnd !== tvs . selStart ) return unhandled ; // do not process when there is a selection
// find the current line
// * if selStart is 0, lastIndexOf(..., -1) is the same as lastIndexOf(..., 0)
// * if lastIndexOf reruns -1, lineStart is 0 and it is still correct.
const lineStart = value . lastIndexOf ( '\n' , selStart - 1 ) + 1 ;
let lineEnd = value . indexOf ( '\n' , selStart ) ;
lineEnd = lineEnd < 0 ? value.length : lineEnd ;
let line = value . slice ( lineStart , lineEnd ) ;
if ( ! line ) return ; // if the line is empty, do nothing, let the browser handle it
const linesBuf = textareaSplitLines ( tvs . value , tvs . selStart ) ;
const line = linesBuf . lines [ linesBuf . posLineIndex ] ? ? '' ;
if ( ! line ) return unhandled ; // if the line is empty, do nothing, let the browser handle it
// parse the indention
const indention = /^\s*/ . exec ( line ) [ 0 ] ;
line = line . slice ( indention . length ) ;
let lineContent = line ;
const indention = /^\s*/ . exec ( lineContent ) [ 0 ] ;
lineContent = lineContent . slice ( indention . length ) ;
if ( linesBuf . inlinePos <= indention . length ) return unhandled ; // if cursor is at the indention, do nothing, let the browser handle it
// parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists
// there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/ . exec ( line ) ;
const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/ . exec ( line Content ) ;
let prefix = '' ;
if ( prefixMatch ) {
prefix = prefixMatch [ 0 ] ;
if ( lineStart + prefix . length > selStart ) prefix = '' ; // do not add new line if cursor is at prefix
if ( prefix. length > linesBuf. inlinePos ) prefix = '' ; // do not add new line if cursor is at prefix
}
line = line . slice ( prefix . length ) ;
if ( ! indention && ! prefix ) return ; // if no indention and no prefix, do nothing, let the browser handle it
line Content = line Content . slice ( prefix . length ) ;
if ( ! indention && ! prefix ) return unhandled ; // if no indention and no prefix, do nothing, let the browser handle it
e . preventDefault ( ) ;
if ( ! line ) {
if ( ! lineContent ) {
// clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
textarea . value = value . slice ( 0 , lineStart ) + value . slice ( lineEnd ) ;
textarea . setSelectionRange ( selStart - prefix . length , selStart - prefix . length ) ;
linesBuf. lines [ linesBuf . posLineIndex ] = '' ;
linesBuf. inlinePos = 0 ;
} else {
// start a new line with the same indention and prefix
// start a new line with the same indention
let newPrefix = prefix ;
// a simple approach, otherwise it needs to parse the lines after the current line
if ( /^\d+\./ . test ( prefix ) ) newPrefix = ` 1. ${ newPrefix . slice ( newPrefix . indexOf ( '.' ) + 2 ) } ` ;
newPrefix = newPrefix . replace ( '[x]' , '[ ]' ) ;
const newLine = ` \ n ${ indention } ${ newPrefix } ` ;
textarea . value = value . slice ( 0 , selStart ) + newLine + value . slice ( selEnd ) ;
textarea . setSelectionRange ( selStart + newLine . length , selStart + newLine . length ) ;
const inlinePos = linesBuf . inlinePos ;
linesBuf . lines [ linesBuf . posLineIndex ] = line . substring ( 0 , inlinePos ) ;
const newLineLeft = ` ${ indention } ${ newPrefix } ` ;
const newLine = ` ${ newLineLeft } ${ line . substring ( inlinePos ) } ` ;
linesBuf . lines . splice ( linesBuf . posLineIndex + 1 , 0 , newLine ) ;
linesBuf . posLineIndex ++ ;
linesBuf . inlinePos = newLineLeft . length ;
recalculateLengthBeforeLine ( linesBuf ) ;
}
markdownReformatListNumbers ( linesBuf , indention ) ;
const newPos = linesBuf . lengthBeforePosLine + linesBuf . inlinePos ;
return { handled : true , valueSelection : { value : linesBuf.lines.join ( '\n' ) , selStart : newPos , selEnd : newPos } } ;
}
function handleNewline ( textarea : HTMLTextAreaElement , e : Event ) {
const ret = markdownHandleIndention ( { value : textarea.value , selStart : textarea.selectionStart , selEnd : textarea.selectionEnd } ) ;
if ( ! ret . handled ) return ;
e . preventDefault ( ) ;
textarea . value = ret . valueSelection . value ;
textarea . setSelectionRange ( ret . valueSelection . selStart , ret . valueSelection . selEnd ) ;
triggerEditorContentChanged ( textarea ) ;
}