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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -8
  3. package/asset/js/algolia.js +27 -10
  4. package/asset/js/insight.js +62 -44
  5. package/lib/core/migrate.js +23 -4
  6. package/lib/core/schema.js +31 -9
  7. package/lib/core/view.js +1 -1
  8. package/lib/hexo/filter/locals.js +5 -2
  9. package/lib/hexo/generator/category.js +8 -1
  10. package/lib/hexo/generator/insight.js +8 -15
  11. package/lib/hexo/generator/manifest.js +6 -0
  12. package/lib/hexo/helper/cdn.js +11 -3
  13. package/lib/hexo/tag/message.js +11 -1
  14. package/lib/hexo/tag/tabs.js +28 -5
  15. package/lib/schema/comment/artalk.json +37 -0
  16. package/lib/schema/comment/beaudar.json +62 -0
  17. package/lib/schema/comment/cusdis.json +34 -0
  18. package/lib/schema/comment/giscus.json +12 -1
  19. package/lib/schema/comment/gitment.json +1 -1
  20. package/lib/schema/comment/utterances.json +15 -1
  21. package/lib/schema/comment/valine.json +1 -1
  22. package/lib/schema/comment/waline.json +2 -2
  23. package/lib/schema/misc/open_graph.json +0 -1
  24. package/lib/schema/plugin/waline_pageview.json +14 -0
  25. package/lib/schema/share/addthis.json +1 -1
  26. package/lib/schema/widget/archives.json +28 -0
  27. package/lib/schema/widget/categories.json +32 -0
  28. package/lib/schema/widget/recent_posts.json +5 -0
  29. package/lib/schema/widget/subscribe_email.json +1 -1
  30. package/lib/schema/widget/tags.json +1 -1
  31. package/lib/util/appearance.js +40 -0
  32. package/lib/util/classname.js +1 -1
  33. package/lib/util/deprecate.js +28 -0
  34. package/lib/util/html.js +51 -0
  35. package/lib/util/i18n.js +24 -0
  36. package/lib/util/script.js +73 -5
  37. package/lib/view/comment/artalk.js +119 -0
  38. package/lib/view/comment/beaudar.js +134 -0
  39. package/lib/view/comment/changyan.js +2 -1
  40. package/lib/view/comment/cusdis.js +120 -0
  41. package/lib/view/comment/disqus.js +3 -1
  42. package/lib/view/comment/disqusjs.js +3 -2
  43. package/lib/view/comment/facebook.js +3 -1
  44. package/lib/view/comment/giscus.js +32 -21
  45. package/lib/view/comment/gitalk.js +4 -2
  46. package/lib/view/comment/gitment.js +7 -2
  47. package/lib/view/comment/isso.js +5 -1
  48. package/lib/view/comment/twikoo.js +5 -1
  49. package/lib/view/comment/utterances.js +26 -16
  50. package/lib/view/comment/valine.js +7 -1
  51. package/lib/view/comment/waline.js +31 -21
  52. package/lib/view/donate/afdian.js +1 -1
  53. package/lib/view/donate/alipay.js +1 -1
  54. package/lib/view/donate/buymeacoffee.js +1 -1
  55. package/lib/view/donate/patreon.js +1 -1
  56. package/lib/view/donate/wechat.js +1 -1
  57. package/lib/view/misc/meta.js +9 -3
  58. package/lib/view/misc/open_graph.js +12 -15
  59. package/lib/view/misc/structured_data.js +8 -9
  60. package/lib/view/misc/web_app.js +15 -6
  61. package/lib/view/plugin/baidu_analytics.js +3 -1
  62. package/lib/view/plugin/cookie_consent.js +3 -1
  63. package/lib/view/plugin/google_analytics.js +3 -1
  64. package/lib/view/plugin/hotjar.js +3 -1
  65. package/lib/view/plugin/matomo.js +3 -1
  66. package/lib/view/plugin/outdated_browser.js +1 -1
  67. package/lib/view/plugin/statcounter.js +3 -1
  68. package/lib/view/plugin/twitter_conversion_tracking.js +3 -1
  69. package/lib/view/plugin/waline_pageview.js +100 -0
  70. package/lib/view/search/algolia.js +3 -1
  71. package/lib/view/search/baidu.js +3 -1
  72. package/lib/view/search/google_cse.js +3 -1
  73. package/lib/view/search/insight.js +4 -2
  74. package/lib/view/share/addthis.js +4 -0
  75. package/lib/view/widget/archives.js +37 -9
  76. package/lib/view/widget/categories.js +41 -15
  77. package/lib/view/widget/links.js +1 -1
  78. package/lib/view/widget/subscribe_email.js +5 -0
  79. package/lib/view/widget/tags.js +18 -5
  80. 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
- # A collection of [Inferno.js](https://infernojs.org/) layout components and utility scripts for Hexo
1
+ # hexo-component-inferno
2
2
 
3
- [![Build Status](https://github.com/ppoffice/hexo-component-inferno/workflows/Test/badge.svg?branch=master)](https://github.com/ppoffice/hexo-component-inferno/actions)
4
- [![NPM version](https://badge.fury.io/js/hexo-component-inferno.svg)](https://www.npmjs.com/package/hexo-component-inferno)
3
+ [![Test](https://github.com/ppoffice/hexo-component-inferno/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/ppoffice/hexo-component-inferno/actions/workflows/test.yml)
4
+ [![npm version](https://img.shields.io/npm/v/hexo-component-inferno.svg)](https://www.npmjs.com/package/hexo-component-inferno)
5
5
  [![Coverage Status](https://img.shields.io/coveralls/ppoffice/hexo-component-inferno.svg)](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
- $ npm install hexo-component-inferno --save
27
+ npm install hexo-component-inferno
11
28
  ```
12
29
 
13
- ## API Documentation
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
- Please refer to [this page](https://ppoffice.github.io/hexo-component-inferno/).
166
+ Releases are published to npm by GitHub Actions when a GitHub release is published.
16
167
 
17
- ## Examples
168
+ ## License
18
169
 
19
- Please refer to tests in the `src` directory or take a look at [hexo-theme-icarus](https://github.com/ppoffice/hexo-theme-icarus).
170
+ [MIT](LICENSE)
@@ -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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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, '&lt;')
21
+ .replace(/>/g, '&gt;')
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
- .replace(new RegExp('<em>', 'ig'), '[algolia-highlight]')
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>
@@ -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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char];
34
+ });
35
+ }
36
+
37
+ function escapeRegExp(text) {
38
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ }
40
+
30
41
  function findAndHighlight(text, matches, maxlen) {
31
- if (!Array.isArray(matches) || !matches.length || !text) {
32
- return maxlen ? text.slice(0, maxlen) : text;
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
- const index = testText.indexOf(match.toLowerCase());
38
- if (!match || index === -1) {
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 + match.length];
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 += text.slice(last, Math.min(range[0], sumRange[0] + maxlen));
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 += text.slice(range[1], Math.min(text.length, sumRange[0] + maxlen + 1));
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
- const matches = obj[field].match(pattern);
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
- .filter(filters.post)
229
- .sort((a, b) => {
230
- return weights.post(b) - weights.post(a);
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
 
@@ -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(cfg.version, migration.version);
144
+ callback(fromVersion, migration.version);
126
145
  }
127
146
  return result;
128
147
  }, config);
@@ -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
- const regex = new RegExp("^(\\s*)(?:-\\s*\\')?" + MAGIC + ".*?:\\s*\\'?(.*?)\\'*$", 'mg');
214
- return yaml.stringify(this.toCommented()).replace(regex, '$1# $2'); // restore comments
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
- if (typeOf(resolveDirs) !== 'array') {
478
- resolveDirs = [resolveDirs];
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 {string} hexo Hexo instance.
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
- const perPage = (config.category_generator ? config.category_generator.per_page : null) || 0;
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 util = require('hexo-util');
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
- function escape(str) {
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: escape(post.title),
28
- text: minify(post.content),
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: escape(tag.name),
35
- slug: minify(tag.slug),
27
+ name: (tag.name || '').trim(),
28
+ slug: (tag.slug || '').trim(),
36
29
  link: url_for(tag.path)
37
30
  };
38
31
  }