MarkdownListHelper.js 4.9 KB

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