MarkdownListUtil.js 4.5 KB

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