Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9baed03f0f | |||
| ab23e4aa3c | |||
| ab573f91ee | |||
| 8a9b9cdcfe | |||
| e56867a269 | |||
| 8e795c6371 | |||
| c3e53c7df8 | |||
| 404b02830d | |||
| 2fe9a8fecd | |||
| 078f3d7416 | |||
| d69e10202c | |||
| 140e472e29 | |||
| 823d97f4bb | |||
| f7167a85a8 | |||
| 4a3b8267ec | |||
| d0bebcba87 | |||
| 56d7993116 | |||
| 99a19edb93 | |||
| da900d2d02 | |||
| 51c1afb4d6 | |||
| dd088a04a3 | |||
| 6439f8eb92 | |||
| 88cb7ce30f | |||
| b0ba52e140 | |||
| fa5cf31983 | |||
| 18b02cf267 | |||
| d194fbf032 | |||
| e9d67985a6 | |||
| f6d6f04d59 | |||
| 5e817cc296 | |||
| 0f5e40c9ff | |||
| ec79c88bdf | |||
| 604d291c37 | |||
| 0896256e20 | |||
| 250b8a8625 | |||
| 4f0d37f959 | |||
| 1bdf10a046 | |||
| d25d1a3aa9 | |||
| 507d0a792e | |||
| da613867d9 | |||
| 50074a17c8 | |||
| 53c197146a | |||
| 09bef2aadd | |||
| 77e723e6f6 | |||
| 1d50072cc5 | |||
| 157bb405c3 | |||
| 4e10d0326b | |||
| c03393604c | |||
| d15f3e04fb | |||
| a21393294b | |||
| 191e24d218 | |||
| 2c31015a14 | |||
| 4b681d5c3e | |||
| b19cf416f9 | |||
| 5c1a41c319 | |||
| 6e4b50eaa5 | |||
| 72e880eaf2 | |||
| 919b13db9c | |||
| e598a43acc | |||
| 6fe5260bd2 | |||
| 29ca0d291c | |||
| 32f9ea74f1 | |||
| 9d1fb69fee | |||
| 24a02d862f | |||
| cfe9965d03 | |||
| d260b9d2f8 | |||
| 9ec55b3c01 | |||
| c693d96339 | |||
| 8bb6b6db66 | |||
| 849cdf2a19 | |||
| d0d3f94049 | |||
| 613e663c13 | |||
| bd10871da9 | |||
| 671a4314d7 | |||
| 1cee7094f3 | |||
| 2284d46bb5 | |||
| 7245876b07 | |||
| 7a35aec7ad | |||
| 870701b6c6 | |||
| 3be86dec58 | |||
| 48d9535007 | |||
| ae2eb52cf8 | |||
| 7e9e1e19fa | |||
| c9ef93088b | |||
| 99e4bb5c4d | |||
| dd5af2b865 | |||
| 4671253147 | |||
| add01b28fe | |||
| a27a53e238 | |||
| 6aff9b4d93 | |||
| c9f57233a4 | |||
| 7d72e94aa3 | |||
| 3d6a0b4306 | |||
| babc533efc | |||
| 36908134e6 | |||
| d426b41368 | |||
| 1836a414eb | |||
| ca49a29dd9 | |||
| 02a768a6af | |||
| bbc4d7c270 | |||
| bfa1521f85 | |||
| a05d380fcf | |||
| 4a32995ca1 | |||
| 53e1fe7201 | |||
| 2e8ff1be92 | |||
| e14f9fc4af | |||
| 896f302bcf | |||
| cc0bd1cf49 | |||
| 7a1d9cbbd6 | |||
| 34e8d4cb6f | |||
| 4a9b70ac68 | |||
| 889258c874 | |||
| 90c343c752 | |||
| ff7542af70 |
+106
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Docker
|
||||
|
||||
on: ["push", "pull_request"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: false
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/node_modules
|
||||
/config.json
|
||||
/config.example.json
|
||||
/robots_list.json
|
||||
/data
|
||||
/data/*
|
||||
/test_data
|
||||
|
||||
@@ -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": {
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +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 --silent --coverage --coverageReporters=text-lcov | coveralls
|
||||
- jshint ./src
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
FROM node:15
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
VOLUME [ "/usr/src/app/data" ]
|
||||
|
||||
# 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 ./
|
||||
COPY src/postinstall.js ./src/postinstall.js
|
||||
COPY src/config.default.json ./src/config.default.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" ]
|
||||
@@ -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.
|
||||
@@ -1,8 +1,9 @@
|
||||
# GitBlog.md
|
||||
|
||||
|
||||
[](https://travis-ci.org/Klemek/GitBlog.md)
|
||||
[](https://github.com/boyter/scc/#badges-beta)
|
||||
[](https://coveralls.io/github/Klemek/GitBlog.md?branch=master)
|
||||
[](https://lgtm.com/projects/g/Klemek/GitBlog.md/context:javascript)
|
||||
[](https://lgtm.com/projects/g/Klemek/GitBlog.md/alerts/)
|
||||
|
||||
# GitBlog.md
|
||||
|
||||
A static blog using Markdown pulled from your git repository.
|
||||
|
||||
@@ -245,6 +246,9 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
It allows you to add math equations to your articles by simply writing LaTeX between `$$` for full size (and between $ for inline) (more info [here](https://www.mathjax.org/))
|
||||
* **PlantUML**
|
||||
It allows you to add UML diagrams with PlantUML Syntax between `@startuml` and `@enduml` (more info [here](http://www.plantuml.com))
|
||||
* **fa-diagrams**
|
||||
It allows you to define SVG diagrams with Font-Awesome icons in [TOML](https://github.com/toml-lang/toml) between `@startfad` and `@endfad` (more info [here](https://github.com/Klemek/fa-diagrams))
|
||||
|
||||
|
||||
## Configuration
|
||||
[back to top](#gitblog-md)
|
||||
@@ -258,6 +262,8 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
the directory where will be located the git repo with templates and articles
|
||||
* `view_engine` (default: ejs)
|
||||
the Express view engine used to render pages from templates
|
||||
* `rate_limit` (default: 100)
|
||||
number of requests allowed in a time-frame of 15 minutes
|
||||
* `access_log` (default: access.log)
|
||||
log file where to save access requests (empty to disable)
|
||||
* `error_log` (default: error.log)
|
||||
@@ -273,6 +279,10 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
activate MathJax equations formatting
|
||||
* `plantuml` (default: true)
|
||||
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**
|
||||
@@ -323,3 +333,16 @@ 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
|
||||
* `robots`
|
||||
* `list_url`: (default: https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json)
|
||||
url to fetch for web crawlers patterns
|
||||
* `list_file`: (default: robots_list.json)
|
||||
file to store web crawlers patterns
|
||||
* `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)
|
||||
|
||||
|
||||
Generated
+11194
-5101
File diff suppressed because it is too large
Load Diff
+17
-10
@@ -1,32 +1,39 @@
|
||||
{
|
||||
"name": "gitblog.md",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.3",
|
||||
"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",
|
||||
"crypto": "^1.0.1",
|
||||
"ejs": "^2.6.2",
|
||||
"ejs": "^3.1.6",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.5.1",
|
||||
"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": {
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"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",
|
||||
|
||||
@@ -19,6 +19,7 @@ If you see this page, that means it's working
|
||||
* [Spoilers](#spoilers)
|
||||
* [Math Equations](#mathequations)
|
||||
* [UML](#uml)
|
||||
* [Diagrams](#diagrams)
|
||||
* [Youtube Videos](#youtubevideos)
|
||||
|
||||
### Headers
|
||||
@@ -253,6 +254,34 @@ showdown -left-> express : 4. html
|
||||
express -up-> web : 5. html
|
||||
@enduml
|
||||
|
||||
### Diagrams
|
||||
[Back to top](#top)
|
||||
|
||||
You can use [fa-diagrams](https://github.com/Klemek/fa-diagrams) with `@startfad` and `@endfad` tags and using [TOML](https://github.com/toml-lang/toml) inside
|
||||
|
||||
@startfad
|
||||
[[nodes]]
|
||||
name = "node1"
|
||||
icon = "laptop-code"
|
||||
color = "#4E342E"
|
||||
bottom = "my app"
|
||||
|
||||
[[nodes]]
|
||||
name = "node2"
|
||||
icon = "globe"
|
||||
color = "#455A64"
|
||||
bottom = "world"
|
||||
|
||||
[[links]]
|
||||
from = "node1"
|
||||
to = "node2"
|
||||
color = "#333333"
|
||||
bottom = '"hello"'
|
||||
|
||||
[links.top]
|
||||
icon = "envelope"
|
||||
@endfad
|
||||
|
||||
### Youtube Videos
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1><%= info.title %></h1>
|
||||
<h1 class="title"><%= info.title %></h1>
|
||||
<%= info.description %>
|
||||
<h2>Articles in this blog :</h2>
|
||||
<% articles.forEach((article) => { %>
|
||||
<div class="article">
|
||||
<h3><%- `<a href="${article.url}">${article.title}</a>` %></h3>
|
||||
<span class="time"><span>Published on</span> <%= article.year + '-' + article.month + '-' + article.day %></span>
|
||||
<%- `<a href="${article.url}">` %>
|
||||
<h3><%- `${article.title}` %></h3>
|
||||
<span class="time"><span>Published on</span> <%= article.year + '-' + ('0' + article.month).slice(-2) + '-' + ('0' + article.day).slice(-2) %></span>
|
||||
<% if(article.thumbnail){ %>
|
||||
<%- `<img alt="thumbnail" src=${article.thumbnail}>` %>
|
||||
<% } %>
|
||||
<%- `</a>` %>
|
||||
</div>
|
||||
<% }); %>
|
||||
<%- include('footer'); %>
|
||||
|
||||
@@ -16,7 +16,7 @@ body {
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 42rem;
|
||||
max-width: 45rem;
|
||||
padding: 2rem;
|
||||
margin: auto;
|
||||
background-color: #F0F0F0;
|
||||
@@ -54,6 +54,13 @@ pre {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
background: #DDD;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.5em solid #ccc;
|
||||
padding-left: 1em;
|
||||
@@ -108,10 +115,11 @@ main.article div.header a.link-home {
|
||||
line-height: 2.4;
|
||||
}
|
||||
|
||||
main.article div.header h1, main.article div.header h2 {
|
||||
main.article div.header h1, main.article div.header h2, .title {
|
||||
margin-top: 0.85em;
|
||||
margin-bottom: 0.25em;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
main.article div.header h1 a, main.article div.header h2 a, div.article h3 a {
|
||||
@@ -129,6 +137,12 @@ div.article {
|
||||
div.article h3 {
|
||||
font-size: 1.3em;
|
||||
margin:0;
|
||||
color: #3C3CA1;
|
||||
}
|
||||
|
||||
div.article a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
div.article img{
|
||||
@@ -138,6 +152,15 @@ div.article img{
|
||||
margin-top:0.25em;
|
||||
}
|
||||
|
||||
div.article:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
div.article:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
#text {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
@@ -147,7 +170,7 @@ div.article img{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#text img {
|
||||
#text img, #text svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<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>
|
||||
<span class="time"><span>Published on</span> <%= article.year + '-' + article.month + '-' + article.day %></span>
|
||||
<span class="time"><span><%= article.draft ? 'Drafted on' : 'Published on' %></span> <%= article.year + '-' + ('0' + article.month).slice(-2) + '-' + ('0' + article.day).slice(-2) %></span>
|
||||
</div>
|
||||
<div id="text"><%- article.content %></div>
|
||||
<br>
|
||||
|
||||
+125
-34
@@ -3,6 +3,7 @@ const app = express();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pjson = require('../package.json');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
app.enable('trust proxy');
|
||||
|
||||
@@ -50,6 +51,33 @@ 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
const botDetector = require('./bot_detector')(config);
|
||||
botDetector.load((status, err) => {
|
||||
switch (status) {
|
||||
case botDetector.status.FETCH_OK:
|
||||
console.log(cons.ok, 'fetched robots list');
|
||||
break;
|
||||
case botDetector.status.FETCH_ERROR:
|
||||
console.error(cons.error, 'error fetching robots list : ' + err);
|
||||
break;
|
||||
case botDetector.status.READ_OK:
|
||||
console.log(cons.ok, `read robots list: ${botDetector.count}`);
|
||||
break;
|
||||
case botDetector.status.READ_ERROR:
|
||||
console.error(cons.error, 'error reading robots list : ' + err);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// set view engine from configuration
|
||||
app.set('view engine', config['view_engine']);
|
||||
@@ -64,24 +92,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 = {
|
||||
@@ -90,7 +121,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'])) {
|
||||
@@ -99,18 +130,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
|
||||
render(req, res, errorPath, {error: code}, code);
|
||||
} else {
|
||||
render(req, res, errorPath, { error: code }, code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -122,6 +155,16 @@ module.exports = (config) => {
|
||||
next();
|
||||
});
|
||||
|
||||
//rate limit for safer server
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: config['rate_limit'],
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
//detect robots
|
||||
app.use(botDetector.handle);
|
||||
|
||||
//log request at result end
|
||||
app.use((req, res, next) => {
|
||||
if (config['access_log']) {
|
||||
@@ -129,7 +172,7 @@ module.exports = (config) => {
|
||||
res.end = (chunk, encoding) => {
|
||||
fs.appendFile(config['access_log'],
|
||||
`${res.statusCode} ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n`,
|
||||
{encoding: 'UTF-8'}, () => {
|
||||
{ encoding: 'UTF-8' }, () => {
|
||||
res.end = end;
|
||||
res.end(chunk, encoding);
|
||||
});
|
||||
@@ -142,15 +185,44 @@ 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, '/', req.isRobot, () => {
|
||||
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']) {
|
||||
if (req.query['all']) {
|
||||
const keys = Object.keys(articles).filter(key => !articles[key].draft);
|
||||
keys.unshift('/');
|
||||
const read = (i, outputData) => {
|
||||
if (i >= keys.length) {
|
||||
res.json(outputData);
|
||||
} else {
|
||||
hc.read(keys[i], (data) => {
|
||||
outputData.push(data);
|
||||
read(i + 1, outputData);
|
||||
});
|
||||
}
|
||||
};
|
||||
read(0, []);
|
||||
} else {
|
||||
hc.read('/', (data) => {
|
||||
res.json(data);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showError(req, res, 404);
|
||||
}
|
||||
});
|
||||
|
||||
//RSS endpoint
|
||||
@@ -158,10 +230,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'])
|
||||
@@ -169,7 +241,7 @@ module.exports = (config) => {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
url: host + article.url,
|
||||
date: article.date
|
||||
date: article.date,
|
||||
});
|
||||
});
|
||||
lastRSS = feed.xml();
|
||||
@@ -183,24 +255,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;
|
||||
}
|
||||
}
|
||||
cp.exec(config['webhook']['pull_command'], {cwd: path.join(__dirname, '..', config['data_dir'])}, (err) => {
|
||||
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);
|
||||
}
|
||||
@@ -208,32 +285,45 @@ 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(data);
|
||||
});
|
||||
} else {
|
||||
showError(req, res, 404);
|
||||
}
|
||||
} else {
|
||||
hc.count(req, articlePath, req.isRobot, () => {
|
||||
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
|
||||
render(req, res, templatePath, {article: article});
|
||||
} else {
|
||||
render(req, res, templatePath, { article: article });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -264,11 +354,12 @@ 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'}, () => {
|
||||
{ encoding: 'UTF-8' }, () => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
module.exports = (config) => {
|
||||
const _this = {
|
||||
status: {
|
||||
FETCH_OK: 1,
|
||||
FETCH_ERROR: 2,
|
||||
READ_OK: 3,
|
||||
READ_ERROR: 4,
|
||||
},
|
||||
count: [],
|
||||
regex: null,
|
||||
};
|
||||
|
||||
const fetchList = (cb) => {
|
||||
https.get(config['robots']['list_url'], (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
cb(res.statusCode);
|
||||
} else {
|
||||
const file = fs.createWriteStream(config['robots']['list_file']);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(cb);
|
||||
});
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
cb(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
const readFile = (cb) => {
|
||||
fs.readFile(config['robots']['list_file'], { encoding: 'utf-8' }, (err, data) => {
|
||||
if (err) {
|
||||
cb(err, undefined);
|
||||
} else {
|
||||
try {
|
||||
cb(undefined, JSON.parse(data));
|
||||
} catch (err) {
|
||||
cb(err, undefined);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_this.load = (cb) => {
|
||||
fetchList((err) => {
|
||||
cb(err ? _this.status.FETCH_ERROR : _this.status.FETCH_OK, err);
|
||||
readFile((err, data) => {
|
||||
if (!err) {
|
||||
_this.count = data.length;
|
||||
_this.regex = new RegExp('(' + data.filter(v => v['pattern']).map(v => v['pattern'])
|
||||
.join('|') + ')');
|
||||
}
|
||||
cb(err ? _this.status.READ_ERROR : _this.status.READ_OK, err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_this.handle = (req, res, next) => {
|
||||
req.isRobot = !!((req.headers['user-agent'] || '').match(_this.regex));
|
||||
next();
|
||||
};
|
||||
|
||||
return _this;
|
||||
};
|
||||
+15
-1
@@ -3,6 +3,7 @@
|
||||
"host": "",
|
||||
"data_dir": "data",
|
||||
"view_engine": "ejs",
|
||||
"rate_limit": 100,
|
||||
"access_log": "access.log",
|
||||
"error_log": "error.log",
|
||||
"modules": {
|
||||
@@ -10,7 +11,9 @@
|
||||
"webhook": true,
|
||||
"prism": true,
|
||||
"mathjax": true,
|
||||
"plantuml": true
|
||||
"plantuml": true,
|
||||
"fa-diagrams": true,
|
||||
"hit_counter": true
|
||||
},
|
||||
"home": {
|
||||
"title": "GitBlog.md",
|
||||
@@ -56,5 +59,16 @@
|
||||
},
|
||||
"plantuml": {
|
||||
"output_format": "svg"
|
||||
},
|
||||
"hit_counter": {
|
||||
"unique_visitor_timeout": 7200000
|
||||
},
|
||||
"robots": {
|
||||
"list_url": "https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json",
|
||||
"list_file": "robots_list.json"
|
||||
},
|
||||
"redis": {
|
||||
"host": "localhost",
|
||||
"port": 6379
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ const merge = (ref, src) => {
|
||||
|
||||
module.exports = () => {
|
||||
try {
|
||||
let configData = fs.readFileSync('config.json', {encoding: 'UTF-8'});
|
||||
let configData = fs.readFileSync('config.json', { encoding: 'UTF-8' });
|
||||
let config = JSON.parse(configData);
|
||||
return merge(refConfig, config);
|
||||
} catch (error) {
|
||||
|
||||
+37
-22
@@ -11,26 +11,32 @@ const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
const getFileTree = (dir, cb) => {
|
||||
let list = [];
|
||||
let remaining = 0;
|
||||
fs.readdir(dir, {withFileTypes: true}, (err, items) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
fs.readdir(dir, { withFileTypes: true }, (err, items) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -41,10 +47,10 @@ const getFileTree = (dir, cb) => {
|
||||
* @param cb
|
||||
*/
|
||||
const readIndexFile = (path, thumbnailTag, cb) => {
|
||||
fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
|
||||
fs.readFile(path, { encoding: 'UTF-8' }, (err, data) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
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, disable, cb) => {
|
||||
if (!client.connected || disable) {
|
||||
cb();
|
||||
} else {
|
||||
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
|
||||
visitors[path] = (visitors[path] || {});
|
||||
const now = Date.now();
|
||||
const isNewVisitor = (now - (visitors[path][ip] || 0)) > config['hit_counter']['unique_visitor_timeout'];
|
||||
visitors[path][ip] = now;
|
||||
client
|
||||
.multi()
|
||||
.hincrby(path, 'h', 1)
|
||||
.hincrby(path, 'v', isNewVisitor ? 1 : 0)
|
||||
.exec(cb);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanVisitors = (path) => {
|
||||
visitors[path] = (visitors[path] || {});
|
||||
const now = Date.now();
|
||||
let count = 0;
|
||||
for (let ip in visitors[path]) {
|
||||
if ((now - visitors[path][ip]) > config['hit_counter']['unique_visitor_timeout']) {
|
||||
delete visitors[path][ip];
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
const read = (path, cb) => {
|
||||
if (!client.connected) {
|
||||
cb({
|
||||
path: path,
|
||||
hits: 0,
|
||||
total_visitors: 0,
|
||||
current_visitors: cleanVisitors(path),
|
||||
});
|
||||
} else {
|
||||
client.hgetall(path, (_, value) => {
|
||||
cb({
|
||||
path: path,
|
||||
hits: value ? parseInt(value.h) || 0 : 0,
|
||||
total_visitors: value ? parseInt(value.v) || 0 : 0,
|
||||
current_visitors: cleanVisitors(path),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
count: count,
|
||||
read: read,
|
||||
};
|
||||
};
|
||||
+6
-4
@@ -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))
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
copy(path.join('sample_data', 'article'), dir);
|
||||
}
|
||||
+155
-27
@@ -5,18 +5,70 @@ const showdown = require('showdown');
|
||||
module.exports = (config) => {
|
||||
const converter = new showdown.Converter(config['showdown']);
|
||||
|
||||
/**
|
||||
* get parts outside of codes/scripts
|
||||
* @param {string} data
|
||||
* @returns {{index:number, end:number, text:string}[]} parts
|
||||
*/
|
||||
const getParts = (data) => {
|
||||
let parts = [];
|
||||
let match;
|
||||
let i = 0;
|
||||
while ((match = /```/m.exec(data.slice(i)))) {
|
||||
parts.push({
|
||||
index: i,
|
||||
text: data.slice(i, i + match.index),
|
||||
});
|
||||
i += match.index + match[0].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
|
||||
|
||||
// detect scripts outside of code
|
||||
parts.forEach((p, pi) => {
|
||||
let i = 0;
|
||||
const subParts = [];
|
||||
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
|
||||
subParts.push({
|
||||
index: p.index + i,
|
||||
text: p.text.slice(i, i + match.index),
|
||||
});
|
||||
i += match.index + match[0].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);
|
||||
});
|
||||
|
||||
parts.forEach(part => part.end = part.index + part.text.length);
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
const renderShowDown = (data, cb) => {
|
||||
const html = converter.makeHtml(data);
|
||||
cb(html);
|
||||
};
|
||||
|
||||
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))) {
|
||||
@@ -26,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']) {
|
||||
@@ -33,18 +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;
|
||||
while ((match = umlRegex.exec(data))) {
|
||||
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
|
||||
data = data.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + data.slice(match.index + match[0].length);
|
||||
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;
|
||||
@@ -53,28 +113,40 @@ 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) => {
|
||||
const doMJ = (match, format, i) => {
|
||||
const eq = match[1].trim();
|
||||
const output = config['mathjax']['output_format'];
|
||||
const mjConf = {
|
||||
math: eq,
|
||||
format: format,
|
||||
speakText: config['mathjax']['speak_text']
|
||||
speakText: config['mathjax']['speak_text'],
|
||||
};
|
||||
mjConf[output] = true;
|
||||
mjAPI.typeset(mjConf, (res) => {
|
||||
data = data.slice(0, match.index) + res[output] + data.slice(match.index + match[0].length);
|
||||
data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length);
|
||||
renderMathJax(data, (data2) => {
|
||||
cb(data2);
|
||||
});
|
||||
@@ -84,29 +156,83 @@ 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(data))) {
|
||||
doMJ(match, 'TeX');
|
||||
} else if ((match = inlineEqRegex.exec(data))) {
|
||||
doMJ(match, 'inline-TeX');
|
||||
if ((match = eqRegex.exec(parts[i].text))) {
|
||||
doMJ(match, 'TeX', i);
|
||||
found = true;
|
||||
break;
|
||||
} else if ((match = inlineEqRegex.exec(parts[i].text))) {
|
||||
doMJ(match, 'inline-TeX', i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
cb(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let faDiagrams;
|
||||
let toml;
|
||||
if (config['modules']['fa-diagrams']) {
|
||||
faDiagrams = require('fa-diagrams');
|
||||
toml = require('@iarna/toml');
|
||||
}
|
||||
|
||||
const renderFaDiagrams = (data, cb) => {
|
||||
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;
|
||||
parts.forEach(part => {
|
||||
while ((match = diagramsRegex.exec(part.text))) {
|
||||
const code = match[1].trim();
|
||||
let output;
|
||||
try {
|
||||
const diagData = toml.parse(code);
|
||||
const findLineBreaks = (data) => {
|
||||
Object.keys(data).forEach(key => {
|
||||
if (typeof data[key] === 'object') {
|
||||
findLineBreaks(data[key]);
|
||||
} else if (typeof data[key] === 'string') {
|
||||
data[key] = data[key].replace(/\\n/gm, '\n');
|
||||
}
|
||||
});
|
||||
};
|
||||
findLineBreaks(diagData);
|
||||
output = faDiagrams.compute(diagData);
|
||||
} catch (err) {
|
||||
output = `<b style="color:red">${err.toString()}</b>`;
|
||||
}
|
||||
part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length);
|
||||
}
|
||||
data = data.slice(0, part.index) + part.text + data.slice(part.end);
|
||||
});
|
||||
cb(data);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getParts: config['test'] ? getParts : undefined,
|
||||
renderShowDown: config['test'] ? renderShowDown : undefined,
|
||||
renderPrism: config['test'] ? renderPrism : undefined,
|
||||
renderPlantUML: config['test'] ? renderPlantUML : undefined,
|
||||
renderMathJax: config['test'] ? renderMathJax : undefined,
|
||||
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
|
||||
render: (file, cb) => {
|
||||
fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
|
||||
renderPrism(data, (data) => {
|
||||
fs.readFile(file, { encoding: 'UTF-8' }, (err, data) => {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else {
|
||||
renderPlantUML(data, (data) => {
|
||||
renderFaDiagrams(data, (data) => {
|
||||
renderMathJax(data, (data) => {
|
||||
renderPrism(data, (data) => {
|
||||
renderShowDown(data, (html) => {
|
||||
cb(null, html);
|
||||
});
|
||||
@@ -115,6 +241,8 @@ module.exports = (config) => {
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ const fs = require('fs');
|
||||
* @param scriptPath
|
||||
*/
|
||||
module.exports = (scriptPath) => {
|
||||
eval.call(global, fs.readFileSync(scriptPath, {encoding: 'UTF-8'}));
|
||||
eval.call(global, fs.readFileSync(scriptPath, { encoding: 'UTF-8' }));
|
||||
};
|
||||
|
||||
|
||||
+232
-77
@@ -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,37 +50,42 @@ 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(() => {
|
||||
fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
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');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('test post 400', (done) => {
|
||||
test('post 400', (done) => {
|
||||
config['access_log'] = path.join(dataDir, 'access.log');
|
||||
request(app).post('/rsstest').then(() => {
|
||||
fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
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');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
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(() => {
|
||||
fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
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' +
|
||||
'400 POST /rsstest ' + 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(() => {
|
||||
fs.readFile(path.join(dataDir, 'error.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
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,7 +189,8 @@ 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();
|
||||
@@ -186,13 +202,15 @@ describe('Test root path', () => {
|
||||
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 +219,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 +242,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 +262,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 +284,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 +323,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 +346,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 +367,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 +379,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 +393,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 +407,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,13 +422,14 @@ 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();
|
||||
});
|
||||
@@ -400,41 +437,45 @@ describe('Test articles rendering', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
test('404 hidden file', (done) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, 'tmp')]);
|
||||
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();
|
||||
});
|
||||
});
|
||||
test('404 hidden folder', (done) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '.git')]);
|
||||
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 +483,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,21 +496,134 @@ 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Test stats', () => {
|
||||
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({
|
||||
path: '/',
|
||||
hits: 0,
|
||||
total_visitors: 0,
|
||||
current_visitors: 0,
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 index stats all no article', (done) => {
|
||||
config['modules']['hit_counter'] = true;
|
||||
app.reload(() => {
|
||||
request(app).get('/stats?all=true')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
path: '/',
|
||||
hits: 0,
|
||||
total_visitors: 0,
|
||||
current_visitors: 0,
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 index stats all 2 article 1 drafted', (done) => {
|
||||
config['modules']['hit_counter'] = true;
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2019', '04', '05'),
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2019', '04', '05', 'draft.md'),
|
||||
]);
|
||||
app.reload(() => {
|
||||
request(app).get('/stats?all=true')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toEqual([
|
||||
{
|
||||
path: '/',
|
||||
hits: 0,
|
||||
total_visitors: 0,
|
||||
current_visitors: 0,
|
||||
},
|
||||
{
|
||||
path: '2019/05/05',
|
||||
hits: 0,
|
||||
total_visitors: 0,
|
||||
current_visitors: 0,
|
||||
},
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
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 article 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({
|
||||
path: '2019/05/05',
|
||||
hits: 0,
|
||||
total_visitors: 0,
|
||||
current_visitors: 0,
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
const fs = require('fs');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
const dataDir = 'test_data';
|
||||
|
||||
const config = {
|
||||
robots: {
|
||||
list_url: '',
|
||||
list_file: `${dataDir}/robots_list.json`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
const botDetector = require('../src/bot_detector')(config);
|
||||
|
||||
describe('load()', () => {
|
||||
test('success', (done) => {
|
||||
config.robots = {
|
||||
list_url: 'https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/COUNTER_Robots_list.json',
|
||||
list_file: `${dataDir}/robots_list_success.json`,
|
||||
};
|
||||
let count = 0;
|
||||
botDetector.load((status, err) => {
|
||||
expect(err).not.toBeDefined();
|
||||
expect(status).toBe(count === 0 ? botDetector.status.FETCH_OK : botDetector.status.READ_OK);
|
||||
if (count > 0) {
|
||||
done();
|
||||
}
|
||||
count++;
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch and file failure', (done) => {
|
||||
let count = 0;
|
||||
config.robots = {
|
||||
list_url: 'https://blog.klemek.fr/invalid.json',
|
||||
list_file: `${dataDir}/robots_list_fail_1.json`,
|
||||
};
|
||||
botDetector.load((status) => {
|
||||
expect(status).toBe(count === 0 ? botDetector.status.FETCH_ERROR : botDetector.status.READ_ERROR);
|
||||
if (count > 0) {
|
||||
done();
|
||||
}
|
||||
count++;
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch failure and file ok', (done) => {
|
||||
let count = 0;
|
||||
config.robots = {
|
||||
list_url: 'https://blog.klemek.fr/invalid.json',
|
||||
list_file: `${dataDir}/robots_list_fail_2.json`,
|
||||
};
|
||||
fs.writeFile(config.robots.list_file, '[]\n', { encoding: 'utf-8' }, () => {
|
||||
botDetector.load((status) => {
|
||||
expect(status).toBe(count === 0 ? botDetector.status.FETCH_ERROR : botDetector.status.READ_OK);
|
||||
if (count > 0) {
|
||||
done();
|
||||
}
|
||||
count++;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('handle()', () => {
|
||||
beforeAll((done) => {
|
||||
config.robots = {
|
||||
list_url: 'https://blog.klemek.fr/invalid.json',
|
||||
list_file: `${dataDir}/robots_list_fake.json`,
|
||||
};
|
||||
fs.writeFile(config.robots.list_file, '[{"pattern":"bot"}]\n', { encoding: 'utf-8' }, () => {
|
||||
botDetector.load((status) => {
|
||||
if (status !== botDetector.status.FETCH_ERROR) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('not bot', (done) => {
|
||||
const req = {
|
||||
headers: {
|
||||
'user-agent': 'my user agent',
|
||||
},
|
||||
};
|
||||
botDetector.handle(req, null, () => {
|
||||
expect(req.isRobot).toBeFalsy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('bot', (done) => {
|
||||
const req = {
|
||||
headers: {
|
||||
'user-agent': 'bot',
|
||||
},
|
||||
};
|
||||
botDetector.handle(req, null, () => {
|
||||
expect(req.isRobot).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+14
-8
@@ -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,11 +30,12 @@ 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'});
|
||||
const data = fs.readFileSync(configFile, { encoding: 'UTF-8' });
|
||||
fs.writeFileSync(configFile, data.replace('3000', '3333'), { encoding: 'UTF-8' });
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3333);
|
||||
@@ -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*',
|
||||
]);
|
||||
});
|
||||
+29
-27
@@ -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();
|
||||
@@ -226,8 +225,8 @@ describe('Test article fetching', () => {
|
||||
test('empty index file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
utils.createEmptyDirs([dir]);
|
||||
utils.createEmptyFiles([file]);
|
||||
utils.createEmptyDirs([ dir ]);
|
||||
utils.createEmptyFiles([ file ]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
@@ -253,7 +252,7 @@ describe('Test article fetching', () => {
|
||||
test('correct index file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
utils.createEmptyDirs([dir]);
|
||||
utils.createEmptyDirs([ dir ]);
|
||||
fs.writeFileSync(file, `
|
||||
# Title with : info !
|
||||

|
||||
@@ -284,7 +283,7 @@ describe('Test article fetching', () => {
|
||||
test('correct draft file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, 'draft.md');
|
||||
utils.createEmptyDirs([dir]);
|
||||
utils.createEmptyDirs([ dir ]);
|
||||
fs.writeFileSync(file, `
|
||||
# Title with : info !
|
||||

|
||||
@@ -316,8 +315,11 @@ describe('Test article fetching', () => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
const file2 = path.join(dir, 'draft.md');
|
||||
utils.createEmptyDirs([dir]);
|
||||
utils.createEmptyFiles([file, file2]);
|
||||
utils.createEmptyDirs([ dir ]);
|
||||
utils.createEmptyFiles([
|
||||
file,
|
||||
file2,
|
||||
]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
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']);
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.hgetall = (_, cb) => {
|
||||
cb();
|
||||
};
|
||||
mockClient.multi = () => mockClient;
|
||||
mockClient.hincrby = () => mockClient;
|
||||
mockClient.exec = (cb) => {
|
||||
cb();
|
||||
};
|
||||
config['hit_counter']['unique_visitor_timeout'] = -1;
|
||||
});
|
||||
|
||||
describe('read()', () => {
|
||||
test('normal', (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.total_visitors).toBe(34);
|
||||
expect(data.current_visitors).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('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.total_visitors).toBe(0);
|
||||
expect(data.current_visitors).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('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.total_visitors).toBe(0);
|
||||
expect(data.current_visitors).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('1 visitor', (done) => {
|
||||
config['hit_counter']['unique_visitor_timeout'] = 1000;
|
||||
hc.count({
|
||||
headers: {},
|
||||
connection: { remoteAddress: 'test1' },
|
||||
}, '/test/path/5', false, () => {
|
||||
hc.read('/test/path/5', (data) => {
|
||||
expect(data).toBeDefined();
|
||||
expect(data.current_visitors).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('cleaned old visitor', (done) => {
|
||||
hc.count({
|
||||
headers: {},
|
||||
connection: { remoteAddress: 'test1' },
|
||||
}, '/test/path/5', false, () => {
|
||||
hc.read('/test/path/5', (data) => {
|
||||
expect(data).toBeDefined();
|
||||
expect(data.current_visitors).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('count()', () => {
|
||||
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', false, () => {
|
||||
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', false, () => {
|
||||
hc.count({
|
||||
headers: {},
|
||||
connection: { remoteAddress: 'test2' },
|
||||
}, '/test/path/2', false, () => {
|
||||
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', false, () => {
|
||||
hc.count({
|
||||
headers: {},
|
||||
connection: { remoteAddress: 'test3' },
|
||||
}, '/test/path/3', false, () => {
|
||||
expect(hincrbyCalls).toEqual([
|
||||
[
|
||||
'/test/path/3',
|
||||
'h',
|
||||
1,
|
||||
],
|
||||
[
|
||||
'/test/path/3',
|
||||
'v',
|
||||
1,
|
||||
],
|
||||
[
|
||||
'/test/path/3',
|
||||
'h',
|
||||
1,
|
||||
],
|
||||
[
|
||||
'/test/path/3',
|
||||
'v',
|
||||
0,
|
||||
],
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+113
-29
@@ -1,4 +1,3 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
@@ -7,23 +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
|
||||
'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);
|
||||
@@ -32,6 +32,7 @@ beforeEach(() => {
|
||||
config['modules']['prism'] = true;
|
||||
config['modules']['mathjax'] = true;
|
||||
config['modules']['plantuml'] = true;
|
||||
config['modules']['fa-diagrams'] = true;
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
@@ -42,6 +43,45 @@ afterAll(() => {
|
||||
}
|
||||
});
|
||||
|
||||
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' ]);
|
||||
});
|
||||
test('lot of stuff', () => {
|
||||
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
|
||||
const parts = renderer.getParts(data);
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
end: 12,
|
||||
text: 'Hello\nthere\n',
|
||||
},
|
||||
{
|
||||
index: 22,
|
||||
end: 30,
|
||||
text: '\ngeneral',
|
||||
},
|
||||
{
|
||||
index: 53,
|
||||
end: 54,
|
||||
text: '\n',
|
||||
},
|
||||
{
|
||||
index: 78,
|
||||
end: 79,
|
||||
text: '\n',
|
||||
},
|
||||
{
|
||||
index: 109,
|
||||
end: 115,
|
||||
text: 'kenobi',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test Showdown', () => {
|
||||
test('normal', (done) => {
|
||||
renderer.renderShowDown('# Hello', (html) => {
|
||||
@@ -112,6 +152,13 @@ describe('Test PlantUML', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('plantuml ignored in code', (done) => {
|
||||
renderer.renderPlantUML('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml', (data) => {
|
||||
expect(data).toBe('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('plantuml multiple uml', (done) => {
|
||||
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml\n@startuml\nBob -> Alice : hello\n@enduml', (data) => {
|
||||
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">\n<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
|
||||
@@ -131,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();
|
||||
@@ -142,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');
|
||||
@@ -157,24 +204,30 @@ describe('Test MathJax', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('no eq in code / script', (done) => {
|
||||
renderer.renderMathJax('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```', (data) => {
|
||||
expect(data).toBe('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
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();
|
||||
@@ -182,6 +235,37 @@ describe('Test MathJax', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test fa-diagrams', () => {
|
||||
test('no fa-diagrams', (done) => {
|
||||
config['modules']['fa-diagrams'] = false;
|
||||
renderer.renderFaDiagrams('@startfad\noptions.rendering.color=\'red\'\n@endfad', (data) => {
|
||||
expect(data).toBe('@startfad\noptions.rendering.color=\'red\'\n@endfad');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('no fa-diagrams in code', (done) => {
|
||||
renderer.renderFaDiagrams('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```', (data) => {
|
||||
expect(data).toBe('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('valid fa-diagrams', (done) => {
|
||||
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=\'red\'\n@endfad\nafter', (data) => {
|
||||
expect(data).toBe('before\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0" width="0" height="0" font-family="Arial" font-size="15" fill="red" stroke-width="0"></svg>\nafter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('invalid toml', (done) => {
|
||||
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=red\n@endfad\nafter', (data) => {
|
||||
expect(data).toBe('before\n<b style="color:red">TomlError: Unexpected character, expecting string, number, datetime, boolean, inline array or inline table at row 1, col 26, pos 25:\n' +
|
||||
'1> options.rendering.color=red\n' +
|
||||
' ^\n' +
|
||||
'\n</b>\nafter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test render', () => {
|
||||
test('invalid file', (done) => {
|
||||
renderer.render('invalid file', (err, html) => {
|
||||
@@ -192,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,4 +1,3 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
+7
-5
@@ -2,17 +2,19 @@ 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});
|
||||
items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
try {
|
||||
items.forEach(deleteItem);
|
||||
} catch (e) {
|
||||
@@ -24,6 +26,6 @@ const deleteFolderSync = (dir) => {
|
||||
|
||||
module.exports = {
|
||||
deleteFolderSync: deleteFolderSync,
|
||||
createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, {recursive: true})),
|
||||
createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, { recursive: true })),
|
||||
createEmptyFiles: (list) => list.forEach((file) => fs.writeFileSync(file, '')),
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
@@ -10,6 +10,7 @@ node nodejs {
|
||||
}
|
||||
|
||||
package data {
|
||||
[template.ejs]
|
||||
package "2019/06/18" {
|
||||
component index [
|
||||
index.md
|
||||
@@ -22,6 +23,7 @@ package data {
|
||||
web -down-> TCP : 1. /2019/06/18/title
|
||||
express -down-> index : 2. fetch
|
||||
index -up-> showdown : 3. markdown
|
||||
template.ejs -up-> express : 4
|
||||
showdown -left-> express : 4. html
|
||||
express -up-> web : 5. html
|
||||
|
||||
|
||||
Reference in New Issue
Block a user