From 7d544c5c3b2ae8e8ba980b96f77bf8d9b329eba0 Mon Sep 17 00:00:00 2001 From: ppoffice Date: Sun, 22 Dec 2019 17:23:49 -0500 Subject: [PATCH] refactor(layout): widget to jsx --- layout/.eslintrc | 4 +- .../{back-to-top.jsx => back_to_top.jsx} | 0 ...aidu-analytics.jsx => baidu_analytics.jsx} | 0 ...gle-analytics.jsx => google_analytics.jsx} | 0 ...dated-browser.jsx => outdated_browser.jsx} | 0 layout/util/cache.jsx | 14 +- layout/widget/archive.ejs | 23 --- layout/widget/archive.locals.js | 3 - layout/widget/archives.jsx | 99 +++++++++++ layout/widget/categories.jsx | 99 +++++++++++ layout/widget/category.ejs | 1 - layout/widget/category.locals.js | 3 - layout/widget/links.ejs | 23 --- layout/widget/links.jsx | 46 +++++ layout/widget/links.locals.js | 8 - layout/widget/profile.ejs | 101 ----------- layout/widget/profile.jsx | 159 ++++++++++++++++++ layout/widget/profile.locals.js | 3 - layout/widget/recent_posts.ejs | 32 ---- layout/widget/recent_posts.jsx | 64 +++++++ layout/widget/recent_posts.locals.js | 18 -- layout/widget/subscribe_email.ejs | 30 ---- layout/widget/subscribe_email.jsx | 50 ++++++ layout/widget/subscribe_email.locals.js | 3 - layout/widget/tag.ejs | 1 - layout/widget/tagcloud.ejs | 8 - layout/widget/tagcloud.locals.js | 6 - layout/widget/tags.jsx | 55 ++++++ layout/widget/toc.ejs | 38 ----- layout/widget/toc.jsx | 135 +++++++++++++++ layout/widget/toc.locals.js | 8 - 31 files changed, 720 insertions(+), 314 deletions(-) rename layout/plugin/{back-to-top.jsx => back_to_top.jsx} (100%) rename layout/plugin/{baidu-analytics.jsx => baidu_analytics.jsx} (100%) rename layout/plugin/{google-analytics.jsx => google_analytics.jsx} (100%) rename layout/plugin/{outdated-browser.jsx => outdated_browser.jsx} (100%) delete mode 100644 layout/widget/archive.ejs delete mode 100644 layout/widget/archive.locals.js create mode 100644 layout/widget/archives.jsx create mode 100644 layout/widget/categories.jsx delete mode 100644 layout/widget/category.ejs delete mode 100644 layout/widget/category.locals.js delete mode 100644 layout/widget/links.ejs create mode 100644 layout/widget/links.jsx delete mode 100644 layout/widget/links.locals.js delete mode 100644 layout/widget/profile.ejs create mode 100644 layout/widget/profile.jsx delete mode 100644 layout/widget/profile.locals.js delete mode 100644 layout/widget/recent_posts.ejs create mode 100644 layout/widget/recent_posts.jsx delete mode 100644 layout/widget/recent_posts.locals.js delete mode 100644 layout/widget/subscribe_email.ejs create mode 100644 layout/widget/subscribe_email.jsx delete mode 100644 layout/widget/subscribe_email.locals.js delete mode 100644 layout/widget/tag.ejs delete mode 100644 layout/widget/tagcloud.ejs delete mode 100644 layout/widget/tagcloud.locals.js create mode 100644 layout/widget/tags.jsx delete mode 100644 layout/widget/toc.ejs create mode 100644 layout/widget/toc.jsx delete mode 100644 layout/widget/toc.locals.js diff --git a/layout/.eslintrc b/layout/.eslintrc index 952e99b..00f715b 100644 --- a/layout/.eslintrc +++ b/layout/.eslintrc @@ -26,7 +26,9 @@ { "allowModules": [ "inferno", - "inferno-create-element" + "inferno-create-element", + "hexo-util", + "cheerio" ] } ] diff --git a/layout/plugin/back-to-top.jsx b/layout/plugin/back_to_top.jsx similarity index 100% rename from layout/plugin/back-to-top.jsx rename to layout/plugin/back_to_top.jsx diff --git a/layout/plugin/baidu-analytics.jsx b/layout/plugin/baidu_analytics.jsx similarity index 100% rename from layout/plugin/baidu-analytics.jsx rename to layout/plugin/baidu_analytics.jsx diff --git a/layout/plugin/google-analytics.jsx b/layout/plugin/google_analytics.jsx similarity index 100% rename from layout/plugin/google-analytics.jsx rename to layout/plugin/google_analytics.jsx diff --git a/layout/plugin/outdated-browser.jsx b/layout/plugin/outdated_browser.jsx similarity index 100% rename from layout/plugin/outdated-browser.jsx rename to layout/plugin/outdated_browser.jsx diff --git a/layout/util/cache.jsx b/layout/util/cache.jsx index 3994b94..97e1e48 100644 --- a/layout/util/cache.jsx +++ b/layout/util/cache.jsx @@ -20,13 +20,16 @@ module.exports = { * @param {string} prefix Cache ID prefix * @param {Function} transform Transform the input props to target props and * its result is used to compute cache ID - * @returns Cached JSX element if cache ID is found. - * Return null if the props transform result is empty, which means the props + * @returns A cache-enabled component. + * It returns cached JSX element when called if cache ID is found. + * It returns null if the props transform result is empty, which means the props * passed to the createElement() indicates the element does not need to be created. - * Otherwise, create a new element and cache it if the transform function is provided. + * Otherwise, it creates a new element and caches it if the transform function is provided. + * The original component can be accessed from the `_type` property of the return value. + * The props transform function can be accessed from the `_transform` property of the return value. */ cacheComponent(type, prefix, transform) { - return props => { + const component = props => { const targetProps = transform(props); if (!targetProps) { return null; @@ -40,5 +43,8 @@ module.exports = { } return cache[cacheId]; }; + component._type = type; + component._transform = transform; + return component; } }; diff --git a/layout/widget/archive.ejs b/layout/widget/archive.ejs deleted file mode 100644 index 20298e0..0000000 --- a/layout/widget/archive.ejs +++ /dev/null @@ -1,23 +0,0 @@ -
-
- -
-
\ No newline at end of file diff --git a/layout/widget/archive.locals.js b/layout/widget/archive.locals.js deleted file mode 100644 index bec3126..0000000 --- a/layout/widget/archive.locals.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = (ctx, locals) => { - return locals; -} \ No newline at end of file diff --git a/layout/widget/archives.jsx b/layout/widget/archives.jsx new file mode 100644 index 0000000..92d5cd8 --- /dev/null +++ b/layout/widget/archives.jsx @@ -0,0 +1,99 @@ +'use strict'; + +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +class Archives extends Component { + render() { + const { + items, + title, + showCount + } = this.props; + + return
+
+ +
+
; + } +} + +module.exports = cacheComponent(Archives, 'widget.archives', props => { + // adapted from hexo/lib/plugins/helper/list_archives.js + const { config, page, type = 'monthly', order = -1, url_for, _p } = props; + const posts = props.site.posts.sort('date', order); + if (!posts.length) { + return null; + } + + const language = page.lang || page.language || config.language; + const format = props.format || type === 'monthly' ? 'MMMM YYYY' : 'YYYY'; + const showCount = Object.prototype.hasOwnProperty.call(props, 'show_count') ? props.show_count : true; + + const data = []; + let length = 0; + + posts.forEach(post => { + // Clone the date object to avoid pollution + let date = post.date.clone(); + + if (config.timezone) { + date = date.tz(config.timezone); + } + if (language) { + date = date.locale(language); + } + + const year = date.year(); + const month = date.month() + 1; + const name = date.format(format); + const lastData = data[length - 1]; + + if (!lastData || lastData.name !== name) { + length = data.push({ + name, + year, + month, + count: 1 + }); + } else { + lastData.count++; + } + }); + + const link = item => { + let url = `${config.archive_dir}/${item.year}/`; + + if (type === 'monthly') { + if (item.month < 10) url += '0'; + url += `${item.month}/`; + } + + return url_for(url); + }; + + return { + items: data.map(item => ({ + name: item.name, + count: item.count, + url: link(item) + })), + title: _p('common.archive', Infinity), + showCount + }; +}); diff --git a/layout/widget/categories.jsx b/layout/widget/categories.jsx new file mode 100644 index 0000000..39fd7a7 --- /dev/null +++ b/layout/widget/categories.jsx @@ -0,0 +1,99 @@ +'use strict'; + +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +class Categories extends Component { + renderList(categories, showCount) { + return categories.map(category =>
  • + + + {category.name} + + {showCount ? + {category.count} + : null} + + {category.children.length ? : null} +
  • ); + } + + render() { + const { + title, + showCount, + categories + } = this.props; + + return
    +
    + +
    +
    ; + } +} + +module.exports = cacheComponent(Categories, 'widget.categories', props => { + // adapted from hexo/lib/plugins/helper/list_categories.js + const categories = props.categories || props.site.categories; + if (!categories.length) { + return null; + } + const { orderBy = 'name', order = 1, page, url_for, _p } = props; + const depth = props.depth ? parseInt(props.depth, 10) : 0; + const showCurrent = props.show_current || false; + const showCount = Object.prototype.hasOwnProperty.call(props, 'show_count') ? props.show_count : true; + + function prepareQuery(parent) { + const query = {}; + + if (parent) { + query.parent = parent; + } else { + query.parent = { $exists: false }; + } + + return categories.find(query).sort(orderBy, order).filter(cat => cat.length); + } + + function hierarchicalList(level, parent) { + return prepareQuery(parent).map((cat, i) => { + let children = []; + if (!depth || level + 1 < depth) { + children = hierarchicalList(level + 1, cat._id); + } + + let isCurrent = false; + if (showCurrent && page) { + for (let j = 0; j < cat.length; j++) { + const post = cat.posts.data[j]; + if (post && post._id === page._id) { + isCurrent = true; + break; + } + } + // special case: category page + isCurrent = isCurrent || (page.base && page.base.startsWith(cat.path)); + } + + return { + children, + isCurrent, + name: cat.name, + count: cat.length, + url: url_for(cat.path) + }; + }); + } + + return { + showCount, + categories: hierarchicalList(0), + title: _p('common.category', Infinity) + }; +}); diff --git a/layout/widget/category.ejs b/layout/widget/category.ejs deleted file mode 100644 index 6aa4862..0000000 --- a/layout/widget/category.ejs +++ /dev/null @@ -1 +0,0 @@ -<%- _partial('categories') %> \ No newline at end of file diff --git a/layout/widget/category.locals.js b/layout/widget/category.locals.js deleted file mode 100644 index bec3126..0000000 --- a/layout/widget/category.locals.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = (ctx, locals) => { - return locals; -} \ No newline at end of file diff --git a/layout/widget/links.ejs b/layout/widget/links.ejs deleted file mode 100644 index 3b2c931..0000000 --- a/layout/widget/links.ejs +++ /dev/null @@ -1,23 +0,0 @@ -
    -
    - -
    -
    diff --git a/layout/widget/links.jsx b/layout/widget/links.jsx new file mode 100644 index 0000000..78c76c7 --- /dev/null +++ b/layout/widget/links.jsx @@ -0,0 +1,46 @@ +'use strict'; + +const { URL } = require('url'); +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +class Links extends Component { + render() { + const { title, links } = this.props; + return
    +
    + +
    +
    ; + } +} + +module.exports = cacheComponent(Links, 'widget.links', props => { + if (!Object.keys(props.links).length) { + return null; + } + return { + title: props.__('widget.links'), + links: props.links + }; +}); diff --git a/layout/widget/links.locals.js b/layout/widget/links.locals.js deleted file mode 100644 index 3f76a55..0000000 --- a/layout/widget/links.locals.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = (ctx, locals) => { - const { get_config_from_obj } = ctx; - const links = get_config_from_obj(locals.widget, 'links'); - if (Object.keys(links).length == 0) { - return null; - } - return Object.assign(locals, { links }); -} \ No newline at end of file diff --git a/layout/widget/profile.ejs b/layout/widget/profile.ejs deleted file mode 100644 index 5c1eef8..0000000 --- a/layout/widget/profile.ejs +++ /dev/null @@ -1,101 +0,0 @@ -<% function avatar() { - const avatar = get_config_from_obj(widget, 'avatar'); - const gravatar_email = get_config_from_obj(widget, 'gravatar'); - if (gravatar_email) { - return gravatar(gravatar_email, 128); - } - if (avatar) { - return url_for(avatar) - } - return url_for('images/avatar.png'); -} %> -
    -
    - - - <% if (widget.follow_link) { %> - - <% } %> - <% const socialLinks = get_config_from_obj(widget, 'social_links'); %> - <% if (socialLinks !== null) { %> -
    - <% for (let name in socialLinks) { - let link = socialLinks[name]; %> - - <% if (typeof(link) === 'string') { %> - <%= name %> - <% } else { %> - - <% } %> - - <% } %> -
    - <% } %> -
    -
    \ No newline at end of file diff --git a/layout/widget/profile.jsx b/layout/widget/profile.jsx new file mode 100644 index 0000000..7ca9e5c --- /dev/null +++ b/layout/widget/profile.jsx @@ -0,0 +1,159 @@ +'use strict'; + +const { Component } = require('inferno'); +const gravatrHelper = require('hexo-util').gravatar; +const { cacheComponent } = require('../util/cache'); + +class Profile extends Component { + renderSocialLinks(links) { + if (!links.length) { + return null; + } + return
    + {links.map(link => { + return + {Object.prototype.hasOwnProperty.call(link, 'icon') ? : link.name} + ; + })} +
    ; + } + + render() { + const { + avatar, + avatarRounded, + author, + authorTitle, + location, + counter, + followLink, + followTitle, + socialLinks + } = this.props; + return
    +
    + + + {followLink ? : null} + {this.renderSocialLinks(socialLinks)} +
    +
    ; + } +} + +module.exports = cacheComponent(Profile, 'widget.profile', props => { + const { + avatar, + gravatar, + avatar_rounded = false, + author = props.config.author, + author_title, + location, + follow_link, + social_links, + site, + url_for, + _p, + __ + } = props; + + function getAvatar() { + if (gravatar) { + return gravatrHelper(gravatar, 128); + } + if (avatar) { + return url_for(avatar); + } + return url_for('/images/avatar.png'); + } + + const postCount = site.posts.length; + const categoryCount = site.categories.filter(category => category.length).length; + const tagCount = site.tags.filter(tag => tag.length).length; + + const socialLinks = Object.keys(social_links).map(name => { + const link = social_links[name]; + if (typeof link === 'string') { + return { + name, + url: url_for(link) + }; + } + return { + name, + url: url_for(link.name), + icon: link.icon + }; + + }); + + return { + avatar: getAvatar(), + avatarRounded: avatar_rounded, + author, + authorTitle: author_title, + location, + counter: { + post: { + count: postCount, + title: _p('common.post', postCount), + url: url_for('/archives') + }, + category: { + count: categoryCount, + title: _p('common.category', categoryCount), + url: url_for('/categories') + }, + tag: { + count: tagCount, + title: _p('common.tag', tagCount), + url: url_for('/tags') + } + }, + followLink: url_for(follow_link), + followTitle: __('widget.follow'), + socialLinks + }; +}); diff --git a/layout/widget/profile.locals.js b/layout/widget/profile.locals.js deleted file mode 100644 index bec3126..0000000 --- a/layout/widget/profile.locals.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = (ctx, locals) => { - return locals; -} \ No newline at end of file diff --git a/layout/widget/recent_posts.ejs b/layout/widget/recent_posts.ejs deleted file mode 100644 index bc4d87a..0000000 --- a/layout/widget/recent_posts.ejs +++ /dev/null @@ -1,32 +0,0 @@ -
    -
    - - <% posts.forEach(post => { %> -
    - <% if (thumbnail) { %> - -

    - <%= post.title %> -

    -
    - <% } %> -
    -
    -
    - <%= post.title %> -

    - <%- list_categories(post.categories(), { - show_count: false, - class: 'has-link-grey ', - depth:2, - style: 'none', - separator: ' / '}) %> -

    -
    -
    -
    - <% }) %> -
    -
    \ No newline at end of file diff --git a/layout/widget/recent_posts.jsx b/layout/widget/recent_posts.jsx new file mode 100644 index 0000000..ff04592 --- /dev/null +++ b/layout/widget/recent_posts.jsx @@ -0,0 +1,64 @@ +'use strict'; + +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +class RecentPosts extends Component { + render() { + const { title, thumbnail, posts } = this.props; + + return
    +
    + + {posts.map(post => { + const categories = []; + post.categories.forEach((category, i) => { + categories.push({category.name}); + if (i < post.categories.length - 1) { + categories.push('/'); + } + }); + return ; + })} +
    +
    ; + } +} + +module.exports = cacheComponent(RecentPosts, 'widget.recentposts', props => { + const { site, config, get_thumbnail, url_for, __, date_xml, date } = props; + if (!site.posts.length) { + return null; + } + const thumbnail = config.article && config.article.thumbnail; + const posts = site.posts.sort('date', -1).limit(5).map(post => ({ + url: url_for(post.permalink || post.link || post.path), + title: post.title, + date: date(post.date), + dateXml: date_xml(post.date), + thumbnail: thumbnail ? get_thumbnail(post) : null, + // TODO: check if categories work + categories: post.categories.map(category => ({ + name: category.name, + url: url_for(category.path) + })) + })); + return { + posts, + thumbnail, + title: __('widget.recents') + }; +}); diff --git a/layout/widget/recent_posts.locals.js b/layout/widget/recent_posts.locals.js deleted file mode 100644 index d245195..0000000 --- a/layout/widget/recent_posts.locals.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = (ctx, locals) => { - const { has_config, get_config, get_thumbnail } = ctx; - const { posts } = ctx.site; - if (!posts.length) { - return null; - } - const thumbnail = !has_config('article.thumbnail') || get_config('article.thumbnail') !== false; - const _posts = posts.sort('date', -1).limit(5).map(post => ({ - link: post.link, - path: post.path, - title: post.title, - date: post.date, - thumbnail: thumbnail ? get_thumbnail(post) : null, - // fix circular JSON serialization issue - categories: () => post.categories - })); - return Object.assign(locals, { thumbnail, posts: _posts }); -} \ No newline at end of file diff --git a/layout/widget/subscribe_email.ejs b/layout/widget/subscribe_email.ejs deleted file mode 100644 index 01f7214..0000000 --- a/layout/widget/subscribe_email.ejs +++ /dev/null @@ -1,30 +0,0 @@ -
    -
    - -
    -
    diff --git a/layout/widget/subscribe_email.jsx b/layout/widget/subscribe_email.jsx new file mode 100644 index 0000000..8224c36 --- /dev/null +++ b/layout/widget/subscribe_email.jsx @@ -0,0 +1,50 @@ +'use strict'; + +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +class SubscribeEmail extends Component { + render() { + const { title, description, feedburnerId, buttonTitle } = this.props; + + return
    +
    + +
    +
    ; + } +} + +module.exports = cacheComponent(SubscribeEmail, 'widget.subscribeemail', props => { + const { feedburner_id, description, __ } = props; + + return { + title: __('widget.email.title'), + feedburnerId: feedburner_id, + description, + buttonTitle: __('widget.email.button') + }; +}); diff --git a/layout/widget/subscribe_email.locals.js b/layout/widget/subscribe_email.locals.js deleted file mode 100644 index fe59b94..0000000 --- a/layout/widget/subscribe_email.locals.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = (ctx, locals) => { - return locals; -} diff --git a/layout/widget/tag.ejs b/layout/widget/tag.ejs deleted file mode 100644 index 6fdbbcc..0000000 --- a/layout/widget/tag.ejs +++ /dev/null @@ -1 +0,0 @@ -<%- _partial('tags') %> \ No newline at end of file diff --git a/layout/widget/tagcloud.ejs b/layout/widget/tagcloud.ejs deleted file mode 100644 index 1c04236..0000000 --- a/layout/widget/tagcloud.ejs +++ /dev/null @@ -1,8 +0,0 @@ -
    -
    - - <%- tagcloud() %> -
    -
    \ No newline at end of file diff --git a/layout/widget/tagcloud.locals.js b/layout/widget/tagcloud.locals.js deleted file mode 100644 index f207e5e..0000000 --- a/layout/widget/tagcloud.locals.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = (ctx, locals) => { - if (!ctx.site.tags.length) { - return null; - } - return locals; -} \ No newline at end of file diff --git a/layout/widget/tags.jsx b/layout/widget/tags.jsx new file mode 100644 index 0000000..63b5f6a --- /dev/null +++ b/layout/widget/tags.jsx @@ -0,0 +1,55 @@ +'use strict'; + +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +class Tags extends Component { + render() { + const { + tags, + title, + showCount + } = this.props; + + return
    +
    + +
    +
    ; + } +} + +module.exports = cacheComponent(Tags, 'widget.tags', props => { + // adapted from hexo/lib/plugins/helper/list_tags.js + let tags = props.tags || props.site.tags; + if (!tags.length) { + return null; + } + const { orderBy = 'name', order = 1, amount, url_for, _p } = props; + const showCount = Object.prototype.hasOwnProperty.call(props, 'show_count') ? props.show_count : true; + + tags = tags.sort(orderBy, order).filter(tag => tag.length); + if (amount) { + tags = tags.limit(amount); + } + + return { + showCount, + title: _p('common.tag', Infinity), + tags: tags.map(tag => ({ + name: tag.name, + count: tag.length, + url: url_for(tag.path) + })) + }; +}); diff --git a/layout/widget/toc.ejs b/layout/widget/toc.ejs deleted file mode 100644 index 69b5c96..0000000 --- a/layout/widget/toc.ejs +++ /dev/null @@ -1,38 +0,0 @@ -<% function buildToc(toc) { - let result = ''; - if (toc.hasOwnProperty('id') && toc.hasOwnProperty('index') && toc.hasOwnProperty('text')) { - result += `
  • - - ${toc.index} - ${toc.text} - `; - } - let keys = Object.keys(toc); - keys.indexOf('id') > -1 && keys.splice(keys.indexOf('id'), 1); - keys.indexOf('text') > -1 && keys.splice(keys.indexOf('text'), 1); - keys.indexOf('index') > -1 && keys.splice(keys.indexOf('index'), 1); - keys = keys.map(k => parseInt(k)).sort((a, b) => a - b); - if (keys.length > 0) { - result += ''; - } - if (toc.hasOwnProperty('id') && toc.hasOwnProperty('index') && toc.hasOwnProperty('text')) { - result += '
  • '; - } - return result; -} %> -<% let tocContent = buildToc(_toc(content)); if (tocContent !== undefined && tocContent !== ''){ %> -
    -
    - -
    -
    -<% } %> \ No newline at end of file diff --git a/layout/widget/toc.jsx b/layout/widget/toc.jsx new file mode 100644 index 0000000..f3cd3da --- /dev/null +++ b/layout/widget/toc.jsx @@ -0,0 +1,135 @@ +'use strict'; + +const cheerio = require('cheerio'); +const { Component } = require('inferno'); +const { cacheComponent } = require('../util/cache'); + +/** + * Export a tree of headings of an article + * { + * "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 $ = cheerio.load(content, { decodeEntities: false }); + const toc = {}; + const levels = [0, 0, 0]; + // Get top 3 headings that are present in the content + const tags = [1, 2, 3, 4, 5, 6].map(i => 'h' + i).filter(h => $(h).length > 0).slice(0, 3); + if (tags.length === 0) { + return toc; + } + $(tags.join(',')).each(function() { + const level = tags.indexOf(this.name); + const id = $(this).attr('id'); + const text = $(this).text(); + + 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 (!Object.prototype.hasOwnProperty.call(node, i)) { + node[i] = {}; + } + node = node[i]; + } + node.id = id; + node.text = text; + node.index = levels.slice(0, level + 1).join('.'); + }); + return toc; +} + +class Toc extends Component { + renderToc(toc) { + 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 = ; + } + if (Object.prototype.hasOwnProperty.call(toc, 'id') + && Object.prototype.hasOwnProperty.call(toc, 'index') + && Object.prototype.hasOwnProperty.call(toc, 'text')) { + result =
  • + + {toc.index} + {toc.text} + + {result} +
  • ; + } + return result; + } + + render() { + const toc = getToc(this.props.content); + if (!Object.keys(toc).length) { + return null; + } + + return
    +
    + +
    +
    ; + } +} + +module.exports = cacheComponent(Toc, 'widget.toc', props => { + const { toc, page, _p } = props; + const { layout, content } = page; + + if (toc !== true || (layout !== 'page' && layout !== 'post')) { + return null; + } + + return { + title: _p('widget.catalogue', Infinity), + content + }; +}); diff --git a/layout/widget/toc.locals.js b/layout/widget/toc.locals.js deleted file mode 100644 index 0011f9a..0000000 --- a/layout/widget/toc.locals.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = (ctx, locals) => { - const { layout, content } = ctx.page; - const { get_config } = ctx; - if (get_config('toc') !== true || (layout !== 'page' && layout !== 'post')) { - return null; - } - return Object.assign(locals, { content }); -} \ No newline at end of file