Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d72e94aa3 | |||
| 3d6a0b4306 | |||
| babc533efc | |||
| 36908134e6 | |||
| d426b41368 | |||
| 1836a414eb | |||
| ca49a29dd9 | |||
| 02a768a6af | |||
| bbc4d7c270 | |||
| bfa1521f85 | |||
| a05d380fcf | |||
| 4a32995ca1 | |||
| 53e1fe7201 | |||
| 2e8ff1be92 | |||
| e14f9fc4af | |||
| 896f302bcf | |||
| cc0bd1cf49 | |||
| 7a1d9cbbd6 | |||
| 34e8d4cb6f | |||
| 4a9b70ac68 | |||
| 889258c874 | |||
| de26feb05c | |||
| 8bb455b576 | |||
| 378ed438b6 | |||
| 3b07b6b9c5 | |||
| b6afcd4992 | |||
| 35fcdc7320 | |||
| dfb93b6764 | |||
| 6af4012522 | |||
| 1b91002c03 | |||
| bedd6a2953 | |||
| 52d37d56cd | |||
| fc7bc63c46 | |||
| 4397a76d9b | |||
| ddf964eb27 | |||
| 4b47276484 | |||
| a7fedb149f | |||
| ea95a285c9 | |||
| 0fde428806 | |||
| 8fc7ff1ca7 | |||
| ae4e2eb8d5 | |||
| 528e4be1fe | |||
| bd42883330 | |||
| b6ac0a73b4 | |||
| aebc3da5bc | |||
| 7a4a4f9006 | |||
| 1341aa5a56 | |||
| 5e05f250f4 | |||
| 6cf7be3afb | |||
| 6aceacad18 | |||
| a3a23be1c2 | |||
| e8e8024021 | |||
| 1806d60ca7 | |||
| 2c5f2e589f | |||
| 847d228c0a | |||
| 576948acee | |||
| fa6d91db20 | |||
| 989bcdf130 | |||
| 000104c99d | |||
| f2bd0ec10e | |||
| 97dab302d8 | |||
| 55e258e093 | |||
| 7b22a4773d | |||
| 14cd1436c3 | |||
| c112e1ea62 | |||
| bd8385ea60 | |||
| 6dbc7f359b | |||
| e773f53da1 | |||
| 35a7747d6d | |||
| 0d173cfcef | |||
| 0480536a20 | |||
| 333bbf6eb8 | |||
| 6bdcd6872e | |||
| 9d3c1d0847 | |||
| def326676c | |||
| d343179764 |
+3
-1
@@ -3,6 +3,8 @@
|
||||
/config.json
|
||||
/config.example.json
|
||||
/data
|
||||
/data/*
|
||||
/test_data
|
||||
/access.log
|
||||
/error.log
|
||||
/error.log
|
||||
/coverage
|
||||
@@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src/lib
|
||||
+2
-6
@@ -6,15 +6,11 @@ cache:
|
||||
npm: true
|
||||
directories:
|
||||
- node_modules
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- graphviz
|
||||
install:
|
||||
- npm install
|
||||
- npm install node-plantuml
|
||||
before_script:
|
||||
- npm install -g jshint
|
||||
script:
|
||||
- jest --silent --coverage --coverageReporters=text-lcov | coveralls
|
||||
- jest --coverage --silent
|
||||
- jshint ./src
|
||||
- cat ./coverage/lcov.info | coveralls
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# GitBlog.md
|
||||
|
||||
|
||||
[](https://travis-ci.org/Klemek/GitBlog.md)
|
||||
[](https://coveralls.io/github/Klemek/GitBlog.md?branch=master)
|
||||
|
||||
# GitBlog.md
|
||||
|
||||
A static blog using Markdown pulled from your git repository.
|
||||
|
||||
> Step 1 : ```$ vi 2019/06/21/index.md```
|
||||
@@ -14,6 +13,7 @@ A static blog using Markdown pulled from your git repository.
|
||||
* **[How it works](#how-it-works)**
|
||||
* **[Installation](#installation)**
|
||||
* **[Writing an article](#writing-an-article)**
|
||||
* **[Modules](#modules)**
|
||||
* **[Configuration](#configuration)**
|
||||
|
||||
## How it works
|
||||
@@ -80,7 +80,7 @@ On the `/rss` endpoint, the servers gives you a RSS feed based on the list of ar
|
||||
#### 1. Download and install the latest version from the repo
|
||||
```bash
|
||||
git clone https://github.com/klemek/gitblog.md.git
|
||||
npm install
|
||||
npm install --production
|
||||
```
|
||||
#### 2. Create your config file
|
||||
```bash
|
||||
@@ -124,6 +124,34 @@ Resources are located on the `data` folder and can be referenced as the root of
|
||||
/styles/main.css => data/styles/main.css
|
||||
```
|
||||
|
||||
In your template, the following data is sent :
|
||||
|
||||
<details>
|
||||
<summary>details (click)</summary>
|
||||
<p>
|
||||
|
||||
* `info` (every pages)
|
||||
* `title` : the blog's title as in the config
|
||||
* `description` the blog's description as in the config
|
||||
* `host` : the specified or guessed host with the protocol
|
||||
* `version` : the GitBlog.md current running version
|
||||
* `request` : the Express request object
|
||||
* `config` : the content of the config
|
||||
* `article` (article pages only)
|
||||
* `title` : the full title
|
||||
* `thumbnail` the URL path of the thumbnail
|
||||
* `url` : the URL path for this article (with the title)
|
||||
* `date` : a JS date
|
||||
* `year`
|
||||
* `month`
|
||||
* `day`
|
||||
* `path` : the URL path for the folder of the article (without the title)
|
||||
* `realPath` : the system's path for the folder
|
||||
* `escapedTitle` : the code with alphanumeric and underscore characters only
|
||||
* `error` (error pages only) : the error code
|
||||
</p>
|
||||
</details>
|
||||
|
||||
#### 5. Create and init your git source
|
||||
|
||||
You need to [create a new repository](https://github.com/new) on your favorite Git service.
|
||||
@@ -131,7 +159,10 @@ You need to [create a new repository](https://github.com/new) on your favorite G
|
||||
```bash
|
||||
#gitblog.md/
|
||||
cd data
|
||||
git init
|
||||
git remote add origin <url_of_your_repo.git>
|
||||
git add .
|
||||
git commit -m "initial commit"
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
@@ -156,7 +187,7 @@ Here are the steps for Github, if you use another platform adapt it your way (he
|
||||
```json
|
||||
"webhook": {
|
||||
"endpoint": "/webhook",
|
||||
"secret": "sha1=<value>",
|
||||
"secret": "<value>",
|
||||
"signature_header": "X-Hub-Signature"
|
||||
},
|
||||
```
|
||||
@@ -164,6 +195,14 @@ Here are the steps for Github, if you use another platform adapt it your way (he
|
||||
* Update your webhook on github to include the secret
|
||||
* Check if Github successfully reached the endpoint
|
||||
|
||||
#### 8. Keep your server always up and running (optionnal)
|
||||
|
||||
This project `package.json` comes with a [nodemon](https://github.com/remy/nodemon) config.
|
||||
|
||||
After installing (`npm i -g nodemon`) you can then run the app with juste the `nodemon` command in the working directory.
|
||||
|
||||
With this method, you can do a simple `git pull` to update your server.
|
||||
|
||||
## Writing an article
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
@@ -192,11 +231,31 @@ On that same folder, you can place resources like images and reference them in r
|
||||
|
||||
Any URL like `/year/month/day/anything/` will redirect to this article (and link to correct resources)
|
||||
|
||||
## Modules
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
* **RSS**
|
||||
It allows your users to use the feed to be updated as soon as a new article is out
|
||||
* **Webhook**
|
||||
It update your blog from your online repo when it's updated
|
||||
* **Prism**
|
||||
It highlight code blocks to be more readable (more info [here](https://prismjs.com/), you will need the corresponding CSS file on your templates)
|
||||
* **MathJax**
|
||||
It allows you to add math equations to your articles by simply writing LaTeX between `$$` for full size (and between $ for inline) (more info [here](https://www.mathjax.org/))
|
||||
* **PlantUML**
|
||||
It allows you to add UML diagrams with PlantUML Syntax between `@startuml` and `@enduml` (more info [here](http://www.plantuml.com))
|
||||
* **fa-diagrams**
|
||||
It allows you to define SVG diagrams with Font-Awesome icons in YAML between `@startfad` and `@endfad` (more info [here](https://github.com/Klemek/fa-diagrams))
|
||||
|
||||
|
||||
## Configuration
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
* `node_port` (default: 3000)
|
||||
the port the server is listening to
|
||||
* `host` (default: none)
|
||||
if set (like `https://mywebsite.com`, it will be used as reference for creating links
|
||||
by default, host is guessed based on first request
|
||||
* `data_dir` (default: data)
|
||||
the directory where will be located the git repo with templates and articles
|
||||
* `view_engine` (default: ejs)
|
||||
@@ -212,18 +271,32 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
activate the webhook endpoint and its features
|
||||
* `prism` (default: true)
|
||||
activate Prism code highlighting
|
||||
* `mathjax` (default: true)
|
||||
activate MathJax equations formatting
|
||||
* `plantuml` (default: true)
|
||||
activate PlantUML diagram rendering
|
||||
* `fa-diagrams` (default: true)
|
||||
activate fa-diagrams rendering
|
||||
* `home`
|
||||
* `title` (default: GitBlog.md)
|
||||
the title of your blog, **strongly advised to be changed**
|
||||
given to the template to complete page title and metadata
|
||||
* `description` (default: A static blog using Markdown pulled from your git repository)
|
||||
the description of your blog, **strongly advised to be changed**
|
||||
given to the template to complete page title and metadata
|
||||
* `index` (default: index.ejs)
|
||||
the name of the home page template on the data directory
|
||||
it will receive `articles`, a list of articles for rendering
|
||||
* `error` (default: error.ejs)
|
||||
the name of the error page template on the data directory
|
||||
it will receive `error`, the error code
|
||||
* `hidden` (default: `[.ejs]`)
|
||||
file extensions to be returned 404 when reached
|
||||
* `hidden` (default: `[*.ejs,/.git*]`)
|
||||
path matches to be returned 404 when reached
|
||||
* `article`
|
||||
* `index` (default: index.md)
|
||||
the name of the Markdown page of the article on the `/year/month/day/` directory
|
||||
* `draft` (default: draft.md)
|
||||
the name of the Markdown page of an article not shown on the list
|
||||
* `template` (default: template.ejs)
|
||||
the name of the article page template on the data directory
|
||||
* `thumbnail_tag`: (default: thumbnail)
|
||||
@@ -248,4 +321,9 @@ Any URL like `/year/month/day/anything/` will redirect to this article (and link
|
||||
* `pull_command`: (default: git pull)
|
||||
the command used by the server on webhook trigger
|
||||
* `showdown`
|
||||
Options to be applied to Showdown renderer (see [showdown options](https://github.com/showdownjs/showdown#valid-options) for more info)
|
||||
Options to be applied to Showdown renderer (see [showdown options](https://github.com/showdownjs/showdown/wiki/Showdown-Options) for more info)
|
||||
* `mathjax`
|
||||
* `output_format`: (default: svg)
|
||||
specify the output format between svg, html or MathMl (mml)
|
||||
* `speak_text`: (default: true)
|
||||
activate the alternate text in equations
|
||||
|
||||
Generated
+201
-200
File diff suppressed because it is too large
Load Diff
+25
-3
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"nodePort": 5000,
|
||||
"name": "gitblog.md",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.6",
|
||||
"description": "A static blog using Markdown pulled from your git repository.",
|
||||
"main": "src/server.js",
|
||||
"dependencies": {
|
||||
@@ -9,6 +8,9 @@
|
||||
"crypto": "^1.0.1",
|
||||
"ejs": "^2.6.2",
|
||||
"express": "^4.17.1",
|
||||
"fa-diagrams": "^1.0.3",
|
||||
"js-yaml": "^3.13.1",
|
||||
"mathjax-node": "^2.1.1",
|
||||
"ncp": "^2.0.0",
|
||||
"node-prismjs": "^0.1.2",
|
||||
"prismjs": "^1.16.0",
|
||||
@@ -37,5 +39,25 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/klemek/gitblog.md/issues"
|
||||
},
|
||||
"homepage": "https://github.com/klemek/gitblog.md#readme"
|
||||
"homepage": "https://github.com/klemek/gitblog.md#readme",
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.js",
|
||||
"!/node_modules/",
|
||||
"!src/server.js",
|
||||
"!src/postinstall.js",
|
||||
"!src/lib/*.js"
|
||||
]
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"verbose": true,
|
||||
"ignore": [
|
||||
"test/*",
|
||||
"sample_data/*",
|
||||
"data/*",
|
||||
"uml/*",
|
||||
"*.log",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,26 @@ If you see this page, that means it's working
|
||||
|
||||
## Guide to Markdown formatting
|
||||
|
||||
* [Headers](#headers)
|
||||
* [Emphasis](#emphasis)
|
||||
* [Lists](#lists)
|
||||
* [Links](#links)
|
||||
* [Images](#images)
|
||||
* [Code and Syntax Highlighting](#codeandsyntaxhighlighting)
|
||||
* [Tables](#tables)
|
||||
* [Blockquotes](#blockquotes)
|
||||
* [Inline HTML](#inlinehtml)
|
||||
* [Horizontal Rule](#horizontalrule)
|
||||
* [Line Breaks](#linebreaks)
|
||||
* [Check Boxes](#checkboxes)
|
||||
* [Spoilers](#spoilers)
|
||||
* [Math Equations](#mathequations)
|
||||
* [UML](#uml)
|
||||
* [Diagrams](#diagrams)
|
||||
* [Youtube Videos](#youtubevideos)
|
||||
|
||||
### Headers
|
||||
[Back to top](#top)
|
||||
|
||||
# H1
|
||||
## H2
|
||||
@@ -22,6 +41,7 @@ Alt-H2
|
||||
------
|
||||
|
||||
### Emphasis
|
||||
[Back to top](#top)
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
@@ -32,6 +52,7 @@ Combined emphasis with **asterisks and _underscores_**.
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
### Lists
|
||||
[Back to top](#top)
|
||||
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
@@ -51,6 +72,7 @@ Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
+ Or pluses
|
||||
|
||||
### Links
|
||||
[Back to top](#top)
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
@@ -75,6 +97,7 @@ Some text to show that the reference links can follow later.
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
### Images
|
||||
[Back to top](#top)
|
||||
|
||||
Here's our logo (hover to see the title text):
|
||||
|
||||
@@ -88,6 +111,7 @@ Reference-style:
|
||||
|
||||
|
||||
### Code and Syntax Highlighting
|
||||
[Back to top](#top)
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
@@ -108,6 +132,7 @@ But let's throw in a <b>tag</b>.
|
||||
|
||||
|
||||
### Tables
|
||||
[Back to top](#top)
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
@@ -127,6 +152,7 @@ Markdown | Less | Pretty
|
||||
1 | 2 | 3
|
||||
|
||||
### Blockquotes
|
||||
[Back to top](#top)
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
@@ -136,6 +162,7 @@ Quote break.
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
|
||||
### Inline HTML
|
||||
[Back to top](#top)
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
@@ -146,6 +173,7 @@ Quote break.
|
||||
</dl>
|
||||
|
||||
### Horizontal Rule
|
||||
[Back to top](#top)
|
||||
|
||||
Three or more...
|
||||
|
||||
@@ -162,6 +190,7 @@ ___
|
||||
Underscores
|
||||
|
||||
### Line Breaks
|
||||
[Back to top](#top)
|
||||
|
||||
Here's a line for us to start with.
|
||||
|
||||
@@ -169,3 +198,89 @@ This line is separated from the one above by two newlines, so it will be a *sepa
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
|
||||
### Check Boxes
|
||||
[Back to top](#top)
|
||||
|
||||
* [x] Task completed
|
||||
* [] Task to do
|
||||
|
||||
### Spoilers
|
||||
[Back to top](#top)
|
||||
|
||||
<details><summary>Title of the spoiler (click)</summary><p>
|
||||
Content of the spoiler
|
||||
|
||||
On several lines
|
||||
</p></details>
|
||||
|
||||
### Math Equations
|
||||
[Back to top](#top)
|
||||
|
||||
You can use LaTeX equations with MathJax for full equations and inline ones (based on the number of $) :
|
||||
|
||||
$$
|
||||
\large{\beta=\sum_{i}^{\alpha }\frac{x^{i}}{\alpha}}
|
||||
$$
|
||||
|
||||
|
||||
Where $\alpha$ is cool
|
||||
|
||||
### UML
|
||||
[Back to top](#top)
|
||||
|
||||
You can use PlantUML diagrams with `@startuml` and `@enduml` tags :
|
||||
|
||||
@startuml
|
||||
title Article
|
||||
cloud web
|
||||
node nodejs {
|
||||
TCP -right- [express]
|
||||
[showdown]
|
||||
}
|
||||
package data {
|
||||
package "2019/06/18" {
|
||||
component index [
|
||||
index.md
|
||||
image.png
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
web -down-> TCP : 1. /2019/06/18/title
|
||||
express -down-> index : 2. fetch
|
||||
index -up-> showdown : 3. markdown
|
||||
showdown -left-> express : 4. html
|
||||
express -up-> web : 5. html
|
||||
@enduml
|
||||
|
||||
### Diagrams
|
||||
[Back to top](#top)
|
||||
|
||||
You can use [fa-diagrams](https://github.com/Klemek/fa-diagrams) with `@startfad` and `@endfad` tags and using YAML inside
|
||||
|
||||
@startuml
|
||||
nodes:
|
||||
- name: node1
|
||||
icon: laptop-code
|
||||
color: '#4E342E'
|
||||
bottom: my app
|
||||
- name: node2
|
||||
icon: globe
|
||||
color: '#455A64'
|
||||
bottom: world
|
||||
links:
|
||||
- from: node1
|
||||
to: node2
|
||||
color: '#333333'
|
||||
top:
|
||||
icon: envelope
|
||||
bottom: '"hello"'
|
||||
@enduml
|
||||
|
||||
### Youtube Videos
|
||||
[Back to top](#top)
|
||||
|
||||
Just use the "embedded" export on Youtube with dimensions of 535x300 for best results
|
||||
|
||||
<iframe width="535" height="300" src="https://www.youtube.com/embed/FTQbiNvZqaY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
@@ -1,9 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Error <%= error %></title>
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - Error <%= error %></title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<hr>
|
||||
<footer>
|
||||
<small>@<%= new Date().getFullYear() %> - Made with <a href="https://github.com/klemek/gitblog.md">GitBlog.md</a>
|
||||
<small><a href="/rss">RSS feed</a> - <%= new Date().getFullYear() %> - Made with <a
|
||||
href="https://github.com/klemek/gitblog.md">GitBlog.md</a> (v<%= info.version %>)
|
||||
</small>
|
||||
</footer>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
|
||||
<META NAME="ROBOTS" CONTENT="INDEX, FOLLOW">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<%- `<meta property="og:description" content="${info.description}">` %>
|
||||
<%- `<meta property="twitter:description" content="${info.description}">` %>
|
||||
<% if(locals.article){ %>
|
||||
<%- `<meta property="org:url" content="${info.host + article.url}">` %>
|
||||
<%- `<meta property="og:title" content="${info.title} - ${article.title}">` %>
|
||||
<%- `<meta property="twitter:title" content="${info.title} - ${article.title}">` %>
|
||||
<% if (article.thumbnail) { %>
|
||||
<%- `<meta property="og:image" content="${info.host}/${article.thumbnail}">` %>
|
||||
<%- `<meta property="twitter:image" content="${info.host}/${article.thumbnail}">` %>
|
||||
<% } %>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/prism.css">
|
||||
<% } else { %>
|
||||
<%- `<meta property="org:url" content="${info.host}/">` %>
|
||||
<%- `<meta property="og:title" content="${info.title} - Home">` %>
|
||||
<%- `<meta property="twitter:title" content="${info.title} - Home">` %>
|
||||
|
||||
<%- `<meta property="description" content="${info.description}">` %>
|
||||
<% } %>
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS feed" href="/rss"/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
@@ -1,22 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GitBlog.md - Home</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>GitBlog.md</h1>
|
||||
A static blog using Markdown pulled from your git repository
|
||||
<h1 class="title"><%= info.title %></h1>
|
||||
<%= info.description %>
|
||||
<h2>Articles in this blog :</h2>
|
||||
<% articles.forEach((article) => { %>
|
||||
<div class="article">
|
||||
<h3><%- `<a href="${article.url}">${article.title}</a>` %></h3>
|
||||
<span class="time"><span>Published on</span> <%= article.year + '-' + article.month + '-' + article.day %></span>
|
||||
<%- `<a href="${article.url}">` %>
|
||||
<h3><%- `${article.title}` %></h3>
|
||||
<span class="time"><span>Published on</span> <%= article.year + '-' + ('0' + article.month).slice(-2) + '-' + ('0' + article.day).slice(-2) %></span>
|
||||
<% if(article.thumbnail){ %>
|
||||
<%- `<img alt="thumbnail" src=${article.thumbnail}>` %>
|
||||
<% } %>
|
||||
<%- `</a>` %>
|
||||
</div>
|
||||
<% }); %>
|
||||
<%- include('footer'); %>
|
||||
|
||||
@@ -8,7 +8,7 @@ body, html {
|
||||
}
|
||||
|
||||
body {
|
||||
font: 14px/1.45 -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif;
|
||||
font: 15px sans-serif;
|
||||
color: #111;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #F5F5F5;
|
||||
@@ -16,8 +16,8 @@ body {
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 75ch;
|
||||
padding: 2ch;
|
||||
max-width: 45rem;
|
||||
padding: 2rem;
|
||||
margin: auto;
|
||||
background-color: #F0F0F0;
|
||||
min-height: 100vh;
|
||||
@@ -54,11 +54,18 @@ pre {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
background: #DDD;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.5em solid #ccc;
|
||||
padding-left: 1em;
|
||||
margin: 0.25em 0;
|
||||
color: #333;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
blockquote > p {
|
||||
@@ -108,10 +115,11 @@ main.article div.header a.link-home {
|
||||
line-height: 2.4;
|
||||
}
|
||||
|
||||
main.article div.header h1, main.article div.header h2, div.article h3 {
|
||||
main.article div.header h1, main.article div.header h2, .title {
|
||||
margin-top: 0.85em;
|
||||
margin-bottom: 0.25em;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
main.article div.header h1 a, main.article div.header h2 a, div.article h3 a {
|
||||
@@ -123,13 +131,36 @@ main.article div.header span.time, div.article span.time {
|
||||
}
|
||||
|
||||
div.article {
|
||||
margin-left: 1em;
|
||||
margin: 0 1em 1em 1em;
|
||||
}
|
||||
|
||||
div.article h3 {
|
||||
font-size: 1.3em;
|
||||
margin:0;
|
||||
color: #3C3CA1;
|
||||
}
|
||||
|
||||
div.article a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
div.article img{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-right:1em;
|
||||
margin-top:0.25em;
|
||||
}
|
||||
|
||||
div.article:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
div.article:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
#text {
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
@@ -139,7 +170,7 @@ div.article h3 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#text img {
|
||||
#text img, #text svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GitBlog.md - <%= article.title %></title>
|
||||
<link rel="stylesheet" type="text/css" href="/prism.css">
|
||||
<link rel="stylesheet" type="text/css" href="/style.css">
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - <%= article.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="article">
|
||||
<div class="header">
|
||||
<a class="link-home" href="/">↑</a>
|
||||
<h1><%= article.title %></h1>
|
||||
<span class="time"><span>Published on</span> <%= article.year + '-' + article.month + '-' + article.day %></span>
|
||||
<span class="time"><span><%= article.draft ? 'Drafted on' : 'Published on' %></span> <%= article.year + '-' + ('0' + article.month).slice(-2) + '-' + ('0' + article.day).slice(-2) %></span>
|
||||
</div>
|
||||
<div id="text"><%- article.content %></div>
|
||||
<br>
|
||||
@@ -19,4 +17,4 @@
|
||||
<%- include('footer'); %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+74
-48
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const app = express();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pjson = require('../package.json');
|
||||
|
||||
app.enable('trust proxy');
|
||||
|
||||
@@ -25,6 +26,28 @@ const cons = {
|
||||
};
|
||||
|
||||
module.exports = (config) => {
|
||||
/**
|
||||
* Fetch articles from the data folder and send success as a response
|
||||
* @param success
|
||||
* @param error
|
||||
*/
|
||||
let reload;
|
||||
/**
|
||||
* Render the page with the view engine and catch errors
|
||||
* @param req
|
||||
* @param res
|
||||
* @param vPath - path of the view
|
||||
* @param data - data to pass to the view
|
||||
* @param code - code to send along the page
|
||||
*/
|
||||
let render;
|
||||
/**
|
||||
* Show an error with the correct page
|
||||
* @param req
|
||||
* @param res
|
||||
* @param code - error code
|
||||
*/
|
||||
let showError;
|
||||
const fw = require('./file_walker')(config);
|
||||
const renderer = require('./renderer')(config);
|
||||
|
||||
@@ -35,13 +58,9 @@ module.exports = (config) => {
|
||||
|
||||
const articles = {};
|
||||
let lastRSS = '';
|
||||
let host = config['host'];
|
||||
|
||||
/**
|
||||
* Fetch articles from the data folder and send success as a response
|
||||
* @param success
|
||||
* @param error
|
||||
*/
|
||||
const reload = (success, error) => {
|
||||
reload = (success, error) => {
|
||||
fw.fetchArticles((err, dict) => {
|
||||
if (err) {
|
||||
console.error(cons.error, 'error loading articles : ' + err);
|
||||
@@ -50,8 +69,9 @@ module.exports = (config) => {
|
||||
Object.keys(articles).forEach((key) => delete articles[key]);
|
||||
Object.keys(dict).forEach((key) => articles[key] = dict[key]);
|
||||
const nb = Object.keys(articles).length;
|
||||
const dnb = Object.values(articles).filter(a => a.draft).length;
|
||||
if (nb > 0)
|
||||
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''}`);
|
||||
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
|
||||
else
|
||||
console.log(cons.warn, `no articles loaded, check your configuration`);
|
||||
|
||||
@@ -63,39 +83,45 @@ module.exports = (config) => {
|
||||
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) => {
|
||||
render = (req, res, vPath, data, code = 200) => {
|
||||
data.info = {
|
||||
title: config['home']['title'],
|
||||
description: config['home']['description'],
|
||||
host: host,
|
||||
version: pjson.version,
|
||||
request: req,
|
||||
config: config
|
||||
};
|
||||
res.render(vPath, data, (err, html) => {
|
||||
if (err) {
|
||||
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
|
||||
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
|
||||
showError(req, res, 500);
|
||||
} else if (err) {
|
||||
res.sendStatus(500);
|
||||
console.log(cons.error, `failed to render ${vPath} : ${err}`);
|
||||
console.log(cons.error, `failed to render error page : ${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) => {
|
||||
showError = (req, res, code) => {
|
||||
const errorPath = path.join(config['data_dir'], config['home']['error']);
|
||||
fs.access(errorPath, fs.constants.R_OK, (err) => {
|
||||
if (err)
|
||||
res.sendStatus(code);
|
||||
else
|
||||
render(res, errorPath, {error: code, path: resPath}, code);
|
||||
render(req, res, errorPath, {error: code}, code);
|
||||
});
|
||||
};
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (!host) {
|
||||
host = 'http://' + req.headers.host;
|
||||
console.log(cons.ok, 'Currently hosted on ' + host);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
//log request at result end
|
||||
app.use((req, res, next) => {
|
||||
if (config['access_log']) {
|
||||
@@ -117,9 +143,13 @@ module.exports = (config) => {
|
||||
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);
|
||||
showError(req, res, 404);
|
||||
else
|
||||
render(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))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,23 +160,23 @@ module.exports = (config) => {
|
||||
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
|
||||
'feed_url': host + req.url,
|
||||
'site_url': host
|
||||
});
|
||||
Object.values(articles)
|
||||
.slice(0, config['rss']['length'])
|
||||
.forEach((article) => {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
url: 'http://' + req.headers.host + article.url,
|
||||
url: host + article.url,
|
||||
date: article.date
|
||||
});
|
||||
});
|
||||
lastRSS = feed.xml();
|
||||
}
|
||||
res.type('rss').send(lastRSS);
|
||||
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
|
||||
} else {
|
||||
showError(req.path, 404, res);
|
||||
showError(req, res, 404);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -154,10 +184,7 @@ module.exports = (config) => {
|
||||
app.post(config['webhook']['endpoint'], (req, res) => {
|
||||
if (config['modules']['webhook']) {
|
||||
if (config['webhook']['signature_header'] && config['webhook']['secret']) {
|
||||
const payload = JSON.stringify(req.body);
|
||||
if (!payload) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
const payload = JSON.stringify(req.body) || '';
|
||||
const hmac = crypto.createHmac('sha1', config['webhook']['secret']);
|
||||
const digest = 'sha1=' + hmac.update(payload).digest('hex');
|
||||
const checksum = req.headers[config['webhook']['signature_header']];
|
||||
@@ -192,21 +219,21 @@ module.exports = (config) => {
|
||||
const articlePath = req.path.substr(1, 10);
|
||||
const article = articles[articlePath];
|
||||
if (!article)
|
||||
showError(req.path, 404, res);
|
||||
showError(req, res, 404);
|
||||
else {
|
||||
renderer.render(path.join(article.realPath, config['article']['index']), (err, html) => {
|
||||
renderer.render(article.realPath, (err, html) => {
|
||||
if (err) {
|
||||
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
|
||||
return showError(req.path, 500, res);
|
||||
return showError(req, res, 500);
|
||||
}
|
||||
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);
|
||||
showError(req, res, 500);
|
||||
} else
|
||||
render(res, templatePath, {article: article});
|
||||
render(req, res, templatePath, {article: article});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -216,18 +243,17 @@ module.exports = (config) => {
|
||||
});
|
||||
|
||||
// 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();
|
||||
config['home']['hidden'].forEach(pathMatcher => {
|
||||
app.get(pathMatcher, (req, res) => {
|
||||
showError(req, res, 404);
|
||||
});
|
||||
});
|
||||
|
||||
// serve all static files via get
|
||||
app.get('*', express.static(config['data_dir']));
|
||||
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
|
||||
// catch express.static errors (mostly not found) by displaying 404
|
||||
app.get('*', (req, res) => {
|
||||
showError(req.path, 404, res);
|
||||
showError(req, res, 404);
|
||||
});
|
||||
|
||||
// catch all other methods and return 400
|
||||
|
||||
+19
-4
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"node_port": 3000,
|
||||
"host": "",
|
||||
"data_dir": "data",
|
||||
"view_engine": "ejs",
|
||||
"access_log": "access.log",
|
||||
@@ -7,17 +8,24 @@
|
||||
"modules": {
|
||||
"rss": true,
|
||||
"webhook": true,
|
||||
"prism": true
|
||||
"prism": true,
|
||||
"mathjax": true,
|
||||
"plantuml": true,
|
||||
"fa-diagrams": true
|
||||
},
|
||||
"home": {
|
||||
"title": "GitBlog.md",
|
||||
"description": "A static blog using Markdown pulled from your git repository",
|
||||
"index": "index.ejs",
|
||||
"error": "error.ejs",
|
||||
"hidden": [
|
||||
".ejs"
|
||||
"*.ejs",
|
||||
"/.git*"
|
||||
]
|
||||
},
|
||||
"article": {
|
||||
"index": "index.md",
|
||||
"draft": "draft.md",
|
||||
"template": "template.ejs",
|
||||
"thumbnail_tag": "thumbnail",
|
||||
"default_title": "Untitled",
|
||||
@@ -33,7 +41,7 @@
|
||||
"endpoint": "/webhook",
|
||||
"secret": "",
|
||||
"signature_header": "",
|
||||
"pull_command": "git pull"
|
||||
"pull_command": "git pull origin master"
|
||||
},
|
||||
"showdown": {
|
||||
"parseImgDimensions": true,
|
||||
@@ -42,5 +50,12 @@
|
||||
"tasklists": true,
|
||||
"openLinksInNewWindow": true,
|
||||
"emoji": true
|
||||
},
|
||||
"mathjax": {
|
||||
"output_format": "svg",
|
||||
"speak_text": true
|
||||
},
|
||||
"plantuml": {
|
||||
"output_format": "svg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -10,6 +10,10 @@ const fs = require('fs');
|
||||
const merge = (ref, src) => {
|
||||
if (typeof ref !== typeof src) {
|
||||
return ref;
|
||||
} else if (ref.length && !src.length) {
|
||||
return ref;
|
||||
} else if (ref.length && src.length) {
|
||||
return src;
|
||||
} else if (typeof ref === 'object') {
|
||||
const out = {};
|
||||
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
|
||||
@@ -25,7 +29,7 @@ module.exports = () => {
|
||||
let config = JSON.parse(configData);
|
||||
return merge(refConfig, config);
|
||||
} catch (error) {
|
||||
console.error('Failed to load config.json : ' + error);
|
||||
console.log('\x1b[33m⚠\x1b[0m %s', 'Failed to load config.json : ' + error);
|
||||
return refConfig;
|
||||
}
|
||||
};
|
||||
+8
-6
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/');
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
|
||||
/**
|
||||
* Get all files path inside a given folder path
|
||||
@@ -71,8 +71,8 @@ module.exports = (config) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
const paths = fileList
|
||||
.map((p) => p.substr(config['data_dir'].length+1).split(path.sep))
|
||||
.filter((p) => p.length === 4 && p[3] === config['article']['index'] &&
|
||||
.map((p) => p.substr(config['data_dir'].length + 1).split(path.sep))
|
||||
.filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) &&
|
||||
/^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2]));
|
||||
if (paths.length === 0)
|
||||
cb(null, {});
|
||||
@@ -81,7 +81,8 @@ module.exports = (config) => {
|
||||
paths.forEach((p) => {
|
||||
const article = {
|
||||
path: joinUrl(p[0], p[1], p[2]),
|
||||
realPath: path.join(config['data_dir'], p[0], p[1], p[2]),
|
||||
draft: p[3] === config['article']['draft'],
|
||||
realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]),
|
||||
year: parseInt(p[0]),
|
||||
month: parseInt(p[1]),
|
||||
day: parseInt(p[2])
|
||||
@@ -89,14 +90,15 @@ module.exports = (config) => {
|
||||
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) => {
|
||||
readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
article.title = info.title || config['article']['default_title'];
|
||||
article.thumbnail = info.thumbnail ? joinUrl(article.path, info.thumbnail) : config['article']['default_thumbnail'];
|
||||
article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_');
|
||||
article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
|
||||
articles[article.path] = article;
|
||||
if (!articles[article.path] || !article.draft)
|
||||
articles[article.path] = article;
|
||||
remaining--;
|
||||
if (remaining === 0)
|
||||
cb(null, articles);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+202
-21
@@ -1,35 +1,216 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const showdown = require('showdown');
|
||||
const Prism = require('node-prismjs');
|
||||
|
||||
module.exports = (config) => {
|
||||
const converter = new showdown.Converter(config['showdown']);
|
||||
|
||||
/**
|
||||
* get parts outside of codes/scripts
|
||||
* @param {string} data
|
||||
* @returns {{index:number, end:number, text:string}[]} parts
|
||||
*/
|
||||
const getParts = (data) => {
|
||||
let parts = [];
|
||||
let match;
|
||||
let i = 0;
|
||||
while ((match = /```/m.exec(data.slice(i)))) {
|
||||
parts.push({
|
||||
index: i,
|
||||
text: data.slice(i, i + match.index),
|
||||
});
|
||||
i += match.index + match[0].length;
|
||||
}
|
||||
if (i < data.length)
|
||||
parts.push({
|
||||
index: i,
|
||||
text: data.slice(i, data.length),
|
||||
});
|
||||
|
||||
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
|
||||
|
||||
// detect scripts outside of code
|
||||
parts.forEach((p, pi) => {
|
||||
let i = 0;
|
||||
const subParts = [];
|
||||
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
|
||||
subParts.push({
|
||||
index: p.index + i,
|
||||
text: p.text.slice(i, i + match.index),
|
||||
});
|
||||
i += match.index + match[0].length;
|
||||
}
|
||||
if (i < p.text.length)
|
||||
subParts.push({
|
||||
index: p.index + i,
|
||||
text: p.text.slice(i, p.text.length),
|
||||
});
|
||||
parts.splice(pi, 1, ...subParts);
|
||||
});
|
||||
|
||||
parts.forEach(part => part.end = part.index + part.text.length);
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
const renderShowDown = (data, cb) => {
|
||||
const html = converter.makeHtml(data);
|
||||
cb(html);
|
||||
};
|
||||
|
||||
let Prism;
|
||||
if (config['modules']['prism'])
|
||||
Prism = require('node-prismjs');
|
||||
|
||||
const renderPrism = (data, cb) => {
|
||||
if (!config['modules']['prism'])
|
||||
return cb(data);
|
||||
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
|
||||
let match;
|
||||
while ((match = codeRegex.exec(data))) {
|
||||
const lang = match[1].trim();
|
||||
const code = match[2].trim();
|
||||
const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang);
|
||||
data = data.slice(0, match.index) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
|
||||
}
|
||||
cb(data);
|
||||
};
|
||||
|
||||
if (config['modules']['plantuml']) {
|
||||
require('./script_loader')(path.join(__dirname, 'lib', 'plantuml_synchro.js'));
|
||||
}
|
||||
|
||||
const renderPlantUML = (data, cb) => {
|
||||
if (!config['modules']['plantuml'])
|
||||
return cb(data);
|
||||
const parts = getParts(data);
|
||||
const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m;
|
||||
let match;
|
||||
parts.forEach(part => {
|
||||
while ((match = umlRegex.exec(part.text))) {
|
||||
const code = match[1].trim();
|
||||
const s = unescape(encodeURIComponent(code)); // jshint ignore:line
|
||||
const compressed = global['zip_deflate'](s);
|
||||
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;// jshint ignore:line
|
||||
part.text = part.text.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
|
||||
}
|
||||
data = data.slice(0, part.index) + part.text + data.slice(part.end);
|
||||
});
|
||||
cb(data);
|
||||
};
|
||||
|
||||
let mjAPI;
|
||||
if (config['modules']['mathjax']) {
|
||||
mjAPI = require('mathjax-node');
|
||||
mjAPI.config({
|
||||
MathJax: {
|
||||
tex2jax: {
|
||||
inlineMath: [['$', '$']],
|
||||
displayMath: [['$$', '$$']]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const renderMathJax = (data, cb) => {
|
||||
if (!config['modules']['mathjax'])
|
||||
return cb(data);
|
||||
|
||||
const parts = getParts(data);
|
||||
|
||||
const doMJ = (match, format, i) => {
|
||||
const eq = match[1].trim();
|
||||
const output = config['mathjax']['output_format'];
|
||||
const mjConf = {
|
||||
math: eq,
|
||||
format: format,
|
||||
speakText: config['mathjax']['speak_text']
|
||||
};
|
||||
mjConf[output] = true;
|
||||
mjAPI.typeset(mjConf, (res) => {
|
||||
data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length);
|
||||
renderMathJax(data, (data2) => {
|
||||
cb(data2);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
|
||||
const inlineEqRegex = /\$([^$\n]*)\$/;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let match;
|
||||
if ((match = eqRegex.exec(parts[i].text))) {
|
||||
return doMJ(match, 'TeX', i);
|
||||
} else if ((match = inlineEqRegex.exec(parts[i].text))) {
|
||||
return doMJ(match, 'inline-TeX', i);
|
||||
}
|
||||
}
|
||||
cb(data);
|
||||
};
|
||||
|
||||
let faDiagrams;
|
||||
let yaml;
|
||||
if (config['modules']['fa-diagrams']) {
|
||||
faDiagrams = require('fa-diagrams');
|
||||
yaml = require('js-yaml');
|
||||
}
|
||||
|
||||
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 = yaml.safeLoad(code);
|
||||
const findLineBreaks = (data) => {
|
||||
Object.keys(data).forEach(key => {
|
||||
if (typeof data[key] === 'object')
|
||||
findLineBreaks(data[key]);
|
||||
else if (typeof data[key] === 'string')
|
||||
data[key] = data[key].replace(/\\n/gm, '\n');
|
||||
});
|
||||
};
|
||||
findLineBreaks(diagData);
|
||||
output = faDiagrams.compute(diagData);
|
||||
} catch (err) {
|
||||
output = `<b style="color:red">${err.toString()}</b>`;
|
||||
}
|
||||
part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length);
|
||||
}
|
||||
data = data.slice(0, part.index) + part.text + data.slice(part.end);
|
||||
});
|
||||
cb(data);
|
||||
};
|
||||
|
||||
return {
|
||||
getParts: config['test'] ? getParts : undefined,
|
||||
renderShowDown: config['test'] ? renderShowDown : undefined,
|
||||
renderPrism: config['test'] ? renderPrism : undefined,
|
||||
renderPlantUML: config['test'] ? renderPlantUML : undefined,
|
||||
renderMathJax: config['test'] ? renderMathJax : undefined,
|
||||
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
|
||||
render: (file, cb) => {
|
||||
fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
|
||||
if (config['modules']['prism']) {
|
||||
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
|
||||
let match;
|
||||
while ((match = codeRegex.exec(data))) {
|
||||
const lang = match[1].trim();
|
||||
const code = match[2].trim();
|
||||
try {
|
||||
const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang);
|
||||
data = data.slice(0, match.index) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const html = converter.makeHtml(data);
|
||||
|
||||
|
||||
cb(null, html);
|
||||
renderPlantUML(data, (data) => {
|
||||
renderFaDiagrams(data, (data) => {
|
||||
renderMathJax(data, (data) => {
|
||||
renderPrism(data, (data) => {
|
||||
renderShowDown(data, (html) => {
|
||||
cb(null, html);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Import client-side script into the "global" var
|
||||
* @param scriptPath
|
||||
*/
|
||||
module.exports = (scriptPath) => {
|
||||
eval.call(global, fs.readFileSync(scriptPath, {encoding: 'UTF-8'}));
|
||||
};
|
||||
|
||||
+119
-44
@@ -13,19 +13,23 @@ const config = require('../src/config')();
|
||||
|
||||
config['test'] = true;
|
||||
config['data_dir'] = dataDir;
|
||||
config['home']['index'] = testIndex;
|
||||
config['home']['error'] = testError;
|
||||
config['article']['template'] = testTemplate;
|
||||
config['home']['hidden'].push('.test');
|
||||
config['webhook']['endpoint'] = '/webhooktest';
|
||||
config['rss']['endpoint'] = '/rsstest';
|
||||
config['rss']['length'] = 2;
|
||||
config['webhook']['endpoint'] = '/webhooktest';
|
||||
config['access_log'] = path.join(dataDir, 'access.log');
|
||||
config['error_log'] = path.join(dataDir, 'error.log');
|
||||
config['home']['error'] = testError;
|
||||
config['article']['template'] = testTemplate;
|
||||
|
||||
const app = require('../src/app')(config);
|
||||
|
||||
beforeEach((done, fail) => {
|
||||
config['home']['index'] = testIndex;
|
||||
config['data_dir'] = dataDir;
|
||||
config['article']['index'] = 'index.md';
|
||||
config['access_log'] = '';
|
||||
config['error_log'] = '';
|
||||
config['modules']['rss'] = true;
|
||||
config['modules']['webhook'] = true;
|
||||
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
app.reload(done, fail);
|
||||
@@ -37,17 +41,22 @@ afterAll(() => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('Test reload', () => {
|
||||
test('reload fail', (done, fail) => {
|
||||
config['data_dir'] = '';
|
||||
app.reload(fail, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test request logging', () => {
|
||||
test('test no log', (done) => {
|
||||
const tmp = config['access_log'];
|
||||
config['access_log'] = '';
|
||||
request(app).get('/rsstest').then(() => {
|
||||
config['access_log'] = tmp;
|
||||
expect(fs.existsSync(path.join(dataDir, 'access.log'))).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('test get 200', (done) => {
|
||||
config['access_log'] = path.join(dataDir, 'access.log');
|
||||
request(app).get('/rsstest').then(() => {
|
||||
fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
@@ -57,6 +66,7 @@ describe('Test request logging', () => {
|
||||
});
|
||||
});
|
||||
test('test post 400', (done) => {
|
||||
config['access_log'] = path.join(dataDir, 'access.log');
|
||||
request(app).post('/rsstest').then(() => {
|
||||
fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
@@ -66,6 +76,7 @@ describe('Test request logging', () => {
|
||||
});
|
||||
});
|
||||
test('test 2 requests', (done) => {
|
||||
config['access_log'] = path.join(dataDir, 'access.log');
|
||||
request(app).get('/rss').then(() => {
|
||||
request(app).post('/rsstest').then(() => {
|
||||
fs.readFile(path.join(dataDir, 'access.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
@@ -81,26 +92,20 @@ describe('Test request logging', () => {
|
||||
|
||||
describe('Test error logging', () => {
|
||||
test('test no log', (done) => {
|
||||
const tmp = config['home']['hidden'];
|
||||
config['home']['hidden'] = null;
|
||||
const tmp2 = config['errpr_log'];
|
||||
config['error_log'] = '';
|
||||
request(app).get('/somefile.txt').then(() => {
|
||||
config['home']['hidden'] = tmp;
|
||||
config['error_log'] = tmp2;
|
||||
config['home']['index'] = null;
|
||||
request(app).get('/').then(() => {
|
||||
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('test null error ', (done) => {
|
||||
const tmp = config['home']['hidden'];
|
||||
config['home']['hidden'] = null;
|
||||
request(app).get('/somefile.txt').then(() => {
|
||||
config['home']['hidden'] = tmp;
|
||||
config['home']['index'] = null;
|
||||
config['error_log'] = path.join(dataDir, 'error.log');
|
||||
request(app).get('/').then(() => {
|
||||
fs.readFile(path.join(dataDir, 'error.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
const start = data.split('\n').slice(0, 2).join('\n');
|
||||
const expected = '500 GET /somefile.txt ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError: Cannot read property \'includes\' of null';
|
||||
const expected = '500 GET / ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type object';
|
||||
expect(start).toBe(expected);
|
||||
done();
|
||||
});
|
||||
@@ -116,10 +121,34 @@ describe('Test root path', () => {
|
||||
});
|
||||
});
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404 at /');
|
||||
expect(response.text).toBe('error 404');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('500 render error', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('500 render error with page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
expect(response.text).toBe('error 500');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('500 render error with failing page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= null.error %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -131,14 +160,17 @@ describe('Test root path', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 2 articles', (done, fail) => {
|
||||
test('200 2 articles 1 drafted', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '05', '05')
|
||||
path.join(dataDir, '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, '2019', '05', '05', 'draft.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'draft.md'),
|
||||
path.join(dataDir, '2017', '05', '05', 'index.md'),
|
||||
]);
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
|
||||
app.reload(() => {
|
||||
@@ -156,18 +188,35 @@ describe('Test RSS feed', () => {
|
||||
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.type).toBe('application/rss+xml');
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 Mozilla fix', (done) => {
|
||||
request(app).get('/rsstest').set('user-agent', 'Mozilla Firefox 64.0').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('text/xml');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 rss cache', (done) => {
|
||||
request(app).get('/rsstest').then(() => {
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 2 rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
@@ -213,7 +262,6 @@ describe('Test webhook', () => {
|
||||
config['modules']['webhook'] = false;
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
config['modules']['webhook'] = true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -244,14 +292,6 @@ 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']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
@@ -282,6 +322,18 @@ describe('Test articles rendering', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('500 fail to render', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
|
||||
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- articl.content %><%- `<a href="${article.url}">reload</a>` %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/hello/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('500 no template', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
|
||||
@@ -306,6 +358,19 @@ describe('Test articles rendering', () => {
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 rendered draft', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'draft.md'), '# Hello');
|
||||
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/hello/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
utils.createEmptyFiles([
|
||||
@@ -344,24 +409,34 @@ describe('Test static files', () => {
|
||||
});
|
||||
});
|
||||
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) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404 at /somefile.txt');
|
||||
expect(response.text).toBe('error 404');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('404 hidden file', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, 'somefile.test'), '');
|
||||
request(app).get('/somefile.test').then((response) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, 'tmp')]);
|
||||
fs.writeFileSync(path.join(dataDir, 'tmp', 'somefile.ejs'), '');
|
||||
request(app).get('/tmp/somefile.ejs').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('404 hidden folder', (done) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '.git')]);
|
||||
fs.writeFileSync(path.join(dataDir, '.git', 'file.txt'), '');
|
||||
request(app).get('/.git/file.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 valid file', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, 'somefile.txt'), 'filecontent');
|
||||
request(app).get('/somefile.txt').then((response) => {
|
||||
fs.writeFileSync(path.join(dataDir, 'somefile.css'), 'filecontent');
|
||||
request(app).get('/somefile.css').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('text/css');
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -64,4 +64,18 @@ test('wrong config fixed', () => {
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data2');
|
||||
});
|
||||
|
||||
test('array parsing', () => {
|
||||
fs.writeFileSync(configFile, '{"home":{"hidden":["item1","item2"]}}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['home']['hidden']).toEqual(['item1', 'item2']);
|
||||
});
|
||||
|
||||
test('array fix', () => {
|
||||
fs.writeFileSync(configFile, '{"home":{"hidden":{}}}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['home']['hidden']).toEqual(['*.ejs', '/.git*']);
|
||||
});
|
||||
@@ -6,13 +6,14 @@ const utils = require('./test_utils');
|
||||
const dataDir = 'test_data';
|
||||
const testIndex = 'testindex.md';
|
||||
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g,'/');
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
|
||||
const config = {
|
||||
'test': true,
|
||||
'data_dir': dataDir,
|
||||
'article': {
|
||||
'index': testIndex,
|
||||
'draft': 'draft.md',
|
||||
'default_title': 'Untitled',
|
||||
'default_thumbnail': 'default.png',
|
||||
'thumbnail_tag': 'thumbnail'
|
||||
@@ -22,6 +23,7 @@ const config = {
|
||||
const fw = require('../src/file_walker')(config);
|
||||
|
||||
beforeEach(() => {
|
||||
config['data_dir'] = dataDir;
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
@@ -193,7 +195,6 @@ describe('Test article fetching', () => {
|
||||
fw.fetchArticles((err, list) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(list).not.toBeDefined();
|
||||
config['data_dir'] = dataDir;
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -235,9 +236,10 @@ describe('Test article fetching', () => {
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: dir,
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
draft: false,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Untitled',
|
||||
@@ -265,10 +267,11 @@ describe('Test article fetching', () => {
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: dir,
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
draft: false,
|
||||
date: date,
|
||||
title: 'Title with : info !',
|
||||
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
|
||||
@@ -278,5 +281,64 @@ describe('Test article fetching', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('correct draft file', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, 'draft.md');
|
||||
utils.createEmptyDirs([dir]);
|
||||
fs.writeFileSync(file, `
|
||||
# Title with : info !
|
||||

|
||||
this is some text
|
||||
`);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
draft: true,
|
||||
date: date,
|
||||
title: 'Title with : info !',
|
||||
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
|
||||
escapedTitle: 'title_with___info',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('index file override draft', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
const file2 = path.join(dir, 'draft.md');
|
||||
utils.createEmptyDirs([dir]);
|
||||
utils.createEmptyFiles([file, file2]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
draft: false,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Untitled',
|
||||
thumbnail: 'default.png',
|
||||
escapedTitle: 'untitled',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+235
-45
@@ -7,18 +7,33 @@ const dataDir = 'test_data';
|
||||
const file = path.join(dataDir, 'test.md');
|
||||
|
||||
const config = {
|
||||
'test': true,
|
||||
'modules': {
|
||||
'prism': true,
|
||||
'mathjax': true,
|
||||
'plantuml': true,
|
||||
'fa-diagrams': true,
|
||||
},
|
||||
'showdown': {
|
||||
'simplifiedAutoLink': true,
|
||||
'smartIndentationFix': true
|
||||
},
|
||||
'mathjax': {
|
||||
'output_format': 'html',
|
||||
'speak_text': false
|
||||
},
|
||||
'plantuml': {
|
||||
'output_format': 'svg'
|
||||
}
|
||||
};
|
||||
|
||||
const renderer = require('../src/renderer')(config);
|
||||
|
||||
beforeEach(() => {
|
||||
config['modules']['prism'] = true;
|
||||
config['modules']['mathjax'] = true;
|
||||
config['modules']['plantuml'] = true;
|
||||
config['modules']['fa-diagrams'] = true;
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
@@ -29,68 +44,243 @@ afterAll(() => {
|
||||
}
|
||||
});
|
||||
|
||||
test('invalid file', (done) => {
|
||||
renderer.render('invalid file', (err, html) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(html).not.toBeDefined();
|
||||
done();
|
||||
describe('get parts', () => {
|
||||
test('normal', () => {
|
||||
const data = 'Hello\nthere\ngeneral\nkenobi';
|
||||
const parts = renderer.getParts(data);
|
||||
expect(parts.map(p => p.text)).toEqual([
|
||||
'Hello\nthere\ngeneral\nkenobi'
|
||||
]);
|
||||
});
|
||||
test('lot of stuff', () => {
|
||||
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
|
||||
const parts = renderer.getParts(data);
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
end: 12,
|
||||
text: 'Hello\nthere\n'
|
||||
},
|
||||
{
|
||||
index: 22,
|
||||
end: 30,
|
||||
text: '\ngeneral'
|
||||
},
|
||||
{
|
||||
index: 53,
|
||||
end: 54,
|
||||
text: '\n'
|
||||
},
|
||||
{
|
||||
index: 78,
|
||||
end: 79,
|
||||
text: '\n'
|
||||
},
|
||||
{
|
||||
index: 109,
|
||||
end: 115,
|
||||
text: 'kenobi'
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('normal file', (done) => {
|
||||
fs.writeFileSync(file, `# Hello`);
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<h1 id="hello">Hello</h1>');
|
||||
done();
|
||||
describe('Test Showdown', () => {
|
||||
test('normal', (done) => {
|
||||
renderer.renderShowDown('# Hello', (html) => {
|
||||
expect(html).toBe('<h1 id="hello">Hello</h1>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('custom rules', (done) => {
|
||||
renderer.renderShowDown('www.google.com', (html) => {
|
||||
expect(html).toBe('<p><a href="http://www.google.com">www.google.com</a></p>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('code format', (done) => {
|
||||
renderer.renderShowDown('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (html) => {
|
||||
expect(html).toBe('<pre><code class="python language-python">print("hello")\n</code></pre>\n<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('custom rules', (done) => {
|
||||
fs.writeFileSync(file, `www.google.com`);
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<p><a href="http://www.google.com">www.google.com</a></p>');
|
||||
done();
|
||||
describe('Test Prism', () => {
|
||||
test('no prism', (done) => {
|
||||
config['modules']['prism'] = false;
|
||||
renderer.renderPrism('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```', (data) => {
|
||||
expect(data).toBe('```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('prism correct', (done) => {
|
||||
renderer.renderPrism('```python\nprint("hello")\n```', (data) => {
|
||||
expect(data).not.toBe('<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
expect(data.indexOf('<pre><code class="python language-python">')).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('prism invalid lang', (done) => {
|
||||
renderer.renderPrism('```pythdon\nprint("hello")\n```', (data) => {
|
||||
expect(data).not.toBe('<pre><code class="pythdon language-pythdon">print("hello")\n</code></pre>');
|
||||
expect(data.indexOf('<pre><code class="pythdon language-pythdon">')).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('prism mutliple code blocks', (done) => {
|
||||
renderer.renderPrism('```python\n\n```\n```python\n\n```', (data) => {
|
||||
expect(data).toBe('<pre><code class="python language-python"></code></pre>\n<pre><code class="python language-python"></code></pre>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('no prism', (done) => {
|
||||
config['modules']['prism'] = false;
|
||||
fs.writeFileSync(file, '```python\nprint("hello")\n```\n\n```python\nprint("hello")\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<pre><code class="python language-python">print("hello")\n</code></pre>\n<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
config['modules']['prism'] = true;
|
||||
done();
|
||||
describe('Test PlantUML', () => {
|
||||
test('no plantuml', (done) => {
|
||||
config['modules']['plantuml'] = false;
|
||||
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
|
||||
expect(data).toBe('@startuml\nBob -> Alice : hello\n@enduml');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('plantuml correct', (done) => {
|
||||
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml', (data) => {
|
||||
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('plantuml ignored in code', (done) => {
|
||||
renderer.renderPlantUML('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml', (data) => {
|
||||
expect(data).toBe('code:\n```@startuml\nBob -> Alice : hello\n@enduml```\n ```@startuml``` @enduml');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('plantuml multiple uml', (done) => {
|
||||
renderer.renderPlantUML('@startuml\nBob -> Alice : hello\n@enduml\n@startuml\nBob -> Alice : hello\n@enduml', (data) => {
|
||||
expect(data).toBe('<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">\n<img alt="generated PlantUML diagram" src="http://www.plantuml.com/plantuml/svg/SyfFKj2rKt3CoKnELR1Io4ZDoSa70000">');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prism correct', (done) => {
|
||||
fs.writeFileSync(file, '```python\nprint("hello")\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).not.toBe('<pre><code class="python language-python">print("hello")\n</code></pre>');
|
||||
expect(html.indexOf('<pre><code class="python language-python">')).toBe(0);
|
||||
done();
|
||||
|
||||
describe('Test MathJax', () => {
|
||||
test('no mathjax', (done) => {
|
||||
config['modules']['mathjax'] = false;
|
||||
renderer.renderMathJax('$$\nhello\n$$\ntest$test$', (data) => {
|
||||
expect(data).toBe('$$\nhello\n$$\ntest$test$');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('full eq', (done) => {
|
||||
renderer.renderMathJax('$$\n\nA\n\n$$', (data) => {
|
||||
expect(data).toBe('<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
|
||||
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
|
||||
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
|
||||
'A' +
|
||||
'</span></span></span></span></span>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('inline eq', (done) => {
|
||||
renderer.renderMathJax('start $a$ end', (data) => {
|
||||
expect(data).toBe('start ' +
|
||||
'<span class=\"mjx-chtml\">' +
|
||||
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
|
||||
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.225em; padding-bottom: 0.298em;\">' +
|
||||
'a' +
|
||||
'</span></span></span></span></span>' +
|
||||
' end');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('fake inline eq', (done) => {
|
||||
renderer.renderMathJax('i have $6\nyou have $5', (data) => {
|
||||
expect(data).toBe('i have $6\nyou have $5');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('no eq in code / script', (done) => {
|
||||
renderer.renderMathJax('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```', (data) => {
|
||||
expect(data).toBe('this code is ```start $a$ end $$hello$$``` beautiful <script>$A$</script>\n```$no eq$```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('multiple eq', (done) => {
|
||||
renderer.renderMathJax('$$\n\nA\n\n$$\nstart $a$ end\n$$\n\nA\n\n$$', (data) => {
|
||||
expect(data).toBe('' +
|
||||
'<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
|
||||
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
|
||||
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
|
||||
'A' +
|
||||
'</span></span></span></span></span>\n' +
|
||||
'start ' +
|
||||
'<span class=\"mjx-chtml\">' +
|
||||
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
|
||||
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.225em; padding-bottom: 0.298em;\">' +
|
||||
'a' +
|
||||
'</span></span></span></span></span>' +
|
||||
' end\n' +
|
||||
'<span class=\"mjx-chtml MJXc-display\" style=\"text-align: center;\">' +
|
||||
'<span class=\"mjx-math\"><span class=\"mjx-mrow\"><span class=\"mjx-mi\">' +
|
||||
'<span class=\"mjx-char MJXc-TeX-math-I\" style=\"padding-top: 0.519em; padding-bottom: 0.298em;\">' +
|
||||
'A' +
|
||||
'</span></span></span></span></span>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prism invalid lang', (done) => {
|
||||
fs.writeFileSync(file, '```pythdon\nprint("hello")\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).not.toBe('<pre><code class="pythdon language-pythdon">print("hello")\n</code></pre>');
|
||||
expect(html.indexOf('<pre><code class="pythdon language-pythdon">')).toBe(0);
|
||||
done();
|
||||
describe('Test fa-diagrams', () => {
|
||||
test('no fa-diagrams', (done) => {
|
||||
config['modules']['fa-diagrams'] = false;
|
||||
renderer.renderFaDiagrams('@startfad\noptions:\n\trendering:\t\tcolor:red\n\n@endfad', (data) => {
|
||||
expect(data).toBe('@startfad\noptions:\n\trendering:\t\tcolor:red\n\n@endfad');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('no fa-diagrams in code', (done) => {
|
||||
renderer.renderFaDiagrams('code:\n```\n@startfad\noptions:\n\trendering:\t\tcolor:red\n\n@endfad\n```', (data) => {
|
||||
expect(data).toBe('code:\n```\n@startfad\noptions:\n\trendering:\t\tcolor:red\n\n@endfad\n```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('valid fa-diagrams', (done) => {
|
||||
renderer.renderFaDiagrams('before\n@startfad\noptions:\n rendering:\n 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 yaml', (done) => {
|
||||
renderer.renderFaDiagrams('before\n@startfad\noptions:\n@endfad\nafter', (data) => {
|
||||
expect(data).toBe('before\n<b style="color:red">TypeError: Cannot convert undefined or null to object</b>\nafter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('prism mutliple code blocks', (done) => {
|
||||
fs.writeFileSync(file, '```python\n\n```\n\n```python\n\n```');
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<pre><code class="python language-python"></code></pre>\n<pre><code class="python language-python"></code></pre>');
|
||||
done();
|
||||
describe('Test render', () => {
|
||||
test('invalid file', (done) => {
|
||||
renderer.render('invalid file', (err, html) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(html).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('normal file', (done) => {
|
||||
fs.writeFileSync(file, `# Hello`);
|
||||
renderer.render(file, (err, html) => {
|
||||
expect(err).toBeNull();
|
||||
expect(html).toBe('<h1 id="hello">Hello</h1>');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
const dataDir = 'test_data';
|
||||
|
||||
beforeEach(() => {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('load 1 script', () => {
|
||||
const file = path.join(dataDir, 'test.js');
|
||||
fs.writeFileSync(file, `
|
||||
var a = 5;
|
||||
function b(){
|
||||
return a;
|
||||
}`);
|
||||
require('../src/script_loader')(file);
|
||||
expect(global['b']).toBeDefined();
|
||||
expect(global['b']()).toBe(5);
|
||||
});
|
||||
|
||||
test('load 2 script', () => {
|
||||
const file1 = path.join(dataDir, 'test.js');
|
||||
fs.writeFileSync(file1, `
|
||||
var a = 5;
|
||||
function b(){
|
||||
return a;
|
||||
}`);
|
||||
const file2 = path.join(dataDir, 'test2.js');
|
||||
fs.writeFileSync(file2, `
|
||||
var a = 9;
|
||||
function b(){
|
||||
return a;
|
||||
}`);
|
||||
require('../src/script_loader')(file1);
|
||||
expect(global['b']).toBeDefined();
|
||||
expect(global['b']()).toBe(5);
|
||||
|
||||
require('../src/script_loader.js')(file2);
|
||||
expect(global['b']).toBeDefined();
|
||||
expect(global['b']()).toBe(9);
|
||||
});
|
||||
Reference in New Issue
Block a user