MarkdownListUtil.js 4.1 KB

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