Compare commits

...

58 Commits

Author SHA1 Message Date
Klemek 32f9ea74f1 Merge pull request #27 from Klemek/dev
GitBlog v1.2.8
2019-09-19 20:38:38 +02:00
Klemek 9d1fb69fee nvm 2019-09-19 20:37:40 +02:00
Klemek 24a02d862f fml2 2019-09-19 20:30:35 +02:00
Klemek cfe9965d03 fml 2019-09-19 20:28:20 +02:00
Klemek d260b9d2f8 Merge pull request #26 from Klemek/dev
GitBlog v1.2.8
2019-09-19 20:13:36 +02:00
Klemek 9ec55b3c01 dependencies fix ffs 2019-09-19 20:12:49 +02:00
Klemek c693d96339 Merge pull request #25 from Klemek/dev
GitBlog v1.2.8
2019-09-19 20:09:57 +02:00
Klemek 8bb6b6db66 Merge branch 'master' into dev 2019-09-19 20:07:59 +02:00
Klemek 849cdf2a19 dependencies fix FINALLY 2019-09-19 20:05:10 +02:00
Klemek d0d3f94049 wtf2 2019-09-19 20:02:23 +02:00
Klemek 613e663c13 wtf 2019-09-19 20:01:32 +02:00
Klemek bd10871da9 Merge branch 'dev' 2019-09-19 19:52:55 +02:00
Klemek 671a4314d7 Merge branch 'master' into dev 2019-09-19 19:51:04 +02:00
Klemek 1cee7094f3 Delete package-lock.json 2019-09-19 19:47:33 +02:00
Klemek 2284d46bb5 Merge remote-tracking branch 'origin/dev' into dev 2019-09-19 19:46:19 +02:00
Klemek 7245876b07 dependencies fix 2019-09-19 19:46:09 +02:00
Klemek 7a35aec7ad Merge pull request #23 from Klemek/dependabot/npm_and_yarn/mixin-deep-1.3.2
Bump mixin-deep from 1.3.1 to 1.3.2
2019-09-19 19:27:34 +02:00
dependabot[bot] 870701b6c6 Bump mixin-deep from 1.3.1 to 1.3.2
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-19 17:23:38 +00:00
Klemek 3be86dec58 Merge pull request #22 from Klemek/dev
GitBlog.md v1.2.8
2019-09-19 19:22:16 +02:00
Klemek 48d9535007 Merge branch 'master' into dev 2019-09-19 19:16:59 +02:00
Klemek ae2eb52cf8 [skip CI]updated README.md 2019-09-19 19:16:31 +02:00
Klemek 7e9e1e19fa changed version number 2019-09-19 19:14:16 +02:00
Klemek c9ef93088b express rate limit 2019-09-19 19:13:41 +02:00
Klemek 99e4bb5c4d [skip CI] lgtm config 2019-09-19 19:03:49 +02:00
Klemek dd5af2b865 [skip CI] update README.md 2019-09-19 09:26:17 +02:00
Klemek 4671253147 Merge pull request #21 from Klemek/dev
v1.2.7
2019-08-19 14:34:32 +02:00
Klemek add01b28fe Update package.json 2019-08-19 14:25:01 +02:00
Klemek a27a53e238 faDiagram switch to TOML lang 2019-08-19 14:14:52 +02:00
Klemek 6aff9b4d93 Merge remote-tracking branch 'origin/dev' into dev 2019-08-19 14:14:05 +02:00
Klemek c9f57233a4 faDiagram switch to TOML lang 2019-08-19 14:13:54 +02:00
Klemek 7d72e94aa3 Merge pull request #20 from Klemek/dev
updated package-lock
2019-07-19 11:00:33 +02:00
Klemek 3d6a0b4306 Merge branch 'master' into dev 2019-07-19 10:58:52 +02:00
Klemek babc533efc updated package-lock 2019-07-19 10:56:26 +02:00
Klemek 36908134e6 Merge pull request #19 from Klemek/dependabot/npm_and_yarn/lodash-4.17.15
Bump lodash from 4.17.11 to 4.17.15
2019-07-19 10:50:53 +02:00
dependabot[bot] d426b41368 Bump lodash from 4.17.11 to 4.17.15
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-19 08:48:39 +00:00
Klemek 1836a414eb Merge pull request #18 from Klemek/dev
v1.2.6
2019-07-19 10:46:33 +02:00
Klemek ca49a29dd9 [skip CI] updated version 2019-07-19 10:44:58 +02:00
Klemek 02a768a6af [skip CI] updated README.md 2019-07-19 10:44:26 +02:00
Klemek bbc4d7c270 [skip CI] updated README.md 2019-07-19 10:42:25 +02:00
Klemek bfa1521f85 updated sample to show fa-diagrams 2019-07-19 10:41:21 +02:00
Klemek a05d380fcf fixed parts detection 2019-07-19 10:41:01 +02:00
Klemek 4a32995ca1 fa-diagrams support 2019-07-19 10:19:12 +02:00
Klemek 53e1fe7201 mathjax/plantuml rendering only outside of code/scripts 2019-07-19 10:01:01 +02:00
Klemek 2e8ff1be92 fixed mathjax in code 2019-07-18 16:41:25 +02:00
Klemek e14f9fc4af Merge remote-tracking branch 'origin/dev' into dev 2019-07-18 14:07:34 +02:00
Klemek 896f302bcf updated default template 2019-07-18 14:07:24 +02:00
Klemek cc0bd1cf49 Merge pull request #17 from Klemek/dev
updated CI
2019-07-12 14:07:36 +02:00
Klemek 7a1d9cbbd6 Merge branch 'master' into dev 2019-07-12 13:59:54 +02:00
Clément GOUIN 34e8d4cb6f updated CI 2019-07-12 13:57:27 +02:00
Klemek 4a9b70ac68 Update .travis.yml 2019-07-12 13:53:55 +02:00
Klemek 889258c874 Update .travis.yml 2019-07-12 11:53:02 +02:00
Klemek de26feb05c Merge pull request #16 from Klemek/dev
v1.2.5
2019-07-01 23:17:56 +02:00
Klemek 8bb455b576 Fixed draft rendering bug 2019-07-01 23:15:13 +02:00
Klemek 378ed438b6 Merge pull request #15 from Klemek/dev
v1.2.4
2019-07-01 23:03:23 +02:00
Klemek 3b07b6b9c5 Drafted articles 2019-07-01 22:18:40 +02:00
Klemek b6afcd4992 Update README.md 2019-06-26 21:20:45 +02:00
Klemek 90c343c752 Merge remote-tracking branch 'origin/master' 2019-06-19 18:41:23 +02:00
Klemek ff7542af70 updated uml 2019-06-18 20:14:37 +02:00
18 changed files with 1663 additions and 5101 deletions
+2 -1
View File
@@ -11,5 +11,6 @@ install:
before_script: before_script:
- npm install -g jshint - npm install -g jshint
script: script:
- jest --silent --coverage --coverageReporters=text-lcov | coveralls - jest --coverage --silent
- jshint ./src - jshint ./src
- cat ./coverage/lcov.info | coveralls
+21 -5
View File
@@ -1,8 +1,9 @@
# GitBlog.md
[![Build Status](https://img.shields.io/travis/Klemek/GitBlog.md.svg?branch=master)](https://travis-ci.org/Klemek/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) [![Coverage Status](https://img.shields.io/coveralls/github/Klemek/GitBlog.md.svg?branch=master)](https://coveralls.io/github/Klemek/GitBlog.md?branch=master)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/Klemek/GitBlog.md.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/GitBlog.md/context:javascript)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/Klemek/GitBlog.md.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Klemek/GitBlog.md/alerts/)
# GitBlog.md
A static blog using Markdown pulled from your git repository. A static blog using Markdown pulled from your git repository.
@@ -127,6 +128,10 @@ Resources are located on the `data` folder and can be referenced as the root of
In your template, the following data is sent : In your template, the following data is sent :
<details>
<summary>details (click)</summary>
<p>
* `info` (every pages) * `info` (every pages)
* `title` : the blog's title as in the config * `title` : the blog's title as in the config
* `description` the blog's description as in the config * `description` the blog's description as in the config
@@ -146,6 +151,8 @@ In your template, the following data is sent :
* `realPath` : the system's path for the folder * `realPath` : the system's path for the folder
* `escapedTitle` : the code with alphanumeric and underscore characters only * `escapedTitle` : the code with alphanumeric and underscore characters only
* `error` (error pages only) : the error code * `error` (error pages only) : the error code
</p>
</details>
#### 5. Create and init your git source #### 5. Create and init your git source
@@ -239,6 +246,9 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
It allows you to add math equations to your articles by simply writing LaTeX between `$$` for full size (and between $ for inline) (more info [here](https://www.mathjax.org/)) It allows you to add math equations to your articles by simply writing LaTeX between `$$` for full size (and between $ for inline) (more info [here](https://www.mathjax.org/))
* **PlantUML** * **PlantUML**
It allows you to add UML diagrams with PlantUML Syntax between `@startuml` and `@enduml` (more info [here](http://www.plantuml.com)) It allows you to add UML diagrams with PlantUML Syntax between `@startuml` and `@enduml` (more info [here](http://www.plantuml.com))
* **fa-diagrams**
It allows you to define SVG diagrams with Font-Awesome icons in [TOML](https://github.com/toml-lang/toml) between `@startfad` and `@endfad` (more info [here](https://github.com/Klemek/fa-diagrams))
## Configuration ## Configuration
[back to top](#gitblog-md) [back to top](#gitblog-md)
@@ -252,6 +262,8 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
the directory where will be located the git repo with templates and articles the directory where will be located the git repo with templates and articles
* `view_engine` (default: ejs) * `view_engine` (default: ejs)
the Express view engine used to render pages from templates the Express view engine used to render pages from templates
* `rate_limit` (default: 100)
number of requests allowed in a time-frame of 15 minutes
* `access_log` (default: access.log) * `access_log` (default: access.log)
log file where to save access requests (empty to disable) log file where to save access requests (empty to disable)
* `error_log` (default: error.log) * `error_log` (default: error.log)
@@ -267,6 +279,8 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
activate MathJax equations formatting activate MathJax equations formatting
* `plantuml` (default: true) * `plantuml` (default: true)
activate PlantUML diagram rendering activate PlantUML diagram rendering
* `fa-diagrams` (default: true)
activate fa-diagrams rendering
* `home` * `home`
* `title` (default: GitBlog.md) * `title` (default: GitBlog.md)
the title of your blog, **strongly advised to be changed** the title of your blog, **strongly advised to be changed**
@@ -280,11 +294,13 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
* `error` (default: error.ejs) * `error` (default: error.ejs)
the name of the error page template on the data directory the name of the error page template on the data directory
it will receive `error`, the error code it will receive `error`, the error code
* `hidden` (default: `[.ejs]`) * `hidden` (default: `[*.ejs,/.git*]`)
file extensions to be returned 404 when reached path matches to be returned 404 when reached
* `article` * `article`
* `index` (default: index.md) * `index` (default: index.md)
the name of the Markdown page of the article on the `/year/month/day/` directory the name of the Markdown page of the article on the `/year/month/day/` directory
* `draft` (default: draft.md)
the name of the Markdown page of an article not shown on the list
* `template` (default: template.ejs) * `template` (default: template.ejs)
the name of the article page template on the data directory the name of the article page template on the data directory
* `thumbnail_tag`: (default: thumbnail) * `thumbnail_tag`: (default: thumbnail)
+7
View File
@@ -0,0 +1,7 @@
path_classifiers:
test:
- test
docs:
- uml
library:
- src/lib
+1240 -5038
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,13 +1,15 @@
{ {
"name": "gitblog.md", "name": "gitblog.md",
"version": "1.2.3", "version": "1.2.8",
"description": "A static blog using Markdown pulled from your git repository.", "description": "A static blog using Markdown pulled from your git repository.",
"main": "src/server.js", "main": "src/server.js",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.3",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"crypto": "^1.0.1",
"ejs": "^2.6.2", "ejs": "^2.6.2",
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^5.0.0",
"fa-diagrams": "^1.0.3",
"mathjax-node": "^2.1.1", "mathjax-node": "^2.1.1",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"node-prismjs": "^0.1.2", "node-prismjs": "^0.1.2",
@@ -16,8 +18,6 @@
"showdown": "^1.9.0" "showdown": "^1.9.0"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"coveralls": "^3.0.4", "coveralls": "^3.0.4",
"jest": "^24.8.0", "jest": "^24.8.0",
"superagent": "^5.1.0", "superagent": "^5.1.0",
+29
View File
@@ -19,6 +19,7 @@ If you see this page, that means it's working
* [Spoilers](#spoilers) * [Spoilers](#spoilers)
* [Math Equations](#mathequations) * [Math Equations](#mathequations)
* [UML](#uml) * [UML](#uml)
* [Diagrams](#diagrams)
* [Youtube Videos](#youtubevideos) * [Youtube Videos](#youtubevideos)
### Headers ### Headers
@@ -253,6 +254,34 @@ showdown -left-> express : 4. html
express -up-> web : 5. html express -up-> web : 5. html
@enduml @enduml
### Diagrams
[Back to top](#top)
You can use [fa-diagrams](https://github.com/Klemek/fa-diagrams) with `@startfad` and `@endfad` tags and using [TOML](https://github.com/toml-lang/toml) inside
@startfad
[[nodes]]
name = "node1"
icon = "laptop-code"
color = "#4E342E"
bottom = "my app"
[[nodes]]
name = "node2"
icon = "globe"
color = "#455A64"
bottom = "world"
[[links]]
from = "node1"
to = "node2"
color = "#333333"
bottom = '"hello"'
[links.top]
icon = "envelope"
@endfad
### Youtube Videos ### Youtube Videos
[Back to top](#top) [Back to top](#top)
+5 -3
View File
@@ -6,16 +6,18 @@
</head> </head>
<body> <body>
<main> <main>
<h1><%= info.title %></h1> <h1 class="title"><%= info.title %></h1>
<%= info.description %> <%= info.description %>
<h2>Articles in this blog :</h2> <h2>Articles in this blog :</h2>
<% articles.forEach((article) => { %> <% articles.forEach((article) => { %>
<div class="article"> <div class="article">
<h3><%- `<a href="${article.url}">${article.title}</a>` %></h3> <%- `<a href="${article.url}">` %>
<span class="time"><span>Published on</span> <%= article.year + '-' + article.month + '-' + article.day %></span> <h3><%- `${article.title}` %></h3>
<span class="time"><span>Published on</span>&nbsp;&nbsp;<%= article.year + '-' + ('0' + article.month).slice(-2) + '-' + ('0' + article.day).slice(-2) %></span>
<% if(article.thumbnail){ %> <% if(article.thumbnail){ %>
<%- `<img alt="thumbnail" src=${article.thumbnail}>` %> <%- `<img alt="thumbnail" src=${article.thumbnail}>` %>
<% } %> <% } %>
<%- `</a>` %>
</div> </div>
<% }); %> <% }); %>
<%- include('footer'); %> <%- include('footer'); %>
+26 -3
View File
@@ -16,7 +16,7 @@ body {
} }
main { main {
max-width: 42rem; max-width: 45rem;
padding: 2rem; padding: 2rem;
margin: auto; margin: auto;
background-color: #F0F0F0; background-color: #F0F0F0;
@@ -54,6 +54,13 @@ pre {
padding: 10px 16px; padding: 10px 16px;
} }
:not(pre) > code {
padding: 0.25em 0.5em;
border-radius: 0.25em;
background: #DDD;
font-size: 90%;
}
blockquote { blockquote {
border-left: 0.5em solid #ccc; border-left: 0.5em solid #ccc;
padding-left: 1em; padding-left: 1em;
@@ -108,10 +115,11 @@ main.article div.header a.link-home {
line-height: 2.4; line-height: 2.4;
} }
main.article div.header h1, main.article div.header h2 { main.article div.header h1, main.article div.header h2, .title {
margin-top: 0.85em; margin-top: 0.85em;
margin-bottom: 0.25em; margin-bottom: 0.25em;
font-size: 2em; font-size: 2em;
font-weight: bold;
} }
main.article div.header h1 a, main.article div.header h2 a, div.article h3 a { main.article div.header h1 a, main.article div.header h2 a, div.article h3 a {
@@ -129,6 +137,12 @@ div.article {
div.article h3 { div.article h3 {
font-size: 1.3em; font-size: 1.3em;
margin:0; margin:0;
color: #3C3CA1;
}
div.article a {
text-decoration: none;
color: inherit;
} }
div.article img{ div.article img{
@@ -138,6 +152,15 @@ div.article img{
margin-top:0.25em; margin-top:0.25em;
} }
div.article:hover {
opacity: 0.9;
}
div.article:active {
opacity: 0.8;
}
#text { #text {
text-align: justify; text-align: justify;
hyphens: auto; hyphens: auto;
@@ -147,7 +170,7 @@ div.article img{
text-align: left; text-align: left;
} }
#text img { #text img, #text svg {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
} }
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="header"> <div class="header">
<a class="link-home" href="/">↑</a> <a class="link-home" href="/">↑</a>
<h1><%= article.title %></h1> <h1><%= article.title %></h1>
<span class="time"><span>Published on</span> <%= article.year + '-' + article.month + '-' + article.day %></span> <span class="time"><span><%= article.draft ? 'Drafted on' : 'Published on' %></span>&nbsp;&nbsp;<%= article.year + '-' + ('0' + article.month).slice(-2) + '-' + ('0' + article.day).slice(-2) %></span>
</div> </div>
<div id="text"><%- article.content %></div> <div id="text"><%- article.content %></div>
<br> <br>
+16 -3
View File
@@ -3,6 +3,7 @@ const app = express();
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const pjson = require('../package.json'); const pjson = require('../package.json');
const rateLimit = require('express-rate-limit');
app.enable('trust proxy'); app.enable('trust proxy');
@@ -69,8 +70,9 @@ module.exports = (config) => {
Object.keys(articles).forEach((key) => delete articles[key]); Object.keys(articles).forEach((key) => delete articles[key]);
Object.keys(dict).forEach((key) => articles[key] = dict[key]); Object.keys(dict).forEach((key) => articles[key] = dict[key]);
const nb = Object.keys(articles).length; const nb = Object.keys(articles).length;
const dnb = Object.values(articles).filter(a => a.draft).length;
if (nb > 0) if (nb > 0)
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''}`); console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
else else
console.log(cons.warn, `no articles loaded, check your configuration`); console.log(cons.warn, `no articles loaded, check your configuration`);
@@ -121,6 +123,13 @@ module.exports = (config) => {
next(); next();
}); });
//rate limit for safer server
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: config['rate_limit']
});
app.use(limiter);
//log request at result end //log request at result end
app.use((req, res, next) => { app.use((req, res, next) => {
if (config['access_log']) { if (config['access_log']) {
@@ -144,7 +153,11 @@ module.exports = (config) => {
if (err) if (err)
showError(req, res, 404); showError(req, res, 404);
else else
render(req, res, homePath, {articles: Object.values(articles).sort((a, b) => ('' + b.path).localeCompare(a.path))}); render(req, res, homePath,
{
articles: Object.values(articles)
.filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path))
});
}); });
}); });
@@ -216,7 +229,7 @@ module.exports = (config) => {
if (!article) if (!article)
showError(req, res, 404); showError(req, res, 404);
else { else {
renderer.render(path.join(article.realPath, config['article']['index']), (err, html) => { renderer.render(article.realPath, (err, html) => {
if (err) { if (err) {
console.log(cons.error, `failed to render article ${req.path} : ${err}`); console.log(cons.error, `failed to render article ${req.path} : ${err}`);
return showError(req, res, 500); return showError(req, res, 500);
+4 -1
View File
@@ -3,6 +3,7 @@
"host": "", "host": "",
"data_dir": "data", "data_dir": "data",
"view_engine": "ejs", "view_engine": "ejs",
"rate_limit": 100,
"access_log": "access.log", "access_log": "access.log",
"error_log": "error.log", "error_log": "error.log",
"modules": { "modules": {
@@ -10,7 +11,8 @@
"webhook": true, "webhook": true,
"prism": true, "prism": true,
"mathjax": true, "mathjax": true,
"plantuml": true "plantuml": true,
"fa-diagrams": true
}, },
"home": { "home": {
"title": "GitBlog.md", "title": "GitBlog.md",
@@ -24,6 +26,7 @@
}, },
"article": { "article": {
"index": "index.md", "index": "index.md",
"draft": "draft.md",
"template": "template.ejs", "template": "template.ejs",
"thumbnail_tag": "thumbnail", "thumbnail_tag": "thumbnail",
"default_title": "Untitled", "default_title": "Untitled",
+7 -5
View File
@@ -1,7 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/'); const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
/** /**
* Get all files path inside a given folder path * Get all files path inside a given folder path
@@ -71,8 +71,8 @@ module.exports = (config) => {
if (err) if (err)
return cb(err); return cb(err);
const paths = fileList const paths = fileList
.map((p) => p.substr(config['data_dir'].length+1).split(path.sep)) .map((p) => p.substr(config['data_dir'].length + 1).split(path.sep))
.filter((p) => p.length === 4 && p[3] === config['article']['index'] && .filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) &&
/^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2])); /^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2]));
if (paths.length === 0) if (paths.length === 0)
cb(null, {}); cb(null, {});
@@ -81,7 +81,8 @@ module.exports = (config) => {
paths.forEach((p) => { paths.forEach((p) => {
const article = { const article = {
path: joinUrl(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]), draft: p[3] === config['article']['draft'],
realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]),
year: parseInt(p[0]), year: parseInt(p[0]),
month: parseInt(p[1]), month: parseInt(p[1]),
day: parseInt(p[2]) day: parseInt(p[2])
@@ -89,13 +90,14 @@ module.exports = (config) => {
article.date = new Date(article.year, article.month, article.day); article.date = new Date(article.year, article.month, article.day);
article.date.setUTCHours(0); article.date.setUTCHours(0);
remaining++; remaining++;
readIndexFile(path.join(article.realPath, config['article']['index']), config['article']['thumbnail_tag'], (err, info) => { readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => {
if (err) if (err)
return cb(err); return cb(err);
article.title = info.title || config['article']['default_title']; article.title = info.title || config['article']['default_title'];
article.thumbnail = info.thumbnail ? joinUrl(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.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_');
article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/'; article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
if (!articles[article.path] || !article.draft)
articles[article.path] = article; articles[article.path] = article;
remaining--; remaining--;
if (remaining === 0) if (remaining === 0)
+109 -11
View File
@@ -5,6 +5,54 @@ const showdown = require('showdown');
module.exports = (config) => { module.exports = (config) => {
const converter = new showdown.Converter(config['showdown']); const converter = new showdown.Converter(config['showdown']);
/**
* get parts outside of codes/scripts
* @param {string} data
* @returns {{index:number, end:number, text:string}[]} parts
*/
const getParts = (data) => {
let parts = [];
let match;
let i = 0;
while ((match = /```/m.exec(data.slice(i)))) {
parts.push({
index: i,
text: data.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < data.length)
parts.push({
index: i,
text: data.slice(i, data.length),
});
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
// detect scripts outside of code
parts.forEach((p, pi) => {
let i = 0;
const subParts = [];
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < p.text.length)
subParts.push({
index: p.index + i,
text: p.text.slice(i, p.text.length),
});
parts.splice(pi, 1, ...subParts);
});
parts.forEach(part => part.end = part.index + part.text.length);
return parts;
};
const renderShowDown = (data, cb) => { const renderShowDown = (data, cb) => {
const html = converter.makeHtml(data); const html = converter.makeHtml(data);
cb(html); cb(html);
@@ -35,15 +83,19 @@ module.exports = (config) => {
const renderPlantUML = (data, cb) => { const renderPlantUML = (data, cb) => {
if (!config['modules']['plantuml']) if (!config['modules']['plantuml'])
return cb(data); return cb(data);
const parts = getParts(data);
const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m; const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m;
let match; let match;
while ((match = umlRegex.exec(data))) { parts.forEach(part => {
while ((match = umlRegex.exec(part.text))) {
const code = match[1].trim(); const code = match[1].trim();
const s = unescape(encodeURIComponent(code)); // jshint ignore:line const s = unescape(encodeURIComponent(code)); // jshint ignore:line
const compressed = global['zip_deflate'](s); const compressed = global['zip_deflate'](s);
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;// jshint ignore:line const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;// jshint ignore:line
data = data.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + data.slice(match.index + match[0].length); part.text = part.text.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
} }
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data); cb(data);
}; };
@@ -64,7 +116,9 @@ module.exports = (config) => {
if (!config['modules']['mathjax']) if (!config['modules']['mathjax'])
return cb(data); return cb(data);
const doMJ = (match, format) => { const parts = getParts(data);
const doMJ = (match, format, i) => {
const eq = match[1].trim(); const eq = match[1].trim();
const output = config['mathjax']['output_format']; const output = config['mathjax']['output_format'];
const mjConf = { const mjConf = {
@@ -74,7 +128,7 @@ module.exports = (config) => {
}; };
mjConf[output] = true; mjConf[output] = true;
mjAPI.typeset(mjConf, (res) => { mjAPI.typeset(mjConf, (res) => {
data = data.slice(0, match.index) + res[output] + data.slice(match.index + match[0].length); data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length);
renderMathJax(data, (data2) => { renderMathJax(data, (data2) => {
cb(data2); cb(data2);
}); });
@@ -84,29 +138,72 @@ module.exports = (config) => {
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m; const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const inlineEqRegex = /\$([^$\n]*)\$/; const inlineEqRegex = /\$([^$\n]*)\$/;
for (let i = 0; i < parts.length; i++) {
let match; let match;
if ((match = eqRegex.exec(data))) { if ((match = eqRegex.exec(parts[i].text))) {
doMJ(match, 'TeX'); return doMJ(match, 'TeX', i);
} else if ((match = inlineEqRegex.exec(data))) { } else if ((match = inlineEqRegex.exec(parts[i].text))) {
doMJ(match, 'inline-TeX'); return doMJ(match, 'inline-TeX', i);
} else {
cb(data);
} }
}
cb(data);
};
let faDiagrams;
let toml;
if (config['modules']['fa-diagrams']) {
faDiagrams = require('fa-diagrams');
toml = require('@iarna/toml');
}
const renderFaDiagrams = (data, cb) => {
if (!config['modules']['fa-diagrams'])
return cb(data);
const parts = getParts(data);
const diagramsRegex = /@startfad\r?\n((?:(?!@endfad)[\s\S])*)\r?\n@endfad/m;
let match;
parts.forEach(part => {
while ((match = diagramsRegex.exec(part.text))) {
const code = match[1].trim();
let output;
try {
const diagData = toml.parse(code);
const findLineBreaks = (data) => {
Object.keys(data).forEach(key => {
if (typeof data[key] === 'object')
findLineBreaks(data[key]);
else if (typeof data[key] === 'string')
data[key] = data[key].replace(/\\n/gm, '\n');
});
};
findLineBreaks(diagData);
output = faDiagrams.compute(diagData);
} catch (err) {
output = `<b style="color:red">${err.toString()}</b>`;
}
part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
}; };
return { return {
getParts: config['test'] ? getParts : undefined,
renderShowDown: config['test'] ? renderShowDown : undefined, renderShowDown: config['test'] ? renderShowDown : undefined,
renderPrism: config['test'] ? renderPrism : undefined, renderPrism: config['test'] ? renderPrism : undefined,
renderPlantUML: config['test'] ? renderPlantUML : undefined, renderPlantUML: config['test'] ? renderPlantUML : undefined,
renderMathJax: config['test'] ? renderMathJax : undefined, renderMathJax: config['test'] ? renderMathJax : undefined,
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
render: (file, cb) => { render: (file, cb) => {
fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => { fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => {
if (err) if (err)
return cb(err); return cb(err);
renderPrism(data, (data) => {
renderPlantUML(data, (data) => { renderPlantUML(data, (data) => {
renderFaDiagrams(data, (data) => {
renderMathJax(data, (data) => { renderMathJax(data, (data) => {
renderPrism(data, (data) => {
renderShowDown(data, (html) => { renderShowDown(data, (html) => {
cb(null, html); cb(null, html);
}); });
@@ -114,6 +211,7 @@ module.exports = (config) => {
}); });
}); });
}); });
});
} }
}; };
}; };
+26 -11
View File
@@ -121,10 +121,10 @@ describe('Test root path', () => {
}); });
}); });
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 %>'); fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
request(app).get('/').then((response) => { request(app).get('/').then((response) => {
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
expect(response.text).toBe('error 404 at /'); expect(response.text).toBe('error 404');
done(); done();
}); });
}); });
@@ -160,14 +160,17 @@ describe('Test root path', () => {
done(); done();
}); });
}); });
test('200 2 articles', (done, fail) => { test('200 2 articles 1 drafted', (done, fail) => {
utils.createEmptyDirs([ utils.createEmptyDirs([
path.join(dataDir, '2019', '05', '05'), path.join(dataDir, '2019', '05', '05'),
path.join(dataDir, '2018', '05', '05') path.join(dataDir, '2018', '05', '05'),
path.join(dataDir, '2017', '05', '05')
]); ]);
utils.createEmptyFiles([ utils.createEmptyFiles([
path.join(dataDir, '2019', '05', '05', 'index.md'), path.join(dataDir, '2019', '05', '05', 'draft.md'),
path.join(dataDir, '2018', '05', '05', 'index.md') path.join(dataDir, '2018', '05', '05', 'index.md'),
path.join(dataDir, '2018', '05', '05', 'draft.md'),
path.join(dataDir, '2017', '05', '05', 'index.md'),
]); ]);
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>'); fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
app.reload(() => { app.reload(() => {
@@ -319,12 +322,11 @@ describe('Test articles rendering', () => {
}); });
}); });
test('500 no index', (done, fail) => { test('500 fail to render', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello'); fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>'); fs.writeFileSync(path.join(dataDir, testTemplate), '<%- articl.content %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => { app.reload(() => {
config['article']['index'] = 'invalid.md';
request(app).get('/2019/05/05/hello/').then((response) => { request(app).get('/2019/05/05/hello/').then((response) => {
expect(response.statusCode).toBe(500); expect(response.statusCode).toBe(500);
done(); done();
@@ -356,6 +358,19 @@ describe('Test articles rendering', () => {
}, fail); }, fail);
}); });
test('200 rendered draft', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'draft.md'), '# Hello');
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>');
app.reload(() => {
request(app).get('/2019/05/05/hello/').then((response) => {
expect(response.statusCode).toBe(200);
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
done();
});
}, fail);
});
test('200 other url', (done, fail) => { test('200 other url', (done, fail) => {
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]); utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
utils.createEmptyFiles([ utils.createEmptyFiles([
@@ -394,10 +409,10 @@ describe('Test static files', () => {
}); });
}); });
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 %>'); fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
request(app).get('/somefile.txt').then((response) => { request(app).get('/somefile.txt').then((response) => {
expect(response.statusCode).toBe(404); expect(response.statusCode).toBe(404);
expect(response.text).toBe('error 404 at /somefile.txt'); expect(response.text).toBe('error 404');
done(); done();
}); });
}); });
+65 -3
View File
@@ -6,13 +6,14 @@ const utils = require('./test_utils');
const dataDir = 'test_data'; const dataDir = 'test_data';
const testIndex = 'testindex.md'; const testIndex = 'testindex.md';
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/'); const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
const config = { const config = {
'test': true, 'test': true,
'data_dir': dataDir, 'data_dir': dataDir,
'article': { 'article': {
'index': testIndex, 'index': testIndex,
'draft': 'draft.md',
'default_title': 'Untitled', 'default_title': 'Untitled',
'default_thumbnail': 'default.png', 'default_thumbnail': 'default.png',
'thumbnail_tag': 'thumbnail' 'thumbnail_tag': 'thumbnail'
@@ -235,9 +236,10 @@ describe('Test article fetching', () => {
expect(Object.keys(dict).length).toBe(1); expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({ expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'), path: joinUrl('2019', '05', '05'),
realPath: dir, realPath: file,
year: 2019, year: 2019,
month: 5, month: 5,
draft: false,
day: 5, day: 5,
date: date, date: date,
title: 'Untitled', title: 'Untitled',
@@ -265,10 +267,11 @@ describe('Test article fetching', () => {
expect(Object.keys(dict).length).toBe(1); expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({ expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'), path: joinUrl('2019', '05', '05'),
realPath: dir, realPath: file,
year: 2019, year: 2019,
month: 5, month: 5,
day: 5, day: 5,
draft: false,
date: date, date: date,
title: 'Title with : info !', title: 'Title with : info !',
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'), thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
@@ -278,5 +281,64 @@ describe('Test article fetching', () => {
done(); done();
}); });
}); });
test('correct draft file', (done) => {
const dir = path.join(dataDir, '2019', '05', '05');
const file = path.join(dir, 'draft.md');
utils.createEmptyDirs([dir]);
fs.writeFileSync(file, `
# Title with : info !
![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();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
day: 5,
draft: true,
date: date,
title: 'Title with : info !',
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
escapedTitle: 'title_with___info',
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
});
done();
});
});
test('index file override draft', (done) => {
const dir = path.join(dataDir, '2019', '05', '05');
const file = path.join(dir, testIndex);
const file2 = path.join(dir, 'draft.md');
utils.createEmptyDirs([dir]);
utils.createEmptyFiles([file, file2]);
const date = new Date(2019, 5, 5);
date.setUTCHours(0);
fw.fetchArticles((err, dict) => {
expect(err).toBeNull();
expect(dict).toBeDefined();
expect(Object.keys(dict).length).toBe(1);
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
path: joinUrl('2019', '05', '05'),
realPath: file,
year: 2019,
month: 5,
draft: false,
day: 5,
date: date,
title: 'Untitled',
thumbnail: 'default.png',
escapedTitle: 'untitled',
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
});
done();
});
});
}); });
+88 -1
View File
@@ -11,7 +11,8 @@ const config = {
'modules': { 'modules': {
'prism': true, 'prism': true,
'mathjax': true, 'mathjax': true,
'plantuml': true 'plantuml': true,
'fa-diagrams': true,
}, },
'showdown': { 'showdown': {
'simplifiedAutoLink': true, 'simplifiedAutoLink': true,
@@ -32,6 +33,7 @@ beforeEach(() => {
config['modules']['prism'] = true; config['modules']['prism'] = true;
config['modules']['mathjax'] = true; config['modules']['mathjax'] = true;
config['modules']['plantuml'] = true; config['modules']['plantuml'] = true;
config['modules']['fa-diagrams'] = true;
utils.deleteFolderSync(dataDir); utils.deleteFolderSync(dataDir);
fs.mkdirSync(dataDir); fs.mkdirSync(dataDir);
}); });
@@ -42,6 +44,47 @@ afterAll(() => {
} }
}); });
describe('get parts', () => {
test('normal', () => {
const data = 'Hello\nthere\ngeneral\nkenobi';
const parts = renderer.getParts(data);
expect(parts.map(p => p.text)).toEqual([
'Hello\nthere\ngeneral\nkenobi'
]);
});
test('lot of stuff', () => {
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
const parts = renderer.getParts(data);
expect(parts).toEqual([
{
index: 0,
end: 12,
text: 'Hello\nthere\n'
},
{
index: 22,
end: 30,
text: '\ngeneral'
},
{
index: 53,
end: 54,
text: '\n'
},
{
index: 78,
end: 79,
text: '\n'
},
{
index: 109,
end: 115,
text: 'kenobi'
},
]);
});
});
describe('Test Showdown', () => { describe('Test Showdown', () => {
test('normal', (done) => { test('normal', (done) => {
renderer.renderShowDown('# Hello', (html) => { renderer.renderShowDown('# Hello', (html) => {
@@ -112,6 +155,13 @@ describe('Test PlantUML', () => {
}); });
}); });
test('plantuml ignored in code', (done) => {
renderer.renderPlantUML('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml', (data) => {
expect(data).toBe('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml');
done();
});
});
test('plantuml multiple uml', (done) => { test('plantuml multiple uml', (done) => {
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml\n@startuml\nBob -> Alice : hello\n@enduml', (data) => { renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml\n@startuml\nBob -> Alice : hello\n@enduml', (data) => {
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">\n<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">'); expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">\n<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
@@ -157,6 +207,12 @@ describe('Test MathJax', () => {
done(); done();
}); });
}); });
test('no eq in code / script', (done) => {
renderer.renderMathJax('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```', (data) => {
expect(data).toBe('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```');
done();
});
});
test('multiple eq', (done) => { test('multiple eq', (done) => {
renderer.renderMathJax('$$\n\nA\n\n$$\nstart $a$ end\n$$\n\nA\n\n$$', (data) => { renderer.renderMathJax('$$\n\nA\n\n$$\nstart $a$ end\n$$\n\nA\n\n$$', (data) => {
expect(data).toBe('' + expect(data).toBe('' +
@@ -182,6 +238,37 @@ describe('Test MathJax', () => {
}); });
}); });
describe('Test fa-diagrams', () => {
test('no fa-diagrams', (done) => {
config['modules']['fa-diagrams'] = false;
renderer.renderFaDiagrams('@startfad\noptions.rendering.color=\'red\'\n@endfad', (data) => {
expect(data).toBe('@startfad\noptions.rendering.color=\'red\'\n@endfad');
done();
});
});
test('no fa-diagrams in code', (done) => {
renderer.renderFaDiagrams('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```', (data) => {
expect(data).toBe('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```');
done();
});
});
test('valid fa-diagrams', (done) => {
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=\'red\'\n@endfad\nafter', (data) => {
expect(data).toBe('before\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0" width="0" height="0" font-family="Arial" font-size="15" fill="red" stroke-width="0"></svg>\nafter');
done();
});
});
test('invalid toml', (done) => {
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=red\n@endfad\nafter', (data) => {
expect(data).toBe('before\n<b style="color:red">TomlError: Unexpected character, expecting string, number, datetime, boolean, inline array or inline table at row 1, col 26, pos 25:\n' +
'1> options.rendering.color=red\n' +
' ^\n' +
'\n</b>\nafter');
done();
});
});
});
describe('Test render', () => { describe('Test render', () => {
test('invalid file', (done) => { test('invalid file', (done) => {
renderer.render('invalid file', (err, html) => { renderer.render('invalid file', (err, html) => {
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

+2
View File
@@ -10,6 +10,7 @@ node nodejs {
} }
package data { package data {
[template.ejs]
package "2019/06/18" { package "2019/06/18" {
component index [ component index [
index.md index.md
@@ -22,6 +23,7 @@ package data {
web -down-> TCP : 1. /2019/06/18/title web -down-> TCP : 1. /2019/06/18/title
express -down-> index : 2. fetch express -down-> index : 2. fetch
index -up-> showdown : 3. markdown index -up-> showdown : 3. markdown
template.ejs -up-> express : 4
showdown -left-> express : 4. html showdown -left-> express : 4. html
express -up-> web : 5. html express -up-> web : 5. html