UNPKG

hexo-theme-amazing

Version:

A simple, delicate, and modern theme for Hexo

174 lines (160 loc) 5.43 kB
const {tocObj: getTocObj, unescapeHTML} = require('hexo-util'); const {Component} = require('inferno'); const {cacheComponent} = require('hexo-component-inferno/lib/util/cache'); /** * Table of contents widget JSX component. * @module view/widget/toc */ /** * Export a tree of headings of an article * @private * @example * getToc('HTML content...'); * // { * // "1": { * // "id": "How-to-enable-table-of-content-for-a-post", * // "text": "How to enable table of content for a post", * // "index": "1" * // }, * // "2": { * // "1": { * // "1": { * // "id": "Third-level-title", * // "text": "Third level title", * // "index": "2.1.1" * // }, * // "id": "Second-level-title", * // "text": "Second level title", * // "index": "2.1" * // }, * // "2": { * // "id": "Another-second-level-title", * // "text": "Another second level title", * // "index": "2.2" * // }, * // "id": "First-level-title", * // "text": "First level title", * // "index": "2" * // } * // } */ function getToc(content) { const toc = {}; const levels = [0, 0, 0]; const tocObj = getTocObj(content, {min_depth: 1, max_depth: 6}); const minLevel = Math.min(...tocObj.map((item) => item.level)); tocObj.forEach((item) => { const {text, id} = item; const level = item.level - minLevel; for (let i = 0; i < levels.length; i++) { if (i > level) { levels[i] = 0; } else if (i < level) { if (levels[i] === 0) { // if headings start with a lower level heading, set the former heading index to 1 // e.g. h3, h2, h1, h2, h3 => 1.1.1, 1.2, 2, 2.1, 2.1.1 levels[i] = 1; } } else { levels[i] += 1; } } let node = toc; for (const i of levels.slice(0, level + 1)) { if (!(i in node)) { node[i] = {}; } node = node[i]; } node.id = id; node.text = text; node.index = levels.slice(0, level + 1).join('.'); }); return toc; } /** * Table of contents widget JSX component. * * @example * <Toc * title="Widget title" * content="HTML content" * jsUrl="******" /> */ class Toc extends Component { renderToc(toc, showIndex = true) { let result; const keys = Object.keys(toc) .filter((key) => !['id', 'index', 'text'].includes(key)) .map((key) => parseInt(key, 10)) .sort((a, b) => a - b); if (keys.length > 0) { result = <ul class="menu-list">{keys.map((i) => this.renderToc(toc[i], showIndex))}</ul>; } if ('id' in toc && 'index' in toc && 'text' in toc) { result = ( <li> <a class="is-flex is-mobile" href={'#' + toc.id}> {showIndex && <span class="mr-2">{toc.index}</span>} <span>{unescapeHTML(toc.text)}</span> </a> {result} </li> ); } return result; } render() { const {showIndex} = this.props; const toc = getToc(this.props.content); if (!Object.keys(toc).length) { return null; } const css = '.menu-list > li > a.is-active + .menu-list { display: block; }' + '.menu-list > li > a + .menu-list { display: none; }'; return ( <div class="card widget" id="toc" data-type="toc"> <div class="card-content"> <div class="menu"> <h3 class="menu-label">{this.props.title}</h3> {this.renderToc(toc, showIndex)} </div> </div> <style dangerouslySetInnerHTML={{__html: css}}></style> <script src={this.props.jsUrl} defer={true}></script> </div> ); } } /** * Cacheable table of contents widget JSX component. * <p> * This class is supposed to be used in combination with the <code>locals</code> hexo filter * ({@link module:hexo/filter/locals}). * * @see module:util/cache.cacheComponent * @example * <Toc.Cacheable * config={{ toc: true }} * page={{ layout: 'post', content: 'HTML content' }} * helper={{ * _p: function() {...}, * url_for: function() {...} * }} /> /> */ Toc.Cacheable = cacheComponent(Toc, 'widget.toc', (props) => { const {config, page, widget, helper} = props; const {layout, content, encrypt, origin} = page; const {index} = widget; if (config.toc !== true || (layout !== 'page' && layout !== 'post') || encrypt) { return null; } return { title: helper._p('widget.catalogue', Infinity), showIndex: index !== false, content: encrypt ? origin : content, jsUrl: helper.url_for('/js/toc.js'), }; }); module.exports = Toc;