Jelajahi Sumber

WIP: impl toc and anchors

* anchors are implemented
* toc is just omitted
Yuki Takei 8 tahun lalu
induk
melakukan
8445d4bf1c

+ 1 - 0
package.json

@@ -96,6 +96,7 @@
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-mathjax": "^2.0.0",
+    "markdown-it-named-headers": "^0.0.4",
     "markdown-it-plantuml": "^0.3.1",
     "markdown-it-plantuml": "^0.3.1",
     "markdown-it-task-lists": "^2.1.0",
     "markdown-it-task-lists": "^2.1.0",
     "markdown-it-toc-and-anchor": "^4.1.2",
     "markdown-it-toc-and-anchor": "^4.1.2",

+ 26 - 71
resource/js/legacy/crowi.js

@@ -20,18 +20,6 @@ Crowi.createErrorView = function(msg) {
   $('#main').prepend($('<p class="alert-message error">' + msg + '</p>'));
   $('#main').prepend($('<p class="alert-message error">' + msg + '</p>'));
 };
 };
 
 
-Crowi.correctHeaders = function(contentId) {
-  // h1 ~ h6 の id 名を補正する
-  var $content = $(contentId || '#revision-body-content');
-  var i = 0;
-  $('h1,h2,h3,h4,h5,h6', $content).each(function(idx, elm) {
-    var id = 'head' + i++;
-    $(this).attr('id', id);
-    $(this).addClass('revision-head');
-    $(this).append('<span class="revision-head-link"><a href="#' + id +'"><i class="fa fa-link"></i></a></span>');
-  });
-};
-
 /**
 /**
  * append buttons to section headers
  * append buttons to section headers
  */
  */
@@ -98,52 +86,6 @@ Crowi.setCaretLineAndFocusToEditor = function() {
   crowi.focusToEditor();
   crowi.focusToEditor();
 }
 }
 
 
-Crowi.revisionToc = function(contentId, tocId) {
-  var $content = $(contentId || '#revision-body-content');
-  var $tocId = $(tocId || '#revision-toc');
-
-  var $tocContent = $('<div id="revision-toc-content" class="revision-toc-content collapse in"></div>');
-  $tocId.append($tocContent);
-
-  $('h1', $content).each(function(idx, elm) {
-    var id = $(this).attr('id');
-    var title = $(this).text();
-    var selector = '#' + id + ' ~ h2:not(#' + id + ' ~ h1 ~ h2)';
-
-    var $toc = $('<ul></ul>');
-    var $tocLi = $('<li><a href="#' + id +'">' + title + '</a></li>');
-
-
-    $tocContent.append($toc);
-    $toc.append($tocLi);
-
-    $(selector).each(function()
-    {
-      var id2 = $(this).attr('id');
-      var title2 = $(this).text();
-      var selector2 = '#' + id2 + ' ~ h3:not(#' + id2 + ' ~ h2 ~ h3)';
-
-      var $toc2 = $('<ul></ul>');
-      var $tocLi2 = $('<li><a href="#' + id2 +'">' + title2 + '</a></li>');
-
-      $tocLi.append($toc2);
-      $toc2.append($tocLi2);
-
-      $(selector2).each(function()
-      {
-        var id3 = $(this).attr('id');
-        var title3 = $(this).text();
-
-        var $toc3 = $('<ul></ul>');
-        var $tocLi3 = $('<li><a href="#' + id3 +'">' + title3 + '</a></li>');
-
-        $tocLi2.append($toc3);
-        $toc3.append($tocLi3);
-      });
-    });
-  });
-};
-
 // original: middleware.swigFilter
 // original: middleware.swigFilter
 Crowi.userPicture = function (user) {
 Crowi.userPicture = function (user) {
   if (!user) {
   if (!user) {
@@ -167,7 +109,7 @@ Crowi.modifyScrollTop = function() {
   }
   }
   var pageHeaderRect = pageHeader.getBoundingClientRect();
   var pageHeaderRect = pageHeader.getBoundingClientRect();
 
 
-  var sectionHeader = document.querySelector(hash);
+  var sectionHeader = Crowi.findSectionHeader(hash);
   if (sectionHeader === null) {
   if (sectionHeader === null) {
     return;
     return;
   }
   }
@@ -572,9 +514,7 @@ $(function() {
             top.location.href = path;
             top.location.href = path;
           });
           });
 
 
-          Crowi.correctHeaders('#revision-body-content');
           Crowi.appendEditSectionButtons('#revision-body-content', markdown);
           Crowi.appendEditSectionButtons('#revision-body-content', markdown);
-          Crowi.revisionToc('#revision-body-content', '#revision-toc');
 
 
           Promise.resolve($('#revision-body-content'));
           Promise.resolve($('#revision-body-content'));
         })
         })
@@ -875,28 +815,43 @@ Crowi.findHashFromUrl = function(url)
 {
 {
   var match;
   var match;
   if (match = url.match(/#(.+)$/)) {
   if (match = url.match(/#(.+)$/)) {
-    return '#' + match[1];
+    return `#${match[1]}`;
   }
   }
 
 
   return "";
   return "";
 }
 }
 
 
+Crowi.findSectionHeader = function(hash) {
+  if (hash.length == 0) {
+    return;
+  }
+
+  // omit '#'
+  const id = hash.replace('#', '');
+  // don't use jQuery and document.querySelector
+  //  because hash may containe Base64 encoded strings
+  const elem = document.getElementById(id);
+  if (elem != null && elem.tagName.match(/h\d+/i)) {  // match h1, h2, h3...
+    return elem;
+  }
+
+  return null;
+}
+
 Crowi.unhighlightSelectedSection = function(hash)
 Crowi.unhighlightSelectedSection = function(hash)
 {
 {
-  if (!hash || hash == "" || !hash.match(/^#head.+/)) {
-    // とりあえず head* だけ (検索結果ページで副作用出た
-    return true;
+  const elem = Crowi.findSectionHeader(hash);
+  if (elem != null) {
+    elem.classList.remove('highlighted');
   }
   }
-  $(hash).removeClass('highlighted');
 }
 }
 
 
 Crowi.highlightSelectedSection = function(hash)
 Crowi.highlightSelectedSection = function(hash)
 {
 {
-  if (!hash || hash == "" || !hash.match(/^#head.+/)) {
-    // とりあえず head* だけ (検索結果ページで副作用出た
-    return true;
+  const elem = Crowi.findSectionHeader(hash);
+  if (elem != null) {
+    elem.classList.add('highlighted');
   }
   }
-  $(hash).addClass('highlighted');
 }
 }
 
 
 window.addEventListener('load', function(e) {
 window.addEventListener('load', function(e) {
@@ -963,7 +918,7 @@ window.addEventListener('hashchange', function(e) {
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
       $('a[data-toggle="tab"][href="#revision-history"]').tab('show');
     }
     }
   }
   }
-  if (location.hash == '' || location.hash.match(/^#head.+/)) {
+  else {
     $('a[data-toggle="tab"][href="#revision-body"]').tab('show');
     $('a[data-toggle="tab"][href="#revision-body"]').tab('show');
   }
   }
 });
 });

+ 7 - 1
resource/js/util/GrowiRenderer.js

@@ -9,9 +9,12 @@ import Template from './LangProcessor/Template';
 
 
 import CommonPluginsConfigurer from './markdown-it/common-plugins';
 import CommonPluginsConfigurer from './markdown-it/common-plugins';
 import EmojiConfigurer from './markdown-it/emoji';
 import EmojiConfigurer from './markdown-it/emoji';
+import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
+import HeaderConfigurer from './markdown-it/header';
 import MathJaxConfigurer from './markdown-it/mathjax';
 import MathJaxConfigurer from './markdown-it/mathjax';
 import PlantUMLConfigurer from './markdown-it/plantuml';
 import PlantUMLConfigurer from './markdown-it/plantuml';
 import TableConfigurer from './markdown-it/table';
 import TableConfigurer from './markdown-it/table';
+import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
 
 
 export default class GrowiRenderer {
 export default class GrowiRenderer {
 
 
@@ -28,10 +31,13 @@ export default class GrowiRenderer {
     ];
     ];
     this.markdownItConfigurers = [
     this.markdownItConfigurers = [
       new CommonPluginsConfigurer(crowi),
       new CommonPluginsConfigurer(crowi),
+      new TocAndAnchorConfigurer(crowi),
+      new HeaderConfigurer(crowi),
+      new HeaderLineNumberConfigurer(crowi),
+      new TableConfigurer(crowi),
       new EmojiConfigurer(crowi),
       new EmojiConfigurer(crowi),
       new MathJaxConfigurer(crowi),
       new MathJaxConfigurer(crowi),
       new PlantUMLConfigurer(crowi),
       new PlantUMLConfigurer(crowi),
-      new TableConfigurer(crowi),
     ];
     ];
     this.langProcessors = {
     this.langProcessors = {
       'template': new Template(crowi),
       'template': new Template(crowi),

+ 0 - 12
resource/js/util/markdown-it/common-plugins.js

@@ -9,19 +9,7 @@ export default class CommonPluginsConfigurer {
       .use(require('markdown-it-task-lists'), {
       .use(require('markdown-it-task-lists'), {
         enabled: true,
         enabled: true,
       })
       })
-      .use(require('markdown-it-toc-and-anchor').default, {
-        anchorLinkBefore: false,
-        anchorLinkSymbol: '',
-        anchorLinkSymbolClassName: 'fa fa-link',
-        anchorClassName: 'revision-head-link',
-      })
       ;
       ;
-
-    md.set({
-      tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-        // TODO impl
-      },
-    });
   }
   }
 
 
 }
 }

+ 44 - 0
resource/js/util/markdown-it/header-line-number.js

@@ -0,0 +1,44 @@
+export default class HeaderLineNumberConfigurer {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+
+    this.injectLineNumbers = this.injectLineNumbers.bind(this);
+    this.combineRules = this.combineRules.bind(this);
+  }
+
+  configure(md) {
+    const rules = md.renderer.rules;
+    const headingOpenOrg = rules.heading_open;
+    const paragraphOpenOrg = rules.paragraph_open;
+    // combine rule and set
+    rules.heading_open = this.combineRules(this.injectLineNumbers, headingOpenOrg);
+    rules.paragraph_open = this.combineRules(this.injectLineNumbers, paragraphOpenOrg);
+  }
+
+  /**
+   * Inject line numbers for sync scroll
+   * @see https://github.com/markdown-it/markdown-it/blob/e6f19eab4204122e85e4a342e0c1c8486ff40c2d/support/demo_template/index.js#L169
+   */
+  injectLineNumbers(tokens, idx, options, env, slf) {
+    var line;
+    if (tokens[idx].map && tokens[idx].level === 0) {
+      line = tokens[idx].map[0];
+      tokens[idx].attrJoin('class', 'line');
+      tokens[idx].attrSet('data-line', String(line));
+    }
+    return slf.renderToken(tokens, idx, options, env, slf);
+  }
+
+  combineRules(rule1, rule2) {
+    return (tokens, idx, options, env, slf) => {
+      if (rule1 != null) {
+        rule1(tokens, idx, options, env, slf);
+      }
+      if (rule2 != null) {
+        rule2(tokens, idx, options, env, slf);
+      }
+      return slf.renderToken(tokens, idx, options, env, slf);
+    }
+  }
+}

+ 38 - 0
resource/js/util/markdown-it/header.js

@@ -0,0 +1,38 @@
+export default class HeaderConfigurer {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+
+    this.injectRevisionHeadClass = this.injectRevisionHeadClass.bind(this);
+    this.combineRules = this.combineRules.bind(this);
+  }
+
+  configure(md) {
+    const rules = md.renderer.rules;
+    const headingOpenOrg = rules.heading_open;
+    // combine rule and set
+    rules.heading_open = this.combineRules(this.injectRevisionHeadClass, headingOpenOrg);
+  }
+
+  /**
+   * Inject 'revision-head' class
+   */
+  injectRevisionHeadClass(tokens, idx, options, env, slf) {
+    if (tokens[idx].map && tokens[idx].level === 0) {
+      tokens[idx].attrJoin('class', 'revision-head');
+    }
+    return slf.renderToken(tokens, idx, options, env, slf);
+  }
+
+  combineRules(rule1, rule2) {
+    return (tokens, idx, options, env, slf) => {
+      if (rule1 != null) {
+        rule1(tokens, idx, options, env, slf);
+      }
+      if (rule2 != null) {
+        rule2(tokens, idx, options, env, slf);
+      }
+      return slf.renderToken(tokens, idx, options, env, slf);
+    }
+  }
+}

+ 39 - 0
resource/js/util/markdown-it/toc-and-anchor.js

@@ -0,0 +1,39 @@
+export default class TocAndAnchorConfigurer {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+  }
+
+  configure(md) {
+    md.use(require('markdown-it-toc-and-anchor').default, {
+        anchorLinkBefore: false,
+        anchorLinkSymbol: '',
+        anchorLinkSymbolClassName: 'fa fa-link',
+        anchorClassName: 'revision-head-link',
+      })
+      .use(require('markdown-it-named-headers'), {  // overwrite id defined by markdown-it-toc-and-anchor
+        slugify: this.customSlugify,
+      })
+      ;
+
+    md.set({
+      tocCallback: (tocMarkdown, tocArray, tocHtml) => {
+        // TODO impl
+        // console.log(tocHtml);
+      },
+    });
+  }
+
+  /**
+   * create Base64 encoded id
+   * @see https://qiita.com/satokaz/items/64582da4640898c4bf42
+   * @param {string} header
+   */
+  customSlugify(header) {
+    return encodeURIComponent(header.trim()
+      .toLowerCase()
+      .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
+      .replace(/\s+/g, '-')) // Replace spaces with hyphens
+      .replace(/\-+$/, ''); // Replace trailing hyphen
+  }
+}

+ 10 - 0
yarn.lock

@@ -3683,6 +3683,12 @@ markdown-it-mathjax@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/markdown-it-mathjax/-/markdown-it-mathjax-2.0.0.tgz#ae2b4f4c5c719a03f9e475c664f7b2685231d9e9"
   resolved "https://registry.yarnpkg.com/markdown-it-mathjax/-/markdown-it-mathjax-2.0.0.tgz#ae2b4f4c5c719a03f9e475c664f7b2685231d9e9"
 
 
+markdown-it-named-headers@^0.0.4:
+  version "0.0.4"
+  resolved "https://registry.yarnpkg.com/markdown-it-named-headers/-/markdown-it-named-headers-0.0.4.tgz#82efc28324240a6b1e77b9aae501771d5f351c1f"
+  dependencies:
+    string "^3.0.1"
+
 markdown-it-plantuml@^0.3.1:
 markdown-it-plantuml@^0.3.1:
   version "0.3.1"
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/markdown-it-plantuml/-/markdown-it-plantuml-0.3.1.tgz#f338df4d691a5561364e65809b6812bcb3d8b047"
   resolved "https://registry.yarnpkg.com/markdown-it-plantuml/-/markdown-it-plantuml-0.3.1.tgz#f338df4d691a5561364e65809b6812bcb3d8b047"
@@ -5849,6 +5855,10 @@ string-width@^2.0.0:
     is-fullwidth-code-point "^2.0.0"
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
     strip-ansi "^4.0.0"
 
 
+string@^3.0.1:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/string/-/string-3.3.3.tgz#5ea211cd92d228e184294990a6cc97b366a77cb0"
+
 string_decoder@^1.0.0, string_decoder@~1.0.3:
 string_decoder@^1.0.0, string_decoder@~1.0.3:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"