MarkdownListHelper.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import { BasicInterceptor } from 'crowi-pluginkit';
  2. import * as codemirror from 'codemirror';
  3. import mlu from '../../util/interceptor/MarkdownListUtil';
  4. export default class MarkdownListHelper extends BasicInterceptor {
  5. constructor() {
  6. super();
  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. }
  13. /**
  14. * @inheritdoc
  15. */
  16. isInterceptWhen(contextName) {
  17. return (
  18. contextName === 'preHandleEnter'
  19. );
  20. }
  21. /**
  22. * return boolean value whether processable parallel
  23. */
  24. isProcessableParallel() {
  25. return false;
  26. }
  27. /**
  28. * @inheritdoc
  29. */
  30. process(contextName, ...args) {
  31. console.log(performance.now() + ': AbortContinueMarkdownListInterceptor.process is started');
  32. const orgContext = args[0];
  33. const editor = orgContext.editor;
  34. console.log('AbortContinueMarkdownListInterceptor.process');
  35. // get strings from current position to EOL(end of line) before break the line
  36. const strToEol = mlu.getStrToEol(editor);
  37. if (this.indentAndMarkRE.test(strToEol)) {
  38. const context = Object.assign(args[0]); // clone
  39. console.log('AbortContinueMarkdownListInterceptor.newlineAndIndentContinueMarkdownList: abort auto indent');
  40. codemirror.commands.newlineAndIndent(editor);
  41. // replace the line with strToEol (abort auto indent)
  42. editor.getDoc().replaceRange(strToEol, mlu.getBol(editor), mlu.getEol(editor));
  43. // report to manager that handling was done
  44. context.handlers.push(this.className);
  45. }
  46. console.log(performance.now() + ': AbortContinueMarkdownListInterceptor.process is finished');
  47. // resolve
  48. // return Promise.resolve(context);
  49. return Promise.resolve(orgContext);
  50. }
  51. /**
  52. * paste text
  53. * @param {any} editor An editor instance of CodeMirror
  54. * @param {any} event
  55. * @param {string} text
  56. */
  57. pasteText(editor, event, text) {
  58. // get strings from BOL(beginning of line) to current position
  59. const strFromBol = mlu.getStrFromBol(editor);
  60. const matched = strFromBol.match(this.indentAndMarkRE);
  61. // when match indentAndMarkOnlyRE
  62. // (this means the current position is the beginning of the list item)
  63. if (this.indentAndMarkOnlyRE.test(strFromBol)) {
  64. const adjusted = this.adjustPastedData(strFromBol, text);
  65. // replace
  66. if (adjusted != null) {
  67. event.preventDefault();
  68. editor.getDoc().replaceRange(adjusted, mlu.getBol(editor), editor.getCursor());
  69. }
  70. }
  71. }
  72. /**
  73. * return adjusted pasted data by indentAndMark
  74. *
  75. * @param {string} indentAndMark
  76. * @param {string} text
  77. * @returns adjusted pasted data
  78. * returns null when adjustment is not necessary
  79. */
  80. adjustPastedData(indentAndMark, text) {
  81. let adjusted = null;
  82. // list data (starts with indent and mark)
  83. if (text.match(this.indentAndMarkRE)) {
  84. const indent = indentAndMark.match(this.indentAndMarkRE)[1];
  85. // splice to an array of line
  86. const lines = text.match(/[^\r\n]+/g);
  87. // indent
  88. const replacedLines = lines.map((line) => {
  89. return indent + line;
  90. })
  91. adjusted = replacedLines.join('\n');
  92. }
  93. // listful data
  94. else if (this.isListfulData(text)) {
  95. // do nothing (return null)
  96. }
  97. // not listful data
  98. else {
  99. // append `indentAndMark` at the beginning of all lines (except the first line)
  100. const replacedText = text.replace(/(\r\n|\r|\n)/g, "$1" + indentAndMark);
  101. // append `indentAndMark` to the first line
  102. adjusted = indentAndMark + replacedText;
  103. }
  104. return adjusted;
  105. }
  106. /**
  107. * evaluate whether `text` is list like data or not
  108. * @param {string} text
  109. */
  110. isListfulData(text) {
  111. // return false if includes at least one blank line
  112. // see https://stackoverflow.com/a/16369725
  113. if (text.match(/^\s*[\r\n]/m) != null) {
  114. return false;
  115. }
  116. const lines = text.match(/[^\r\n]+/g);
  117. // count lines that starts with indent and mark
  118. let isListful = false;
  119. let count = 0;
  120. lines.forEach((line) => {
  121. if (line.match(this.indentAndMarkRE)) {
  122. count++;
  123. }
  124. // ensure to be true if it is 50% or more
  125. if (count >= lines.length / 2) {
  126. isListful = true;
  127. return;
  128. }
  129. });
  130. return isListful;
  131. }
  132. }