├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jscsrc ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── dom-filter.js └── hexo-excerpt.js ├── package-lock.json ├── package.json ├── tea.yaml └── tests ├── with-config.js ├── with-filter.js └── without-config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "rules": { 10 | "semi": 2 11 | }, 12 | "root": true 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | *.log 4 | coverage/ 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": ["node_modules/**", "coverage/**"], 3 | "preset": "hexo" 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | tmp/ 3 | coverage/ 4 | *.log 5 | .travis.yml 6 | gulpfile.js 7 | .idea/ 8 | appveyor.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | cache: 6 | apt: true 7 | directories: 8 | - node_modules 9 | 10 | node_js: 11 | - 12 12 | - 14 13 | - 15 14 | 15 | script: 16 | - npm run eslint 17 | - npm run test-cover 18 | 19 | after_script: 20 | - npm run test-cover-report 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Kun Che 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-excerpt 2 | 3 | [![Build Status](https://travis-ci.org/chekun/hexo-excerpt.svg?branch=master)](https://travis-ci.org/chekun/hexo-excerpt) [![NPM version](https://badge.fury.io/js/hexo-excerpt.svg)](http://badge.fury.io/js/hexo-excerpt) [![Coverage Status](https://img.shields.io/coveralls/chekun/hexo-excerpt.svg)](https://coveralls.io/r/chekun/hexo-excerpt?branch=master) 4 | 5 | Automatic excerpt generator for [Hexo!](http://hexo.io/). 6 | 7 | [Hexo-excerpt: https://chekun.me/post/hexo-excerpt](https://chekun.me/post/hexo-excerpt) 8 | 9 | ## Installation 10 | 11 | ``` bash 12 | $ npm install hexo-excerpt --save 13 | ``` 14 | 15 | > This Plugin use es6 syntax, make sure your node support it. 16 | 17 | ## Features 18 | 19 | - still works! 20 | - If you're lazy as I am, the plugin generate the excerpt for you, without breaking your sentences or codes! 21 | - If no tag is specified, the post will be updated with excerpt and more variables. 22 | - CSS selector can be used to filter generated excerpt. 23 | 24 | ## How? 25 | 26 | This plugin runs through all your posts, if your post has more than the configured number of direct tags, then they will be the excerpt, otherwise the whole post will be used. 27 | 28 | ## Configuration 29 | 30 | You can specify the size of the excerpt by setting depth in your config, which defaults to 10. 31 | 32 | You can also exclude certain tags from the generated excerpt using css selectors. 33 | Tags matching any of the selectors will be excluded. 34 | 35 | The default behaviour is to only show an excerpt if it would not be the whole post. Set `hideWholePostExcerpts` to `false` to override that and show whole post excerpts. 36 | 37 | ``` yaml 38 | excerpt: 39 | depth: 10 40 | excerpt_excludes: [] 41 | more_excludes: [] 42 | hideWholePostExcerpts: true 43 | ``` 44 | 45 | ## License 46 | 47 | MIT 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let excerpt = require('./lib/hexo-excerpt'); 4 | 5 | hexo.extend.generator.register('excerpt', excerpt); 6 | -------------------------------------------------------------------------------- /lib/dom-filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const domutils = require('domutils'); 4 | const CSSselect = require('css-select'); 5 | 6 | class Filter { 7 | constructor(hexo, excludes) { 8 | this.hexo = hexo; 9 | this.selectors = excludes.map(this._compile.bind(this)); 10 | } 11 | 12 | _compile(selector) { 13 | try { 14 | return CSSselect.compile(selector); 15 | } catch (err) { 16 | this.hexo.log.error('hexo-excerpt: Ignore invalid CSS selector: ' + selector); 17 | return () => false; 18 | } 19 | } 20 | 21 | match(node) { 22 | // not match any 23 | return !this.selectors.some(s => s(node)); 24 | } 25 | 26 | filter(nodes) { 27 | // remove inner nodes that doesn't match 28 | domutils.filter(n => !this.match(n), nodes) 29 | .forEach(node => domutils.removeElement(node)); 30 | 31 | // only keep top level nodes that match 32 | return nodes.filter(this.match.bind(this)); 33 | } 34 | } 35 | 36 | module.exports = Filter; 37 | -------------------------------------------------------------------------------- /lib/hexo-excerpt.js: -------------------------------------------------------------------------------- 1 | /* global hexo */ 2 | 'use strict'; 3 | 4 | const htmlparser = require('htmlparser2'); 5 | const domrender = require("dom-serializer").default; 6 | const defaults = require('lodash.defaults'); 7 | 8 | const Filter = require('./dom-filter'); 9 | 10 | const DEFAULT_CONFIG = { 11 | depth: 10, 12 | excerpt_excludes: [], 13 | more_excludes: [], 14 | hideWholePostExcerpts: false 15 | }; 16 | 17 | module.exports = function(db) { 18 | let legacy = {}; 19 | if (this.config.excerpt_depth) { 20 | this.log.warn('excerpt_depth is deprecated, please use excerpt.depth instead.'); 21 | legacy.depth = this.config.excerpt_depth; 22 | } 23 | 24 | let opts = defaults({}, this.config.excerpt, legacy, DEFAULT_CONFIG); 25 | opts.depth = parseInt(opts.depth); 26 | if (!Array.isArray(opts.excerpt_excludes)) opts.excerpt_excludes = [opts.excerpt_excludes]; 27 | if (!Array.isArray(opts.more_excludes)) opts.more_excludes = [opts.more_excludes]; 28 | 29 | // create filters 30 | let excerptFilter = new Filter(this, opts.excerpt_excludes); 31 | let moreFilter = new Filter(this, opts.more_excludes); 32 | 33 | return db.posts.map(post => { 34 | 35 | //honour the !!! 36 | if (post.excerpt || //.test(post.content) || post.content.indexOf('') !== -1 || post.content.indexOf('') !== -1) { 37 | return { 38 | path: post.path, 39 | data: post, 40 | layout: post.layout 41 | }; 42 | } 43 | 44 | const nodes = htmlparser.parseDocument(post.content, { decodeEntities: false }).children; 45 | 46 | // tracks how many tag nodes we found 47 | let stopIndex = 1; 48 | // tracks how many nodes we found in total 49 | let index = 0; 50 | for (; index < nodes.length && stopIndex <= opts.depth; index++) { 51 | if (nodes[index].type === 'tag' && excerptFilter.match(nodes[index])) { 52 | stopIndex++; 53 | } 54 | } 55 | 56 | // set correct excerpt and more nodes values 57 | let excerptNodes = nodes.slice(0, index); 58 | let moreNodes = nodes.slice(index); 59 | 60 | // filter nodes 61 | excerptNodes = excerptFilter.filter(excerptNodes); 62 | moreNodes = moreFilter.filter(moreNodes); 63 | 64 | // If the hideWholePostExcerpts option is set to true (the default), don't show 65 | // excerpts for short posts (i.e. ones where the excerpt is the whole post) 66 | if (moreNodes.length != 0 || !opts.hideWholePostExcerpts) { 67 | post.excerpt = domrender(excerptNodes, { decodeEntities: false }); 68 | post.more = domrender(moreNodes, { decodeEntities: false }); 69 | } 70 | 71 | return { 72 | path: post.path, 73 | data: post, 74 | layout: post.layout 75 | }; 76 | }); 77 | 78 | }; 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-excerpt", 3 | "version": "1.3.1", 4 | "description": "Automatic excerpt generator for Hexo!", 5 | "main": "index", 6 | "scripts": { 7 | "eslint": "eslint .", 8 | "test": "mocha tests/*.js", 9 | "test-cover": "nyc mocha tests/*.js", 10 | "test-cover-report": "nyc report --reporter=text-lcov | coveralls" 11 | }, 12 | "directories": { 13 | "lib": "./lib" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/chekun/hexo-excerpt.git" 18 | }, 19 | "homepage": "https://chekun.me/post/hexo-excerpt", 20 | "keywords": [ 21 | "hexo", 22 | "excerpt" 23 | ], 24 | "author": "Kun Che (https://chekun.me/)", 25 | "license": "MIT", 26 | "dependencies": { 27 | "css-select": "^4.0.0", 28 | "dom-serializer": "^1.3.1", 29 | "domutils": "^2.5.2", 30 | "htmlparser2": "^6.1.0", 31 | "lodash.defaults": "^4.2.0" 32 | }, 33 | "devDependencies": { 34 | "chai": "^4.3.4", 35 | "coveralls": "^3.1.0", 36 | "eslint": "^7.24.0", 37 | "eslint-config-hexo": "^4.1.0", 38 | "hexo": "^5.4.1", 39 | "mocha": "^10.0.0", 40 | "nyc": "^15.1.0", 41 | "json-schema": "^0.4.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x7956727e94e1903dbC1Ef9C038b29f51f814F2B9' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tests/with-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('chai').should(); 4 | const Hexo = require('hexo'); 5 | const hexoExcerptGenerator = require('../lib/hexo-excerpt'); 6 | 7 | const hexo = new Hexo(__dirname, { 8 | silent: true 9 | }); 10 | 11 | let posts = [ 12 | { 13 | path: '', 14 | title: 'post with <= 10 blocks', 15 | excerpt: '', 16 | layout: '', 17 | content: ` 18 |

block 1

19 |

block 2 123

20 |
block3
`.trim() 21 | }, 22 | { 23 | path: '', 24 | title: 'post with > 10 blocks', 25 | excerpt: '', 26 | layout: '', 27 | content: ` 28 |

block 1

29 |

block 2

30 |

block 3

31 |

block 4

32 |

block 5

33 |

block 6

34 |

block 7

35 |

block 8

36 |

block 9

37 |

block 10

38 |

block 11

`.trim() 39 | }, 40 | { 41 | path: '', 42 | title: 'post with user defined more seperator', 43 | excerpt: '', 44 | layout: '', 45 | content: ` 46 |

block 1

47 | 48 |

block 2 123

49 |
block3
`.trim() 50 | } 51 | ]; 52 | 53 | hexo.locals.set('posts', posts); 54 | 55 | describe('Automatic excerpt generator with excerpt.depth = 5', () => { 56 | 57 | hexo.config.excerpt = { 58 | depth: 5 59 | }; 60 | 61 | let generatedPosts = hexoExcerptGenerator.call(hexo, hexo.locals.toObject()); 62 | 63 | it('post with <= 5 tags should have full content as excerpt', () => { 64 | generatedPosts[0].data.excerpt.should.equal('

block 1

\n

block 2 123

\n
block3
'); 65 | generatedPosts[0].data.more.should.equal(''); 66 | }); 67 | 68 | it('post with > 5 tags should have excerpt and more set', () => { 69 | generatedPosts[1].data.excerpt.should.not.equal(''); 70 | generatedPosts[1].data.excerpt.should.equal('

block 1

\n

block 2

\n

block 3

\n

block 4

\n

block 5

'); 71 | generatedPosts[1].data.more.should.equal('\n

block 6

\n

block 7

\n

block 8

\n

block 9

\n

block 10

\n

block 11

'); 72 | }); 73 | 74 | it('post with should return the way it is', () => { 75 | generatedPosts[2].data.excerpt.should.equal(''); 76 | }); 77 | 78 | }); 79 | 80 | describe('Automatic excerpt generator accepts deprecated excerpt_depth = 5', () => { 81 | 82 | hexo.config.excerpt_depth = 5; 83 | 84 | let generatedPosts = hexoExcerptGenerator.call(hexo, hexo.locals.toObject()); 85 | 86 | it('post with <= 5 tags should have full content as excerpt', () => { 87 | generatedPosts[0].data.excerpt.should.equal('

block 1

\n

block 2 123

\n
block3
'); 88 | generatedPosts[0].data.more.should.equal(''); 89 | }); 90 | 91 | it('post with > 5 tags should have excerpt and more set', () => { 92 | generatedPosts[1].data.excerpt.should.not.equal(''); 93 | generatedPosts[1].data.excerpt.should.equal('

block 1

\n

block 2

\n

block 3

\n

block 4

\n

block 5

'); 94 | generatedPosts[1].data.more.should.equal('\n

block 6

\n

block 7

\n

block 8

\n

block 9

\n

block 10

\n

block 11

'); 95 | }); 96 | 97 | it('post with should return the way it is', () => { 98 | generatedPosts[2].data.excerpt.should.equal(''); 99 | }); 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /tests/with-filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('chai').should(); 4 | const Hexo = require('hexo'); 5 | const hexoExcerptGenerator = require('../lib/hexo-excerpt'); 6 | 7 | const hexo = new Hexo(__dirname, { 8 | silent: true 9 | }); 10 | 11 | let posts = [ 12 | { 13 | path: '', 14 | title: 'post with <= 10 blocks', 15 | excerpt: '', 16 | layout: '', 17 | content: ` 18 |

block 1

19 |

block 2 123

20 |
block3
`.trim() 21 | }, 22 | { 23 | path: '', 24 | title: 'post with > 10 blocks', 25 | excerpt: '', 26 | layout: '', 27 | content: ` 28 |

block 1

29 |

block 2

30 |

block 3

31 |

block 4 abc

32 |

block 5

33 |

block 6

34 |

block 7

35 |

block 8

36 |

block 9

37 |

block 10

38 |

block 11

`.trim() 39 | }, 40 | { 41 | path: '', 42 | title: 'post with user defined more seperator', 43 | excerpt: '', 44 | layout: '', 45 | content: ` 46 |

block 1

47 | 48 |

block 2 123

49 |
block3
`.trim() 50 | } 51 | ]; 52 | 53 | hexo.locals.set('posts', posts); 54 | 55 | describe('Automatic excerpt generator with CSS selector', () => { 56 | hexo.config.excerpt = { 57 | depth: 5, 58 | excerpt_excludes: [ 59 | '.ignore', 60 | 'span.also', 61 | '.invalid.' 62 | ], 63 | more_excludes: [ 64 | '.ignore' 65 | ] 66 | }; 67 | 68 | let generatedPosts = hexoExcerptGenerator.call(hexo, hexo.locals.toObject()); 69 | 70 | it('tags matching selectors should not be included in excerpt', () => { 71 | generatedPosts[0].data.excerpt.should.equal('

block 1

\n

block 2

\n'); 72 | generatedPosts[0].data.more.should.equal(''); 73 | }); 74 | 75 | it('top level tags should be equal to depth after filtering', () => { 76 | generatedPosts[1].data.excerpt.should.not.equal(''); 77 | generatedPosts[1].data.excerpt.should.equal('

block 1

\n

block 2

\n

block 3

\n

block 4

\n\n

block 6

'); 78 | generatedPosts[1].data.more.should.equal('\n\n

block 8

\n

block 9

\n

block 10

\n

block 11

'); 79 | }); 80 | 81 | it('post with should return the way it is', () => { 82 | generatedPosts[2].data.excerpt.should.equal(''); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /tests/without-config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const should = require('chai').should(); 4 | const Hexo = require('hexo'); 5 | const hexoExcerptGenerator = require('../lib/hexo-excerpt'); 6 | 7 | const hexo = new Hexo(__dirname, { 8 | silent: true 9 | }); 10 | 11 | let posts = [ 12 | { 13 | path: '', 14 | title: 'post with <= 10 blocks', 15 | excerpt: '', 16 | layout: '', 17 | content: ` 18 |

block 1

19 |

block 2 123

20 |
block3
`.trim() 21 | }, 22 | { 23 | path: '', 24 | title: 'post with > 10 blocks', 25 | excerpt: '', 26 | layout: '', 27 | content: ` 28 |

block 1

29 |

block 2

30 |

block 3

31 |

block 4

32 |

block 5

33 |

block 6

34 |

block 7

35 |

block 8

36 |

block 9

37 |

block 10

38 |

block 11

`.trim() 39 | }, 40 | { 41 | path: '', 42 | title: 'post with user defined more seperator', 43 | excerpt: '', 44 | layout: '', 45 | content: ` 46 |

block 1

47 | 48 |

block 2 123

49 |
block3
`.trim() 50 | }, 51 | { 52 | path: '', 53 | title: 'post with html entities', 54 | excerpt: '', 55 | layout: '', 56 | content: ` 57 |

block 1 <span>

58 |

2

3

4

59 |

5

6

7

60 |

8

9

10

61 |

block 11 <span>

62 | `.trim() 63 | } 64 | ]; 65 | 66 | hexo.locals.set('posts', posts); 67 | 68 | describe('Automatic excerpt generator with default depth config', () => { 69 | 70 | hexo.config.excerpt_depth = null; 71 | 72 | let generatedPosts = hexoExcerptGenerator.call(hexo, hexo.locals.toObject()); 73 | 74 | it('post with <= 10 tags should have full content as excerpt', () => { 75 | generatedPosts[0].data.excerpt.should.equal('

block 1

\n

block 2 123

\n
block3
'); 76 | generatedPosts[0].data.more.should.equal(''); 77 | }); 78 | 79 | it('post with > 10 tags should have excerpt and more set', () => { 80 | generatedPosts[1].data.excerpt.should.not.equal(''); 81 | generatedPosts[1].data.excerpt.should.equal('

block 1

\n

block 2

\n

block 3

\n

block 4

\n

block 5

\n

block 6

\n

block 7

\n

block 8

\n

block 9

\n

block 10

'); 82 | generatedPosts[1].data.more.should.equal('\n

block 11

'); 83 | }); 84 | 85 | it('post with should return the way it is', () => { 86 | generatedPosts[2].data.excerpt.should.equal(''); 87 | }); 88 | 89 | it('post with html entities should have them intact', () => { 90 | generatedPosts[3].data.excerpt.should.equal('

block 1 <span>

\n

2

3

4

\n

5

6

7

\n

8

9

10

'); 91 | generatedPosts[3].data.more.should.equal('\n

block 11 <span>

'); 92 | }); 93 | 94 | }); 95 | --------------------------------------------------------------------------------