MarkdownListHelper.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import * as codemirror from 'codemirror';
  2. class MarkdownListHelper {
  3. constructor() {
  4. // https://github.com/codemirror/CodeMirror/blob/c7853a989c77bb9f520c9c530cbe1497856e96fc/addon/edit/continuelist.js#L14
  5. // https://regex101.com/r/7BN2fR/5
  6. this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
  7. this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
  8. this.indentAndUnorderedMarkRE = /[*+-]\s/;
  9. this.newlineAndIndentContinueMarkdownList = this.newlineAndIndentContinueMarkdownList.bind(this);
  10. this.pasteText = this.pasteText.bind(this);
  11. this.getBol = this.getBol.bind(this);
  12. this.getEol = this.getEol.bind(this);
  13. this.getStrFromBol = this.getStrFromBol.bind(this);
  14. this.getStrToEol = this.getStrToEol.bind(this);
  15. }
  16. /**
  17. * return whether context is matched by list
  18. * @param {any} editor An editor instance of CodeMirror
  19. */
  20. isMatchedContext(editor) {
  21. console.log('MarkdownListHelper.isMatchedContext');
  22. // get strings from BOL(beginning of line) to current position
  23. const strFromBol = this.getStrFromBol(editor);
  24. const strToEol = this.getStrToEol(editor);
  25. console.log('strToEol: ' + strToEol);
  26. console.log('strFromBol: ' + strFromBol);
  27. console.log('will return ' + (this.indentAndMarkRE.test(strToEol)
  28. || this.indentAndMarkRE.test(strFromBol)
  29. || this.indentAndMarkOnlyRE.test(strFromBol)
  30. || this.indentAndUnorderedMarkRE.test(strFromBol) ? 'true' : 'false'));
  31. return this.indentAndMarkRE.test(strToEol)
  32. || this.indentAndMarkRE.test(strFromBol)
  33. || this.indentAndMarkOnlyRE.test(strFromBol)
  34. || this.indentAndUnorderedMarkRE.test(strFromBol);
  35. }
  36. /**
  37. * handle new line
  38. * @param {any} editor An editor instance of CodeMirror
  39. */
  40. handleNewLine(editor) {
  41. console.log('MarkdownListHelper.handleNewLine');
  42. this.newlineAndIndentContinueMarkdownList(editor);
  43. }
  44. /**
  45. * wrap codemirror.commands.newlineAndIndentContinueMarkdownList
  46. * @param {any} editor An editor instance of CodeMirror
  47. */
  48. newlineAndIndentContinueMarkdownList(editor) {
  49. console.log('MarkdownListHelper.newlineAndIndentContinueMarkdownList');
  50. // get strings from current position to EOL(end of line) before break the line
  51. const strToEol = this.getStrToEol(editor);
  52. if (this.indentAndMarkRE.test(strToEol)) {
  53. console.log('MarkdownListHelper.newlineAndIndentContinueMarkdownList: abort auto indent');
  54. codemirror.commands.newlineAndIndent(editor);
  55. // replace the line with strToEol (abort auto indent)
  56. editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
  57. }
  58. else {
  59. console.log('MarkdownListHelper.newlineAndIndentContinueMarkdownList: will auto indent');
  60. codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
  61. }
  62. }
  63. /**
  64. * paste text
  65. * @param {any} editor An editor instance of CodeMirror
  66. * @param {any} event
  67. * @param {string} text
  68. */
  69. pasteText(editor, event, text) {
  70. // get strings from BOL(beginning of line) to current position
  71. const strFromBol = this.getStrFromBol(editor);
  72. const matched = strFromBol.match(this.indentAndMarkRE);
  73. // when match indentAndMarkOnlyRE
  74. // (this means the current position is the beginning of the list item)
  75. if (this.indentAndMarkOnlyRE.test(strFromBol)) {
  76. const adjusted = this.adjustPastedData(strFromBol, text);
  77. // replace
  78. if (adjusted != null) {
  79. event.preventDefault();
  80. editor.getDoc().replaceRange(adjusted, this.getBol(editor), editor.getCursor());
  81. }
  82. }
  83. }
  84. /**
  85. * return adjusted pasted data by indentAndMark
  86. *
  87. * @param {string} indentAndMark
  88. * @param {string} text
  89. * @returns adjusted pasted data
  90. * returns null when adjustment is not necessary
  91. */
  92. adjustPastedData(indentAndMark, text) {
  93. let adjusted = null;
  94. // list data (starts with indent and mark)
  95. if (text.match(this.indentAndMarkRE)) {
  96. const indent = indentAndMark.match(this.indentAndMarkRE)[1];
  97. // splice to an array of line
  98. const lines = text.match(/[^\r\n]+/g);
  99. // indent
  100. const replacedLines = lines.map((line) => {
  101. return indent + line;
  102. })
  103. adjusted = replacedLines.join('\n');
  104. }
  105. // listful data
  106. else if (this.isListfulData(text)) {
  107. // do nothing (return null)
  108. }
  109. // not listful data
  110. else {
  111. // append `indentAndMark` at the beginning of all lines (except the first line)
  112. const replacedText = text.replace(/(\r\n|\r|\n)/g, "$1" + indentAndMark);
  113. // append `indentAndMark` to the first line
  114. adjusted = indentAndMark + replacedText;
  115. }
  116. return adjusted;
  117. }
  118. /**
  119. * evaluate whether `text` is list like data or not
  120. * @param {string} text
  121. */
  122. isListfulData(text) {
  123. // return false if includes at least one blank line
  124. // see https://stackoverflow.com/a/16369725
  125. if (text.match(/^\s*[\r\n]/m) != null) {
  126. return false;
  127. }
  128. const lines = text.match(/[^\r\n]+/g);
  129. // count lines that starts with indent and mark
  130. let isListful = false;
  131. let count = 0;
  132. lines.forEach((line) => {
  133. if (line.match(this.indentAndMarkRE)) {
  134. count++;
  135. }
  136. // ensure to be true if it is 50% or more
  137. if (count >= lines.length / 2) {
  138. isListful = true;
  139. return;
  140. }
  141. });
  142. return isListful;
  143. }
  144. /**
  145. * return the postion of the BOL(beginning of line)
  146. */
  147. getBol(editor) {
  148. const curPos = editor.getCursor();
  149. return { line: curPos.line, ch: 0 };
  150. }
  151. /**
  152. * return the postion of the EOL(end of line)
  153. */
  154. getEol(editor) {
  155. const curPos = editor.getCursor();
  156. const lineLength = editor.getDoc().getLine(curPos.line).length;
  157. return { line: curPos.line, ch: lineLength };
  158. }
  159. /**
  160. * return strings from BOL(beginning of line) to current position
  161. */
  162. getStrFromBol(editor) {
  163. const curPos = editor.getCursor();
  164. return editor.getDoc().getRange(this.getBol(editor), curPos);
  165. }
  166. /**
  167. * return strings from current position to EOL(end of line)
  168. */
  169. getStrToEol(editor) {
  170. const curPos = editor.getCursor();
  171. return editor.getDoc().getRange(curPos, this.getEol(editor));
  172. }
  173. }
  174. // singleton pattern
  175. const instance = new MarkdownListHelper();
  176. Object.freeze(instance);
  177. export default instance;