Compare commits
169 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba53dc9266 | |||
| 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 | |||
| de26feb05c | |||
| 8bb455b576 | |||
| 378ed438b6 | |||
| 3b07b6b9c5 | |||
| b6afcd4992 | |||
| 35fcdc7320 | |||
| dfb93b6764 | |||
| 6af4012522 | |||
| 1b91002c03 | |||
| bedd6a2953 | |||
| 52d37d56cd | |||
| fc7bc63c46 | |||
| 4397a76d9b | |||
| ddf964eb27 | |||
| 4b47276484 | |||
| a7fedb149f | |||
| ea95a285c9 | |||
| 0fde428806 | |||
| 8fc7ff1ca7 | |||
| ae4e2eb8d5 | |||
| 528e4be1fe | |||
| bd42883330 | |||
| b6ac0a73b4 | |||
| aebc3da5bc | |||
| 7a4a4f9006 | |||
| 1341aa5a56 | |||
| 5e05f250f4 | |||
| 6cf7be3afb | |||
| 6aceacad18 | |||
| a3a23be1c2 | |||
| e8e8024021 | |||
| 1806d60ca7 | |||
| 2c5f2e589f | |||
| 847d228c0a | |||
| 576948acee | |||
| fa6d91db20 | |||
| 989bcdf130 | |||
| 000104c99d | |||
| f2bd0ec10e | |||
| 97dab302d8 | |||
| 55e258e093 | |||
| 7b22a4773d | |||
| 14cd1436c3 | |||
| c112e1ea62 | |||
| bd8385ea60 | |||
| 6dbc7f359b | |||
| e773f53da1 | |||
| 35a7747d6d | |||
| 0d173cfcef | |||
| 0480536a20 | |||
| 333bbf6eb8 | |||
| 6bdcd6872e | |||
| 9d3c1d0847 | |||
| def326676c | |||
| d343179764 | |||
| 90c343c752 | |||
| ff7542af70 |
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src/lib
|
||||
+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 }}"
|
||||
|
||||
+4
-1
@@ -2,7 +2,10 @@
|
||||
/node_modules
|
||||
/config.json
|
||||
/config.example.json
|
||||
/robots_list.json
|
||||
/data
|
||||
/data/*
|
||||
/test_data
|
||||
/access.log
|
||||
/error.log
|
||||
/error.log
|
||||
/coverage
|
||||
@@ -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": {
|
||||
}
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
dist: xenial
|
||||
language: node_js
|
||||
node_js:
|
||||
- "12"
|
||||
cache:
|
||||
npm: true
|
||||
directories:
|
||||
- node_modules
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- graphviz
|
||||
install:
|
||||
- npm install
|
||||
- npm install node-plantuml
|
||||
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.
|
||||
|
||||
@@ -14,6 +15,7 @@ A static blog using Markdown pulled from your git repository.
|
||||
* **[How it works](#how-it-works)**
|
||||
* **[Installation](#installation)**
|
||||
* **[Writing an article](#writing-an-article)**
|
||||
* **[Modules](#modules)**
|
||||
* **[Configuration](#configuration)**
|
||||
|
||||
## How it works
|
||||
@@ -80,7 +82,7 @@ On the `/rss` endpoint, the servers gives you a RSS feed based on the list of ar
|
||||
#### 1. Download and install the latest version from the repo
|
||||
```bash
|
||||
git clone https://github.com/klemek/gitblog.md.git
|
||||
npm install
|
||||
npm install --production
|
||||
```
|
||||
#### 2. Create your config file
|
||||
```bash
|
||||
@@ -124,6 +126,34 @@ Resources are located on the `data` folder and can be referenced as the root of
|
||||
/styles/main.css => data/styles/main.css
|
||||
```
|
||||
|
||||
In your template, the following data is sent :
|
||||
|
||||
<details>
|
||||
<summary>details (click)</summary>
|
||||
<p>
|
||||
|
||||
* `info` (every pages)
|
||||
* `title` : the blog's title as in the config
|
||||
* `description` the blog's description as in the config
|
||||
* `host` : the specified or guessed host with the protocol
|
||||
* `version` : the GitBlog.md current running version
|
||||
* `request` : the Express request object
|
||||
* `config` : the content of the config
|
||||
* `article` (article pages only)
|
||||
* `title` : the full title
|
||||
* `thumbnail` the URL path of the thumbnail
|
||||
* `url` : the URL path for this article (with the title)
|
||||
* `date` : a JS date
|
||||
* `year`
|
||||
* `month`
|
||||
* `day`
|
||||
* `path` : the URL path for the folder of the article (without the title)
|
||||
* `realPath` : the system's path for the folder
|
||||
* `escapedTitle` : the code with alphanumeric and underscore characters only
|
||||
* `error` (error pages only) : the error code
|
||||
</p>
|
||||
</details>
|
||||
|
||||
#### 5. Create and init your git source
|
||||
|
||||
You need to [create a new repository](https://github.com/new) on your favorite Git service.
|
||||
@@ -131,7 +161,10 @@ You need to [create a new repository](https://github.com/new) on your favorite G
|
||||
```bash
|
||||
#gitblog.md/
|
||||
cd data
|
||||
git init
|
||||
git remote add origin <url_of_your_repo.git>
|
||||
git add .
|
||||
git commit -m "initial commit"
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
@@ -156,7 +189,7 @@ Here are the steps for Github, if you use another platform adapt it your way (he
|
||||
```json
|
||||
"webhook": {
|
||||
"endpoint": "/webhook",
|
||||
"secret": "sha1=<value>",
|
||||
"secret": "<value>",
|
||||
"signature_header": "X-Hub-Signature"
|
||||
},
|
||||
```
|
||||
@@ -164,6 +197,14 @@ Here are the steps for Github, if you use another platform adapt it your way (he
|
||||
* Update your webhook on github to include the secret
|
||||
* Check if Github successfully reached the endpoint
|
||||
|
||||
#### 8. Keep your server always up and running (optionnal)
|
||||
|
||||
This project `package.json` comes with a [nodemon](https://github.com/remy/nodemon) config.
|
||||
|
||||
After installing (`npm i -g nodemon`) you can then run the app with juste the `nodemon` command in the working directory.
|
||||
|
||||
With this method, you can do a simple `git pull` to update your server.
|
||||
|
||||
## Writing an article
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
@@ -192,15 +233,37 @@ On that same folder, you can place resources like images and reference them in r
|
||||
|
||||
Any URL like `/year/month/day/anything/` will redirect to this article (and link to correct resources)
|
||||
|
||||
## Modules
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
* **RSS**
|
||||
It allows your users to use the feed to be updated as soon as a new article is out
|
||||
* **Webhook**
|
||||
It update your blog from your online repo when it's updated
|
||||
* **Prism**
|
||||
It highlight code blocks to be more readable (more info [here](https://prismjs.com/), you will need the corresponding CSS file on your templates)
|
||||
* **MathJax**
|
||||
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)
|
||||
|
||||
* `node_port` (default: 3000)
|
||||
the port the server is listening to
|
||||
* `host` (default: none)
|
||||
if set (like `https://mywebsite.com`, it will be used as reference for creating links
|
||||
by default, host is guessed based on first request
|
||||
* `data_dir` (default: data)
|
||||
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)
|
||||
@@ -212,18 +275,34 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
activate the webhook endpoint and its features
|
||||
* `prism` (default: true)
|
||||
activate Prism code highlighting
|
||||
* `mathjax` (default: true)
|
||||
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**
|
||||
given to the template to complete page title and metadata
|
||||
* `description` (default: A static blog using Markdown pulled from your git repository)
|
||||
the description of your blog, **strongly advised to be changed**
|
||||
given to the template to complete page title and metadata
|
||||
* `index` (default: index.ejs)
|
||||
the name of the home page template on the data directory
|
||||
it will receive `articles`, a list of articles for rendering
|
||||
* `error` (default: error.ejs)
|
||||
the name of the error page template on the data directory
|
||||
it will receive `error`, the error code
|
||||
* `hidden` (default: `[.ejs]`)
|
||||
file extensions to be returned 404 when reached
|
||||
* `hidden` (default: `[*.ejs,/.git*]`)
|
||||
path matches to be returned 404 when reached
|
||||
* `article`
|
||||
* `index` (default: index.md)
|
||||
the name of the Markdown page of the article on the `/year/month/day/` directory
|
||||
* `draft` (default: draft.md)
|
||||
the name of the Markdown page of an article not shown on the list
|
||||
* `template` (default: template.ejs)
|
||||
the name of the article page template on the data directory
|
||||
* `thumbnail_tag`: (default: thumbnail)
|
||||
@@ -248,4 +327,22 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
* `pull_command`: (default: git pull)
|
||||
the command used by the server on webhook trigger
|
||||
* `showdown`
|
||||
Options to be applied to Showdown renderer (see [showdown options](https://github.com/showdownjs/showdown#valid-options) for more info)
|
||||
Options to be applied to Showdown renderer (see [showdown options](https://github.com/showdownjs/showdown/wiki/Showdown-Options) for more info)
|
||||
* `mathjax`
|
||||
* `output_format`: (default: svg)
|
||||
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
+10982
-5283
File diff suppressed because it is too large
Load Diff
+39
-12
@@ -1,32 +1,39 @@
|
||||
{
|
||||
"nodePort": 5000,
|
||||
"name": "gitblog.md",
|
||||
"version": "1.0.2",
|
||||
"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.0.0",
|
||||
"fa-diagrams": "^1.0.3",
|
||||
"mathjax-node": "^2.1.1",
|
||||
"ncp": "^2.0.0",
|
||||
"node-prismjs": "^0.1.2",
|
||||
"prismjs": "^1.16.0",
|
||||
"node-prismjs": "^0.1.0",
|
||||
"prismjs": "^1.23.0",
|
||||
"redis": "^3.0.2",
|
||||
"rss": "^1.2.2",
|
||||
"showdown": "^1.9.0"
|
||||
"showdown": "^2.0.2"
|
||||
},
|
||||
"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",
|
||||
@@ -37,5 +44,25 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/klemek/gitblog.md/issues"
|
||||
},
|
||||
"homepage": "https://github.com/klemek/gitblog.md#readme"
|
||||
"homepage": "https://github.com/klemek/gitblog.md#readme",
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!/node_modules/",
|
||||
"!src/server.js",
|
||||
"!src/postinstall.js",
|
||||
"!src/lib/*.js"
|
||||
]
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"verbose": true,
|
||||
"ignore": [
|
||||
"test/*",
|
||||
"sample_data/*",
|
||||
"data/*",
|
||||
"uml/*",
|
||||
"*.log",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,26 @@ If you see this page, that means it's working
|
||||
|
||||
## Guide to Markdown formatting
|
||||
|
||||
* [Headers](#headers)
|
||||
* [Emphasis](#emphasis)
|
||||
* [Lists](#lists)
|
||||
* [Links](#links)
|
||||
* [Images](#images)
|
||||
* [Code and Syntax Highlighting](#codeandsyntaxhighlighting)
|
||||
* [Tables](#tables)
|
||||
* [Blockquotes](#blockquotes)
|
||||
* [Inline HTML](#inlinehtml)
|
||||
* [Horizontal Rule](#horizontalrule)
|
||||
* [Line Breaks](#linebreaks)
|
||||
* [Check Boxes](#checkboxes)
|
||||
* [Spoilers](#spoilers)
|
||||
* [Math Equations](#mathequations)
|
||||
* [UML](#uml)
|
||||
* [Diagrams](#diagrams)
|
||||
* [Youtube Videos](#youtubevideos)
|
||||
|
||||
### Headers
|
||||
[Back to top](#top)
|
||||
|
||||
# H1
|
||||
## H2
|
||||
@@ -22,6 +41,7 @@ Alt-H2
|
||||
------
|
||||
|
||||
### Emphasis
|
||||
[Back to top](#top)
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
@@ -32,6 +52,7 @@ Combined emphasis with **asterisks and _underscores_**.
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
### Lists
|
||||
[Back to top](#top)
|
||||
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
@@ -51,6 +72,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
+ Or pluses
|
||||
|
||||
### Links
|
||||
[Back to top](#top)
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
@@ -75,6 +97,7 @@ Some text to show that the reference links can follow later.
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
### Images
|
||||
[Back to top](#top)
|
||||
|
||||
Here's our logo (hover to see the title text):
|
||||
|
||||
@@ -88,6 +111,7 @@ Reference-style:
|
||||
|
||||
|
||||
### Code and Syntax Highlighting
|
||||
[Back to top](#top)
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
@@ -108,6 +132,7 @@ But let's throw in a <b>tag</b>.
|
||||
|
||||
|
||||
### Tables
|
||||
[Back to top](#top)
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
@@ -127,6 +152,7 @@ Markdown | Less | Pretty
|
||||
1 | 2 | 3
|
||||
|
||||
### Blockquotes
|
||||
[Back to top](#top)
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
@@ -136,6 +162,7 @@ Quote break.
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
|
||||
### Inline HTML
|
||||
[Back to top](#top)
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
@@ -146,6 +173,7 @@ Quote break.
|
||||
</dl>
|
||||
|
||||
### Horizontal Rule
|
||||
[Back to top](#top)
|
||||
|
||||
Three or more...
|
||||
|
||||
@@ -162,6 +190,7 @@ ___
|
||||
Underscores
|
||||
|
||||
### Line Breaks
|
||||
[Back to top](#top)
|
||||
|
||||
Here's a line for us to start with.
|
||||
|
||||
@@ -169,3 +198,93 @@ This line is separated from the one above by two newlines, so it will be a *sepa
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
|
||||
### Check Boxes
|
||||
[Back to top](#top)
|
||||
|
||||
* [x] Task completed
|
||||
* [] Task to do
|
||||
|
||||
### Spoilers
|
||||
[Back to top](#top)
|
||||
|
||||
<details><summary>Title of the spoiler (click)</summary><p>
|
||||
Content of the spoiler
|
||||
|
||||
On several lines
|
||||
</p></details>
|
||||
|
||||
### Math Equations
|
||||
[Back to top](#top)
|
||||
|
||||
You can use LaTeX equations with MathJax for full equations and inline ones (based on the number of $) :
|
||||
|
||||
$$
|
||||
\large{\beta=\sum_{i}^{\alpha }\frac{x^{i}}{\alpha}}
|
||||
$$
|
||||
|
||||
|
||||
Where $\alpha$ is cool
|
||||
|
||||
### UML
|
||||
[Back to top](#top)
|
||||
|
||||
You can use PlantUML diagrams with `@startuml` and `@enduml` tags :
|
||||
|
||||
@startuml
|
||||
title Article
|
||||
cloud web
|
||||
node nodejs {
|
||||
TCP -right- [express]
|
||||
[showdown]
|
||||
}
|
||||
package data {
|
||||
package "2019/06/18" {
|
||||
component index [
|
||||
index.md
|
||||
image.png
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
web -down-> TCP : 1. /2019/06/18/title
|
||||
express -down-> index : 2. fetch
|
||||
index -up-> showdown : 3. markdown
|
||||
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)
|
||||
|
||||
Just use the "embedded" export on Youtube with dimensions of 535x300 for best results
|
||||
|
||||
<iframe width="535" height="300" src="https://www.youtube.com/embed/FTQbiNvZqaY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
@@ -1,9 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Error <%= error %></title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - Error <%= error %></title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<hr>
|
||||
<footer>
|
||||
<small>@<%= new Date().getFullYear() %> - Made with <a href="https://github.com/klemek/gitblog.md">GitBlog.md</a>
|
||||
<small><a href="/rss">RSS feed</a> - <%= new Date().getFullYear() %> - Made with <a
|
||||
href="https://github.com/klemek/gitblog.md">GitBlog.md</a> (v<%= info.version %>)
|
||||
</small>
|
||||
</footer>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
|
||||
<META NAME="ROBOTS" CONTENT="INDEX, FOLLOW">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<%- `<meta property="og:description" content="${info.description}">` %>
|
||||
<%- `<meta property="twitter:description" content="${info.description}">` %>
|
||||
<% if(locals.article){ %>
|
||||
<%- `<meta property="org:url" content="${info.host + article.url}">` %>
|
||||
<%- `<meta property="og:title" content="${info.title} - ${article.title}">` %>
|
||||
<%- `<meta property="twitter:title" content="${info.title} - ${article.title}">` %>
|
||||
<% if (article.thumbnail) { %>
|
||||
<%- `<meta property="og:image" content="${info.host}/${article.thumbnail}">` %>
|
||||
<%- `<meta property="twitter:image" content="${info.host}/${article.thumbnail}">` %>
|
||||
<% } %>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/prism.css">
|
||||
<% } else { %>
|
||||
<%- `<meta property="org:url" content="${info.host}/">` %>
|
||||
<%- `<meta property="og:title" content="${info.title} - Home">` %>
|
||||
<%- `<meta property="twitter:title" content="${info.title} - Home">` %>
|
||||
|
||||
<%- `<meta property="description" content="${info.description}">` %>
|
||||
<% } %>
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS feed" href="/rss"/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
@@ -1,22 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GitBlog.md - Home</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>GitBlog.md</h1>
|
||||
A static blog using Markdown pulled from your git repository
|
||||
<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'); %>
|
||||
|
||||
@@ -8,7 +8,7 @@ body, html {
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px/1.45 -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif;
|
||||
font: 15px sans-serif;
|
||||
color: #111;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #F5F5F5;
|
||||
@@ -16,8 +16,8 @@ body {
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 75ch;
|
||||
padding: 2ch;
|
||||
max-width: 45rem;
|
||||
padding: 2rem;
|
||||
margin: auto;
|
||||
background-color: #F0F0F0;
|
||||
min-height: 100vh;
|
||||
@@ -54,11 +54,18 @@ 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;
|
||||
margin: 0.25em 0;
|
||||
color: #333;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
blockquote > p {
|
||||
@@ -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, div.article h3 {
|
||||
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 {
|
||||
@@ -123,13 +131,36 @@ main.article div.header span.time, div.article span.time {
|
||||
}
|
||||
|
||||
div.article {
|
||||
margin-left: 1em;
|
||||
margin: 0 1em 1em 1em;
|
||||
}
|
||||
|
||||
div.article h3 {
|
||||
font-size: 1.3em;
|
||||
margin:0;
|
||||
color: #3C3CA1;
|
||||
}
|
||||
|
||||
div.article a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
div.article img{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-right:1em;
|
||||
margin-top:0.25em;
|
||||
}
|
||||
|
||||
div.article:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
div.article:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
#text {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
@@ -139,7 +170,7 @@ div.article h3 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#text img {
|
||||
#text img, #text svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GitBlog.md - <%= article.title %></title>
|
||||
<link rel="stylesheet" type="text/css" href="/prism.css">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<%- include('head'); %>
|
||||
<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>
|
||||
@@ -19,4 +17,4 @@
|
||||
<%- include('footer'); %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+341
-224
@@ -2,6 +2,8 @@ const express = require('express');
|
||||
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');
|
||||
|
||||
@@ -19,244 +21,359 @@ app.use(bodyParser.json());
|
||||
* @type {{warn: string, ok: string, error: string}}
|
||||
*/
|
||||
const cons = {
|
||||
ok: '\x1b[32m✔\x1b[0m %s',
|
||||
warn: '\x1b[33m⚠\x1b[0m %s',
|
||||
error: '\x1b[31m✘\x1b[0m %s',
|
||||
ok: '\x1b[32m✔\x1b[0m %s',
|
||||
warn: '\x1b[33m⚠\x1b[0m %s',
|
||||
error: '\x1b[31m✘\x1b[0m %s',
|
||||
};
|
||||
|
||||
module.exports = (config) => {
|
||||
const fw = require('./file_walker')(config);
|
||||
const renderer = require('./renderer')(config);
|
||||
|
||||
// set view engine from configuration
|
||||
app.set('view engine', config['view_engine']);
|
||||
// reroute the views folder to the root folder
|
||||
app.set('views', path.join(__dirname, '..'));
|
||||
|
||||
const articles = {};
|
||||
let lastRSS = '';
|
||||
|
||||
/**
|
||||
* Fetch articles from the data folder and send success as a response
|
||||
* @param success
|
||||
* @param error
|
||||
*/
|
||||
const reload = (success, error) => {
|
||||
fw.fetchArticles((err, dict) => {
|
||||
if (err) {
|
||||
console.error(cons.error, 'error loading articles : ' + err);
|
||||
return error ? error() : null;
|
||||
}
|
||||
Object.keys(articles).forEach((key) => delete articles[key]);
|
||||
Object.keys(dict).forEach((key) => articles[key] = dict[key]);
|
||||
const nb = Object.keys(articles).length;
|
||||
if (nb > 0)
|
||||
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''}`);
|
||||
else
|
||||
console.log(cons.warn, `no articles loaded, check your configuration`);
|
||||
|
||||
lastRSS = '';
|
||||
|
||||
success();
|
||||
});
|
||||
};
|
||||
if (config['test'])
|
||||
app.reload = reload;
|
||||
|
||||
/**
|
||||
* Render the page with the view engine and catch errors
|
||||
* @param res
|
||||
* @param vPath - path of the view
|
||||
* @param data - data to pass to the view
|
||||
* @param code - code to send along the page
|
||||
*/
|
||||
const render = (res, vPath, data, code = 200) => {
|
||||
res.render(vPath, data, (err, html) => {
|
||||
if (err) {
|
||||
res.sendStatus(500);
|
||||
console.log(cons.error, `failed to render ${vPath} : ${err}`);
|
||||
} else
|
||||
res.status(code).send(html);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Show an error with the correct page
|
||||
* @param resPath - the page of the original error
|
||||
* @param code - error code
|
||||
* @param res
|
||||
*/
|
||||
const showError = (resPath, code, res) => {
|
||||
const errorPath = path.join(config['data_dir'], config['home']['error']);
|
||||
fs.access(errorPath, fs.constants.R_OK, (err) => {
|
||||
if (err)
|
||||
res.sendStatus(code);
|
||||
else
|
||||
render(res, errorPath, {error: code, path: resPath}, code);
|
||||
});
|
||||
};
|
||||
|
||||
//log request at result end
|
||||
app.use((req, res, next) => {
|
||||
if (config['access_log']) {
|
||||
const end = res.end;
|
||||
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'}, () => {
|
||||
res.end = end;
|
||||
res.end(chunk, encoding);
|
||||
});
|
||||
};
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// home endpoint : send the correct index page or error if not existing
|
||||
app.get('/', (req, res) => {
|
||||
const homePath = path.join(config['data_dir'], config['home']['index']);
|
||||
fs.access(homePath, fs.constants.R_OK, (err) => {
|
||||
if (err)
|
||||
showError(req.path, 404, res);
|
||||
else
|
||||
render(res, homePath, {articles: Object.values(articles).sort((a, b) => ('' + b.path).localeCompare(a.path))});
|
||||
});
|
||||
});
|
||||
|
||||
//RSS endpoint
|
||||
app.get(config['rss']['endpoint'], (req, res) => {
|
||||
if (config['modules']['rss']) {
|
||||
if (!lastRSS) {
|
||||
const feed = new Rss({
|
||||
'title': config['rss']['title'],
|
||||
'description': config['rss']['description'],
|
||||
'feed_url': 'http://' + req.headers.host + req.url,
|
||||
'site_url': 'http://' + req.headers.host
|
||||
});
|
||||
Object.values(articles)
|
||||
.slice(0, config['rss']['length'])
|
||||
.forEach((article) => {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
url: 'http://' + req.headers.host + article.url,
|
||||
date: article.date
|
||||
});
|
||||
});
|
||||
lastRSS = feed.xml();
|
||||
}
|
||||
res.type('rss').send(lastRSS);
|
||||
} else {
|
||||
showError(req.path, 404, res);
|
||||
}
|
||||
});
|
||||
|
||||
//webhook endpoint
|
||||
app.post(config['webhook']['endpoint'], (req, res) => {
|
||||
if (config['modules']['webhook']) {
|
||||
if (config['webhook']['signature_header'] && config['webhook']['secret']) {
|
||||
const payload = JSON.stringify(req.body);
|
||||
if (!payload) {
|
||||
return res.sendStatus(403);
|
||||
/**
|
||||
* Fetch articles from the data folder and send success as a response
|
||||
* @param success
|
||||
* @param error
|
||||
*/
|
||||
let reload;
|
||||
/**
|
||||
* Render the page with the view engine and catch errors
|
||||
* @param req
|
||||
* @param res
|
||||
* @param vPath - path of the view
|
||||
* @param data - data to pass to the view
|
||||
* @param code - code to send along the page
|
||||
*/
|
||||
let render;
|
||||
/**
|
||||
* Show an error with the correct page
|
||||
* @param req
|
||||
* @param res
|
||||
* @param code - error code
|
||||
*/
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
reload(() => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//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))
|
||||
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
|
||||
next();
|
||||
});
|
||||
// set view engine from configuration
|
||||
app.set('view engine', config['view_engine']);
|
||||
// reroute the views folder to the root folder
|
||||
app.set('views', path.join(__dirname, '..'));
|
||||
|
||||
// catch all article urls and render them
|
||||
app.get('*', (req, res, next) => {
|
||||
if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) {
|
||||
const articlePath = req.path.substr(1, 10);
|
||||
const article = articles[articlePath];
|
||||
if (!article)
|
||||
showError(req.path, 404, res);
|
||||
else {
|
||||
renderer.render(path.join(article.realPath, config['article']['index']), (err, html) => {
|
||||
if (err) {
|
||||
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
|
||||
return showError(req.path, 500, res);
|
||||
}
|
||||
article.content = html;
|
||||
const templatePath = path.join(config['data_dir'], config['article']['template']);
|
||||
fs.access(templatePath, fs.constants.R_OK, (err) => {
|
||||
const articles = {};
|
||||
let lastRSS = '';
|
||||
let host = config['host'];
|
||||
|
||||
reload = (success, error) => {
|
||||
fw.fetchArticles((err, dict) => {
|
||||
if (err) {
|
||||
console.log(cons.error, `no template found at ${templatePath}`);
|
||||
showError(req.path, 500, res);
|
||||
} else
|
||||
render(res, templatePath, {article: article});
|
||||
});
|
||||
console.error(cons.error, 'error loading articles : ' + err);
|
||||
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) {
|
||||
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
|
||||
} else {
|
||||
console.log(cons.warn, 'no articles loaded, check your configuration');
|
||||
}
|
||||
|
||||
lastRSS = '';
|
||||
|
||||
success();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
};
|
||||
if (config['test']) {
|
||||
app.reload = reload;
|
||||
}
|
||||
});
|
||||
|
||||
// catch all hidden file type and return 404
|
||||
app.get('*', (req, res, next) => {
|
||||
if (config['home']['hidden'].includes(path.extname(req.path)))
|
||||
showError(req.path, 404, res);
|
||||
else
|
||||
next();
|
||||
});
|
||||
render = (req, res, vPath, data, code = 200) => {
|
||||
data.info = {
|
||||
title: config['home']['title'],
|
||||
description: config['home']['description'],
|
||||
host: host,
|
||||
version: pjson.version,
|
||||
request: req,
|
||||
config: config,
|
||||
};
|
||||
res.render(vPath, data, (err, html) => {
|
||||
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
|
||||
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
|
||||
showError(req, res, 500);
|
||||
} else if (err) {
|
||||
res.sendStatus(500);
|
||||
console.log(cons.error, `failed to render error page : ${err}`);
|
||||
} else {
|
||||
res.status(code).send(html);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// serve all static files via get
|
||||
app.get('*', express.static(config['data_dir']));
|
||||
// catch express.static errors (mostly not found) by displaying 404
|
||||
app.get('*', (req, res) => {
|
||||
showError(req.path, 404, res);
|
||||
});
|
||||
showError = (req, res, code) => {
|
||||
const errorPath = path.join(config['data_dir'], config['home']['error']);
|
||||
fs.access(errorPath, fs.constants.R_OK, (err) => {
|
||||
if (err) {
|
||||
res.sendStatus(code);
|
||||
} else {
|
||||
render(req, res, errorPath, { error: code }, code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// catch all other methods and return 400
|
||||
app.all('*', (req, res) => {
|
||||
res.status(400).send('bad request');
|
||||
});
|
||||
|
||||
//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'])
|
||||
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'}, () => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
// must be use in a server.js to start the server
|
||||
app.start = () => {
|
||||
reload(() => {
|
||||
app.listen(config['node_port'], () => {
|
||||
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
|
||||
});
|
||||
app.use((req, res, next) => {
|
||||
if (!host) {
|
||||
host = 'http://' + req.headers.host;
|
||||
console.log(cons.ok, 'Currently hosted on ' + host);
|
||||
}
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
return app;
|
||||
//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']) {
|
||||
const end = res.end;
|
||||
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' }, () => {
|
||||
res.end = end;
|
||||
res.end(chunk, encoding);
|
||||
});
|
||||
};
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// home endpoint : send the correct index page or error if not existing
|
||||
app.get('/', (req, res) => {
|
||||
const homePath = path.join(config['data_dir'], config['home']['index']);
|
||||
fs.access(homePath, fs.constants.R_OK, (err) => {
|
||||
if (err) {
|
||||
showError(req, res, 404);
|
||||
} 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)),
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
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
|
||||
app.get(config['rss']['endpoint'], (req, res) => {
|
||||
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,
|
||||
});
|
||||
Object.values(articles)
|
||||
.slice(0, config['rss']['length'])
|
||||
.forEach((article) => {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
url: host + article.url,
|
||||
date: article.date,
|
||||
});
|
||||
});
|
||||
lastRSS = feed.xml();
|
||||
}
|
||||
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
|
||||
} else {
|
||||
showError(req, res, 404);
|
||||
}
|
||||
});
|
||||
|
||||
//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) {
|
||||
res.sendStatus(403);
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
cp.exec(config['webhook']['pull_command'], { cwd: path.join(__dirname, '..', config['data_dir']) }, (err) => {
|
||||
if (err) {
|
||||
console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`);
|
||||
res.sendStatus(500);
|
||||
} else {
|
||||
reload(() => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.sendStatus(400);
|
||||
}
|
||||
});
|
||||
|
||||
//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)) {
|
||||
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}\/(stats)?$/.test(req.path)) {
|
||||
const articlePath = req.path.substr(1, 10);
|
||||
const article = articles[articlePath];
|
||||
if (!article) {
|
||||
showError(req, res, 404);
|
||||
} 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}`);
|
||||
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 {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// catch all hidden file type and return 404
|
||||
config['home']['hidden'].forEach(pathMatcher => {
|
||||
app.get(pathMatcher, (req, res) => {
|
||||
showError(req, res, 404);
|
||||
});
|
||||
});
|
||||
|
||||
// serve all static files via get
|
||||
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
|
||||
// catch express.static errors (mostly not found) by displaying 404
|
||||
app.get('*', (req, res) => {
|
||||
showError(req, res, 404);
|
||||
});
|
||||
|
||||
// catch all other methods and return 400
|
||||
app.all('*', (req, res) => {
|
||||
res.status(400).send('bad request');
|
||||
});
|
||||
|
||||
//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']) {
|
||||
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' }, () => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
// must be use in a server.js to start the server
|
||||
app.start = () => {
|
||||
reload(() => {
|
||||
app.listen(config['node_port'], () => {
|
||||
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
+32
-4
@@ -1,23 +1,33 @@
|
||||
{
|
||||
"node_port": 3000,
|
||||
"host": "",
|
||||
"data_dir": "data",
|
||||
"view_engine": "ejs",
|
||||
"rate_limit": 100,
|
||||
"access_log": "access.log",
|
||||
"error_log": "error.log",
|
||||
"modules": {
|
||||
"rss": true,
|
||||
"webhook": true,
|
||||
"prism": true
|
||||
"prism": true,
|
||||
"mathjax": true,
|
||||
"plantuml": true,
|
||||
"fa-diagrams": true,
|
||||
"hit_counter": true
|
||||
},
|
||||
"home": {
|
||||
"title": "GitBlog.md",
|
||||
"description": "A static blog using Markdown pulled from your git repository",
|
||||
"index": "index.ejs",
|
||||
"error": "error.ejs",
|
||||
"hidden": [
|
||||
".ejs"
|
||||
"*.ejs",
|
||||
"/.git*"
|
||||
]
|
||||
},
|
||||
"article": {
|
||||
"index": "index.md",
|
||||
"draft": "draft.md",
|
||||
"template": "template.ejs",
|
||||
"thumbnail_tag": "thumbnail",
|
||||
"default_title": "Untitled",
|
||||
@@ -33,7 +43,7 @@
|
||||
"endpoint": "/webhook",
|
||||
"secret": "",
|
||||
"signature_header": "",
|
||||
"pull_command": "git pull"
|
||||
"pull_command": "git pull origin master"
|
||||
},
|
||||
"showdown": {
|
||||
"parseImgDimensions": true,
|
||||
@@ -42,5 +52,23 @@
|
||||
"tasklists": true,
|
||||
"openLinksInNewWindow": true,
|
||||
"emoji": true
|
||||
},
|
||||
"mathjax": {
|
||||
"output_format": "svg",
|
||||
"speak_text": true
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-18
@@ -8,24 +8,28 @@ const fs = require('fs');
|
||||
* @returns {*}
|
||||
*/
|
||||
const merge = (ref, src) => {
|
||||
if (typeof ref !== typeof src) {
|
||||
return ref;
|
||||
} else if (typeof ref === 'object') {
|
||||
const out = {};
|
||||
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
|
||||
return out;
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
if (typeof ref !== typeof src) {
|
||||
return ref;
|
||||
} else if (ref.length && !src.length) {
|
||||
return ref;
|
||||
} else if (ref.length && src.length) {
|
||||
return src;
|
||||
} else if (typeof ref === 'object') {
|
||||
const out = {};
|
||||
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
|
||||
return out;
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
try {
|
||||
let configData = fs.readFileSync('config.json', {encoding: 'UTF-8'});
|
||||
let config = JSON.parse(configData);
|
||||
return merge(refConfig, config);
|
||||
} catch (error) {
|
||||
console.error('Failed to load config.json : ' + error);
|
||||
return refConfig;
|
||||
}
|
||||
};
|
||||
try {
|
||||
let configData = fs.readFileSync('config.json', { encoding: 'UTF-8' });
|
||||
let config = JSON.parse(configData);
|
||||
return merge(refConfig, config);
|
||||
} catch (error) {
|
||||
console.log('\x1b[33m⚠\x1b[0m %s', 'Failed to load config.json : ' + error);
|
||||
return refConfig;
|
||||
}
|
||||
};
|
||||
|
||||
+100
-83
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/');
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
|
||||
/**
|
||||
* Get all files path inside a given folder path
|
||||
@@ -9,29 +9,35 @@ const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/');
|
||||
* @param cb
|
||||
*/
|
||||
const getFileTree = (dir, cb) => {
|
||||
let list = [];
|
||||
let remaining = 0;
|
||||
fs.readdir(dir, {withFileTypes: true}, (err, items) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
items.forEach((item) => {
|
||||
if (item.isDirectory()) {
|
||||
remaining++;
|
||||
getFileTree(path.join(dir, item.name), (err, out) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
list.push(...out);
|
||||
remaining--;
|
||||
if (remaining === 0)
|
||||
cb(null, list);
|
||||
});
|
||||
} else {
|
||||
list.push(path.join(dir, item.name));
|
||||
}
|
||||
let list = [];
|
||||
let remaining = 0;
|
||||
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) {
|
||||
cb(err);
|
||||
} else {
|
||||
list.push(...out);
|
||||
remaining--;
|
||||
if (remaining === 0) {
|
||||
cb(null, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
list.push(path.join(dir, item.name));
|
||||
}
|
||||
});
|
||||
if (remaining === 0) {
|
||||
cb(null, list);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (remaining === 0)
|
||||
cb(null, list);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -41,69 +47,80 @@ 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 = {};
|
||||
|
||||
let info = {};
|
||||
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
|
||||
info.title = regRes1 ? regRes1[2].trim() : undefined;
|
||||
|
||||
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
|
||||
info.title = regRes1 ? regRes1[2].trim() : undefined;
|
||||
const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i');
|
||||
const regRes2 = data.match(thumbnailRegEx);
|
||||
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
|
||||
|
||||
const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i');
|
||||
const regRes2 = data.match(thumbnailRegEx);
|
||||
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
|
||||
|
||||
cb(null, info);
|
||||
});
|
||||
cb(null, info);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (config) => {
|
||||
return {
|
||||
fileTree: config['test'] ? getFileTree : undefined,
|
||||
readIndexFile: config['test'] ? readIndexFile : undefined,
|
||||
/**
|
||||
* find and read all articles inside the data directory
|
||||
* @param cb
|
||||
*/
|
||||
fetchArticles: (cb) => {
|
||||
getFileTree(config['data_dir'], (err, fileList) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
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'] &&
|
||||
/^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2]));
|
||||
if (paths.length === 0)
|
||||
cb(null, {});
|
||||
const articles = {};
|
||||
let remaining = 0;
|
||||
paths.forEach((p) => {
|
||||
const article = {
|
||||
path: joinUrl(p[0], p[1], p[2]),
|
||||
realPath: path.join(config['data_dir'], p[0], p[1], p[2]),
|
||||
year: parseInt(p[0]),
|
||||
month: parseInt(p[1]),
|
||||
day: parseInt(p[2])
|
||||
};
|
||||
article.date = new Date(article.year, article.month, article.day);
|
||||
article.date.setUTCHours(0);
|
||||
remaining++;
|
||||
readIndexFile(path.join(article.realPath, config['article']['index']), config['article']['thumbnail_tag'], (err, info) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
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.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
|
||||
articles[article.path] = article;
|
||||
remaining--;
|
||||
if (remaining === 0)
|
||||
cb(null, articles);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
return {
|
||||
fileTree: config['test'] ? getFileTree : undefined,
|
||||
readIndexFile: config['test'] ? readIndexFile : undefined,
|
||||
/**
|
||||
* find and read all articles inside the data directory
|
||||
* @param cb
|
||||
*/
|
||||
fetchArticles: (cb) => {
|
||||
getFileTree(config['data_dir'], (err, fileList) => {
|
||||
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) {
|
||||
cb(null, {});
|
||||
}
|
||||
const articles = {};
|
||||
let remaining = 0;
|
||||
paths.forEach((p) => {
|
||||
const article = {
|
||||
path: joinUrl(p[0], p[1], p[2]),
|
||||
draft: p[3] === config['article']['draft'],
|
||||
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]),
|
||||
};
|
||||
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) {
|
||||
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.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
|
||||
if (!articles[article.path] || !article.draft) {
|
||||
articles[article.path] = article;
|
||||
}
|
||||
remaining--;
|
||||
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,
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
+17
-15
@@ -3,28 +3,30 @@ const path = require('path');
|
||||
const ncp = require('ncp').ncp;
|
||||
|
||||
const copy = (src, dest) => {
|
||||
ncp(src, dest, function (err) {
|
||||
if (err)
|
||||
console.error(err);
|
||||
else
|
||||
console.log(`copied ${src} to ${dest}`);
|
||||
});
|
||||
ncp(src, dest, function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log(`copied ${src} to ${dest}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
copy(path.join('src', 'config.default.json'), 'config.example.json');
|
||||
|
||||
if (!fs.existsSync('data')) {
|
||||
fs.mkdirSync('data');
|
||||
fs.mkdirSync('data');
|
||||
|
||||
copy(path.join('sample_data', 'home'), 'data');
|
||||
copy(path.join('sample_data', 'home'), 'data');
|
||||
|
||||
const pad0 = (n) => ('0' + n).substr(-2);
|
||||
const pad0 = (n) => ('0' + n).substr(-2);
|
||||
|
||||
const datetime = new Date();
|
||||
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
|
||||
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);
|
||||
}
|
||||
copy(path.join('sample_data', 'article'), dir);
|
||||
}
|
||||
|
||||
+235
-24
@@ -1,37 +1,248 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const showdown = require('showdown');
|
||||
const Prism = require('node-prismjs');
|
||||
|
||||
module.exports = (config) => {
|
||||
const converter = new showdown.Converter(config['showdown']);
|
||||
return {
|
||||
render: (file, cb) => {
|
||||
fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
const converter = new showdown.Converter(config['showdown']);
|
||||
|
||||
if (config['modules']['prism']) {
|
||||
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
|
||||
let match;
|
||||
while ((match = codeRegex.exec(data))) {
|
||||
const lang = match[1].trim();
|
||||
const code = match[2].trim();
|
||||
try {
|
||||
const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang);
|
||||
data = data.slice(0, match.index) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
|
||||
cb(null, html);
|
||||
});
|
||||
let Prism;
|
||||
if (config['modules']['prism']) {
|
||||
Prism = require('node-prismjs');
|
||||
}
|
||||
};
|
||||
|
||||
const renderPrism = (data, cb) => {
|
||||
if (!config['modules']['prism']) {
|
||||
cb(data);
|
||||
} else {
|
||||
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
|
||||
let match;
|
||||
while ((match = codeRegex.exec(data))) {
|
||||
const lang = match[1].trim();
|
||||
const code = match[2].trim();
|
||||
const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang);
|
||||
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']) {
|
||||
require('./script_loader')(path.join(__dirname, 'lib', 'plantuml_synchro.js'));
|
||||
}
|
||||
|
||||
const renderPlantUML = (data, cb) => {
|
||||
/* global encode64 */
|
||||
if (!config['modules']['plantuml']) {
|
||||
cb(data);
|
||||
} else {
|
||||
const parts = getParts(data);
|
||||
const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m;
|
||||
let match;
|
||||
parts.forEach(part => {
|
||||
while ((match = umlRegex.exec(part.text))) {
|
||||
const code = match[1].trim();
|
||||
const s = unescape(encodeURIComponent(code));
|
||||
const compressed = global['zip_deflate'](s);
|
||||
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;
|
||||
if (config['modules']['mathjax']) {
|
||||
mjAPI = require('mathjax-node');
|
||||
mjAPI.config({
|
||||
MathJax: {
|
||||
tex2jax: {
|
||||
inlineMath: [
|
||||
[
|
||||
'$',
|
||||
'$',
|
||||
],
|
||||
],
|
||||
displayMath: [
|
||||
[
|
||||
'$$',
|
||||
'$$',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const renderMathJax = (data, cb) => {
|
||||
if (!config['modules']['mathjax']) {
|
||||
cb(data);
|
||||
} else {
|
||||
const parts = getParts(data);
|
||||
|
||||
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'],
|
||||
};
|
||||
mjConf[output] = true;
|
||||
mjAPI.typeset(mjConf, (res) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
|
||||
const inlineEqRegex = /\$([^$\n]*)\$/;
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let match;
|
||||
if ((match = eqRegex.exec(parts[i].text))) {
|
||||
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) {
|
||||
cb(err);
|
||||
} else {
|
||||
renderPlantUML(data, (data) => {
|
||||
renderFaDiagrams(data, (data) => {
|
||||
renderMathJax(data, (data) => {
|
||||
renderPrism(data, (data) => {
|
||||
renderShowDown(data, (html) => {
|
||||
cb(null, html);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Import client-side script into the "global" var
|
||||
* @param scriptPath
|
||||
*/
|
||||
module.exports = (scriptPath) => {
|
||||
eval.call(global, fs.readFileSync(scriptPath, { encoding: 'UTF-8' }));
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const config = require('./config')();
|
||||
const app = require('./app')(config);
|
||||
|
||||
app.start();
|
||||
app.start();
|
||||
|
||||
+551
-321
@@ -1,4 +1,3 @@
|
||||
/* jshint -W117 */
|
||||
const request = require('supertest');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -13,387 +12,618 @@ const config = require('../src/config')();
|
||||
|
||||
config['test'] = true;
|
||||
config['data_dir'] = dataDir;
|
||||
config['home']['index'] = testIndex;
|
||||
config['home']['error'] = testError;
|
||||
config['article']['template'] = testTemplate;
|
||||
config['home']['hidden'].push('.test');
|
||||
config['webhook']['endpoint'] = '/webhooktest';
|
||||
config['rss']['endpoint'] = '/rsstest';
|
||||
config['rss']['length'] = 2;
|
||||
config['webhook']['endpoint'] = '/webhooktest';
|
||||
config['access_log'] = path.join(dataDir, 'access.log');
|
||||
config['error_log'] = path.join(dataDir, 'error.log');
|
||||
config['home']['error'] = testError;
|
||||
config['article']['template'] = testTemplate;
|
||||
config['modules']['hit_counter'] = false;
|
||||
|
||||
const app = require('../src/app')(config);
|
||||
|
||||
beforeEach((done, fail) => {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
app.reload(done, fail);
|
||||
config['home']['index'] = testIndex;
|
||||
config['data_dir'] = dataDir;
|
||||
config['article']['index'] = 'index.md';
|
||||
config['access_log'] = '';
|
||||
config['error_log'] = '';
|
||||
config['modules']['rss'] = true;
|
||||
config['modules']['webhook'] = true;
|
||||
config['modules']['hit_counter'] = false;
|
||||
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
app.reload(done, fail);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Test reload', () => {
|
||||
test('reload fail', (done, fail) => {
|
||||
config['data_dir'] = '';
|
||||
app.reload(fail, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test request logging', () => {
|
||||
test('test no log', (done) => {
|
||||
const tmp = config['access_log'];
|
||||
config['access_log'] = '';
|
||||
request(app).get('/rsstest').then(() => {
|
||||
config['access_log'] = tmp;
|
||||
expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false);
|
||||
done();
|
||||
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) => {
|
||||
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('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) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBe('200 GET /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
test('test post 400', (done) => {
|
||||
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('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) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBe('400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
test('test 2 requests', (done) => {
|
||||
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' +
|
||||
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) => {
|
||||
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');
|
||||
done();
|
||||
});
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test error logging', () => {
|
||||
test('test no log', (done) => {
|
||||
const tmp = config['home']['hidden'];
|
||||
config['home']['hidden'] = null;
|
||||
const tmp2 = config['errpr_log'];
|
||||
config['error_log'] = '';
|
||||
request(app).get('/somefile.txt').then(() => {
|
||||
config['home']['hidden'] = tmp;
|
||||
config['error_log'] = tmp2;
|
||||
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
|
||||
done();
|
||||
test('no log', (done) => {
|
||||
config['home']['index'] = null;
|
||||
request(app).get('/')
|
||||
.then(() => {
|
||||
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('test null error ', (done) => {
|
||||
const tmp = config['home']['hidden'];
|
||||
config['home']['hidden'] = null;
|
||||
request(app).get('/somefile.txt').then(() => {
|
||||
config['home']['hidden'] = tmp;
|
||||
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 /somefile.txt ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError: Cannot read property \'includes\' of null';
|
||||
expect(start).toBe(expected);
|
||||
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) => {
|
||||
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.';
|
||||
expect(start.indexOf(expected)).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test root path', () => {
|
||||
test('404 no index no error', (done) => {
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
test('404 no index no error', (done) => {
|
||||
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 %> at <%= path %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404 at /');
|
||||
done();
|
||||
test('404 no index but error page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
|
||||
request(app).get('/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 no articles', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('articles 0');
|
||||
done();
|
||||
test('500 render error', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
request(app).get('/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
expect(response.text).toBe('error 500');
|
||||
done();
|
||||
});
|
||||
});
|
||||
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) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('articles 0');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 2 articles 1 drafted', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '05', '05'),
|
||||
path.join(dataDir, '2017', '05', '05'),
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'draft.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'draft.md'),
|
||||
path.join(dataDir, '2017', '05', '05', 'index.md'),
|
||||
]);
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('articles 2');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
test('200 2 articles', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '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')
|
||||
]);
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('articles 2');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test RSS feed', () => {
|
||||
test('404 rss deactivated', (done) => {
|
||||
config['modules']['rss'] = false;
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
config['modules']['rss'] = true;
|
||||
done();
|
||||
test('404 rss deactivated', (done) => {
|
||||
config['modules']['rss'] = false;
|
||||
request(app).get('/rsstest')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 empty rss', (done) => {
|
||||
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);
|
||||
done();
|
||||
test('200 empty rss', (done) => {
|
||||
request(app).get('/rsstest')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('application/rss+xml');
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 Mozilla fix', (done) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 2 rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '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'),
|
||||
]);
|
||||
app.reload(() => {
|
||||
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);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('200 max rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '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'),
|
||||
]);
|
||||
app.reload(() => {
|
||||
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);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
test('200 2 rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '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')
|
||||
]);
|
||||
app.reload(() => {
|
||||
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);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('200 max rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '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')
|
||||
]);
|
||||
app.reload(() => {
|
||||
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);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test webhook', () => {
|
||||
test('400 webhook deactivated', (done) => {
|
||||
config['modules']['webhook'] = false;
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
config['modules']['webhook'] = true;
|
||||
done();
|
||||
test('400 webhook deactivated', (done) => {
|
||||
config['modules']['webhook'] = false;
|
||||
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.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate)
|
||||
]);
|
||||
config['webhook']['pull_command'] = 'git --help';
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
request(app).get('/2019/05/05/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
test('200 no secret', (done) => {
|
||||
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate),
|
||||
]);
|
||||
config['webhook']['pull_command'] = 'git --help';
|
||||
request(app).post('/webhooktest')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
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.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate)
|
||||
]);
|
||||
config['webhook']['pull_command'] = 'qzgfqgqz';
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
test('500 command failed', (done) => {
|
||||
utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05') ]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate),
|
||||
]);
|
||||
config['webhook']['pull_command'] = 'qzgfqgqz';
|
||||
request(app).post('/webhooktest')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('403 no payload', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
test('403 wrong secret', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
request(app).post('/webhooktest')
|
||||
.set('testheader', 'sha1=invalid')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('403 wrong secret', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
request(app).post('/webhooktest').set('testheader', 'sha1=invalid').then((response) => {
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
test('200 valid secret', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
config['webhook']['pull_command'] = 'git --help';
|
||||
request(app).post('/webhooktest')
|
||||
.send({})
|
||||
.set('testheader', 'sha1=d924d5bd4b36faf9d572844ac9c12a09ce3e7134')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 valid secret', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
config['webhook']['pull_command'] = 'git --help';
|
||||
request(app).post('/webhooktest')
|
||||
.send({})
|
||||
.set('testheader', 'sha1=d924d5bd4b36faf9d572844ac9c12a09ce3e7134')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test articles rendering', () => {
|
||||
test('404 article not found', (done) => {
|
||||
request(app).get('/2019/05/06/untitled/').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
test('404 article not found', (done) => {
|
||||
request(app).get('/2019/05/06/untitled/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('500 no template', (done, fail) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('500 fail to render', (done, fail) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 rendered article', (done, fail) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('500 no template', (done, fail) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url', (done, fail) => {
|
||||
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/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('200 rendered article', (done, fail) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url 2', (done, fail) => {
|
||||
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/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('200 rendered draft', (done, fail) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url', (done, fail) => {
|
||||
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/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url 2', (done, fail) => {
|
||||
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/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Test static files', () => {
|
||||
test('404 invalid file no error page', (done) => {
|
||||
request(app).get('/somefile.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
test('404 invalid file no error page', (done) => {
|
||||
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 %> at <%= path %>');
|
||||
request(app).get('/somefile.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404 at /somefile.txt');
|
||||
done();
|
||||
test('404 invalid file but error page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
|
||||
request(app).get('/somefile.txt')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('404 hidden file', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, 'somefile.test'), '');
|
||||
request(app).get('/somefile.test').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
test('404 hidden file', (done) => {
|
||||
utils.createEmptyDirs([ path.join(dataDir, 'tmp') ]);
|
||||
fs.writeFileSync(path.join(dataDir, 'tmp', 'somefile.ejs'), '');
|
||||
request(app).get('/tmp/somefile.ejs')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 valid file', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, 'somefile.txt'), 'filecontent');
|
||||
request(app).get('/somefile.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
test('404 hidden folder', (done) => {
|
||||
utils.createEmptyDirs([ path.join(dataDir, '.git') ]);
|
||||
fs.writeFileSync(path.join(dataDir, '.git', 'file.txt'), '');
|
||||
request(app).get('/.git/file.txt')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 valid resource of article', (done) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
test('200 valid file', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, 'somefile.css'), 'filecontent');
|
||||
request(app).get('/somefile.css')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('text/css');
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 valid resource of article', (done) => {
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test other requests', () => {
|
||||
test('400 POST', (done) => {
|
||||
request(app).post('/').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
test('400 POST', (done) => {
|
||||
request(app).post('/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('400 PUT', (done) => {
|
||||
request(app).put('/').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
test('400 PUT', (done) => {
|
||||
request(app).put('/')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('400 DELETE', (done) => {
|
||||
request(app).delete('/').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
test('400 DELETE', (done) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+62
-42
@@ -1,4 +1,3 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -6,62 +5,83 @@ const configFile = 'config.json';
|
||||
const tmpConfigFile = 'config.temp.json';
|
||||
|
||||
beforeAll(() => {
|
||||
if (fs.existsSync(configFile)) {
|
||||
fs.renameSync(configFile, tmpConfigFile);
|
||||
}
|
||||
expect(fs.existsSync(configFile)).toBeFalsy();
|
||||
if (fs.existsSync(configFile)) {
|
||||
fs.renameSync(configFile, tmpConfigFile);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(tmpConfigFile)) {
|
||||
fs.renameSync(tmpConfigFile, configFile);
|
||||
} else if (fs.existsSync(configFile)) {
|
||||
fs.unlinkSync(configFile); //remove config file if remaining
|
||||
}
|
||||
if (fs.existsSync(tmpConfigFile)) {
|
||||
fs.renameSync(tmpConfigFile, configFile);
|
||||
} else if (fs.existsSync(configFile)) {
|
||||
fs.unlinkSync(configFile); //remove config file if remaining
|
||||
}
|
||||
});
|
||||
|
||||
test('no config', () => {
|
||||
if (fs.existsSync(configFile))
|
||||
fs.unlinkSync(configFile);
|
||||
expect(fs.existsSync(configFile)).toBeFalsy();
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
if (fs.existsSync(configFile)) {
|
||||
fs.unlinkSync(configFile);
|
||||
}
|
||||
expect(fs.existsSync(configFile)).toBeFalsy();
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('example config', () => {
|
||||
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 config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3333);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
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 config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3333);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('invalid config ignored', () => {
|
||||
fs.writeFileSync(configFile, 'invalid JSON');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
fs.writeFileSync(configFile, 'invalid JSON');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('good config merged', () => {
|
||||
fs.writeFileSync(configFile, '{"node_port":5000}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(5000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
fs.writeFileSync(configFile, '{"node_port":5000}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(5000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('wrong config fixed', () => {
|
||||
fs.writeFileSync(configFile, '{"node_port":"hello","data_dir":"data2"}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data2');
|
||||
});
|
||||
fs.writeFileSync(configFile, '{"node_port":"hello","data_dir":"data2"}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data2');
|
||||
});
|
||||
|
||||
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',
|
||||
]);
|
||||
});
|
||||
|
||||
test('array fix', () => {
|
||||
fs.writeFileSync(configFile, '{"home":{"hidden":{}}}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['home']['hidden']).toEqual([
|
||||
'*.ejs',
|
||||
'/.git*',
|
||||
]);
|
||||
});
|
||||
|
||||
+275
-211
@@ -1,4 +1,3 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
@@ -6,277 +5,342 @@ const utils = require('./test_utils');
|
||||
const dataDir = 'test_data';
|
||||
const testIndex = 'testindex.md';
|
||||
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/');
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
|
||||
const config = {
|
||||
'test': true,
|
||||
'data_dir': dataDir,
|
||||
'article': {
|
||||
'index': testIndex,
|
||||
'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);
|
||||
|
||||
beforeEach(() => {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
config['data_dir'] = dataDir;
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Test function fileTree', () => {
|
||||
test('empty root', (done) => {
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(0);
|
||||
done();
|
||||
test('empty root', (done) => {
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('empty folders', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
path.join(dataDir, 'test', 'test2'),
|
||||
path.join(dataDir, 'test2')
|
||||
]);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(0);
|
||||
done();
|
||||
test('empty folders', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
path.join(dataDir, 'test', 'test2'),
|
||||
path.join(dataDir, 'test2'),
|
||||
]);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('simple files', (done) => {
|
||||
const fileList = [
|
||||
path.join(dataDir, 'f1.txt'),
|
||||
path.join(dataDir, 'f2.txt')
|
||||
];
|
||||
utils.createEmptyFiles(fileList);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(fileList.length);
|
||||
expect(list).toEqual(expect.arrayContaining(fileList));
|
||||
done();
|
||||
test('simple files', (done) => {
|
||||
const fileList = [
|
||||
path.join(dataDir, 'f1.txt'),
|
||||
path.join(dataDir, 'f2.txt'),
|
||||
];
|
||||
utils.createEmptyFiles(fileList);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(fileList.length);
|
||||
expect(list).toEqual(expect.arrayContaining(fileList));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('nested files', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
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')
|
||||
];
|
||||
utils.createEmptyFiles(fileList);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(fileList.length);
|
||||
expect(list).toEqual(expect.arrayContaining(fileList));
|
||||
done();
|
||||
test('nested files', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
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'),
|
||||
];
|
||||
utils.createEmptyFiles(fileList);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(fileList.length);
|
||||
expect(list).toEqual(expect.arrayContaining(fileList));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('invalid root', (done) => {
|
||||
fw.fileTree('invalid root', (err, list) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(list).not.toBeDefined();
|
||||
done();
|
||||
test('invalid root', (done) => {
|
||||
fw.fileTree('invalid root', (err, list) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(list).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test index article reading', () => {
|
||||
const file = path.join(dataDir, testIndex);
|
||||
const file = path.join(dataDir, testIndex);
|
||||
|
||||
test('invalid file', (done) => {
|
||||
fw.readIndexFile('invalid file', 'thumbnail', (err, info) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(info).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
test('invalid file', (done) => {
|
||||
fw.readIndexFile('invalid file', 'thumbnail', (err, info) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(info).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('correct file', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
test('correct file', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
# This is an awesome title !?¤
|
||||

|
||||
this is some text
|
||||
`);
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'This is an awesome title !?¤',
|
||||
thumbnail: './thumbnail.jpg'
|
||||
});
|
||||
done();
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'This is an awesome title !?¤',
|
||||
thumbnail: './thumbnail.jpg',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('no title', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
test('no title', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
## This is an awesome title !?¤
|
||||

|
||||
### this is some text
|
||||
`);
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: undefined,
|
||||
thumbnail: './thumbnail.jpg'
|
||||
});
|
||||
done();
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: undefined,
|
||||
thumbnail: './thumbnail.jpg',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('title at beginning', (done) => {
|
||||
fs.writeFileSync(file, '#title');
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'title',
|
||||
thumbnail: undefined
|
||||
});
|
||||
done();
|
||||
test('title at beginning', (done) => {
|
||||
fs.writeFileSync(file, '#title');
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'title',
|
||||
thumbnail: undefined,
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('no thumbnail', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
test('no thumbnail', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
# This is an awesome title !?¤
|
||||

|
||||
this is some text
|
||||
`);
|
||||
fw.readIndexFile(file, 'thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'This is an awesome title !?¤',
|
||||
thumbnail: undefined
|
||||
});
|
||||
done();
|
||||
fw.readIndexFile(file, 'thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'This is an awesome title !?¤',
|
||||
thumbnail: undefined,
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('multiple thumbnails', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
test('multiple thumbnails', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
# This is an awesome title !?¤
|
||||

|
||||
this is some text
|
||||

|
||||
`);
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'This is an awesome title !?¤',
|
||||
thumbnail: './thumbnail.jpg'
|
||||
});
|
||||
done();
|
||||
fw.readIndexFile(file, 'custom_thumbnail', (err, info) => {
|
||||
expect(err).toBeNull();
|
||||
expect(info).toEqual({
|
||||
title: 'This is an awesome title !?¤',
|
||||
thumbnail: './thumbnail.jpg',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test article fetching', () => {
|
||||
test('invalid data dir', (done) => {
|
||||
config['data_dir'] = 'invalid root';
|
||||
fw.fetchArticles((err, list) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(list).not.toBeDefined();
|
||||
config['data_dir'] = dataDir;
|
||||
done();
|
||||
test('invalid data dir', (done) => {
|
||||
config['data_dir'] = 'invalid root';
|
||||
fw.fetchArticles((err, list) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(list).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('empty data dir', (done) => {
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(0);
|
||||
done();
|
||||
test('empty data dir', (done) => {
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('misplaced index file', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
path.join(dataDir, '2019', '05', '05')
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, testIndex),
|
||||
path.join(dataDir, 'test', 'test', testIndex),
|
||||
path.join(dataDir, '2019', '05', testIndex)
|
||||
]);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(0);
|
||||
done();
|
||||
test('misplaced index file', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, testIndex),
|
||||
path.join(dataDir, 'test', 'test', testIndex),
|
||||
path.join(dataDir, '2019', '05', testIndex),
|
||||
]);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
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]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: dir,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Untitled',
|
||||
thumbnail: 'default.png',
|
||||
escapedTitle: 'untitled',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
|
||||
});
|
||||
done();
|
||||
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 ]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
draft: false,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Untitled',
|
||||
thumbnail: 'default.png',
|
||||
escapedTitle: 'untitled',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('correct index file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
utils.createEmptyDirs([dir]);
|
||||
fs.writeFileSync(file, `
|
||||
test('correct index file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
utils.createEmptyDirs([ dir ]);
|
||||
fs.writeFileSync(file, `
|
||||
# Title with : info !
|
||||

|
||||
this is some text
|
||||
`);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: dir,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Title with : info !',
|
||||
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
|
||||
escapedTitle: 'title_with___info',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
|
||||
});
|
||||
done();
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
draft: false,
|
||||
date: date,
|
||||
title: 'Title with : info !',
|
||||
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
|
||||
escapedTitle: 'title_with___info',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('correct draft file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, 'draft.md');
|
||||
utils.createEmptyDirs([ dir ]);
|
||||
fs.writeFileSync(file, `
|
||||
# Title with : info !
|
||||

|
||||
this is some text
|
||||
`);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
draft: true,
|
||||
date: date,
|
||||
title: 'Title with : info !',
|
||||
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
|
||||
escapedTitle: 'title_with___info',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('index file override draft', (done) => {
|
||||
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,
|
||||
]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
draft: false,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Untitled',
|
||||
thumbnail: 'default.png',
|
||||
escapedTitle: 'untitled',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+256
-66
@@ -1,4 +1,3 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
@@ -7,90 +6,281 @@ const dataDir = 'test_data';
|
||||
const file = path.join(dataDir, 'test.md');
|
||||
|
||||
const config = {
|
||||
'modules': {
|
||||
'prism': true,
|
||||
},
|
||||
'showdown': {
|
||||
'simplifiedAutoLink': true,
|
||||
'smartIndentationFix': true
|
||||
}
|
||||
test: true,
|
||||
modules: {
|
||||
'prism': true,
|
||||
'mathjax': true,
|
||||
'plantuml': true,
|
||||
'fa-diagrams': true,
|
||||
},
|
||||
showdown: {
|
||||
simplifiedAutoLink: true,
|
||||
smartIndentationFix: true,
|
||||
},
|
||||
mathjax: {
|
||||
output_format: 'html',
|
||||
speak_text: false,
|
||||
},
|
||||
plantuml: {
|
||||
output_format: 'svg',
|
||||
},
|
||||
};
|
||||
|
||||
const renderer = require('../src/renderer')(config);
|
||||
|
||||
beforeEach(() => {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
config['modules']['prism'] = true;
|
||||
config['modules']['mathjax'] = true;
|
||||
config['modules']['plantuml'] = true;
|
||||
config['modules']['fa-diagrams'] = true;
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('invalid file', (done) => {
|
||||
renderer.render('invalid file', (err, html) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(html).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('normal file', (done) => {
|
||||
fs.writeFileSync(file, `# Hello`);
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<h1 id="hello">Hello</h1>');
|
||||
done();
|
||||
});
|
||||
describe('Test Showdown', () => {
|
||||
test('normal', (done) => {
|
||||
renderer.renderShowDown('# Hello', (html) => {
|
||||
expect(html).toBe('<h1 id="hello">Hello</h1>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('custom rules', (done) => {
|
||||
renderer.renderShowDown('www.google.com', (html) => {
|
||||
expect(html).toBe('<p><a href="http://www.google.com">www.google.com</a></p>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('code format', (done) => {
|
||||
renderer.renderShowDown('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (html) => {
|
||||
expect(html).toBe('<pre><code class="python language-python">print("hello")\n</code></pre>\n<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('custom rules', (done) => {
|
||||
fs.writeFileSync(file, `www.google.com`);
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<p><a href="http://www.google.com">www.google.com</a></p>');
|
||||
done();
|
||||
});
|
||||
describe('Test Prism', () => {
|
||||
test('no prism', (done) => {
|
||||
config['modules']['prism'] = false;
|
||||
renderer.renderPrism('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (data) => {
|
||||
expect(data).toBe('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('prism correct', (done) => {
|
||||
renderer.renderPrism('```python\nprint("hello")\n```', (data) => {
|
||||
expect(data).not.toBe('<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
expect(data.indexOf('<pre><code class="python language-python">')).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('prism invalid lang', (done) => {
|
||||
renderer.renderPrism('```pythdon\nprint("hello")\n```', (data) => {
|
||||
expect(data).not.toBe('<pre><code class="pythdon language-pythdon">print("hello")\n</code></pre>');
|
||||
expect(data.indexOf('<pre><code class="pythdon language-pythdon">')).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('prism mutliple code blocks', (done) => {
|
||||
renderer.renderPrism('```python\n\n```\n```python\n\n```', (data) => {
|
||||
expect(data).toBe('<pre><code class="python language-python"></code></pre>\n<pre><code class="python language-python"></code></pre>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('no prism', (done) => {
|
||||
config['modules']['prism'] = false;
|
||||
fs.writeFileSync(file, '```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<pre><code class="python language-python">print("hello")\n</code></pre>\n<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
config['modules']['prism'] = true;
|
||||
done();
|
||||
});
|
||||
describe('Test PlantUML', () => {
|
||||
test('no plantuml', (done) => {
|
||||
config['modules']['plantuml'] = false;
|
||||
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
|
||||
expect(data).toBe('@startuml\nBob -> Alice : hello\n@enduml');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('plantuml correct', (done) => {
|
||||
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
|
||||
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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">');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prism correct', (done) => {
|
||||
fs.writeFileSync(file, '```python\nprint("hello")\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).not.toBe('<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
expect(html.indexOf('<pre><code class="python language-python">')).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
describe('Test MathJax', () => {
|
||||
test('no mathjax', (done) => {
|
||||
config['modules']['mathjax'] = false;
|
||||
renderer.renderMathJax('$$\nhello\n$$\ntest$test$', (data) => {
|
||||
expect(data).toBe('$$\nhello\n$$\ntest$test$');
|
||||
done();
|
||||
});
|
||||
});
|
||||
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;">' +
|
||||
'A' +
|
||||
'</span></span></span></span></span>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
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;">' +
|
||||
'a' +
|
||||
'</span></span></span></span></span>' +
|
||||
' end');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('fake inline eq', (done) => {
|
||||
renderer.renderMathJax('i have $6\nyou have $5', (data) => {
|
||||
expect(data).toBe('i have $6\nyou have $5');
|
||||
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;">' +
|
||||
'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;">' +
|
||||
'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;">' +
|
||||
'A' +
|
||||
'</span></span></span></span></span>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prism invalid lang', (done) => {
|
||||
fs.writeFileSync(file, '```pythdon\nprint("hello")\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).not.toBe('<pre><code class="pythdon language-pythdon">print("hello")\n</code></pre>');
|
||||
expect(html.indexOf('<pre><code class="pythdon language-pythdon">')).toBe(0);
|
||||
done();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prism mutliple code blocks', (done) => {
|
||||
fs.writeFileSync(file, '```python\n\n```\n\n```python\n\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<pre><code class="python language-python"></code></pre>\n<pre><code class="python language-python"></code></pre>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
describe('Test render', () => {
|
||||
test('invalid file', (done) => {
|
||||
renderer.render('invalid file', (err, html) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(html).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('normal file', (done) => {
|
||||
fs.writeFileSync(file, '# Hello');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<h1 id="hello">Hello</h1>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
const dataDir = 'test_data';
|
||||
|
||||
beforeEach(() => {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('load 1 script', () => {
|
||||
const file = path.join(dataDir, 'test.js');
|
||||
fs.writeFileSync(file, `
|
||||
var a = 5;
|
||||
function b(){
|
||||
return a;
|
||||
}`);
|
||||
require('../src/script_loader')(file);
|
||||
expect(global['b']).toBeDefined();
|
||||
expect(global['b']()).toBe(5);
|
||||
});
|
||||
|
||||
test('load 2 script', () => {
|
||||
const file1 = path.join(dataDir, 'test.js');
|
||||
fs.writeFileSync(file1, `
|
||||
var a = 5;
|
||||
function b(){
|
||||
return a;
|
||||
}`);
|
||||
const file2 = path.join(dataDir, 'test2.js');
|
||||
fs.writeFileSync(file2, `
|
||||
var a = 9;
|
||||
function b(){
|
||||
return a;
|
||||
}`);
|
||||
require('../src/script_loader')(file1);
|
||||
expect(global['b']).toBeDefined();
|
||||
expect(global['b']()).toBe(5);
|
||||
|
||||
require('../src/script_loader.js')(file2);
|
||||
expect(global['b']).toBeDefined();
|
||||
expect(global['b']()).toBe(9);
|
||||
});
|
||||
+23
-21
@@ -2,28 +2,30 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const deleteFolderSync = (dir) => {
|
||||
if (!fs.existsSync(dir))
|
||||
return;
|
||||
let items;
|
||||
const deleteItem = (item) => {
|
||||
if (item.isDirectory())
|
||||
deleteFolderSync(path.join(dir, item.name));
|
||||
else
|
||||
fs.unlinkSync(path.join(dir, item.name));
|
||||
};
|
||||
do {
|
||||
items = fs.readdirSync(dir, {withFileTypes: true});
|
||||
try {
|
||||
items.forEach(deleteItem);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (!fs.existsSync(dir)) {
|
||||
return;
|
||||
}
|
||||
} while (items.length > 0);
|
||||
fs.rmdirSync(dir);
|
||||
let items;
|
||||
const deleteItem = (item) => {
|
||||
if (item.isDirectory()) {
|
||||
deleteFolderSync(path.join(dir, item.name));
|
||||
} else {
|
||||
fs.unlinkSync(path.join(dir, item.name));
|
||||
}
|
||||
};
|
||||
do {
|
||||
items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
try {
|
||||
items.forEach(deleteItem);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} while (items.length > 0);
|
||||
fs.rmdirSync(dir);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
deleteFolderSync: deleteFolderSync,
|
||||
createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, {recursive: true})),
|
||||
createEmptyFiles: (list) => list.forEach((file) => fs.writeFileSync(file, '')),
|
||||
};
|
||||
deleteFolderSync: deleteFolderSync,
|
||||
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