refactor(layout): widget to jsx
parent
0dab77811f
commit
7d544c5c3b
|
@ -26,7 +26,9 @@
|
|||
{
|
||||
"allowModules": [
|
||||
"inferno",
|
||||
"inferno-create-element"
|
||||
"inferno-create-element",
|
||||
"hexo-util",
|
||||
"cheerio"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<div class="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">
|
||||
<%= _p('common.archive', Infinity) %>
|
||||
</h3>
|
||||
<ul class="menu-list">
|
||||
<% _list_archives().forEach(archive => { %>
|
||||
<li>
|
||||
<a class="level is-marginless" href="<%= archive.url %>">
|
||||
<span class="level-start">
|
||||
<span class="level-item"><%= archive.name %></span>
|
||||
</span>
|
||||
<span class="level-end">
|
||||
<span class="level-item tag"><%= archive.count %></span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = (ctx, locals) => {
|
||||
return locals;
|
||||
}
|
|
@ -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 <div className="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">{title}</h3>
|
||||
<ul class="menu-list">
|
||||
{items.map(archive => <li>
|
||||
<a class="level is-marginless" href={archive.url}>
|
||||
<span class="level-start">
|
||||
<span class="level-item">{archive.name}</span>
|
||||
</span>
|
||||
{showCount ? <span class="level-end">
|
||||
<span class="level-item tag">{archive.count}</span>
|
||||
</span> : null}
|
||||
</a>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
|
@ -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 => <li>
|
||||
<a class="level is-marginless" href={category.url}>
|
||||
<span class="level-start">
|
||||
<span class="level-item">{category.name}</span>
|
||||
</span>
|
||||
{showCount ? <span class="level-end">
|
||||
<span class="level-item tag">{category.count}</span>
|
||||
</span> : null}
|
||||
</a>
|
||||
{category.children.length ? <ul>{this.renderList(category.children)}</ul> : null}
|
||||
</li>);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
showCount,
|
||||
categories
|
||||
} = this.props;
|
||||
|
||||
return <div className="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">{title}</h3>
|
||||
<ul class="menu-list">
|
||||
{this.renderList(categories, showCount)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
<%- _partial('categories') %>
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = (ctx, locals) => {
|
||||
return locals;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<div class="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">
|
||||
<%= __('widget.links') %>
|
||||
</h3>
|
||||
<ul class="menu-list">
|
||||
<% for (let i in links) { %>
|
||||
<li>
|
||||
<a class="level is-mobile" href="<%- links[i] %>" target="_blank" rel="noopener">
|
||||
<span class="level-left">
|
||||
<span class="level-item"><%= i %></span>
|
||||
</span>
|
||||
<span class="level-right">
|
||||
<span class="level-item tag"><%- get_domain(links[i]) %></span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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 <div class="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">{title}</h3>
|
||||
<ul class="menu-list">
|
||||
{Object.keys(links).map(i => {
|
||||
let hostname = links[i];
|
||||
try {
|
||||
hostname = new URL(hostname).hostname;
|
||||
} catch (e) { }
|
||||
return <li>
|
||||
<a class="level is-mobile" href="<%- links[i] %>" target="_blank" rel="noopener">
|
||||
<span class="level-left">
|
||||
<span class="level-item">{i}</span>
|
||||
</span>
|
||||
<span class="level-right">
|
||||
<span class="level-item tag">{hostname}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = cacheComponent(Links, 'widget.links', props => {
|
||||
if (!Object.keys(props.links).length) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
title: props.__('widget.links'),
|
||||
links: props.links
|
||||
};
|
||||
});
|
|
@ -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 });
|
||||
}
|
|
@ -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');
|
||||
} %>
|
||||
<div class="card widget">
|
||||
<div class="card-content">
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered" style="flex-shrink: 1">
|
||||
<div>
|
||||
<% const is_rounded = get_config_from_obj(widget, 'avatar_rounded', false); %>
|
||||
<figure class="image is-128x128 has-mb-6">
|
||||
<img class="<%= is_rounded ? 'is-rounded' : '' %>" src="<%= avatar() %>" alt="<%= get_config_from_obj(widget, 'author') %>">
|
||||
</figure>
|
||||
<% if (get_config_from_obj(widget, 'author')) { %>
|
||||
<p class="is-size-4 is-block">
|
||||
<%= get_config_from_obj(widget, 'author') %>
|
||||
</p>
|
||||
<% } %>
|
||||
<% if (get_config_from_obj(widget, 'author_title')) { %>
|
||||
<p class="is-size-6 is-block">
|
||||
<%= get_config_from_obj(widget, 'author_title') %>
|
||||
</p>
|
||||
<% } %>
|
||||
<% if (get_config_from_obj(widget, 'location')) { %>
|
||||
<p class="is-size-6 is-flex is-flex-center has-text-grey">
|
||||
<i class="fas fa-map-marker-alt has-mr-7"></i>
|
||||
<span><%= get_config_from_obj(widget, 'location') %></span>
|
||||
</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-item has-text-centered is-marginless">
|
||||
<div>
|
||||
<p class="heading">
|
||||
<%= _p('common.post', post_count()) %>
|
||||
</p>
|
||||
<a href="<%= url_for('/archives') %>">
|
||||
<p class="title has-text-weight-normal">
|
||||
<%= post_count() %>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered is-marginless">
|
||||
<div>
|
||||
<p class="heading">
|
||||
<%= _p('common.category', category_count()) %>
|
||||
</p>
|
||||
<a href="<%= url_for('/categories') %>">
|
||||
<p class="title has-text-weight-normal">
|
||||
<%= category_count() %>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered is-marginless">
|
||||
<div>
|
||||
<p class="heading">
|
||||
<%= _p('common.tag', tag_count()) %>
|
||||
</p>
|
||||
<a href="<%= url_for('/tags') %>">
|
||||
<p class="title has-text-weight-normal">
|
||||
<%= tag_count() %>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<% if (widget.follow_link) { %>
|
||||
<div class="level">
|
||||
<a class="level-item button is-link is-rounded" href="<%= url_for(widget.follow_link) %>" target="_blank" rel="noopener">
|
||||
<%= __('widget.follow') %></a>
|
||||
</div>
|
||||
<% } %>
|
||||
<% const socialLinks = get_config_from_obj(widget, 'social_links'); %>
|
||||
<% if (socialLinks !== null) { %>
|
||||
<div class="level is-mobile">
|
||||
<% for (let name in socialLinks) {
|
||||
let link = socialLinks[name]; %>
|
||||
<a class="level-item button is-white is-marginless" target="_blank" rel="noopener"
|
||||
title="<%= name %>" href="<%= url_for(typeof(link) === 'string' ? link : link.url) %>">
|
||||
<% if (typeof(link) === 'string') { %>
|
||||
<%= name %>
|
||||
<% } else { %>
|
||||
<i class="<%= link.icon %>"></i>
|
||||
<% } %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
|
@ -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 <div class="level is-mobile">
|
||||
{links.map(link => {
|
||||
return <a class="level-item button is-white is-marginless"
|
||||
target="_blank" rel="noopener" title={link.name} href={link.url}>
|
||||
{Object.prototype.hasOwnProperty.call(link, 'icon') ? <i class={link.icon}></i> : link.name}
|
||||
</a>;
|
||||
})}
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
avatar,
|
||||
avatarRounded,
|
||||
author,
|
||||
authorTitle,
|
||||
location,
|
||||
counter,
|
||||
followLink,
|
||||
followTitle,
|
||||
socialLinks
|
||||
} = this.props;
|
||||
return <div class="card widget">
|
||||
<div class="card-content">
|
||||
<nav class="level">
|
||||
<div class="level-item has-text-centered" style="flex-shrink: 1">
|
||||
<div>
|
||||
<figure class="image is-128x128 has-mb-6">
|
||||
<img class={avatarRounded ? 'is-rounded' : ''} src={avatar} alt={author} />
|
||||
</figure>
|
||||
{author ? <p class="is-size-4 is-block">{author}</p> : null}
|
||||
{authorTitle ? <p class="is-size-6 is-block">{authorTitle}</p> : null}
|
||||
{location ? <p class="is-size-6 is-flex is-flex-center has-text-grey">
|
||||
<i class="fas fa-map-marker-alt has-mr-7"></i>
|
||||
<span>{location}</span>
|
||||
</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-item has-text-centered is-marginless">
|
||||
<div>
|
||||
<p class="heading">{counter.post.title}</p>
|
||||
<a href={counter.post.url}>
|
||||
<p class="title has-text-weight-normal">{counter.post.count}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered is-marginless">
|
||||
<div>
|
||||
<p class="heading">{counter.category.title}</p>
|
||||
<a href={counter.category.url}>
|
||||
<p class="title has-text-weight-normal">{counter.category.count}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered is-marginless">
|
||||
<div>
|
||||
<p class="heading">{counter.tag.title}</p>
|
||||
<a href={counter.tag.url}>
|
||||
<p class="title has-text-weight-normal">{counter.tag.count}</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{followLink ? <div class="level">
|
||||
<a class="level-item button is-link is-rounded" href={followLink} target="_blank" rel="noopener">{followTitle}</a>
|
||||
</div> : null}
|
||||
{this.renderSocialLinks(socialLinks)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = (ctx, locals) => {
|
||||
return locals;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<div class="card widget">
|
||||
<div class="card-content">
|
||||
<h3 class="menu-label">
|
||||
<%= __('widget.recents') %>
|
||||
</h3>
|
||||
<% posts.forEach(post => { %>
|
||||
<article class="media">
|
||||
<% if (thumbnail) { %>
|
||||
<a href="<%- url_for(post.link ? post.link : post.path) %>" class="media-left">
|
||||
<p class="image is-64x64">
|
||||
<img class="thumbnail" src="<%= post.thumbnail %>" alt="<%= post.title %>">
|
||||
</p>
|
||||
</a>
|
||||
<% } %>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<div><time class="has-text-grey is-size-7 is-uppercase" datetime="<%= date_xml(post.date) %>"><%= date(post.date) %></time></div>
|
||||
<a href="<%- url_for((post.link?post.link:post.path)) %>" class="title has-link-black-ter is-size-6 has-text-weight-normal"><%= post.title %></a>
|
||||
<p class="is-size-7 is-uppercase">
|
||||
<%- list_categories(post.categories(), {
|
||||
show_count: false,
|
||||
class: 'has-link-grey ',
|
||||
depth:2,
|
||||
style: 'none',
|
||||
separator: ' / '}) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
|
@ -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 <div class="card widget">
|
||||
<div class="card-content">
|
||||
<h3 class="menu-label">{title}</h3>
|
||||
{posts.map(post => {
|
||||
const categories = [];
|
||||
post.categories.forEach((category, i) => {
|
||||
categories.push(<a class="has-link-grey" href={category.url}>{category.name}</a>);
|
||||
if (i < post.categories.length - 1) {
|
||||
categories.push('/');
|
||||
}
|
||||
});
|
||||
return <article class="media">
|
||||
{thumbnail ? <a href={post.url} class="media-left">
|
||||
<p class="image is-64x64">
|
||||
<img class="thumbnail" src={post.thumbnail} alt={post.title} />
|
||||
</p>
|
||||
</a> : null}
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<div><time class="has-text-grey is-size-7 is-uppercase" datetime={post.dateXml}>{post.date}</time></div>
|
||||
<a href={post.url} class="title has-link-black-ter is-size-6 has-text-weight-normal">{post.title}</a>
|
||||
<p class="is-size-7 is-uppercase">{categories}</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
};
|
||||
});
|
|
@ -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 });
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<div class="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">
|
||||
<%= __('widget.email.title') %>
|
||||
</h3>
|
||||
<div>
|
||||
<form action="https://feedburner.google.com/fb/a/mailverify" method="post" target="popupwindow"
|
||||
onsubmit="window.open('https://feedburner.google.com/fb/a/mailverify?uri=<%= get_config_from_obj(widget, 'feedburner_id') %>', 'popupwindow', 'scrollbars=yes,width=550,height=520');return true">
|
||||
<input type="hidden" value="<%= get_config_from_obj(widget, 'feedburner_id') %>" name="uri" />
|
||||
<input type="hidden" name="loc" value="en_US" />
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" name="email" type="email" />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
<p class="help"><%= get_config_from_obj(widget, 'description') %></p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<input class="button is-link" type="submit" value="<%= __('widget.email.button') %>" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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 <div class="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">{title}</h3>
|
||||
<div>
|
||||
<form action="https://feedburner.google.com/fb/a/mailverify" method="post" target="popupwindow"
|
||||
onsubmit={`window.open('https://feedburner.google.com/fb/a/mailverify?uri=${feedburnerId}','popupwindow','scrollbars=yes,width=550,height=520');return true`}>
|
||||
<input type="hidden" value={feedburnerId} name="uri" />
|
||||
<input type="hidden" name="loc" value="en_US" />
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" name="email" type="email" />
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
<p class="help">{description}</p>
|
||||
</div>
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<input class="button is-link" type="submit" value={buttonTitle} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
};
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = (ctx, locals) => {
|
||||
return locals;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<%- _partial('tags') %>
|
|
@ -1,8 +0,0 @@
|
|||
<div class="card widget">
|
||||
<div class="card-content">
|
||||
<h3 class="menu-label">
|
||||
<%= __('widget.tag_cloud') %>
|
||||
</h3>
|
||||
<%- tagcloud() %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = (ctx, locals) => {
|
||||
if (!ctx.site.tags.length) {
|
||||
return null;
|
||||
}
|
||||
return locals;
|
||||
}
|
|
@ -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 <div className="card widget">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">{title}</h3>
|
||||
<div class="field is-grouped is-grouped-multiline">
|
||||
{tags.map(tag => <div class="control">
|
||||
<a class="tags has-addons" href={tag.url}>
|
||||
<span class="tag">{tag.name}</span>
|
||||
{showCount ? <span class="tag is-grey">{tag.count}</span> : null}
|
||||
</a>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}))
|
||||
};
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
<% function buildToc(toc) {
|
||||
let result = '';
|
||||
if (toc.hasOwnProperty('id') && toc.hasOwnProperty('index') && toc.hasOwnProperty('text')) {
|
||||
result += `<li>
|
||||
<a class="is-flex" href="#${toc.id}">
|
||||
<span class="has-mr-6">${toc.index}</span>
|
||||
<span>${toc.text}</span>
|
||||
</a>`;
|
||||
}
|
||||
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 += '<ul class="menu-list">';
|
||||
for (let i of keys) {
|
||||
result += buildToc(toc[i]);
|
||||
}
|
||||
result += '</ul>';
|
||||
}
|
||||
if (toc.hasOwnProperty('id') && toc.hasOwnProperty('index') && toc.hasOwnProperty('text')) {
|
||||
result += '</li>';
|
||||
}
|
||||
return result;
|
||||
} %>
|
||||
<% let tocContent = buildToc(_toc(content)); if (tocContent !== undefined && tocContent !== ''){ %>
|
||||
<div class="card widget" id="toc">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">
|
||||
<%= _p('widget.catalogue', Infinity) %>
|
||||
</h3>
|
||||
<%- tocContent %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
|
@ -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 = <ul class="menu-list">
|
||||
{keys.map(i => this.renderToc(toc[i]))}
|
||||
</ul>;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(toc, 'id')
|
||||
&& Object.prototype.hasOwnProperty.call(toc, 'index')
|
||||
&& Object.prototype.hasOwnProperty.call(toc, 'text')) {
|
||||
result = <li>
|
||||
<a class="is-flex" href={'#' + toc.id}>
|
||||
<span class="has-mr-6">{toc.index}</span>
|
||||
<span>{toc.text}</span>
|
||||
</a>
|
||||
{result}
|
||||
</li>;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
render() {
|
||||
const toc = getToc(this.props.content);
|
||||
if (!Object.keys(toc).length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div class="card widget" id="toc">
|
||||
<div class="card-content">
|
||||
<div class="menu">
|
||||
<h3 class="menu-label">{this.props.title}</h3>
|
||||
{this.renderToc(toc)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
});
|
|
@ -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 });
|
||||
}
|
Loading…
Reference in New Issue