Merge branch 'dev'
This commit is contained in:
+7
-1
@@ -1,4 +1,10 @@
|
||||
/.idea
|
||||
/node_modules
|
||||
/config.json
|
||||
/data
|
||||
/config.example.json
|
||||
/data
|
||||
/data/*
|
||||
/test_data
|
||||
/access.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:
|
||||
- npm test
|
||||
- jest --coverage --silent
|
||||
- jshint ./src
|
||||
- cat ./coverage/lcov.info | coveralls
|
||||
|
||||
@@ -1,87 +1,333 @@
|
||||
# GitBlog.md (WIP)
|
||||
> This is a work in progress, some information written here might not be true yet.
|
||||
|
||||
[](https://travis-ci.org/Klemek/GitBlog.md)
|
||||
[](https://coveralls.io/github/Klemek/GitBlog.md?branch=master)
|
||||
[](https://lgtm.com/projects/g/Klemek/GitBlog.md/context:javascript)
|
||||
[](https://lgtm.com/projects/g/Klemek/GitBlog.md/alerts/)
|
||||
|
||||
# GitBlog.md
|
||||
|
||||
A static blog using Markdown pulled from your git repository.
|
||||
|
||||
## Flow
|
||||
> Step 1 : ```$ vi 2019/06/21/index.md```
|
||||
> Step 2 : ```$ git add . && git commit -m "new article" && git push origin master```
|
||||
> Step 3 : That's it
|
||||
> Step 4 : No seriously you're done, go check your updated blog
|
||||
|
||||
* **[How it works](#how-it-works)**
|
||||
* **[Installation](#installation)**
|
||||
* **[Writing an article](#writing-an-article)**
|
||||
* **[Modules](#modules)**
|
||||
* **[Configuration](#configuration)**
|
||||
|
||||
## How it works
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
There are 4 majors features of this project :
|
||||
|
||||
#### 1. Home page
|
||||
|
||||
<details>
|
||||
<summary>diagram (click)</summary>
|
||||
<p>
|
||||
|
||||

|
||||
</p>
|
||||
</details>
|
||||
|
||||
When you access the root url of your blog, the app will fetch the template and inject the list of currently available articles.
|
||||
|
||||
|
||||
#### 2. Article page
|
||||
|
||||
<details>
|
||||
<summary>diagram (click)</summary>
|
||||
<p>
|
||||
|
||||

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

|
||||
</p>
|
||||
</details>
|
||||
|
||||
As you configured your data repository, when you push any data, it will trigger the webhook that will perform a `git pull` then refresh the article list.
|
||||
|
||||
|
||||
#### 4. RSS feed
|
||||
|
||||
<details>
|
||||
<summary>diagram (click)</summary>
|
||||
<p>
|
||||
|
||||

|
||||
</p>
|
||||
</details>
|
||||
|
||||
On the `/rss` endpoint, the servers gives you a RSS feed based on the list of articles which you can bookmark.
|
||||
|
||||
|
||||
|
||||
## Installation
|
||||
**1. Download and install the latest version from the repo**
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
#### 1. Download and install the latest version from the repo
|
||||
```bash
|
||||
git clone https://github.com/klemek/gitblog.md.git
|
||||
npm install
|
||||
npm install --production
|
||||
```
|
||||
**2. Create your config file**
|
||||
#### 2. Create your config file
|
||||
```bash
|
||||
cd gitblog.md
|
||||
cp config.example.json config.json
|
||||
```
|
||||
then edit the config.json file with your values :
|
||||
> default values for config.json
|
||||
````json
|
||||
{
|
||||
"nodePort": 3000,
|
||||
"dataDir": "data",
|
||||
"modules" : {
|
||||
"plantuml" : false,
|
||||
"rss": true,
|
||||
"webhook": true
|
||||
},
|
||||
"home" : {
|
||||
"index" : "index.ejs"
|
||||
},
|
||||
"article" : {
|
||||
"index" : "index.md"
|
||||
},
|
||||
"rss" : {
|
||||
"endpoint" : "/rss",
|
||||
"length" : 10
|
||||
},
|
||||
"webhook" : {
|
||||
"endpoint": "/webhook",
|
||||
"secretFile": "git_secret"
|
||||
}
|
||||
}
|
||||
````
|
||||
then edit the config.json file with your custom values.
|
||||
For example, you might want to change the app's port with :
|
||||
|
||||
**3. Start your server**
|
||||
```json
|
||||
{
|
||||
"node_port": 3030
|
||||
}
|
||||
```
|
||||
|
||||
See [Configuration](#configuration) for more info.
|
||||
|
||||
#### 3. Start your server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
npm run
|
||||
#or
|
||||
node src/server.js
|
||||
```
|
||||
|
||||
You can check that it's up and running at [http://localhost:3000/](http://localhost:3000/)
|
||||
|
||||
You might want to use something like screen to separate the process from your current terminal session.
|
||||
|
||||
**4. Create and init your git source**
|
||||
#### 4. Customize the blog's style
|
||||
|
||||
At `npm install` a first article will be created for the current date.
|
||||
You can see it as an example of rendering of your blog.
|
||||
Use it to edit your templates and styles located on the `data` folder.
|
||||
|
||||
At first, home page and articles are rendered using EJS engine but you can customize that into the configuration.
|
||||
|
||||
Resources are located on the `data` folder and can be referenced as the root of your blog.
|
||||
|
||||
```
|
||||
/styles/main.css => data/styles/main.css
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
**5. Refresh content with a webhook (optional)**
|
||||
Now you just have to edit a local copy of your articles and, when you push them, to perform a simple `git pull` on that data folder.
|
||||
|
||||
At first start, a `git_secret` file will be generated, use it to create a new webhook as following :
|
||||
#### 6. Refresh content with a webhook (optional)
|
||||
|
||||
Create a webhook on your git source (On GitHub, in the `Settings/Webhooks` part of the repository.) with the following parameters :
|
||||
|
||||
* Payload URL : `https://<url_of_your_server>/webhook`
|
||||
* Content type : `application/json`
|
||||
* Secret : `<content of the git_secret file>`
|
||||
* Events : Just the push event
|
||||
|
||||
On GitHub, webhooks can be created in the `Settings/Webhooks` part of the repository.
|
||||
Now the server will perform the `git pull` task for you after a successful push on GitHub.
|
||||
|
||||
#### 7. Securize your webhook (optional)
|
||||
|
||||
Here are the steps for Github, if you use another platform adapt it your way (header format on the config) :
|
||||
|
||||
* Create a password or random secret
|
||||
* Edit your configuration to add webhook info
|
||||
```json
|
||||
"webhook": {
|
||||
"endpoint": "/webhook",
|
||||
"secret": "<value>",
|
||||
"signature_header": "X-Hub-Signature"
|
||||
},
|
||||
```
|
||||
* Launch the server
|
||||
* 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)
|
||||
|
||||
You need to write your article (and templates) on the git repository but **keep the data directory on the server untouched** to prevent any changes to harm the git pull normal behavior.
|
||||
|
||||
To be referenced, an article need to be on a specific path containing its date and have a Markdown index file :
|
||||
|
||||
```
|
||||
data/year/month/day/index.md
|
||||
```
|
||||
|
||||
> note that month and day need to be 0 padded (`5th of june 2019 => 2019/06/05`)
|
||||
|
||||
On your Markdown file you can write anything but some informations will be fetched automatically :
|
||||
|
||||
* Title : first level 1 header (#)
|
||||
* Thumbnail : first thumbnail tagged image (like ``)
|
||||
|
||||
On that same folder, you can place resources like images and reference them in relative paths :
|
||||
|
||||
```
|
||||
 => data/year/month/day/image.png
|
||||
```
|
||||
|
||||
> note that you cannot place resources on subfolders
|
||||
|
||||
Any URL like `/year/month/day/anything/` will redirect to this article (and link to correct resources)
|
||||
|
||||
## Modules
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
* **RSS**
|
||||
It allows your users to use the feed to be updated as soon as a new article is out
|
||||
* **Webhook**
|
||||
It update your blog from your online repo when it's updated
|
||||
* **Prism**
|
||||
It highlight code blocks to be more readable (more info [here](https://prismjs.com/), you will need the corresponding CSS file on your templates)
|
||||
* **MathJax**
|
||||
It allows you to add math equations to your articles by simply writing LaTeX between `$$` for full size (and between $ for inline) (more info [here](https://www.mathjax.org/))
|
||||
* **PlantUML**
|
||||
It allows you to add UML diagrams with PlantUML Syntax between `@startuml` and `@enduml` (more info [here](http://www.plantuml.com))
|
||||
* **fa-diagrams**
|
||||
It allows you to define SVG diagrams with Font-Awesome icons in [TOML](https://github.com/toml-lang/toml) between `@startfad` and `@endfad` (more info [here](https://github.com/Klemek/fa-diagrams))
|
||||
|
||||
|
||||
## Configuration
|
||||
[back to top](#gitblog-md)
|
||||
|
||||
* `node_port` (default: 3000)
|
||||
the port the server is listening to
|
||||
* `host` (default: none)
|
||||
if set (like `https://mywebsite.com`, it will be used as reference for creating links
|
||||
by default, host is guessed based on first request
|
||||
* `data_dir` (default: data)
|
||||
the directory where will be located the git repo with templates and articles
|
||||
* `view_engine` (default: ejs)
|
||||
the Express view engine used to render pages from templates
|
||||
* `rate_limit` (default: 100)
|
||||
number of requests allowed in a time-frame of 15 minutes
|
||||
* `access_log` (default: access.log)
|
||||
log file where to save access requests (empty to disable)
|
||||
* `error_log` (default: error.log)
|
||||
log file where to save all server errors (empty to disable)
|
||||
* `modules`
|
||||
* `rss` (default: true)
|
||||
activate the RSS endpoint and its features
|
||||
* `webhook` (default: true)
|
||||
activate the webhook endpoint and its features
|
||||
* `prism` (default: true)
|
||||
activate Prism code highlighting
|
||||
* `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,/.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)
|
||||
the alt text searched to get the thumbnail image on the article
|
||||
as in ``
|
||||
* `default_title`: (default: Untitled)
|
||||
the title of the article in case a level 1 title was not found
|
||||
* `default_thumbnail`: (default: none)
|
||||
the path of the default thumbnail to get from the data directory
|
||||
* `rss`
|
||||
* `title`: (default: mygitblog RSS feed)
|
||||
* `description`: (default: a generated RSS feed from my articles)
|
||||
* `endpoint`: (default: /rss)
|
||||
* `length`: (default: 10)
|
||||
how many last articles will be present in the feed
|
||||
* `webhook`
|
||||
* `endpoint`: (default: /webhook)
|
||||
* `secret`: (default: none)
|
||||
see [above](#7-securize-your-webhook-optional-)
|
||||
* `signature_header`: (default: none)
|
||||
see [above](#7-securize-your-webhook-optional-)
|
||||
* `pull_command`: (default: git pull)
|
||||
the command used by the server on webhook trigger
|
||||
* `showdown`
|
||||
Options to be applied to Showdown renderer (see [showdown options](https://github.com/showdownjs/showdown/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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"nodePort": 3000,
|
||||
"dataDir": "data",
|
||||
"modules" : {
|
||||
"plantuml" : false,
|
||||
"rss": true,
|
||||
"webhook": true
|
||||
},
|
||||
"home" : {
|
||||
"index" : "index.ejs"
|
||||
},
|
||||
"article" : {
|
||||
"index" : "index.md"
|
||||
},
|
||||
"rss" : {
|
||||
"endpoint" : "/rss",
|
||||
"length" : 10
|
||||
},
|
||||
"webhook" : {
|
||||
"endpoint": "/webhook",
|
||||
"secretFile": "git_secret"
|
||||
}
|
||||
}
|
||||
Generated
+1336
-5088
File diff suppressed because it is too large
Load Diff
+35
-7
@@ -1,23 +1,31 @@
|
||||
{
|
||||
"name": "gitblog.md",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.8",
|
||||
"description": "A static blog using Markdown pulled from your git repository.",
|
||||
"main": "src/server.js",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"ejs": "^2.6.2",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.0.0",
|
||||
"fa-diagrams": "^1.0.3",
|
||||
"mathjax-node": "^2.1.1",
|
||||
"ncp": "^2.0.0",
|
||||
"showdown": "^1.9.0",
|
||||
"xml": "^1.0.1"
|
||||
"node-prismjs": "^0.1.2",
|
||||
"prismjs": "^1.16.0",
|
||||
"rss": "^1.2.2",
|
||||
"showdown": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"coveralls": "^3.0.4",
|
||||
"jest": "^24.8.0",
|
||||
"superagent": "^5.1.0",
|
||||
"supertest": "^4.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"start": "node src/server.js",
|
||||
"test": "jest --silent",
|
||||
"install": "node src/postinstall.js"
|
||||
},
|
||||
"repository": {
|
||||
@@ -29,5 +37,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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
# Welcome to your new blog
|
||||
|
||||
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
|
||||
### H3
|
||||
#### H4
|
||||
##### H5
|
||||
###### H6
|
||||
|
||||
Alternatively, for H1 and H2, an underline-ish style:
|
||||
|
||||
Alt-H1
|
||||
======
|
||||
|
||||
Alt-H2
|
||||
------
|
||||
|
||||
### Emphasis
|
||||
[Back to top](#top)
|
||||
|
||||
Emphasis, aka italics, with *asterisks* or _underscores_.
|
||||
|
||||
Strong emphasis, aka bold, with **asterisks** or __underscores__.
|
||||
|
||||
Combined emphasis with **asterisks and _underscores_**.
|
||||
|
||||
Strikethrough uses two tildes. ~~Scratch this.~~
|
||||
|
||||
### Lists
|
||||
[Back to top](#top)
|
||||
|
||||
1. First ordered list item
|
||||
2. Another item
|
||||
* Unordered sub-list.
|
||||
1. Actual numbers don't matter, just that it's a number
|
||||
1. Ordered sub-list
|
||||
4. And another item.
|
||||
|
||||
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
|
||||
|
||||
To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅
|
||||
Note that this line is separate, but within the same paragraph.⋅⋅
|
||||
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
|
||||
|
||||
* Unordered list can use asterisks
|
||||
- Or minuses
|
||||
+ Or pluses
|
||||
|
||||
### Links
|
||||
[Back to top](#top)
|
||||
|
||||
[I'm an inline-style link](https://www.google.com)
|
||||
|
||||
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||
|
||||
[I'm a reference-style link][Arbitrary case-insensitive reference text]
|
||||
|
||||
[I'm a relative reference to a repository file](../blob/master/LICENSE)
|
||||
|
||||
[You can use numbers for reference-style link definitions][1]
|
||||
|
||||
Or leave it empty and use the [link text itself].
|
||||
|
||||
URLs and URLs in angle brackets will automatically get turned into links.
|
||||
http://www.example.com or <http://www.example.com> and sometimes
|
||||
example.com (but not on Github, for example).
|
||||
|
||||
Some text to show that the reference links can follow later.
|
||||
|
||||
[arbitrary case-insensitive reference text]: https://www.mozilla.org
|
||||
[1]: http://slashdot.org
|
||||
[link text itself]: http://www.reddit.com
|
||||
|
||||
### Images
|
||||
[Back to top](#top)
|
||||
|
||||
Here's our logo (hover to see the title text):
|
||||
|
||||
Inline-style:
|
||||

|
||||
|
||||
Reference-style:
|
||||
![alt text][logo]
|
||||
|
||||
[logo]: https://github.com/adam-p/markdown-here/raw/master/src/common/images/icon48.png "Logo Title Text 2"
|
||||
|
||||
|
||||
### Code and Syntax Highlighting
|
||||
[Back to top](#top)
|
||||
|
||||
Inline `code` has `back-ticks around` it.
|
||||
|
||||
```javascript
|
||||
var s = "JavaScript syntax highlighting";
|
||||
alert(s);
|
||||
```
|
||||
|
||||
```python
|
||||
s = "Python syntax highlighting"
|
||||
print s
|
||||
```
|
||||
|
||||
```
|
||||
No language indicated, so no syntax highlighting.
|
||||
But let's throw in a <b>tag</b>.
|
||||
```
|
||||
|
||||
|
||||
### Tables
|
||||
[Back to top](#top)
|
||||
|
||||
Colons can be used to align columns.
|
||||
|
||||
| Tables | Are | Cool |
|
||||
| ------------- |:-------------:| -----:|
|
||||
| col 3 is | right-aligned | $1600 |
|
||||
| col 2 is | centered | $12 |
|
||||
| zebra stripes | are neat | $1 |
|
||||
|
||||
There must be at least 3 dashes separating each header cell.
|
||||
The outer pipes (|) are optional, and you don't need to make the
|
||||
raw Markdown line up prettily. You can also use inline Markdown.
|
||||
|
||||
Markdown | Less | Pretty
|
||||
--- | --- | ---
|
||||
*Still* | `renders` | **nicely**
|
||||
1 | 2 | 3
|
||||
|
||||
### Blockquotes
|
||||
[Back to top](#top)
|
||||
|
||||
> Blockquotes are very handy in email to emulate reply text.
|
||||
> This line is part of the same quote.
|
||||
|
||||
Quote break.
|
||||
|
||||
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
|
||||
|
||||
### Inline HTML
|
||||
[Back to top](#top)
|
||||
|
||||
<dl>
|
||||
<dt>Definition list</dt>
|
||||
<dd>Is something people use sometimes.</dd>
|
||||
|
||||
<dt>Markdown in HTML</dt>
|
||||
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
|
||||
</dl>
|
||||
|
||||
### Horizontal Rule
|
||||
[Back to top](#top)
|
||||
|
||||
Three or more...
|
||||
|
||||
---
|
||||
|
||||
Hyphens
|
||||
|
||||
***
|
||||
|
||||
Asterisks
|
||||
|
||||
___
|
||||
|
||||
Underscores
|
||||
|
||||
### Line Breaks
|
||||
[Back to top](#top)
|
||||
|
||||
Here's a line for us to start with.
|
||||
|
||||
This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
|
||||
|
||||
This line is also a separate paragraph, but...
|
||||
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
|
||||
|
||||
### Check Boxes
|
||||
[Back to top](#top)
|
||||
|
||||
* [x] Task completed
|
||||
* [] Task to do
|
||||
|
||||
### Spoilers
|
||||
[Back to top](#top)
|
||||
|
||||
<details><summary>Title of the spoiler (click)</summary><p>
|
||||
Content of the spoiler
|
||||
|
||||
On several lines
|
||||
</p></details>
|
||||
|
||||
### Math Equations
|
||||
[Back to top](#top)
|
||||
|
||||
You can use LaTeX equations with MathJax for full equations and inline ones (based on the number of $) :
|
||||
|
||||
$$
|
||||
\large{\beta=\sum_{i}^{\alpha }\frac{x^{i}}{\alpha}}
|
||||
$$
|
||||
|
||||
|
||||
Where $\alpha$ is cool
|
||||
|
||||
### UML
|
||||
[Back to top](#top)
|
||||
|
||||
You can use PlantUML diagrams with `@startuml` and `@enduml` tags :
|
||||
|
||||
@startuml
|
||||
title Article
|
||||
cloud web
|
||||
node nodejs {
|
||||
TCP -right- [express]
|
||||
[showdown]
|
||||
}
|
||||
package data {
|
||||
package "2019/06/18" {
|
||||
component index [
|
||||
index.md
|
||||
image.png
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
web -down-> TCP : 1. /2019/06/18/title
|
||||
express -down-> index : 2. fetch
|
||||
index -up-> showdown : 3. markdown
|
||||
showdown -left-> express : 4. html
|
||||
express -up-> web : 5. html
|
||||
@enduml
|
||||
|
||||
### Diagrams
|
||||
[Back to top](#top)
|
||||
|
||||
You can use [fa-diagrams](https://github.com/Klemek/fa-diagrams) with `@startfad` and `@endfad` tags and using [TOML](https://github.com/toml-lang/toml) inside
|
||||
|
||||
@startfad
|
||||
[[nodes]]
|
||||
name = "node1"
|
||||
icon = "laptop-code"
|
||||
color = "#4E342E"
|
||||
bottom = "my app"
|
||||
|
||||
[[nodes]]
|
||||
name = "node2"
|
||||
icon = "globe"
|
||||
color = "#455A64"
|
||||
bottom = "world"
|
||||
|
||||
[[links]]
|
||||
from = "node1"
|
||||
to = "node2"
|
||||
color = "#333333"
|
||||
bottom = '"hello"'
|
||||
|
||||
[links.top]
|
||||
icon = "envelope"
|
||||
@endfad
|
||||
|
||||
### Youtube Videos
|
||||
[Back to top](#top)
|
||||
|
||||
Just use the "embedded" export on Youtube with dimensions of 535x300 for best results
|
||||
|
||||
<iframe width="535" height="300" src="https://www.youtube.com/embed/FTQbiNvZqaY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 898 KiB |
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - Error <%= error %></title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Somehing went wrong
|
||||
<small>(Error <%= error %>)</small>
|
||||
</h1>
|
||||
It means the resource you're trying to access is unavailable right now.<br>
|
||||
We're terribly sorry that you encountered this error.<br><br>
|
||||
<a href="/">Back to home</a>
|
||||
<%- include('footer'); %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<hr>
|
||||
<footer>
|
||||
<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>
|
||||
@@ -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">
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('head'); %>
|
||||
<title><%= info.title %> - Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1 class="title"><%= info.title %></h1>
|
||||
<%= info.description %>
|
||||
<h2>Articles in this blog :</h2>
|
||||
<% articles.forEach((article) => { %>
|
||||
<div class="article">
|
||||
<%- `<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'); %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,143 @@
|
||||
/* PrismJS 1.16.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+apacheconf+apl+applescript+c+arff+asciidoc+asm6502+csharp+autohotkey+autoit+bash+basic+batch+bison+bnf+brainfuck+bro+cpp+aspnet+arduino+cil+coffeescript+cmake+clojure+ruby+csp+css-extras+d+dart+diff+markup-templating+docker+ebnf+eiffel+ejs+elixir+elm+erb+erlang+fsharp+flow+fortran+gcode+gedcom+gherkin+git+glsl+gml+go+graphql+groovy+less+handlebars+haskell+haxe+hcl+http+hpkp+hsts+ichigojam+icon+inform7+ini+io+j+java+scala+php+javastacktrace+jolie+jq+javadoclike+n4js+json+jsonp+json5+julia+keyman+kotlin+latex+markdown+liquid+lisp+livescript+lolcode+lua+makefile+crystal+django+matlab+mel+mizar+monkey+n1ql+typescript+nand2tetris-hdl+nasm+nginx+nim+nix+nsis+objectivec+ocaml+opencl+oz+parigp+parser+pascal+perl+jsdoc+phpdoc+php-extras+sql+powershell+processing+prolog+properties+protobuf+scss+puppet+pure+python+q+qore+r+js-extras+jsx+renpy+reason+vala+rest+rip+roboconf+textile+rust+sas+sass+stylus+javadoc+scheme+shell-session+smalltalk+smarty+plsql+soy+twig+swift+yaml+tcl+haml+toml+tt2+pug+tsx+t4-templating+visual-basic+t4-cs+regex+vbnet+velocity+verilog+vhdl+vim+t4-vb+wasm+wiki+xeora+xojo+xquery+tap */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 15px sans-serif;
|
||||
color: #111;
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #F5F5F5;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 45rem;
|
||||
padding: 2rem;
|
||||
margin: auto;
|
||||
background-color: #F0F0F0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* hide redundant text in article */
|
||||
#text h1:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #e1e4e8;
|
||||
border: 0;
|
||||
height: 0.25em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3C3CA1;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #8484C6;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-size: 96%;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
: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: #555;
|
||||
}
|
||||
|
||||
blockquote > p {
|
||||
margin: 0.6rem 0;
|
||||
}
|
||||
|
||||
table td {
|
||||
vertical-align: baseline;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
table td:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#text table td, #text table th {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
details {
|
||||
background: #f8f8f8;
|
||||
margin: 0.25em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
cursor: pointer;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
details > p {
|
||||
background: #f5f5f5;
|
||||
padding: 0.5em 0.5em 0.5em 2em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main.article div.header span.time span, div.article span.time span {
|
||||
color: #888;
|
||||
font-family: serif;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main.article div.header a.link-home {
|
||||
text-decoration: none;
|
||||
float: right;
|
||||
line-height: 2.4;
|
||||
}
|
||||
|
||||
main.article div.header h1, main.article div.header h2, .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 {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
main.article div.header span.time, div.article span.time {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.article {
|
||||
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;
|
||||
}
|
||||
|
||||
#text li, #text table, #text blockquote {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#text img, #text svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- 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><%= 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>
|
||||
<a href="#top">Go to top</a> - <a href="/">Back to home</a>
|
||||
<%- include('footer'); %>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
# Welcome to your new blog
|
||||
|
||||
## If you see this page, that means it's working
|
||||
|
||||

|
||||
|
||||
+288
-4
@@ -1,12 +1,296 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pjson = require('../package.json');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
module.exports = function(/*config*/){
|
||||
app.get('/', (req,res) => {
|
||||
res.status(200).send('Hello World!');
|
||||
app.enable('trust proxy');
|
||||
|
||||
//rss
|
||||
const Rss = require('rss');
|
||||
|
||||
///webhook
|
||||
const bodyParser = require('body-parser');
|
||||
const crypto = require('crypto');
|
||||
const cp = require('child_process');
|
||||
app.use(bodyParser.json());
|
||||
|
||||
/**
|
||||
* Terminal colors and symbols to display status messages
|
||||
* @type {{warn: string, ok: string, error: string}}
|
||||
*/
|
||||
const cons = {
|
||||
ok: '\x1b[32m✔\x1b[0m %s',
|
||||
warn: '\x1b[33m⚠\x1b[0m %s',
|
||||
error: '\x1b[31m✘\x1b[0m %s',
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// set view engine from configuration
|
||||
app.set('view engine', config['view_engine']);
|
||||
// reroute the views folder to the root folder
|
||||
app.set('views', path.join(__dirname, '..'));
|
||||
|
||||
const articles = {};
|
||||
let lastRSS = '';
|
||||
let host = config['host'];
|
||||
|
||||
reload = (success, error) => {
|
||||
fw.fetchArticles((err, dict) => {
|
||||
if (err) {
|
||||
console.error(cons.error, 'error loading articles : ' + err);
|
||||
return error ? error() : null;
|
||||
}
|
||||
Object.keys(articles).forEach((key) => delete articles[key]);
|
||||
Object.keys(dict).forEach((key) => articles[key] = dict[key]);
|
||||
const nb = Object.keys(articles).length;
|
||||
const dnb = Object.values(articles).filter(a => a.draft).length;
|
||||
if (nb > 0)
|
||||
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
|
||||
else
|
||||
console.log(cons.warn, `no articles loaded, check your configuration`);
|
||||
|
||||
lastRSS = '';
|
||||
|
||||
success();
|
||||
});
|
||||
};
|
||||
if (config['test'])
|
||||
app.reload = reload;
|
||||
|
||||
return app;
|
||||
render = (req, res, vPath, data, code = 200) => {
|
||||
data.info = {
|
||||
title: config['home']['title'],
|
||||
description: config['home']['description'],
|
||||
host: host,
|
||||
version: pjson.version,
|
||||
request: req,
|
||||
config: config
|
||||
};
|
||||
res.render(vPath, data, (err, html) => {
|
||||
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
|
||||
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
|
||||
showError(req, res, 500);
|
||||
} else if (err) {
|
||||
res.sendStatus(500);
|
||||
console.log(cons.error, `failed to render error page : ${err}`);
|
||||
} else
|
||||
res.status(code).send(html);
|
||||
});
|
||||
};
|
||||
|
||||
showError = (req, res, code) => {
|
||||
const errorPath = path.join(config['data_dir'], config['home']['error']);
|
||||
fs.access(errorPath, fs.constants.R_OK, (err) => {
|
||||
if (err)
|
||||
res.sendStatus(code);
|
||||
else
|
||||
render(req, res, errorPath, {error: code}, code);
|
||||
});
|
||||
};
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (!host) {
|
||||
host = 'http://' + req.headers.host;
|
||||
console.log(cons.ok, 'Currently hosted on ' + host);
|
||||
}
|
||||
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
|
||||
app.use((req, res, next) => {
|
||||
if (config['access_log']) {
|
||||
const end = res.end;
|
||||
res.end = (chunk, encoding) => {
|
||||
fs.appendFile(config['access_log'],
|
||||
`${res.statusCode} ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n`,
|
||||
{encoding: 'UTF-8'}, () => {
|
||||
res.end = end;
|
||||
res.end(chunk, encoding);
|
||||
});
|
||||
};
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// home endpoint : send the correct index page or error if not existing
|
||||
app.get('/', (req, res) => {
|
||||
const homePath = path.join(config['data_dir'], config['home']['index']);
|
||||
fs.access(homePath, fs.constants.R_OK, (err) => {
|
||||
if (err)
|
||||
showError(req, res, 404);
|
||||
else
|
||||
render(req, res, homePath,
|
||||
{
|
||||
articles: Object.values(articles)
|
||||
.filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//RSS endpoint
|
||||
app.get(config['rss']['endpoint'], (req, res) => {
|
||||
if (config['modules']['rss']) {
|
||||
if (!lastRSS) {
|
||||
const feed = new Rss({
|
||||
'title': config['rss']['title'],
|
||||
'description': config['rss']['description'],
|
||||
'feed_url': host + req.url,
|
||||
'site_url': host
|
||||
});
|
||||
Object.values(articles)
|
||||
.slice(0, config['rss']['length'])
|
||||
.forEach((article) => {
|
||||
feed.item({
|
||||
title: article.title,
|
||||
url: host + article.url,
|
||||
date: article.date
|
||||
});
|
||||
});
|
||||
lastRSS = feed.xml();
|
||||
}
|
||||
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
|
||||
} else {
|
||||
showError(req, res, 404);
|
||||
}
|
||||
});
|
||||
|
||||
//webhook endpoint
|
||||
app.post(config['webhook']['endpoint'], (req, res) => {
|
||||
if (config['modules']['webhook']) {
|
||||
if (config['webhook']['signature_header'] && config['webhook']['secret']) {
|
||||
const payload = JSON.stringify(req.body) || '';
|
||||
const hmac = crypto.createHmac('sha1', config['webhook']['secret']);
|
||||
const digest = 'sha1=' + hmac.update(payload).digest('hex');
|
||||
const checksum = req.headers[config['webhook']['signature_header']];
|
||||
if (!checksum || !digest || checksum !== digest) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
}
|
||||
cp.exec(config['webhook']['pull_command'], {cwd: path.join(__dirname, '..', config['data_dir'])}, (err) => {
|
||||
if (err) {
|
||||
console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
reload(() => {
|
||||
res.sendStatus(200);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
res.sendStatus(400);
|
||||
}
|
||||
});
|
||||
|
||||
//rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png
|
||||
app.use((req, res, next) => {
|
||||
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url))
|
||||
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
|
||||
next();
|
||||
});
|
||||
|
||||
// catch all article urls and render them
|
||||
app.get('*', (req, res, next) => {
|
||||
if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) {
|
||||
const articlePath = req.path.substr(1, 10);
|
||||
const article = articles[articlePath];
|
||||
if (!article)
|
||||
showError(req, res, 404);
|
||||
else {
|
||||
renderer.render(article.realPath, (err, html) => {
|
||||
if (err) {
|
||||
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
|
||||
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, res, 500);
|
||||
} else
|
||||
render(req, res, templatePath, {article: article});
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
// catch all hidden file type and return 404
|
||||
config['home']['hidden'].forEach(pathMatcher => {
|
||||
app.get(pathMatcher, (req, res) => {
|
||||
showError(req, res, 404);
|
||||
});
|
||||
});
|
||||
|
||||
// serve all static files via get
|
||||
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
|
||||
// catch express.static errors (mostly not found) by displaying 404
|
||||
app.get('*', (req, res) => {
|
||||
showError(req, res, 404);
|
||||
});
|
||||
|
||||
// catch all other methods and return 400
|
||||
app.all('*', (req, res) => {
|
||||
res.status(400).send('bad request');
|
||||
});
|
||||
|
||||
//log all server errors
|
||||
app.use((err, req, res, next) => {
|
||||
console.log(cons.error, `error when handling ${req.method} ${req.path} request : ${err}`);
|
||||
if (!config['error_log'])
|
||||
next(err);
|
||||
fs.appendFile(config['error_log'],
|
||||
`500 ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n${err.stack}\n`,
|
||||
{encoding: 'UTF-8'}, () => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
// must be use in a server.js to start the server
|
||||
app.start = () => {
|
||||
reload(() => {
|
||||
app.listen(config['node_port'], () => {
|
||||
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"node_port": 3000,
|
||||
"host": "",
|
||||
"data_dir": "data",
|
||||
"view_engine": "ejs",
|
||||
"rate_limit": 100,
|
||||
"access_log": "access.log",
|
||||
"error_log": "error.log",
|
||||
"modules": {
|
||||
"rss": true,
|
||||
"webhook": true,
|
||||
"prism": true,
|
||||
"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",
|
||||
"/.git*"
|
||||
]
|
||||
},
|
||||
"article": {
|
||||
"index": "index.md",
|
||||
"draft": "draft.md",
|
||||
"template": "template.ejs",
|
||||
"thumbnail_tag": "thumbnail",
|
||||
"default_title": "Untitled",
|
||||
"default_thumbnail": ""
|
||||
},
|
||||
"rss": {
|
||||
"title": "mygitblog RSS feed",
|
||||
"description": "a generated RSS feed from my articles",
|
||||
"endpoint": "/rss",
|
||||
"length": 10
|
||||
},
|
||||
"webhook": {
|
||||
"endpoint": "/webhook",
|
||||
"secret": "",
|
||||
"signature_header": "",
|
||||
"pull_command": "git pull origin master"
|
||||
},
|
||||
"showdown": {
|
||||
"parseImgDimensions": true,
|
||||
"strikethrough": true,
|
||||
"tables": true,
|
||||
"tasklists": true,
|
||||
"openLinksInNewWindow": true,
|
||||
"emoji": true
|
||||
},
|
||||
"mathjax": {
|
||||
"output_format": "svg",
|
||||
"speak_text": true
|
||||
},
|
||||
"plantuml": {
|
||||
"output_format": "svg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
const refConfig = require('./config.default.json');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Merge resources by reading object keys and keeping reference value only if it's type is different from the source
|
||||
* @param ref - reference object/value
|
||||
* @param src - source object/value
|
||||
* @returns {*}
|
||||
*/
|
||||
const merge = (ref, src) => {
|
||||
if (typeof ref !== typeof src) {
|
||||
return ref;
|
||||
} else if (ref.length && !src.length) {
|
||||
return ref;
|
||||
} else if (ref.length && src.length) {
|
||||
return src;
|
||||
} else if (typeof ref === 'object') {
|
||||
const out = {};
|
||||
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
|
||||
return out;
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
try {
|
||||
let configData = fs.readFileSync('config.json', {encoding: 'UTF-8'});
|
||||
let config = JSON.parse(configData);
|
||||
return merge(refConfig, config);
|
||||
} catch (error) {
|
||||
console.log('\x1b[33m⚠\x1b[0m %s', 'Failed to load config.json : ' + error);
|
||||
return refConfig;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
|
||||
/**
|
||||
* Get all files path inside a given folder path
|
||||
* @param dir
|
||||
* @param cb
|
||||
*/
|
||||
const getFileTree = (dir, cb) => {
|
||||
let list = [];
|
||||
let remaining = 0;
|
||||
fs.readdir(dir, {withFileTypes: true}, (err, items) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
items.forEach((item) => {
|
||||
if (item.isDirectory()) {
|
||||
remaining++;
|
||||
getFileTree(path.join(dir, item.name), (err, out) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
list.push(...out);
|
||||
remaining--;
|
||||
if (remaining === 0)
|
||||
cb(null, list);
|
||||
});
|
||||
} else {
|
||||
list.push(path.join(dir, item.name));
|
||||
}
|
||||
});
|
||||
if (remaining === 0)
|
||||
cb(null, list);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to read a markdown file and match a title and a thumbnail
|
||||
* @param path
|
||||
* @param thumbnailTag - how the thumbnail image desc is given as 
|
||||
* @param cb
|
||||
*/
|
||||
const readIndexFile = (path, thumbnailTag, cb) => {
|
||||
fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
|
||||
let info = {};
|
||||
|
||||
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
|
||||
info.title = regRes1 ? regRes1[2].trim() : undefined;
|
||||
|
||||
const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i');
|
||||
const regRes2 = data.match(thumbnailRegEx);
|
||||
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
|
||||
|
||||
cb(null, info);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (config) => {
|
||||
return {
|
||||
fileTree: config['test'] ? getFileTree : undefined,
|
||||
readIndexFile: config['test'] ? readIndexFile : undefined,
|
||||
/**
|
||||
* find and read all articles inside the data directory
|
||||
* @param cb
|
||||
*/
|
||||
fetchArticles: (cb) => {
|
||||
getFileTree(config['data_dir'], (err, fileList) => {
|
||||
if (err)
|
||||
return cb(err);
|
||||
const paths = fileList
|
||||
.map((p) => p.substr(config['data_dir'].length + 1).split(path.sep))
|
||||
.filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) &&
|
||||
/^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2]));
|
||||
if (paths.length === 0)
|
||||
cb(null, {});
|
||||
const articles = {};
|
||||
let remaining = 0;
|
||||
paths.forEach((p) => {
|
||||
const article = {
|
||||
path: joinUrl(p[0], p[1], p[2]),
|
||||
draft: p[3] === config['article']['draft'],
|
||||
realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]),
|
||||
year: parseInt(p[0]),
|
||||
month: parseInt(p[1]),
|
||||
day: parseInt(p[2])
|
||||
};
|
||||
article.date = new Date(article.year, article.month, article.day);
|
||||
article.date.setUTCHours(0);
|
||||
remaining++;
|
||||
readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => {
|
||||
if (err)
|
||||
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) + '/';
|
||||
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
+26
-14
@@ -1,18 +1,30 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const ncp = require('ncp').ncp;
|
||||
|
||||
const pad0 = n => ('0'+n).substr(-2);
|
||||
|
||||
const datetime = new Date();
|
||||
const dir = `./data/${datetime.getFullYear()}/${pad0(datetime.getMonth())}/${pad0(datetime.getDay())}/`;
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
}
|
||||
|
||||
ncp('./sample_data/',dir, function(err){
|
||||
if(err)
|
||||
console.error(err);
|
||||
const copy = (src, dest) => {
|
||||
ncp(src, dest, function (err) {
|
||||
if (err)
|
||||
console.error(err);
|
||||
else
|
||||
console.log(`sample data copied to ${dir}`);
|
||||
});
|
||||
console.log(`copied ${src} to ${dest}`);
|
||||
});
|
||||
};
|
||||
|
||||
copy(path.join('src', 'config.default.json'), 'config.example.json');
|
||||
|
||||
if (!fs.existsSync('data')) {
|
||||
fs.mkdirSync('data');
|
||||
|
||||
copy(path.join('sample_data', 'home'), 'data');
|
||||
|
||||
const pad0 = (n) => ('0' + n).substr(-2);
|
||||
|
||||
const datetime = new Date();
|
||||
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
|
||||
|
||||
if (!fs.existsSync(dir))
|
||||
fs.mkdirSync(dir, {recursive: true});
|
||||
|
||||
copy(path.join('sample_data', 'article'), dir);
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const showdown = require('showdown');
|
||||
|
||||
module.exports = (config) => {
|
||||
const converter = new showdown.Converter(config['showdown']);
|
||||
|
||||
/**
|
||||
* get parts outside of codes/scripts
|
||||
* @param {string} data
|
||||
* @returns {{index:number, end:number, text:string}[]} parts
|
||||
*/
|
||||
const getParts = (data) => {
|
||||
let parts = [];
|
||||
let match;
|
||||
let i = 0;
|
||||
while ((match = /```/m.exec(data.slice(i)))) {
|
||||
parts.push({
|
||||
index: i,
|
||||
text: data.slice(i, i + match.index),
|
||||
});
|
||||
i += match.index + match[0].length;
|
||||
}
|
||||
if (i < data.length)
|
||||
parts.push({
|
||||
index: i,
|
||||
text: data.slice(i, data.length),
|
||||
});
|
||||
|
||||
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
|
||||
|
||||
// detect scripts outside of code
|
||||
parts.forEach((p, pi) => {
|
||||
let i = 0;
|
||||
const subParts = [];
|
||||
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
|
||||
subParts.push({
|
||||
index: p.index + i,
|
||||
text: p.text.slice(i, i + match.index),
|
||||
});
|
||||
i += match.index + match[0].length;
|
||||
}
|
||||
if (i < p.text.length)
|
||||
subParts.push({
|
||||
index: p.index + i,
|
||||
text: p.text.slice(i, p.text.length),
|
||||
});
|
||||
parts.splice(pi, 1, ...subParts);
|
||||
});
|
||||
|
||||
parts.forEach(part => part.end = part.index + part.text.length);
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
const renderShowDown = (data, cb) => {
|
||||
const html = converter.makeHtml(data);
|
||||
cb(html);
|
||||
};
|
||||
|
||||
let Prism;
|
||||
if (config['modules']['prism'])
|
||||
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 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 {
|
||||
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);
|
||||
|
||||
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'}));
|
||||
};
|
||||
|
||||
+2
-7
@@ -1,9 +1,4 @@
|
||||
const config = require('../config.json');
|
||||
const config = require('./config')();
|
||||
const app = require('./app')(config);
|
||||
|
||||
const port = config.nodePort|3000;
|
||||
|
||||
app.listen(config.nodePort|3000, () => {
|
||||
console.log(`gitblog.md server listening on port ${port}`);
|
||||
});
|
||||
|
||||
app.start();
|
||||
+468
-8
@@ -1,14 +1,474 @@
|
||||
/* jshint -W117 */
|
||||
const request = require('supertest');
|
||||
const config = require('./config.test.json');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
const dataDir = 'test_data';
|
||||
const testIndex = 'testindex.ejs';
|
||||
const testError = 'testerror.ejs';
|
||||
const testTemplate = 'testtemplate.ejs';
|
||||
|
||||
const config = require('../src/config')();
|
||||
|
||||
config['test'] = true;
|
||||
config['data_dir'] = dataDir;
|
||||
config['webhook']['endpoint'] = '/webhooktest';
|
||||
config['rss']['endpoint'] = '/rsstest';
|
||||
config['rss']['length'] = 2;
|
||||
config['home']['error'] = testError;
|
||||
config['article']['template'] = testTemplate;
|
||||
|
||||
const app = require('../src/app')(config);
|
||||
|
||||
describe('Test root path', () => {
|
||||
test('GET / 200', done => {
|
||||
request(app).get('/').then(response => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Test reload', () => {
|
||||
test('reload fail', (done, fail) => {
|
||||
config['data_dir'] = '';
|
||||
app.reload(fail, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test request logging', () => {
|
||||
test('test no log', (done) => {
|
||||
request(app).get('/rsstest').then(() => {
|
||||
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();
|
||||
expect(data).toBe('200 GET /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
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();
|
||||
expect(data).toBe('400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
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) => {
|
||||
expect(err).toBeNull();
|
||||
expect(data).toBe('404 GET /rss ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n' +
|
||||
'400 POST /rsstest ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\n');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test error logging', () => {
|
||||
test('test no log', (done) => {
|
||||
config['home']['index'] = null;
|
||||
request(app).get('/').then(() => {
|
||||
expect(fs.existsSync(path.join(dataDir, 'error.log'))).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('test null error ', (done) => {
|
||||
config['home']['index'] = null;
|
||||
config['error_log'] = path.join(dataDir, 'error.log');
|
||||
request(app).get('/').then(() => {
|
||||
fs.readFile(path.join(dataDir, 'error.log'), {encoding: 'UTF-8'}, (err, data) => {
|
||||
expect(err).toBeNull();
|
||||
const start = data.split('\n').slice(0, 2).join('\n');
|
||||
const expected = '500 GET / ' + new Date().toUTCString() + ' ::ffff:127.0.0.1\nTypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type object';
|
||||
expect(start).toBe(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test root path', () => {
|
||||
test('404 no index no error', (done) => {
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('404 no index but error page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('500 render error', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('500 render error with page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
expect(response.text).toBe('error 500');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('500 render error with failing page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= null.length %>');
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= null.error %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 no articles', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('articles 0');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 2 articles 1 drafted', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '05', '05'),
|
||||
path.join(dataDir, '2017', '05', '05')
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'draft.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'draft.md'),
|
||||
path.join(dataDir, '2017', '05', '05', 'index.md'),
|
||||
]);
|
||||
fs.writeFileSync(path.join(dataDir, testIndex), 'articles <%= articles.length %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('articles 2');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test RSS feed', () => {
|
||||
test('404 rss deactivated', (done) => {
|
||||
config['modules']['rss'] = false;
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 empty rss', (done) => {
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('application/rss+xml');
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 Mozilla fix', (done) => {
|
||||
request(app).get('/rsstest').set('user-agent', 'Mozilla Firefox 64.0').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('text/xml');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 rss cache', (done) => {
|
||||
request(app).get('/rsstest').then(() => {
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('200 2 rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '05', '05')
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'index.md')
|
||||
]);
|
||||
app.reload(() => {
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(3);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
test('200 max rss items', (done, fail) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, '2019', '05', '05'),
|
||||
path.join(dataDir, '2018', '05', '05'),
|
||||
path.join(dataDir, '2017', '05', '05')
|
||||
]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2018', '05', '05', 'index.md'),
|
||||
path.join(dataDir, '2017', '05', '05', 'index.md')
|
||||
]);
|
||||
app.reload(() => {
|
||||
request(app).get('/rsstest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text.length).toBeGreaterThan(0);
|
||||
expect(response.text.split('<item>').length).toBe(3);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test webhook', () => {
|
||||
test('400 webhook deactivated', (done) => {
|
||||
config['modules']['webhook'] = false;
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 no secret', (done) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate)
|
||||
]);
|
||||
config['webhook']['pull_command'] = 'git --help';
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
request(app).get('/2019/05/05/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('500 command failed', (done) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate)
|
||||
]);
|
||||
config['webhook']['pull_command'] = 'qzgfqgqz';
|
||||
request(app).post('/webhooktest').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('403 wrong secret', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
request(app).post('/webhooktest').set('testheader', 'sha1=invalid').then((response) => {
|
||||
expect(response.statusCode).toBe(403);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 valid secret', (done) => {
|
||||
config['webhook']['signature_header'] = 'testheader';
|
||||
config['webhook']['secret'] = 'testvalue';
|
||||
config['webhook']['pull_command'] = 'git --help';
|
||||
request(app).post('/webhooktest')
|
||||
.send({})
|
||||
.set('testheader', 'sha1=d924d5bd4b36faf9d572844ac9c12a09ce3e7134')
|
||||
.then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test articles rendering', () => {
|
||||
test('404 article not found', (done) => {
|
||||
request(app).get('/2019/05/06/untitled/').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('500 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');
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/hello/').then((response) => {
|
||||
expect(response.statusCode).toBe(500);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 rendered article', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'index.md'), '# Hello');
|
||||
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/hello/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 rendered draft', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'draft.md'), '# Hello');
|
||||
fs.writeFileSync(path.join(dataDir, testTemplate), '<%- article.content %><%- `<a href="${article.url}">reload</a>` %>');
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/hello/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('<h1 id="hello">Hello</h1><a href="/2019/05/05/hello/">reload</a>');
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate)
|
||||
]);
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/anything/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
|
||||
test('200 other url 2', (done, fail) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
utils.createEmptyFiles([
|
||||
path.join(dataDir, '2019', '05', '05', 'index.md'),
|
||||
path.join(dataDir, testTemplate)
|
||||
]);
|
||||
app.reload(() => {
|
||||
request(app).get('/2019/05/05/').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
done();
|
||||
});
|
||||
}, fail);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Test static files', () => {
|
||||
test('404 invalid file no error page', (done) => {
|
||||
request(app).get('/somefile.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('404 invalid file but error page', (done) => {
|
||||
fs.writeFileSync(path.join(dataDir, testError), 'error <%= error %>');
|
||||
request(app).get('/somefile.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(response.text).toBe('error 404');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('404 hidden file', (done) => {
|
||||
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.css'), 'filecontent');
|
||||
request(app).get('/somefile.css').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.type).toBe('text/css');
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('200 valid resource of article', (done) => {
|
||||
utils.createEmptyDirs([path.join(dataDir, '2019', '05', '05'),]);
|
||||
fs.writeFileSync(path.join(dataDir, '2019', '05', '05', 'somefile.txt'), 'filecontent');
|
||||
request(app).get('/2019/05/05/title/somefile.txt').then((response) => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.text).toBe('filecontent');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test other requests', () => {
|
||||
test('400 POST', (done) => {
|
||||
request(app).post('/').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('400 PUT', (done) => {
|
||||
request(app).put('/').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('400 DELETE', (done) => {
|
||||
request(app).delete('/').then((response) => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const configFile = 'config.json';
|
||||
const tmpConfigFile = 'config.temp.json';
|
||||
|
||||
beforeAll(() => {
|
||||
if (fs.existsSync(configFile)) {
|
||||
fs.renameSync(configFile, tmpConfigFile);
|
||||
}
|
||||
expect(fs.existsSync(configFile)).toBeFalsy();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(tmpConfigFile)) {
|
||||
fs.renameSync(tmpConfigFile, configFile);
|
||||
} else if (fs.existsSync(configFile)) {
|
||||
fs.unlinkSync(configFile); //remove config file if remaining
|
||||
}
|
||||
});
|
||||
|
||||
test('no config', () => {
|
||||
if (fs.existsSync(configFile))
|
||||
fs.unlinkSync(configFile);
|
||||
expect(fs.existsSync(configFile)).toBeFalsy();
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('example config', () => {
|
||||
if (fs.existsSync(configFile))
|
||||
fs.unlinkSync(configFile);
|
||||
fs.copyFileSync(path.join('src', 'config.default.json'), configFile);
|
||||
const data = fs.readFileSync(configFile, {encoding: 'UTF-8'});
|
||||
fs.writeFileSync(configFile, data.replace('3000', '3333'), {encoding: 'UTF-8'});
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3333);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('invalid config ignored', () => {
|
||||
fs.writeFileSync(configFile, 'invalid JSON');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('good config merged', () => {
|
||||
fs.writeFileSync(configFile, '{"node_port":5000}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(5000);
|
||||
expect(config['data_dir']).toBe('data');
|
||||
});
|
||||
|
||||
test('wrong config fixed', () => {
|
||||
fs.writeFileSync(configFile, '{"node_port":"hello","data_dir":"data2"}');
|
||||
const config = require('../src/config')();
|
||||
expect(config).toBeDefined();
|
||||
expect(config['node_port']).toBe(3000);
|
||||
expect(config['data_dir']).toBe('data2');
|
||||
});
|
||||
|
||||
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*']);
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
const dataDir = 'test_data';
|
||||
const testIndex = 'testindex.md';
|
||||
|
||||
const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
|
||||
|
||||
const config = {
|
||||
'test': true,
|
||||
'data_dir': dataDir,
|
||||
'article': {
|
||||
'index': testIndex,
|
||||
'draft': 'draft.md',
|
||||
'default_title': 'Untitled',
|
||||
'default_thumbnail': 'default.png',
|
||||
'thumbnail_tag': 'thumbnail'
|
||||
}
|
||||
};
|
||||
|
||||
const fw = require('../src/file_walker')(config);
|
||||
|
||||
beforeEach(() => {
|
||||
config['data_dir'] = dataDir;
|
||||
utils.deleteFolderSync(dataDir);
|
||||
fs.mkdirSync(dataDir);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
describe('Test function fileTree', () => {
|
||||
test('empty root', (done) => {
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('empty folders', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
path.join(dataDir, 'test', 'test2'),
|
||||
path.join(dataDir, 'test2')
|
||||
]);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('simple files', (done) => {
|
||||
const fileList = [
|
||||
path.join(dataDir, 'f1.txt'),
|
||||
path.join(dataDir, 'f2.txt')
|
||||
];
|
||||
utils.createEmptyFiles(fileList);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(fileList.length);
|
||||
expect(list).toEqual(expect.arrayContaining(fileList));
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('nested files', (done) => {
|
||||
utils.createEmptyDirs([
|
||||
path.join(dataDir, 'test', 'test'),
|
||||
path.join(dataDir, 'test2')
|
||||
]);
|
||||
const fileList = [
|
||||
path.join(dataDir, 'f1.txt'),
|
||||
path.join(dataDir, 'test', 'f2.txt'),
|
||||
path.join(dataDir, 'test', 'test', 'f3.txt'),
|
||||
path.join(dataDir, 'test2', 'f4.txt')
|
||||
];
|
||||
utils.createEmptyFiles(fileList);
|
||||
fw.fileTree(dataDir, (err, list) => {
|
||||
expect(err).toBeNull();
|
||||
expect(list).toBeDefined();
|
||||
expect(list.length).toBe(fileList.length);
|
||||
expect(list).toEqual(expect.arrayContaining(fileList));
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('invalid root', (done) => {
|
||||
fw.fileTree('invalid root', (err, list) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(list).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test index article reading', () => {
|
||||
const file = path.join(dataDir, testIndex);
|
||||
|
||||
test('invalid file', (done) => {
|
||||
fw.readIndexFile('invalid file', 'thumbnail', (err, info) => {
|
||||
expect(err).not.toBeNull();
|
||||
expect(info).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test('correct file', (done) => {
|
||||
fs.writeFileSync(file, `
|
||||
# This is an awesome title !?¤
|
||||

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

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

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

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

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

|
||||
this is some text
|
||||
`);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
day: 5,
|
||||
draft: true,
|
||||
date: date,
|
||||
title: 'Title with : info !',
|
||||
thumbnail: joinUrl('2019', '05', '05', './thumbnail.jpg'),
|
||||
escapedTitle: 'title_with___info',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'title_with___info') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('index file override draft', (done) => {
|
||||
const dir = path.join(dataDir, '2019', '05', '05');
|
||||
const file = path.join(dir, testIndex);
|
||||
const file2 = path.join(dir, 'draft.md');
|
||||
utils.createEmptyDirs([dir]);
|
||||
utils.createEmptyFiles([file, file2]);
|
||||
const date = new Date(2019, 5, 5);
|
||||
date.setUTCHours(0);
|
||||
fw.fetchArticles((err, dict) => {
|
||||
expect(err).toBeNull();
|
||||
expect(dict).toBeDefined();
|
||||
expect(Object.keys(dict).length).toBe(1);
|
||||
expect(dict[joinUrl('2019', '05', '05')]).toEqual({
|
||||
path: joinUrl('2019', '05', '05'),
|
||||
realPath: file,
|
||||
year: 2019,
|
||||
month: 5,
|
||||
draft: false,
|
||||
day: 5,
|
||||
date: date,
|
||||
title: 'Untitled',
|
||||
thumbnail: 'default.png',
|
||||
escapedTitle: 'untitled',
|
||||
url: '/' + joinUrl('2019', '05', '05', 'untitled') + '/',
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/* jshint -W117 */
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const utils = require('./test_utils');
|
||||
|
||||
const dataDir = 'test_data';
|
||||
const file = path.join(dataDir, 'test.md');
|
||||
|
||||
const config = {
|
||||
'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);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dataDir)) {
|
||||
utils.deleteFolderSync(dataDir);
|
||||
}
|
||||
});
|
||||
|
||||
describe('get parts', () => {
|
||||
test('normal', () => {
|
||||
const data = 'Hello\nthere\ngeneral\nkenobi';
|
||||
const parts = renderer.getParts(data);
|
||||
expect(parts.map(p => p.text)).toEqual([
|
||||
'Hello\nthere\ngeneral\nkenobi'
|
||||
]);
|
||||
});
|
||||
test('lot of stuff', () => {
|
||||
const data = 'Hello\nthere\n```code```\ngeneral<script>script</script>\n<script>script2</script>\n```<script>script3</script>```kenobi';
|
||||
const parts = renderer.getParts(data);
|
||||
expect(parts).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
end: 12,
|
||||
text: 'Hello\nthere\n'
|
||||
},
|
||||
{
|
||||
index: 22,
|
||||
end: 30,
|
||||
text: '\ngeneral'
|
||||
},
|
||||
{
|
||||
index: 53,
|
||||
end: 54,
|
||||
text: '\n'
|
||||
},
|
||||
{
|
||||
index: 78,
|
||||
end: 79,
|
||||
text: '\n'
|
||||
},
|
||||
{
|
||||
index: 109,
|
||||
end: 115,
|
||||
text: 'kenobi'
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test Showdown', () => {
|
||||
test('normal', (done) => {
|
||||
renderer.renderShowDown('# Hello', (html) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test fa-diagrams', () => {
|
||||
test('no fa-diagrams', (done) => {
|
||||
config['modules']['fa-diagrams'] = false;
|
||||
renderer.renderFaDiagrams('@startfad\noptions.rendering.color=\'red\'\n@endfad', (data) => {
|
||||
expect(data).toBe('@startfad\noptions.rendering.color=\'red\'\n@endfad');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('no fa-diagrams in code', (done) => {
|
||||
renderer.renderFaDiagrams('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```', (data) => {
|
||||
expect(data).toBe('code:\n```\n@startfad\noptions.rendering.color=\'red\'\n@endfad\n```');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('valid fa-diagrams', (done) => {
|
||||
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=\'red\'\n@endfad\nafter', (data) => {
|
||||
expect(data).toBe('before\n<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0" width="0" height="0" font-family="Arial" font-size="15" fill="red" stroke-width="0"></svg>\nafter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
test('invalid toml', (done) => {
|
||||
renderer.renderFaDiagrams('before\n@startfad\noptions.rendering.color=red\n@endfad\nafter', (data) => {
|
||||
expect(data).toBe('before\n<b style="color:red">TomlError: Unexpected character, expecting string, number, datetime, boolean, inline array or inline table at row 1, col 26, pos 25:\n' +
|
||||
'1> options.rendering.color=red\n' +
|
||||
' ^\n' +
|
||||
'\n</b>\nafter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test render', () => {
|
||||
test('invalid file', (done) => {
|
||||
renderer.render('invalid file', (err, html) => {
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const deleteFolderSync = (dir) => {
|
||||
if (!fs.existsSync(dir))
|
||||
return;
|
||||
let items;
|
||||
const deleteItem = (item) => {
|
||||
if (item.isDirectory())
|
||||
deleteFolderSync(path.join(dir, item.name));
|
||||
else
|
||||
fs.unlinkSync(path.join(dir, item.name));
|
||||
};
|
||||
do {
|
||||
items = fs.readdirSync(dir, {withFileTypes: true});
|
||||
try {
|
||||
items.forEach(deleteItem);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} while (items.length > 0);
|
||||
fs.rmdirSync(dir);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
deleteFolderSync: deleteFolderSync,
|
||||
createEmptyDirs: (list) => list.forEach((path) => fs.mkdirSync(path, {recursive: true})),
|
||||
createEmptyFiles: (list) => list.forEach((file) => fs.writeFileSync(file, '')),
|
||||
};
|
||||
Reference in New Issue
Block a user