Compare commits

...

76 Commits

Author SHA1 Message Date
Klemek 7d72e94aa3 Merge pull request #20 from Klemek/dev
updated package-lock
2019-07-19 11:00:33 +02:00
Klemek 3d6a0b4306 Merge branch 'master' into dev 2019-07-19 10:58:52 +02:00
Klemek babc533efc updated package-lock 2019-07-19 10:56:26 +02:00
Klemek 36908134e6 Merge pull request #19 from Klemek/dependabot/npm_and_yarn/lodash-4.17.15
Bump lodash from 4.17.11 to 4.17.15
2019-07-19 10:50:53 +02:00
dependabot[bot] d426b41368 Bump lodash from 4.17.11 to 4.17.15
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

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