hexo-component-inferno 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -8
- package/asset/js/algolia.js +27 -10
- package/asset/js/insight.js +62 -44
- package/lib/core/migrate.js +23 -4
- package/lib/core/schema.js +31 -9
- package/lib/core/view.js +1 -1
- package/lib/hexo/filter/locals.js +5 -2
- package/lib/hexo/generator/category.js +8 -1
- package/lib/hexo/generator/insight.js +8 -15
- package/lib/hexo/generator/manifest.js +6 -0
- package/lib/hexo/helper/cdn.js +11 -3
- package/lib/hexo/tag/message.js +11 -1
- package/lib/hexo/tag/tabs.js +28 -5
- package/lib/schema/comment/artalk.json +37 -0
- package/lib/schema/comment/beaudar.json +62 -0
- package/lib/schema/comment/cusdis.json +34 -0
- package/lib/schema/comment/giscus.json +12 -1
- package/lib/schema/comment/gitment.json +1 -1
- package/lib/schema/comment/utterances.json +15 -1
- package/lib/schema/comment/valine.json +1 -1
- package/lib/schema/comment/waline.json +2 -2
- package/lib/schema/misc/open_graph.json +0 -1
- package/lib/schema/plugin/waline_pageview.json +14 -0
- package/lib/schema/share/addthis.json +1 -1
- package/lib/schema/widget/archives.json +28 -0
- package/lib/schema/widget/categories.json +32 -0
- package/lib/schema/widget/recent_posts.json +5 -0
- package/lib/schema/widget/subscribe_email.json +1 -1
- package/lib/schema/widget/tags.json +1 -1
- package/lib/util/appearance.js +40 -0
- package/lib/util/classname.js +1 -1
- package/lib/util/deprecate.js +28 -0
- package/lib/util/html.js +51 -0
- package/lib/util/i18n.js +24 -0
- package/lib/util/script.js +73 -5
- package/lib/view/comment/artalk.js +119 -0
- package/lib/view/comment/beaudar.js +134 -0
- package/lib/view/comment/changyan.js +2 -1
- package/lib/view/comment/cusdis.js +120 -0
- package/lib/view/comment/disqus.js +3 -1
- package/lib/view/comment/disqusjs.js +3 -2
- package/lib/view/comment/facebook.js +3 -1
- package/lib/view/comment/giscus.js +32 -21
- package/lib/view/comment/gitalk.js +4 -2
- package/lib/view/comment/gitment.js +7 -2
- package/lib/view/comment/isso.js +5 -1
- package/lib/view/comment/twikoo.js +5 -1
- package/lib/view/comment/utterances.js +26 -16
- package/lib/view/comment/valine.js +7 -1
- package/lib/view/comment/waline.js +31 -21
- package/lib/view/donate/afdian.js +1 -1
- package/lib/view/donate/alipay.js +1 -1
- package/lib/view/donate/buymeacoffee.js +1 -1
- package/lib/view/donate/patreon.js +1 -1
- package/lib/view/donate/wechat.js +1 -1
- package/lib/view/misc/meta.js +9 -3
- package/lib/view/misc/open_graph.js +12 -15
- package/lib/view/misc/structured_data.js +8 -9
- package/lib/view/misc/web_app.js +15 -6
- package/lib/view/plugin/baidu_analytics.js +3 -1
- package/lib/view/plugin/cookie_consent.js +3 -1
- package/lib/view/plugin/google_analytics.js +3 -1
- package/lib/view/plugin/hotjar.js +3 -1
- package/lib/view/plugin/matomo.js +3 -1
- package/lib/view/plugin/outdated_browser.js +1 -1
- package/lib/view/plugin/statcounter.js +3 -1
- package/lib/view/plugin/twitter_conversion_tracking.js +3 -1
- package/lib/view/plugin/waline_pageview.js +100 -0
- package/lib/view/search/algolia.js +3 -1
- package/lib/view/search/baidu.js +3 -1
- package/lib/view/search/google_cse.js +3 -1
- package/lib/view/search/insight.js +4 -2
- package/lib/view/share/addthis.js +4 -0
- package/lib/view/widget/archives.js +37 -9
- package/lib/view/widget/categories.js +41 -15
- package/lib/view/widget/links.js +1 -1
- package/lib/view/widget/subscribe_email.js +5 -0
- package/lib/view/widget/tags.js +18 -5
- package/package.json +4 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 PPOffice
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,19 +1,170 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hexo-component-inferno
|
|
2
2
|
|
|
3
|
-
[](https://github.com/ppoffice/hexo-component-inferno/actions/workflows/test.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/hexo-component-inferno)
|
|
5
5
|
[](https://coveralls.io/r/ppoffice/hexo-component-inferno?branch=master)
|
|
6
6
|
|
|
7
|
+
Ready-made [Inferno.js](https://infernojs.org/) layout components, Hexo extensions, and configuration tools
|
|
8
|
+
for building [Hexo](https://hexo.io/) themes in JSX. It is the component library behind
|
|
9
|
+
[Icarus](https://github.com/ppoffice/hexo-theme-icarus), and any theme rendered with
|
|
10
|
+
[hexo-renderer-inferno](https://github.com/hexojs/hexo-renderer-inferno) can use it.
|
|
11
|
+
|
|
12
|
+
- **Components**: comment systems, search, share buttons, donation buttons, widgets, analytics and other
|
|
13
|
+
plugins, and SEO metadata, each with a JSON schema for its configuration.
|
|
14
|
+
- **Hexo extensions**: generators, helpers, tag plugins, and a filter that merges the theme config into
|
|
15
|
+
the template locals.
|
|
16
|
+
- **Configuration tools**: a JSON schema loader that validates the theme config and generates its default
|
|
17
|
+
values, and a framework for migrating configs between theme versions.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node.js 14 or later
|
|
22
|
+
- Hexo 7.1.1 or later, or Hexo 8 (a peer dependency)
|
|
23
|
+
|
|
7
24
|
## Install
|
|
8
25
|
|
|
9
26
|
```sh
|
|
10
|
-
|
|
27
|
+
npm install hexo-component-inferno
|
|
11
28
|
```
|
|
12
29
|
|
|
13
|
-
|
|
30
|
+
The components render [Bulma](https://bulma.io/) markup and [Font Awesome](https://fontawesome.com/)
|
|
31
|
+
icons, so your theme needs to include their styles.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
The examples below follow how Icarus uses this library. See its
|
|
36
|
+
[`include/register.js`](https://github.com/ppoffice/hexo-theme-icarus/blob/master/include/register.js) and
|
|
37
|
+
[`layout/common/`](https://github.com/ppoffice/hexo-theme-icarus/tree/master/layout/common) for a complete
|
|
38
|
+
example.
|
|
39
|
+
|
|
40
|
+
### Register the Hexo extensions
|
|
41
|
+
|
|
42
|
+
Register the extensions you need from a script of your theme, for example `scripts/index.js`:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
require('hexo-component-inferno/lib/hexo/filter/locals')(hexo);
|
|
46
|
+
require('hexo-component-inferno/lib/hexo/generator/assets')(hexo);
|
|
47
|
+
require('hexo-component-inferno/lib/hexo/helper/cdn')(hexo);
|
|
48
|
+
require('hexo-component-inferno/lib/hexo/tag/message')(hexo);
|
|
49
|
+
// ...
|
|
50
|
+
require('hexo-component-inferno/lib/core/view').init(hexo);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Module | What it registers |
|
|
54
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `hexo/filter/locals` | Merges the theme config, `_config.post.yml` / `_config.page.yml`, and the front matter of the page into `config`, and puts all helpers in `helper`. Most components expect these locals. |
|
|
56
|
+
| `hexo/generator/assets` | Copies the browser scripts in [`asset/`](asset) (search, table of contents) to the site, unless the theme has a file at the same path. |
|
|
57
|
+
| `hexo/generator/insight` | `content.json`, the index of the Insight search. |
|
|
58
|
+
| `hexo/generator/manifest` | `manifest.json` for web apps. |
|
|
59
|
+
| `hexo/generator/categories` | The `/categories/` page. |
|
|
60
|
+
| `hexo/generator/category` | The page of each category. |
|
|
61
|
+
| `hexo/generator/tags` | The `/tags/` page. |
|
|
62
|
+
| `hexo/helper/cdn` | `cdn(package, version, file)`, `fontcdn(name, type)` and `iconcdn()`, which build URLs for the CDN providers set in `providers.cdn`, `providers.fontcdn` and `providers.iconcdn`. `providers.cdn_overrides` sets a different CDN for some packages. |
|
|
63
|
+
| `hexo/helper/page` | `is_categories()` and `is_tags()`. |
|
|
64
|
+
| `hexo/tag/message` | The `{% message %}` tag for Bulma message boxes. |
|
|
65
|
+
| `hexo/tag/tabs` | The `{% tabs %}` tag for Bulma tabs. |
|
|
66
|
+
|
|
67
|
+
### Render components
|
|
68
|
+
|
|
69
|
+
Every component lives in `lib/view/<category>/<name>.js`. Each one has a `Cacheable` version that takes
|
|
70
|
+
the template locals (`config`, `page`, `helper` and `site`) together with its own section of the theme
|
|
71
|
+
config, maps them to its props, and caches the rendered element:
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
const { Component } = require('inferno');
|
|
75
|
+
const view = require('hexo-component-inferno/lib/core/view');
|
|
76
|
+
|
|
77
|
+
module.exports = class extends Component {
|
|
78
|
+
render() {
|
|
79
|
+
const { config, page, helper } = this.props;
|
|
80
|
+
const { comment } = config;
|
|
81
|
+
// Resolves layout/comment/<type>.jsx in the theme first, then lib/view/comment/<type>.js here
|
|
82
|
+
const Comment = view.require('comment/' + comment.type);
|
|
83
|
+
return <Comment.Cacheable config={config} page={page} helper={helper} comment={comment} />;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Because `view.require()` looks in the theme's `layout/` directory first, a theme can replace any component
|
|
89
|
+
by adding a file with the same path.
|
|
90
|
+
|
|
91
|
+
### Validate the theme config
|
|
92
|
+
|
|
93
|
+
The JSON schema of each component is in `lib/schema/<category>/<name>.json`. Load your theme's schema
|
|
94
|
+
with `SchemaLoader`. A `$ref` is resolved from the directories you pass in first, then from the schemas of
|
|
95
|
+
this library:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
const { SchemaLoader } = require('hexo-component-inferno/lib/core/schema');
|
|
99
|
+
|
|
100
|
+
const loader = SchemaLoader.load(require('./schema/config.json'), [path.join(__dirname, 'schema')]);
|
|
101
|
+
const schema = loader.getSchema('/config.json');
|
|
102
|
+
|
|
103
|
+
const result = schema.validate(themeConfig); // true, or a list of errors
|
|
104
|
+
const defaults = schema.getDefaultValue().toYaml(); // a commented default config
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
For example, `{ "$ref": "/comment/giscus.json" }` in your schema refers to the Giscus schema of this
|
|
108
|
+
library. `lib/core/migrate` upgrades existing configs between theme versions with a chain of `Migration`
|
|
109
|
+
classes.
|
|
110
|
+
|
|
111
|
+
### Utilities
|
|
112
|
+
|
|
113
|
+
| Module | Description |
|
|
114
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `util/cache` | `cacheComponent()`, which builds the `Cacheable` components. |
|
|
116
|
+
| `util/classname` | Builds a class name string from a string, an array, or an object of class names to booleans. |
|
|
117
|
+
| `util/yaml` | YAML parse and stringify for theme configs, writing `null` as an empty value. |
|
|
118
|
+
| `util/script` | Builds inline scripts that load external scripts without blocking the page and work with PJAX. |
|
|
119
|
+
| `util/appearance` | Builds inline expressions that follow the light, dark, or auto appearance of the page (`<html data-appearance>`). |
|
|
120
|
+
| `util/i18n` | Translates a key with a fallback when the theme has no translation. |
|
|
121
|
+
| `util/deprecate` | Warns once when a deprecated component is rendered. |
|
|
122
|
+
|
|
123
|
+
## Components
|
|
124
|
+
|
|
125
|
+
| Category | Components |
|
|
126
|
+
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `comment` | Artalk, Beaudar, Changyan, Cusdis, Disqus, DisqusJS, Facebook, Giscus, Gitalk, Gitment, Isso, LiveRe, Twikoo, Utterances, Valine, Waline |
|
|
128
|
+
| `search` | Algolia, Baidu, Google CSE, Insight |
|
|
129
|
+
| `share` | AddThis, AddToAny, Baidu Share, Share.js, ShareThis |
|
|
130
|
+
| `donate` | Afdian, Alipay, Buy Me a Coffee, Patreon, PayPal, WeChat |
|
|
131
|
+
| `widget` | AdSense, archives, categories, follow.it, links, recent posts, email subscription, tags, table of contents |
|
|
132
|
+
| `plugin` | Baidu Analytics, Bing Webmaster, Busuanzi, CNZZ, cookie consent, gallery, Google Analytics, Hotjar, KaTeX, MathJax, Matomo, outdated browser warning, progress bar, StatCounter, Twitter conversion tracking, Umami, Waline page views |
|
|
133
|
+
| `misc` | Article licensing, meta tags, Open Graph, paginator, structured data, web app |
|
|
134
|
+
| `common` | Article media (a thumbnail with a title and date) |
|
|
135
|
+
|
|
136
|
+
The components below are deprecated because their services have shut down or are shutting down. They print
|
|
137
|
+
a warning when they are rendered, and will be removed in the next major version.
|
|
138
|
+
|
|
139
|
+
| Component | Reason | Alternatives |
|
|
140
|
+
| ------------------------ | ------------------------------------------------------------------------------ | -------------------------- |
|
|
141
|
+
| `share/addthis` | AddThis shut down on May 31, 2023. | AddToAny, ShareThis |
|
|
142
|
+
| `comment/gitment` | Gitment is no longer maintained, and its GitHub sign-in proxy no longer works. | Giscus, Utterances, Gitalk |
|
|
143
|
+
| `comment/valine` | LeanCloud, where Valine stores the comments, shuts down on January 12, 2027. | Waline, Twikoo |
|
|
144
|
+
| `widget/subscribe_email` | Google FeedBurner stopped email subscriptions in July 2021. | follow.it |
|
|
145
|
+
|
|
146
|
+
The [API documentation](https://ppoffice.github.io/hexo-component-inferno/) lists the props of every
|
|
147
|
+
component and the options of every module.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
npm install
|
|
153
|
+
npm run build # compile src/ to lib/
|
|
154
|
+
npm test # run the tests in lib/ (build first)
|
|
155
|
+
npm run test-cov # run the tests with coverage
|
|
156
|
+
npm run lint # ESLint
|
|
157
|
+
npm run format-check # Prettier; `npm run format` fixes it
|
|
158
|
+
npm run docs # generate the API documentation in doc/
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Tests sit next to the source files (`*.test.js` / `*.test.jsx`) and run against the compiled files in
|
|
162
|
+
`lib/`, so run `npm run build` after changing `src/`.
|
|
163
|
+
|
|
164
|
+
The tests use Hexo 8, which needs Node.js 20.19 or later. CI also tests Node.js 14 with Hexo 7.
|
|
14
165
|
|
|
15
|
-
|
|
166
|
+
Releases are published to npm by GitHub Actions when a GitHub release is published.
|
|
16
167
|
|
|
17
|
-
##
|
|
168
|
+
## License
|
|
18
169
|
|
|
19
|
-
|
|
170
|
+
[MIT](LICENSE)
|
package/asset/js/algolia.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
/* global instantsearch, algoliasearch */
|
|
2
2
|
// eslint-disable-next-line no-unused-vars
|
|
3
3
|
function loadAlgolia(config, translation) {
|
|
4
|
+
function escapeHTML(text) {
|
|
5
|
+
return String(text).replace(/[&<>"']/g, (char) => {
|
|
6
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char];
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Keep the <em> highlights of Algolia, and escape the rest, so that a record cannot add HTML. The
|
|
11
|
+
// excerpt is indexed from HTML, so its tags are removed.
|
|
12
|
+
function highlighted(html, stripTags) {
|
|
13
|
+
html = html
|
|
14
|
+
.replace(/<em>/gi, '[algolia-highlight]')
|
|
15
|
+
.replace(/<\/em>/gi, '[/algolia-highlight]');
|
|
16
|
+
if (stripTags) {
|
|
17
|
+
html = html.replace(/<[^>]+>/g, '');
|
|
18
|
+
}
|
|
19
|
+
return html
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
.replace(/\[algolia-highlight\]/g, '<em>')
|
|
23
|
+
.replace(/\[\/algolia-highlight\]/g, '</em>');
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
const search = instantsearch({
|
|
5
27
|
indexName: config.indexName,
|
|
6
28
|
searchClient: algoliasearch(config.applicationId, config.apiKey),
|
|
@@ -43,21 +65,16 @@ function loadAlgolia(config, translation) {
|
|
|
43
65
|
},
|
|
44
66
|
templates: {
|
|
45
67
|
empty: function (results) {
|
|
46
|
-
return translation.no_result + ': ' + results.query;
|
|
68
|
+
return escapeHTML(translation.no_result + ': ' + results.query);
|
|
47
69
|
},
|
|
48
70
|
item: function (hit) {
|
|
49
71
|
let title = instantsearch.highlight({ attribute: 'title', hit });
|
|
50
72
|
let excerpt = instantsearch.highlight({ attribute: config.excerpt, hit });
|
|
51
|
-
title = title ? title : translation.untitled;
|
|
52
|
-
excerpt = excerpt
|
|
53
|
-
|
|
54
|
-
.replace(new RegExp('</em>', 'ig'), '[/algolia-highlight]')
|
|
55
|
-
.replace(/(<([^>]+)>)/gi, '')
|
|
56
|
-
.replace(/(\[algolia-highlight\])/gi, '<em>')
|
|
57
|
-
.replace(/(\[\/algolia-highlight\])/gi, '</em>');
|
|
58
|
-
excerpt = excerpt ? excerpt : translation.empty_preview;
|
|
73
|
+
title = title ? highlighted(title, false) : escapeHTML(translation.untitled);
|
|
74
|
+
excerpt = highlighted(excerpt, true);
|
|
75
|
+
excerpt = excerpt ? excerpt : escapeHTML(translation.empty_preview);
|
|
59
76
|
return `<section class="searchbox-result-section">
|
|
60
|
-
<a class="searchbox-result-item" href="${hit.permalink}">
|
|
77
|
+
<a class="searchbox-result-item" href="${escapeHTML(hit.permalink)}">
|
|
61
78
|
<span class="searchbox-result-content">
|
|
62
79
|
<span class="searchbox-result-title">${title}</span>
|
|
63
80
|
<span class="searchbox-result-preview">${excerpt}</span>
|
package/asset/js/insight.js
CHANGED
|
@@ -27,18 +27,33 @@ function loadInsight(config, translation) {
|
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// The search content is plain text, so escape it before showing it as HTML
|
|
31
|
+
function escapeHTML(text) {
|
|
32
|
+
return text.replace(/[&<>"']/g, (char) => {
|
|
33
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function escapeRegExp(text) {
|
|
38
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
function findAndHighlight(text, matches, maxlen) {
|
|
31
|
-
if (!
|
|
32
|
-
return
|
|
42
|
+
if (!text) {
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(matches) || !matches.length) {
|
|
46
|
+
return escapeHTML(maxlen ? text.slice(0, maxlen) : text);
|
|
33
47
|
}
|
|
34
|
-
const testText = text.toLowerCase();
|
|
35
48
|
const indices = matches
|
|
36
49
|
.map((match) => {
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
// Match the original text case-insensitively, as the lowercase of some characters, such as
|
|
51
|
+
// "İ", is longer, and its indices would not fit the original text
|
|
52
|
+
const found = match ? new RegExp(escapeRegExp(match), 'i').exec(text) : null;
|
|
53
|
+
if (!found) {
|
|
39
54
|
return null;
|
|
40
55
|
}
|
|
41
|
-
return [index, index +
|
|
56
|
+
return [found.index, found.index + found[0].length];
|
|
42
57
|
})
|
|
43
58
|
.filter((match) => {
|
|
44
59
|
return match !== null;
|
|
@@ -48,7 +63,7 @@ function loadInsight(config, translation) {
|
|
|
48
63
|
});
|
|
49
64
|
|
|
50
65
|
if (!indices.length) {
|
|
51
|
-
return text;
|
|
66
|
+
return escapeHTML(maxlen ? text.slice(0, maxlen) : text);
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
let result = '';
|
|
@@ -61,17 +76,21 @@ function loadInsight(config, translation) {
|
|
|
61
76
|
|
|
62
77
|
for (let i = 0; i < ranges.length; i++) {
|
|
63
78
|
const range = ranges[i];
|
|
64
|
-
result +=
|
|
79
|
+
result += escapeHTML(
|
|
80
|
+
text.slice(last, maxlen ? Math.min(range[0], sumRange[0] + maxlen) : range[0]),
|
|
81
|
+
);
|
|
65
82
|
if (maxlen && range[0] >= sumRange[0] + maxlen) {
|
|
66
83
|
break;
|
|
67
84
|
}
|
|
68
|
-
result += '<em>' + text.slice(range[0], range[1]) + '</em>';
|
|
85
|
+
result += '<em>' + escapeHTML(text.slice(range[0], range[1])) + '</em>';
|
|
69
86
|
last = range[1];
|
|
70
87
|
if (i === ranges.length - 1) {
|
|
71
88
|
if (maxlen) {
|
|
72
|
-
result +=
|
|
89
|
+
result += escapeHTML(
|
|
90
|
+
text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1)),
|
|
91
|
+
);
|
|
73
92
|
} else {
|
|
74
|
-
result += text.slice(range[1]);
|
|
93
|
+
result += escapeHTML(text.slice(range[1]));
|
|
75
94
|
}
|
|
76
95
|
}
|
|
77
96
|
}
|
|
@@ -80,12 +99,12 @@ function loadInsight(config, translation) {
|
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
function searchItem(icon, title, slug, preview, url) {
|
|
83
|
-
title = title != null && title !== '' ? title : translation.untitled;
|
|
102
|
+
title = title != null && title !== '' ? title : escapeHTML(translation.untitled);
|
|
84
103
|
const subtitle = slug
|
|
85
104
|
? '<span class="searchbox-result-title-secondary">(' + slug + ')</span>'
|
|
86
105
|
: '';
|
|
87
106
|
|
|
88
|
-
return `<a class="searchbox-result-item" href="${url}">
|
|
107
|
+
return `<a class="searchbox-result-item" href="${escapeHTML(url)}">
|
|
89
108
|
<span class="searchbox-result-icon">
|
|
90
109
|
<i class="fa fa-${icon}"></i>
|
|
91
110
|
</span>
|
|
@@ -188,17 +207,24 @@ function loadInsight(config, translation) {
|
|
|
188
207
|
function weight(keywords, obj, fields, weights) {
|
|
189
208
|
let value = 0;
|
|
190
209
|
parseKeywords(keywords).forEach((keyword) => {
|
|
191
|
-
const pattern = new RegExp(keyword, 'img'); // Global, Multi-line, Case-insensitive
|
|
192
210
|
fields.forEach((field, index) => {
|
|
193
211
|
if (Object.prototype.hasOwnProperty.call(obj, field)) {
|
|
194
|
-
|
|
195
|
-
value += matches ? matches.length * weights[index] : 0;
|
|
212
|
+
value += countMatches(obj[field].toLowerCase(), keyword) * weights[index];
|
|
196
213
|
}
|
|
197
214
|
});
|
|
198
215
|
});
|
|
199
216
|
return value;
|
|
200
217
|
}
|
|
201
218
|
|
|
219
|
+
// Count the occurrences of a lowercase keyword, which is plain text, not a regular expression
|
|
220
|
+
function countMatches(text, keyword) {
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (let i = text.indexOf(keyword); i !== -1; i = text.indexOf(keyword, i + keyword.length)) {
|
|
223
|
+
count++;
|
|
224
|
+
}
|
|
225
|
+
return count;
|
|
226
|
+
}
|
|
227
|
+
|
|
202
228
|
function weightFactory(keywords) {
|
|
203
229
|
return {
|
|
204
230
|
post: function (obj) {
|
|
@@ -216,38 +242,30 @@ function loadInsight(config, translation) {
|
|
|
216
242
|
};
|
|
217
243
|
}
|
|
218
244
|
|
|
245
|
+
// Weigh each matching item once, instead of in every comparison of the sort
|
|
246
|
+
function topItems(items, filter, weigh) {
|
|
247
|
+
return items
|
|
248
|
+
.filter(filter)
|
|
249
|
+
.map((item) => {
|
|
250
|
+
return { item, weight: weigh(item) };
|
|
251
|
+
})
|
|
252
|
+
.sort((a, b) => {
|
|
253
|
+
return b.weight - a.weight;
|
|
254
|
+
})
|
|
255
|
+
.slice(0, 5)
|
|
256
|
+
.map((result) => {
|
|
257
|
+
return result.item;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
function search(json, keywords) {
|
|
220
262
|
const weights = weightFactory(keywords);
|
|
221
263
|
const filters = filterFactory(keywords);
|
|
222
|
-
const posts = json.posts;
|
|
223
|
-
const pages = json.pages;
|
|
224
|
-
const tags = json.tags;
|
|
225
|
-
const categories = json.categories;
|
|
226
264
|
return {
|
|
227
|
-
posts: posts
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
})
|
|
232
|
-
.slice(0, 5),
|
|
233
|
-
pages: pages
|
|
234
|
-
.filter(filters.page)
|
|
235
|
-
.sort((a, b) => {
|
|
236
|
-
return weights.page(b) - weights.page(a);
|
|
237
|
-
})
|
|
238
|
-
.slice(0, 5),
|
|
239
|
-
categories: categories
|
|
240
|
-
.filter(filters.category)
|
|
241
|
-
.sort((a, b) => {
|
|
242
|
-
return weights.category(b) - weights.category(a);
|
|
243
|
-
})
|
|
244
|
-
.slice(0, 5),
|
|
245
|
-
tags: tags
|
|
246
|
-
.filter(filters.tag)
|
|
247
|
-
.sort((a, b) => {
|
|
248
|
-
return weights.tag(b) - weights.tag(a);
|
|
249
|
-
})
|
|
250
|
-
.slice(0, 5),
|
|
265
|
+
posts: topItems(json.posts, filters.post, weights.post),
|
|
266
|
+
pages: topItems(json.pages, filters.page, weights.page),
|
|
267
|
+
categories: topItems(json.categories, filters.category, weights.category),
|
|
268
|
+
tags: topItems(json.tags, filters.tag, weights.tag),
|
|
251
269
|
};
|
|
252
270
|
}
|
|
253
271
|
|
package/lib/core/migrate.js
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const semver = require('semver');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Convert the version of a configuration to a semantic version. A version written in a YAML file can
|
|
9
|
+
* be a number, such as 4.1, or have fewer than three parts.
|
|
10
|
+
*
|
|
11
|
+
* @private
|
|
12
|
+
* @param {string|number} version The version of a configuration.
|
|
13
|
+
* @returns {string} The semantic version.
|
|
14
|
+
* @throws {Error} The version is not a version number.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeVersion(version) {
|
|
17
|
+
const valid = semver.valid(String(version)) || semver.valid(semver.coerce(String(version)));
|
|
18
|
+
if (!valid) {
|
|
19
|
+
throw new Error(`${version} is not a valid configuration version`);
|
|
20
|
+
}
|
|
21
|
+
return valid;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
/**
|
|
8
25
|
* Configuration migration base class.
|
|
9
26
|
* <p>
|
|
@@ -72,7 +89,7 @@ class Migrator {
|
|
|
72
89
|
throw new Error(`Migration ${migration.constructor.name} is not a Migration class.`);
|
|
73
90
|
}
|
|
74
91
|
if (!semver.valid(migration.version)) {
|
|
75
|
-
throw new Error(`${migration.version} is not a valid version in ${migration.constructor.name}
|
|
92
|
+
throw new Error(`${migration.version} is not a valid version in ${migration.constructor.name}`);
|
|
76
93
|
}
|
|
77
94
|
this.versions.push(migration.version);
|
|
78
95
|
this.migrations[migration.version] = migration;
|
|
@@ -100,7 +117,7 @@ class Migrator {
|
|
|
100
117
|
* @returns {boolean} true if the given version is the latest version.
|
|
101
118
|
*/
|
|
102
119
|
isOudated(version) {
|
|
103
|
-
return this.getLatestVersion() && semver.lt(version, this.getLatestVersion());
|
|
120
|
+
return this.getLatestVersion() && semver.lt(normalizeVersion(version), this.getLatestVersion());
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
/**
|
|
@@ -114,15 +131,17 @@ class Migrator {
|
|
|
114
131
|
* @returns {Object} The upgrade configuration object.
|
|
115
132
|
*/
|
|
116
133
|
migrate(config, toVersion = null, callback = null) {
|
|
117
|
-
const fVer = config.version;
|
|
134
|
+
const fVer = normalizeVersion(config.version);
|
|
118
135
|
const tVer = toVersion || this.getLatestVersion();
|
|
119
136
|
// find all migrations whose version is larger than fromVer, smaller or equal to toVer
|
|
120
137
|
// and run migrations on the config one by one.
|
|
121
138
|
return this.versions.filter(ver => semver.gt(ver, fVer) && !semver.gt(ver, tVer)).sort(semver.compare).reduce((cfg, ver) => {
|
|
122
139
|
const migration = this.migrations[ver.toString()];
|
|
140
|
+
// Upgrades can change the version of the given object, so get it before
|
|
141
|
+
const fromVersion = cfg.version;
|
|
123
142
|
const result = migration.migrate(cfg);
|
|
124
143
|
if (typeof callback === 'function') {
|
|
125
|
-
callback(
|
|
144
|
+
callback(fromVersion, migration.version);
|
|
126
145
|
}
|
|
127
146
|
return result;
|
|
128
147
|
}, config);
|
package/lib/core/schema.js
CHANGED
|
@@ -11,6 +11,15 @@ const yaml = require('../util/yaml');
|
|
|
11
11
|
*/
|
|
12
12
|
const MAGIC = 'c823d4d4';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Deep copy a value of a JSON schema definition, so that changing a default value does not change the
|
|
16
|
+
* definition, which other schemas may refer to.
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
function copy(value) {
|
|
20
|
+
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Default value for the primitive types.
|
|
16
25
|
* @access private
|
|
@@ -210,8 +219,23 @@ class DefaultValue {
|
|
|
210
219
|
* @returns {string} The formatted YAML string.
|
|
211
220
|
*/
|
|
212
221
|
toYaml() {
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
// Comments are written as properties or array elements, whose values the YAML writer may quote.
|
|
223
|
+
// Read them back with the YAML parser, rather than removing the quotes by hand.
|
|
224
|
+
const property = new RegExp(`^(\\s*)(['"]?)${MAGIC}.*?\\2: (.*)$`);
|
|
225
|
+
const element = new RegExp(`^(\\s*)- (.*${MAGIC}.*)$`);
|
|
226
|
+
const comment = (indent, text) => `${indent}# ${text === null || text === undefined ? '' : text}`;
|
|
227
|
+
return yaml.stringify(this.toCommented()).split('\n').map(line => {
|
|
228
|
+
let match = property.exec(line);
|
|
229
|
+
if (match) {
|
|
230
|
+
return comment(match[1], yaml.parse(match[3]));
|
|
231
|
+
}
|
|
232
|
+
match = element.exec(line);
|
|
233
|
+
if (match) {
|
|
234
|
+
const text = String(yaml.parse(match[2]));
|
|
235
|
+
return comment(match[1], text.slice(text.indexOf(': ') + 2));
|
|
236
|
+
}
|
|
237
|
+
return line;
|
|
238
|
+
}).join('\n');
|
|
215
239
|
}
|
|
216
240
|
}
|
|
217
241
|
|
|
@@ -388,13 +412,13 @@ class Schema {
|
|
|
388
412
|
def = this.def;
|
|
389
413
|
}
|
|
390
414
|
if (hasOwnProperty(def, 'const')) {
|
|
391
|
-
return new DefaultValue(def.const, def.description);
|
|
415
|
+
return new DefaultValue(copy(def.const), def.description);
|
|
392
416
|
}
|
|
393
417
|
if (hasOwnProperty(def, 'default')) {
|
|
394
|
-
return new DefaultValue(def.default, def.description);
|
|
418
|
+
return new DefaultValue(copy(def.default), def.description);
|
|
395
419
|
}
|
|
396
420
|
if (hasOwnProperty(def, 'examples') && typeOf(def.examples) === 'array' && def.examples.length) {
|
|
397
|
-
return new DefaultValue(def.examples[0], def.description);
|
|
421
|
+
return new DefaultValue(copy(def.examples[0]), def.description);
|
|
398
422
|
}
|
|
399
423
|
if (hasOwnProperty(def, 'type') && def.type) {
|
|
400
424
|
return this.getTypedDefaultValue(def);
|
|
@@ -474,10 +498,8 @@ class SchemaLoader {
|
|
|
474
498
|
* @throws {Error} Referred schema not found in any of the resolve directories.
|
|
475
499
|
*/
|
|
476
500
|
SchemaLoader.load = (rootSchemaDef, resolveDirs = []) => {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
resolveDirs.push(path.join(__dirname, '../schema/'));
|
|
501
|
+
// A new array, so that the directories of the caller are not changed
|
|
502
|
+
resolveDirs = [].concat(resolveDirs, path.join(__dirname, '../schema/'));
|
|
481
503
|
const loader = new SchemaLoader();
|
|
482
504
|
loader.addSchema(rootSchemaDef);
|
|
483
505
|
function handler($ref) {
|
package/lib/core/view.js
CHANGED
|
@@ -57,7 +57,7 @@ _require.resolve = _resolve;
|
|
|
57
57
|
* Must be used before {@link module:core/view.require} or {@link module:core/view.resolve}.
|
|
58
58
|
*
|
|
59
59
|
* @static
|
|
60
|
-
* @param {
|
|
60
|
+
* @param {Hexo} hexo The Hexo instance.
|
|
61
61
|
*/
|
|
62
62
|
function init(hexo) {
|
|
63
63
|
themeDir = hexo.theme_dir;
|
|
@@ -35,9 +35,12 @@ const yaml = require('js-yaml');
|
|
|
35
35
|
* @param {Hexo} hexo The Hexo instance.
|
|
36
36
|
*/
|
|
37
37
|
module.exports = hexo => {
|
|
38
|
+
// Properties that Hexo sets on pages, rather than the front matter, such as the previous and next
|
|
39
|
+
// posts that the post generator adds
|
|
40
|
+
const GENERATED_KEYS = ['prev', 'next', 'canonical_path'];
|
|
38
41
|
const RESERVED_KEYS = {
|
|
39
|
-
post: Object.keys(hexo.model('Post').schema.paths),
|
|
40
|
-
page: Object.keys(hexo.model('Page').schema.paths)
|
|
42
|
+
post: Object.keys(hexo.model('Post').schema.paths).concat(GENERATED_KEYS),
|
|
43
|
+
page: Object.keys(hexo.model('Page').schema.paths).concat(GENERATED_KEYS)
|
|
41
44
|
};
|
|
42
45
|
function loadLayoutConfig(layout) {
|
|
43
46
|
let config = {};
|
|
@@ -16,7 +16,14 @@ module.exports = function (hexo) {
|
|
|
16
16
|
hexo.extend.generator.register('category', function (locals) {
|
|
17
17
|
const config = this.config;
|
|
18
18
|
const paginationDir = config.pagination_dir || 'page';
|
|
19
|
-
|
|
19
|
+
// Like hexo-generator-category, use per_page of the site config without a category_generator one
|
|
20
|
+
const generatorConfig = config.category_generator || {};
|
|
21
|
+
let perPage = 10;
|
|
22
|
+
if (typeof generatorConfig.per_page === 'number') {
|
|
23
|
+
perPage = generatorConfig.per_page;
|
|
24
|
+
} else if (typeof config.per_page === 'number') {
|
|
25
|
+
perPage = config.per_page;
|
|
26
|
+
}
|
|
20
27
|
const orderBy = (config.category_generator ? config.category_generator.order_by : null) || '-date';
|
|
21
28
|
function findParent(category) {
|
|
22
29
|
let parents = [];
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Insight search content.json generator.
|
|
3
3
|
* @module hexo/generator/insight
|
|
4
4
|
*/
|
|
5
|
-
const
|
|
5
|
+
const {
|
|
6
|
+
toPlainText
|
|
7
|
+
} = require('../../util/html');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Insight search content.json generator.
|
|
@@ -12,27 +14,18 @@ const util = require('hexo-util');
|
|
|
12
14
|
module.exports = function (hexo) {
|
|
13
15
|
hexo.extend.generator.register('insight', function (locals) {
|
|
14
16
|
const url_for = hexo.extend.helper.get('url_for').bind(this);
|
|
15
|
-
|
|
16
|
-
return util.escapeHTML(str).trim();
|
|
17
|
-
}
|
|
18
|
-
function minify(str) {
|
|
19
|
-
return util.stripHTML(str).trim().replace(/\n/g, ' ').replace(/\s+/g, ' ').replace(/&#x([\da-fA-F]+);/g, (match, hex) => {
|
|
20
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
21
|
-
}).replace(/&#([\d]+);/g, (match, dec) => {
|
|
22
|
-
return String.fromCharCode(dec);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
17
|
+
// content.json holds plain text, which the search script escapes when it shows it
|
|
25
18
|
function mapPost(post) {
|
|
26
19
|
return {
|
|
27
|
-
title:
|
|
28
|
-
text:
|
|
20
|
+
title: (post.title || '').trim(),
|
|
21
|
+
text: toPlainText(post.content),
|
|
29
22
|
link: url_for(post.path)
|
|
30
23
|
};
|
|
31
24
|
}
|
|
32
25
|
function mapTag(tag) {
|
|
33
26
|
return {
|
|
34
|
-
name:
|
|
35
|
-
slug:
|
|
27
|
+
name: (tag.name || '').trim(),
|
|
28
|
+
slug: (tag.slug || '').trim(),
|
|
36
29
|
link: url_for(tag.path)
|
|
37
30
|
};
|
|
38
31
|
}
|