├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.js
├── lib
└── generator.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Yusen
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hexo-generator-restful
2 |
3 | Generate restful json data for Hexo plugins.
4 |
5 | 生成 restful 风格的 json 数据,可以当作 api 接口,开始构建一个 SPA 应用吧。
6 |
7 | ## Install
8 |
9 | ```bash
10 | npm install hexo-generator-restful --save
11 | ```
12 |
13 | ## Config
14 |
15 | 以下为默认配置,属性值为 `false` 表示不生成。
16 |
17 | ```yml
18 | restful:
19 | # site 可配置为数组选择性生成某些属性
20 | # site: ['title', 'subtitle', 'description', 'author', 'since', email', 'favicon', 'avatar']
21 | site: true # hexo.config mix theme.config
22 | posts_size: 10 # 文章列表分页,0 表示不分页
23 | posts_props: # 文章列表项的需要生成的属性
24 | title: true
25 | slug: true
26 | date: true
27 | updated: true
28 | comments: true
29 | path: true
30 | excerpt: false
31 | cover: true # 封面图,取文章第一张图片
32 | content: false
33 | keywords: false
34 | categories: true
35 | tags: true
36 | categories: true # 分类数据
37 | use_category_slug: false # Use slug for filename of category data
38 | tags: true # 标签数据
39 | use_tag_slug: false # Use slug for filename of tag data
40 | post: true # 文章数据
41 | pages: false # 额外的 Hexo 页面数据, 如 About
42 | ```
43 |
44 | ## Document
45 |
46 | ### Get Hexo Config
47 |
48 | 获取所有 Hexo 配置(站点配置和主题配置)。
49 |
50 | ###### Request
51 |
52 | ```
53 | GET /api/site.json
54 | ```
55 |
56 | ###### Response
57 |
58 | [/api/site.json](http://www.imys.net/api/site.json)
59 |
60 | ### Get Posts
61 |
62 | 如果配置 `posts_size: 0` 则不分页,以下请求会获取全部文章。
63 |
64 | ###### Request
65 |
66 | ```
67 | GET /api/posts.json
68 | ```
69 |
70 | ###### Response
71 |
72 | 示例为分页配置下的数据,会包含分页属性 `total`、`pageSize`、`pageCount`,不分页的数据不包含这三项。
73 |
74 | [/api/posts.json](http://www.imys.net/api/posts.json)
75 |
76 | ### Get Posts By Page
77 |
78 | 获取分页数据
79 |
80 | ###### Request
81 |
82 | ```
83 | GET /api/posts/:PageNum.json
84 | ```
85 |
86 | ###### Response
87 |
88 | [/api/posts/1.json](http://www.imys.net/api/posts/1.json)
89 |
90 | ### Get All Tags
91 |
92 | 获取所有文章标签,如果文章无标签则不生成。
93 |
94 | ###### Request
95 |
96 | ```
97 | GET /api/tags.json
98 | ```
99 |
100 | ###### Response
101 |
102 | [/api/tags.json](http://www.imys.net/api/tags.json)
103 |
104 | ### Get Posts By Tag
105 |
106 | 获取某一标签下的所有文章
107 |
108 | ###### Request
109 |
110 | ```
111 | GET /api/tags/:TagName.json
112 | ```
113 |
114 | ###### Response
115 |
116 | [/api/tags/Hexo.json](http://www.imys.net/api/tags/Hexo.json)
117 |
118 | ### Get All Categories
119 |
120 | 获取所有文章分类,如果文章无分类则不生成。
121 |
122 | ###### Request
123 |
124 | ```
125 | GET /api/categories.json
126 | ```
127 |
128 | ###### Response
129 |
130 | 数据格式同 Get All Tags
131 |
132 | ### Get Posts By Categorie
133 |
134 | 获取某一分类下的所有文章
135 |
136 | ###### Request
137 |
138 | ```
139 | GET /api/categories/:CategorieName.json
140 | ```
141 |
142 | ###### Response
143 |
144 | 数据格式同 Get Posts By Tag
145 |
146 | ### Get Post By Slug
147 |
148 | 根据文章别名获取文章详细信息
149 |
150 | ###### Request
151 |
152 | ```
153 | GET /api/articles/:Slug.json
154 | ```
155 |
156 | ###### Response
157 |
158 | [/api/articles/javascript-advanced-functions.json](http://www.imys.net/api/articles/javascript-advanced-functions.json)
159 |
160 | ### Get Implecit Pages
161 |
162 | 获取来自主题的 Hexo 隐式页面内容,如 About 等。因隐式页面(除 About 等导航栏入口页外)一般在 Hexo 不提供直接访问入口,调用此 API 的开发者需要了解其完整路径,此接口默认关闭。
163 |
164 | 例如:
165 |
166 | ###### Request
167 |
168 | ```
169 | GET /api/pages/about.json
170 | ```
171 |
172 | ###### Response
173 |
174 | 格式类似于于 Get Post By Slug。
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var generator = require('./lib/generator');
4 |
5 | hexo.extend.generator.register('restful', function(site) {
6 | return generator(Object.assign({}, hexo.config, hexo.theme.config), site);
7 | });
8 |
--------------------------------------------------------------------------------
/lib/generator.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var pagination = require('hexo-pagination');
4 | var _pick = require('lodash.pick');
5 |
6 | function filterHTMLTags(str) {
7 | return str ? str
8 | .replace(/\<(?!img|br).*?\>/g, "")
9 | .replace(/\r?\n|\r/g, '')
10 | .replace(/
/g, ' [Figure] ') : null
11 | }
12 | function fetchCovers(str) {
13 | var temp,
14 | imgURLs = [],
15 | rex = /
]+src="?([^"\s]+)"(.*)>/g;
16 | while ( temp = rex.exec( str ) ) {
17 | imgURLs.push( temp[1] );
18 | }
19 | return imgURLs.length > 0 ? imgURLs : null;
20 | }
21 | function fetchCover(str) {
22 | var covers = fetchCovers(str)
23 | return covers ? covers[0] : null;
24 | }
25 |
26 | module.exports = function (cfg, site) {
27 |
28 | var restful = cfg.hasOwnProperty('restful') ? cfg.restful :
29 | {
30 | site: true,
31 | posts_size: 10,
32 | posts_props: {
33 | title: true,
34 | slug: true,
35 | date: true,
36 | updated: true,
37 | comments: true,
38 | cover: true,
39 | path: true,
40 | raw: false,
41 | excerpt: false,
42 | content: false,
43 | categories: true,
44 | tags: true
45 | },
46 | categories: true,
47 | use_category_slug: false,
48 | tags: true,
49 | use_tag_slug: false,
50 | post: true,
51 | pages: false,
52 | },
53 |
54 | posts = site.posts.sort('-date').filter(function (post) {
55 | return post.published;
56 | }),
57 |
58 | posts_props = (function () {
59 | var props = restful.posts_props;
60 |
61 | return function (name, val) {
62 | return props[name] ? (typeof val === 'function' ? val() : val) : null;
63 | }
64 | })(),
65 |
66 | postMap = function (post) {
67 | return {
68 | title: posts_props('title', post.title),
69 | slug: posts_props('slug', post.slug),
70 | date: posts_props('date', post.date),
71 | updated: posts_props('updated', post.updated),
72 | comments: posts_props('comments', post.comments),
73 | path: posts_props('path', 'api/articles/' + post.slug + '.json'),
74 | excerpt: posts_props('excerpt', filterHTMLTags(post.excerpt)),
75 | keywords: posts_props('keywords', cfg.keywords),
76 | // cover: posts_props('cover', fetchCover(post.content)),
77 | cover: posts_props('cover', post.cover || fetchCover(post.content)),
78 | content: posts_props('content', post.content),
79 | raw: posts_props('raw', post.raw),
80 | categories: posts_props('categories', function () {
81 | return post.categories.map(function (cat) {
82 | const name = (
83 | cfg.restful.use_category_slug && cat.slug
84 | ) ? cat.slug : cat.name;
85 | return {
86 | name: name,
87 | path: 'api/categories/' + name + '.json'
88 | };
89 | });
90 | }),
91 | tags: posts_props('tags', function () {
92 | return post.tags.map(function (tag) {
93 | const name = (
94 | cfg.restful.use_tag_slug && tag.slug
95 | ) ? tag.slug : tag.name;
96 | return {
97 | name: name,
98 | path: 'api/tags/' + name + '.json'
99 | };
100 | });
101 | })
102 | };
103 | },
104 |
105 | cateReduce = function (cates, kind) {
106 | return cates.reduce(function (result, item) {
107 | if (!item.length) return result;
108 |
109 | let use_slug = null;
110 | switch (kind) {
111 | case 'categories':
112 | use_slug = cfg.restful.use_category_slug;
113 | break;
114 | case 'tags':
115 | use_slug = cfg.restful.use_tag_slug;
116 | break;
117 | }
118 |
119 | const name = (use_slug && item.slug) ? item.slug : item.name;
120 |
121 | return result.concat(pagination(item.path, posts, {
122 | perPage: 0,
123 | data: {
124 | name: name,
125 | path: 'api/' + kind + '/' + name + '.json',
126 | postlist: item.posts.map(postMap)
127 | }
128 |
129 | }));
130 | }, []);
131 | },
132 |
133 | catesMap = function (item) {
134 | return {
135 | name: item.data.name,
136 | path: item.data.path,
137 | count: item.data.postlist.length
138 | };
139 | },
140 |
141 | cateMap = function (item) {
142 | var itemData = item.data;
143 | return {
144 | path: itemData.path,
145 | data: JSON.stringify({
146 | name: itemData.name,
147 | postlist: itemData.postlist
148 | })
149 | };
150 | },
151 |
152 | apiData = [];
153 |
154 |
155 | if (restful.site) {
156 | apiData.push({
157 | path: 'api/site.json',
158 | data: JSON.stringify(restful.site instanceof Array ? _pick(cfg, restful.site) : cfg)
159 | });
160 | }
161 |
162 | if (restful.categories) {
163 |
164 | var cates = cateReduce(site.categories, 'categories');
165 |
166 | if (!!cates.length) {
167 | apiData.push({
168 | path: 'api/categories.json',
169 | data: JSON.stringify(cates.map(catesMap))
170 | });
171 |
172 | apiData = apiData.concat(cates.map(cateMap));
173 | }
174 |
175 | }
176 |
177 | if (restful.tags) {
178 | var tags = cateReduce(site.tags, 'tags');
179 |
180 | if (tags.length) {
181 | apiData.push({
182 | path: 'api/tags.json',
183 | data: JSON.stringify(tags.map(catesMap))
184 | });
185 |
186 | apiData = apiData.concat(tags.map(cateMap));
187 | }
188 |
189 | }
190 |
191 | var postlist = posts.map(postMap);
192 |
193 | if (restful.posts_size > 0) {
194 |
195 | var page_posts = [],
196 | i = 0,
197 | len = postlist.length,
198 | ps = restful.posts_size,
199 | pc = Math.ceil(len / ps);
200 |
201 | for (; i < len; i += ps) {
202 | page_posts.push({
203 | path: 'api/posts/' + Math.ceil((i + 1) / ps) + '.json',
204 | data: JSON.stringify({
205 | total: len,
206 | pageSize: ps,
207 | pageCount: pc,
208 | data: postlist.slice(i, i + ps)
209 | })
210 | });
211 | }
212 |
213 | apiData.push({
214 | path: 'api/posts.json',
215 | data: page_posts[0].data
216 | });
217 |
218 | apiData = apiData.concat(page_posts);
219 |
220 | } else {
221 |
222 | apiData.push({
223 | path: 'api/posts.json',
224 | data: JSON.stringify(postlist)
225 | });
226 | }
227 |
228 | if (restful.post) {
229 | apiData = apiData.concat(posts.map(function (post) {
230 | var path = 'api/articles/' + post.slug + '.json';
231 | return {
232 | path: path,
233 | data: JSON.stringify({
234 | title: post.title,
235 | slug: post.slug,
236 | date: post.date,
237 | updated: post.updated,
238 | comments: post.comments,
239 | path: path,
240 | excerpt: filterHTMLTags(post.excerpt),
241 | covers: fetchCovers(post.content),
242 | keywords: cfg.keyword,
243 | content: post.content,
244 | more: post.more,
245 | categories: post.categories.map(function (cat) {
246 | return {
247 | name: cat.name,
248 | path: 'api/categories/' + cat.name + '.json'
249 | };
250 | }),
251 | tags: post.tags.map(function (tag) {
252 | return {
253 | name: tag.name,
254 | path: 'api/tags/' + tag.name + '.json'
255 | };
256 | })
257 | })
258 | };
259 | }));
260 | }
261 |
262 | if (restful.pages) {
263 | apiData = apiData.concat(site.pages.data.map(function (page) {
264 | var safe_title = page.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()
265 | var path = 'api/pages/' + safe_title + '.json';
266 |
267 | return {
268 | path: path,
269 | data: JSON.stringify({
270 | title: page.title,
271 | date: page.date,
272 | updated: page.updated,
273 | comments: page.comments,
274 | path: path,
275 | covers: fetchCovers(page.content),
276 | excerpt: filterHTMLTags(page.excerpt),
277 | content: page.content
278 | })
279 | };
280 | }));
281 | }
282 |
283 | return apiData;
284 | };
285 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hexo-generator-restful",
3 | "version": "0.2.3",
4 | "description": "Generate restful json data for Hexo plugins.",
5 | "main": "index.js",
6 | "keywords": ["hexo", "restful", "json", "generator"],
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/yscoder/hexo-generator-restful.git"
10 | },
11 | "author": "yusen",
12 | "license": "MIT",
13 | "dependencies": {
14 | "hexo-pagination": "^0.1.0",
15 | "lodash.pick": "^4.4.0"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------