From b8d88eb00c1bd93e96469e10d3de7df17dbbc17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 13:44:02 +0200 Subject: [PATCH 01/42] Fixed install script (bis) --- .gitignore | 1 + README.md | 2 +- package-lock.json | 391 ++++++++---------- package.json | 5 +- src/config.js | 16 + .../default_config.json | 0 src/postinstall.js | 18 - src/server.js | 8 +- 8 files changed, 208 insertions(+), 233 deletions(-) create mode 100644 src/config.js rename config.example.json => src/default_config.json (100%) delete mode 100644 src/postinstall.js diff --git a/.gitignore b/.gitignore index d60caaf..8dd596e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea /node_modules /config.json +/config.example.json /data \ No newline at end of file diff --git a/README.md b/README.md index 5d3d913..b36d91a 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ then edit the config.json file with your values : **3. Start your server** ```bash -npm start +npm run #or node src/server.js ``` diff --git a/package-lock.json b/package-lock.json index 5a5577b..790eafc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1352,12 +1352,14 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true }, "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true }, "array-equal": { "version": "1.0.0", @@ -1395,7 +1397,8 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "astral-regex": { "version": "1.0.0", @@ -1425,7 +1428,8 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true }, "aws-sign2": { "version": "0.7.0", @@ -2224,13 +2228,13 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -2245,6 +2249,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -2253,6 +2258,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2261,6 +2267,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2269,6 +2276,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -2278,12 +2286,14 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -2324,85 +2334,11 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "braces": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", - "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "kind-of": "^6.0.2", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, "browser-process-hrtime": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", @@ -2460,6 +2396,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -2475,7 +2412,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -2569,6 +2507,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -2580,6 +2519,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -2587,7 +2527,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -2616,6 +2557,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -2648,18 +2590,19 @@ "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "content-disposition": { "version": "0.5.3", @@ -2702,7 +2645,8 @@ "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true }, "core-js": { "version": "2.6.9", @@ -2790,7 +2734,8 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true }, "deep-is": { "version": "0.1.3", @@ -2811,6 +2756,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -2820,6 +2766,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2828,6 +2775,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2836,6 +2784,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -2845,12 +2794,14 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -2969,6 +2920,11 @@ "is-symbol": "^1.0.2" } }, + "es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3179,6 +3135,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -3188,6 +3145,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -3250,27 +3208,6 @@ "dev": true, "optional": true }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -3296,7 +3233,8 @@ "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true }, "for-own": { "version": "0.1.5", @@ -3359,8 +3297,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.9", @@ -3929,7 +3866,8 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true }, "getpass": { "version": "0.1.7", @@ -3944,7 +3882,6 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4071,6 +4008,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -4080,7 +4018,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -4088,6 +4027,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -4097,6 +4037,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -4105,6 +4046,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -4115,6 +4057,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -4197,7 +4140,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4208,6 +4150,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, + "interpret": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", + "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4231,6 +4178,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, "requires": { "kind-of": "^3.0.2" } @@ -4254,7 +4202,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-callable": { "version": "1.1.4", @@ -4275,6 +4224,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, "requires": { "kind-of": "^3.0.2" } @@ -4289,6 +4239,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -4298,7 +4249,8 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, @@ -4322,7 +4274,8 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true }, "is-extglob": { "version": "1.0.0", @@ -4361,18 +4314,11 @@ "is-extglob": "^1.0.0" } }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" }, @@ -4380,7 +4326,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -4442,18 +4389,14 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -6739,6 +6682,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6869,12 +6813,14 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, "requires": { "object-visit": "^1.0.0" } @@ -6981,7 +6927,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6996,6 +6941,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -7005,6 +6951,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -7111,22 +7058,6 @@ "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", "dev": true }, - "node-nailgun-client": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/node-nailgun-client/-/node-nailgun-client-0.1.2.tgz", - "integrity": "sha512-OC611lR0fsDUSptwnhBf8d3sj4DZ5fiRKfS2QaGPe0kR3Dt9YoZr1MY7utK0scFPTbXuQdSBBbeoKYVbME1q5g==", - "requires": { - "commander": "^2.8.1" - } - }, - "node-nailgun-server": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/node-nailgun-server/-/node-nailgun-server-0.1.4.tgz", - "integrity": "sha512-e0Hbh6XPb/7GqATJ45BePaUEO5AwR7InRW/pGeMKHH1cqPMBFCeqdBNfvi+bkVLnsbYOOQE+pAek9nmNoD8sYw==", - "requires": { - "commander": "^2.8.1" - } - }, "node-notifier": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", @@ -7140,17 +7071,6 @@ "which": "^1.3.0" } }, - "node-plantuml": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/node-plantuml/-/node-plantuml-0.8.1.tgz", - "integrity": "sha512-sEI9j61MLunxkV0QTUyycanLhk9qP23iEkv4IBT+bcndR8qJ1hm9TCD21I0cK5XyBCXNQ3gywXKujHWDojwcQg==", - "requires": { - "commander": "^2.8.1", - "node-nailgun-client": "^0.1.0", - "node-nailgun-server": "^0.1.4", - "plantuml-encoder": "^1.2.5" - } - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -7207,6 +7127,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -7217,6 +7138,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -7233,6 +7155,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, "requires": { "isobject": "^3.0.0" }, @@ -7240,7 +7163,8 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -7294,7 +7218,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -7417,11 +7340,6 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" }, - "pako": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.3.tgz", - "integrity": "sha1-X1FbDGci4ZgpIK6ABerLC3ynPM8=" - }, "parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", @@ -7459,7 +7377,8 @@ "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true }, "path-exists": { "version": "3.0.0", @@ -7469,8 +7388,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "2.0.1", @@ -7480,8 +7398,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -7572,15 +7489,6 @@ } } }, - "plantuml-encoder": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/plantuml-encoder/-/plantuml-encoder-1.2.5.tgz", - "integrity": "sha512-viV7Sz+BJNX/sC3iyebh2VfLyAZKuu3+JuBs2ISms8+zoTGwPqwk3/WEDw/zROmGAJ/xD4sNd8zsBw/YmTo7ng==", - "requires": { - "pako": "1.0.3", - "utf8-bytes": "0.0.1" - } - }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -8152,6 +8060,14 @@ "util.promisify": "^1.0.0" } }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "^1.1.6" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -8189,6 +8105,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" @@ -8237,12 +8154,14 @@ "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true }, "repeating": { "version": "2.0.1", @@ -8339,7 +8258,6 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -8362,12 +8280,14 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true }, "rimraf": { "version": "2.6.3", @@ -8393,6 +8313,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, "requires": { "ret": "~0.1.10" } @@ -8807,6 +8728,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -8818,6 +8740,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -8842,6 +8765,16 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, + "shelljs": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -8856,6 +8789,23 @@ "yargs": "^10.0.3" } }, + "shx": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.2.tgz", + "integrity": "sha512-aS0mWtW3T2sHAenrSrip2XGv39O9dXIFUqxAEWHEOS1ePtGIBavdPJY1kE2IHl14V/4iCbUiNDPGdyYTtmhSoA==", + "requires": { + "es6-object-assign": "^1.0.3", + "minimist": "^1.2.0", + "shelljs": "^0.8.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -8877,6 +8827,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, "requires": { "base": "^0.11.1", "debug": "^2.2.0", @@ -8892,6 +8843,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -8900,6 +8852,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -8910,6 +8863,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -8920,6 +8874,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -8928,6 +8883,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -8936,6 +8892,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -8944,6 +8901,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -8953,12 +8911,14 @@ "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true } } }, @@ -8966,6 +8926,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, "requires": { "kind-of": "^3.2.0" } @@ -8973,12 +8934,14 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, "requires": { "atob": "^2.1.1", "decode-uri-component": "^0.2.0", @@ -8999,7 +8962,8 @@ "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true }, "spdx-correct": { "version": "3.1.0", @@ -9037,6 +9001,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, "requires": { "extend-shallow": "^3.0.0" } @@ -9068,6 +9033,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -9077,6 +9043,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -9299,6 +9266,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, "requires": { "kind-of": "^3.0.2" } @@ -9307,6 +9275,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -9318,6 +9287,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -9327,6 +9297,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" } @@ -9420,6 +9391,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -9431,6 +9403,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -9439,6 +9412,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -9457,6 +9431,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -9466,6 +9441,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -9476,6 +9452,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, "requires": { "isarray": "1.0.0" } @@ -9485,12 +9462,14 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true } } }, @@ -9506,12 +9485,14 @@ "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true }, "user-home": { "version": "1.1.1", @@ -9519,11 +9500,6 @@ "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", "dev": true }, - "utf8-bytes": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/utf8-bytes/-/utf8-bytes-0.0.1.tgz", - "integrity": "sha1-EWsCVEjJtQAIHN+/H01sbDfYg30=" - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9700,8 +9676,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "2.4.1", diff --git a/package.json b/package.json index 63e60c2..3e519c2 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "nodePort": 5000, "name": "gitblog.md", "version": "1.0.0", "description": "A static blog using Markdown pulled from your git repository.", @@ -7,6 +8,7 @@ "express": "^4.17.1", "ncp": "^2.0.0", "showdown": "^1.9.0", + "shx": "^0.3.2", "xml": "^1.0.1" }, "devDependencies": { @@ -17,8 +19,9 @@ "supertest": "^4.0.2" }, "scripts": { + "start": "node src/server.js", "test": "jest", - "install": "node src/postinstall.js" + "install": "shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d)/ && shx cp -rf ./sample_data/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f ./src/default_config.json ./config.example.json" }, "repository": { "type": "git", diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..b64d0c1 --- /dev/null +++ b/src/config.js @@ -0,0 +1,16 @@ +const refConfig = require('./default_config.json'); +const srcConfig = require('../config.json'); + +const merge = function (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; + } +}; + +module.exports = merge(refConfig, srcConfig); \ No newline at end of file diff --git a/config.example.json b/src/default_config.json similarity index 100% rename from config.example.json rename to src/default_config.json diff --git a/src/postinstall.js b/src/postinstall.js deleted file mode 100644 index 3b68c2b..0000000 --- a/src/postinstall.js +++ /dev/null @@ -1,18 +0,0 @@ -const fs = require('fs'); -const ncp = require('ncp').ncp; - -const pad0 = n => ('0'+n).substr(-2); - -const datetime = new Date(); -const dir = `./data/${datetime.getFullYear()}/${pad0(datetime.getMonth())}/${pad0(datetime.getDay())}/`; - -if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, {recursive: true}); -} - -ncp('./sample_data/',dir, function(err){ - if(err) - console.error(err); - else - console.log(`sample data copied to ${dir}`); -}); \ No newline at end of file diff --git a/src/server.js b/src/server.js index db33f45..f52bfce 100644 --- a/src/server.js +++ b/src/server.js @@ -1,9 +1,7 @@ -const config = require('../config.json'); +const config = require('./config'); const app = require('./app')(config); -const port = config.nodePort|3000; - -app.listen(config.nodePort|3000, () => { - console.log(`gitblog.md server listening on port ${port}`); +app.listen(config.nodePort, () => { + console.log(`gitblog.md server listening on port ${config.nodePort}`); }); From 2dd87a3d9b07f385c9dcf0f45165e28824cda7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 13:51:02 +0200 Subject: [PATCH 02/42] Separated sample article and home page --- package.json | 2 +- sample_data/{ => article}/birthday-cake.png | Bin sample_data/{ => article}/index.md | 0 sample_data/home/index.ejs | 10 ++++++++++ 4 files changed, 11 insertions(+), 1 deletion(-) rename sample_data/{ => article}/birthday-cake.png (100%) rename sample_data/{ => article}/index.md (100%) create mode 100644 sample_data/home/index.ejs diff --git a/package.json b/package.json index 3e519c2..4c4b2ab 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "scripts": { "start": "node src/server.js", "test": "jest", - "install": "shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d)/ && shx cp -rf ./sample_data/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f ./src/default_config.json ./config.example.json" + "install": "shx mkdir ./data && shx cp -rf ./sample_data/home/* ./data && shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -rf ./sample_data/article/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f ./src/default_config.json ./config.example.json" }, "repository": { "type": "git", diff --git a/sample_data/birthday-cake.png b/sample_data/article/birthday-cake.png similarity index 100% rename from sample_data/birthday-cake.png rename to sample_data/article/birthday-cake.png diff --git a/sample_data/index.md b/sample_data/article/index.md similarity index 100% rename from sample_data/index.md rename to sample_data/article/index.md diff --git a/sample_data/home/index.ejs b/sample_data/home/index.ejs new file mode 100644 index 0000000..566549b --- /dev/null +++ b/sample_data/home/index.ejs @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file From 611d88d08863332519239a1c6e7ab9daba3448fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 14:33:12 +0200 Subject: [PATCH 03/42] config.js tests --- package-lock.json | 5 ---- package.json | 1 - src/config.js | 15 ++++++++++-- src/default_config.json | 4 +-- src/server.js | 6 ++--- test/config.test.js | 54 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 test/config.test.js diff --git a/package-lock.json b/package-lock.json index 790eafc..a5b988b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7024,11 +7024,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=" - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", diff --git a/package.json b/package.json index 4c4b2ab..08df023 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "main": "src/server.js", "dependencies": { "express": "^4.17.1", - "ncp": "^2.0.0", "showdown": "^1.9.0", "shx": "^0.3.2", "xml": "^1.0.1" diff --git a/src/config.js b/src/config.js index b64d0c1..f0b1273 100644 --- a/src/config.js +++ b/src/config.js @@ -1,16 +1,27 @@ const refConfig = require('./default_config.json'); -const srcConfig = require('../config.json'); +const fs = require('fs'); const merge = function (ref, src) { if (typeof ref !== typeof src) { + console.log(ref, src, ref); return ref; } else if (typeof ref === 'object') { const out = {}; Object.keys(ref).forEach(key => out[key] = merge(ref[key], src[key])); return out; } else { + console.log(ref, src, src); return src; } }; -module.exports = merge(refConfig, srcConfig); \ No newline at end of file +module.exports = function () { + try { + let configData = fs.readFileSync('./config.json'); + let config = JSON.parse(configData); + return merge(refConfig, config); + } catch (error) { + console.error('Failed to load config.json'); + return refConfig; + } +}; \ No newline at end of file diff --git a/src/default_config.json b/src/default_config.json index 5ab3b35..f62a4ed 100644 --- a/src/default_config.json +++ b/src/default_config.json @@ -1,6 +1,6 @@ { - "nodePort": 3000, - "dataDir": "data", + "node_port": 3000, + "data_dir": "data", "modules" : { "plantuml" : false, "rss": true, diff --git a/src/server.js b/src/server.js index f52bfce..7e68e9c 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,7 @@ -const config = require('./config'); +const config = require('./config')(); const app = require('./app')(config); -app.listen(config.nodePort, () => { - console.log(`gitblog.md server listening on port ${config.nodePort}`); +app.listen(config['node_port'], () => { + console.log(`gitblog.md server listening on port ${config['node_port']}`); }); diff --git a/test/config.test.js b/test/config.test.js new file mode 100644 index 0000000..5f61b3f --- /dev/null +++ b/test/config.test.js @@ -0,0 +1,54 @@ +/* jshint -W117 */ +const fs = require('fs'); + +const configFile = './config.json'; +const tmpConfigFile = './config.temp.json'; + +beforeAll(() => { + if (fs.existsSync(configFile)) { + fs.renameSync(configFile, tmpConfigFile); + } + expect(fs.existsSync(configFile)).toBeFalsy(); +}); + +afterAll(() => { + 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'); +}); + +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'); +}); + +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'); +}); + +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'); +}); \ No newline at end of file From d7fb67401b20022ffcc54d86933bd23fdb0649c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 14:35:56 +0200 Subject: [PATCH 04/42] updated package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 08df023..b05eae5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "scripts": { "start": "node src/server.js", - "test": "jest", + "test": "jest --silent", "install": "shx mkdir ./data && shx cp -rf ./sample_data/home/* ./data && shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -rf ./sample_data/article/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f ./src/default_config.json ./config.example.json" }, "repository": { From d6a493dac6b831a82255735b677747715da79377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 14:36:16 +0200 Subject: [PATCH 05/42] removed debug log --- src/config.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config.js b/src/config.js index f0b1273..a768790 100644 --- a/src/config.js +++ b/src/config.js @@ -3,14 +3,12 @@ const fs = require('fs'); const merge = function (ref, src) { if (typeof ref !== typeof src) { - console.log(ref, src, ref); return ref; } else if (typeof ref === 'object') { const out = {}; Object.keys(ref).forEach(key => out[key] = merge(ref[key], src[key])); return out; } else { - console.log(ref, src, src); return src; } }; From 15761551e756a62e8b58eeebc350be728a28f5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 14:39:20 +0200 Subject: [PATCH 06/42] minor error log --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index a768790..42f18b3 100644 --- a/src/config.js +++ b/src/config.js @@ -19,7 +19,7 @@ module.exports = function () { let config = JSON.parse(configData); return merge(refConfig, config); } catch (error) { - console.error('Failed to load config.json'); + console.error('Failed to load config.json : '+error); return refConfig; } }; \ No newline at end of file From 20b4d0353f087c7fe9c26fb1909410d0f5ebccf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 16:27:49 +0200 Subject: [PATCH 07/42] file_walker.js detect articles in data file tree --- .gitignore | 3 +- package.json | 2 +- ...efault_config.json => config.default.json} | 0 src/config.js | 2 +- src/file_walker.js | 55 ++++++ test/app.test.js | 5 +- test/config.test.json | 3 - test/file_walker.test.js | 165 ++++++++++++++++++ 8 files changed, 227 insertions(+), 8 deletions(-) rename src/{default_config.json => config.default.json} (100%) create mode 100644 src/file_walker.js delete mode 100644 test/config.test.json create mode 100644 test/file_walker.test.js diff --git a/.gitignore b/.gitignore index 8dd596e..aba5ca9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /node_modules /config.json /config.example.json -/data \ No newline at end of file +/data +/test_data \ No newline at end of file diff --git a/package.json b/package.json index b05eae5..ee88950 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "scripts": { "start": "node src/server.js", "test": "jest --silent", - "install": "shx mkdir ./data && shx cp -rf ./sample_data/home/* ./data && shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -rf ./sample_data/article/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f ./src/default_config.json ./config.example.json" + "install": "shx mkdir ./data && shx cp -rf ./sample_data/home/* ./data && shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -rf ./sample_data/article/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f src/config.default.json ./config.example.json" }, "repository": { "type": "git", diff --git a/src/default_config.json b/src/config.default.json similarity index 100% rename from src/default_config.json rename to src/config.default.json diff --git a/src/config.js b/src/config.js index 42f18b3..30ec53d 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,4 @@ -const refConfig = require('./default_config.json'); +const refConfig = require('./config.default.json'); const fs = require('fs'); const merge = function (ref, src) { diff --git a/src/file_walker.js b/src/file_walker.js new file mode 100644 index 0000000..134a170 --- /dev/null +++ b/src/file_walker.js @@ -0,0 +1,55 @@ +const fs = require('fs'); + +const fileTree = (path, cb) => { + let list = []; + let remaining = 0; + fs.readdir(path, {withFileTypes: true}, (err, items) => { + if (err) + return cb(err); + items.forEach((item) => { + if (item.isDirectory()) { + remaining++; + fileTree(`${path}/${item.name}`, (err, out) => { + if (err) + return cb(err); + list.push(...out); + remaining--; + if (remaining === 0) + cb(null, list); + }); + } else { + list.push(`${path}/${item.name}`); + } + }); + if (remaining === 0) + cb(null, list); + }); +}; + +module.exports = (config) => { + return { + fileTree: config['test'] ? fileTree : undefined, + fetchArticles: (cb) => { + fileTree(config['data_dir'], (err, fileList) => { + if (err) + return cb(err); + const paths = fileList + .map(path => path.substr(config['data_dir'].length)) + .filter(path => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) + .map(path => path.substr(0, path.length - config['article']['index'].length)) + .map(path => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) + .filter(path => path && path.length > 1); + const list = []; + paths.forEach(path => { + list.push({ + path: config['data_dir'] + path[0] + config['article']['index'], + year: parseInt(path[1]), + month: parseInt(path[2]), + day: parseInt(path[3]) + }); + }); + cb(null, list); + }); + } + }; +}; \ No newline at end of file diff --git a/test/app.test.js b/test/app.test.js index 6f24b00..8e33e19 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,7 +1,8 @@ /* jshint -W117 */ const request = require('supertest'); -const config = require('./config.test.json'); -const app = require('../src/app')(config); +const app = require('../src/app')({ + +}); describe('Test root path', () => { test('GET / 200', done => { diff --git a/test/config.test.json b/test/config.test.json deleted file mode 100644 index 0e0dcd2..0000000 --- a/test/config.test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/test/file_walker.test.js b/test/file_walker.test.js new file mode 100644 index 0000000..10da942 --- /dev/null +++ b/test/file_walker.test.js @@ -0,0 +1,165 @@ +/* jshint -W117 */ +const fs = require('fs'); + +const dataDir = './test_data'; +const testIndex = 'testindex.md'; + +const config = { + 'test': true, + 'data_dir': dataDir, + 'article': { + 'index': testIndex + } +}; + +const fw = require('../src/file_walker')(config); + +const deleteFolderSync = (path) => { + if(!fs.existsSync(path)) + return; + fs.readdirSync(path, {withFileTypes: true}).forEach((item) => { + if (item.isDirectory()) + deleteFolderSync(`${path}/${item.name}`); + else + fs.unlinkSync(`${path}/${item.name}`); + }); + fs.rmdirSync(path); +}; + +beforeEach(() => { + deleteFolderSync(dataDir); + fs.mkdirSync(dataDir); +}); + +afterAll(() => { + if (fs.existsSync(dataDir)) { + deleteFolderSync(dataDir); + } +}); + +const createEmptyDirs = list => list.forEach(path => fs.mkdirSync(path, {recursive: true})); +const createEmptyFiles = list => list.forEach(file => fs.writeFileSync(file, '')); + +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 folders', (done) => { + createEmptyDirs([ + `${dataDir}/test/test`, + `${dataDir}/test/test2`, + `${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 = [ + `${dataDir}/f1.txt`, + `${dataDir}/f2.txt` + ]; + 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) => { + createEmptyDirs([ + `${dataDir}/test/test`, + `${dataDir}/test2` + ]); + const fileList = [ + `${dataDir}/f1.txt`, + `${dataDir}/test/f2.txt`, + `${dataDir}/test/test/f3.txt`, + `${dataDir}/test2/f4.txt` + ]; + 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(); + }); + }); +}); + +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('empty data dir', (done) => { + fw.fetchArticles((err,list) => { + expect(err).toBeNull(); + expect(list).toBeDefined(); + expect(list.length).toBe(0); + done(); + }); + }); + test('misplaced index file', (done) => { + createEmptyDirs([ + `${dataDir}/test/test`, + `${dataDir}/2019/05/05`, + ]); + createEmptyFiles([ + `${dataDir}/${testIndex}`, + `${dataDir}/test/test/${testIndex}`, + `${dataDir}/2019/05/${testIndex}`, + ]); + fw.fetchArticles((err,list) => { + expect(err).toBeNull(); + expect(list).toBeDefined(); + expect(list.length).toBe(0); + done(); + }); + }); + test('correct index file', (done) => { + createEmptyDirs([ + `${dataDir}/2019/05/05`, + ]); + const fileList = [ + `${dataDir}/2019/05/05/${testIndex}`, + ]; + createEmptyFiles(fileList); + fw.fetchArticles((err,list) => { + expect(err).toBeNull(); + expect(list).toBeDefined(); + expect(list.length).toBe(1); + expect(list[0]).toEqual({ + path: fileList[0], + year:2019, + month:5, + day:5 + }); + done(); + }); + }); +}); + From f17bcbd3c927b6043bd2eab55ff6a35793d3598b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 16:28:53 +0200 Subject: [PATCH 08/42] code 'function' --- src/app.js | 2 +- src/config.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app.js b/src/app.js index c0f527e..ed30e1e 100644 --- a/src/app.js +++ b/src/app.js @@ -1,7 +1,7 @@ const express = require('express'); const app = express(); -module.exports = function(/*config*/){ +module.exports = (/*config*/) => { app.get('/', (req,res) => { res.status(200).send('Hello World!'); }); diff --git a/src/config.js b/src/config.js index 30ec53d..f698325 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,7 @@ const refConfig = require('./config.default.json'); const fs = require('fs'); -const merge = function (ref, src) { +const merge = (ref, src) => { if (typeof ref !== typeof src) { return ref; } else if (typeof ref === 'object') { @@ -13,7 +13,7 @@ const merge = function (ref, src) { } }; -module.exports = function () { +module.exports = () => { try { let configData = fs.readFileSync('./config.json'); let config = JSON.parse(configData); From 1c30f95677f9635e2847f7a6e111d57cee102371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Wed, 19 Jun 2019 16:33:14 +0200 Subject: [PATCH 09/42] [skip ci]wip file_walker reading article info --- sample_data/article/index.md | 2 +- src/file_walker.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sample_data/article/index.md b/sample_data/article/index.md index 8212cc1..e45f07a 100644 --- a/sample_data/article/index.md +++ b/sample_data/article/index.md @@ -2,5 +2,5 @@ ## If you see this page, that means it's working -![missing image](./birthday-cake.png) +![thumbnail](./birthday-cake.png) diff --git a/src/file_walker.js b/src/file_walker.js index 134a170..2d2c9ad 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -26,9 +26,14 @@ const fileTree = (path, cb) => { }); }; +const readIndexFile = (path,cb) => { + //TODO reading page title and possibly ![thumbnail](url) +}; + module.exports = (config) => { return { fileTree: config['test'] ? fileTree : undefined, + readIndexFile: config['test'] ? readIndexFile : undefined, fetchArticles: (cb) => { fileTree(config['data_dir'], (err, fileList) => { if (err) From 9300bfbb784adbc80398bcf98b6c22da8a079b87 Mon Sep 17 00:00:00 2001 From: Klemek Date: Wed, 19 Jun 2019 19:41:29 +0200 Subject: [PATCH 10/42] file walker now read title and thumbnail infos --- README.md | 28 +------- src/config.default.json | 5 +- src/config.js | 2 +- src/file_walker.js | 44 +++++++++--- test/file_walker.test.js | 141 +++++++++++++++++++++++++++++++++++---- 5 files changed, 169 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index b36d91a..0d18322 100644 --- a/README.md +++ b/README.md @@ -26,33 +26,7 @@ npm install cd gitblog.md cp config.example.json config.json ``` -then edit the config.json file with your values : -> default values for config.json -````json -{ - "nodePort": 3000, - "dataDir": "data", - "modules" : { - "plantuml" : false, - "rss": true, - "webhook": true - }, - "home" : { - "index" : "index.ejs" - }, - "article" : { - "index" : "index.md" - }, - "rss" : { - "endpoint" : "/rss", - "length" : 10 - }, - "webhook" : { - "endpoint": "/webhook", - "secretFile": "git_secret" - } -} -```` +then edit the config.json file with your custom values. **3. Start your server** diff --git a/src/config.default.json b/src/config.default.json index f62a4ed..2ee6f02 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -10,7 +10,10 @@ "index" : "index.ejs" }, "article" : { - "index" : "index.md" + "index" : "index.md", + "thumbnail_tag" : "thumbnail", + "default_title": "Untitled", + "default_thumbnail" : null }, "rss" : { "endpoint" : "/rss", diff --git a/src/config.js b/src/config.js index f698325..ac75ed5 100644 --- a/src/config.js +++ b/src/config.js @@ -15,7 +15,7 @@ const merge = (ref, src) => { module.exports = () => { try { - let configData = fs.readFileSync('./config.json'); + let configData = fs.readFileSync('./config.json', {encoding:'UTF-8'}); let config = JSON.parse(configData); return merge(refConfig, config); } catch (error) { diff --git a/src/file_walker.js b/src/file_walker.js index 2d2c9ad..1b1766b 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -1,6 +1,6 @@ const fs = require('fs'); -const fileTree = (path, cb) => { +const getFileTree = (path, cb) => { let list = []; let remaining = 0; fs.readdir(path, {withFileTypes: true}, (err, items) => { @@ -9,7 +9,7 @@ const fileTree = (path, cb) => { items.forEach((item) => { if (item.isDirectory()) { remaining++; - fileTree(`${path}/${item.name}`, (err, out) => { + getFileTree(`${path}/${item.name}`, (err, out) => { if (err) return cb(err); list.push(...out); @@ -26,16 +26,30 @@ const fileTree = (path, cb) => { }); }; -const readIndexFile = (path,cb) => { - //TODO reading page title and possibly ![thumbnail](url) +const readIndexFile = (path, thumbnailTag, cb) => { + fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => { + if (err) + return cb(err); + + let info = {}; + + 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; + + cb(null, info); + }); }; module.exports = (config) => { return { - fileTree: config['test'] ? fileTree : undefined, + fileTree: config['test'] ? getFileTree : undefined, readIndexFile: config['test'] ? readIndexFile : undefined, fetchArticles: (cb) => { - fileTree(config['data_dir'], (err, fileList) => { + getFileTree(config['data_dir'], (err, fileList) => { if (err) return cb(err); const paths = fileList @@ -44,16 +58,30 @@ module.exports = (config) => { .map(path => path.substr(0, path.length - config['article']['index'].length)) .map(path => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) .filter(path => path && path.length > 1); + if (paths.length === 0) + cb(null, []); const list = []; + let remaining = 0; paths.forEach(path => { - list.push({ + const article = { path: config['data_dir'] + path[0] + config['article']['index'], year: parseInt(path[1]), month: parseInt(path[2]), day: parseInt(path[3]) + }; + remaining++; + readIndexFile(article.path, config['article']['thumbnail_tag'], (err, info) => { + if (err) + return cb(err); + article.title = info.title || config['article']['default_title']; + article.thumbnail = info.thumbnail ? (config['data_dir'] + path[0] + info.thumbnail) : config['article']['default_thumbnail']; + list.push(article); + remaining--; + if (remaining === 0) + cb(null, list); }); }); - cb(null, list); + }); } }; diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 10da942..e1a1c1a 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -8,14 +8,17 @@ const config = { 'test': true, 'data_dir': dataDir, 'article': { - 'index': testIndex + 'index': testIndex, + 'default_title': 'Untitled', + 'default_thumbnail': 'default.png', + 'thumbnail_tag': 'thumbnail' } }; const fw = require('../src/file_walker')(config); const deleteFolderSync = (path) => { - if(!fs.existsSync(path)) + if (!fs.existsSync(path)) return; fs.readdirSync(path, {withFileTypes: true}).forEach((item) => { if (item.isDirectory()) @@ -105,10 +108,95 @@ describe('Test function fileTree', () => { }); }); +describe('Test index article reading', () => { + const file = `${dataDir}/${testIndex}`; + + 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, ` + # This is an awesome title !?¤ + ![custom_thumbnail](./thumbnail.jpg) + this is some text + `); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('This is an awesome title !?¤'); + expect(info.thumbnail).toBe('./thumbnail.jpg'); + done(); + }); + }); + + test('no title', (done) => { + fs.writeFileSync(file, ` + ## This is an awesome title !?¤ + ![custom_thumbnail](./thumbnail.jpg) + ### this is some text + `); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).not.toBeDefined(); + expect(info.thumbnail).toBe('./thumbnail.jpg'); + done(); + }); + }); + + test('title at beginning', (done) => { + fs.writeFileSync(file, '#title'); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('title'); + expect(info.thumbnail).not.toBeDefined(); + done(); + }); + }); + + test('no thumbnail', (done) => { + fs.writeFileSync(file, ` + # This is an awesome title !?¤ + ![custom_thumbnail](./thumbnail.jpg) + this is some text + `); + fw.readIndexFile(file, 'thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('This is an awesome title !?¤'); + expect(info.thumbnail).not.toBeDefined(); + done(); + }); + }); + + test('multiple thumbnails', (done) => { + fs.writeFileSync(file, ` + # This is an awesome title !?¤ + ![custom_thumbnail](./thumbnail.jpg) + this is some text + ![custom_thumbnail](./thumbnail2.jpg) + `); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('This is an awesome title !?¤'); + expect(info.thumbnail).toBe('./thumbnail.jpg'); + done(); + }); + }); +}); + describe('Test article fetching', () => { test('invalid data dir', (done) => { config['data_dir'] = 'invalid root'; - fw.fetchArticles((err,list) => { + fw.fetchArticles((err, list) => { expect(err).not.toBeNull(); expect(list).not.toBeDefined(); config['data_dir'] = dataDir; @@ -116,7 +204,7 @@ describe('Test article fetching', () => { }); }); test('empty data dir', (done) => { - fw.fetchArticles((err,list) => { + fw.fetchArticles((err, list) => { expect(err).toBeNull(); expect(list).toBeDefined(); expect(list.length).toBe(0); @@ -133,30 +221,55 @@ describe('Test article fetching', () => { `${dataDir}/test/test/${testIndex}`, `${dataDir}/2019/05/${testIndex}`, ]); - fw.fetchArticles((err,list) => { + fw.fetchArticles((err, list) => { expect(err).toBeNull(); expect(list).toBeDefined(); expect(list.length).toBe(0); done(); }); }); + test('empty index file', (done) => { + createEmptyDirs([ + `${dataDir}/2019/05/05`, + ]); + const file = `${dataDir}/2019/05/05/${testIndex}`; + createEmptyFiles([file]); + fw.fetchArticles((err, list) => { + expect(err).toBeNull(); + expect(list).toBeDefined(); + expect(list.length).toBe(1); + expect(list[0]).toEqual({ + path: file, + year: 2019, + month: 5, + day: 5, + title:'Untitled', + thumbnail:'default.png' + }); + done(); + }); + }); test('correct index file', (done) => { createEmptyDirs([ `${dataDir}/2019/05/05`, ]); - const fileList = [ - `${dataDir}/2019/05/05/${testIndex}`, - ]; - createEmptyFiles(fileList); - fw.fetchArticles((err,list) => { + const file = `${dataDir}/2019/05/05/${testIndex}`; + fs.writeFileSync(file, ` + # Title + ![thumbnail](./thumbnail.jpg) + this is some text + `); + fw.fetchArticles((err, list) => { expect(err).toBeNull(); expect(list).toBeDefined(); expect(list.length).toBe(1); expect(list[0]).toEqual({ - path: fileList[0], - year:2019, - month:5, - day:5 + path: file, + year: 2019, + month: 5, + day: 5, + title:'Title', + thumbnail:`${dataDir}/2019/05/05/./thumbnail.jpg` }); done(); }); From f335cb9f139e6c089e084081258c5026430f6446 Mon Sep 17 00:00:00 2001 From: Klemek Date: Wed, 19 Jun 2019 19:49:54 +0200 Subject: [PATCH 11/42] nothing beat a js postinstall --- package-lock.json | 74 ++++++++++++++-------------------------------- package.json | 4 +-- src/postinstall.js | 28 ++++++++++++++++++ 3 files changed, 53 insertions(+), 53 deletions(-) create mode 100644 src/postinstall.js diff --git a/package-lock.json b/package-lock.json index a5b988b..27426e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2228,7 +2228,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -2334,6 +2335,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2602,7 +2604,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "content-disposition": { "version": "0.5.3", @@ -2920,11 +2923,6 @@ "is-symbol": "^1.0.2" } }, - "es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=" - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3297,7 +3295,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "1.2.9", @@ -3882,6 +3881,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4140,6 +4140,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4150,11 +4151,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==" - }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -6927,6 +6923,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7024,6 +7021,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=" + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -7213,6 +7215,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -7383,7 +7386,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "2.0.1", @@ -7393,7 +7397,8 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true }, "path-to-regexp": { "version": "0.1.7", @@ -8055,14 +8060,6 @@ "util.promisify": "^1.0.0" } }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "requires": { - "resolve": "^1.1.6" - } - }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -8253,6 +8250,7 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", + "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -8760,16 +8758,6 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, - "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", - "requires": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - } - }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -8784,23 +8772,6 @@ "yargs": "^10.0.3" } }, - "shx": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.2.tgz", - "integrity": "sha512-aS0mWtW3T2sHAenrSrip2XGv39O9dXIFUqxAEWHEOS1ePtGIBavdPJY1kE2IHl14V/4iCbUiNDPGdyYTtmhSoA==", - "requires": { - "es6-object-assign": "^1.0.3", - "minimist": "^1.2.0", - "shelljs": "^0.8.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -9671,7 +9642,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "2.4.1", diff --git a/package.json b/package.json index ee88950..4f8859b 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "main": "src/server.js", "dependencies": { "express": "^4.17.1", + "ncp": "^2.0.0", "showdown": "^1.9.0", - "shx": "^0.3.2", "xml": "^1.0.1" }, "devDependencies": { @@ -20,7 +20,7 @@ "scripts": { "start": "node src/server.js", "test": "jest --silent", - "install": "shx mkdir ./data && shx cp -rf ./sample_data/home/* ./data && shx mkdir -p ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -rf ./sample_data/article/* ./data/$(date +%Y)/$(date +%m)/$(date +%d) && shx cp -f src/config.default.json ./config.example.json" + "install": "node src/postinstall.js" }, "repository": { "type": "git", diff --git a/src/postinstall.js b/src/postinstall.js new file mode 100644 index 0000000..2181c14 --- /dev/null +++ b/src/postinstall.js @@ -0,0 +1,28 @@ +const fs = require('fs'); +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}`); + }); +}; + +copy('./src/config.default.json','./config.example.json'); + +if (!fs.existsSync('./data')) + fs.mkdirSync('./data'); + +copy('./sample_data/home','./data'); + +const pad0 = n => ('0'+n).substr(-2); + +const datetime = new Date(); +const dir = `./data/${datetime.getFullYear()}/${pad0(datetime.getMonth())}/${pad0(datetime.getDay())}`; + +if (!fs.existsSync(dir)) + fs.mkdirSync(dir, {recursive: true}); + +copy('./sample_data/article',dir); \ No newline at end of file From 94d96df26fff1ccf40d3ae1e5b3bb33ac93efbcb Mon Sep 17 00:00:00 2001 From: Klemek Date: Wed, 19 Jun 2019 20:17:00 +0200 Subject: [PATCH 12/42] main app loading articles in memory --- src/app.js | 38 ++++++++++++++++++++++++++++++++++++-- src/server.js | 5 +---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/app.js b/src/app.js index ed30e1e..3ff4815 100644 --- a/src/app.js +++ b/src/app.js @@ -1,11 +1,45 @@ const express = require('express'); const app = express(); -module.exports = (/*config*/) => { - app.get('/', (req,res) => { +const cons = { + 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 articles = []; + + const reload = (callback) => { + fw.fetchArticles((err, list) => { + if (err) { + callback(false); + return console.error(cons.error, 'loading articles : ' + err); + } + articles.splice(0, articles.length, ...list); + if (articles.length > 0) + console.log(cons.ok, `loaded ${articles.length} article${articles.length > 1 ? 's' : ''}`); + else + console.log(cons.warn, `no articles loaded, check your configuration`); + callback(true); + }); + }; + + app.get('/', (req, res) => { res.status(200).send('Hello World!'); }); + app.start = () => { + reload((res) => { + if (res) + app.listen(config['node_port'], () => { + console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); + }); + }); + }; + return app; }; diff --git a/src/server.js b/src/server.js index 7e68e9c..3a9e0ef 100644 --- a/src/server.js +++ b/src/server.js @@ -1,7 +1,4 @@ const config = require('./config')(); const app = require('./app')(config); -app.listen(config['node_port'], () => { - console.log(`gitblog.md server listening on port ${config['node_port']}`); -}); - +app.start(); \ No newline at end of file From 7710eaad252a7f997fa34ef8128f250aadf01ab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 09:06:47 +0200 Subject: [PATCH 13/42] Fixed wrong date on postinstall.js --- src/postinstall.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/postinstall.js b/src/postinstall.js index 2181c14..7aeedcf 100644 --- a/src/postinstall.js +++ b/src/postinstall.js @@ -12,17 +12,18 @@ const copy = (src,dest) => { copy('./src/config.default.json','./config.example.json'); -if (!fs.existsSync('./data')) +if (!fs.existsSync('./data')) { fs.mkdirSync('./data'); -copy('./sample_data/home','./data'); + copy('./sample_data/home','./data'); -const pad0 = n => ('0'+n).substr(-2); + const pad0 = n => ('0'+n).substr(-2); -const datetime = new Date(); -const dir = `./data/${datetime.getFullYear()}/${pad0(datetime.getMonth())}/${pad0(datetime.getDay())}`; + const datetime = new Date(); + const dir = `./data/${datetime.getFullYear()}/${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('./sample_data/article',dir); \ No newline at end of file + copy('./sample_data/article',dir); +} \ No newline at end of file From fe6cca6453ecf2318c2f0caad2cc04d6ca3d4c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 10:07:22 +0200 Subject: [PATCH 14/42] app default endpoints --- package-lock.json | 5 ++ package.json | 1 + src/app.js | 52 ++++++++++++++++++-- src/config.default.json | 26 +++++----- test/app.test.js | 100 ++++++++++++++++++++++++++++++++++++--- test/file_walker.test.js | 38 +++++---------- test/test_utils.js | 19 ++++++++ 7 files changed, 193 insertions(+), 48 deletions(-) create mode 100644 test/test_utils.js diff --git a/package-lock.json b/package-lock.json index 27426e9..7967736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2869,6 +2869,11 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "ejs": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.2.tgz", + "integrity": "sha512-PcW2a0tyTuPHz3tWyYqtK6r1fZ3gp+3Sop8Ph+ZYN81Ob5rwmbHEzaqs10N3BEsaGTkh/ooniXK+WwszGlc2+Q==" + }, "electron-to-chromium": { "version": "1.3.165", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.165.tgz", diff --git a/package.json b/package.json index 4f8859b..7ca4d39 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "A static blog using Markdown pulled from your git repository.", "main": "src/server.js", "dependencies": { + "ejs": "^2.6.2", "express": "^4.17.1", "ncp": "^2.0.0", "showdown": "^1.9.0", diff --git a/src/app.js b/src/app.js index 3ff4815..06308eb 100644 --- a/src/app.js +++ b/src/app.js @@ -1,5 +1,7 @@ const express = require('express'); const app = express(); +const fs = require('fs'); +const path = require('path'); const cons = { ok: '\x1b[32m✔\x1b[0m %s', @@ -10,8 +12,17 @@ const cons = { module.exports = (config) => { const fw = require('./file_walker')(config); + app.set('view engine', config['view_engine']); + app.set('views', path.join(__dirname, '..')); + const articles = []; + const log = (status, msg) => { + if (config['test']) + return; + console.log(status, msg); + }; + const reload = (callback) => { fw.fetchArticles((err, list) => { if (err) { @@ -20,22 +31,55 @@ module.exports = (config) => { } articles.splice(0, articles.length, ...list); if (articles.length > 0) - console.log(cons.ok, `loaded ${articles.length} article${articles.length > 1 ? 's' : ''}`); + log(cons.ok, `loaded ${articles.length} article${articles.length > 1 ? 's' : ''}`); else - console.log(cons.warn, `no articles loaded, check your configuration`); + log(cons.warn, `no articles loaded, check your configuration`); callback(true); }); }; + const render = (res, path, data, code = 200) => { + res.render(path, data, (err, html) => { + if (err) { + res.sendStatus(500); + log(cons.error, `failed to render ${path} : ${err}`); + } else + res.status(code).send(html); + }); + }; + + const showError = (path, code, res) => { + const errorPath = `${config['data_dir']}/${config['home']['error']}`; + if (fs.existsSync(errorPath)) + render(res, errorPath, {error: code, path: path}, code); + else + res.sendStatus(code); + }; + app.get('/', (req, res) => { - res.status(200).send('Hello World!'); + const homePath = `${config['data_dir']}/${config['home']['index']}`; + if (fs.existsSync(homePath)) + render(res, homePath, {articles: articles}); + else { + showError(req.path, 404, res); + } + }); + + app.get('*', express.static(config['data_dir'])); + + app.get('*', (req, res) => { + showError(req.path, 404, res); + }); + + app.all('*', (req, res) => { + res.status(400).send('bad request'); }); app.start = () => { reload((res) => { if (res) app.listen(config['node_port'], () => { - console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); + log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); }); }); }; diff --git a/src/config.default.json b/src/config.default.json index 2ee6f02..7427b2c 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -1,25 +1,27 @@ { "node_port": 3000, "data_dir": "data", - "modules" : { - "plantuml" : false, + "view_engine": "ejs", + "modules": { + "plantuml": false, "rss": true, "webhook": true }, - "home" : { - "index" : "index.ejs" + "home": { + "index": "index.ejs", + "error": "error.ejs" }, - "article" : { - "index" : "index.md", - "thumbnail_tag" : "thumbnail", + "article": { + "index": "index.md", + "thumbnail_tag": "thumbnail", "default_title": "Untitled", - "default_thumbnail" : null + "default_thumbnail": null }, - "rss" : { - "endpoint" : "/rss", - "length" : 10 + "rss": { + "endpoint": "/rss", + "length": 10 }, - "webhook" : { + "webhook": { "endpoint": "/webhook", "secretFile": "git_secret" } diff --git a/test/app.test.js b/test/app.test.js index 8e33e19..03cf8a3 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,15 +1,103 @@ /* jshint -W117 */ const request = require('supertest'); -const app = require('../src/app')({ +const fs = require('fs'); +const utils = require('./test_utils'); +const dataDir = './test_data'; +const testIndex = 'testindex.ejs'; +const testError = 'testerror.ejs'; + +const config = { + 'test': true, + 'data_dir': dataDir, + 'view_engine': 'ejs', + 'home': { + 'index': testIndex, + 'error': testError + } +}; + +const app = require('../src/app')(config); + +beforeEach(() => { + utils.deleteFolderSync(dataDir); + fs.mkdirSync(dataDir); +}); + +afterAll(() => { + if (fs.existsSync(dataDir)) { + utils.deleteFolderSync(dataDir); + } }); describe('Test root path', () => { - test('GET / 200', done => { - request(app).get('/').then(response => { - expect(response.statusCode).toBe(200); - 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(`${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('200 index page', done => { + fs.writeFileSync(`${dataDir}/${testIndex}`, 'hello there'); + request(app).get('/').then(response => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('hello there'); + done(); + }); + }); + //TODO test articles list +}); + +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 but error page', done => { + fs.writeFileSync(`${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('200 valid file', done => { + fs.writeFileSync(`${dataDir}/somefile.txt`, 'filecontent'); + request(app).get('/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 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(); + }); + }); +}); diff --git a/test/file_walker.test.js b/test/file_walker.test.js index e1a1c1a..4500d87 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -1,5 +1,6 @@ /* jshint -W117 */ const fs = require('fs'); +const utils = require('./test_utils'); const dataDir = './test_data'; const testIndex = 'testindex.md'; @@ -17,32 +18,17 @@ const config = { const fw = require('../src/file_walker')(config); -const deleteFolderSync = (path) => { - if (!fs.existsSync(path)) - return; - fs.readdirSync(path, {withFileTypes: true}).forEach((item) => { - if (item.isDirectory()) - deleteFolderSync(`${path}/${item.name}`); - else - fs.unlinkSync(`${path}/${item.name}`); - }); - fs.rmdirSync(path); -}; - beforeEach(() => { - deleteFolderSync(dataDir); + utils.deleteFolderSync(dataDir); fs.mkdirSync(dataDir); }); afterAll(() => { if (fs.existsSync(dataDir)) { - deleteFolderSync(dataDir); + utils.deleteFolderSync(dataDir); } }); -const createEmptyDirs = list => list.forEach(path => fs.mkdirSync(path, {recursive: true})); -const createEmptyFiles = list => list.forEach(file => fs.writeFileSync(file, '')); - describe('Test function fileTree', () => { test('empty root', (done) => { fw.fileTree(dataDir, (err, list) => { @@ -53,7 +39,7 @@ describe('Test function fileTree', () => { }); }); test('empty folders', (done) => { - createEmptyDirs([ + utils.createEmptyDirs([ `${dataDir}/test/test`, `${dataDir}/test/test2`, `${dataDir}/test2` @@ -70,7 +56,7 @@ describe('Test function fileTree', () => { `${dataDir}/f1.txt`, `${dataDir}/f2.txt` ]; - createEmptyFiles(fileList); + utils.createEmptyFiles(fileList); fw.fileTree(dataDir, (err, list) => { expect(err).toBeNull(); expect(list).toBeDefined(); @@ -80,7 +66,7 @@ describe('Test function fileTree', () => { }); }); test('nested files', (done) => { - createEmptyDirs([ + utils.createEmptyDirs([ `${dataDir}/test/test`, `${dataDir}/test2` ]); @@ -90,7 +76,7 @@ describe('Test function fileTree', () => { `${dataDir}/test/test/f3.txt`, `${dataDir}/test2/f4.txt` ]; - createEmptyFiles(fileList); + utils.createEmptyFiles(fileList); fw.fileTree(dataDir, (err, list) => { expect(err).toBeNull(); expect(list).toBeDefined(); @@ -212,11 +198,11 @@ describe('Test article fetching', () => { }); }); test('misplaced index file', (done) => { - createEmptyDirs([ + utils.createEmptyDirs([ `${dataDir}/test/test`, `${dataDir}/2019/05/05`, ]); - createEmptyFiles([ + utils.createEmptyFiles([ `${dataDir}/${testIndex}`, `${dataDir}/test/test/${testIndex}`, `${dataDir}/2019/05/${testIndex}`, @@ -229,11 +215,11 @@ describe('Test article fetching', () => { }); }); test('empty index file', (done) => { - createEmptyDirs([ + utils.createEmptyDirs([ `${dataDir}/2019/05/05`, ]); const file = `${dataDir}/2019/05/05/${testIndex}`; - createEmptyFiles([file]); + utils.createEmptyFiles([file]); fw.fetchArticles((err, list) => { expect(err).toBeNull(); expect(list).toBeDefined(); @@ -250,7 +236,7 @@ describe('Test article fetching', () => { }); }); test('correct index file', (done) => { - createEmptyDirs([ + utils.createEmptyDirs([ `${dataDir}/2019/05/05`, ]); const file = `${dataDir}/2019/05/05/${testIndex}`; diff --git a/test/test_utils.js b/test/test_utils.js new file mode 100644 index 0000000..e03ef7a --- /dev/null +++ b/test/test_utils.js @@ -0,0 +1,19 @@ +const fs = require('fs'); + +const deleteFolderSync = (path) => { + if (!fs.existsSync(path)) + return; + fs.readdirSync(path, {withFileTypes: true}).forEach((item) => { + if (item.isDirectory()) + deleteFolderSync(`${path}/${item.name}`); + else + fs.unlinkSync(`${path}/${item.name}`); + }); + fs.rmdirSync(path); +}; + +module.exports = { + deleteFolderSync: deleteFolderSync, + createEmptyDirs: list => list.forEach(path => fs.mkdirSync(path, {recursive: true})), + createEmptyFiles: list => list.forEach(file => fs.writeFileSync(file, '')), +}; \ No newline at end of file From 07dd06a338ba0e06e31e6fd6822b0262847a901c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 10:40:48 +0200 Subject: [PATCH 15/42] use of path.join everywhere --- src/app.js | 19 +++++-------- src/config.js | 2 +- src/file_walker.js | 24 ++++++++-------- src/postinstall.js | 21 +++++++------- test/app.test.js | 9 +++--- test/config.test.js | 4 +-- test/file_walker.test.js | 61 ++++++++++++++++++++-------------------- test/test_utils.js | 13 +++++---- 8 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/app.js b/src/app.js index 06308eb..e825e6d 100644 --- a/src/app.js +++ b/src/app.js @@ -17,11 +17,6 @@ module.exports = (config) => { const articles = []; - const log = (status, msg) => { - if (config['test']) - return; - console.log(status, msg); - }; const reload = (callback) => { fw.fetchArticles((err, list) => { @@ -31,9 +26,9 @@ module.exports = (config) => { } articles.splice(0, articles.length, ...list); if (articles.length > 0) - log(cons.ok, `loaded ${articles.length} article${articles.length > 1 ? 's' : ''}`); + console.log(cons.ok, `loaded ${articles.length} article${articles.length > 1 ? 's' : ''}`); else - log(cons.warn, `no articles loaded, check your configuration`); + console.log(cons.warn, `no articles loaded, check your configuration`); callback(true); }); }; @@ -42,16 +37,16 @@ module.exports = (config) => { res.render(path, data, (err, html) => { if (err) { res.sendStatus(500); - log(cons.error, `failed to render ${path} : ${err}`); + console.log(cons.error, `failed to render ${path} : ${err}`); } else res.status(code).send(html); }); }; - const showError = (path, code, res) => { - const errorPath = `${config['data_dir']}/${config['home']['error']}`; + const showError = (resPath, code, res) => { + const errorPath = path.join(config['data_dir'],config['home']['error']); if (fs.existsSync(errorPath)) - render(res, errorPath, {error: code, path: path}, code); + render(res, errorPath, {error: code, path: resPath}, code); else res.sendStatus(code); }; @@ -79,7 +74,7 @@ module.exports = (config) => { reload((res) => { if (res) app.listen(config['node_port'], () => { - log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); + console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); }); }); }; diff --git a/src/config.js b/src/config.js index ac75ed5..fd44034 100644 --- a/src/config.js +++ b/src/config.js @@ -15,7 +15,7 @@ const merge = (ref, src) => { module.exports = () => { try { - let configData = fs.readFileSync('./config.json', {encoding:'UTF-8'}); + let configData = fs.readFileSync('config.json', {encoding:'UTF-8'}); let config = JSON.parse(configData); return merge(refConfig, config); } catch (error) { diff --git a/src/file_walker.js b/src/file_walker.js index 1b1766b..52ac51c 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -1,15 +1,16 @@ const fs = require('fs'); +const path = require('path'); -const getFileTree = (path, cb) => { +const getFileTree = (dir, cb) => { let list = []; let remaining = 0; - fs.readdir(path, {withFileTypes: true}, (err, items) => { + fs.readdir(dir, {withFileTypes: true}, (err, items) => { if (err) return cb(err); items.forEach((item) => { if (item.isDirectory()) { remaining++; - getFileTree(`${path}/${item.name}`, (err, out) => { + getFileTree(path.join(dir, item.name), (err, out) => { if (err) return cb(err); list.push(...out); @@ -18,7 +19,7 @@ const getFileTree = (path, cb) => { cb(null, list); }); } else { - list.push(`${path}/${item.name}`); + list.push(path.join(dir, item.name)); } }); if (remaining === 0) @@ -57,24 +58,25 @@ module.exports = (config) => { .filter(path => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) .map(path => path.substr(0, path.length - config['article']['index'].length)) .map(path => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) - .filter(path => path && path.length > 1); + .filter(matches => matches && matches.length > 1); if (paths.length === 0) cb(null, []); const list = []; let remaining = 0; - paths.forEach(path => { + paths.forEach(matches => { const article = { - path: config['data_dir'] + path[0] + config['article']['index'], - year: parseInt(path[1]), - month: parseInt(path[2]), - day: parseInt(path[3]) + path: path.join(config['data_dir'], matches[1], matches[2], matches[3], config['article']['index']), + parent: path.join(config['data_dir'], matches[1], matches[2], matches[3]), + year: parseInt(matches[1]), + month: parseInt(matches[2]), + day: parseInt(matches[3]) }; remaining++; readIndexFile(article.path, config['article']['thumbnail_tag'], (err, info) => { if (err) return cb(err); article.title = info.title || config['article']['default_title']; - article.thumbnail = info.thumbnail ? (config['data_dir'] + path[0] + info.thumbnail) : config['article']['default_thumbnail']; + article.thumbnail = info.thumbnail ? path.join(article.parent, info.thumbnail) : config['article']['default_thumbnail']; list.push(article); remaining--; if (remaining === 0) diff --git a/src/postinstall.js b/src/postinstall.js index 7aeedcf..8654aa5 100644 --- a/src/postinstall.js +++ b/src/postinstall.js @@ -1,29 +1,30 @@ const fs = require('fs'); +const path = require('path'); const ncp = require('ncp').ncp; -const copy = (src,dest) => { - ncp(src,dest, function(err){ - if(err) +const copy = (src, dest) => { + ncp(src, dest, function (err) { + if (err) console.error(err); else console.log(`copied ${src} to ${dest}`); }); }; -copy('./src/config.default.json','./config.example.json'); +copy(path.join('src', 'config.default.json'), 'config.example.json'); -if (!fs.existsSync('./data')) { - fs.mkdirSync('./data'); +if (!fs.existsSync('data')) { + fs.mkdirSync('data'); - copy('./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 = `./data/${datetime.getFullYear()}/${pad0(datetime.getMonth()+1)}/${pad0(datetime.getDate())}`; + const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate())); if (!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true}); - copy('./sample_data/article',dir); + copy(path.join('sample_data','article'), dir); } \ No newline at end of file diff --git a/test/app.test.js b/test/app.test.js index 03cf8a3..f9f2eae 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -1,9 +1,10 @@ /* jshint -W117 */ const request = require('supertest'); const fs = require('fs'); +const path = require('path'); const utils = require('./test_utils'); -const dataDir = './test_data'; +const dataDir = 'test_data'; const testIndex = 'testindex.ejs'; const testError = 'testerror.ejs'; @@ -38,7 +39,7 @@ describe('Test root path', () => { }); }); test('404 no index but error page', done => { - fs.writeFileSync(`${dataDir}/${testError}`, 'error <%= error %> at <%= path %>'); + 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 /'); @@ -46,7 +47,7 @@ describe('Test root path', () => { }); }); test('200 index page', done => { - fs.writeFileSync(`${dataDir}/${testIndex}`, 'hello there'); + fs.writeFileSync(path.join(dataDir,testIndex), 'hello there'); request(app).get('/').then(response => { expect(response.statusCode).toBe(200); expect(response.text).toBe('hello there'); @@ -64,7 +65,7 @@ describe('Test static files', () => { }); }); test('404 invalid file but error page', done => { - fs.writeFileSync(`${dataDir}/${testError}`, 'error <%= error %> at <%= path %>'); + 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'); diff --git a/test/config.test.js b/test/config.test.js index 5f61b3f..ad60ea4 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -1,8 +1,8 @@ /* jshint -W117 */ const fs = require('fs'); -const configFile = './config.json'; -const tmpConfigFile = './config.temp.json'; +const configFile = 'config.json'; +const tmpConfigFile = 'config.temp.json'; beforeAll(() => { if (fs.existsSync(configFile)) { diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 4500d87..b60131a 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -1,8 +1,9 @@ /* jshint -W117 */ const fs = require('fs'); +const path = require('path'); const utils = require('./test_utils'); -const dataDir = './test_data'; +const dataDir = 'test_data'; const testIndex = 'testindex.md'; const config = { @@ -40,9 +41,9 @@ describe('Test function fileTree', () => { }); test('empty folders', (done) => { utils.createEmptyDirs([ - `${dataDir}/test/test`, - `${dataDir}/test/test2`, - `${dataDir}/test2` + path.join(dataDir, 'test', 'test'), + path.join(dataDir, 'test', 'test2'), + path.join(dataDir, 'test2') ]); fw.fileTree(dataDir, (err, list) => { expect(err).toBeNull(); @@ -53,8 +54,8 @@ describe('Test function fileTree', () => { }); test('simple files', (done) => { const fileList = [ - `${dataDir}/f1.txt`, - `${dataDir}/f2.txt` + path.join(dataDir, 'f1.txt'), + path.join(dataDir, 'f2.txt') ]; utils.createEmptyFiles(fileList); fw.fileTree(dataDir, (err, list) => { @@ -67,14 +68,14 @@ describe('Test function fileTree', () => { }); test('nested files', (done) => { utils.createEmptyDirs([ - `${dataDir}/test/test`, - `${dataDir}/test2` + path.join(dataDir, 'test', 'test'), + path.join(dataDir, 'test2') ]); const fileList = [ - `${dataDir}/f1.txt`, - `${dataDir}/test/f2.txt`, - `${dataDir}/test/test/f3.txt`, - `${dataDir}/test2/f4.txt` + 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) => { @@ -95,7 +96,7 @@ describe('Test function fileTree', () => { }); describe('Test index article reading', () => { - const file = `${dataDir}/${testIndex}`; + const file = path.join(dataDir, testIndex); test('invalid file', (done) => { fw.readIndexFile('invalid file', 'thumbnail', (err, info) => { @@ -199,13 +200,13 @@ describe('Test article fetching', () => { }); test('misplaced index file', (done) => { utils.createEmptyDirs([ - `${dataDir}/test/test`, - `${dataDir}/2019/05/05`, + path.join(dataDir, 'test', 'test'), + path.join(dataDir, '2019', '05', '05') ]); utils.createEmptyFiles([ - `${dataDir}/${testIndex}`, - `${dataDir}/test/test/${testIndex}`, - `${dataDir}/2019/05/${testIndex}`, + path.join(dataDir, testIndex), + path.join(dataDir, 'test', 'test', testIndex), + path.join(dataDir, '2019', '05', testIndex) ]); fw.fetchArticles((err, list) => { expect(err).toBeNull(); @@ -215,10 +216,9 @@ describe('Test article fetching', () => { }); }); test('empty index file', (done) => { - utils.createEmptyDirs([ - `${dataDir}/2019/05/05`, - ]); - const file = `${dataDir}/2019/05/05/${testIndex}`; + const dir = path.join(dataDir, '2019', '05', '05'); + const file = path.join(dir, testIndex); + utils.createEmptyDirs([dir]); utils.createEmptyFiles([file]); fw.fetchArticles((err, list) => { expect(err).toBeNull(); @@ -226,20 +226,20 @@ describe('Test article fetching', () => { expect(list.length).toBe(1); expect(list[0]).toEqual({ path: file, + parent:dir, year: 2019, month: 5, day: 5, - title:'Untitled', - thumbnail:'default.png' + title: 'Untitled', + thumbnail: 'default.png' }); done(); }); }); test('correct index file', (done) => { - utils.createEmptyDirs([ - `${dataDir}/2019/05/05`, - ]); - const file = `${dataDir}/2019/05/05/${testIndex}`; + const dir = path.join(dataDir, '2019', '05', '05'); + const file = path.join(dir, testIndex); + utils.createEmptyDirs([dir]); fs.writeFileSync(file, ` # Title ![thumbnail](./thumbnail.jpg) @@ -251,11 +251,12 @@ describe('Test article fetching', () => { expect(list.length).toBe(1); expect(list[0]).toEqual({ path: file, + parent:dir, year: 2019, month: 5, day: 5, - title:'Title', - thumbnail:`${dataDir}/2019/05/05/./thumbnail.jpg` + title: 'Title', + thumbnail: path.join(dataDir, '2019', '05', '05', './thumbnail.jpg') }); done(); }); diff --git a/test/test_utils.js b/test/test_utils.js index e03ef7a..5b39983 100644 --- a/test/test_utils.js +++ b/test/test_utils.js @@ -1,15 +1,16 @@ const fs = require('fs'); +const path = require('path'); -const deleteFolderSync = (path) => { - if (!fs.existsSync(path)) +const deleteFolderSync = (dir) => { + if (!fs.existsSync(dir)) return; - fs.readdirSync(path, {withFileTypes: true}).forEach((item) => { + fs.readdirSync(dir, {withFileTypes: true}).forEach((item) => { if (item.isDirectory()) - deleteFolderSync(`${path}/${item.name}`); + deleteFolderSync(path.join(dir,item.name)); else - fs.unlinkSync(`${path}/${item.name}`); + fs.unlinkSync(path.join(dir,item.name)); }); - fs.rmdirSync(path); + fs.rmdirSync(dir); }; module.exports = { From f9a2db38d2210454f7665123309701f41e5fc642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 10:49:51 +0200 Subject: [PATCH 16/42] code convention --- src/app.js | 21 ++++++++++++--------- src/config.js | 2 +- src/file_walker.js | 12 ++++++------ src/postinstall.js | 2 +- test/app.test.js | 36 ++++++++++++++++++------------------ test/test_utils.js | 4 ++-- 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/app.js b/src/app.js index e825e6d..e51e450 100644 --- a/src/app.js +++ b/src/app.js @@ -45,19 +45,22 @@ module.exports = (config) => { const showError = (resPath, code, res) => { const errorPath = path.join(config['data_dir'],config['home']['error']); - if (fs.existsSync(errorPath)) - render(res, errorPath, {error: code, path: resPath}, code); - else - res.sendStatus(code); + fs.access(errorPath, (err) => { + if(err) + res.sendStatus(code); + else + render(res, errorPath, {error: code, path: resPath}, code); + }); }; app.get('/', (req, res) => { const homePath = `${config['data_dir']}/${config['home']['index']}`; - if (fs.existsSync(homePath)) - render(res, homePath, {articles: articles}); - else { - showError(req.path, 404, res); - } + fs.access(homePath,(err)=>{ + if(err) + showError(req.path, 404, res); + else + render(res, homePath, {articles: articles}); + }); }); app.get('*', express.static(config['data_dir'])); diff --git a/src/config.js b/src/config.js index fd44034..143af88 100644 --- a/src/config.js +++ b/src/config.js @@ -6,7 +6,7 @@ const merge = (ref, src) => { return ref; } else if (typeof ref === 'object') { const out = {}; - Object.keys(ref).forEach(key => out[key] = merge(ref[key], src[key])); + Object.keys(ref).forEach((key) =>out[key] = merge(ref[key], src[key])); return out; } else { return src; diff --git a/src/file_walker.js b/src/file_walker.js index 52ac51c..5ce2e57 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -54,16 +54,16 @@ module.exports = (config) => { if (err) return cb(err); const paths = fileList - .map(path => path.substr(config['data_dir'].length)) - .filter(path => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) - .map(path => path.substr(0, path.length - config['article']['index'].length)) - .map(path => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) - .filter(matches => matches && matches.length > 1); + .map((path) =>path.substr(config['data_dir'].length)) + .filter((path) =>path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) + .map((path) =>path.substr(0, path.length - config['article']['index'].length)) + .map((path) =>path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) + .filter((matches) =>matches && matches.length > 1); if (paths.length === 0) cb(null, []); const list = []; let remaining = 0; - paths.forEach(matches => { + paths.forEach((matches) =>{ const article = { path: path.join(config['data_dir'], matches[1], matches[2], matches[3], config['article']['index']), parent: path.join(config['data_dir'], matches[1], matches[2], matches[3]), diff --git a/src/postinstall.js b/src/postinstall.js index 8654aa5..e9acd6e 100644 --- a/src/postinstall.js +++ b/src/postinstall.js @@ -18,7 +18,7 @@ if (!fs.existsSync('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())); diff --git a/test/app.test.js b/test/app.test.js index f9f2eae..62e4832 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -32,23 +32,23 @@ afterAll(() => { }); describe('Test root path', () => { - test('404 no index no error', done => { - request(app).get('/').then(response => { + 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 => { + test('404 no index but error page', (done) =>{ fs.writeFileSync(path.join(dataDir,testError), 'error <%= error %> at <%= path %>'); - request(app).get('/').then(response => { + request(app).get('/').then((response) =>{ expect(response.statusCode).toBe(404); expect(response.text).toBe('error 404 at /'); done(); }); }); - test('200 index page', done => { + test('200 index page', (done) =>{ fs.writeFileSync(path.join(dataDir,testIndex), 'hello there'); - request(app).get('/').then(response => { + request(app).get('/').then((response) =>{ expect(response.statusCode).toBe(200); expect(response.text).toBe('hello there'); done(); @@ -58,23 +58,23 @@ describe('Test root path', () => { }); describe('Test static files', () => { - test('404 invalid file no error page', done => { - request(app).get('/somefile.txt').then(response => { + 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 => { + 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 => { + request(app).get('/somefile.txt').then((response) =>{ expect(response.statusCode).toBe(404); expect(response.text).toBe('error 404 at /somefile.txt'); done(); }); }); - test('200 valid file', done => { + test('200 valid file', (done) =>{ fs.writeFileSync(`${dataDir}/somefile.txt`, 'filecontent'); - request(app).get('/somefile.txt').then(response => { + request(app).get('/somefile.txt').then((response) =>{ expect(response.statusCode).toBe(200); expect(response.text).toBe('filecontent'); done(); @@ -83,20 +83,20 @@ describe('Test static files', () => { }); describe('Test other requests', () => { - test('400 POST', done => { - request(app).post('/').then(response => { + test('400 POST', (done) =>{ + request(app).post('/').then((response) =>{ expect(response.statusCode).toBe(400); done(); }); }); - test('400 PUT', done => { - request(app).put('/').then(response => { + test('400 PUT', (done) =>{ + request(app).put('/').then((response) =>{ expect(response.statusCode).toBe(400); done(); }); }); - test('400 DELETE', done => { - request(app).delete('/').then(response => { + test('400 DELETE', (done) =>{ + request(app).delete('/').then((response) =>{ expect(response.statusCode).toBe(400); done(); }); diff --git a/test/test_utils.js b/test/test_utils.js index 5b39983..a80a886 100644 --- a/test/test_utils.js +++ b/test/test_utils.js @@ -15,6 +15,6 @@ const deleteFolderSync = (dir) => { module.exports = { deleteFolderSync: deleteFolderSync, - createEmptyDirs: list => list.forEach(path => fs.mkdirSync(path, {recursive: true})), - createEmptyFiles: list => list.forEach(file => fs.writeFileSync(file, '')), + createEmptyDirs: (list) =>list.forEach((path) =>fs.mkdirSync(path, {recursive: true})), + createEmptyFiles: (list) =>list.forEach((file) =>fs.writeFileSync(file, '')), }; \ No newline at end of file From dcb4e0144703853aeabe8bd54e884eb286837706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 10:50:48 +0200 Subject: [PATCH 17/42] added readability check for fs.access --- src/app.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app.js b/src/app.js index e51e450..3c645b2 100644 --- a/src/app.js +++ b/src/app.js @@ -44,9 +44,9 @@ module.exports = (config) => { }; const showError = (resPath, code, res) => { - const errorPath = path.join(config['data_dir'],config['home']['error']); - fs.access(errorPath, (err) => { - if(err) + 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); @@ -55,8 +55,8 @@ module.exports = (config) => { app.get('/', (req, res) => { const homePath = `${config['data_dir']}/${config['home']['index']}`; - fs.access(homePath,(err)=>{ - if(err) + fs.access(homePath, fs.constants.R_OK, (err) => { + if (err) showError(req.path, 404, res); else render(res, homePath, {articles: articles}); From e88eb94d78a7e2d65ae673e326468f058beefbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 11:42:01 +0200 Subject: [PATCH 18/42] Home minimal EJS template --- sample_data/home/error.ejs | 12 ++++++++++++ sample_data/home/index.ejs | 17 +++++++++++++++-- sample_data/home/style.css | 18 ++++++++++++++++++ src/file_walker.js | 23 +++++++++++++---------- test/file_walker.test.js | 22 ++++++++++++++-------- 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 sample_data/home/error.ejs create mode 100644 sample_data/home/style.css diff --git a/sample_data/home/error.ejs b/sample_data/home/error.ejs new file mode 100644 index 0000000..c2471f6 --- /dev/null +++ b/sample_data/home/error.ejs @@ -0,0 +1,12 @@ + + + + + Error <%= error %> + + +
+

Error <%= error %> at path <%= path %>

+
+ + \ No newline at end of file diff --git a/sample_data/home/index.ejs b/sample_data/home/index.ejs index 566549b..25c46d0 100644 --- a/sample_data/home/index.ejs +++ b/sample_data/home/index.ejs @@ -2,9 +2,22 @@ - Title + GitBlog.md - Home + - +
+

GitBlog.md

+ A static blog using Markdown pulled from your git repository +

Articles in this blog :

+ <% articles.forEach((article) => { %> +
+

<%- `${article.title}`%> (<%= `${article.day}/${article.month}/${article.year}`%>)

+ <% if(article.thumbnail){%> + <%- `thumbnail`%> + <% }%> +
+ <% }); %> +
\ No newline at end of file diff --git a/sample_data/home/style.css b/sample_data/home/style.css new file mode 100644 index 0000000..979ab2a --- /dev/null +++ b/sample_data/home/style.css @@ -0,0 +1,18 @@ +main { + max-width: 70ch; + padding: 2ch; + margin: auto; +} + +.article a, .article a:visited{ + color:black; +} + +.article a:visited{ + color:black; +} + +.article img{ + max-width:100%; + max-height:10vh; +} \ No newline at end of file diff --git a/src/file_walker.js b/src/file_walker.js index 5ce2e57..76647ba 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -54,29 +54,32 @@ module.exports = (config) => { if (err) return cb(err); const paths = fileList - .map((path) =>path.substr(config['data_dir'].length)) - .filter((path) =>path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) - .map((path) =>path.substr(0, path.length - config['article']['index'].length)) - .map((path) =>path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) - .filter((matches) =>matches && matches.length > 1); + .map((path) => path.substr(config['data_dir'].length)) + .filter((path) => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) + .map((path) => path.substr(0, path.length - config['article']['index'].length)) + .map((path) => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) + .filter((matches) => matches && matches.length > 1); if (paths.length === 0) cb(null, []); const list = []; let remaining = 0; - paths.forEach((matches) =>{ + paths.forEach((matches) => { const article = { - path: path.join(config['data_dir'], matches[1], matches[2], matches[3], config['article']['index']), - parent: path.join(config['data_dir'], matches[1], matches[2], matches[3]), + path: path.join(matches[1], matches[2], matches[3]), + realPath: path.join(config['data_dir'], matches[1], matches[2], matches[3]), year: parseInt(matches[1]), month: parseInt(matches[2]), day: parseInt(matches[3]) }; + article.date = new Date(article.year, article.month, article.day); remaining++; - readIndexFile(article.path, config['article']['thumbnail_tag'], (err, info) => { + 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 ? path.join(article.parent, info.thumbnail) : config['article']['default_thumbnail']; + article.thumbnail = info.thumbnail ? path.join(article.path, info.thumbnail) : config['article']['default_thumbnail']; + article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_'); + article.url = path.join(article.path, article.escapedTitle); list.push(article); remaining--; if (remaining === 0) diff --git a/test/file_walker.test.js b/test/file_walker.test.js index b60131a..c36a58e 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -225,13 +225,16 @@ describe('Test article fetching', () => { expect(list).toBeDefined(); expect(list.length).toBe(1); expect(list[0]).toEqual({ - path: file, - parent:dir, + path: path.join('2019', '05', '05'), + realPath: dir, year: 2019, month: 5, day: 5, + date : new Date(2019,5,5), title: 'Untitled', - thumbnail: 'default.png' + thumbnail: 'default.png', + escapedTitle: 'untitled', + url: path.join('2019', '05', '05', 'untitled'), }); done(); }); @@ -241,7 +244,7 @@ describe('Test article fetching', () => { const file = path.join(dir, testIndex); utils.createEmptyDirs([dir]); fs.writeFileSync(file, ` - # Title + # Title with : info ! ![thumbnail](./thumbnail.jpg) this is some text `); @@ -250,13 +253,16 @@ describe('Test article fetching', () => { expect(list).toBeDefined(); expect(list.length).toBe(1); expect(list[0]).toEqual({ - path: file, - parent:dir, + path: path.join('2019', '05', '05'), + realPath: dir, year: 2019, month: 5, day: 5, - title: 'Title', - thumbnail: path.join(dataDir, '2019', '05', '05', './thumbnail.jpg') + date : new Date(2019,5,5), + title: 'Title with : info !', + thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), + escapedTitle: 'title_with___info', + url: path.join('2019', '05', '05', 'title_with___info'), }); done(); }); From c755ea939d44004bb425f6fd94f77499dd6ea95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 13:05:18 +0200 Subject: [PATCH 19/42] Hide given file extensions --- src/app.js | 10 ++++++++- src/config.default.json | 3 ++- test/app.test.js | 45 +++++++++++++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/app.js b/src/app.js index 3c645b2..b7d432b 100644 --- a/src/app.js +++ b/src/app.js @@ -17,7 +17,6 @@ module.exports = (config) => { const articles = []; - const reload = (callback) => { fw.fetchArticles((err, list) => { if (err) { @@ -32,6 +31,8 @@ module.exports = (config) => { callback(true); }); }; + if (config['test']) + app.reload = reload; const render = (res, path, data, code = 200) => { res.render(path, data, (err, html) => { @@ -63,6 +64,13 @@ module.exports = (config) => { }); }); + app.get('*', (req, res, next) => { + if (config['home']['hidden'].includes(path.extname(req.path))) + showError(req.path, 404, res); + else + next(); + }); + app.get('*', express.static(config['data_dir'])); app.get('*', (req, res) => { diff --git a/src/config.default.json b/src/config.default.json index 7427b2c..ece0152 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -9,7 +9,8 @@ }, "home": { "index": "index.ejs", - "error": "error.ejs" + "error": "error.ejs", + "hidden": [".ejs"] }, "article": { "index": "index.md", diff --git a/test/app.test.js b/test/app.test.js index 62e4832..ae3b5de 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -14,8 +14,15 @@ const config = { 'view_engine': 'ejs', 'home': { 'index': testIndex, - 'error': testError - } + 'error': testError, + 'hidden': ['.ejs','.test'] + }, + 'article': { + 'index': 'index.md', + 'thumbnail_tag': 'thumbnail', + 'default_title': 'Untitled', + 'default_thumbnail': null + }, }; const app = require('../src/app')(config); @@ -46,15 +53,34 @@ describe('Test root path', () => { done(); }); }); - test('200 index page', (done) =>{ - fs.writeFileSync(path.join(dataDir,testIndex), 'hello there'); + 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('hello there'); + expect(response.text).toBe('articles 0'); done(); }); }); - //TODO test articles list + test('200 2 articles', (done) =>{ + 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((res) => { + expect(res).toBe(true); + request(app).get('/').then((response) =>{ + expect(response.statusCode).toBe(200); + expect(response.text).toBe('articles 2'); + done(); + }); + }); + + }); }); describe('Test static files', () => { @@ -72,6 +98,13 @@ describe('Test static files', () => { 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('200 valid file', (done) =>{ fs.writeFileSync(`${dataDir}/somefile.txt`, 'filecontent'); request(app).get('/somefile.txt').then((response) =>{ From 69d05e39002dbd2fb67f7b7072b541c716430328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 13:15:51 +0200 Subject: [PATCH 20/42] Comments are good --- src/app.js | 30 +++++++++++++++++++++++++++++- src/config.js | 6 ++++++ src/file_walker.js | 15 +++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index b7d432b..257490c 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,10 @@ const app = express(); const fs = require('fs'); const path = require('path'); +/** + * Terminal colors and symbols to display status messages + * @type {{warn: string, ok: string, error: string}} + */ const cons = { ok: '\x1b[32m✔\x1b[0m %s', warn: '\x1b[33m⚠\x1b[0m %s', @@ -12,11 +16,17 @@ const cons = { module.exports = (config) => { const fw = require('./file_walker')(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 = []; + /** + * Fetch articles from the data folder and send success as a response + * @param callback + */ const reload = (callback) => { fw.fetchArticles((err, list) => { if (err) { @@ -34,6 +44,13 @@ module.exports = (config) => { if (config['test']) app.reload = reload; + /** + * Render the page with the view engine and catch errors + * @param res + * @param path - path of the view + * @param data - data to pass to the view + * @param code - code to send along the page + */ const render = (res, path, data, code = 200) => { res.render(path, data, (err, html) => { if (err) { @@ -44,6 +61,12 @@ module.exports = (config) => { }); }; + /** + * 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) => { @@ -54,6 +77,7 @@ module.exports = (config) => { }); }; + // home endpoint : send the correct index page or error if not existing app.get('/', (req, res) => { const homePath = `${config['data_dir']}/${config['home']['index']}`; fs.access(homePath, fs.constants.R_OK, (err) => { @@ -64,6 +88,7 @@ module.exports = (config) => { }); }); + // catch all gets and return 404 if it's an hidden file type app.get('*', (req, res, next) => { if (config['home']['hidden'].includes(path.extname(req.path))) showError(req.path, 404, res); @@ -71,16 +96,19 @@ module.exports = (config) => { next(); }); + // 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); }); + // catch all other methods and return 400 app.all('*', (req, res) => { res.status(400).send('bad request'); }); + // must be use in a server.js to start the server app.start = () => { reload((res) => { if (res) diff --git a/src/config.js b/src/config.js index 143af88..3869386 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,12 @@ const refConfig = require('./config.default.json'); const fs = require('fs'); +/** + * Merge resources by reading object keys and keeping reference value only if it's type is different from the source + * @param ref - reference object/value + * @param src - source object/value + * @returns {*} + */ const merge = (ref, src) => { if (typeof ref !== typeof src) { return ref; diff --git a/src/file_walker.js b/src/file_walker.js index 76647ba..98a2126 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -1,6 +1,11 @@ const fs = require('fs'); const path = require('path'); +/** + * Get all files path inside a given folder path + * @param dir + * @param cb + */ const getFileTree = (dir, cb) => { let list = []; let remaining = 0; @@ -27,6 +32,12 @@ const getFileTree = (dir, cb) => { }); }; +/** + * Tries to read a markdown file and match a title and a thumbnail + * @param path + * @param thumbnailTag - how the thumbnail image desc is given as ![thumbnailTag](url) + * @param cb + */ const readIndexFile = (path, thumbnailTag, cb) => { fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => { if (err) @@ -49,6 +60,10 @@ 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) From b7a2fd0740aca50fecad454140ddfa5f1e14cf7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 13:49:44 +0200 Subject: [PATCH 21/42] Articles rendering system --- sample_data/home/template.ejs | 13 +++++++++ src/app.js | 40 ++++++++++++++++++++++---- src/config.default.json | 9 ++++++ src/file_walker.js | 8 +++--- src/renderer.js | 16 +++++++++++ test/file_walker.test.js | 4 +-- test/renderer.test.js | 53 +++++++++++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 sample_data/home/template.ejs create mode 100644 src/renderer.js create mode 100644 test/renderer.test.js diff --git a/sample_data/home/template.ejs b/sample_data/home/template.ejs new file mode 100644 index 0000000..6cd69bf --- /dev/null +++ b/sample_data/home/template.ejs @@ -0,0 +1,13 @@ + + + + + GitBlog.md - Home + + + +
+ <%- article.content %> +
+ + \ No newline at end of file diff --git a/src/app.js b/src/app.js index 257490c..4b3d7bb 100644 --- a/src/app.js +++ b/src/app.js @@ -15,13 +15,14 @@ const cons = { 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 = []; + const articles = {}; /** * Fetch articles from the data folder and send success as a response @@ -34,8 +35,9 @@ module.exports = (config) => { return console.error(cons.error, 'loading articles : ' + err); } articles.splice(0, articles.length, ...list); - if (articles.length > 0) - console.log(cons.ok, `loaded ${articles.length} article${articles.length > 1 ? 's' : ''}`); + 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`); callback(true); @@ -79,16 +81,42 @@ module.exports = (config) => { // home endpoint : send the correct index page or error if not existing app.get('/', (req, res) => { - const homePath = `${config['data_dir']}/${config['home']['index']}`; + 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: articles}); + render(res, homePath, {articles: Object.values(articles)}); }); }); - // catch all gets and return 404 if it's an hidden file type + // catch all article urls and render them + app.get('*', (req, res, next) => { + if (req.path.test(/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/)) { + const articlePath = req.path.substr(0, 11); + const article = articles[articlePath]; + if (!article) + showError(req.path, 404, res); + else { + renderer.render(article.realPath, (err, html) => { + if (err) + return showError(req.path, 500, res); + article.content = html; + const templatePath = path.join(config['data_dir'], config['home']['index']); + fs.access(templatePath, fs.constants.R_OK, (err) => { + if (err) + showError(req.path, 404, res); + else + render(res, templatePath, {article: article}); + }); + }); + } + } else { + next(); + } + }); + + // 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); diff --git a/src/config.default.json b/src/config.default.json index ece0152..065a961 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -14,6 +14,7 @@ }, "article": { "index": "index.md", + "template": "template.ejs", "thumbnail_tag": "thumbnail", "default_title": "Untitled", "default_thumbnail": null @@ -25,5 +26,13 @@ "webhook": { "endpoint": "/webhook", "secretFile": "git_secret" + }, + "showdown": { + "parseImgDimensions": true, + "strikethrough": true, + "tables": true, + "tasklists": true, + "openLinksInNewWindow": true, + "emoji": true } } \ No newline at end of file diff --git a/src/file_walker.js b/src/file_walker.js index 98a2126..ac30e12 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -76,7 +76,7 @@ module.exports = (config) => { .filter((matches) => matches && matches.length > 1); if (paths.length === 0) cb(null, []); - const list = []; + const articles = {}; let remaining = 0; paths.forEach((matches) => { const article = { @@ -94,11 +94,11 @@ module.exports = (config) => { article.title = info.title || config['article']['default_title']; article.thumbnail = info.thumbnail ? path.join(article.path, info.thumbnail) : config['article']['default_thumbnail']; article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_'); - article.url = path.join(article.path, article.escapedTitle); - list.push(article); + article.url = path.join(article.path, article.escapedTitle)+'/'; + articles[article.path]=article; remaining--; if (remaining === 0) - cb(null, list); + cb(null, articles); }); }); diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 0000000..139956b --- /dev/null +++ b/src/renderer.js @@ -0,0 +1,16 @@ +const fs = require('fs'); +const showdown = require('showdown'); + +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 html = converter.makeHtml(data); + cb(null,html); + }); + } + }; +}; \ No newline at end of file diff --git a/test/file_walker.test.js b/test/file_walker.test.js index c36a58e..70abf86 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -234,7 +234,7 @@ describe('Test article fetching', () => { title: 'Untitled', thumbnail: 'default.png', escapedTitle: 'untitled', - url: path.join('2019', '05', '05', 'untitled'), + url: path.join('2019', '05', '05', 'untitled')+'/', }); done(); }); @@ -262,7 +262,7 @@ describe('Test article fetching', () => { title: 'Title with : info !', thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), escapedTitle: 'title_with___info', - url: path.join('2019', '05', '05', 'title_with___info'), + url: path.join('2019', '05', '05', 'title_with___info')+'/', }); done(); }); diff --git a/test/renderer.test.js b/test/renderer.test.js new file mode 100644 index 0000000..1a42d27 --- /dev/null +++ b/test/renderer.test.js @@ -0,0 +1,53 @@ +/* jshint -W117 */ +const fs = require('fs'); +const path = require('path'); +const utils = require('./test_utils'); + +const dataDir = 'test_data'; +const file = path.join(dataDir, 'test.md'); + +const config = { + 'showdown': { + 'simplifiedAutoLink': true, + 'smartIndentationFix': true + } +}; + +const renderer = require('../src/renderer')(config); + +beforeEach(() => { + utils.deleteFolderSync(dataDir); + fs.mkdirSync(dataDir); +}); + +afterAll(() => { + 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(); + }); +}); + +test('normal file', (done) => { + fs.writeFileSync(file, `# Hello`); + renderer.render(file, (err, html) => { + expect(err).toBeNull(); + expect(html).toBe('

Hello

'); + done(); + }); +}); + +test('custom rules', (done) => { + fs.writeFileSync(file, `www.google.com`); + renderer.render(file, (err, html) => { + expect(err).toBeNull(); + expect(html).toBe('

www.google.com

'); + done(); + }); +}); \ No newline at end of file From 3df43b4872faecf9731e8553f6ceb9fc56cdeb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 14:18:49 +0200 Subject: [PATCH 22/42] Article rendering tests --- src/app.js | 38 ++++++++++++++-------- src/file_walker.js | 6 ++-- test/app.test.js | 69 ++++++++++++++++++++++++++++++++++++++++ test/file_walker.test.js | 32 +++++++++---------- 4 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/app.js b/src/app.js index 4b3d7bb..f7c2495 100644 --- a/src/app.js +++ b/src/app.js @@ -29,12 +29,13 @@ module.exports = (config) => { * @param callback */ const reload = (callback) => { - fw.fetchArticles((err, list) => { + fw.fetchArticles((err, dict) => { if (err) { callback(false); return console.error(cons.error, 'loading articles : ' + err); } - articles.splice(0, articles.length, ...list); + 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' : ''}`); @@ -49,15 +50,15 @@ module.exports = (config) => { /** * Render the page with the view engine and catch errors * @param res - * @param path - path of the view + * @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, path, data, code = 200) => { - res.render(path, data, (err, html) => { + 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 ${path} : ${err}`); + console.log(cons.error, `failed to render ${vPath} : ${err}`); } else res.status(code).send(html); }); @@ -92,21 +93,24 @@ module.exports = (config) => { // catch all article urls and render them app.get('*', (req, res, next) => { - if (req.path.test(/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/)) { - const articlePath = req.path.substr(0, 11); + if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.test(req.path)) { + const articlePath = req.path.substr(1, 10); const article = articles[articlePath]; if (!article) showError(req.path, 404, res); else { - renderer.render(article.realPath, (err, html) => { - if (err) + 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['home']['index']); + const templatePath = path.join(config['data_dir'], config['article']['template']); fs.access(templatePath, fs.constants.R_OK, (err) => { - if (err) - showError(req.path, 404, res); - else + if (err) { + console.log(cons.error, `no template found at ${templatePath}`); + showError(req.path, 500, res); + } else render(res, templatePath, {article: article}); }); }); @@ -136,6 +140,12 @@ module.exports = (config) => { res.status(400).send('bad request'); }); + app.use((err, req, res, next) => { + console.log(cons.error, `error when handling ${req.path} request : ${err}`); + console.error(err.stack); + next(err); + }); + // must be use in a server.js to start the server app.start = () => { reload((res) => { diff --git a/src/file_walker.js b/src/file_walker.js index ac30e12..d336b7d 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -75,7 +75,7 @@ module.exports = (config) => { .map((path) => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) .filter((matches) => matches && matches.length > 1); if (paths.length === 0) - cb(null, []); + cb(null, {}); const articles = {}; let remaining = 0; paths.forEach((matches) => { @@ -94,8 +94,8 @@ module.exports = (config) => { article.title = info.title || config['article']['default_title']; article.thumbnail = info.thumbnail ? path.join(article.path, info.thumbnail) : config['article']['default_thumbnail']; article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_'); - article.url = path.join(article.path, article.escapedTitle)+'/'; - articles[article.path]=article; + article.url = '/' + path.join(article.path, article.escapedTitle) + '/'; + articles[article.path] = article; remaining--; if (remaining === 0) cb(null, articles); diff --git a/test/app.test.js b/test/app.test.js index ae3b5de..ccc5953 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -7,6 +7,7 @@ const utils = require('./test_utils'); const dataDir = 'test_data'; const testIndex = 'testindex.ejs'; const testError = 'testerror.ejs'; +const testTemplate = 'testtemplate.ejs'; const config = { 'test': true, @@ -19,10 +20,12 @@ const config = { }, 'article': { 'index': 'index.md', + 'template' : testTemplate, 'thumbnail_tag': 'thumbnail', 'default_title': 'Untitled', 'default_thumbnail': null }, + 'showdown' : {} }; const app = require('../src/app')(config); @@ -83,6 +86,72 @@ describe('Test root path', () => { }); }); +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('500 no template', (done) =>{ + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + fs.writeFileSync(path.join(dataDir, '2019', '05', '05','index.md'), '# Hello'); + app.reload((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/hello/').then((response) =>{ + expect(response.statusCode).toBe(500); + done(); + }); + }); + }); + + test('200 rendered article', (done) =>{ + 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 %><%- `reload` %>'); + app.reload((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/hello/').then((response) =>{ + expect(response.statusCode).toBe(200); + expect(response.text).toBe('

Hello

reload'); + done(); + }); + }); + }); + + test('200 other url', (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((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/anything/').then((response) =>{ + expect(response.statusCode).toBe(200); + done(); + }); + }); + }); + + test('200 other url 2', (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((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/').then((response) =>{ + expect(response.statusCode).toBe(200); + done(); + }); + }); + }); +}); + + describe('Test static files', () => { test('404 invalid file no error page', (done) =>{ request(app).get('/somefile.txt').then((response) =>{ diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 70abf86..a3e9cd5 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -191,10 +191,10 @@ describe('Test article fetching', () => { }); }); test('empty data dir', (done) => { - fw.fetchArticles((err, list) => { + fw.fetchArticles((err, dict) => { expect(err).toBeNull(); - expect(list).toBeDefined(); - expect(list.length).toBe(0); + expect(dict).toBeDefined(); + expect(Object.keys(dict).length).toBe(0); done(); }); }); @@ -208,10 +208,10 @@ describe('Test article fetching', () => { path.join(dataDir, 'test', 'test', testIndex), path.join(dataDir, '2019', '05', testIndex) ]); - fw.fetchArticles((err, list) => { + fw.fetchArticles((err, dict) => { expect(err).toBeNull(); - expect(list).toBeDefined(); - expect(list.length).toBe(0); + expect(dict).toBeDefined(); + expect(Object.keys(dict).length).toBe(0); done(); }); }); @@ -220,11 +220,11 @@ describe('Test article fetching', () => { const file = path.join(dir, testIndex); utils.createEmptyDirs([dir]); utils.createEmptyFiles([file]); - fw.fetchArticles((err, list) => { + fw.fetchArticles((err, dict) => { expect(err).toBeNull(); - expect(list).toBeDefined(); - expect(list.length).toBe(1); - expect(list[0]).toEqual({ + expect(dict).toBeDefined(); + expect(Object.keys(dict).length).toBe(1); + expect(dict[path.join('2019', '05', '05')]).toEqual({ path: path.join('2019', '05', '05'), realPath: dir, year: 2019, @@ -234,7 +234,7 @@ describe('Test article fetching', () => { title: 'Untitled', thumbnail: 'default.png', escapedTitle: 'untitled', - url: path.join('2019', '05', '05', 'untitled')+'/', + url: '/'+path.join('2019', '05', '05', 'untitled')+'/', }); done(); }); @@ -248,11 +248,11 @@ describe('Test article fetching', () => { ![thumbnail](./thumbnail.jpg) this is some text `); - fw.fetchArticles((err, list) => { + fw.fetchArticles((err, dict) => { expect(err).toBeNull(); - expect(list).toBeDefined(); - expect(list.length).toBe(1); - expect(list[0]).toEqual({ + expect(dict).toBeDefined(); + expect(Object.keys(dict).length).toBe(1); + expect(dict[path.join('2019', '05', '05')]).toEqual({ path: path.join('2019', '05', '05'), realPath: dir, year: 2019, @@ -262,7 +262,7 @@ describe('Test article fetching', () => { title: 'Title with : info !', thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), escapedTitle: 'title_with___info', - url: path.join('2019', '05', '05', 'title_with___info')+'/', + url: '/'+path.join('2019', '05', '05', 'title_with___info')+'/', }); done(); }); From 92021164c98fb4ac0b12db2260bd46fd503966b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 14:24:34 +0200 Subject: [PATCH 23/42] Code formatting with 2 space indent --- src/app.js | 264 +++++++++++++------------- src/config.default.json | 4 +- src/config.js | 34 ++-- src/file_walker.js | 156 +++++++-------- src/postinstall.js | 28 +-- src/renderer.js | 22 +-- test/app.test.js | 304 ++++++++++++++--------------- test/config.test.js | 62 +++--- test/file_walker.test.js | 400 +++++++++++++++++++-------------------- test/renderer.test.js | 52 ++--- test/test_utils.js | 24 +-- 11 files changed, 676 insertions(+), 674 deletions(-) diff --git a/src/app.js b/src/app.js index f7c2495..b4c129b 100644 --- a/src/app.js +++ b/src/app.js @@ -8,155 +8,155 @@ const path = require('path'); * @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); + 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, '..')); + // 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 = {}; + const articles = {}; - /** - * Fetch articles from the data folder and send success as a response - * @param callback - */ - const reload = (callback) => { - fw.fetchArticles((err, dict) => { - if (err) { - callback(false); - return console.error(cons.error, 'loading articles : ' + err); - } - 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`); - callback(true); - }); - }; - 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); - }); - }; - - // 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)}); - }); + /** + * Fetch articles from the data folder and send success as a response + * @param callback + */ + const reload = (callback) => { + fw.fetchArticles((err, dict) => { + if (err) { + callback(false); + return console.error(cons.error, 'loading articles : ' + err); + } + 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`); + callback(true); }); + }; + if (config['test']) + app.reload = reload; - // catch all article urls and render them - app.get('*', (req, res, next) => { - if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.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) => { - if (err) { - console.log(cons.error, `no template found at ${templatePath}`); - showError(req.path, 500, res); - } else - render(res, templatePath, {article: article}); - }); - }); - } - } else { - next(); - } + /** + * 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); }); + }; - // 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(); + /** + * 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); }); + }; - // 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) => { + // 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)}); }); + }); - // catch all other methods and return 400 - app.all('*', (req, res) => { - res.status(400).send('bad request'); - }); - - app.use((err, req, res, next) => { - console.log(cons.error, `error when handling ${req.path} request : ${err}`); - console.error(err.stack); - next(err); - }); - - // must be use in a server.js to start the server - app.start = () => { - reload((res) => { - if (res) - app.listen(config['node_port'], () => { - console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); - }); + // catch all article urls and render them + app.get('*', (req, res, next) => { + if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.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) => { + if (err) { + console.log(cons.error, `no template found at ${templatePath}`); + showError(req.path, 500, res); + } else + render(res, templatePath, {article: article}); + }); }); - }; + } + } else { + next(); + } + }); - return app; + // 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(); + }); + + // 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); + }); + + // catch all other methods and return 400 + app.all('*', (req, res) => { + res.status(400).send('bad request'); + }); + + app.use((err, req, res, next) => { + console.log(cons.error, `error when handling ${req.path} request : ${err}`); + console.error(err.stack); + next(err); + }); + + // must be use in a server.js to start the server + app.start = () => { + reload((res) => { + if (res) + app.listen(config['node_port'], () => { + console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); + }); + }); + }; + + return app; }; diff --git a/src/config.default.json b/src/config.default.json index 065a961..25acebc 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -10,7 +10,9 @@ "home": { "index": "index.ejs", "error": "error.ejs", - "hidden": [".ejs"] + "hidden": [ + ".ejs" + ] }, "article": { "index": "index.md", diff --git a/src/config.js b/src/config.js index 3869386..a51b3c7 100644 --- a/src/config.js +++ b/src/config.js @@ -8,24 +8,24 @@ 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 (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.error('Failed to load config.json : ' + error); + return refConfig; + } }; \ No newline at end of file diff --git a/src/file_walker.js b/src/file_walker.js index d336b7d..d545359 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -7,29 +7,29 @@ const path = require('path'); * @param cb */ const getFileTree = (dir, cb) => { - let list = []; - let remaining = 0; - fs.readdir(dir, {withFileTypes: true}, (err, items) => { - if (err) + 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); - 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)); - } - }); - if (remaining === 0) + list.push(...out); + remaining--; + if (remaining === 0) cb(null, list); + }); + } else { + list.push(path.join(dir, item.name)); + } }); + if (remaining === 0) + cb(null, list); + }); }; /** @@ -39,70 +39,70 @@ 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) + return cb(err); - 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((path) => path.substr(config['data_dir'].length)) - .filter((path) => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) - .map((path) => path.substr(0, path.length - config['article']['index'].length)) - .map((path) => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) - .filter((matches) => matches && matches.length > 1); - if (paths.length === 0) - cb(null, {}); - const articles = {}; - let remaining = 0; - paths.forEach((matches) => { - const article = { - path: path.join(matches[1], matches[2], matches[3]), - realPath: path.join(config['data_dir'], matches[1], matches[2], matches[3]), - year: parseInt(matches[1]), - month: parseInt(matches[2]), - day: parseInt(matches[3]) - }; - article.date = new Date(article.year, article.month, article.day); - 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 ? path.join(article.path, info.thumbnail) : config['article']['default_thumbnail']; - article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_'); - article.url = '/' + path.join(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) + return cb(err); + const paths = fileList + .map((path) => path.substr(config['data_dir'].length)) + .filter((path) => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) + .map((path) => path.substr(0, path.length - config['article']['index'].length)) + .map((path) => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) + .filter((matches) => matches && matches.length > 1); + if (paths.length === 0) + cb(null, {}); + const articles = {}; + let remaining = 0; + paths.forEach((matches) => { + const article = { + path: path.join(matches[1], matches[2], matches[3]), + realPath: path.join(config['data_dir'], matches[1], matches[2], matches[3]), + year: parseInt(matches[1]), + month: parseInt(matches[2]), + day: parseInt(matches[3]) + }; + article.date = new Date(article.year, article.month, article.day); + 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 ? path.join(article.path, info.thumbnail) : config['article']['default_thumbnail']; + article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_'); + article.url = '/' + path.join(article.path, article.escapedTitle) + '/'; + articles[article.path] = article; + remaining--; + if (remaining === 0) + cb(null, articles); + }); + }); - }); - } - }; + }); + } + }; }; \ No newline at end of file diff --git a/src/postinstall.js b/src/postinstall.js index e9acd6e..e5466ac 100644 --- a/src/postinstall.js +++ b/src/postinstall.js @@ -3,28 +3,28 @@ 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); } \ No newline at end of file diff --git a/src/renderer.js b/src/renderer.js index 139956b..236d931 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -2,15 +2,15 @@ const fs = require('fs'); const showdown = require('showdown'); 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 html = converter.makeHtml(data); - cb(null,html); - }); - } - }; + 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 html = converter.makeHtml(data); + cb(null, html); + }); + } + }; }; \ No newline at end of file diff --git a/test/app.test.js b/test/app.test.js index ccc5953..bbf98df 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -10,197 +10,197 @@ const testError = 'testerror.ejs'; const testTemplate = 'testtemplate.ejs'; const config = { - 'test': true, - 'data_dir': dataDir, - 'view_engine': 'ejs', - 'home': { - 'index': testIndex, - 'error': testError, - 'hidden': ['.ejs','.test'] - }, - 'article': { - 'index': 'index.md', - 'template' : testTemplate, - 'thumbnail_tag': 'thumbnail', - 'default_title': 'Untitled', - 'default_thumbnail': null - }, - 'showdown' : {} + 'test': true, + 'data_dir': dataDir, + 'view_engine': 'ejs', + 'home': { + 'index': testIndex, + 'error': testError, + 'hidden': ['.ejs', '.test'] + }, + 'article': { + 'index': 'index.md', + 'template': testTemplate, + 'thumbnail_tag': 'thumbnail', + 'default_title': 'Untitled', + 'default_thumbnail': null + }, + 'showdown': {} }; const app = require('../src/app')(config); beforeEach(() => { - utils.deleteFolderSync(dataDir); - fs.mkdirSync(dataDir); + utils.deleteFolderSync(dataDir); + fs.mkdirSync(dataDir); }); afterAll(() => { - if (fs.existsSync(dataDir)) { - utils.deleteFolderSync(dataDir); - } + if (fs.existsSync(dataDir)) { + utils.deleteFolderSync(dataDir); + } }); 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 %> at <%= path %>'); + request(app).get('/').then((response) => { + expect(response.statusCode).toBe(404); + expect(response.text).toBe('error 404 at /'); + 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 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', (done) => { + 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((res) => { + expect(res).toBe(true); + request(app).get('/').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('articles 2'); + done(); + }); }); - test('200 2 articles', (done) =>{ - 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((res) => { - expect(res).toBe(true); - request(app).get('/').then((response) =>{ - expect(response.statusCode).toBe(200); - expect(response.text).toBe('articles 2'); - 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) =>{ - utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); - fs.writeFileSync(path.join(dataDir, '2019', '05', '05','index.md'), '# Hello'); - app.reload((res) => { - expect(res).toBe(true); - request(app).get('/2019/05/05/hello/').then((response) =>{ - expect(response.statusCode).toBe(500); - done(); - }); - }); + test('500 no template', (done) => { + utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); + fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); + app.reload((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/hello/').then((response) => { + expect(response.statusCode).toBe(500); + done(); + }); }); + }); - test('200 rendered article', (done) =>{ - 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 %><%- `reload` %>'); - app.reload((res) => { - expect(res).toBe(true); - request(app).get('/2019/05/05/hello/').then((response) =>{ - expect(response.statusCode).toBe(200); - expect(response.text).toBe('

Hello

reload'); - done(); - }); - }); + test('200 rendered article', (done) => { + 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 %><%- `reload` %>'); + app.reload((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/hello/').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text).toBe('

Hello

reload'); + done(); + }); }); + }); - test('200 other url', (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((res) => { - expect(res).toBe(true); - request(app).get('/2019/05/05/anything/').then((response) =>{ - expect(response.statusCode).toBe(200); - done(); - }); - }); + test('200 other url', (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((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/anything/').then((response) => { + expect(response.statusCode).toBe(200); + done(); + }); }); + }); - test('200 other url 2', (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((res) => { - expect(res).toBe(true); - request(app).get('/2019/05/05/').then((response) =>{ - expect(response.statusCode).toBe(200); - done(); - }); - }); + test('200 other url 2', (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((res) => { + expect(res).toBe(true); + request(app).get('/2019/05/05/').then((response) => { + expect(response.statusCode).toBe(200); + done(); + }); }); + }); }); 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 %> 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 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) => { + fs.writeFileSync(path.join(dataDir, 'somefile.test'), ''); + request(app).get('/somefile.test').then((response) => { + expect(response.statusCode).toBe(404); + done(); }); - test('200 valid file', (done) =>{ - fs.writeFileSync(`${dataDir}/somefile.txt`, 'filecontent'); - request(app).get('/somefile.txt').then((response) =>{ - expect(response.statusCode).toBe(200); - expect(response.text).toBe('filecontent'); - done(); - }); + }); + test('200 valid file', (done) => { + fs.writeFileSync(`${dataDir}/somefile.txt`, 'filecontent'); + request(app).get('/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(); }); + }); }); diff --git a/test/config.test.js b/test/config.test.js index ad60ea4..fa72475 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -5,50 +5,50 @@ 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); + } + expect(fs.existsSync(configFile)).toBeFalsy(); }); 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('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'); }); \ No newline at end of file diff --git a/test/file_walker.test.js b/test/file_walker.test.js index a3e9cd5..1285863 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -7,265 +7,265 @@ const dataDir = 'test_data'; const testIndex = 'testindex.md'; 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, + 'default_title': 'Untitled', + 'default_thumbnail': 'default.png', + 'thumbnail_tag': 'thumbnail' + } }; const fw = require('../src/file_walker')(config); beforeEach(() => { - utils.deleteFolderSync(dataDir); - fs.mkdirSync(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); - - test('invalid file', (done) => { - fw.readIndexFile('invalid file', 'thumbnail', (err, info) => { - expect(err).not.toBeNull(); - expect(info).not.toBeDefined(); - done(); - }); + 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('correct file', (done) => { - fs.writeFileSync(file, ` + }); + + test('correct file', (done) => { + fs.writeFileSync(file, ` # This is an awesome title !?¤ ![custom_thumbnail](./thumbnail.jpg) this is some text `); - fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { - expect(err).toBeNull(); - expect(info).toBeDefined(); - expect(info.title).toBe('This is an awesome title !?¤'); - expect(info.thumbnail).toBe('./thumbnail.jpg'); - done(); - }); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('This is an awesome title !?¤'); + expect(info.thumbnail).toBe('./thumbnail.jpg'); + done(); }); + }); - test('no title', (done) => { - fs.writeFileSync(file, ` + test('no title', (done) => { + fs.writeFileSync(file, ` ## This is an awesome title !?¤ ![custom_thumbnail](./thumbnail.jpg) ### this is some text `); - fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { - expect(err).toBeNull(); - expect(info).toBeDefined(); - expect(info.title).not.toBeDefined(); - expect(info.thumbnail).toBe('./thumbnail.jpg'); - done(); - }); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).not.toBeDefined(); + expect(info.thumbnail).toBe('./thumbnail.jpg'); + done(); }); + }); - test('title at beginning', (done) => { - fs.writeFileSync(file, '#title'); - fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { - expect(err).toBeNull(); - expect(info).toBeDefined(); - expect(info.title).toBe('title'); - expect(info.thumbnail).not.toBeDefined(); - done(); - }); + test('title at beginning', (done) => { + fs.writeFileSync(file, '#title'); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('title'); + expect(info.thumbnail).not.toBeDefined(); + done(); }); + }); - test('no thumbnail', (done) => { - fs.writeFileSync(file, ` + test('no thumbnail', (done) => { + fs.writeFileSync(file, ` # This is an awesome title !?¤ ![custom_thumbnail](./thumbnail.jpg) this is some text `); - fw.readIndexFile(file, 'thumbnail', (err, info) => { - expect(err).toBeNull(); - expect(info).toBeDefined(); - expect(info.title).toBe('This is an awesome title !?¤'); - expect(info.thumbnail).not.toBeDefined(); - done(); - }); + fw.readIndexFile(file, 'thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('This is an awesome title !?¤'); + expect(info.thumbnail).not.toBeDefined(); + done(); }); + }); - test('multiple thumbnails', (done) => { - fs.writeFileSync(file, ` + test('multiple thumbnails', (done) => { + fs.writeFileSync(file, ` # This is an awesome title !?¤ ![custom_thumbnail](./thumbnail.jpg) this is some text ![custom_thumbnail](./thumbnail2.jpg) `); - fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { - expect(err).toBeNull(); - expect(info).toBeDefined(); - expect(info.title).toBe('This is an awesome title !?¤'); - expect(info.thumbnail).toBe('./thumbnail.jpg'); - done(); - }); + fw.readIndexFile(file, 'custom_thumbnail', (err, info) => { + expect(err).toBeNull(); + expect(info).toBeDefined(); + expect(info.title).toBe('This is an awesome title !?¤'); + expect(info.thumbnail).toBe('./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(); + config['data_dir'] = dataDir; + 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]); - fw.fetchArticles((err, dict) => { - expect(err).toBeNull(); - expect(dict).toBeDefined(); - expect(Object.keys(dict).length).toBe(1); - expect(dict[path.join('2019', '05', '05')]).toEqual({ - path: path.join('2019', '05', '05'), - realPath: dir, - year: 2019, - month: 5, - day: 5, - date : new Date(2019,5,5), - title: 'Untitled', - thumbnail: 'default.png', - escapedTitle: 'untitled', - url: '/'+path.join('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]); + fw.fetchArticles((err, dict) => { + expect(err).toBeNull(); + expect(dict).toBeDefined(); + expect(Object.keys(dict).length).toBe(1); + expect(dict[path.join('2019', '05', '05')]).toEqual({ + path: path.join('2019', '05', '05'), + realPath: dir, + year: 2019, + month: 5, + day: 5, + date: new Date(2019, 5, 5), + title: 'Untitled', + thumbnail: 'default.png', + escapedTitle: 'untitled', + url: '/' + path.join('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 ! ![thumbnail](./thumbnail.jpg) this is some text `); - fw.fetchArticles((err, dict) => { - expect(err).toBeNull(); - expect(dict).toBeDefined(); - expect(Object.keys(dict).length).toBe(1); - expect(dict[path.join('2019', '05', '05')]).toEqual({ - path: path.join('2019', '05', '05'), - realPath: dir, - year: 2019, - month: 5, - day: 5, - date : new Date(2019,5,5), - title: 'Title with : info !', - thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), - escapedTitle: 'title_with___info', - url: '/'+path.join('2019', '05', '05', 'title_with___info')+'/', - }); - done(); - }); + fw.fetchArticles((err, dict) => { + expect(err).toBeNull(); + expect(dict).toBeDefined(); + expect(Object.keys(dict).length).toBe(1); + expect(dict[path.join('2019', '05', '05')]).toEqual({ + path: path.join('2019', '05', '05'), + realPath: dir, + year: 2019, + month: 5, + day: 5, + date: new Date(2019, 5, 5), + title: 'Title with : info !', + thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), + escapedTitle: 'title_with___info', + url: '/' + path.join('2019', '05', '05', 'title_with___info') + '/', + }); + done(); }); + }); }); diff --git a/test/renderer.test.js b/test/renderer.test.js index 1a42d27..37aa81f 100644 --- a/test/renderer.test.js +++ b/test/renderer.test.js @@ -7,47 +7,47 @@ const dataDir = 'test_data'; const file = path.join(dataDir, 'test.md'); const config = { - 'showdown': { - 'simplifiedAutoLink': true, - 'smartIndentationFix': true - } + 'showdown': { + 'simplifiedAutoLink': true, + 'smartIndentationFix': true + } }; const renderer = require('../src/renderer')(config); beforeEach(() => { - utils.deleteFolderSync(dataDir); - fs.mkdirSync(dataDir); + 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(); - }); + 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('

Hello

'); - done(); - }); + fs.writeFileSync(file, `# Hello`); + renderer.render(file, (err, html) => { + expect(err).toBeNull(); + expect(html).toBe('

Hello

'); + done(); + }); }); test('custom rules', (done) => { - fs.writeFileSync(file, `www.google.com`); - renderer.render(file, (err, html) => { - expect(err).toBeNull(); - expect(html).toBe('

www.google.com

'); - done(); - }); + fs.writeFileSync(file, `www.google.com`); + renderer.render(file, (err, html) => { + expect(err).toBeNull(); + expect(html).toBe('

www.google.com

'); + done(); + }); }); \ No newline at end of file diff --git a/test/test_utils.js b/test/test_utils.js index a80a886..6b509c5 100644 --- a/test/test_utils.js +++ b/test/test_utils.js @@ -2,19 +2,19 @@ const fs = require('fs'); const path = require('path'); const deleteFolderSync = (dir) => { - if (!fs.existsSync(dir)) - return; - fs.readdirSync(dir, {withFileTypes: true}).forEach((item) => { - if (item.isDirectory()) - deleteFolderSync(path.join(dir,item.name)); - else - fs.unlinkSync(path.join(dir,item.name)); - }); - fs.rmdirSync(dir); + if (!fs.existsSync(dir)) + return; + fs.readdirSync(dir, {withFileTypes: true}).forEach((item) => { + if (item.isDirectory()) + deleteFolderSync(path.join(dir, item.name)); + else + fs.unlinkSync(path.join(dir, item.name)); + }); + 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, '')), }; \ No newline at end of file From 7937985c3d728a564be980364f5b35c7741c277f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 14:26:29 +0200 Subject: [PATCH 24/42] Code formatting --- sample_data/home/error.ejs | 2 +- sample_data/home/index.ejs | 11 +++++++---- sample_data/home/style.css | 14 +++++++------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/sample_data/home/error.ejs b/sample_data/home/error.ejs index c2471f6..e4b1427 100644 --- a/sample_data/home/error.ejs +++ b/sample_data/home/error.ejs @@ -6,7 +6,7 @@
-

Error <%= error %> at path <%= path %>

+

Error <%= error %> at path <%= path %>

\ No newline at end of file diff --git a/sample_data/home/index.ejs b/sample_data/home/index.ejs index 25c46d0..680f203 100644 --- a/sample_data/home/index.ejs +++ b/sample_data/home/index.ejs @@ -12,10 +12,13 @@

Articles in this blog :

<% articles.forEach((article) => { %>
-

<%- `${article.title}`%> (<%= `${article.day}/${article.month}/${article.year}`%>)

- <% if(article.thumbnail){%> - <%- `thumbnail`%> - <% }%> +

<%- `${article.title}` %> ( + <%= `${article.day}/${article.month}/${article.year}` %> + ) +

+ <% if(article.thumbnail){ %> + <%- `thumbnail` %> + <% } %>
<% }); %> diff --git a/sample_data/home/style.css b/sample_data/home/style.css index 979ab2a..6d915f0 100644 --- a/sample_data/home/style.css +++ b/sample_data/home/style.css @@ -4,15 +4,15 @@ main { margin: auto; } -.article a, .article a:visited{ - color:black; +.article a, .article a:visited { + color: black; } -.article a:visited{ - color:black; +.article a:visited { + color: black; } -.article img{ - max-width:100%; - max-height:10vh; +.article img { + max-width: 100%; + max-height: 10vh; } \ No newline at end of file From 1fa8007e0e037fd15fcc74679b0494e345d00906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 15:08:22 +0200 Subject: [PATCH 25/42] RSS function --- package-lock.json | 14 +++--- package.json | 2 +- src/config.default.json | 3 ++ src/rss.js | 40 ++++++++++++++++ test/rss.test.js | 100 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 src/rss.js create mode 100644 test/rss.test.js diff --git a/package-lock.json b/package-lock.json index 7967736..56e9b3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8670,8 +8670,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "5.7.0", @@ -9670,10 +9669,13 @@ "async-limiter": "~1.0.0" } }, - "xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 7ca4d39..5136935 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "express": "^4.17.1", "ncp": "^2.0.0", "showdown": "^1.9.0", - "xml": "^1.0.1" + "xml-js": "^1.6.11" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/src/config.default.json b/src/config.default.json index 25acebc..88c5156 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -2,6 +2,7 @@ "node_port": 3000, "data_dir": "data", "view_engine": "ejs", + "loopback_url": "http://localhost:3000", "modules": { "plantuml": false, "rss": true, @@ -22,6 +23,8 @@ "default_thumbnail": null }, "rss": { + "title": "mygitblog RSS feed", + "description": "a generated RSS feed from my articles", "endpoint": "/rss", "length": 10 }, diff --git a/src/rss.js b/src/rss.js new file mode 100644 index 0000000..efd36aa --- /dev/null +++ b/src/rss.js @@ -0,0 +1,40 @@ +const convert = require('xml-js'); + +const mapArticle = (url, article) => { + return { + 'title': {'_text': article.title}, + 'link': {'_text': (url + article.url).replace(/([^:])\/\//g, '$1/')}, + 'pubDate': {'_text': article.date.toString()}, + }; +}; + +module.exports = (config) => { + return { + get: (dict) => { + const items = Object.values(dict) + .sort((a, b) => ('' + b.path).localeCompare(a.path)) + .slice(0, config['rss']['length']); + const data = { + '_declaration': { + '_attributes': { + 'version': '1.0', + 'encoding': 'UTF-8' + } + }, + 'rss': { + '_attributes': { + 'version': '2.0' + }, + 'title': {'_text': config['rss']['title']}, + 'description': {'_text': config['rss']['description']}, + 'link': {'_text': config['loopback_url']}, + 'lastBuildDate': {'_text': new Date().toString()}, + 'lastPubDate': {'_text': new Date().toString()}, + 'ttl': {'_text': '60'}, + 'item': items.map((a) => mapArticle(config['loopback_url'], a)) + } + }; + return convert.js2xml(data, {compact: true}); + } + }; +}; \ No newline at end of file diff --git a/test/rss.test.js b/test/rss.test.js new file mode 100644 index 0000000..087a2eb --- /dev/null +++ b/test/rss.test.js @@ -0,0 +1,100 @@ +/* jshint -W117 */ +const config = { + 'loopback_url': 'http://test.test/', + 'rss': { + 'title': 'test rss', + 'description': 'description', + 'endpoint': '/rss', + 'length': 2 + }, +}; + +const rss = require('../src/rss')(config); + +test('empty rss', () => { + const xml = rss.get({}); + expect(xml).toBe('' + + '' + + 'test rss' + + 'description' + + 'http://test.test/' + + '' + new Date().toString() + '' + + '' + new Date().toString() + '' + + '60' + + ''); +}); + +test('1 item', () => { + const data = { + 'a': { + path: 'a', + realPath: 'b', + year: 2019, + month: 5, + day: 5, + date: new Date(2019, 5, 5), + title: 'Title with : info !', + thumbnail: '/2019/05/05/thumbnail.jpg/', + escapedTitle: 'title_with___info', + url: '/2019/05/05/title_with___info/', + } + }; + const xml = rss.get(data); + expect(xml).toEqual('' + + '' + + 'test rss' + + 'description' + + 'http://test.test/' + + '' + new Date().toString() + '' + + '' + new Date().toString() + '' + + '60' + + '' + + 'Title with : info !' + + 'http://test.test/2019/05/05/title_with___info/' + + '' + new Date(2019, 5, 5).toString() + '' + + '' + + ''); +}); + +test('3 items only 2 shown sorted', () => { + const data = { + 'a': { + path: '2019/05/05', + date: new Date(2019, 5, 5), + title: 'a', + url: 'a', + }, + 'b': { + path: '2018/05/05', + date: new Date(2018, 5, 5), + title: 'b', + url: 'b', + }, + 'c': { + path: '2020/05/05', + date: new Date(2020, 5, 5), + title: 'c', + url: 'c', + } + }; + const xml = rss.get(data); + expect(xml).toEqual('' + + '' + + 'test rss' + + 'description' + + 'http://test.test/' + + '' + new Date().toString() + '' + + '' + new Date().toString() + '' + + '60' + + '' + + 'c' + + 'http://test.test/c' + + '' + new Date(2020, 5, 5).toString() + '' + + '' + + '' + + 'a' + + 'http://test.test/a' + + '' + new Date(2019, 5, 5).toString() + '' + + '' + + ''); +}); \ No newline at end of file From 9cb601528e148695afb933c4923b67e79a492511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 16:13:50 +0200 Subject: [PATCH 26/42] RSS feed in app --- package-lock.json | 38 +++++++++++---- package.json | 4 +- src/app.js | 34 ++++++++++++- src/config.default.json | 2 +- src/file_walker.js | 1 + src/rss.js | 40 ---------------- test/app.test.js | 93 ++++++++++++++++++++++++++++-------- test/file_walker.test.js | 8 +++- test/rss.test.js | 100 --------------------------------------- 9 files changed, 147 insertions(+), 173 deletions(-) delete mode 100644 src/rss.js delete mode 100644 test/rss.test.js diff --git a/package-lock.json b/package-lock.json index 56e9b3a..3667629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8296,6 +8296,30 @@ "glob": "^7.1.3" } }, + "rss": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", + "integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=", + "requires": { + "mime-types": "2.1.13", + "xml": "1.0.1" + }, + "dependencies": { + "mime-db": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", + "integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I=" + }, + "mime-types": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", + "integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=", + "requires": { + "mime-db": "~1.25.0" + } + } + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -8670,7 +8694,8 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true }, "semver": { "version": "5.7.0", @@ -9669,13 +9694,10 @@ "async-limiter": "~1.0.0" } }, - "xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "requires": { - "sax": "^1.2.4" - } + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 5136935..04dee11 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "ejs": "^2.6.2", "express": "^4.17.1", "ncp": "^2.0.0", - "showdown": "^1.9.0", - "xml-js": "^1.6.11" + "rss": "^1.2.2", + "showdown": "^1.9.0" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/src/app.js b/src/app.js index b4c129b..4319149 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,7 @@ const express = require('express'); const app = express(); const fs = require('fs'); const path = require('path'); +const Rss = require('rss'); /** * Terminal colors and symbols to display status messages @@ -23,6 +24,7 @@ module.exports = (config) => { app.set('views', path.join(__dirname, '..')); const articles = {}; + let lastRSS = ''; /** * Fetch articles from the data folder and send success as a response @@ -41,6 +43,9 @@ module.exports = (config) => { console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''}`); else console.log(cons.warn, `no articles loaded, check your configuration`); + + lastRSS = ''; + callback(true); }); }; @@ -87,10 +92,37 @@ module.exports = (config) => { if (err) showError(req.path, 404, res); else - render(res, homePath, {articles: Object.values(articles)}); + 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); + } + }); + // catch all article urls and render them app.get('*', (req, res, next) => { if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.test(req.path)) { diff --git a/src/config.default.json b/src/config.default.json index 88c5156..d5ac59a 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -2,7 +2,7 @@ "node_port": 3000, "data_dir": "data", "view_engine": "ejs", - "loopback_url": "http://localhost:3000", + "language": "en-us", "modules": { "plantuml": false, "rss": true, diff --git a/src/file_walker.js b/src/file_walker.js index d545359..b9bc8a6 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -87,6 +87,7 @@ module.exports = (config) => { day: parseInt(matches[3]) }; 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) diff --git a/src/rss.js b/src/rss.js deleted file mode 100644 index efd36aa..0000000 --- a/src/rss.js +++ /dev/null @@ -1,40 +0,0 @@ -const convert = require('xml-js'); - -const mapArticle = (url, article) => { - return { - 'title': {'_text': article.title}, - 'link': {'_text': (url + article.url).replace(/([^:])\/\//g, '$1/')}, - 'pubDate': {'_text': article.date.toString()}, - }; -}; - -module.exports = (config) => { - return { - get: (dict) => { - const items = Object.values(dict) - .sort((a, b) => ('' + b.path).localeCompare(a.path)) - .slice(0, config['rss']['length']); - const data = { - '_declaration': { - '_attributes': { - 'version': '1.0', - 'encoding': 'UTF-8' - } - }, - 'rss': { - '_attributes': { - 'version': '2.0' - }, - 'title': {'_text': config['rss']['title']}, - 'description': {'_text': config['rss']['description']}, - 'link': {'_text': config['loopback_url']}, - 'lastBuildDate': {'_text': new Date().toString()}, - 'lastPubDate': {'_text': new Date().toString()}, - 'ttl': {'_text': '60'}, - 'item': items.map((a) => mapArticle(config['loopback_url'], a)) - } - }; - return convert.js2xml(data, {compact: true}); - } - }; -}; \ No newline at end of file diff --git a/test/app.test.js b/test/app.test.js index bbf98df..2342920 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -9,30 +9,26 @@ const testIndex = 'testindex.ejs'; const testError = 'testerror.ejs'; const testTemplate = 'testtemplate.ejs'; -const config = { - 'test': true, - 'data_dir': dataDir, - 'view_engine': 'ejs', - 'home': { - 'index': testIndex, - 'error': testError, - 'hidden': ['.ejs', '.test'] - }, - 'article': { - 'index': 'index.md', - 'template': testTemplate, - 'thumbnail_tag': 'thumbnail', - 'default_title': 'Untitled', - 'default_thumbnail': null - }, - 'showdown': {} -}; +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['rss']['endpoint'] = '/rsstest'; +config['rss']['length'] = 2; const app = require('../src/app')(config); -beforeEach(() => { +beforeEach((done) => { utils.deleteFolderSync(dataDir); fs.mkdirSync(dataDir); + app.reload((res) => { + expect(res).toBe(true); + done(); + }); }); afterAll(() => { @@ -86,6 +82,65 @@ describe('Test root path', () => { }); }); +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('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('').length).toBe(1); + done(); + }); + }); + test('200 2 rss items', (done) => { + 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((res) => { + expect(res).toBe(true); + request(app).get('/rsstest').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(3); + done(); + }); + }); + }); + test('200 max rss items', (done) => { + 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((res) => { + expect(res).toBe(true); + request(app).get('/rsstest').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(3); + done(); + }); + }); + }); +}); + describe('Test articles rendering', () => { test('404 article not found', (done) => { request(app).get('/2019/05/06/untitled/').then((response) => { diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 1285863..20ec67b 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -220,6 +220,8 @@ describe('Test article fetching', () => { 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(); @@ -230,7 +232,7 @@ describe('Test article fetching', () => { year: 2019, month: 5, day: 5, - date: new Date(2019, 5, 5), + date: date, title: 'Untitled', thumbnail: 'default.png', escapedTitle: 'untitled', @@ -248,6 +250,8 @@ describe('Test article fetching', () => { ![thumbnail](./thumbnail.jpg) this is some text `); + const date = new Date(2019, 5, 5); + date.setUTCHours(0); fw.fetchArticles((err, dict) => { expect(err).toBeNull(); expect(dict).toBeDefined(); @@ -258,7 +262,7 @@ describe('Test article fetching', () => { year: 2019, month: 5, day: 5, - date: new Date(2019, 5, 5), + date: date, title: 'Title with : info !', thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), escapedTitle: 'title_with___info', diff --git a/test/rss.test.js b/test/rss.test.js deleted file mode 100644 index 087a2eb..0000000 --- a/test/rss.test.js +++ /dev/null @@ -1,100 +0,0 @@ -/* jshint -W117 */ -const config = { - 'loopback_url': 'http://test.test/', - 'rss': { - 'title': 'test rss', - 'description': 'description', - 'endpoint': '/rss', - 'length': 2 - }, -}; - -const rss = require('../src/rss')(config); - -test('empty rss', () => { - const xml = rss.get({}); - expect(xml).toBe('' + - '' + - 'test rss' + - 'description' + - 'http://test.test/' + - '' + new Date().toString() + '' + - '' + new Date().toString() + '' + - '60' + - ''); -}); - -test('1 item', () => { - const data = { - 'a': { - path: 'a', - realPath: 'b', - year: 2019, - month: 5, - day: 5, - date: new Date(2019, 5, 5), - title: 'Title with : info !', - thumbnail: '/2019/05/05/thumbnail.jpg/', - escapedTitle: 'title_with___info', - url: '/2019/05/05/title_with___info/', - } - }; - const xml = rss.get(data); - expect(xml).toEqual('' + - '' + - 'test rss' + - 'description' + - 'http://test.test/' + - '' + new Date().toString() + '' + - '' + new Date().toString() + '' + - '60' + - '' + - 'Title with : info !' + - 'http://test.test/2019/05/05/title_with___info/' + - '' + new Date(2019, 5, 5).toString() + '' + - '' + - ''); -}); - -test('3 items only 2 shown sorted', () => { - const data = { - 'a': { - path: '2019/05/05', - date: new Date(2019, 5, 5), - title: 'a', - url: 'a', - }, - 'b': { - path: '2018/05/05', - date: new Date(2018, 5, 5), - title: 'b', - url: 'b', - }, - 'c': { - path: '2020/05/05', - date: new Date(2020, 5, 5), - title: 'c', - url: 'c', - } - }; - const xml = rss.get(data); - expect(xml).toEqual('' + - '' + - 'test rss' + - 'description' + - 'http://test.test/' + - '' + new Date().toString() + '' + - '' + new Date().toString() + '' + - '60' + - '' + - 'c' + - 'http://test.test/c' + - '' + new Date(2020, 5, 5).toString() + '' + - '' + - '' + - 'a' + - 'http://test.test/a' + - '' + new Date(2019, 5, 5).toString() + '' + - '' + - ''); -}); \ No newline at end of file From ca642f1cab90a2a7baade1aceffeab02904322da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Thu, 20 Jun 2019 16:29:04 +0200 Subject: [PATCH 27/42] code coverage with coveralls --- .travis.yml | 2 +- README.md | 1 + package-lock.json | 73 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aa98b10..3f71628 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ install: before_script: - npm install -g jshint script: - - npm test + - jest --silent --coverage --coverageReporters=text-lcov | coveralls - jshint ./src diff --git a/README.md b/README.md index 0d18322..4d4045e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ > This is a work in progress, some information written here might not be true yet. [![Build Status](https://img.shields.io/travis/Klemek/GitBlog.md.svg?branch=master)](https://travis-ci.org/Klemek/GitBlog.md) +[![Coverage Status](https://img.shields.io/coveralls/github/Klemek/GitBlog.md.svg?branch=master)](https://coveralls.io/github/Klemek/GitBlog.md?branch=master) A static blog using Markdown pulled from your git repository. diff --git a/package-lock.json b/package-lock.json index 3667629..0862e4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1339,6 +1339,15 @@ "normalize-path": "^2.0.0" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "arr-diff": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", @@ -2663,6 +2672,28 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "coveralls": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.4.tgz", + "integrity": "sha512-eyqUWA/7RT0JagiL0tThVhjbIjoiEUyWCjtUJoOPcWoeofP5WK/jb2OJYoBFrR6DvplR+AxOyuBqk4JHkk5ykA==", + "dev": true, + "requires": { + "growl": "~> 1.10.0", + "js-yaml": "^3.11.0", + "lcov-parse": "^0.0.10", + "log-driver": "^1.2.7", + "minimist": "^1.2.0", + "request": "^2.86.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -3929,6 +3960,12 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -6591,6 +6628,24 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -6702,6 +6757,12 @@ "invert-kv": "^1.0.0" } }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -6757,6 +6818,12 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "log-driver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", + "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9001,6 +9068,12 @@ "extend-shallow": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", diff --git a/package.json b/package.json index 04dee11..e309b2a 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "babel-cli": "^6.26.0", "babel-preset-env": "^1.7.0", + "coveralls": "^3.0.4", "jest": "^24.8.0", "superagent": "^5.1.0", "supertest": "^4.0.2" From 2552ac0ea2cd5a1faa6fecbb8c6910a17e263d3c Mon Sep 17 00:00:00 2001 From: Klemek Date: Thu, 20 Jun 2019 18:17:37 +0200 Subject: [PATCH 28/42] fixed path separator on multi-system --- src/file_walker.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/file_walker.js b/src/file_walker.js index b9bc8a6..d62b9e6 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -69,22 +69,20 @@ module.exports = (config) => { if (err) return cb(err); const paths = fileList - .map((path) => path.substr(config['data_dir'].length)) - .filter((path) => path.indexOf(config['article']['index']) === path.length - config['article']['index'].length) - .map((path) => path.substr(0, path.length - config['article']['index'].length)) - .map((path) => path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/$/)) - .filter((matches) => matches && matches.length > 1); + .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((matches) => { + paths.forEach((p) => { const article = { - path: path.join(matches[1], matches[2], matches[3]), - realPath: path.join(config['data_dir'], matches[1], matches[2], matches[3]), - year: parseInt(matches[1]), - month: parseInt(matches[2]), - day: parseInt(matches[3]) + path: path.join(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); From 61308c20e79efb0bc1ad701f5f4a876f309e8189 Mon Sep 17 00:00:00 2001 From: Klemek Date: Thu, 20 Jun 2019 18:23:10 +0200 Subject: [PATCH 29/42] fixed path separator on multi-system --- src/file_walker.js | 8 +++++--- test/file_walker.test.js | 16 +++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/file_walker.js b/src/file_walker.js index d62b9e6..b0bdcae 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -1,6 +1,8 @@ const fs = require('fs'); const path = require('path'); +const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/'); + /** * Get all files path inside a given folder path * @param dir @@ -78,7 +80,7 @@ module.exports = (config) => { let remaining = 0; paths.forEach((p) => { const article = { - path: path.join(p[0], p[1], p[2]), + 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]), @@ -91,9 +93,9 @@ module.exports = (config) => { if (err) return cb(err); article.title = info.title || config['article']['default_title']; - article.thumbnail = info.thumbnail ? path.join(article.path, info.thumbnail) : config['article']['default_thumbnail']; + 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 = '/' + path.join(article.path, article.escapedTitle) + '/'; + article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/'; articles[article.path] = article; remaining--; if (remaining === 0) diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 20ec67b..4eb6c2c 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -6,6 +6,8 @@ const utils = require('./test_utils'); const dataDir = 'test_data'; const testIndex = 'testindex.md'; +const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/'); + const config = { 'test': true, 'data_dir': dataDir, @@ -226,8 +228,8 @@ describe('Test article fetching', () => { expect(err).toBeNull(); expect(dict).toBeDefined(); expect(Object.keys(dict).length).toBe(1); - expect(dict[path.join('2019', '05', '05')]).toEqual({ - path: path.join('2019', '05', '05'), + expect(dict[joinUrl('2019', '05', '05')]).toEqual({ + path: joinUrl('2019', '05', '05'), realPath: dir, year: 2019, month: 5, @@ -236,7 +238,7 @@ describe('Test article fetching', () => { title: 'Untitled', thumbnail: 'default.png', escapedTitle: 'untitled', - url: '/' + path.join('2019', '05', '05', 'untitled') + '/', + url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/', }); done(); }); @@ -256,17 +258,17 @@ describe('Test article fetching', () => { expect(err).toBeNull(); expect(dict).toBeDefined(); expect(Object.keys(dict).length).toBe(1); - expect(dict[path.join('2019', '05', '05')]).toEqual({ - path: path.join('2019', '05', '05'), + 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: path.join('2019', '05', '05', './thumbnail.jpg'), + thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'), escapedTitle: 'title_with___info', - url: '/' + path.join('2019', '05', '05', 'title_with___info') + '/', + url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/', }); done(); }); From 553aa40fb3c702a414fbd3785cb7b9c03e6ae095 Mon Sep 17 00:00:00 2001 From: Klemek Date: Thu, 20 Jun 2019 19:15:49 +0200 Subject: [PATCH 30/42] generating git_secret at start --- .gitignore | 3 +- src/app.js | 56 +++++++++++++++++++--- src/config.default.json | 2 +- test/app.test.js | 101 +++++++++++++++++++++++++++------------- 4 files changed, 120 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index aba5ca9..606eb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /config.json /config.example.json /data -/test_data \ No newline at end of file +/test_data +/git_secret \ No newline at end of file diff --git a/src/app.js b/src/app.js index 4319149..05d62e1 100644 --- a/src/app.js +++ b/src/app.js @@ -14,6 +14,16 @@ const cons = { error: '\x1b[31m✘\x1b[0m %s', }; +const randStr = (length) => { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}; + module.exports = (config) => { const fw = require('./file_walker')(config); const renderer = require('./renderer')(config); @@ -25,16 +35,18 @@ module.exports = (config) => { const articles = {}; let lastRSS = ''; + let webhookSecret; /** * Fetch articles from the data folder and send success as a response - * @param callback + * @param success + * @param error */ - const reload = (callback) => { + const reload = (success, error) => { fw.fetchArticles((err, dict) => { if (err) { - callback(false); - return console.error(cons.error, 'loading articles : ' + 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]); @@ -46,12 +58,41 @@ module.exports = (config) => { lastRSS = ''; - callback(true); + success(); }); }; if (config['test']) app.reload = reload; + /** + * Fetch or create secret token for git webhook + * @param success + * @param error + */ + const checkSecret = (success, error) => { + if (!config['modules']['webhook']) + success(); + fs.readFile(config['webhook']['secret_file'], {encoding: 'UTF-8'}, (err, data) => { + if (err) { + webhookSecret = randStr(32); + fs.writeFile(config['webhook']['secret_file'], webhookSecret, {encoding: 'UTF-8'}, (err) => { + if (err) { + console.error(cons.error, 'error creating secret : ' + err); + return error ? error() : null; + } + console.log(cons.ok,'created git secret at '+config['webhook']['secret_file']); + success(); + }); + } else { + webhookSecret = data; + console.log(cons.ok,'loaded git secret from '+config['webhook']['secret_file']); + success(); + } + }); + }; + if (config['test']) + app.checkSecret = checkSecret; + /** * Render the page with the view engine and catch errors * @param res @@ -180,11 +221,12 @@ module.exports = (config) => { // must be use in a server.js to start the server app.start = () => { - reload((res) => { - if (res) + reload(() => { + checkSecret(() => { app.listen(config['node_port'], () => { console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); }); + }); }); }; diff --git a/src/config.default.json b/src/config.default.json index d5ac59a..027b7d2 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -30,7 +30,7 @@ }, "webhook": { "endpoint": "/webhook", - "secretFile": "git_secret" + "secret_file": "git_secret" }, "showdown": { "parseImgDimensions": true, diff --git a/test/app.test.js b/test/app.test.js index 2342920..900da2a 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -22,13 +22,10 @@ config['rss']['length'] = 2; const app = require('../src/app')(config); -beforeEach((done) => { +beforeEach((done, fail) => { utils.deleteFolderSync(dataDir); fs.mkdirSync(dataDir); - app.reload((res) => { - expect(res).toBe(true); - done(); - }); + app.reload(done, fail); }); afterAll(() => { @@ -60,7 +57,7 @@ describe('Test root path', () => { done(); }); }); - test('200 2 articles', (done) => { + test('200 2 articles', (done, fail) => { utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05'), path.join(dataDir, '2018', '05', '05') @@ -70,18 +67,62 @@ describe('Test root path', () => { path.join(dataDir, '2018', '05', '05', 'index.md') ]); fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>'); - app.reload((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/').then((response) => { expect(response.statusCode).toBe(200); expect(response.text).toBe('articles 2'); done(); }); - }); + }, fail); }); }); +describe('Test check secret', () => { + const secretFile = 'git_secret'; + const tmpSecretFile = 'tmp_git_secret'; + beforeEach(() => { + if (fs.existsSync(secretFile)) + fs.renameSync(secretFile, tmpSecretFile); + }); + + afterEach(() => { + if (fs.existsSync(tmpSecretFile)) { + fs.renameSync(tmpSecretFile, secretFile); + } else if (fs.existsSync(secretFile)) { + fs.unlinkSync(secretFile); //remove secret file if remaining + } + }); + + test('no check if not activated', (done, fail) => { + config['modules']['webhook'] = false; + app.checkSecret(() => { + config['modules']['webhook'] = true; + done(); + }, () => { + config['modules']['webhook'] = true; + fail(); + }); + }); + test('create if not exists', (done, fail) => { + if (fs.existsSync(secretFile)) + fs.unlinkSync(secretFile); + app.checkSecret(() => { + expect(fs.existsSync(secretFile)).toBe(true); + expect(fs.readFileSync(secretFile).length).toBeGreaterThan(0); + done(); + }, fail); + }); + test('read if exists', (done, fail) => { + fs.writeFileSync(secretFile,'secret value'); + app.checkSecret(() => { + expect(fs.existsSync(secretFile)).toBe(true); + expect(fs.readFileSync(secretFile, {encoding:'UTF-8'})).toBe('secret value'); + done(); + }, fail); + }); +}); + describe('Test RSS feed', () => { test('404 rss deactivated', (done) => { config['modules']['rss'] = false; @@ -99,7 +140,7 @@ describe('Test RSS feed', () => { done(); }); }); - test('200 2 rss items', (done) => { + test('200 2 rss items', (done, fail) => { utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05'), path.join(dataDir, '2018', '05', '05') @@ -108,17 +149,16 @@ describe('Test RSS feed', () => { path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, '2018', '05', '05', 'index.md') ]); - app.reload((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/rsstest').then((response) => { expect(response.statusCode).toBe(200); expect(response.text.length).toBeGreaterThan(0); expect(response.text.split('').length).toBe(3); done(); - }); + }, fail); }); }); - test('200 max rss items', (done) => { + test('200 max rss items', (done, fail) => { utils.createEmptyDirs([ path.join(dataDir, '2019', '05', '05'), path.join(dataDir, '2018', '05', '05'), @@ -129,14 +169,13 @@ describe('Test RSS feed', () => { path.join(dataDir, '2018', '05', '05', 'index.md'), path.join(dataDir, '2017', '05', '05', 'index.md') ]); - app.reload((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/rsstest').then((response) => { expect(response.statusCode).toBe(200); expect(response.text.length).toBeGreaterThan(0); expect(response.text.split('').length).toBe(3); done(); - }); + }, fail); }); }); }); @@ -149,59 +188,55 @@ describe('Test articles rendering', () => { }); }); - test('500 no template', (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((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/2019/05/05/hello/').then((response) => { expect(response.statusCode).toBe(500); done(); - }); + }, fail); }); }); - test('200 rendered article', (done) => { + 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 %><%- `reload` %>'); - app.reload((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/2019/05/05/hello/').then((response) => { expect(response.statusCode).toBe(200); expect(response.text).toBe('

Hello

reload'); done(); - }); + }, fail); }); }); - test('200 other url', (done) => { + 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((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/2019/05/05/anything/').then((response) => { expect(response.statusCode).toBe(200); done(); - }); + }, fail); }); }); - test('200 other url 2', (done) => { + 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((res) => { - expect(res).toBe(true); + app.reload(() => { request(app).get('/2019/05/05/').then((response) => { expect(response.statusCode).toBe(200); done(); - }); + }, fail); }); }); }); From c1c8672380595d1eb239abd78ad5ab4888086b27 Mon Sep 17 00:00:00 2001 From: Klemek Date: Thu, 20 Jun 2019 19:55:59 +0200 Subject: [PATCH 31/42] Webhook endpoint with secret in config --- README.md | 24 ++++++++++-- src/app.js | 60 ++++++++---------------------- src/config.default.json | 3 +- test/app.test.js | 82 ++++++++++++++++++----------------------- 4 files changed, 75 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 4d4045e..8837c18 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,29 @@ git push -u origin master **5. Refresh content with a webhook (optional)** -At first start, a `git_secret` file will be generated, use it to create a new webhook as following : +Create a webhook on your git source (On GitHub, in the `Settings/Webhooks` part of the repository.) with the following parameters : * Payload URL : `https:///webhook` * Content type : `application/json` -* Secret : `` * Events : Just the push event -On GitHub, webhooks can be created in the `Settings/Webhooks` part of the repository. +**6. Securize your webhook (optional)** + +Here are the steps for Github, if you use another platform adapt it your way (header format on the config) : + +* Create a password or random secret +* Calculate it's SHA1 +* Edit your configuration to add webhook info +```json +{ +... +"webhook": { + "secret_value": "sha1=", + "secret_header": "X-Hub-Signature" + }, +... +} +``` +* Launch the server +* Update your webhook on github to include the secret +* Check if Github successfully reached the endpoint \ No newline at end of file diff --git a/src/app.js b/src/app.js index 05d62e1..1ce5022 100644 --- a/src/app.js +++ b/src/app.js @@ -14,16 +14,6 @@ const cons = { error: '\x1b[31m✘\x1b[0m %s', }; -const randStr = (length) => { - let result = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -}; - module.exports = (config) => { const fw = require('./file_walker')(config); const renderer = require('./renderer')(config); @@ -35,7 +25,6 @@ module.exports = (config) => { const articles = {}; let lastRSS = ''; - let webhookSecret; /** * Fetch articles from the data folder and send success as a response @@ -64,35 +53,6 @@ module.exports = (config) => { if (config['test']) app.reload = reload; - /** - * Fetch or create secret token for git webhook - * @param success - * @param error - */ - const checkSecret = (success, error) => { - if (!config['modules']['webhook']) - success(); - fs.readFile(config['webhook']['secret_file'], {encoding: 'UTF-8'}, (err, data) => { - if (err) { - webhookSecret = randStr(32); - fs.writeFile(config['webhook']['secret_file'], webhookSecret, {encoding: 'UTF-8'}, (err) => { - if (err) { - console.error(cons.error, 'error creating secret : ' + err); - return error ? error() : null; - } - console.log(cons.ok,'created git secret at '+config['webhook']['secret_file']); - success(); - }); - } else { - webhookSecret = data; - console.log(cons.ok,'loaded git secret from '+config['webhook']['secret_file']); - success(); - } - }); - }; - if (config['test']) - app.checkSecret = checkSecret; - /** * Render the page with the view engine and catch errors * @param res @@ -164,6 +124,20 @@ module.exports = (config) => { } }); + //webhook endpoint + app.post(config['webhook']['endpoint'], (req, res) => { + if (config['modules']['webhook']) { + if (config['webhook']['secret_header'] && req.get(config['webhook']['secret_header']) !== config['webhook']['secret_value']) { + res.sendStatus(403); + } else { + res.sendStatus(200); + //TODO reload + } + } else { + res.sendStatus(400); + } + }); + // catch all article urls and render them app.get('*', (req, res, next) => { if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.test(req.path)) { @@ -222,10 +196,8 @@ module.exports = (config) => { // must be use in a server.js to start the server app.start = () => { reload(() => { - checkSecret(() => { - app.listen(config['node_port'], () => { - console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); - }); + app.listen(config['node_port'], () => { + console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`); }); }); }; diff --git a/src/config.default.json b/src/config.default.json index 027b7d2..9e30297 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -30,7 +30,8 @@ }, "webhook": { "endpoint": "/webhook", - "secret_file": "git_secret" + "secret_value": "", + "secret_header": "" }, "showdown": { "parseImgDimensions": true, diff --git a/test/app.test.js b/test/app.test.js index 900da2a..3174ce5 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -19,6 +19,7 @@ config['article']['template'] = testTemplate; config['home']['hidden'].push('.test'); config['rss']['endpoint'] = '/rsstest'; config['rss']['length'] = 2; +config['webhook']['endpoint'] = '/webhooktest'; const app = require('../src/app')(config); @@ -74,52 +75,6 @@ describe('Test root path', () => { done(); }); }, fail); - - }); -}); - -describe('Test check secret', () => { - const secretFile = 'git_secret'; - const tmpSecretFile = 'tmp_git_secret'; - beforeEach(() => { - if (fs.existsSync(secretFile)) - fs.renameSync(secretFile, tmpSecretFile); - }); - - afterEach(() => { - if (fs.existsSync(tmpSecretFile)) { - fs.renameSync(tmpSecretFile, secretFile); - } else if (fs.existsSync(secretFile)) { - fs.unlinkSync(secretFile); //remove secret file if remaining - } - }); - - test('no check if not activated', (done, fail) => { - config['modules']['webhook'] = false; - app.checkSecret(() => { - config['modules']['webhook'] = true; - done(); - }, () => { - config['modules']['webhook'] = true; - fail(); - }); - }); - test('create if not exists', (done, fail) => { - if (fs.existsSync(secretFile)) - fs.unlinkSync(secretFile); - app.checkSecret(() => { - expect(fs.existsSync(secretFile)).toBe(true); - expect(fs.readFileSync(secretFile).length).toBeGreaterThan(0); - done(); - }, fail); - }); - test('read if exists', (done, fail) => { - fs.writeFileSync(secretFile,'secret value'); - app.checkSecret(() => { - expect(fs.existsSync(secretFile)).toBe(true); - expect(fs.readFileSync(secretFile, {encoding:'UTF-8'})).toBe('secret value'); - done(); - }, fail); }); }); @@ -180,6 +135,41 @@ describe('Test RSS feed', () => { }); }); +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('200 no secret', (done) => { + request(app).post('/webhooktest').then((response) => { + expect(response.statusCode).toBe(200); + //TODO test reload + done(); + }); + }); + test('403 wrong secret', (done) => { + config['webhook']['secret_header'] = 'testheader'; + config['webhook']['secret_value'] = 'testvalue'; + request(app).post('/webhooktest').set('testheader','testvalue2').then((response) => { + expect(response.statusCode).toBe(403); + done(); + }); + }); + test('200 valid secret', (done) => { + config['webhook']['secret_header'] = 'testheader'; + config['webhook']['secret_value'] = 'testvalue'; + request(app).post('/webhooktest').set('testheader','testvalue').then((response) => { + expect(response.statusCode).toBe(200); + //TODO test reload + done(); + }); + }); +}); + describe('Test articles rendering', () => { test('404 article not found', (done) => { request(app).get('/2019/05/06/untitled/').then((response) => { From 0f5b3f138d07cd985322a1bb411aa95eeabf936f Mon Sep 17 00:00:00 2001 From: Klemek Date: Thu, 20 Jun 2019 20:19:03 +0200 Subject: [PATCH 32/42] valid github signature check --- README.md | 6 +++--- package-lock.json | 5 +++++ package.json | 2 ++ src/app.js | 21 ++++++++++++++++----- src/config.default.json | 4 ++-- test/app.test.js | 23 +++++++++++++++++------ 6 files changed, 45 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8837c18..3d30a55 100644 --- a/README.md +++ b/README.md @@ -63,14 +63,14 @@ Create a webhook on your git source (On GitHub, in the `Settings/Webhooks` part Here are the steps for Github, if you use another platform adapt it your way (header format on the config) : * Create a password or random secret -* Calculate it's SHA1 * Edit your configuration to add webhook info ```json { ... "webhook": { - "secret_value": "sha1=", - "secret_header": "X-Hub-Signature" + "endpoint": "/webhook", + "secret": "sha1=", + "signature_header": "X-Hub-Signature" }, ... } diff --git a/package-lock.json b/package-lock.json index 0862e4d..4328a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2704,6 +2704,11 @@ "which": "^1.2.9" } }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "cssom": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", diff --git a/package.json b/package.json index e309b2a..9acd811 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,8 @@ "description": "A static blog using Markdown pulled from your git repository.", "main": "src/server.js", "dependencies": { + "body-parser": "^1.19.0", + "crypto": "^1.0.1", "ejs": "^2.6.2", "express": "^4.17.1", "ncp": "^2.0.0", diff --git a/src/app.js b/src/app.js index 1ce5022..f5f1ef2 100644 --- a/src/app.js +++ b/src/app.js @@ -3,6 +3,9 @@ const app = express(); const fs = require('fs'); const path = require('path'); const Rss = require('rss'); +const bodyParser = require('body-parser'); +const crypto = require('crypto'); +app.use(bodyParser.json()); /** * Terminal colors and symbols to display status messages @@ -127,12 +130,20 @@ module.exports = (config) => { //webhook endpoint app.post(config['webhook']['endpoint'], (req, res) => { if (config['modules']['webhook']) { - if (config['webhook']['secret_header'] && req.get(config['webhook']['secret_header']) !== config['webhook']['secret_value']) { - res.sendStatus(403); - } else { - res.sendStatus(200); - //TODO reload + if (config['webhook']['signature_header'] && config['webhook']['secret']) { + const payload = JSON.stringify(req.body); + if (!payload) { + return res.sendStatus(403); + } + const hmac = crypto.createHmac('sha1', config['webhook']['secret']); + const digest = 'sha1=' + hmac.update(payload).digest('hex'); + const checksum = req.headers[config['webhook']['signature_header']]; + if (!checksum || !digest || checksum !== digest) { + return res.sendStatus(403); + } } + res.sendStatus(200); + //TODO reload } else { res.sendStatus(400); } diff --git a/src/config.default.json b/src/config.default.json index 9e30297..f138c22 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -30,8 +30,8 @@ }, "webhook": { "endpoint": "/webhook", - "secret_value": "", - "secret_header": "" + "secret": "", + "signature_header": "" }, "showdown": { "parseImgDimensions": true, diff --git a/test/app.test.js b/test/app.test.js index 3174ce5..1ca5f06 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -151,18 +151,29 @@ describe('Test webhook', () => { 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']['secret_header'] = 'testheader'; - config['webhook']['secret_value'] = 'testvalue'; - request(app).post('/webhooktest').set('testheader','testvalue2').then((response) => { + 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']['secret_header'] = 'testheader'; - config['webhook']['secret_value'] = 'testvalue'; - request(app).post('/webhooktest').set('testheader','testvalue').then((response) => { + config['webhook']['signature_header'] = 'testheader'; + config['webhook']['secret'] = 'testvalue'; + request(app).post('/webhooktest') + .send({}) + .set('testheader', 'sha1=d924d5bd4b36faf9d572844ac9c12a09ce3e7134') + .then((response) => { expect(response.statusCode).toBe(200); //TODO test reload done(); From 00d1d6c4e198e2ff089f5b287982a9b460d9647c Mon Sep 17 00:00:00 2001 From: Klemek Date: Thu, 20 Jun 2019 20:37:35 +0200 Subject: [PATCH 33/42] webhook : pull command then reload articles --- src/app.js | 16 ++++++++++++-- src/config.default.json | 3 ++- test/app.test.js | 48 +++++++++++++++++++++++++++++------------ 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/app.js b/src/app.js index f5f1ef2..2f34a07 100644 --- a/src/app.js +++ b/src/app.js @@ -2,9 +2,14 @@ const express = require('express'); const app = express(); const fs = require('fs'); const path = require('path'); + +//rss const Rss = require('rss'); + +///webhook const bodyParser = require('body-parser'); const crypto = require('crypto'); +const cp = require('child_process'); app.use(bodyParser.json()); /** @@ -142,8 +147,15 @@ module.exports = (config) => { return res.sendStatus(403); } } - res.sendStatus(200); - //TODO reload + 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); } diff --git a/src/config.default.json b/src/config.default.json index f138c22..a959260 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -31,7 +31,8 @@ "webhook": { "endpoint": "/webhook", "secret": "", - "signature_header": "" + "signature_header": "", + "pull_command": "git pull" }, "showdown": { "parseImgDimensions": true, diff --git a/test/app.test.js b/test/app.test.js index 1ca5f06..730d9e8 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -110,8 +110,8 @@ describe('Test RSS feed', () => { expect(response.text.length).toBeGreaterThan(0); expect(response.text.split('').length).toBe(3); done(); - }, fail); - }); + }); + }, fail); }); test('200 max rss items', (done, fail) => { utils.createEmptyDirs([ @@ -130,8 +130,8 @@ describe('Test RSS feed', () => { expect(response.text.length).toBeGreaterThan(0); expect(response.text.split('').length).toBe(3); done(); - }, fail); - }); + }); + }, fail); }); }); @@ -145,9 +145,29 @@ describe('Test webhook', () => { }); }); 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); - //TODO test reload + 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(); }); }); @@ -170,12 +190,12 @@ describe('Test webhook', () => { 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); - //TODO test reload done(); }); }); @@ -196,8 +216,8 @@ describe('Test articles rendering', () => { request(app).get('/2019/05/05/hello/').then((response) => { expect(response.statusCode).toBe(500); done(); - }, fail); - }); + }); + }, fail); }); test('200 rendered article', (done, fail) => { @@ -209,8 +229,8 @@ describe('Test articles rendering', () => { expect(response.statusCode).toBe(200); expect(response.text).toBe('

Hello

reload'); done(); - }, fail); - }); + }); + }, fail); }); test('200 other url', (done, fail) => { @@ -223,8 +243,8 @@ describe('Test articles rendering', () => { request(app).get('/2019/05/05/anything/').then((response) => { expect(response.statusCode).toBe(200); done(); - }, fail); - }); + }); + }, fail); }); test('200 other url 2', (done, fail) => { @@ -237,8 +257,8 @@ describe('Test articles rendering', () => { request(app).get('/2019/05/05/').then((response) => { expect(response.statusCode).toBe(200); done(); - }, fail); - }); + }); + }, fail); }); }); From 5fb8e0fa4523c47202bde07e97b7fdce95e5536c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 09:09:08 +0200 Subject: [PATCH 34/42] Add rewrite url for articles relative resources --- src/app.js | 9 ++++++++- test/app.test.js | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/app.js b/src/app.js index 2f34a07..f6802d2 100644 --- a/src/app.js +++ b/src/app.js @@ -161,9 +161,16 @@ module.exports = (config) => { } }); + //rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png + app.use((req, res, next) => { + if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url)) + 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}\/(\w*\/)?$/.test(req.path)) { + if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) { const articlePath = req.path.substr(1, 10); const article = articles[articlePath]; if (!article) diff --git a/test/app.test.js b/test/app.test.js index 730d9e8..c694be9 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -286,13 +286,22 @@ describe('Test static files', () => { }); }); test('200 valid file', (done) => { - fs.writeFileSync(`${dataDir}/somefile.txt`, 'filecontent'); + 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('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', () => { From 193e7f62cb260a7879cb4b1f90f6c1547adc5341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 11:31:26 +0200 Subject: [PATCH 35/42] Prism code rendering --- package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ src/config.default.json | 3 +- src/renderer.js | 23 ++++++++++++- test/renderer.test.js | 43 +++++++++++++++++++++++ 5 files changed, 144 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4328a5a..9fb4096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2543,6 +2543,17 @@ } } }, + "clipboard": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz", + "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==", + "optional": true, + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -2850,6 +2861,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -3959,6 +3976,15 @@ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "optional": true, + "requires": { + "delegate": "^3.1.2" + } + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -7145,6 +7171,35 @@ "which": "^1.3.0" } }, + "node-prismjs": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/node-prismjs/-/node-prismjs-0.1.2.tgz", + "integrity": "sha512-WKb6ZbUlPWarzS8jR2UdIbV4lYpt6sOTkIx3u5Ldz55K1Zzs982KyF6aj1zjZbrrx/UGZSZ1e0j28lIzcm3ceg==", + "requires": { + "prismjs": "~1.6.0" + }, + "dependencies": { + "clipboard": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz", + "integrity": "sha1-Ng1taUbpmnof7zleQrqStem1oWs=", + "optional": true, + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "prismjs": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.6.0.tgz", + "integrity": "sha1-EY2V+3pm26InLjQ7NF9SNmWds2U=", + "requires": { + "clipboard": "^1.5.5" + } + } + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -7620,6 +7675,14 @@ } } }, + "prismjs": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.16.0.tgz", + "integrity": "sha512-OA4MKxjFZHSvZcisLGe14THYsug/nF6O1f0pAJc0KN0wTyAcLqmsbE+lTGKSpyh+9pEW57+k6pg2AfYR+coyHA==", + "requires": { + "clipboard": "^2.0.0" + } + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -8769,6 +8832,12 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", + "optional": true + }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -9323,6 +9392,12 @@ "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", "dev": true }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", diff --git a/package.json b/package.json index 9acd811..a09266c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "ejs": "^2.6.2", "express": "^4.17.1", "ncp": "^2.0.0", + "node-prismjs": "^0.1.2", + "prismjs": "^1.16.0", "rss": "^1.2.2", "showdown": "^1.9.0" }, diff --git a/src/config.default.json b/src/config.default.json index a959260..6c9827d 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -6,7 +6,8 @@ "modules": { "plantuml": false, "rss": true, - "webhook": true + "webhook": true, + "prism": true }, "home": { "index": "index.ejs", diff --git a/src/renderer.js b/src/renderer.js index 236d931..f24a81e 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1,5 +1,6 @@ const fs = require('fs'); const showdown = require('showdown'); +const Prism = require('node-prismjs'); module.exports = (config) => { const converter = new showdown.Converter(config['showdown']); @@ -8,9 +9,29 @@ module.exports = (config) => { fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => { if (err) return cb(err); + + if (config['modules']['prism']) { + const codeRegex = /```([\w-]+)\n((?:(?!```)[\s\S])*)\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) + `
` + block + '
' + data.slice(match.index + match[0].length); + } catch (err) { + console.error(err); + } + } + } + + const html = converter.makeHtml(data); + + cb(null, html); }); } }; -}; \ No newline at end of file +}; + diff --git a/test/renderer.test.js b/test/renderer.test.js index 37aa81f..c2912eb 100644 --- a/test/renderer.test.js +++ b/test/renderer.test.js @@ -7,6 +7,9 @@ const dataDir = 'test_data'; const file = path.join(dataDir, 'test.md'); const config = { + 'modules': { + 'prism': true, + }, 'showdown': { 'simplifiedAutoLink': true, 'smartIndentationFix': true @@ -50,4 +53,44 @@ test('custom rules', (done) => { expect(html).toBe('

www.google.com

'); 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('
print("hello")\n
\n
print("hello")\n
'); + config['modules']['prism'] = true; + done(); + }); +}); + +test('prism correct', (done) => { + fs.writeFileSync(file, '```python\nprint("hello")\n```'); + renderer.render(file, (err, html) => { + expect(err).toBeNull(); + expect(html).not.toBe('
print("hello")\n
'); + expect(html.indexOf('
')).toBe(0);
+    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('
print("hello")\n
'); + expect(html.indexOf('
')).toBe(0);
+    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('
\n
'); + done(); + }); }); \ No newline at end of file From 17db174926ded2f8354a5c33edb94a2db13617a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 11:38:13 +0200 Subject: [PATCH 36/42] Sample templates --- sample_data/article/birthday-cake.png | Bin 919584 -> 0 bytes sample_data/article/index.md | 169 +++++++++++++++++++++++++- sample_data/home/error.ejs | 8 +- sample_data/home/prism.css | 143 ++++++++++++++++++++++ sample_data/home/style.css | 138 ++++++++++++++++++++- sample_data/home/template.ejs | 16 ++- 6 files changed, 461 insertions(+), 13 deletions(-) delete mode 100644 sample_data/article/birthday-cake.png create mode 100644 sample_data/home/prism.css diff --git a/sample_data/article/birthday-cake.png b/sample_data/article/birthday-cake.png deleted file mode 100644 index c5d5d9fc153968394bda9cb2561669732efcdd2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 919584 zcmeI*37A|}ng8)33kgfY(qRvz*=1E{*by)g)Bplv5FAk$70?lILAJ(0U}*mej~gH; z;KB?d$^gnbia>%Rpb`N=A%TI2AVQigfb8=By(HCL)m_zftLol+w(ohKH>vgBb3X5> zdaG`os{YEpd%t_qvTH6|sZ=J-e9w&iDwU;XS1MyF6PMN{t8Dnof6;%-eC9nLJi1a@ zuE%1j%6S*8Ua9m}X3m&C>-eQAJ^cfHV|62%JAnz>^+Lt_6mMU0`Z4`aOGAMCr;+Do zqa^>$WVk#Dfhij08^hvGoqtafqi2xhKkYd{{?pEX@}pl@0>cYn_57u}Zmr~n;szl9 zap$+y=*WL7A+C>FU`vgzTJReEZ-+FjIpY4u@{I@H{?V}RdU!xgb=tevLb={sK7|#?S|MBFqlaY}BPSm(hGy-;*bF_lj z=(=y}J`}+?rU3bmBafYqg8X+X$Ni!ZI9H=+RODJ+c3c#rXAtB+?KwdH)6ReCqu=3( zhc@$dEx;UUv?_efu0No3Np_gG0Qt{bG_38B|FH5!Z*vr|Lz)AMW(BU*g!qn=0dr1fxUSqaI^Z8<=tME+t@G4{Ih4{qdTJukpIzrV!xLB$DU!v*Hl1% z!>aPSB1>`0dYiknz41-=$Q3>bkbj@DQGQ>@fBEL@(IbI_)R(Mf=L-b_-g&w|_wUm=J(RYF#A% zsb#!=yyU;$gL8=q9H{ZOTg*DW{n3fphA>?6A3~Oj>m&Iu&Xz5EE?^(j%~f<-(AvG< zGuqdRo|~l9J@TJYwnvV&{QSRo`?XIFtp(t@U}R3X#F0Qz>!L?1k|#MDE4v*jK>kOX zq;v`SPbu4swN7A)&Mj1oR;-G+?rH6Fg}R<_#)Sa+ccJdjKFPm7xv1;O*t&C^DN5iR zZFq;GHa^OBC+fQA6~7wgHe6M_0QoQ8*ylZvf1k5M-wG9&p{*D3?k?*#^^riuzKle3 zBS8M$xcjnK^6yI~=tt=SYio~>E3y`=qHgzz(ns!ihkLy3mW_ti0yyr>;@vYByhwwy zPLO|R^z7KRz_RK7o@?VT=C|f1}p=64F<}P4=zWqmvtcA$c?QY9`oI{F8qk|7p*G zWJhnm`**9tWijhy-*+UNv3v*aWBymV?Cx8tYXP#%4dy~VbHc~DK>qW|En8_JV22dv zE3!;DA8*4d(H04AaW(mG0TlVwXx-nhm;8x*}lks zWC?9^IOM-gFm4`?z+M{43yQ3T#@E|j5>HA>9SQj_mD{_Wk$>+}LWjx{I7~aTZ<_mB z)Y|FSQyRy(vfv`w5BZNIksS_${C5b)-HH~N_V@FqRo4O(y{}5;GulNv#U_N?zO{I^ zX!bz7)R;+;i@x68n zODMNLzgW5`wod+|NMpx?ApafHamUaF?9V^P^7&t*qI_rQ2`ae%tb zD^6ftZTd+?qrwr(Wv^*`%N0i$^p?qgDCzslnLpM_mEOwC8PjJK(`V*FF|OWi)&AxO ztzKZF&R(Hdx&Gql&W+WGHi~Xk-4BTTcW)T-7RZ0d*=w`!6u^;S$LgX~R!jDd9Deeb zkpHx&0QpZS{}jObWZR*S6Y4LD`kalIHY7m)hb&S&Mg9}ZIt8#k;os0%I|PzSqX5=t zn*7&$q;`({Czf#v;84Q1(aF7s&ia39?jBowKgYEI6vLsE2R9dny58|A@;^jJ{)a4* zIYluXO7-DGCzCm~YBD2jJy7!B+9>8*kpGyo+hlAMz{M~t=)VJ-7{z??@-Yvtjj@sc zHn1YRDfy2usab|Z0bH!~A^m565Y<#+g?&%%#&9YSU4Y z|E4raTtWVmNOR*s@@}wF*;F(s7C)O2yZ|Eqjh`9Pxs=C$I=LP>(w_c-KB{1)vfW6U zWG;CznUOXhDEV)0lfpIRKZPte86yR-Qn4=trP?Tf7&is{A`j0Zh99F3f*Q1-HeNp8&}}kN@Bvuz7Zv0+{v#;L$uMQ=r>Qxd4)X9{+(p zZ1d?h1&{|oJ~JuYtxy2N&0QWnC;xdU7QT%;4&A)3(6s;+ieV*bgTEF&hyC9{%8vo^ z&*R_!M`(dPO!>iIWqq!CBEkRvoZy&;51ITA7fI_p`A;j~b)%&KRw|3?MAABQXIjH; zI9l@GkR_Q5$$v7b9yw47V5KrJk|l{t?1BGK5+fabkmP@KpV+S@|FLIyl<`pjE0qUE z@rn7WyEM3e#5}-^WAi`%2b^}w`o*pVU@k14EdI}hjFY;}n~{HkQ4A}U`E5>|SZ+Qy zmVtCR4)Wh6VBmKr|A8m4o9Wd3K%r=cyUqw#a{YIm@lb{N0?z zzZaU?((uz>7^!_LR>E(QI#JmHnR34MYy@MiYoVZ29r1&k) zHTh3t&Nmw+`EMqZz~$Z-z;PG9{8F)N0Y+r``Vp7JeZh5c53lXfk^i>IqPhe5k1CxE zrd@&Ub%!?+30o05wOyddZ%_UsPjHLjk^dGzTphQ7eJ*;xB84LO^SD#l(dfv3M{LpD ziTp>C$~Fhm(?8JX{V~xt$JFV~t@{0-(>n)tzf{KAPFl!+J6iGGmi)(;Q%2OLKy{g9 z%x?wl@8nEq(w+bkm^|5=o{P1FKM zXml|az@wthT7H8g|M^*lxFzx*V%F$$ZUW=9r8^Wc6~W(YJT@*`YXS1#+9>8*kpGyo z%h)mrY_41VUE!bzu6EGmod(wer zKBjTyU5s38?~F+EQ}nRNf6>BzZ=d}8ogaEvuz-DY_5y{AqLtS^Z&v@u7wi=4=E;Ao znawsX@}JF#TSY8jpAB58$g3cgxgGm8;58!7O&LQY|79rqyASg3Z*J&WNdopYf;oyZ zi&wY%dRqN`Ye_z_?}q%xp5f8QS7QFpxNyo3a$gI;bHV8O<=Qp{>;Z9=qT3=>#{Czm zpDciBGr=uxPX1c}MSeB;k37K)ueE?38u9(}!6(#5`w4tn{sQDbf76h*Nd8007JV&5 zz&<4ZsKU`#Cq{h-dZ+reP9dXaqg4U&UzJGnfAXJZ-Wl~U0sGF7PhX-xU;SBbSRXlC zoB;VR&NhTClm8I1L>~(k7^|%xp>X+|q7P~RGYTDLSsUMK@^OC}TnkXvPIKE2b0Ig% zAU8z*gG?Kp&RbwrZSc#A=M-fuc(Hv9D2`F^4p0b=8~Jyf0LQy0|Be&D?)?y0N!>V6 zQRJVapRes7tC*yq`6WR9{mMt#y&?Z)i?cs31?)rf?G)cs{87>EyTW1o@h z`7J>H{mxI(y(9lc3+K80p8kP8o>lV{ZM-e}sg|7;n|^Lw#Oh zf4$S*M%vrGdlhpOk0@y23XuPBbC*ZY$^R&dJ3MpZY2mH~7-e8wg#ZEwAbVCJ&02umS=k^02;?n5G0a<(?IC~w0tf^l zKmiOQO>`0g1Q0+VZvhHm-lA*|0R#|0AP4~pU=V4dlL#PyKu`jE?)B^$L9GR#7zQ=J z=qv&VAb>z#0u;c!B-suE2q1t!Py!UdpwdNW5kLR|1o9G~0Olphb`U@S0R(~)pa2Gy zE;<{AzCp<@UjfB*tT3glVy-0u;a^RM{p1i3zNF*&kPC zEkI&Pk8HRU!^jfKa1cNM0R*xMPyn+zaVrE6KmdWr1So)!C6wVHfB*srWD}qOW^>|J z2q1s}0+9(&0J~4ozb&n1Q0*~0ha<4K$rUL8vz7D7P$PWY4@`hAmj|z^qpc@gTYAz5I_Kdgamr} z2l}Xj2_>ANB7gt_2n-2O0Ea9%g#ZEwAdrv%1u&sxGgJf+KmdUu0Se%d1*ftKY`OC% ze$QHftXkX-0R#dNpcn>_B)W(I0tg_GzW@a=e^a)I00Iag5P$#$Fn}b{MFbE)0D=4k zD1iB!vPA?CKp;+mG55aj!;!89pcuwEWf>I$2q1t!3jqpX3m~pW009ILh*N+97-wD? z6#@t#fItfY3SbK$u0{X>1Q3W*fC3n2UKv#Z0t>(Qja?#L3s8U-n?xWi0g7Q*`J%T7 zAb``y+r^41Q5tcfC88kB%4710R#{TOCW0jyl97G zR%9(eSh=OQ2q1s}0vQFe7Q>92<7NmTfB*sr1R~JWKhQ@N3?x}}69EJeKpzR0u;b> za?MB)KmY**Y6K{NH5i<%RQCP+h4--*fSG^*0;vg53{%TE<3#`g1Q2*rfC9)wKmY** z5J*#?r+=W2Dwt;O88re3AdtKO1u*$30006ABrb5pxoh(`0w{)wKMEK=0tg_GssIHr z)yy+)1Q0+VaRCZo;uC=3BY*$`sR~d4Q_Vc%MgRc>5*MHVCO!ce{tEBi?sH-wQ)Mo(WM*sl?5*45TCYpGLjQ|1&q%J@KOnnA0egqIeAW;DdV4{g<*a#qiK-vNy z*?R6-Lu&yjhH1|RMvnjj2qY^&0Zcaa3>*Oj5J+2q0+@FG89f3BAdsv81u)svGjIeD zKp<@a3Sip#XY{EG?D~%9w;5Uskg5XXMqp5YVmL^_2?P*80D+VQD1a$to3SE*00IaM z3QzzCDL8=u0tg_Glt54aKp#~wsgyHV1Q0*~fvP}i1@Qb&?7xt;0M+dCKLQ9KfWVsq zsTD)gjsOA(Ab`M|0u(?d0s;sifIyN06u>0Y&Y%%M009IjfXo2|5J*8_nR$KtuofVN zOm{d&ieZOv+zkN)5I`Vo0SaK)`J?v;AbufxcyS zdN*qULdzQcMZlc^#n7ESdqw~O1Q2j2Kml}!&#nmwE!;r%)SxuLV#lEMH=V? z0tg_0K-mHmz_P{J9|8y69^!HKwtt96~n;f=`I2YAb@~V0Scf~es+!k0tg`BN`M0BN}l~9fB*sr zI2E7(I^}2Q2q4hCz%rXHc$l>S-8&|}g`WO_J}P43lYrqPfB*ui3Qz!3%{=2q009IN z7oY$pJ^>g$0tg_GssIHr)yy+)1Q0+VaRCZoz!TuCD?e~GYXJgIAYDfQ0dE8-hTf!t zjv#;l0tl2TKmjaMn|&dG00IbjBR~Q4CKYr90R#|0piBV@V42$N3jqWW$U)$3Cm-?( zYXNeAtl7j;HK%<$xq>>h@sikR$^TRJ|D<6tUgv2DAbETX?5MhY&yj0R%b`xLbF+L2;wvW`!38(RBf|qPM@|BZ?gr z9G(>d4FLoYh*;ns8r!cG|EaiC;j9?CD1cVvc2^vt*jF)8K|=rm1Q1A1;3WifZp;}sheG!8b400Iag5Qo468pf9u_73s2B8OCT0o-1lKUeYAA_u{?5kLR| z1fmnTOC$QE;?|-@R@4HNDeCYAidz&EzoN3ob`d}Tfp`V1c32HLU(r*=!5)g+yV+5?2rfagMz)4nKP!(YJF>Nfk27^%WKe9kL=KB zN3GLUF}A~t6`kI{qt0?C1Q0*~0R(CUtOmVbF;8(%&E=U+cJg_kz5TSRJ3iAMxDf&f zAbJN*yDuDKO@;*f&KiM6MBK<@F0R#{TPQZR?`z;FljId#AbqLgc^?JvK z_vAtZ5I~?nfzO}($`PyuC=fWUP1_;Sl@(3j5svn5(EbAMEopf%S_BY40D;H^td`x@ zXiUut;5gmbzGJ~(z-^?)We6aE00N;1+^LS*S0-K?-r;6nk2qL2r1%Zj;yeNfAb>z< z0&i1?Khmhvjoty;yT5xC8#KBDmmz=v0tg@wn7{+-?yM<69^!H00NN+Y^HI{tR2hn0(eO6m7GQZ0R#|0AR>Xf)iJflLG_oa zD}Pf=jA*b71px#Q2t?rVgT8wWXJ?Gr-^p{dXiKmY**VicI5 zK@I)|h}r^J_ck%ckuxR)5I_Kd0tg`BNMNcR0-fY&=j;{%1Q0*~0apT(D1ff$ z*)IYTn7H$8=eb`CKq?SOUVvhl{1gBH0R#|8Q(%&PLH-rR_%z4Qs1ZN_0R$2ic%{!^0`{RG4FLoYKmdW{1RNE>$M#-yH){csn{5mj0R#~6RKQU& z^i+dxA%Fk^2qYsw0Zb;<3={zb5I~?-fC5;n!D$2#KmdVc1So*Xq?&;u;Df;Hm%RUI z)&lsD3HnekKryWM;2Z)7Ab>z30u;bR63s9XKmY**>IEo(^&Xr<009ILNJM}Fm`I`- zCISc`fIz(f1+doA z0tg_`fdB=t12pb}00Iag5WWBfF#H@c0t687UEps|Kl|^j1@Jmc6hp7mL+21c009Jw z5}*JUCChdZKmY**ycVDUdYv9RhX4WyAW)b!{}4Y01-d{0R$QePyidLa2Wy!Ab>#h0u;dLQ_KJnKmY** z8VOJU8>w&^0tg_0KsN$=?)B^$L9GR#7?KmqJVkNYCfP+-5!9z8p#wEzuA!G#E1Q0*~fs_O&fGK60u_Ax~0tgHWPyh!hIDr5H2q2J>K>h`= z_k=I6!CHWn<`82=009Jo7s$UD22a8O5I_I{1X2*70H%;-#)tp{2p}**fC4yzgbNTr z009J&5a{V2=%WfIk!l8sK(PXor~cO*)&dl3%+`w)pcoeG%hnM<009I%6`%lmni#r; z00IagP^)|OK=npcx`d9R{6r#9AUn`gAN1YXl{A5>JIt5kt23l+`g z)JwW<8AYQxWtXk0*iccOSL^EZSVeMeDUJAzL`Hq5#(AUSHibPgJ)p3nR#Y?FH5E3= zwopu0yi>8Ff)HqU5r2)wFc+^Vp7 zb-Tjm)B=TFUQ}CJ8@3nk_5y1wh0UpL6ww#Om`Hm5B9S5f2(b8S9T zv!f68{AABlHm}~Qh_YD%VhMZo{Z+-5fn>@KeSKJA6F-(5#5N8qaKBJ|P+^Cad~bG` zehSao`a;)jYC6`{|oUznA+yicp`R)b)mK z2VMGeMTj{Gz0Xg`{TPKUMv19S(O~|c!agPlHAlg9+zQ_}6>BPDZdM5QQ`kbDP$ySN z{kC^W?^VQB_%x)tX74_(RfLqAQ2S~h&m66=k3C{;)k6531{qqeg6Xh*F7q8l?1gXm z1EpFB1363@%zUaoQ127?o{HED-|%^5EB=0^s5f3+IGPaLpG^E;g z(~nNpv(b{Z0JT>%I=!VXyhY)E4tx3s>V9F(j^7=j zU=4hG4=(%ALJM1IO$2t*<&P+Qe!}vxZ{L<#2;ZM+lJ-I`UQJuSUg2ZjtWREc>?`Wm zci6=W4Y5dDEF&1+z|eOKw>sV24EQvr<;f)IYfF7pnKKaq8;iX{*_HyE3i?=xR@1 z-uWHs6^BRb=a)UP+ER56e|9!y*VNtSC`Kz#WnSk+N9=db?3Y_<-3i#b_XUbFCy=lE zx?K+ID`?#b*x}J@6u#!U%ziyDhYxkf$-O0D3mm_sC^L7>_T{t_PYYom1uZ`TTiy3F zMYDvbaQT@Uf6Cu|$Zs|+-jW?2JywzO;nBM_{1XEkK@~{JI8h$Af9*2>i2l^Qa=v9Qw8``^(5{l*7qh zBrsWVmBP1qkY|7FMWnr$q?IFJU!kzC#pRj9EZZt=hp%dIA1}v}y$lnuRan1OWSJ15 z-R44#i?2QmOLz%q?FsA4iqPgumj2q2g?}o+kX?)vI9M0GtjLn5JZ@9Ujs`TKT}FdexEfzf%D#7XPlu^=MmvbVIeP5(8a5SMdv+hn$ z)2{7D2=d75Xxk`s2bX97_F*T@F9G{ILH2c&(I$qk*WIChZ{Qba)VRlnvcx}88} z-v4~{mtW@abV8|H;}k9yHFPrvRp~1`_)D66&#Z=gQc=`rgtLHgP(lcx*`c3v z72Q06lyTqZ)W<^L z@Mi$CX;>HEt@HLuxX}c0ec27_SFeH=mcVN2uCG7jzDRvLRKXFdu;!E<5_;Ruk0&P}c{zdVWqMNyp=Y4;vzD!fl z!WEdVu0N#6V_tN%5ql-|1;z4;2-^=1_}KCG_G9LZ>9ZmnUh5(4q?_EL$YY{)vW?fZ zZ~NSDEd?zOfhiis*Azv4rjuo!&C~wv5zRY5Aw8pxPUtzWN1UFwh(wiiHlS&3DH2W!#nTksluP8b#Y@^-pNp1HViY*khGz8Yy zF#8pcDn^?d-CSoMCfo7YWfZjd1y+1hj^z6eRn^3mCnT9~EfIV?-uCR$_>y>Q@pdE@>c}Yyx z|Mr2g&8KSEt9_eKzgFC@uqQVf0<{8b>&*5Fn^(OGn^U#(>Ls18K1WsO)g8Lbo~!06 z?76J_cJt=DJf8GgfbI!B-GaS{-%erkYh#5yUsdN<5AW`zv80(rufm`VY?@*aN~c#+ z41X}%3xzus_F%kSkfpI!rnR4 z5U3Tfk147zMz_)F)fKf*?89G-+T5}iqrXwui_!ZOyciWi>TQAmW-0z#@tC5W!!N^c z`>O756YM_}9F7q}!tE^@`&Sh9PUUsQ@VU~?c`Jq&D0Wfsf>j8KHY;#PDC|2P&neoO zL&I-t@1(C(9HdxAK}$?v3yt^diUGy&$&uIj8@25ZDVShFNTAs|?SmD+Qsgy$OSgj_~mOJd;<1Pe-A|&e>S0ZE?%pBZW$jZBeDo+Ninj`IQ z*A_d>440FhOTZRReOu9P?u)v;J!jh2q3k)77NEd->ho+xQ4^$_?b-+0hbg>%XQ=+s z>ILk{;FAh_=haRAi@NUx>emViT37>Rv+Jyda?o;{{;2(Oofm0sJ2h`T>8;KOq1@Wt;+kp zqBeQHpXM)j`aflNQ5S7tlkan>+P~M-*Uu?9!XN}s+cfp-w~A`cod18q3jHd`gKGg= z_2;|lJFDG2om?BLUuCbRYBgc9UT>@Yj#BXC<=5o0!=J|~TrR50I)@tF&c64`Vi~{Y zl=a3wyS4A=HOiUqm)Un-*H_Ry60iyNtit!n)2e?LX$%uRa!t7_%WLqLD_Z5u_v`J~ zKBynrk?AO?~&h0{DZX zQGP?w_9R|UPGPS1Zw+gdvL43`XGt0;e^=rm~| z+|Ry~Gpl6RRCo92xSwBEvlie@hQsx*eKs4yTUfZ_cOw1!+A#B6G=CM+No@{!tfc=8d zi;58DM7Mo>P~)_pqYSI{^bgnvv_Z9f)LZ+)O1Bvc?fyU3nC<%?wEPA18*!Bz6`{?E zZu>h=W8Nr#PPQlkTTXncqT3vWcK<~h^Ii&C&H{Gare6`-gvhhM_MH!&u=1T)+i7!u zPIz9~QtJKkB~5;7_JZnaMV^oSkhWz@Uak0#RnT$}SWDZuSrO7CD5|ga`)UW~V4C_S z>=pVkir7E&%Q_FgAq!SEbpNQ>L7T7@ZCTx8zTHi7@O*ju-0vQwr5A(03($T33*ExI zwDl(xF;ALK#&)hu_%dNzEFCdBC930TWs}fnXTQJH2F*I1=Z&i z9P(`Mam69gCJ&uebombyJo$8PaKHTQGgI1P0eg}AEvMxkYw9lqvlB}pUE`*iQYaZJX*0# zVl!(sjrUANOuwQ&Jh!gS+rozP6sf(qvhTTlNb!imm3_?nwHjcxAepwKM*0gy%t`fS zY>&zLvx?Y%U1oKS(O#X~53TxQA8S8elCizWSW6Les}{a{HON?V>C?Ebm$R+dj<)Tj zk^NHP6M8Cru@9o{J4?$cVrsU+%)Z?Hq9T=C`Z7){gl8+JDq?E(dFMfjc?w^wQ|iYJ za^G1IMVq2g*dLdgtMDd?7vD4PBSUKe{OMQ8M$c$}=PI^UETsss*^`z1CZYX_^j8$# z%&|oK@~Xytg~EQzcf2ChwvjsCui*Qmemth^Gw4q%teynbs!vdtsni4RJd;T;(RMbbx!T5(-T~Giv4F6cv`o-Nl|^Sx=*LAaCY3Mk@- z1@2P6?1{_fmVMUtfZ_#3QEiMiY|l&fTxIV_?Rjb!#UuqS4gp*6XLD+f!j5R!bJc(% zb5XP>n57lVHwimL(5tXXRE?E%nwFNp>l*HZ3Y%aS_bcob{qs%27E9RIF`CV*)pVT| zyXu^prqdKVAtcrw(RizK%KrbWqS>6XVrhrKn$0PDURqyKol_g?^oXwn-FW2cN7_@` z%o)>X>3Uk+0=DL%c783;`9+F`bIhKAGFMLBmWBWV2q1t!6ase0fQA492q1t!dICND z1ASD%^zzP#5kLR|1SCKKWEvoV00IakDL?^CBJJaro4Xfl0g^~HgG2y@KmqKCjXNQL00IbvEcY@uozdiiU0x#AmF(G1<>;Z(LDqZKmdVa z1So*TxUy9Q5I_I{&jl!eo+pUzA%Fk^2(&4%+iPE0q0O}b6vH-+xj6y|Ab>y|0u;bF z^2jIeAG0R#|0pge(y3g9UVpWKwS0Od^`_JjZe2*e{0Q8A1sDU1XG1Q0-=cmWDv z@y6@{0R#|0z(WBFpodAJO9&u<00PAePymZJW)BD;P^iE%n=N>lwE%?@C$MpfVFF2J zhzKBn00JWgD1akPxC8+N5I`US0SaIONoI%$Ab>dFG5J13<00q#EJ9|X{0R#|mEI#R0u;c=6U^`sKmY**8VgVW8@q6+zXEgDx%NiZ0{ELBdWJxd z0u;j_(?+KeKmY**x)q=Rb}PvJ5kLR|1cDTx00x;hI*kAV2q4g{00ppHLGF(L0tg^b zhQQ2cE}2r;S^$b+87X8R2q1s}0^SKw0KH2I9YO#B1P~}gfC5;CGW$RP0R#~6PJjaF zT}tQ>0tg_0Kp6rQz%rEC2LfFSocONe7ZtV^pzGeT0R%!9pcsapLHdsX0tg_`kpKm- zBR1}Y00Iag5V`;bF!T)4e*_Rf0D+DKD1aTYaVG>2KmdW@1vWfo{z=|f!5J5=If1nR z!Ot}YfB*srcrD<4G4xuG&LMyR0tf^vKmiOkb#xp71Q0-=8vzPnH+tL`0R#|0AXotk zV6ds9;|Rnp@ViaNpUGN)xO42&=qQFhWrMyTfB*srlqNs{ERC8SA%Fk^2>2vG0rV*w z^aTL~5I~?b0SaJg)a(cW1Q0;LCjknePuUpiOW)=v?#fz#ArDR=fB*u83Q!CSE1Q765fCA`iX6P3J2&5=*&CFX)V=aJ# z%u@^k2q1s}0uBTyfDX{vB?1T_fPi}e3ZVNO&;tYzKmY*; z0u(?8=z20u;bPSlK872q1uf?*bG+-!nx25I_I{1PT$L02ac^MiIy+u;($? zobAC{fNY)RRtQ8aKrxIsxeN^f1Q0-=xc~*QxeeDKfB*srL@Yo7j5xUr4FLoYK%lt* z1+cjd*C2oZ0tiGb;IaUI;IUUHuofWV$;HqRKmdVI1Y8!wP>kp&0tg_0K!gGmzzCDd zkPtus0R&nLPykySaSH?xKmdUV1t@?KCY2!};JLtRlP_7mQWXdDGRUDr4Sw h-J9Of5hnt-&D;H%S+mAWwVlu0eeW5U?|R6o{~ti$^#K3? diff --git a/sample_data/article/index.md b/sample_data/article/index.md index e45f07a..04e93ea 100644 --- a/sample_data/article/index.md +++ b/sample_data/article/index.md @@ -1,6 +1,171 @@ # Welcome to your new blog -## If you see this page, that means it's working +If you see this page, that means it's working -![thumbnail](./birthday-cake.png) +## Guide to Markdown formatting +### Headers + +# H1 +## H2 +### H3 +#### H4 +##### H5 +###### H6 + +Alternatively, for H1 and H2, an underline-ish style: + +Alt-H1 +====== + +Alt-H2 +------ + +### Emphasis + +Emphasis, aka italics, with *asterisks* or _underscores_. + +Strong emphasis, aka bold, with **asterisks** or __underscores__. + +Combined emphasis with **asterisks and _underscores_**. + +Strikethrough uses two tildes. ~~Scratch this.~~ + +### Lists + +1. First ordered list item +2. Another item +⋅⋅* Unordered sub-list. +1. Actual numbers don't matter, just that it's a number +⋅⋅1. Ordered sub-list +4. And another item. + +⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + +⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ +⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅ +⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) + +* Unordered list can use asterisks +- Or minuses ++ Or pluses + +### Links + +[I'm an inline-style link](https://www.google.com) + +[I'm an inline-style link with title](https://www.google.com "Google's Homepage") + +[I'm a reference-style link][Arbitrary case-insensitive reference text] + +[I'm a relative reference to a repository file](../blob/master/LICENSE) + +[You can use numbers for reference-style link definitions][1] + +Or leave it empty and use the [link text itself]. + +URLs and URLs in angle brackets will automatically get turned into links. +http://www.example.com or and sometimes +example.com (but not on Github, for example). + +Some text to show that the reference links can follow later. + +[arbitrary case-insensitive reference text]: https://www.mozilla.org +[1]: http://slashdot.org +[link text itself]: http://www.reddit.com + +### Images + +Here's our logo (hover to see the title text): + +Inline-style: +![alt text](https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 1") + +Reference-style: +![alt text][logo] + +[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2" + + +### Code and Syntax Highlighting + +Inline `code` has `back-ticks around` it. + +```javascript +var s = "JavaScript syntax highlighting"; +alert(s); +``` + +```python +s = "Python syntax highlighting" +print s +``` + +``` +No language indicated, so no syntax highlighting. +But let's throw in a tag. +``` + + +### Tables + +Colons can be used to align columns. + +| Tables | Are | Cool | +| ------------- |:-------------:| -----:| +| col 3 is | right-aligned | $1600 | +| col 2 is | centered | $12 | +| zebra stripes | are neat | $1 | + +There must be at least 3 dashes separating each header cell. +The outer pipes (|) are optional, and you don't need to make the +raw Markdown line up prettily. You can also use inline Markdown. + +Markdown | Less | Pretty +--- | --- | --- +*Still* | `renders` | **nicely** +1 | 2 | 3 + +### Blockquotes + +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +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 + +
+
Definition list
+
Is something people use sometimes.
+ +
Markdown in HTML
+
Does *not* work **very** well. Use HTML tags.
+
+ +### Horizontal Rule + +Three or more... + +--- + +Hyphens + +*** + +Asterisks + +___ + +Underscores + +### Line Breaks + +Here's a line for us to start with. + +This line is separated from the one above by two newlines, so it will be a *separate paragraph*. + +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*. diff --git a/sample_data/home/error.ejs b/sample_data/home/error.ejs index e4b1427..3d343ba 100644 --- a/sample_data/home/error.ejs +++ b/sample_data/home/error.ejs @@ -3,10 +3,16 @@ Error <%= error %> +
-

Error <%= error %> at path <%= path %>

+

Somehing went wrong + (Error <%= error %>) +

+ It means the resource you're trying to access is unavailable right now.
+ We're terribly sorry that you encountered this error.

+
Back to home
\ No newline at end of file diff --git a/sample_data/home/prism.css b/sample_data/home/prism.css new file mode 100644 index 0000000..80fb936 --- /dev/null +++ b/sample_data/home/prism.css @@ -0,0 +1,143 @@ +/* PrismJS 1.16.0 +https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+apacheconf+apl+applescript+c+arff+asciidoc+asm6502+csharp+autohotkey+autoit+bash+basic+batch+bison+bnf+brainfuck+bro+cpp+aspnet+arduino+cil+coffeescript+cmake+clojure+ruby+csp+css-extras+d+dart+diff+markup-templating+docker+ebnf+eiffel+ejs+elixir+elm+erb+erlang+fsharp+flow+fortran+gcode+gedcom+gherkin+git+glsl+gml+go+graphql+groovy+less+handlebars+haskell+haxe+hcl+http+hpkp+hsts+ichigojam+icon+inform7+ini+io+j+java+scala+php+javastacktrace+jolie+jq+javadoclike+n4js+json+jsonp+json5+julia+keyman+kotlin+latex+markdown+liquid+lisp+livescript+lolcode+lua+makefile+crystal+django+matlab+mel+mizar+monkey+n1ql+typescript+nand2tetris-hdl+nasm+nginx+nim+nix+nsis+objectivec+ocaml+opencl+oz+parigp+parser+pascal+perl+jsdoc+phpdoc+php-extras+sql+powershell+processing+prolog+properties+protobuf+scss+puppet+pure+python+q+qore+r+js-extras+jsx+renpy+reason+vala+rest+rip+roboconf+textile+rust+sas+sass+stylus+javadoc+scheme+shell-session+smalltalk+smarty+plsql+soy+twig+swift+yaml+tcl+haml+toml+tt2+pug+tsx+t4-templating+visual-basic+t4-cs+regex+vbnet+velocity+verilog+vhdl+vim+t4-vb+wasm+wiki+xeora+xojo+xquery+tap */ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, pre[class*="language-"] ::selection, +code[class*="language-"]::selection, code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #9a6e3a; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function, +.token.class-name { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + diff --git a/sample_data/home/style.css b/sample_data/home/style.css index 6d915f0..396d21f 100644 --- a/sample_data/home/style.css +++ b/sample_data/home/style.css @@ -1,18 +1,144 @@ +body, html { + padding: 0; + margin: 0; +} + +* { + box-sizing: border-box; +} + +body { + font: 14px/1.45 -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; + color: #111; + -webkit-text-size-adjust: none; + + background-color: #F5F5F5; + + height: 100vh; +} + main { max-width: 70ch; padding: 2ch; margin: auto; + + background-color: #F0F0F0; + + min-height: 100%; } -.article a, .article a:visited { - color: black; +q:before { + content: open-quote; } -.article a:visited { - color: black; +q:after { + content: close-quote; } -.article img { +hr { + background: #e1e4e8; + border: 0; + height: 0.25em; + margin: 1em 0; +} + +a { + color: #3C3CA1; +} + +a:hover { + color: #8484C6; +} + +pre, code { + font-size: 96%; + background: #f8f8f8; +} + +pre { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + padding: 10px 16px; +} + +table td { + vertical-align: baseline; + padding-left: 8px; +} + +table td:first-of-type { + padding-left: 0; +} + +div.header span.time span { + color: #888; + font-family: serif; + font-style: italic; +} + +main.article div.header a.link-home { + text-decoration: none; + float: right; + line-height: 2.4; +} + +main.article div.header h1, main.article div.header h2 { + margin-top: 0.85em; + margin-bottom: 0.25em; + font-size: 1.5em; +} + +main.article div.header h1 a, main.article div.header h2 a { + text-decoration: none; +} + +main.article div.header span.time { + display: block; +} + +#text h1:first-child { + display: none; +} + +#text { + text-align: justify; + hyphens: auto; +} + +#text li, #text table, #text blockquote { + text-align: left; +} + +#text img { max-width: 100%; - max-height: 10vh; + height: auto; +} + +.note { + padding: 1em; + background: #ff02; +} + +.note > p { + margin: 0.6rem 0; +} + +.important { + padding: 1em; + background: #eff5ff; +} + +.important > p { + margin: 0.6rem 0; +} + +/* Sidenotes */ + +#text .side > p:nth-child(2n+0) { + font-style: italic; + color: #555; +} + +#text .side > p:nth-child(2n+0) > i, #text .side > p:nth-child(2n+0) > em { + font-style: normal; } \ No newline at end of file diff --git a/sample_data/home/template.ejs b/sample_data/home/template.ejs index 6cd69bf..064c1a1 100644 --- a/sample_data/home/template.ejs +++ b/sample_data/home/template.ejs @@ -2,12 +2,20 @@ - GitBlog.md - Home - + GitBlog.md - <%= article.title %> + + -
- <%- article.content %> +
+
+ +

<%= article.title %>

+ Published on <%= article.year + '-' + article.month + '-' + article.day %> +
+
<%- article.content %>
+
+ Go to top - Back to home
\ No newline at end of file From 3fc01cadb89d6e0a33e925808fc10ed74c9591e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 11:44:39 +0200 Subject: [PATCH 37/42] Pages includes footer --- sample_data/home/error.ejs | 1 + sample_data/home/footer.ejs | 5 +++++ sample_data/home/index.ejs | 1 + sample_data/home/template.ejs | 3 ++- 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 sample_data/home/footer.ejs diff --git a/sample_data/home/error.ejs b/sample_data/home/error.ejs index 3d343ba..9593025 100644 --- a/sample_data/home/error.ejs +++ b/sample_data/home/error.ejs @@ -13,6 +13,7 @@ It means the resource you're trying to access is unavailable right now.
We're terribly sorry that you encountered this error.

Back to home + <%- include('footer'); %>
\ No newline at end of file diff --git a/sample_data/home/footer.ejs b/sample_data/home/footer.ejs new file mode 100644 index 0000000..42edf34 --- /dev/null +++ b/sample_data/home/footer.ejs @@ -0,0 +1,5 @@ +
+
+ @<%= new Date().getFullYear() %> - Made with GitBlog.md + +
\ No newline at end of file diff --git a/sample_data/home/index.ejs b/sample_data/home/index.ejs index 680f203..df1c851 100644 --- a/sample_data/home/index.ejs +++ b/sample_data/home/index.ejs @@ -21,6 +21,7 @@ <% } %> <% }); %> + <%- include('footer'); %> \ No newline at end of file diff --git a/sample_data/home/template.ejs b/sample_data/home/template.ejs index 064c1a1..6acc674 100644 --- a/sample_data/home/template.ejs +++ b/sample_data/home/template.ejs @@ -14,8 +14,9 @@ Published on <%= article.year + '-' + article.month + '-' + article.day %>
<%- article.content %>
-
+
Go to top - Back to home + <%- include('footer'); %> \ No newline at end of file From 5e7b700304f26015cadc53639760550d706b967e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 14:45:49 +0200 Subject: [PATCH 38/42] Style update --- sample_data/home/index.ejs | 6 ++---- sample_data/home/style.css | 17 +++++------------ 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/sample_data/home/index.ejs b/sample_data/home/index.ejs index df1c851..4c48df3 100644 --- a/sample_data/home/index.ejs +++ b/sample_data/home/index.ejs @@ -12,10 +12,8 @@

Articles in this blog :

<% articles.forEach((article) => { %>
-

<%- `${article.title}` %> ( - <%= `${article.day}/${article.month}/${article.year}` %> - ) -

+

<%- `${article.title}` %>

+ Published on <%= article.year + '-' + article.month + '-' + article.day %> <% if(article.thumbnail){ %> <%- `thumbnail` %> <% } %> diff --git a/sample_data/home/style.css b/sample_data/home/style.css index 396d21f..f9f689f 100644 --- a/sample_data/home/style.css +++ b/sample_data/home/style.css @@ -18,7 +18,7 @@ body { } main { - max-width: 70ch; + max-width: 75ch; padding: 2ch; margin: auto; @@ -45,7 +45,6 @@ hr { a { color: #3C3CA1; } - a:hover { color: #8484C6; } @@ -65,12 +64,11 @@ table td { vertical-align: baseline; padding-left: 8px; } - table td:first-of-type { padding-left: 0; } -div.header span.time span { +main.article div.header span.time span, div.article span.time span { color: #888; font-family: serif; font-style: italic; @@ -82,17 +80,17 @@ main.article div.header a.link-home { line-height: 2.4; } -main.article div.header h1, main.article div.header h2 { +main.article div.header h1, main.article div.header h2, div.article h3 { margin-top: 0.85em; margin-bottom: 0.25em; font-size: 1.5em; } -main.article div.header h1 a, main.article div.header h2 a { +main.article div.header h1 a, main.article div.header h2 a, div.article h3 a { text-decoration: none; } -main.article div.header span.time { +main.article div.header span.time, div.article span.time { display: block; } @@ -104,11 +102,9 @@ main.article div.header span.time { text-align: justify; hyphens: auto; } - #text li, #text table, #text blockquote { text-align: left; } - #text img { max-width: 100%; height: auto; @@ -118,7 +114,6 @@ main.article div.header span.time { padding: 1em; background: #ff02; } - .note > p { margin: 0.6rem 0; } @@ -127,7 +122,6 @@ main.article div.header span.time { padding: 1em; background: #eff5ff; } - .important > p { margin: 0.6rem 0; } @@ -138,7 +132,6 @@ main.article div.header span.time { font-style: italic; color: #555; } - #text .side > p:nth-child(2n+0) > i, #text .side > p:nth-child(2n+0) > em { font-style: normal; } \ No newline at end of file From fd7b24c08ea90135846051a2d9a4a638fe87ed57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 15:06:23 +0200 Subject: [PATCH 39/42] Style update --- sample_data/home/style.css | 71 +++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/sample_data/home/style.css b/sample_data/home/style.css index f9f689f..f01d8a3 100644 --- a/sample_data/home/style.css +++ b/sample_data/home/style.css @@ -11,9 +11,7 @@ body { font: 14px/1.45 -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; color: #111; -webkit-text-size-adjust: none; - background-color: #F5F5F5; - height: 100vh; } @@ -21,18 +19,13 @@ main { max-width: 75ch; padding: 2ch; margin: auto; - background-color: #F0F0F0; - - min-height: 100%; + min-height: 100vh; } -q:before { - content: open-quote; -} - -q:after { - content: close-quote; +/* hide redundant text in article */ +#text h1:first-child { + display: none; } hr { @@ -45,6 +38,7 @@ hr { a { color: #3C3CA1; } + a:hover { color: #8484C6; } @@ -60,14 +54,31 @@ pre { padding: 10px 16px; } +blockquote { + border-left: 0.5em solid #ccc; + padding-left: 1em; + margin: 0.25em 0; + color: #333; +} + +blockquote > p { + margin: 0.6rem 0; +} + table td { vertical-align: baseline; padding-left: 8px; } + table td:first-of-type { padding-left: 0; } +#text table td, #text table th { + border: 1px solid #ccc; + padding: 0.25em 0.5em; +} + main.article div.header span.time span, div.article span.time span { color: #888; font-family: serif; @@ -83,7 +94,7 @@ main.article div.header a.link-home { main.article div.header h1, main.article div.header h2, div.article h3 { margin-top: 0.85em; margin-bottom: 0.25em; - font-size: 1.5em; + font-size: 2em; } main.article div.header h1 a, main.article div.header h2 a, div.article h3 a { @@ -94,44 +105,24 @@ main.article div.header span.time, div.article span.time { display: block; } -#text h1:first-child { - display: none; +div.article { + margin-left: 1em; +} + +div.article h3 { + font-size: 1.3em; } #text { text-align: justify; hyphens: auto; } + #text li, #text table, #text blockquote { text-align: left; } + #text img { max-width: 100%; height: auto; -} - -.note { - padding: 1em; - background: #ff02; -} -.note > p { - margin: 0.6rem 0; -} - -.important { - padding: 1em; - background: #eff5ff; -} -.important > p { - margin: 0.6rem 0; -} - -/* Sidenotes */ - -#text .side > p:nth-child(2n+0) { - font-style: italic; - color: #555; -} -#text .side > p:nth-child(2n+0) > i, #text .side > p:nth-child(2n+0) > em { - font-style: normal; } \ No newline at end of file From 24841d02f19fce888a511b6e66a1250bcb51229b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 15:18:33 +0200 Subject: [PATCH 40/42] Style update --- sample_data/article/index.md | 12 ++++++------ sample_data/home/style.css | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/sample_data/article/index.md b/sample_data/article/index.md index 04e93ea..971b3d2 100644 --- a/sample_data/article/index.md +++ b/sample_data/article/index.md @@ -35,16 +35,16 @@ Strikethrough uses two tildes. ~~Scratch this.~~ 1. First ordered list item 2. Another item -⋅⋅* Unordered sub-list. + * Unordered sub-list. 1. Actual numbers don't matter, just that it's a number -⋅⋅1. Ordered sub-list + 1. Ordered sub-list 4. And another item. -⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). + You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown). -⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ -⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅ -⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) + To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅ + Note that this line is separate, but within the same paragraph.⋅⋅ + (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.) * Unordered list can use asterisks - Or minuses diff --git a/sample_data/home/style.css b/sample_data/home/style.css index f01d8a3..fb4ca4e 100644 --- a/sample_data/home/style.css +++ b/sample_data/home/style.css @@ -79,6 +79,23 @@ table td:first-of-type { padding: 0.25em 0.5em; } +details { + background: #f8f8f8; + margin: 0.25em 0; + padding: 0; +} + +details > summary { + cursor: pointer; + padding: 0.5em 1em; +} + +details > p { + background: #f5f5f5; + padding: 0.5em 0.5em 0.5em 2em; + margin: 0; +} + main.article div.header span.time span, div.article span.time span { color: #888; font-family: serif; From 682a237323dd7baf26d256c10d984efa02e6d6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 16:08:47 +0200 Subject: [PATCH 41/42] updated doc --- README.md | 181 ++++++++++++++++++++++++++++++++++++---- src/config.default.json | 2 - 2 files changed, 167 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3d30a55..52af428 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,98 @@ -# GitBlog.md (WIP) -> This is a work in progress, some information written here might not be true yet. +# GitBlog.md [![Build Status](https://img.shields.io/travis/Klemek/GitBlog.md.svg?branch=master)](https://travis-ci.org/Klemek/GitBlog.md) [![Coverage Status](https://img.shields.io/coveralls/github/Klemek/GitBlog.md.svg?branch=master)](https://coveralls.io/github/Klemek/GitBlog.md?branch=master) A static blog using Markdown pulled from your git repository. -## Flow +* **[How it works](#how-it-works)** +* **[Installation](#installation)** +* **[Writing an article](#writing-an-article)** +* **[Configuration](#configuration)** + +## How it works +[back to top](#gitblog-md) + +There are 4 majors features of this project : + +#### 1. Home page + +
+diagram (click) +

![root](./uml/root.png) +

+
+ +When you access the root url of your blog, the app will fetch the template and inject the list of currently available articles. + + +#### 2. Article page + +
+diagram (click) +

![article](./uml/article.png) +

+
+ +As you access an article link, the server will fetch it's `index.md` Markdown file and render it in plain HTML using Showdown. + + +#### 3. Git webhook + +
+diagram (click) +

![webhook](./uml/webhook.png) +

+
+ +As you configured your data repository, when you push any data, it will trigger the webhook that will perform a `git pull` then refresh the data you just committed. + + +#### 4. RSS feed + +
+diagram (click) +

![rss](./uml/rss.png) +

+
+ +On the `/rss` endpoint, the servers gives you a RSS feed based on the list of articles which you can bookmark. + + ## Installation -**1. Download and install the latest version from the repo** +[back to top](#gitblog-md) + +#### 1. Download and install the latest version from the repo ```bash git clone https://github.com/klemek/gitblog.md.git npm install ``` -**2. Create your config file** +#### 2. Create your config file ```bash cd gitblog.md cp config.example.json config.json ``` then edit the config.json file with your custom values. +For example, you might want to change the app's port with : -**3. Start your server** +```json +{ + "node_port": 3030 +} +``` + +See [Configuration](#configuration) for more info. + +#### 3. Start your server ```bash npm run @@ -37,9 +100,18 @@ npm run node src/server.js ``` +You can check that it's up and running at [http://localhost:3000/](http://localhost:3000/) + You might want to use something like screen to separate the process from your current terminal session. -**4. Create and init your git source** +#### 4. Customize the blog's style + +At `npm install` a first article will be created for the current date. +You see it as an example of rendering of your blog. +Use it to edit your templates and styles located on the `data` folder. +At first, home page and articles are rendered using EJS engine but you can customize that into the configuration. + +#### 5. Create and init your git source You need to [create a new repository](https://github.com/new) on your favorite Git service. @@ -50,7 +122,9 @@ git remote add origin git push -u origin master ``` -**5. Refresh content with a webhook (optional)** +Now you just have to edit a local copy of your articles and, when you push them, to perform a simple `git pull` on that data folder. + +#### 6. Refresh content with a webhook (optional) Create a webhook on your git source (On GitHub, in the `Settings/Webhooks` part of the repository.) with the following parameters : @@ -58,23 +132,102 @@ Create a webhook on your git source (On GitHub, in the `Settings/Webhooks` part * Content type : `application/json` * Events : Just the push event -**6. Securize your webhook (optional)** +Now the server will perform the `git pull` task for you after a successful push on GitHub. + +#### 7. Securize your webhook (optional) Here are the steps for Github, if you use another platform adapt it your way (header format on the config) : * Create a password or random secret * Edit your configuration to add webhook info ```json -{ -... "webhook": { "endpoint": "/webhook", "secret": "sha1=", "signature_header": "X-Hub-Signature" }, -... -} ``` * Launch the server * Update your webhook on github to include the secret -* Check if Github successfully reached the endpoint \ No newline at end of file +* Check if Github successfully reached the endpoint + +## Writing an article +[back to top](#gitblog-md) + +TODO + +## Configuration +[back to top](#gitblog-md) + +* `node_port` (default: 3000) + + the port the server is listening to +* `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 +* `modules` + * `rss` (default: true) + + activate the RSS endpoint and its features + * `webhook` (default: true) + + activate the webhook endpoint and its features + * `prism` (default: true) + + activate Prism code highlighting +* `home` + * `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 +* `article` + * `index` (default: index.md) + + the name of the Markdown page of the article on the `/year/month/day/` directory + * `template` (default: template.ejs) + + the name of the article page template on the data directory + * `thumbnail_tag`: (default: thumbnail) + + the alt text searched to get the thumbnail image on the article + + as in `![]()` + * `default_title`: (default: Untitled) + + the title of the article in case a level 1 title was not found + * `default_thumbnail`: (default: none) + + the path of the default thumbnail to get from the data directory +* `rss` + * `title`: (default: mygitblog RSS feed) + * `description`: (default: a generated RSS feed from my articles) + * `endpoint`: (default: /rss) + * `length`: (default: 10) + + how many last articles will be present in the feed +* `webhook` + * `endpoint`: (default: /webhook) + * `secret`: (default: none) + + see [above](#7-securize-your-webhook-optional-) + * `signature_header`: (default: none) + + see [above](#7-securize-your-webhook-optional-) + * `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) \ No newline at end of file diff --git a/src/config.default.json b/src/config.default.json index 6c9827d..0ab7f55 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -2,9 +2,7 @@ "node_port": 3000, "data_dir": "data", "view_engine": "ejs", - "language": "en-us", "modules": { - "plantuml": false, "rss": true, "webhook": true, "prism": true From 217c6069425ff801f8bee96137cfffa1fb15097a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20GOUIN?= Date: Fri, 21 Jun 2019 16:24:38 +0200 Subject: [PATCH 42/42] updated doc --- README.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 52af428..da633f3 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,17 @@ You might want to use something like screen to separate the process from your cu #### 4. Customize the blog's style At `npm install` a first article will be created for the current date. -You see it as an example of rendering of your blog. +You can see it as an example of rendering of your blog. Use it to edit your templates and styles located on the `data` folder. + At first, home page and articles are rendered using EJS engine but you can customize that into the configuration. +Resources are located on the `data` folder and can be referenced as the root of your blog. + +``` +/styles/main.css => data/styles/main.css +``` + #### 5. Create and init your git source You need to [create a new repository](https://github.com/new) on your favorite Git service. @@ -154,7 +161,30 @@ Here are the steps for Github, if you use another platform adapt it your way (he ## Writing an article [back to top](#gitblog-md) -TODO +You need to write your article (and templates) on the git repository but **keep the data directory on the server untouched** to prevent any changes to harm the git pull normal behavior. + +To be referenced, an article need to be on a specific path containing its date and have a Markdown index file : + +``` +data/year/month/day/index.md +``` + +> note that month and day need to be 0 padded (`5th of june 2019 => 2019/06/05`) + +On your Markdown file you can write anything but some informations will be fetched automatically : + +* Title : first level 1 header (#) +* Thumbnail : first thumbnail tagged image (like `![thumbnail](url)`) + +On that same folder, you can place resources like images and reference them in relative paths : + +``` +![](./image.png) => data/year/month/day/image.png +``` + +> note that you cannot place resources on subfolders + +Any URL like `/year/month/day/anything/` will redirect to this article (and link to correct resources) ## Configuration [back to top](#gitblog-md)