├── .editorconfig ├── .github └── workflows │ ├── benchmark.yml │ ├── build.yml │ ├── codeql.yml │ ├── deploy.js │ └── linter.yml ├── .gitignore ├── .no-postinstall ├── Cargo.toml ├── LICENSE ├── README.md ├── eslint.config.js ├── index.js ├── lib └── helper.js ├── package.json ├── postinstall.js ├── renovate.json ├── src └── lib.rs └── test ├── benchmark.py ├── benchmark.sh └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | run: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | - name: Set up Rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | profile: minimal 23 | default: true 24 | - run: npm install 25 | - run: npm run build-release 26 | - run: | 27 | cd test 28 | bash benchmark.sh 29 | cat report.csv 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Node.js package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-20.04, macos-12, macos-14, windows-2019] 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Get version 19 | id: version 20 | shell: bash 21 | run: | 22 | echo "VERSION=$([[ "$GITHUB_REF" == refs/tags/v* ]] && echo ${GITHUB_REF#refs/tags/v} || echo '0.0.0')" >> $GITHUB_OUTPUT 23 | 24 | - name: Set up Node.js 25 | uses: actions/setup-node@v4 26 | 27 | - name: Build native module 28 | id: module 29 | shell: bash 30 | run: | 31 | npm install 32 | npm run build-release 33 | npm test 34 | echo "BINARY_NAME=$(node -e 'console.log([process.platform, process.arch].join("__"))')" >> $GITHUB_OUTPUT 35 | 36 | - name: Upload to R2 37 | run: | 38 | node .github/workflows/deploy.js index.node hexo-word-counter/bin/nodejs/${{ steps.version.outputs.VERSION }}/${{ steps.module.outputs.BINARY_NAME }}.node 39 | env: 40 | ACCOUNT_ID: ${{ secrets.ACCOUNT_ID }} 41 | SECRET_ACCESS_KEY: ${{ secrets.SECRET_ACCESS_KEY }} 42 | ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }} 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "31 9 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v3 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v3 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const fs = require('fs'); 3 | const process = require('process'); 4 | 5 | function uploadPkgToR2(accountId, secretAccessKey, accessKeyId, bucketName, filename, uploadFilePath) { 6 | const endpointUrl = `https://${accountId}.r2.cloudflarestorage.com`; 7 | 8 | // Configure the AWS SDK to use the custom endpoint and credentials 9 | const s3 = new AWS.S3({ 10 | endpoint: new AWS.Endpoint(endpointUrl), 11 | accessKeyId: accessKeyId, 12 | secretAccessKey: secretAccessKey, 13 | s3ForcePathStyle: true, // needed with custom endpoint 14 | signatureVersion: 'v4' 15 | }); 16 | 17 | console.log(`Uploading asset: ${filename} to ${uploadFilePath} in bucket ${bucketName}, using ${endpointUrl}`); 18 | 19 | // Set up the parameters for the S3 upload 20 | const params = { 21 | Bucket: bucketName, 22 | Key: uploadFilePath, 23 | Body: fs.createReadStream(filename) 24 | }; 25 | 26 | // Perform the upload to S3 (R2 in this case) 27 | s3.upload(params, function(err, data) { 28 | if (err) { 29 | console.log("An error occurred", err); 30 | throw err; 31 | } 32 | console.log(`Successfully uploaded '${filename}' to ${data.Location}`); 33 | }); 34 | } 35 | 36 | if (process.argv.length < 4) { 37 | console.log("Usage: node deploy.js "); 38 | process.exit(1); 39 | } 40 | 41 | const accountId = process.env.ACCOUNT_ID; 42 | const secretAccessKey = process.env.SECRET_ACCESS_KEY; 43 | const accessKeyId = process.env.ACCESS_KEY_ID; 44 | const bucketName = "archive"; 45 | const filename = process.argv[2]; 46 | const uploadFilePath = process.argv[3]; 47 | 48 | uploadPkgToR2(accountId, secretAccessKey, accessKeyId, bucketName, filename, uploadFilePath); 49 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Use Node.js 13 | uses: actions/setup-node@v4 14 | - run: npm install 15 | - run: npm run build-release 16 | - run: npm run lint 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | *.log 5 | .idea/ 6 | package-lock.json 7 | Cargo.lock 8 | target/ 9 | index.node 10 | -------------------------------------------------------------------------------- /.no-postinstall: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-theme/hexo-word-counter/b72e4b7d444f6f475bb83c68ed88d7132e9af0d9/.no-postinstall -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "word-counter" 3 | publish = false 4 | version = "0.10.3" 5 | description = "Node.js bindings for word-counter" 6 | authors = ["Mimi "] 7 | edition = "2021" 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | regex = "1" 14 | unicode-segmentation = "1.11.0" 15 | 16 | [dependencies.neon] 17 | version = "1" 18 | default-features = false 19 | features = ["napi-1"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hexo Word Counter 2 | 3 | [![Build Status][github-image]][github-url] 4 | [![npm-image]][npm-url] 5 | [![hexo-image]][hexo-url] 6 | [![lic-image]](LICENSE) 7 | 8 | Word count and time to read for articles in Hexo blog. 9 | 10 | The word count is based on [Unicode® Standard Annex #29](https://www.unicode.org/reports/tr29/). Thus, when multiple languages are present in the post content, the total word count can be accurately counted. 11 | 12 | With the power of Rust, this plugin is faster than almost all other Hexo plugins that offer similar functionality. See the [benchmark](#Benchmark) below. 13 | 14 | ## Installation 15 | 16 | ![size-image] 17 | [![dm-image]][npm-url] 18 | [![dt-image]][npm-url] 19 | 20 | ```bash 21 | npm install hexo-word-counter 22 | hexo clean 23 | ``` 24 | 25 | ## Usage 26 | 27 | You can set options of hexo-word-counter in the **Hexo's `_config.yml`** (which locates in the root dir of your blog): 28 | 29 | ```yml 30 | symbols_count_time: 31 | symbols: true 32 | time: true 33 | total_symbols: true 34 | total_time: true 35 | exclude_codeblock: false 36 | wpm: 275 37 | suffix: "mins." 38 | ``` 39 | 40 | If `symbols_count_time` option is not specified, the default parameters will be used. 41 | 42 | ### Parameters 43 | 44 | * `wpm` – Words Per Minute. Default: `275`. You can check this [here](https://wordcounter.net). 45 | * Slow ≈ `200` 46 | * Normal ≈ `275` 47 | * Fast ≈ `350` 48 | * `suffix` – If time to read less then 60 minutes, added suffix as string parameter.\ 49 | If not defined, `mins.` will be used as default. 50 | * `exclude_codeblock` – Allow to exclude all content inside code blocks for more accurate words counting.\ 51 | If not defined, `false` will be used as default. 52 | 53 | **Note for Chinese users:** if you write posts in Chinese at most cases (without mixed English), recommended to set `wpm` to `300`.\ 54 | But if you usually mix your posts with English, set `wpm` to `275` will be nice. 55 | 56 | ### For NexT Theme 57 | 58 | This plugin integrated in «NexT» and after plugin enabled in main Hexo config, you may adjust options in NexT config: 59 | 60 | ```yml 61 | post_meta: 62 | item_text: true 63 | 64 | symbols_count_time: 65 | separated_meta: true 66 | item_text_total: false 67 | ``` 68 | 69 | ## Development 70 | 71 | You have to prepare both Node.js and rust toolchain to develop this plugin. 72 | 73 | ```bash 74 | git clone https://github.com/next-theme/hexo-word-counter.git 75 | cd hexo-word-counter 76 | npm install 77 | ``` 78 | 79 | You can run tests with or without coverage feedback: 80 | 81 | ```bash 82 | npm test 83 | npm run test-cov 84 | ``` 85 | 86 | And you can install the development version in your blog: 87 | 88 | ```bash 89 | cd blog 90 | npm i ../path/to/hexo-word-counter 91 | ``` 92 | 93 | ## Theme Integration 94 | 95 | If you're a theme developer, you can use the following code to integrate this plugin. 96 | 97 | ### Word Count 98 | 99 | The syntax is different depending on the templating engine of the theme. 100 | 101 | For Nunjucks / Swig: 102 | ``` 103 | {{ symbolsCount(post) }} 104 | ``` 105 | 106 | For Ejs: 107 | ``` 108 | <%- symbolsCount(post) %> 109 | ``` 110 | 111 | For Pug / Jade: 112 | ``` 113 | span=symbolsCount(post) 114 | ``` 115 | 116 | In the latter part, we use Nunjucks syntax as an example. 117 | 118 | ### Post Reading Time 119 | 120 | ``` 121 | {{ symbolsTime(post) }} 122 | ``` 123 | 124 | Or with predefined parameters: 125 | 126 | ``` 127 | {{ symbolsTime(post, awl, wpm, suffix) }} 128 | ``` 129 | 130 | ### Total Word Count 131 | 132 | ``` 133 | {{ symbolsCountTotal(site) }} 134 | ``` 135 | 136 | ### Total Post Reading Time 137 | 138 | ``` 139 | {{ symbolsTimeTotal(site) }} 140 | ``` 141 | 142 | Or with predefined parameters: 143 | 144 | ``` 145 | {{ symbolsTimeTotal(site, awl, wpm, suffix) }} 146 | ``` 147 | 148 | ## Benchmark 149 | 150 | See [GitHub actions](https://github.com/next-theme/hexo-word-counter/actions/runs/3391961808/jobs/5637627050). 151 | 152 | | Plugin installed | Time of `hexo g` | 153 | | - | - | 154 | | Baseline | 19.48s | 155 | | hexo-word-counter | 19.63s (+0.78%) | 156 | | [hexo-symbols-count-time](https://github.com/theme-next/hexo-symbols-count-time) | 19.86s (+1.99%) | 157 | | [hexo-wordcount](https://github.com/willin/hexo-wordcount) | 21.44s (+10.08%) | 158 | | [hexo-reading-time](https://github.com/ierhyna/hexo-reading-time) | 23.81s (+22.26%) | 159 | 160 | [github-image]: https://img.shields.io/github/actions/workflow/status/next-theme/hexo-word-counter/linter.yml?branch=main&style=flat-square 161 | [npm-image]: https://img.shields.io/npm/v/hexo-word-counter?style=flat-square 162 | [hexo-image]: https://img.shields.io/badge/hexo-%3E%3D%203.0-blue?style=flat-square 163 | [cover-image]: https://img.shields.io/coveralls/next-theme/hexo-word-counter/master?style=flat-square 164 | [lic-image]: https://img.shields.io/npm/l/hexo-word-counter?style=flat-square 165 | 166 | [size-image]: https://img.shields.io/github/languages/code-size/next-theme/hexo-word-counter?style=flat-square 167 | [dm-image]: https://img.shields.io/npm/dm/hexo-word-counter?style=flat-square 168 | [dt-image]: https://img.shields.io/npm/dt/hexo-word-counter?style=flat-square 169 | 170 | [github-url]: https://github.com/next-theme/hexo-word-counter/actions?query=workflow%3ALinter 171 | [npm-url]: https://www.npmjs.com/package/hexo-word-counter 172 | [hexo-url]: https://hexo.io 173 | [cover-url]: https://coveralls.io/github/next-theme/hexo-word-counter?branch=master "Coverage of Tests" 174 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const config = require("@next-theme/eslint-config"); 2 | 3 | module.exports = config; 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global hexo */ 2 | 3 | 'use strict'; 4 | 5 | const helper = require('./lib/helper'); 6 | const { wordCount } = require('./index.node'); 7 | 8 | const rBacktick = /^((?:[^\S\r\n]*>){0,3}[^\S\r\n]*)(`{3,}|~{3,})[^\S\r\n]*((?:.*?[^`\s])?)[^\S\r\n]*\n((?:[\s\S]*?\n)?)(?:(?:[^\S\r\n]*>){0,3}[^\S\r\n]*)\2[^\S\r\n]?(\n+|$)/gm; 9 | 10 | hexo.config.symbols_count_time = Object.assign({ 11 | symbols : true, 12 | time : true, 13 | total_symbols : true, 14 | total_time : true, 15 | exclude_codeblock: false, 16 | wpm : 275, 17 | suffix : 'mins.' 18 | }, hexo.config.symbols_count_time); 19 | const config = hexo.config.symbols_count_time; 20 | 21 | helper.setConfig(config); 22 | 23 | if (config.symbols) { 24 | hexo.extend.helper.register('symbolsCount', helper.symbolsCount); 25 | hexo.extend.helper.register('wordcount', helper.symbolsCount); 26 | } 27 | 28 | if (config.time) { 29 | hexo.extend.helper.register('symbolsTime', helper.symbolsTime); 30 | hexo.extend.helper.register('min2read', helper.symbolsTime); 31 | } 32 | 33 | if (config.total_symbols) { 34 | hexo.extend.helper.register('symbolsCountTotal', helper.symbolsCountTotal); 35 | hexo.extend.helper.register('totalcount', helper.symbolsCountTotal); 36 | } 37 | 38 | if (config.total_time) { 39 | hexo.extend.helper.register('symbolsTimeTotal', helper.symbolsTimeTotal); 40 | } 41 | 42 | if (config.symbols || config.time || config.total_symbols || config.total_time) { 43 | hexo.extend.filter.register('after_post_render', data => { 44 | let { _content } = data; 45 | if (config.exclude_codeblock) _content = _content.replace(rBacktick, ''); 46 | data.length = wordCount(_content); 47 | }, 0); 48 | } 49 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let config = {}; 4 | 5 | module.exports.setConfig = function(_config) { 6 | config = _config; 7 | }; 8 | 9 | function getSymbols(post) { 10 | return post.length; 11 | } 12 | 13 | function getFormatTime(minutes, suffix) { 14 | const fHours = Math.floor(minutes / 60); 15 | let fMinutes = Math.floor(minutes - (fHours * 60)); 16 | if (fMinutes < 1) { 17 | fMinutes = 1; // 0 => 1 18 | } 19 | return fHours < 1 20 | ? fMinutes + ' ' + suffix // < 59 => 59 mins. 21 | : fHours + ':' + ('00' + fMinutes).slice(-2); // = 61 => 1:01 22 | } 23 | 24 | module.exports.symbolsCount = function(post) { 25 | let symbolsResult = getSymbols(post); 26 | if (symbolsResult > 9999) { 27 | symbolsResult = Math.round(symbolsResult / 1000) + 'k'; // > 9999 => 11k 28 | } else if (symbolsResult > 999) { 29 | symbolsResult = (Math.round(symbolsResult / 100) / 10) + 'k'; // > 999 => 1.1k 30 | } // < 999 => 111 31 | return symbolsResult; 32 | }; 33 | 34 | module.exports.symbolsTime = function(post, awl, wpm = config.wpm, suffix = config.suffix) { 35 | const minutes = Math.round(getSymbols(post) / wpm); 36 | return getFormatTime(minutes, suffix); 37 | }; 38 | 39 | function getSymbolsTotal(site) { 40 | let symbolsResultCount = 0; 41 | site.posts.forEach(post => { 42 | symbolsResultCount += getSymbols(post); 43 | }); 44 | return symbolsResultCount; 45 | } 46 | 47 | module.exports.symbolsCountTotal = function(site) { 48 | const symbolsResultTotal = getSymbolsTotal(site); 49 | return symbolsResultTotal < 1000000 50 | ? Math.round(symbolsResultTotal / 1000) + 'k' // < 999k => 111k 51 | : (Math.round(symbolsResultTotal / 100000) / 10) + 'm'; // > 999k => 1.1m 52 | }; 53 | 54 | module.exports.symbolsTimeTotal = function(site, awl, wpm = config.wpm, suffix = config.suffix) { 55 | const minutes = Math.round(getSymbolsTotal(site) / wpm); 56 | return getFormatTime(minutes, suffix); 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-word-counter", 3 | "version": "0.2.0", 4 | "description": "Symbols count and time to read for articles.", 5 | "main": "index", 6 | "files": [ 7 | "src/**", 8 | "Cargo.toml", 9 | "lib", 10 | "index.js", 11 | "postinstall.js" 12 | ], 13 | "scripts": { 14 | "build": "cargo-cp-artifact --artifact cdylib word-counter index.node -- cargo build --message-format=json-render-diagnostics", 15 | "build-release": "npm run build -- --release", 16 | "lint": "eslint index.js lib/", 17 | "postinstall": "node postinstall.js", 18 | "test": "mocha test --reporter spec", 19 | "test-cov": "c8 --print both _mocha -- test/index.js" 20 | }, 21 | "repository": "next-theme/hexo-word-counter", 22 | "keywords": [ 23 | "hexo", 24 | "count", 25 | "symbols", 26 | "time-to-read" 27 | ], 28 | "author": "Mimi (https://zhangshuqiao.org)", 29 | "license": "LGPL-3.0", 30 | "dependencies": { 31 | "cargo-cp-artifact": "^0.1" 32 | }, 33 | "devDependencies": { 34 | "@next-theme/eslint-config": "0.0.4", 35 | "aws-sdk": "^2.1599.0", 36 | "c8": "^10.0.0", 37 | "chai": "4.4.1", 38 | "eslint": "9.15.0", 39 | "lorem-ipsum": "^2.0.8", 40 | "mocha": "10.8.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | /*! 2 | MIT License 3 | 4 | Copyright (c) 2020 Wilson Lin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | https://github.com/wilsonzlin/minify-html/blob/master/nodejs/postinstall.js 25 | */ 26 | 27 | const fs = require('fs'); 28 | const https = require('https'); 29 | const path = require('path'); 30 | const pkg = require('./package.json'); 31 | const cp = require('child_process'); 32 | 33 | const MAX_DOWNLOAD_ATTEMPTS = 4; 34 | 35 | const binaryName = [process.platform, process.arch].join('__'); 36 | const binaryPath = path.join(__dirname, 'index.node'); 37 | 38 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); 39 | 40 | class StatusError extends Error { 41 | constructor(status) { 42 | super(`Bad status of ${status}`); 43 | this.status = status; 44 | } 45 | } 46 | 47 | const fetch = url => 48 | new Promise((resolve, reject) => { 49 | const stream = https.get(url, resp => { 50 | if (!resp.statusCode || resp.statusCode < 200 || resp.statusCode > 299) { 51 | return reject(new StatusError(resp.statusCode)); 52 | } 53 | const parts = []; 54 | resp.on('data', chunk => parts.push(chunk)); 55 | resp.on('end', () => resolve(Buffer.concat(parts))); 56 | }); 57 | stream.on('error', reject); 58 | }); 59 | 60 | const downloadNativeBinary = async() => { 61 | for (let attempt = 0; ; attempt++) { 62 | let binary; 63 | try { 64 | binary = await fetch( 65 | `https://archive.zsq.im/hexo-word-counter/bin/nodejs/${pkg.version}/${binaryName}.node` 66 | ); 67 | } catch (e) { 68 | if ( 69 | e instanceof StatusError 70 | && e.status !== 404 71 | && attempt < MAX_DOWNLOAD_ATTEMPTS 72 | ) { 73 | await wait((Math.random() * 2500) + 500); 74 | continue; 75 | } 76 | throw e; 77 | } 78 | 79 | fs.writeFileSync(binaryPath, binary); 80 | break; 81 | } 82 | }; 83 | 84 | if ( 85 | !fs.existsSync(path.join(__dirname, '.no-postinstall')) 86 | && !fs.existsSync(binaryPath) 87 | ) { 88 | downloadNativeBinary().then( 89 | () => console.log(`Downloaded ${pkg.name}`), 90 | err => { 91 | console.error( 92 | `Failed to download ${pkg.name}, will build from source: ${err}` 93 | ); 94 | const out = cp.spawnSync('npm', ['run', 'build-release'], { 95 | cwd: __dirname, 96 | stdio: ['ignore', 'inherit', 'inherit'] 97 | }); 98 | process.exitCode = out.exitCode; 99 | if (out.error) { 100 | throw out.error; 101 | } 102 | } 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use neon::prelude::*; 2 | 3 | // unicode word seperator (CJK friendly) 4 | use unicode_segmentation::UnicodeSegmentation; 5 | use regex::Regex; 6 | 7 | fn word_count(mut cx: FunctionContext) -> JsResult { 8 | let str = cx.argument::(0)?; 9 | 10 | let content = str.value(&mut cx); 11 | let mut word_count = 0; 12 | // match links and files in grammar "[](...)" 13 | let link_re = Regex::new(r"\]\((.*?)\)").unwrap(); 14 | 15 | // process document 16 | for line in content.lines() { 17 | let line = link_re.replace_all(&line, "]"); 18 | let words: Vec<&str> = line.unicode_words().collect(); 19 | word_count += words.len(); 20 | } 21 | 22 | Ok(cx.number(word_count as f64)) 23 | } 24 | 25 | #[neon::main] 26 | fn main(mut cx: ModuleContext) -> NeonResult<()> { 27 | cx.export_function("wordCount", word_count)?; 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /test/benchmark.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | def run_command_and_measure_time(): 6 | os.system("npx hexo clean") 7 | start_time = time.time() 8 | os.system("npx hexo g") 9 | return time.time() - start_time 10 | 11 | 12 | if __name__ == "__main__": 13 | name = sys.argv[1] 14 | with open("../report.csv", "a") as f: 15 | results = [] 16 | for i in range(10): 17 | res = run_command_and_measure_time() 18 | results.append(res) 19 | f.write(name + "," + str(res) + "\n") 20 | f.flush() 21 | f.write(name + "-avg," + str(sum(results) / len(results)) + "\n") 22 | -------------------------------------------------------------------------------- /test/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ============================================================== # 3 | # Shell script to autodeploy Hexo & NexT & NexT website source. 4 | # ============================================================== # 5 | PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin:$PATH 6 | export PATH 7 | 8 | # https://en.wikipedia.org/wiki/ANSI_escape_code 9 | #red='\033[0;31m' 10 | #green='\033[0;32m' 11 | #brown='\033[0;33m' 12 | #blue='\033[0;34m' 13 | #purple='\033[0;35m' 14 | cyan='\033[0;36m' 15 | #lgray='\033[0;37m' 16 | #dgray='\033[1;30m' 17 | lred='\033[1;31m' 18 | lgreen='\033[1;32m' 19 | yellow='\033[1;33m' 20 | lblue='\033[1;34m' 21 | lpurple='\033[1;35m' 22 | lcyan='\033[1;36m' 23 | white='\033[1;37m' 24 | norm='\033[0m' 25 | bold='\033[1m' 26 | 27 | echo 28 | echo "==============================================================" 29 | echo " ${yellow}Checking starting directory structure...${norm}" 30 | echo "==============================================================" 31 | echo "${lcyan}`pwd`${norm}" 32 | du -sh 33 | du -sh * 34 | 35 | echo 36 | echo "==============================================================" 37 | echo " ${lgreen}Checking Node.js & NPM version...${norm}" 38 | echo "==============================================================" 39 | echo "${yellow}Node version:${norm} ${lcyan}`node -v`${norm}" 40 | echo "${yellow}NPM version:${norm} ${lcyan}`npm -v`${norm}" 41 | 42 | echo 43 | echo "==============================================================" 44 | echo " ${lgreen}Installing Hexo & NPM modules...${norm}" 45 | echo "==============================================================" 46 | rm -rf .tmp-hexo-optimize-test 47 | git clone https://github.com/hexojs/hexo-theme-unit-test .tmp-hexo-optimize-test 48 | cd .tmp-hexo-optimize-test 49 | git submodule add https://github.com/hexojs/hexo-many-posts source/_posts/hexo-many-posts 50 | git submodule add -f https://github.com/hexojs/hexo-theme-landscape themes/landscape 51 | npm install --silent 52 | 53 | echo 54 | echo "==============================================================" 55 | echo " ${yellow}Checking Hexo version...${norm}" 56 | echo "==============================================================" 57 | hexo() { 58 | npx hexo "$@" 59 | } 60 | hexo -v 61 | npm ls --depth 0 62 | 63 | echo 64 | echo "==============================================================" 65 | echo " ${lpurple}Generating content for baseline...${norm}" 66 | echo "==============================================================" 67 | python3 ../benchmark.py baseline 68 | 69 | echo 70 | echo "==============================================================" 71 | echo " ${lpurple}Generating content for hexo-symbols-count-time...${norm}" 72 | echo "==============================================================" 73 | ejs=themes/landscape/layout/_partial/article.ejs 74 | npm install hexo-symbols-count-time 75 | echo '<%= symbolsCount(post) %>' >> $ejs 76 | python3 ../benchmark.py hexo-symbols-count-time 77 | 78 | echo 79 | echo "==============================================================" 80 | echo " ${lpurple}Generating content for hexo-reading-time...${norm}" 81 | echo "==============================================================" 82 | npm uninstall hexo-symbols-count-time 83 | npm install hexo-reading-time 84 | git submodule foreach git reset --hard 85 | echo '<%= readingTime(post.content) %>' >> $ejs 86 | python3 ../benchmark.py hexo-reading-time 87 | 88 | echo 89 | echo "==============================================================" 90 | echo " ${lpurple}Generating content for hexo-wordcount...${norm}" 91 | echo "==============================================================" 92 | npm uninstall hexo-reading-time 93 | npm install hexo-wordcount 94 | git submodule foreach git reset --hard 95 | echo '<%= wordcount(post.content) %>' >> $ejs 96 | python3 ../benchmark.py hexo-wordcount 97 | 98 | echo 99 | echo "==============================================================" 100 | echo " ${lpurple}Generating content for hexo-word-counter...${norm}" 101 | echo "==============================================================" 102 | npm uninstall hexo-wordcount 103 | npm install ../../ 104 | git submodule foreach git reset --hard 105 | echo '<%= symbolsCount(post) %>' >> $ejs 106 | python3 ../benchmark.py hexo-word-counter 107 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('chai').should(); 4 | 5 | const { LoremIpsum } = require('lorem-ipsum'); 6 | const helper = require('../lib/helper'); 7 | const { wordCount } = require('../index.node'); 8 | 9 | class Post { 10 | constructor(content) { 11 | this._content = content; 12 | this.length = wordCount(this._content); 13 | } 14 | } 15 | 16 | const cn = '天地玄黄,宇宙洪荒。日月盈昃,辰宿列张。寒来暑往,秋收冬藏。闰余成岁,律吕调阳。云腾致雨,露结为霜。金生丽水,玉出昆冈。剑号巨阙,珠称夜光。果珍李柰,菜重芥姜。海咸河淡,鳞潜羽翔。龙师火帝,鸟官人皇。始制文字,乃服衣裳。推位让国,有虞陶唐。吊民伐罪,周发殷汤。坐朝问道,垂拱平章。爱育黎首,臣伏戎羌。遐迩一体,率宾归王。鸣凤在竹,白驹食场。化被草木,赖及万方。盖此身发,四大五常。恭惟鞠养,岂敢毁伤。女慕贞洁,男效才良。知过必改,得能莫忘。罔谈彼短,靡恃己长。信使可覆,器欲难量。墨悲丝染,诗赞羔羊。景行维贤,克念作圣。德建名立,形端表正。空谷传声,虚堂习听。祸因恶积,福缘善庆。尺璧非宝,寸阴是竞。资父事君,曰严与敬。孝当竭力,忠则尽命。临深履薄,夙兴温凊。似兰斯馨,如松之盛。川流不息,渊澄取映。容止若思,言辞安定。笃初诚美,慎终宜令。荣业所基,籍甚无竟。学优登仕,摄职从政。存以甘棠,去而益咏。乐殊贵贱,礼别尊卑。上和下睦,夫唱妇随。外受傅训,入奉母仪。诸姑伯叔,犹子比儿。孔怀兄弟,同气连枝。交友投分,切磨箴规。仁慈隐恻,造次弗离。节义廉退,颠沛匪亏。性静情逸,心动神疲。守真志满,逐物意移。坚持雅操,好爵自縻。都邑华夏,东西二京。背邙面洛,浮渭据泾。宫殿盘郁,楼观飞惊。图写禽兽,画彩仙灵。丙舍旁启,甲帐对楹。肆筵设席,鼓瑟吹笙。升阶纳陛,弁转疑星。右通广内,左达承明。既集坟典,亦聚群英。杜稿钟隶,漆书壁经。府罗将相,路侠槐卿。户封八县,家给千兵。高冠陪辇,驱毂振缨。世禄侈富,车驾肥轻。策功茂实,勒碑刻铭。磻溪伊尹,佐时阿衡。奄宅曲阜,微旦孰营。桓公匡合,济弱扶倾。绮回汉惠,说感武丁。俊义密勿,多士实宁。晋楚更霸,赵魏困横。假途灭虢,践土会盟。何遵约法,韩弊烦刑。起翦颇牧,用军最精。宣威沙漠,驰誉丹青。九州禹迹,百郡秦并。岳宗泰岱,禅主云亭。雁门紫塞,鸡田赤城。昆池碣石,钜野洞庭。旷远绵邈,岩岫杳冥。治本于农,务兹稼穑。俶载南亩,我艺黍稷。税熟贡新,劝赏黜陟。孟轲敦素,史鱼秉直。庶几中庸,劳谦谨敕。聆音察理,鉴貌辨色。贻厥嘉猷,勉其祗植。省躬讥诫,宠增抗极。殆辱近耻,林皋幸即。两疏见机,解组谁逼。索居闲处,沉默寂寥。求古寻论,散虑逍遥。欣奏累遣,戚谢欢招。渠荷的历,园莽抽条。枇杷晚翠,梧桐蚤凋。陈根委翳,落叶飘摇。游鹍独运,凌摩绛霄。耽读玩市,寓目囊箱。易輶攸畏,属耳垣墙。具膳餐饭,适口充肠。饱饫烹宰,饥厌糟糠。亲戚故旧,老少异粮。妾御绩纺,侍巾帷房。纨扇圆洁,银烛炜煌。昼眠夕寐,蓝笋象床。弦歌酒宴,接杯举觞。矫手顿足,悦豫且康。嫡后嗣续,祭祀烝尝。稽颡再拜,悚惧恐惶。笺牒简要,顾答审详。骸垢想浴,执热愿凉。驴骡犊特,骇跃超骧。诛斩贼盗,捕获叛亡。布射僚丸,嵇琴阮啸。恬笔伦纸,钧巧任钓。释纷利俗,并皆佳妙。毛施淑姿,工颦妍笑。年矢每催,曦晖朗曜。璇玑悬斡,晦魄环照。指薪修祜,永绥吉劭。矩步引领,俯仰廊庙。束带矜庄,徘徊瞻眺。孤陋寡闻,愚蒙等诮。谓语助者,焉哉乎也。'; 17 | 18 | const ja = '人類社会のすべての構成員の固有の尊厳と平等で譲ることのできない権利とを承認することは、世界における自由、正義及び平和の基礎であるので、 人権の無視及び軽侮が、人類の良心を踏みにじった野蛮行為をもたらし、言論及び信仰の自由が受けられ、恐怖及び欠乏のない世界の到来が、一般の人々の最高の願望として宣言されたので、 人間が専制と圧迫とに対する最後の手段として反逆に訴えることがないようにするためには、法の支配によって人権を保護することが肝要であるので、 諸国間の友好関係の発展を促進することが肝要であるので、国際連合の諸国民は、国連憲章において、基本的人権、人間の尊厳及び価値並びに男女の同権についての信念を再確認し、かつ、一層大きな自由のうちで社会的進歩と生活水準の向上とを促進することを決意したので、 加盟国は、国際連合と協力して、人権及び基本的自由の普遍的な尊重及び遵守の促進を達成することを誓約したので、 これらの権利及び自由に対する共通の理解は、この誓約を完全にするためにもっとも重要であるので、 よって、ここに、国連総会は、 社会の各個人及び各機関が、この世界人権宣言を常に念頭に置きながら、加盟国自身の人民の間にも、また、加盟国の管轄下にある地域の人民の間にも、これらの権利と自由との尊重を指導及び教育によって促進すること並びにそれらの普遍的措置によって確保することに努力するように、すべての人民とすべての国とが達成すべき共通の基準として、この人権宣言を公布する。'; 19 | const jaGroundTruth = 596; 20 | const ko = '모든 사람은 의견의 자유와 표현의 자유에 대한 권리를 가진다. 이러한 권리는 간섭없이 의견을 가질 자유와 국경에 관계없이 어떠한 매체를 통해서도 정보와 사상을 추구하고, 얻으며, 전달하는 자유를 포함한다. 모든 사람은 사회의 일원으로서 사회보장을 받을 권리를 가지며, 국가적 노력과 국제적 협력을 통하여, 그리고 각 국가의 조직과 자원에 따라서 자신의 존엄과 인격의 자유로운 발전에 불가결한 경제적, 사회적 및 문화적 권리들을 실현할 권리를 가진다. 모든 사람은 노동시간의 합리적 제한과 정기적인 유급휴가를 포함하여 휴식과 여가의 권리를 가진다. 모든 사람은 이 선언에 규정된 권리와 자유가 완전히 실현될 수 있도록 사회적, 국제적 질서에 대한 권리를 가진다.'; 21 | const koGroundTruth = 89; 22 | 23 | const markdown = `![There are 4 words](https://example.com/should/exclude/words/in/url.jpg) 24 | [而这有六个字](https://example.com/should/exclude/words/in/url/)`; 25 | 26 | const config = { 27 | wpm: 275, 28 | suffix: 'mins.' 29 | }; 30 | 31 | helper.setConfig(config); 32 | 33 | describe('Hexo Symbols Count Time', () => { 34 | 35 | const lorem = new LoremIpsum({ 36 | sentencesPerParagraph: { 37 | max: 8, 38 | min: 8 39 | }, 40 | wordsPerSentence: { 41 | max: 16, 42 | min: 16 43 | } 44 | }); 45 | const en = lorem.generateParagraphs(1); 46 | 47 | describe('Test wordsCount multilingual', () => { 48 | 49 | it('Chinese', () => { 50 | const post = new Post(cn); 51 | const words = helper.symbolsCount(post); 52 | words.should.eq('1k'); 53 | }); 54 | 55 | it('Japanese', () => { 56 | const post = new Post(ja); 57 | const words = helper.symbolsCount(post); 58 | words.should.eq(jaGroundTruth); 59 | }); 60 | 61 | it('Korean', () => { 62 | const post = new Post(ko); 63 | const words = helper.symbolsCount(post); 64 | words.should.eq(koGroundTruth); 65 | }); 66 | 67 | it('Multilingual', () => { 68 | const post = new Post([ja, en, ko].join(' ')); 69 | const words = helper.symbolsCount(post); 70 | words.should.eq(jaGroundTruth + 128 + koGroundTruth); 71 | }); 72 | 73 | }); 74 | 75 | describe('Test wordsCount with markdown', () => { 76 | const post = new Post(markdown); 77 | const words = helper.symbolsCount(post); 78 | 79 | it('Markdown', () => { 80 | words.should.eq(10); 81 | }); 82 | 83 | }); 84 | 85 | describe('Test wordsCount > 999', () => { 86 | const post = new Post(lorem.generateParagraphs(10)); 87 | const words = helper.symbolsCount(post); 88 | 89 | it('Words: 1280 => 1.3k', () => { 90 | words.should.eq('1.3k'); 91 | }); 92 | }); 93 | 94 | describe('Test wordsCount > 9999', () => { 95 | const post = new Post(lorem.generateParagraphs(80)); 96 | const words = helper.symbolsCount(post); 97 | 98 | it('Words: 10240 => 10k', () => { 99 | words.should.eq('10k'); 100 | }); 101 | }); 102 | 103 | describe('Test wordsCount & wordsTime (wpm)', () => { 104 | 105 | const post = new Post(en); 106 | 107 | it('Words: (symbolsCount = 128)', () => { 108 | const words = helper.symbolsCount(post); 109 | words.should.eq(128); 110 | }); 111 | 112 | it('Time: [wpm = 200] => 1 mins.', () => { 113 | const words = helper.symbolsTime(post); 114 | words.should.eq('1 mins.'); 115 | }); 116 | 117 | it('Time: [wpm = 50] => 3 mins.', () => { 118 | const words = helper.symbolsTime(post, null, 50); 119 | words.should.eq('3 mins.'); 120 | }); 121 | 122 | it('Time: [wpm = 10] => 13 mins.', () => { 123 | const words = helper.symbolsTime(post, null, 10); 124 | words.should.eq('13 mins.'); 125 | }); 126 | 127 | }); 128 | 129 | describe('Test wordsTime (< 1h / > 1h 10min)', () => { 130 | 131 | const lessThanOneHourReading = new Post(lorem.generateParagraphs(120)); 132 | const moreThanOneHourReading = new Post(lorem.generateParagraphs(130)); 133 | const moreThanOneHourReadingAndMoreThanTenMinutes = new Post(lorem.generateParagraphs(160)); 134 | 135 | it('Time: 120 = 56 => 56 minutes', () => { 136 | const words = helper.symbolsTime(lessThanOneHourReading, null, 275, 'minutes'); 137 | words.should.eq('56 minutes'); 138 | }); 139 | 140 | it('Time: 130 = 61 => 1:01', () => { 141 | const words = helper.symbolsTime(moreThanOneHourReading, null, 275, 'minutes to read'); 142 | words.should.eq('1:01'); 143 | }); 144 | 145 | it('Time: 160 = 74 => 1:14', () => { 146 | const words = helper.symbolsTime(moreThanOneHourReadingAndMoreThanTenMinutes, null, 275, 'minutes to read'); 147 | words.should.eq('1:14'); 148 | }); 149 | 150 | }); 151 | 152 | }); 153 | --------------------------------------------------------------------------------