MarkdownListUtil.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. }
  12. /**
  13. * paste text
  14. * @param {any} editor An editor instance of AbstractEditor
  15. * @param {any} event
  16. * @param {string} text
  17. */
  18. pasteText(editor, event, text) {
  19. // get strings from BOL(beginning of line) to current position
  20. const strFromBol = editor.getStrFromBol();
  21. // when match indentAndMarkOnlyRE
  22. // (this means the current position is the beginning of the list item)
  23. if (this.indentAndMarkOnlyRE.test(strFromBol)) {
  24. const adjusted = this.adjustPastedData(strFromBol, text);
  25. // replace
  26. if (adjusted != null) {
  27. event.preventDefault();
  28. editor.replaceBolToCurrentPos(adjusted);
  29. }
  30. }
  31. }
  32. /**
  33. * return adjusted pasted data by indentAndMark
  34. *
  35. * @param {string} indentAndMark
  36. * @param {string} text
  37. * @returns adjusted pasted data
  38. * returns null when adjustment is not necessary
  39. */
  40. adjustPastedData(indentAndMark, text) {
  41. let adjusted = null;
  42. // list data (starts with indent and mark)
  43. if (text.match(this.indentAndMarkRE)) {
  44. const indent = indentAndMark.match(this.indentAndMarkRE)[1];
  45. // splice to an array of line
  46. const lines = text.match(/[^\r\n]+/g);
  47. // indent
  48. const replacedLines = lines.map((line) => {
  49. return indent + line;
  50. });
  51. adjusted = replacedLines.join('\n');
  52. }
  53. // listful data
  54. else if (this.isListfulData(text)) {
  55. // do nothing (return null)
  56. }
  57. // not listful data
  58. else {
  59. // append `indentAndMark` at the beginning of all lines (except the first line)
  60. const replacedText = text.replace(/(\r\n|\r|\n)/g, '$1' + indentAndMark);
  61. // append `indentAndMark` to the first line
  62. adjusted = indentAndMark + replacedText;
  63. }
  64. return adjusted;
  65. }
  66. /**
  67. * evaluate whether `text` is list like data or not
  68. * @param {string} text
  69. */
  70. isListfulData(text) {
  71. // return false if includes at least one blank line
  72. // see https://stackoverflow.com/a/16369725
  73. if (text.match(/^\s*[\r\n]/m) != null) {
  74. return false;
  75. }
  76. const lines = text.match(/[^\r\n]+/g);
  77. // count lines that starts with indent and mark
  78. let isListful = false;
  79. let count = 0;
  80. lines.forEach((line) => {
  81. if (line.match(this.indentAndMarkRE)) {
  82. count++;
  83. }
  84. // ensure to be true if it is 50% or more
  85. if (count >= lines.length / 2) {
  86. isListful = true;
  87. return;
  88. }
  89. });
  90. return isListful;
  91. }
  92. }
  93. // singleton pattern
  94. const instance = new MarkdownListUtil();
  95. Object.freeze(instance);
  96. export default instance;