Compare commits

...

44 Commits

Author SHA1 Message Date
Klemek 99a19edb93 updated README.md 2021-03-30 19:19:55 +02:00
Klemek da900d2d02 update version to 1.3.0 2021-03-30 19:19:29 +02:00
Klemek 51c1afb4d6 Merge pull request #53 from Klemek/f-hit-counter
Hit counter #47
2021-03-30 19:17:30 +02:00
Klemek dd088a04a3 removed invalid tests 2021-03-30 19:14:00 +02:00
Klemek 6439f8eb92 hit-counter 2021-03-30 19:11:31 +02:00
Klemek 88cb7ce30f eslint update 2021-03-30 16:29:20 +02:00
Klemek b0ba52e140 work in progress hit_counter 2021-03-30 15:30:54 +02:00
Klemek fa5cf31983 comma dangle 2021-03-30 15:20:22 +02:00
Klemek 18b02cf267 eslint jest 2021-03-30 15:07:08 +02:00
Klemek d194fbf032 eslint in CI 2021-03-30 14:56:04 +02:00
Klemek e9d67985a6 eslint integration 2021-03-30 14:53:26 +02:00
Klemek f6d6f04d59 Ci setup (#52)
* Create node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml

* Update node.js.yml
2021-03-30 13:37:31 +02:00
Klemek 5e817cc296 fix tests 2021-03-30 12:48:04 +02:00
Klemek 0f5e40c9ff fix dependencies 2021-03-30 12:32:18 +02:00
Klemek ec79c88bdf Merge pull request #51 from Klemek/snyk-fix-1fcf8450e3fce8a6dcdfb766b19e91f2
[Snyk] Security upgrade ejs from 2.7.1 to 3.1.6
2021-03-30 10:56:07 +02:00
snyk-bot 604d291c37 fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-EJS-1049328
2021-03-30 08:55:25 +00:00
Klemek 0896256e20 Merge branch 'master' of github.com:klemek/gitblog.md 2021-03-30 10:50:32 +02:00
Klemek 250b8a8625 Removed travis CI 2021-03-30 10:50:26 +02:00
Klemek 4f0d37f959 Merge pull request #49 from Klemek/dependabot/npm_and_yarn/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1
2021-03-30 10:46:34 +02:00
dependabot[bot] 1bdf10a046 Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 08:46:04 +00:00
Klemek d25d1a3aa9 Merge pull request #48 from Klemek/dependabot/npm_and_yarn/handlebars-4.7.7
Bump handlebars from 4.2.0 to 4.7.7
2021-03-30 10:45:52 +02:00
Klemek 507d0a792e Merge branch 'master' into dependabot/npm_and_yarn/handlebars-4.7.7 2021-03-30 10:45:45 +02:00
dependabot[bot] da613867d9 Bump handlebars from 4.2.0 to 4.7.7
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.2.0 to 4.7.7.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.2.0...v4.7.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 08:45:09 +00:00
Klemek 50074a17c8 Merge pull request #45 from Klemek/dependabot/npm_and_yarn/prismjs-1.23.0
Bump prismjs from 1.17.1 to 1.23.0
2021-03-30 10:44:47 +02:00
Klemek 53c197146a Merge pull request #41 from Klemek/dependabot/npm_and_yarn/handlebars-4.7.6
Bump handlebars from 4.2.0 to 4.7.6
2021-03-30 10:44:35 +02:00
Klemek 09bef2aadd Merge branch 'master' into dependabot/npm_and_yarn/handlebars-4.7.6 2021-03-30 10:44:16 +02:00
Klemek 77e723e6f6 Merge pull request #40 from Klemek/dependabot/npm_and_yarn/showdown-1.9.1
Bump showdown from 1.9.0 to 1.9.1
2021-03-30 10:44:06 +02:00
Klemek 1d50072cc5 Merge pull request #37 from Klemek/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2021-03-30 10:43:54 +02:00
Klemek 157bb405c3 Merge pull request #34 from Klemek/dependabot/npm_and_yarn/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4
2021-03-30 10:43:41 +02:00
Klemek 4e10d0326b removed travis CI 2021-03-30 10:43:27 +02:00
dependabot[bot] c03393604c Bump prismjs from 1.17.1 to 1.23.0
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.17.1 to 1.23.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.17.1...v1.23.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-01 20:51:32 +00:00
klemek d15f3e04fb Dockerfile support 2021-01-20 17:00:28 +01:00
dependabot[bot] a21393294b Bump handlebars from 4.2.0 to 4.7.6
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.2.0 to 4.7.6.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.2.0...v4.7.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-07 08:56:42 +00:00
dependabot[bot] 191e24d218 Bump showdown from 1.9.0 to 1.9.1
Bumps [showdown](https://github.com/showdownjs/showdown) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/showdownjs/showdown/releases)
- [Changelog](https://github.com/showdownjs/showdown/blob/1.9.1/CHANGELOG.md)
- [Commits](https://github.com/showdownjs/showdown/compare/1.9.0...1.9.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 06:02:55 +00:00
dependabot[bot] 2c31015a14 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-18 08:05:56 +00:00
dependabot[bot] 4b681d5c3e Bump acorn from 5.7.3 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-16 12:59:05 +00:00
Klemek b19cf416f9 Merge pull request #29 from Klemek/add-license-1
Create LICENSE
2019-11-12 15:11:43 +01:00
Klemek 5c1a41c319 Create LICENSE 2019-11-12 15:09:46 +01:00
klemek 6e4b50eaa5 link to top was not working in most browsers 2019-10-25 08:26:58 +02:00
klemek 72e880eaf2 test 2019-10-22 21:48:42 +02:00
klemek 919b13db9c test 2019-10-19 16:25:52 +02:00
klemek e598a43acc test 2019-10-19 16:23:11 +02:00
Klemek 6fe5260bd2 Update README.md 2019-10-01 09:04:52 +02:00
Klemek 29ca0d291c Update README.md 2019-10-01 09:04:19 +02:00
27 changed files with 12665 additions and 1954 deletions
View File
+106
View File
@@ -0,0 +1,106 @@
module.exports = {
plugins: [ 'jest' ],
env: {
'commonjs': true,
'es2021': true,
'node': true,
'jest/globals': true,
},
extends: [
'eslint:recommended',
'plugin:jest/recommended',
],
parserOptions: {
ecmaVersion: 12,
},
rules: {
'indent': [
'error',
4,
],
'linebreak-style': [
'error',
'unix',
],
'quotes': [
'error',
'single',
],
'semi': [
'error',
'always',
],
'curly': [
'error',
'all',
],
'brace-style': [
'error',
'1tbs',
],
'jest/no-done-callback': 'off',
'jest/expect-expect': 'off',
'comma-dangle': [
'error',
'always-multiline',
],
'complexity': 'error',
'consistent-return': 'error',
'dot-location': [
'error',
'property',
],
'eqeqeq': [
'error',
'always',
{ null: 'ignore' },
],
'no-empty-function': 'error',
'no-floating-decimal': 'error',
'no-multi-spaces': 'error',
'camelcase': [
'error',
{ properties: 'never' },
],
'comma-spacing': [
'error',
{ before: false, after: true },
],
'array-bracket-newline': [
'error',
{ multiline: true },
],
'array-element-newline': [
'error',
{ multiline: true, minItems: 2 },
],
'array-bracket-spacing': [
'error',
'always',
],
'object-curly-spacing': [
'error',
'always',
],
'comma-style': 'error',
'computed-property-spacing': 'error',
'eol-last': 'error',
'func-call-spacing': 'error',
'key-spacing': 'error',
'keyword-spacing': 'error',
'multiline-comment-style': 'error',
'newline-per-chained-call': 'error',
'no-lonely-if': 'error',
'no-multiple-empty-lines': 'error',
'no-trailing-spaces': 'error',
'no-unneeded-ternary': 'error',
'no-whitespace-before-property': 'error',
'operator-assignment': 'error',
'quote-props': [
'error',
'consistent-as-needed',
],
'space-before-blocks': 'error',
'space-infix-ops': 'error',
},
};
+31
View File
@@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Tests and coverage
on: ["push", "pull_request"]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 15.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- name: ESLint
run: npm run test-lint
- name: Jest
run: npm run test-cov
- name: Coveralls
run: npm run coveralls
env:
COVERALLS_REPO_TOKEN: "${{ secrets.COVERALLS_REPO_TOKEN }}"
COVERALLS_GIT_BRANCH: "${{ github.ref }}"
-21
View File
@@ -1,21 +0,0 @@
{
"esversion": 6,
"maxerr": 999,
"indent": true,
"camelcase": true,
"eqeqeq": true,
"forin": true,
"immed": true,
"latedef": true,
"noarg": true,
"noempty": true,
"nonew": true,
"undef": true,
"unused": true,
"varstmt": true,
"sub": true,
"quotmark": "single",
"node": true,
"globals": {
}
}
-16
View File
@@ -1,16 +0,0 @@
dist: xenial
language: node_js
node_js:
- "12"
cache:
npm: true
directories:
- node_modules
install:
- npm install
before_script:
- npm install -g jshint
script:
- jest --coverage --silent
- jshint ./src
- cat ./coverage/lcov.info | coveralls
Executable
+18
View File
@@ -0,0 +1,18 @@
FROM node:14
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
CMD [ "sh", "-c", "node src/server.js" ]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+11 -1
View File
@@ -1,4 +1,4 @@
[![Build Status](https://img.shields.io/travis/Klemek/GitBlog.md.svg?branch=master)](https://travis-ci.org/Klemek/GitBlog.md)
[![Scc Count Badge](https://sloc.xyz/github/klemek/gitblog.md/?category=code)](https://github.com/boyter/scc/#badges-beta)
[![Coverage Status](https://img.shields.io/coveralls/github/Klemek/GitBlog.md.svg?branch=master)](https://coveralls.io/github/Klemek/GitBlog.md?branch=master)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/Klemek/GitBlog.md.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/GitBlog.md/context:javascript)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/Klemek/GitBlog.md.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/GitBlog.md/alerts/)
@@ -281,6 +281,8 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
activate PlantUML diagram rendering
* `fa-diagrams` (default: true)
activate fa-diagrams rendering
* `hit_counter` (default: true)
activate /stats endpoints and visitor counting (need an active redis connection)
* `home`
* `title` (default: GitBlog.md)
the title of your blog, **strongly advised to be changed**
@@ -331,3 +333,11 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
specify the output format between svg, html or MathMl (mml)
* `speak_text`: (default: true)
activate the alternate text in equations
* `hit_counter`
* `unique_visitor_timeout`: (default: 7200000 / 2h)
specify the time (in ms) before a visitor can be accounted again
* `redis`
Options to connect to redis (see [redis options](https://github.com/NodeRedis/node-redis#options-object-properties) for more info)
* `host`: (default: localhost)
* `port`: (default: 6379)
+10276 -386
View File
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -1,32 +1,39 @@
{
"name": "gitblog.md",
"version": "1.2.8",
"version": "1.3.0",
"description": "A static blog using Markdown pulled from your git repository.",
"main": "src/server.js",
"dependencies": {
"@iarna/toml": "^2.2.3",
"body-parser": "^1.19.0",
"ejs": "^2.6.2",
"ejs": "^3.1.6",
"express": "^4.17.1",
"express-rate-limit": "^5.0.0",
"fa-diagrams": "^1.0.3",
"mathjax-node": "^2.1.1",
"ncp": "^2.0.0",
"node-prismjs": "^0.1.2",
"prismjs": "^1.16.0",
"node-prismjs": "^0.1.0",
"prismjs": "^1.23.0",
"redis": "^3.0.2",
"rss": "^1.2.2",
"showdown": "^1.9.0"
"showdown": "^1.9.1"
},
"devDependencies": {
"coveralls": "^3.0.4",
"eslint": "^7.23.0",
"eslint-plugin-jest": "^24.3.2",
"jest": "^24.8.0",
"superagent": "^5.1.0",
"supertest": "^4.0.2"
},
"scripts": {
"start": "node src/server.js",
"test": "jest --silent",
"install": "node src/postinstall.js"
"test": "jest --silent -i",
"test-cov": "jest --silent -i --coverage",
"coveralls": "coveralls < coverage/lcov.info",
"test-lint": "eslint .",
"install": "node src/postinstall.js",
"lint": "eslint --fix ."
},
"repository": {
"type": "git",
+1 -1
View File
@@ -5,7 +5,7 @@
<title><%= info.title %> - <%= article.title %></title>
</head>
<body>
<main class="article">
<main class="article" id="top">
<div class="header">
<a class="link-home" href="/">↑</a>
<h1><%= article.title %></h1>
+83 -30
View File
@@ -51,6 +51,16 @@ module.exports = (config) => {
let showError;
const fw = require('./file_walker')(config);
const renderer = require('./renderer')(config);
const hc = require('./hit_counter')(config,
() => {
console.log(cons.ok, 'redis connected');
},
(err) => {
if (err.code !== 'ECONNREFUSED') {
console.log(cons.warn, 'redis error: ' + err);
}
},
);
// set view engine from configuration
app.set('view engine', config['view_engine']);
@@ -65,24 +75,27 @@ module.exports = (config) => {
fw.fetchArticles((err, dict) => {
if (err) {
console.error(cons.error, 'error loading articles : ' + err);
return error ? error() : null;
}
error();
} else {
Object.keys(articles).forEach((key) => delete articles[key]);
Object.keys(dict).forEach((key) => articles[key] = dict[key]);
const nb = Object.keys(articles).length;
const dnb = Object.values(articles).filter(a => a.draft).length;
if (nb > 0)
if (nb > 0) {
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
else
console.log(cons.warn, `no articles loaded, check your configuration`);
} else {
console.log(cons.warn, 'no articles loaded, check your configuration');
}
lastRSS = '';
success();
}
});
};
if (config['test'])
if (config['test']) {
app.reload = reload;
}
render = (req, res, vPath, data, code = 200) => {
data.info = {
@@ -91,7 +104,7 @@ module.exports = (config) => {
host: host,
version: pjson.version,
request: req,
config: config
config: config,
};
res.render(vPath, data, (err, html) => {
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
@@ -100,18 +113,20 @@ module.exports = (config) => {
} else if (err) {
res.sendStatus(500);
console.log(cons.error, `failed to render error page : ${err}`);
} else
} else {
res.status(code).send(html);
}
});
};
showError = (req, res, code) => {
const errorPath = path.join(config['data_dir'], config['home']['error']);
fs.access(errorPath, fs.constants.R_OK, (err) => {
if (err)
if (err) {
res.sendStatus(code);
else
} else {
render(req, res, errorPath, { error: code }, code);
}
});
};
@@ -126,7 +141,7 @@ module.exports = (config) => {
//rate limit for safer server
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: config['rate_limit']
max: config['rate_limit'],
});
app.use(limiter);
@@ -150,15 +165,31 @@ module.exports = (config) => {
app.get('/', (req, res) => {
const homePath = path.join(config['data_dir'], config['home']['index']);
fs.access(homePath, fs.constants.R_OK, (err) => {
if (err)
if (err) {
showError(req, res, 404);
else
} else {
hc.count(req, '/', () => {
render(req, res, homePath,
{
articles: Object.values(articles)
.filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path))
.filter(d => !d.draft)
.sort((a, b) => ('' + b.path).localeCompare(a.path)),
});
});
}
});
});
app.get('/stats', (req, res) => {
if (config['modules']['hit_counter']) {
hc.read('/', (data) => {
res.json({
hits: data.hits,
visitors: data.visitors,
});
});
} else {
showError(req, res, 404);
}
});
//RSS endpoint
@@ -166,10 +197,10 @@ module.exports = (config) => {
if (config['modules']['rss']) {
if (!lastRSS) {
const feed = new Rss({
'title': config['rss']['title'],
'description': config['rss']['description'],
'feed_url': host + req.url,
'site_url': host
title: config['rss']['title'],
description: config['rss']['description'],
feed_url: host + req.url,
site_url: host,
});
Object.values(articles)
.slice(0, config['rss']['length'])
@@ -177,7 +208,7 @@ module.exports = (config) => {
feed.item({
title: article.title,
url: host + article.url,
date: article.date
date: article.date,
});
});
lastRSS = feed.xml();
@@ -191,24 +222,29 @@ module.exports = (config) => {
//webhook endpoint
app.post(config['webhook']['endpoint'], (req, res) => {
if (config['modules']['webhook']) {
let valid = true;
if (config['webhook']['signature_header'] && config['webhook']['secret']) {
const payload = JSON.stringify(req.body) || '';
const hmac = crypto.createHmac('sha1', config['webhook']['secret']);
const digest = 'sha1=' + hmac.update(payload).digest('hex');
const checksum = req.headers[config['webhook']['signature_header']];
if (!checksum || !digest || checksum !== digest) {
return res.sendStatus(403);
res.sendStatus(403);
valid = false;
}
}
if (valid) {
cp.exec(config['webhook']['pull_command'], { cwd: path.join(__dirname, '..', config['data_dir']) }, (err) => {
if (err) {
console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`);
return res.sendStatus(500);
}
res.sendStatus(500);
} else {
reload(() => {
res.sendStatus(200);
});
}
});
}
} else {
res.sendStatus(400);
}
@@ -216,32 +252,48 @@ module.exports = (config) => {
//rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png
app.use((req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url))
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url)) {
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
}
next();
});
// catch all article urls and render them
app.get('*', (req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) {
if (/^\/\d{4}\/\d{2}\/\d{2}\/(stats)?$/.test(req.path)) {
const articlePath = req.path.substr(1, 10);
const article = articles[articlePath];
if (!article)
if (!article) {
showError(req, res, 404);
else {
} else if (req.path.endsWith('stats')) {
if (config['modules']['hit_counter']) {
hc.read(articlePath, (data) => {
res.json({
hits: data.hits,
visitors: data.visitors,
});
});
} else {
showError(req, res, 404);
}
} else {
hc.count(req, articlePath, () => {
renderer.render(article.realPath, (err, html) => {
if (err) {
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
return showError(req, res, 500);
}
showError(req, res, 500);
} else {
article.content = html;
const templatePath = path.join(config['data_dir'], config['article']['template']);
fs.access(templatePath, fs.constants.R_OK, (err) => {
if (err) {
console.log(cons.error, `no template found at ${templatePath}`);
showError(req, res, 500);
} else
} else {
render(req, res, templatePath, { article: article });
}
});
}
});
});
}
@@ -272,8 +324,9 @@ module.exports = (config) => {
//log all server errors
app.use((err, req, res, next) => {
console.log(cons.error, `error when handling ${req.method} ${req.path} request : ${err}`);
if (!config['error_log'])
if (!config['error_log']) {
next(err);
}
fs.appendFile(config['error_log'],
`500 ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n${err.stack}\n`,
{ encoding: 'UTF-8' }, () => {
+9 -1
View File
@@ -12,7 +12,8 @@
"prism": true,
"mathjax": true,
"plantuml": true,
"fa-diagrams": true
"fa-diagrams": true,
"hit_counter": true
},
"home": {
"title": "GitBlog.md",
@@ -58,5 +59,12 @@
},
"plantuml": {
"output_format": "svg"
},
"hit_counter": {
"unique_visitor_timeout": 7200000
},
"redis": {
"host": "localhost",
"port": 6379
}
}
+35 -20
View File
@@ -12,25 +12,31 @@ const getFileTree = (dir, cb) => {
let list = [];
let remaining = 0;
fs.readdir(dir, { withFileTypes: true }, (err, items) => {
if (err)
return cb(err);
if (err) {
cb(err);
} else {
items.forEach((item) => {
if (item.isDirectory()) {
remaining++;
getFileTree(path.join(dir, item.name), (err, out) => {
if (err)
return cb(err);
if (err) {
cb(err);
} else {
list.push(...out);
remaining--;
if (remaining === 0)
if (remaining === 0) {
cb(null, list);
}
}
});
} else {
list.push(path.join(dir, item.name));
}
});
if (remaining === 0)
if (remaining === 0) {
cb(null, list);
}
}
});
};
@@ -42,9 +48,9 @@ const getFileTree = (dir, cb) => {
*/
const readIndexFile = (path, thumbnailTag, cb) => {
fs.readFile(path, { encoding: 'UTF-8' }, (err, data) => {
if (err)
return cb(err);
if (err) {
cb(err);
} else {
let info = {};
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
@@ -55,6 +61,7 @@ const readIndexFile = (path, thumbnailTag, cb) => {
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
cb(null, info);
}
});
};
@@ -68,14 +75,16 @@ module.exports = (config) => {
*/
fetchArticles: (cb) => {
getFileTree(config['data_dir'], (err, fileList) => {
if (err)
return cb(err);
if (err) {
cb(err);
} else {
const paths = fileList
.map((p) => p.substr(config['data_dir'].length + 1).split(path.sep))
.filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) &&
/^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2]));
if (paths.length === 0)
if (paths.length === 0) {
cb(null, {});
}
const articles = {};
let remaining = 0;
paths.forEach((p) => {
@@ -85,27 +94,33 @@ module.exports = (config) => {
realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]),
year: parseInt(p[0]),
month: parseInt(p[1]),
day: parseInt(p[2])
day: parseInt(p[2]),
};
article.date = new Date(article.year, article.month, article.day);
article.date.setUTCHours(0);
remaining++;
readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => {
if (err)
return cb(err);
if (err) {
cb(err);
} else {
article.title = info.title || config['article']['default_title'];
article.thumbnail = info.thumbnail ? joinUrl(article.path, info.thumbnail) : config['article']['default_thumbnail'];
article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_');
article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ')
.trim()
.replace(/ /gm, '_');
article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
if (!articles[article.path] || !article.draft)
if (!articles[article.path] || !article.draft) {
articles[article.path] = article;
}
remaining--;
if (remaining === 0)
if (remaining === 0) {
cb(null, articles);
}
}
});
});
});
}
});
},
};
};
+48
View File
@@ -0,0 +1,48 @@
const redis = require('redis');
module.exports = (config, onConnect, onError) => {
const client = config['modules']['hit_counter'] ? redis.createClient(config['redis']) : { connected: false, on: () => { /* ignore */ } };
client.on('connect', onConnect);
client.on('error', onError);
const visitors = {};
const count = (req, path, cb) => {
if (!client.connected) {
cb();
} else {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const key = path + ':' + ip;
const now = Date.now();
const isNewVisitor = (now - (visitors[key] || 0)) > config['hit_counter']['unique_visitor_timeout'];
visitors[key] = now;
client
.multi()
.hincrby(path, 'h', 1)
.hincrby(path, 'v', isNewVisitor ? 1 : 0)
.exec(cb);
}
};
const read = (path, cb) => {
if (!client.connected) {
cb({
hits: 0,
visitors: 0,
});
} else {
client.hgetall(path, (_, value) => {
cb({
hits: value ? value.h || 0 : 0,
visitors: value ? value.v || 0 : 0,
});
});
}
};
return {
count: count,
read: read,
};
};
+5 -3
View File
@@ -4,10 +4,11 @@ const ncp = require('ncp').ncp;
const copy = (src, dest) => {
ncp(src, dest, function (err) {
if (err)
if (err) {
console.error(err);
else
} else {
console.log(`copied ${src} to ${dest}`);
}
});
};
@@ -23,8 +24,9 @@ if (!fs.existsSync('data')) {
const datetime = new Date();
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
if (!fs.existsSync(dir))
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
copy(path.join('sample_data', 'article'), dir);
}
+57 -27
View File
@@ -21,11 +21,12 @@ module.exports = (config) => {
});
i += match.index + match[0].length;
}
if (i < data.length)
if (i < data.length) {
parts.push({
index: i,
text: data.slice(i, data.length),
});
}
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
@@ -40,11 +41,12 @@ module.exports = (config) => {
});
i += match.index + match[0].length;
}
if (i < p.text.length)
if (i < p.text.length) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, p.text.length),
});
}
parts.splice(pi, 1, ...subParts);
});
@@ -59,12 +61,14 @@ module.exports = (config) => {
};
let Prism;
if (config['modules']['prism'])
if (config['modules']['prism']) {
Prism = require('node-prismjs');
}
const renderPrism = (data, cb) => {
if (!config['modules']['prism'])
return cb(data);
if (!config['modules']['prism']) {
cb(data);
} else {
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
let match;
while ((match = codeRegex.exec(data))) {
@@ -74,6 +78,7 @@ module.exports = (config) => {
data = data.slice(0, match.index) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
}
cb(data);
}
};
if (config['modules']['plantuml']) {
@@ -81,22 +86,25 @@ module.exports = (config) => {
}
const renderPlantUML = (data, cb) => {
if (!config['modules']['plantuml'])
return cb(data);
/* global encode64 */
if (!config['modules']['plantuml']) {
cb(data);
} else {
const parts = getParts(data);
const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m;
let match;
parts.forEach(part => {
while ((match = umlRegex.exec(part.text))) {
const code = match[1].trim();
const s = unescape(encodeURIComponent(code)); // jshint ignore:line
const s = unescape(encodeURIComponent(code));
const compressed = global['zip_deflate'](s);
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;// jshint ignore:line
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;
part.text = part.text.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
}
};
let mjAPI;
@@ -105,17 +113,27 @@ module.exports = (config) => {
mjAPI.config({
MathJax: {
tex2jax: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
}
}
inlineMath: [
[
'$',
'$',
],
],
displayMath: [
[
'$$',
'$$',
],
],
},
},
});
}
const renderMathJax = (data, cb) => {
if (!config['modules']['mathjax'])
return cb(data);
if (!config['modules']['mathjax']) {
cb(data);
} else {
const parts = getParts(data);
const doMJ = (match, format, i) => {
@@ -124,7 +142,7 @@ module.exports = (config) => {
const mjConf = {
math: eq,
format: format,
speakText: config['mathjax']['speak_text']
speakText: config['mathjax']['speak_text'],
};
mjConf[output] = true;
mjAPI.typeset(mjConf, (res) => {
@@ -138,15 +156,23 @@ module.exports = (config) => {
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const inlineEqRegex = /\$([^$\n]*)\$/;
let found = false;
for (let i = 0; i < parts.length; i++) {
let match;
if ((match = eqRegex.exec(parts[i].text))) {
return doMJ(match, 'TeX', i);
doMJ(match, 'TeX', i);
found = true;
break;
} else if ((match = inlineEqRegex.exec(parts[i].text))) {
return doMJ(match, 'inline-TeX', i);
doMJ(match, 'inline-TeX', i);
found = true;
break;
}
}
if (!found) {
cb(data);
}
}
};
let faDiagrams;
@@ -157,8 +183,9 @@ module.exports = (config) => {
}
const renderFaDiagrams = (data, cb) => {
if (!config['modules']['fa-diagrams'])
return cb(data);
if (!config['modules']['fa-diagrams']) {
cb(data);
} else {
const parts = getParts(data);
const diagramsRegex = /@startfad\r?\n((?:(?!@endfad)[\s\S])*)\r?\n@endfad/m;
let match;
@@ -170,10 +197,11 @@ module.exports = (config) => {
const diagData = toml.parse(code);
const findLineBreaks = (data) => {
Object.keys(data).forEach(key => {
if (typeof data[key] === 'object')
if (typeof data[key] === 'object') {
findLineBreaks(data[key]);
else if (typeof data[key] === 'string')
} else if (typeof data[key] === 'string') {
data[key] = data[key].replace(/\\n/gm, '\n');
}
});
};
findLineBreaks(diagData);
@@ -186,6 +214,7 @@ module.exports = (config) => {
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
}
};
return {
@@ -197,9 +226,9 @@ module.exports = (config) => {
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
render: (file, cb) => {
fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => {
if (err)
return cb(err);
if (err) {
cb(err);
} else {
renderPlantUML(data, (data) => {
renderFaDiagrams(data, (data) => {
renderMathJax(data, (data) => {
@@ -211,8 +240,9 @@ module.exports = (config) => {
});
});
});
});
}
});
},
};
};
+164 -70
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const request = require('supertest');
const fs = require('fs');
const path = require('path');
@@ -18,6 +17,7 @@ config['rss']['endpoint'] = '/rsstest';
config['rss']['length'] = 2;
config['home']['error'] = testError;
config['article']['template'] = testTemplate;
config['modules']['hit_counter'] = false;
const app = require('../src/app')(config);
@@ -29,6 +29,7 @@ beforeEach((done, fail) => {
config['error_log'] = '';
config['modules']['rss'] = true;
config['modules']['webhook'] = true;
config['modules']['hit_counter'] = false;
utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir);
@@ -49,15 +50,17 @@ describe('Test reload', () => {
});
describe('Test request logging', () => {
test('test no log', (done) => {
request(app).get('/rsstest').then(() => {
test('no log', (done) => {
request(app).get('/rsstest')
.then(() => {
expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false);
done();
});
});
test('test get 200', (done) => {
test('get 200', (done) => {
config['access_log'] = path.join(dataDir, 'access.log');
request(app).get('/rsstest').then(() => {
request(app).get('/rsstest')
.then(() => {
fs.readFile(path.join(dataDir, 'access.log'), { encoding: 'UTF-8' }, (err, data) => {
expect(err).toBeNull();
expect(data).toBe('200 GET /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
@@ -65,9 +68,10 @@ describe('Test request logging', () => {
});
});
});
test('test post 400', (done) => {
test('post 400', (done) => {
config['access_log'] = path.join(dataDir, 'access.log');
request(app).post('/rsstest').then(() => {
request(app).post('/rsstest')
.then(() => {
fs.readFile(path.join(dataDir, 'access.log'), { encoding: 'UTF-8' }, (err, data) => {
expect(err).toBeNull();
expect(data).toBe('400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
@@ -75,10 +79,12 @@ describe('Test request logging', () => {
});
});
});
test('test 2 requests', (done) => {
test('2 requests', (done) => {
config['access_log'] = path.join(dataDir, 'access.log');
request(app).get('/rss').then(() => {
request(app).post('/rsstest').then(() => {
request(app).get('/rss')
.then(() => {
request(app).post('/rsstest')
.then(() => {
fs.readFile(path.join(dataDir, 'access.log'), { encoding: 'UTF-8' }, (err, data) => {
expect(err).toBeNull();
expect(data).toBe('404 GET /rss ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n' +
@@ -91,22 +97,25 @@ describe('Test request logging', () => {
});
describe('Test error logging', () => {
test('test no log', (done) => {
test('no log', (done) => {
config['home']['index'] = null;
request(app).get('/').then(() => {
request(app).get('/')
.then(() => {
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
done();
});
});
test('test null error ', (done) => {
test('null error', (done) => {
config['home']['index'] = null;
config['error_log'] = path.join(dataDir, 'error.log');
request(app).get('/').then(() => {
request(app).get('/')
.then(() => {
fs.readFile(path.join(dataDir, 'error.log'), { encoding: 'UTF-8' }, (err, data) => {
expect(err).toBeNull();
const start = data.split('\n').slice(0, 2).join('\n');
const expected = '500 GET / ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type object';
expect(start).toBe(expected);
const start = data.split('\n').slice(0, 2)
.join('\n');
const expected = '500 GET / ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string.';
expect(start.indexOf(expected)).toBe(0);
done();
});
});
@@ -115,14 +124,16 @@ describe('Test error logging', () => {
describe('Test root path', () => {
test('404 no index no error', (done) => {
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
test('404 no index but error page', (done) => {
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(404);
expect(response.text).toBe('error 404');
done();
@@ -130,7 +141,8 @@ describe('Test root path', () => {
});
test('500 render error', (done) => {
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(500);
done();
});
@@ -138,7 +150,8 @@ describe('Test root path', () => {
test('500 render error with page', (done) => {
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(500);
expect(response.text).toBe('error 500');
done();
@@ -147,14 +160,16 @@ describe('Test root path', () => {
test('500 render error with failing page', (done) => {
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
fs.writeFileSync(path.join(dataDir, testError), 'error <%= null.error %>');
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(500);
done();
});
});
test('200 no articles', (done) => {
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('articles 0');
done();
@@ -164,7 +179,7 @@ describe('Test root path', () => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '05', '05'),
path.join(dataDir, '2017', '05', '05')
path.join(dataDir, '2017', '05', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'draft.md'),
@@ -174,25 +189,44 @@ describe('Test root path', () => {
]);
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
app.reload(() => {
request(app).get('/').then((response) => {
request(app).get('/')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('articles 2');
done();
});
}, fail);
});
test('404 index no stats', (done) => {
request(app).get('/stats')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
test('200 index stats', (done) => {
config['modules']['hit_counter'] = true;
request(app).get('/stats')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ hits: 0, visitors: 0 });
done();
});
});
});
describe('Test RSS feed', () => {
test('404 rss deactivated', (done) => {
config['modules']['rss'] = false;
request(app).get('/rsstest').then((response) => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
test('200 empty rss', (done) => {
request(app).get('/rsstest').then((response) => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.type).toBe('application/rss+xml');
expect(response.text.length).toBeGreaterThan(0);
@@ -201,15 +235,19 @@ describe('Test RSS feed', () => {
});
});
test('200 Mozilla fix', (done) => {
request(app).get('/rsstest').set('user-agent', 'Mozilla Firefox 64.0').then((response) => {
request(app).get('/rsstest')
.set('user-agent', 'Mozilla Firefox 64.0')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.type).toBe('text/xml');
done();
});
});
test('200 rss cache', (done) => {
request(app).get('/rsstest').then(() => {
request(app).get('/rsstest').then((response) => {
request(app).get('/rsstest')
.then(() => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(1);
@@ -220,14 +258,15 @@ describe('Test RSS feed', () => {
test('200 2 rss items', (done, fail) => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '05', '05')
path.join(dataDir, '2018', '05', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, '2018', '05', '05', 'index.md')
path.join(dataDir, '2018', '05', '05', 'index.md'),
]);
app.reload(() => {
request(app).get('/rsstest').then((response) => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(3);
@@ -239,15 +278,16 @@ describe('Test RSS feed', () => {
utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '05', '05'),
path.join(dataDir, '2017', '05', '05')
path.join(dataDir, '2017', '05', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, '2018', '05', '05', 'index.md'),
path.join(dataDir, '2017', '05', '05', 'index.md')
path.join(dataDir, '2017', '05', '05', 'index.md'),
]);
app.reload(() => {
request(app).get('/rsstest').then((response) => {
request(app).get('/rsstest')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text.length).toBeGreaterThan(0);
expect(response.text.split('<item>').length).toBe(3);
@@ -260,34 +300,38 @@ describe('Test RSS feed', () => {
describe('Test webhook', () => {
test('400 webhook deactivated', (done) => {
config['modules']['webhook'] = false;
request(app).post('/webhooktest').then((response) => {
request(app).post('/webhooktest')
.then((response) => {
expect(response.statusCode).toBe(400);
done();
});
});
test('200 no secret', (done) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, testTemplate)
path.join(dataDir, testTemplate),
]);
config['webhook']['pull_command'] = 'git --help';
request(app).post('/webhooktest').then((response) => {
request(app).post('/webhooktest')
.then((response) => {
expect(response.statusCode).toBe(200);
request(app).get('/2019/05/05/').then((response) => {
request(app).get('/2019/05/05/')
.then((response) => {
expect(response.statusCode).toBe(200);
done();
});
});
});
test('500 command failed', (done) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, testTemplate)
path.join(dataDir, testTemplate),
]);
config['webhook']['pull_command'] = 'qzgfqgqz';
request(app).post('/webhooktest').then((response) => {
request(app).post('/webhooktest')
.then((response) => {
expect(response.statusCode).toBe(500);
done();
});
@@ -295,7 +339,9 @@ describe('Test webhook', () => {
test('403 wrong secret', (done) => {
config['webhook']['signature_header'] = 'testheader';
config['webhook']['secret'] = 'testvalue';
request(app).post('/webhooktest').set('testheader', 'sha1=invalid').then((response) => {
request(app).post('/webhooktest')
.set('testheader', 'sha1=invalid')
.then((response) => {
expect(response.statusCode).toBe(403);
done();
});
@@ -316,18 +362,20 @@ describe('Test webhook', () => {
describe('Test articles rendering', () => {
test('404 article not found', (done) => {
request(app).get('/2019/05/06/untitled/').then((response) => {
request(app).get('/2019/05/06/untitled/')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
test('500 fail to render', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- articl.content %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
request(app).get('/2019/05/05/hello/')
.then((response) => {
expect(response.statusCode).toBe(500);
done();
});
@@ -335,10 +383,11 @@ describe('Test articles rendering', () => {
});
test('500 no template', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
request(app).get('/2019/05/05/hello/')
.then((response) => {
expect(response.statusCode).toBe(500);
done();
});
@@ -346,11 +395,12 @@ describe('Test articles rendering', () => {
});
test('200 rendered article', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
request(app).get('/2019/05/05/hello/')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
@@ -359,11 +409,12 @@ describe('Test articles rendering', () => {
});
test('200 rendered draft', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'draft.md'), '# Hello');
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
request(app).get('/2019/05/05/hello/')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
@@ -372,13 +423,14 @@ describe('Test articles rendering', () => {
});
test('200 other url', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, testTemplate)
path.join(dataDir, testTemplate),
]);
app.reload(() => {
request(app).get('/2019/05/05/anything/').then((response) => {
request(app).get('/2019/05/05/anything/')
.then((response) => {
expect(response.statusCode).toBe(200);
done();
});
@@ -386,31 +438,66 @@ describe('Test articles rendering', () => {
});
test('200 other url 2', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, testTemplate)
path.join(dataDir, testTemplate),
]);
app.reload(() => {
request(app).get('/2019/05/05/').then((response) => {
request(app).get('/2019/05/05/')
.then((response) => {
expect(response.statusCode).toBe(200);
done();
});
}, fail);
});
test('404 article no stats', (done) => {
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, testTemplate),
]);
app.reload(() => {
request(app).get('/2019/05/05/hello/stats')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
});
test('200 index stats', (done) => {
config['modules']['hit_counter'] = true;
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'),
path.join(dataDir, testTemplate),
]);
app.reload(() => {
request(app).get('/2019/05/05/anything/stats')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ hits: 0, visitors: 0 });
done();
});
});
});
});
describe('Test static files', () => {
test('404 invalid file no error page', (done) => {
request(app).get('/somefile.txt').then((response) => {
request(app).get('/somefile.txt')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
test('404 invalid file but error page', (done) => {
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
request(app).get('/somefile.txt').then((response) => {
request(app).get('/somefile.txt')
.then((response) => {
expect(response.statusCode).toBe(404);
expect(response.text).toBe('error 404');
done();
@@ -419,7 +506,8 @@ describe('Test static files', () => {
test('404 hidden file', (done) => {
utils.createEmptyDirs([ path.join(dataDir, 'tmp') ]);
fs.writeFileSync(path.join(dataDir, 'tmp', 'somefile.ejs'), '');
request(app).get('/tmp/somefile.ejs').then((response) => {
request(app).get('/tmp/somefile.ejs')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
@@ -427,14 +515,16 @@ describe('Test static files', () => {
test('404 hidden folder', (done) => {
utils.createEmptyDirs([ path.join(dataDir, '.git') ]);
fs.writeFileSync(path.join(dataDir, '.git', 'file.txt'), '');
request(app).get('/.git/file.txt').then((response) => {
request(app).get('/.git/file.txt')
.then((response) => {
expect(response.statusCode).toBe(404);
done();
});
});
test('200 valid file', (done) => {
fs.writeFileSync(path.join(dataDir, 'somefile.css'), 'filecontent');
request(app).get('/somefile.css').then((response) => {
request(app).get('/somefile.css')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.type).toBe('text/css');
expect(response.text).toBe('filecontent');
@@ -442,9 +532,10 @@ describe('Test static files', () => {
});
});
test('200 valid resource of article', (done) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'somefile.txt'), 'filecontent');
request(app).get('/2019/05/05/title/somefile.txt').then((response) => {
request(app).get('/2019/05/05/title/somefile.txt')
.then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('filecontent');
done();
@@ -454,19 +545,22 @@ describe('Test static files', () => {
describe('Test other requests', () => {
test('400 POST', (done) => {
request(app).post('/').then((response) => {
request(app).post('/')
.then((response) => {
expect(response.statusCode).toBe(400);
done();
});
});
test('400 PUT', (done) => {
request(app).put('/').then((response) => {
request(app).put('/')
.then((response) => {
expect(response.statusCode).toBe(400);
done();
});
});
test('400 DELETE', (done) => {
request(app).delete('/').then((response) => {
request(app).delete('/')
.then((response) => {
expect(response.statusCode).toBe(400);
done();
});
+12 -6
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
@@ -9,7 +8,6 @@ beforeAll(() => {
if (fs.existsSync(configFile)) {
fs.renameSync(configFile, tmpConfigFile);
}
expect(fs.existsSync(configFile)).toBeFalsy();
});
afterAll(() => {
@@ -21,8 +19,9 @@ afterAll(() => {
});
test('no config', () => {
if (fs.existsSync(configFile))
if (fs.existsSync(configFile)) {
fs.unlinkSync(configFile);
}
expect(fs.existsSync(configFile)).toBeFalsy();
const config = require('../src/config')();
expect(config).toBeDefined();
@@ -31,8 +30,9 @@ test('no config', () => {
});
test('example config', () => {
if (fs.existsSync(configFile))
if (fs.existsSync(configFile)) {
fs.unlinkSync(configFile);
}
fs.copyFileSync(path.join('src', 'config.default.json'), configFile);
const data = fs.readFileSync(configFile, { encoding: 'UTF-8' });
fs.writeFileSync(configFile, data.replace('3000', '3333'), { encoding: 'UTF-8' });
@@ -70,12 +70,18 @@ test('array parsing', () => {
fs.writeFileSync(configFile, '{"home":{"hidden":["item1","item2"]}}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['home']['hidden']).toEqual(['item1', 'item2']);
expect(config['home']['hidden']).toEqual([
'item1',
'item2',
]);
});
test('array fix', () => {
fs.writeFileSync(configFile, '{"home":{"hidden":{}}}');
const config = require('../src/config')();
expect(config).toBeDefined();
expect(config['home']['hidden']).toEqual(['*.ejs', '/.git*']);
expect(config['home']['hidden']).toEqual([
'*.ejs',
'/.git*',
]);
});
+24 -22
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
const utils = require('./test_utils');
@@ -9,15 +8,15 @@ const testIndex = 'testindex.md';
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
const config = {
'test': true,
'data_dir': dataDir,
'article': {
'index': testIndex,
'draft': 'draft.md',
'default_title': 'Untitled',
'default_thumbnail': 'default.png',
'thumbnail_tag': 'thumbnail'
}
test: true,
data_dir: dataDir,
article: {
index: testIndex,
draft: 'draft.md',
default_title: 'Untitled',
default_thumbnail: 'default.png',
thumbnail_tag: 'thumbnail',
},
};
const fw = require('../src/file_walker')(config);
@@ -47,7 +46,7 @@ describe('Test function fileTree', () => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, 'test', 'test2'),
path.join(dataDir, 'test2')
path.join(dataDir, 'test2'),
]);
fw.fileTree(dataDir, (err, list) => {
expect(err).toBeNull();
@@ -59,7 +58,7 @@ describe('Test function fileTree', () => {
test('simple files', (done) => {
const fileList = [
path.join(dataDir, 'f1.txt'),
path.join(dataDir, 'f2.txt')
path.join(dataDir, 'f2.txt'),
];
utils.createEmptyFiles(fileList);
fw.fileTree(dataDir, (err, list) => {
@@ -73,13 +72,13 @@ describe('Test function fileTree', () => {
test('nested files', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, 'test2')
path.join(dataDir, 'test2'),
]);
const fileList = [
path.join(dataDir, 'f1.txt'),
path.join(dataDir, 'test', 'f2.txt'),
path.join(dataDir, 'test', 'test', 'f3.txt'),
path.join(dataDir, 'test2', 'f4.txt')
path.join(dataDir, 'test2', 'f4.txt'),
];
utils.createEmptyFiles(fileList);
fw.fileTree(dataDir, (err, list) => {
@@ -121,7 +120,7 @@ describe('Test index article reading', () => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: './thumbnail.jpg'
thumbnail: './thumbnail.jpg',
});
done();
});
@@ -137,7 +136,7 @@ describe('Test index article reading', () => {
expect(err).toBeNull();
expect(info).toEqual({
title: undefined,
thumbnail: './thumbnail.jpg'
thumbnail: './thumbnail.jpg',
});
done();
});
@@ -149,7 +148,7 @@ describe('Test index article reading', () => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'title',
thumbnail: undefined
thumbnail: undefined,
});
done();
});
@@ -165,7 +164,7 @@ describe('Test index article reading', () => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: undefined
thumbnail: undefined,
});
done();
});
@@ -182,7 +181,7 @@ describe('Test index article reading', () => {
expect(err).toBeNull();
expect(info).toEqual({
title: 'This is an awesome title !?¤',
thumbnail: './thumbnail.jpg'
thumbnail: './thumbnail.jpg',
});
done();
});
@@ -209,12 +208,12 @@ describe('Test article fetching', () => {
test('misplaced index file', (done) => {
utils.createEmptyDirs([
path.join(dataDir, 'test', 'test'),
path.join(dataDir, '2019', '05', '05')
path.join(dataDir, '2019', '05', '05'),
]);
utils.createEmptyFiles([
path.join(dataDir, testIndex),
path.join(dataDir, 'test', 'test', testIndex),
path.join(dataDir, '2019', '05', testIndex)
path.join(dataDir, '2019', '05', testIndex),
]);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
@@ -317,7 +316,10 @@ describe('Test article fetching', () => {
const file = path.join(dir, testIndex);
const file2 = path.join(dir, 'draft.md');
utils.createEmptyDirs([ dir ]);
utils.createEmptyFiles([file, file2]);
utils.createEmptyFiles([
file,
file2,
]);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
+229
View File
@@ -0,0 +1,229 @@
const mockClient = {
options: {},
connected: true,
on: () => { /* ignore */ },
};
jest.mock('redis', () => {
return {
createClient: (options) => {
mockClient.options = options;
return mockClient;
},
};
});
const config = {
test: true,
modules: {
hit_counter: true,
},
redis: {
host: 'test-host',
port: 'test-port',
},
hit_counter: {
unique_visitor_timeout: -1,
},
};
const hc = require('../src/hit_counter')(config, () => { /* ignore */ }, () => { /* ignore */ });
afterAll(() => {
jest.resetModules();
});
test('options passed to redis', () => {
expect(mockClient.options).toEqual(config['redis']);
});
describe('read()', () => {
beforeEach(() => {
mockClient.hgetall = (_, cb) => {
cb();
};
});
test('read path', (done) => {
mockClient.hgetall = (path, cb) => {
expect(path).toBe('/test/path/');
cb(undefined, { h: 12, v: 34 });
};
hc.read('/test/path/', (data) => {
expect(data).toBeDefined();
expect(data.hits).toBe(12);
expect(data.visitors).toBe(34);
done();
});
});
test('read path with error', (done) => {
mockClient.hgetall = (path, cb) => {
expect(path).toBe('/test/path/');
cb('error', undefined);
};
hc.read('/test/path/', (data) => {
expect(data).toBeDefined();
expect(data.hits).toBe(0);
expect(data.visitors).toBe(0);
done();
});
});
test('read path with error 2', (done) => {
mockClient.hgetall = (path, cb) => {
expect(path).toBe('/test/path/');
cb(undefined, {});
};
hc.read('/test/path/', (data) => {
expect(data).toBeDefined();
expect(data.hits).toBe(0);
expect(data.visitors).toBe(0);
done();
});
});
});
describe('count()', () => {
beforeEach(() => {
mockClient.multi = () => mockClient;
mockClient.hincrby = () => mockClient;
mockClient.exec = (cb) => {
cb();
};
config['hit_counter']['unique_visitor_timeout'] = -1;
});
test('simple visit', (done) => {
let multiCalled = false;
let execCalled = false;
let hincrbyCalls = [];
mockClient.multi = () => {
multiCalled = true;
return mockClient;
};
mockClient.hincrby = (hash, key, value) => {
hincrbyCalls.push([
hash,
key,
value,
]);
return mockClient;
};
mockClient.exec = (cb) => {
execCalled = true;
cb();
};
hc.count({
headers: {},
connection: { remoteAddress: 'test1' },
}, '/test/path/1', () => {
expect(multiCalled).toBeTruthy();
expect(hincrbyCalls).toEqual([
[
'/test/path/1',
'h',
1,
],
[
'/test/path/1',
'v',
1,
],
]);
expect(execCalled).toBeTruthy();
done();
});
});
test('re-visit after long time', (done) => {
let hincrbyCalls = [];
mockClient.hincrby = (hash, key, value) => {
hincrbyCalls.push([
hash,
key,
value,
]);
return mockClient;
};
hc.count({
headers: {},
connection: { remoteAddress: 'test2' },
}, '/test/path/2', () => {
hc.count({
headers: {},
connection: { remoteAddress: 'test2' },
}, '/test/path/2', () => {
expect(hincrbyCalls).toEqual([
[
'/test/path/2',
'h',
1,
],
[
'/test/path/2',
'v',
1,
],
[
'/test/path/2',
'h',
1,
],
[
'/test/path/2',
'v',
1,
],
]);
done();
});
});
});
test('re-visit after short time', (done) => {
config['hit_counter']['unique_visitor_timeout'] = 10000;
let hincrbyCalls = [];
mockClient.hincrby = (hash, key, value) => {
hincrbyCalls.push([
hash,
key,
value,
]);
return mockClient;
};
hc.count({
headers: {},
connection: { remoteAddress: 'test3' },
}, '/test/path/3', () => {
hc.count({
headers: {},
connection: { remoteAddress: 'test3' },
}, '/test/path/3', () => {
expect(hincrbyCalls).toEqual([
[
'/test/path/3',
'h',
1,
],
[
'/test/path/3',
'v',
1,
],
[
'/test/path/3',
'h',
1,
],
[
'/test/path/3',
'v',
0,
],
]);
done();
});
});
});
});
+33 -36
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
const utils = require('./test_utils');
@@ -7,24 +6,24 @@ const dataDir = 'test_data';
const file = path.join(dataDir, 'test.md');
const config = {
'test': true,
'modules': {
test: true,
modules: {
'prism': true,
'mathjax': true,
'plantuml': true,
'fa-diagrams': true,
},
'showdown': {
'simplifiedAutoLink': true,
'smartIndentationFix': true
showdown: {
simplifiedAutoLink: true,
smartIndentationFix: true,
},
'mathjax': {
'output_format': 'html',
'speak_text': false
mathjax: {
output_format: 'html',
speak_text: false,
},
plantuml: {
output_format: 'svg',
},
'plantuml': {
'output_format': 'svg'
}
};
const renderer = require('../src/renderer')(config);
@@ -48,9 +47,7 @@ describe('get parts', () => {
test('normal', () => {
const data = 'Hello\nthere\ngeneral\nkenobi';
const parts = renderer.getParts(data);
expect(parts.map(p => p.text)).toEqual([
'Hello\nthere\ngeneral\nkenobi'
]);
expect(parts.map(p => p.text)).toEqual([ 'Hello\nthere\ngeneral\nkenobi' ]);
});
test('lot of stuff', () => {
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
@@ -59,27 +56,27 @@ describe('get parts', () => {
{
index: 0,
end: 12,
text: 'Hello\nthere\n'
text: 'Hello\nthere\n',
},
{
index: 22,
end: 30,
text: '\ngeneral'
text: '\ngeneral',
},
{
index: 53,
end: 54,
text: '\n'
text: '\n',
},
{
index: 78,
end: 79,
text: '\n'
text: '\n',
},
{
index: 109,
end: 115,
text: 'kenobi'
text: 'kenobi',
},
]);
});
@@ -181,9 +178,9 @@ describe('Test MathJax', () => {
});
test('full eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$', (data) => {
expect(data).toBe('<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
expect(data).toBe('<span class="mjx-chtml MJXc-display" style="text-align: center;">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.519em; padding-bottom: 0.298em;">' +
'A' +
'</span></span></span></span></span>');
done();
@@ -192,9 +189,9 @@ describe('Test MathJax', () => {
test('inline eq', (done) => {
renderer.renderMathJax('start $a$ end', (data) => {
expect(data).toBe('start ' +
'<span class=\"mjx-chtml\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.225em; padding-bottom: 0.298em;\">' +
'<span class="mjx-chtml">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.225em; padding-bottom: 0.298em;">' +
'a' +
'</span></span></span></span></span>' +
' end');
@@ -216,21 +213,21 @@ describe('Test MathJax', () => {
test('multiple eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$\nstart $a$ end\n$$\n\nA\n\n$$', (data) => {
expect(data).toBe('' +
'<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
'<span class="mjx-chtml MJXc-display" style="text-align: center;">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.519em; padding-bottom: 0.298em;">' +
'A' +
'</span></span></span></span></span>\n' +
'start ' +
'<span class=\"mjx-chtml\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.225em; padding-bottom: 0.298em;\">' +
'<span class="mjx-chtml">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.225em; padding-bottom: 0.298em;">' +
'a' +
'</span></span></span></span></span>' +
' end\n' +
'<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
'<span class="mjx-chtml MJXc-display" style="text-align: center;">' +
'<span class="mjx-math"><span class="mjx-mrow"><span class="mjx-mi">' +
'<span class="mjx-char MJXc-TeX-math-I" style="padding-top: 0.519em; padding-bottom: 0.298em;">' +
'A' +
'</span></span></span></span></span>');
done();
@@ -279,7 +276,7 @@ describe('Test render', () => {
});
test('normal file', (done) => {
fs.writeFileSync(file, `# Hello`);
fs.writeFileSync(file, '# Hello');
renderer.render(file, (err, html) => {
expect(err).toBeNull();
expect(html).toBe('<h1 id="hello">Hello</h1>');
-1
View File
@@ -1,4 +1,3 @@
/* jshint -W117 */
const fs = require('fs');
const path = require('path');
const utils = require('./test_utils');
+5 -3
View File
@@ -2,14 +2,16 @@ const fs = require('fs');
const path = require('path');
const deleteFolderSync = (dir) => {
if (!fs.existsSync(dir))
if (!fs.existsSync(dir)) {
return;
}
let items;
const deleteItem = (item) => {
if (item.isDirectory())
if (item.isDirectory()) {
deleteFolderSync(path.join(dir, item.name));
else
} else {
fs.unlinkSync(path.join(dir, item.name));
}
};
do {
items = fs.readdirSync(dir, { withFileTypes: true });