Просмотр исходного кода

Modify growi-renderer

https://youtrack.weseek.co.jp/issue/GW-7856
- Remove renderer rules of ordered_list_open
- Remove ListConfigurer from growi-renderer
- Try to modify block.ruler of list
Mudana-Grune 3 лет назад
Родитель
Сommit
dcca5a901e

+ 0 - 2
packages/app/src/services/renderer/growi-renderer.ts

@@ -17,7 +17,6 @@ import HeaderConfigurer from './markdown-it/header';
 import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
 import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
 import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
-import ListConfigurer from './markdown-it/list';
 import MathJaxConfigurer from './markdown-it/mathjax';
 import PlantUMLConfigurer from './markdown-it/plantuml';
 import TableConfigurer from './markdown-it/table';
@@ -103,7 +102,6 @@ export default class GrowiRenderer {
       new DrawioViewerConfigurer(),
       new PlantUMLConfigurer(this.growiRendererConfig),
       new BlockdiagConfigurer(this.growiRendererConfig),
-      new ListConfigurer(),
     ];
 
     if (this.pagePath != null) {

+ 362 - 24
packages/app/src/services/renderer/markdown-it/list.ts

@@ -1,38 +1,376 @@
-export default class ListConfigurer {
+import { isSpace } from 'markdown-it/lib/common/utils';
 
 
-  configure(md): void {
+// Search `[-+*][\n ]`, returns next pos after marker on success
+// or -1 on fail.
+function skipBulletListMarker(state, startLine) {
+  let pos; let
+    ch;
 
-    type ListOject = {
-      indent: number,
-      content: string,
-      parent: number | null
+  pos = state.bMarks[startLine] + state.tShift[startLine];
+  const max = state.eMarks[startLine];
+
+  const marker = state.src.charCodeAt(pos++);
+  // Check bullet
+  if (marker !== 0x2A
+      &&/* * */ marker !== 0x2D
+      &&/* - */ marker !== 0x2B/* + */) {
+    return -1;
+  }
+
+  if (pos < max) {
+    ch = state.src.charCodeAt(pos);
+
+    if (!isSpace(ch)) {
+      // " -test " - is not a list item
+      return -1;
     }
+  }
+
+  return pos;
+}
+
+// Search `\d+[.)][\n ]`, returns next pos after marker on success
+// or -1 on fail.
+function skipOrderedListMarker(state, startLine) {
+  let ch;
+  const start = state.bMarks[startLine] + state.tShift[startLine];
+  let pos = start;
+  const max = state.eMarks[startLine];
+
+  // List marker should have at least 2 chars (digit + dot)
+  if (pos + 1 >= max) { return -1 }
+
+  ch = state.src.charCodeAt(pos++);
+
+  if (ch < 0x30/* 0 */ || ch > 0x39/* 9 */) { return -1 }
+
+  for (;;) {
+    // EOL -> fail
+    if (pos >= max) { return -1 }
+
+    ch = state.src.charCodeAt(pos++);
 
-    md.renderer.rules.ordered_list_open = function(tokens, idx, options, env, self) {
+    if (ch >= 0x30/* 0 */ && ch <= 0x39/* 9 */) {
 
-      const contents = tokens[idx + 3].content;
+      // List marker should have no more than 9 digits
+      // (prevents integer overflow in browsers)
+      if (pos - start >= 10) { return -1 }
+
+      continue;
+    }
+
+    // found valid marker
+    if (ch === 0x29/* ) */ || ch === 0x2e/* . */) {
+      break;
+    }
+
+    return -1;
+  }
+
+
+  if (pos < max) {
+    ch = state.src.charCodeAt(pos);
+
+    if (!isSpace(ch)) {
+      // " 1.test " - is not a list item
+      return -1;
+    }
+  }
+  return pos;
+}
 
-      const splittedContent = contents.split('\n');
-      if (splittedContent.length > 1) {
-        const contentList: ListOject[] = [];
-        let prevIndent;
-        splittedContent.forEach((content) => {
-          const indent = content.match(/^\s*/)[0].length;
-          const listObject = {
-            indent,
-            content,
-            parent: indent === 0 ? null : prevIndent,
-          };
-          prevIndent = indent;
-          contentList.push(listObject);
-        });
+function markTightParagraphs(state, idx) {
+  let i; let l;
+  const level = state.level + 2;
 
+  for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
+    if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
+      state.tokens[i + 2].hidden = true;
+      state.tokens[i].hidden = true;
+      i += 2;
+    }
+  }
+}
+
+export function list(state, startLine, endLine, silent) {
+  let ch;
+  let contentStart;
+  let i;
+  let indent;
+  let indentAfterMarker;
+  let initial;
+  let isOrdered;
+  let itemLines;
+  let l;
+  let listLines;
+  let listTokIdx;
+  let markerCharCode;
+  let markerValue;
+  let max;
+  let nextLine;
+  let offset;
+  let oldListIndent;
+  let oldParentType;
+  let oldSCount;
+  let oldTShift;
+  let oldTight;
+  let pos;
+  let posAfterMarker;
+  let prevEmptyEnd;
+  let start;
+  let terminate;
+  let terminatorRules;
+  let token;
+  let isTerminatingParagraph = false;
+  let tight = true;
+
+  // if it's indented more than 3 spaces, it should be a code block
+  if (state.sCount[startLine] - state.blkIndent >= 4) { return false }
+
+  // Special case:
+  //  - item 1
+  //   - item 2
+  //    - item 3
+  //     - item 4
+  //      - this one is a paragraph continuation
+  if (state.listIndent >= 0
+      && state.sCount[startLine] - state.listIndent >= 4
+      && state.sCount[startLine] < state.blkIndent) {
+    return false;
+  }
+
+  // limit conditions when list can interrupt
+  // a paragraph (validation mode only)
+  if (silent && state.parentType === 'paragraph') {
+    // Next list item should still terminate previous list item;
+    //
+    // This code can fail if plugins use blkIndent as well as lists,
+    // but I hope the spec gets fixed long before that happens.
+    //
+    if (state.tShift[startLine] >= state.blkIndent) {
+      isTerminatingParagraph = true;
+    }
+  }
+
+  // Detect list type and position after marker
+  // eslint-disable-next-line no-cond-assign
+  if ((posAfterMarker = skipOrderedListMarker(state, startLine)) >= 0) {
+    isOrdered = true;
+    start = state.bMarks[startLine] + state.tShift[startLine];
+    markerValue = Number(state.src.substr(start, posAfterMarker - start - 1));
+
+    // If we're starting a new ordered list right after
+    // a paragraph, it should start with 1.
+    if (isTerminatingParagraph && markerValue !== 1) return false;
+
+  }
+  // eslint-disable-next-line no-cond-assign
+  else if ((posAfterMarker = skipBulletListMarker(state, startLine)) >= 0) {
+    isOrdered = false;
+
+  }
+  else {
+    return false;
+  }
+
+  // If we're starting a new unordered list right after
+  // a paragraph, first line should not be empty.
+  if (isTerminatingParagraph) {
+    if (state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]) return false;
+  }
+
+  // We should terminate list on style change. Remember first one to compare.
+  // eslint-disable-next-line prefer-const
+  markerCharCode = state.src.charCodeAt(posAfterMarker - 1);
+
+  // For validation mode we can terminate immediately
+  if (silent) { return true }
+
+  // Start list
+  // eslint-disable-next-line prefer-const
+  listTokIdx = state.tokens.length;
+
+  if (isOrdered) {
+    token = state.push('ordered_list_open', 'ol', 1);
+    if (markerValue !== 1) {
+      token.attrs = [['start', markerValue]];
+    }
+
+  }
+  else {
+    token = state.push('bullet_list_open', 'ul', 1);
+  }
+
+  // eslint-disable-next-line no-multi-assign
+  token.map = listLines = [startLine, 0];
+  token.markup = String.fromCharCode(markerCharCode);
+
+  //
+  // Iterate list items
+  //
+
+  nextLine = startLine;
+  prevEmptyEnd = false;
+  // eslint-disable-next-line prefer-const
+  terminatorRules = state.md.block.ruler.getRules('list');
+
+  // eslint-disable-next-line prefer-const
+  oldParentType = state.parentType;
+  state.parentType = 'list';
+
+  while (nextLine < endLine) {
+    pos = posAfterMarker;
+    max = state.eMarks[nextLine];
+
+    // eslint-disable-next-line no-multi-assign
+    initial = offset = state.sCount[nextLine] + posAfterMarker - (state.bMarks[startLine] + state.tShift[startLine]);
+
+    while (pos < max) {
+      ch = state.src.charCodeAt(pos);
+
+      if (ch === 0x09) {
+        // eslint-disable-next-line no-mixed-operators
+        offset += 4 - (offset + state.bsCount[nextLine]) % 4;
+      }
+      else if (ch === 0x20) {
+        offset++;
+      }
+      else {
+        break;
+      }
+
+      pos++;
+    }
+
+    contentStart = pos;
+
+    if (contentStart >= max) {
+      // trimming space in "-    \n  3" case, indent is 1 here
+      indentAfterMarker = 1;
+    }
+    else {
+      indentAfterMarker = offset - initial;
+    }
+
+    // If we have more than 4 spaces, the indent is 1
+    // (the rest is just indented code block)
+    if (indentAfterMarker > 4) { indentAfterMarker = 1 }
+
+    // "  -  test"
+    //  ^^^^^ - calculating total length of this thing
+    indent = initial + indentAfterMarker;
+
+    // Run subparser & write tokens
+    token = state.push('list_item_open', 'li', 1);
+    token.markup = String.fromCharCode(markerCharCode);
+    // eslint-disable-next-line no-multi-assign
+    token.map = itemLines = [startLine, 0];
+
+    // change current state, then restore it after parser subcall
+    oldTight = state.tight;
+    oldTShift = state.tShift[startLine];
+    oldSCount = state.sCount[startLine];
+
+    //  - example list
+    // ^ listIndent position will be here
+    //   ^ blkIndent position will be here
+    //
+    oldListIndent = state.listIndent;
+    state.listIndent = state.blkIndent;
+    state.blkIndent = indent;
+
+    state.tight = true;
+    state.tShift[startLine] = contentStart - state.bMarks[startLine];
+    state.sCount[startLine] = offset;
+
+    if (contentStart >= max && state.isEmpty(startLine + 1)) {
+      // workaround for this case
+      // (list item is empty, list terminates before "foo"):
+      // ~~~~~~~~
+      //   -
+      //
+      //     foo
+      // ~~~~~~~~
+      state.line = Math.min(state.line + 2, endLine);
+    }
+    else {
+      state.md.block.tokenize(state, startLine, endLine, true);
+    }
+
+    // If any of list item is tight, mark list as tight
+    if (!state.tight || prevEmptyEnd) {
+      tight = false;
+    }
+    // Item become loose if finish with empty line,
+    // but we should filter last element, because it means list finish
+    prevEmptyEnd = (state.line - startLine) > 1 && state.isEmpty(state.line - 1);
+
+    state.blkIndent = state.listIndent;
+    state.listIndent = oldListIndent;
+    state.tShift[startLine] = oldTShift;
+    state.sCount[startLine] = oldSCount;
+    state.tight = oldTight;
+
+    token = state.push('list_item_close', 'li', -1);
+    token.markup = String.fromCharCode(markerCharCode);
+
+    // eslint-disable-next-line no-multi-assign, no-param-reassign
+    nextLine = startLine = state.line;
+    itemLines[1] = nextLine;
+    contentStart = state.bMarks[startLine];
+
+    if (nextLine >= endLine) { break }
+
+    //
+    // Try to check if list is terminated or continued.
+    //
+    if (state.sCount[nextLine] < state.blkIndent) { break }
+
+    // if it's indented more than 3 spaces, it should be a code block
+    if (state.sCount[startLine] - state.blkIndent >= 4) { break }
+
+    // fail if terminating block found
+    terminate = false;
+    for (i = 0, l = terminatorRules.length; i < l; i++) {
+      if (terminatorRules[i](state, nextLine, endLine, true)) {
+        terminate = true;
+        break;
       }
-      return self.renderToken(tokens, idx);
+    }
+    if (terminate) { break }
+
+    // fail if list has another type
+    if (isOrdered) {
+      posAfterMarker = skipOrderedListMarker(state, nextLine);
+      if (posAfterMarker < 0) { break }
+    }
+    else {
+      posAfterMarker = skipBulletListMarker(state, nextLine);
+      if (posAfterMarker < 0) { break }
+    }
+
+    if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { break }
+  }
+
+  // Finalize list
+  if (isOrdered) {
+    token = state.push('ordered_list_close', 'ol', -1);
+  }
+  else {
+    token = state.push('bullet_list_close', 'ul', -1);
+  }
+  token.markup = String.fromCharCode(markerCharCode);
+
+  listLines[1] = nextLine;
+  state.line = nextLine;
 
-    };
+  state.parentType = oldParentType;
 
+  // mark paragraphs tight if needed
+  if (tight) {
+    markTightParagraphs(state, listTokIdx);
   }
 
+  return true;
 }