├── .babelrc
├── .eslintignore
├── .eslintrc.yml
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── bin
└── pen
├── index.js
├── media
└── logo.png
├── package-lock.json
├── package.json
├── src
├── argv.js
├── frontend
│ ├── .eslintrc.yml
│ ├── html-renderer.js
│ ├── main.js
│ ├── socket-client.js
│ └── style.css
├── markdown-socket.js
├── markdown-watcher.js
├── markdown.js
├── server.js
└── watcher.js
├── test
├── .eslintrc.yml
├── lib
│ └── helper.js
├── setup.js
├── test-build-html.js
├── test-html-renderer.js
├── test-index.js
├── test-markdown-socket.js
├── test-markdown-watcher.js
├── test-server.js
├── test-socket-client.js
└── test-watcher.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Built files
2 | dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | root: true
2 | parser: "babel-eslint"
3 | extends:
4 | - "eslint:recommended"
5 | - "plugin:prettier/recommended"
6 | rules:
7 | no-console: 0
8 | env:
9 | node: true
10 | es6: true
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | # Temporary files for tests
30 | /test/temp
31 |
32 | # Built files
33 | /dist
34 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /.eslintignore
4 | /.eslintrc
5 | /.gitignore
6 | /.travis.yml
7 | /test/temp
8 | /resource
9 |
10 | # Built JS and CSS files
11 | /dist/build.js
12 | /dist/build.css
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 6
4 | - 8
5 | - node
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2018 Jun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | > We need a better Markdown previewer.
4 |
5 | [](https://travis-ci.org/utatti/pen)
6 |
7 | `pen` is a Markdown previewer written in JavaScript, aiming to *just work*.
8 |
9 | There are literally tons of Markdown previewers out there. I love some of them,
10 | I even made [one](https://github.com/utatti/orange-cat) of them. Nevertheless,
11 | we always need a better one, don't we?
12 |
13 | Using `pen` is super simple, we don't need to install any special editor or
14 | launch any GUI application. `pen` is just a tidy command-line tool. You can use
15 | your favourite editor and browser. No manual refresh is even needed.
16 |
17 | Also, the previewer renders the content using [React](https://facebook.github.io/react/).
18 | It means that it will not re-render entire DOM when the document is updated.
19 | This is a huge advantage because images or other media won't be reloaded for
20 | the DOM update.
21 |
22 | I personally love to use `pen`, and I hope you love it too. :black_nib:
23 |
24 | ## Demo
25 |
26 | Here is a short demo showing how awesome `pen` is.
27 |
28 | 
29 |
30 | The following demo shows `pen` incrementally updates only modified part using
31 | [React](https://facebook.github.io/react/) and its [Reconciliation](https://reactjs.org/docs/reconciliation.html).
32 |
33 | 
34 |
35 | ## Requirement
36 |
37 | `pen` uses [Node.js >= 4.0](https://nodejs.org/en/docs/es6/). It may not work
38 | on earlier versions.
39 |
40 | ## Install
41 |
42 | Using [npm](http://npmjs.com):
43 |
44 | ```shell
45 | npm i -g pen
46 | ```
47 |
48 | You can try using `pen` with `npx`:
49 |
50 | ```shell
51 | npx pen
52 | ```
53 |
54 | ## Usage
55 |
56 | To use `pen`, simply run the `pen` command.
57 |
58 | ```shell
59 | pen README.md
60 | ```
61 |
62 | The command above will launch a `pen` server and open the file in your default
63 | browser. The server will listen to a 6060 port by default. To be honest, you
64 | don't even need to launch it with a filename. You can manually open
65 | http://localhost:6060/README.md, or any other files in the same directory.
66 |
67 | To stop the server, enter `^C`.
68 |
69 | For the further details of the `pen` command, please enter `pen -h` or `pen
70 | --help`.
71 |
72 | ### Pandoc
73 |
74 | Pen uses [markdown-it](https://github.com/markdown-it/markdown-it) as Markdown
75 | parser by default, but it also supports Pandoc. Please provide [a proper Pandoc
76 | format](http://pandoc.org/MANUAL.html#general-options) for the value.
77 |
78 | ```shell
79 | pen --pandoc gfm README.md
80 | ```
81 |
82 | ## Contribution
83 |
84 | I welcome every contribution on `pen`. You may start from forking and cloning
85 | this repo.
86 |
87 | ```shell
88 | git clone git@github.com:your_username/pen.git
89 | cd pen
90 |
91 | # Install dependencies
92 | npm i
93 |
94 | # Lint, build, and test pen codes at once
95 | npm test
96 | ```
97 |
98 | To build frontend scripts:
99 |
100 | ```shell
101 | npm run build
102 | ```
103 |
104 | To lint with [ESLint](http://eslint.org):
105 |
106 | ```shell
107 | npm run lint
108 | ```
109 |
110 | To test with [Mocha](http://mochajs.org)
111 |
112 | ```shell
113 | npm run mocha
114 | ```
115 |
116 | ## License
117 |
118 | Pen is released under the [MIT License](LICENSE).
119 |
--------------------------------------------------------------------------------
/bin/pen:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('../index');
3 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const open = require("opn");
4 | const Server = require("./src/server");
5 | const argv = require("./src/argv");
6 |
7 | let server = new Server(process.cwd());
8 | server.listen(argv.port, () => {
9 | console.log(`listening ${argv.port} ...`);
10 |
11 | argv._.forEach(file =>
12 | open(`http://localhost:${argv.port}/${file}`).catch(() => {})
13 | );
14 | });
15 |
--------------------------------------------------------------------------------
/media/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hatashiro/pen/5b1e0123c25c37b9fc26dec9f91aa9c27539b505/media/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pen",
3 | "version": "2.2.0",
4 | "description": "A better Markdown previewer",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "npm run lint && npm run build && npm run mocha",
8 | "lint": "eslint --ext .js --ignore-path .gitignore .",
9 | "lintfix": "eslint --fix --ext .js --ignore-path .gitignore .",
10 | "build": "NODE_ENV=production webpack -p",
11 | "mocha": "mocha -r babel-core/register -r babel-polyfill -r test/setup.js test/**/test-*",
12 | "release": "npm run build && npm publish --access=public"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/utatti/pen.git"
17 | },
18 | "keywords": [
19 | "markdown",
20 | "previewer"
21 | ],
22 | "author": "Jun ",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/utatti/pen/issues"
26 | },
27 | "homepage": "https://github.com/utatti/pen",
28 | "devDependencies": {
29 | "babel-core": "^6.26.3",
30 | "babel-eslint": "^8.2.3",
31 | "babel-loader": "^7.1.2",
32 | "babel-polyfill": "^6.26.0",
33 | "babel-preset-env": "^1.7.0",
34 | "css-loader": "^0.28.7",
35 | "eslint": "^4.19.1",
36 | "eslint-config-prettier": "^2.9.0",
37 | "eslint-plugin-prettier": "^2.6.0",
38 | "eslint-plugin-react": "^7.7.0",
39 | "extract-text-webpack-plugin": "^3.0.2",
40 | "github-markdown-css": "^2.9.0",
41 | "html-webpack-inline-source-plugin": "^0.0.9",
42 | "html-webpack-plugin": "^2.30.1",
43 | "jsdom": "^9.4.2",
44 | "json-loader": "^0.5.7",
45 | "mocha": "^5.2.0",
46 | "prettier": "^1.12.1",
47 | "react": "^16.2.0",
48 | "react-dom": "^16.2.0",
49 | "react-render-html": "^0.6.0",
50 | "request": "^2.87.0",
51 | "rimraf": "^2.5.4",
52 | "style-loader": "^0.19.0",
53 | "webpack": "^3.8.1",
54 | "webpack-fail-plugin": "^2.0.0"
55 | },
56 | "dependencies": {
57 | "highlight.js": "^9.9.0",
58 | "markdown-it": "^8.2.2",
59 | "markdown-it-anchor": "^5.0.2",
60 | "markdown-it-checkbox": "^1.1.0",
61 | "markdown-it-emoji": "^1.2.0",
62 | "markdown-it-highlightjs": "^2.0.0",
63 | "opn": "^4.0.2",
64 | "prop-types": "^15.6.0",
65 | "simple-pandoc": "^0.1.0",
66 | "websocket": "^1.0.26",
67 | "yargs": "^6.6.0"
68 | },
69 | "preferGlobal": true,
70 | "bin": {
71 | "pen": "./bin/pen"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/argv.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const DEFAULT_PORT = 6060;
4 |
5 | module.exports = require("yargs")
6 | .usage("Usage: $0 [options] [file]")
7 | .option("p", {
8 | alias: "port",
9 | default: DEFAULT_PORT,
10 | describe: "Set a custom port"
11 | })
12 | .option("pandoc", {
13 | type: "string",
14 | describe:
15 | "Use local pandoc as markdown converter. Provide target format as the value."
16 | })
17 | .help("h")
18 | .alias("h", "help").argv;
19 |
--------------------------------------------------------------------------------
/src/frontend/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | parserOptions:
2 | sourceType: module
3 | extends:
4 | - "eslint:recommended"
5 | - "plugin:react/recommended"
6 | env:
7 | browser: true
8 |
--------------------------------------------------------------------------------
/src/frontend/html-renderer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import renderHTML from "react-render-html";
3 | import SocketClient from "./socket-client";
4 | import PropTypes from "prop-types";
5 |
6 | class HTMLRenderer extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = { html: "" };
10 | }
11 |
12 | componentDidMount() {
13 | this.socketClient = new SocketClient(this.props.location);
14 | this.socketClient.onData(html => this.setState({ html }));
15 | }
16 |
17 | componentDidUpdate() {
18 | if (this.props.onUpdate) {
19 | this.props.onUpdate();
20 | }
21 | }
22 |
23 | render() {
24 | return React.createElement("div", null, renderHTML(this.state.html));
25 | }
26 | }
27 |
28 | HTMLRenderer.propTypes = {
29 | location: PropTypes.shape({
30 | host: PropTypes.string.isRequired,
31 | pathname: PropTypes.string.isRequired
32 | }).isRequired,
33 | onUpdate: PropTypes.func
34 | };
35 |
36 | export default HTMLRenderer;
37 |
--------------------------------------------------------------------------------
/src/frontend/main.js:
--------------------------------------------------------------------------------
1 | // Non-js dependencies
2 | import "github-markdown-css/github-markdown.css";
3 | import "highlight.js/styles/foundation.css";
4 | import "./style.css";
5 |
6 | import React from "react";
7 | import ReactDOM from "react-dom";
8 | import HTMLRenderer from "./html-renderer";
9 |
10 | const app = document.createElement("div");
11 | app.setAttribute("id", "app");
12 | app.setAttribute("class", "markdown-body");
13 | document.body.appendChild(app);
14 |
15 | // Set title to Markdown filename
16 | const pathTokens = location.pathname.split("/");
17 | document.title = pathTokens[pathTokens.length - 1];
18 |
19 | ReactDOM.render(React.createElement(HTMLRenderer, { location }), app);
20 |
--------------------------------------------------------------------------------
/src/frontend/socket-client.js:
--------------------------------------------------------------------------------
1 | import { w3cwebsocket as WebSocket } from "websocket";
2 |
3 | export default class SocketClient {
4 | constructor(location) {
5 | this.host = location.host;
6 | this.pathname = location.pathname;
7 |
8 | const url = `ws://${this.host}${this.pathname}`;
9 |
10 | this._socket = new WebSocket(url);
11 | this._socket.onmessage = event => {
12 | this.triggerOnData(event.data);
13 | };
14 |
15 | this._dataCallback = null;
16 | }
17 |
18 | onData(callback) {
19 | this._dataCallback = callback;
20 | return this;
21 | }
22 |
23 | triggerOnData(data) {
24 | if (this._dataCallback) {
25 | this._dataCallback(data);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/frontend/style.css:
--------------------------------------------------------------------------------
1 | #app {
2 | max-width: 790px;
3 | margin: 30px auto;
4 | }
5 |
6 | /* checkbox */
7 | input[type='checkbox'] {
8 | vertical-align: middle;
9 | margin: 0 0.5em 0.25em 0;
10 | }
11 | li:has(> input[type='checkbox']) {
12 | list-style-type:none;
13 | }
14 | li>input[type='checkbox'] {
15 | margin: 0 0.7em 0.25em -1.6em;
16 | }
17 |
--------------------------------------------------------------------------------
/src/markdown-socket.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const MarkdownWatcher = require("./markdown-watcher");
4 | const path = require("path");
5 | const WebSocketServer = require("websocket").server;
6 |
7 | class MarkdownSocket {
8 | constructor(rootPath) {
9 | this.rootPath = rootPath;
10 | this._server = null;
11 | this.pathname = null;
12 | }
13 |
14 | listenTo(httpServer) {
15 | this._server = new WebSocketServer();
16 | this._server.mount({ httpServer: httpServer });
17 | this._server.on("request", this.onRequest.bind(this));
18 | this._server.on("connect", this.onConnect.bind(this));
19 | }
20 |
21 | onRequest(request) {
22 | const extname = path.extname(request.resource);
23 |
24 | if (extname !== ".md" && extname !== ".markdown") {
25 | request.reject();
26 | return;
27 | }
28 |
29 | this.pathname = request.resource;
30 | request.accept(null, request.origin);
31 | }
32 |
33 | onConnect(connection) {
34 | const decodedPath = decodeURIComponent(this.pathname);
35 | const watcher = new MarkdownWatcher(path.join(this.rootPath, decodedPath));
36 | watcher.onData(data => connection.send(data));
37 | watcher.onError(err => {
38 | if (err.code === "ENOENT") {
39 | // if there is no file, ignore and send 'no file'
40 | connection.send("Not found");
41 | return;
42 | }
43 | throw err;
44 | });
45 |
46 | connection.on("close", () => {
47 | watcher.stop();
48 | });
49 | }
50 |
51 | close() {
52 | this._server.closeAllConnections();
53 | }
54 | }
55 |
56 | module.exports = MarkdownSocket;
57 |
--------------------------------------------------------------------------------
/src/markdown-watcher.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const convert = require("./markdown");
4 | const Watcher = require("./watcher");
5 |
6 | class MarkdownWatcher extends Watcher {
7 | onData(callback) {
8 | this._dataCallback = data => convert(data.toString()).then(callback);
9 | return this;
10 | }
11 | }
12 |
13 | module.exports = MarkdownWatcher;
14 |
--------------------------------------------------------------------------------
/src/markdown.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const argv = require("./argv");
4 | const mdit = require("markdown-it");
5 | const pandoc = require("simple-pandoc");
6 |
7 | const singleton = creator => {
8 | let obj;
9 | return () => obj || (obj = creator());
10 | };
11 |
12 | const md = singleton(() =>
13 | mdit({ html: true, linkify: true })
14 | .use(require("markdown-it-highlightjs"))
15 | .use(require("markdown-it-emoji"))
16 | .use(require("markdown-it-checkbox"))
17 | .use(require("markdown-it-anchor"))
18 | );
19 |
20 | const pd = singleton(() => pandoc(argv.pandoc, "html"));
21 |
22 | module.exports = markdown =>
23 | argv.pandoc ? pd()(markdown) : Promise.resolve(md().render(markdown));
24 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 | const http = require("http");
5 | const path = require("path");
6 | const urllib = require("url");
7 | const MarkdownSocket = require("./markdown-socket");
8 |
9 | function isMarkdown(path) {
10 | const lowerCasedPath = path.toLowerCase();
11 | return lowerCasedPath.endsWith(".md") || lowerCasedPath.endsWith(".markdown");
12 | }
13 |
14 | class Server {
15 | constructor(rootPath) {
16 | this.rootPath = rootPath;
17 | this._server = http.createServer(this.handler.bind(this));
18 |
19 | this._ws = new MarkdownSocket(this.rootPath);
20 | this._ws.listenTo(this._server);
21 | }
22 |
23 | listen(port, cb) {
24 | this._server.listen(port, cb);
25 | }
26 |
27 | close(cb) {
28 | this._ws.close();
29 | this._server.close(cb);
30 | }
31 |
32 | handler(req, res) {
33 | const url = urllib.parse(req.url);
34 |
35 | if (isMarkdown(url.pathname)) {
36 | this.handleAsMarkdown(res);
37 | } else {
38 | this.handleAsStatic(url.pathname, res);
39 | }
40 | }
41 |
42 | handleAsMarkdown(res) {
43 | res.setHeader("Content-Type", "text/html");
44 | const indexHTMLPath = path.join(__dirname, "../dist/index.html");
45 | fs.createReadStream(indexHTMLPath).pipe(res);
46 | }
47 |
48 | handleAsStatic(pathname, res) {
49 | const fullPath = path.join(this.rootPath, pathname);
50 |
51 | try {
52 | const stat = fs.statSync(fullPath);
53 | if (stat.isDirectory()) {
54 | if (!pathname.endsWith("/")) {
55 | res.writeHead(302, { Location: pathname + "/" });
56 | res.end();
57 | return;
58 | }
59 |
60 | const fileList = fs.readdirSync(fullPath).filter(isMarkdown);
61 | res.setHeader("Content-Type", "text/html");
62 | res.end(fileList.map(f => `${f}`).join(" "));
63 | } else {
64 | fs.createReadStream(fullPath).pipe(res);
65 | }
66 | } catch (err) {
67 | if (err.code === "ENOENT") {
68 | res.statusCode = 404;
69 | res.end("Not found");
70 | } else {
71 | throw err;
72 | }
73 | }
74 | }
75 | }
76 |
77 | module.exports = Server;
78 |
--------------------------------------------------------------------------------
/src/watcher.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 |
5 | const WatchInterval = 200; // milliseconds
6 |
7 | class Watcher {
8 | constructor(p) {
9 | this.path = p;
10 | this._watchLoop = null;
11 | this._dataCallback = null;
12 | this._errorCallback = null;
13 | this._previousData = null;
14 |
15 | this.start();
16 | }
17 |
18 | start() {
19 | if (this._watchLoop) {
20 | clearInterval(this._watchLoop);
21 | }
22 |
23 | setTimeout(() => this.watch(), 0); // for the first execution
24 | this._watchLoop = setInterval(() => this.watch(), WatchInterval);
25 | }
26 |
27 | stop() {
28 | clearInterval(this._watchLoop);
29 | this._watchLoop = null;
30 | }
31 |
32 | watch() {
33 | fs.readFile(this.path, (error, data) => {
34 | if (error) {
35 | this.triggerOnError(error);
36 | this.stop();
37 | } else {
38 | if (!this._previousData || data.compare(this._previousData) !== 0) {
39 | this.triggerOnData(data);
40 | this._previousData = data;
41 | }
42 | }
43 | });
44 | }
45 |
46 | onData(callback) {
47 | this._dataCallback = callback;
48 | return this;
49 | }
50 |
51 | onError(callback) {
52 | this._errorCallback = callback;
53 | return this;
54 | }
55 |
56 | triggerOnData(data) {
57 | if (this._dataCallback) {
58 | this._dataCallback(data);
59 | }
60 | }
61 |
62 | triggerOnError(error) {
63 | if (this._errorCallback) {
64 | this._errorCallback(error);
65 | } else {
66 | throw error;
67 | }
68 | }
69 | }
70 |
71 | module.exports = Watcher;
72 |
--------------------------------------------------------------------------------
/test/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | parserOptions:
2 | sourceType: module
3 | extends:
4 | - "eslint:recommended"
5 | - "plugin:react/recommended"
6 | rules:
7 | no-console: 1
8 |
9 | env:
10 | mocha: true
11 |
--------------------------------------------------------------------------------
/test/lib/helper.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import path from "path";
3 | import rimraf from "rimraf";
4 |
5 | const root = path.join(__dirname, "../temp");
6 |
7 | function filePath(p) {
8 | return path.join(root, p);
9 | }
10 |
11 | const helper = {
12 | createFile(p, initialContent) {
13 | fs.writeFileSync(filePath(p), initialContent);
14 | },
15 | makeDirectory(p) {
16 | try {
17 | fs.mkdirSync(filePath(p));
18 | } catch (e) {
19 | if (e.code !== "EEXIST") {
20 | throw e;
21 | }
22 | }
23 | },
24 | path(p) {
25 | return path.join(root, p);
26 | },
27 | createRootDirectory() {
28 | try {
29 | fs.mkdirSync(root);
30 | } catch (e) {
31 | // do nothing
32 | }
33 | },
34 | clean() {
35 | rimraf.sync(path.join(root, "*"));
36 | }
37 | };
38 |
39 | helper.createRootDirectory();
40 | helper.clean();
41 |
42 | export default helper;
43 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import jsdom from "jsdom";
2 |
3 | // test setup for browser mocking
4 | global.document = jsdom.jsdom("");
5 | global.window = global.document.defaultView;
6 | global.navigator = { userAgent: "node.js" };
7 |
--------------------------------------------------------------------------------
/test/test-build-html.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import path from "path";
3 | import fs from "fs";
4 |
5 | describe("built HTML", () => {
6 | const indexHTMLPath = path.join(__dirname, "../dist/index.html");
7 |
8 | it("exists", () => {
9 | assert.ok(fs.readFileSync(indexHTMLPath));
10 | });
11 |
12 | it("contains a style tag", () => {
13 | const html = fs.readFileSync(indexHTMLPath);
14 | assert.ok(//.test(html));
15 | });
16 |
17 | it("contains a script tag", () => {
18 | const html = fs.readFileSync(indexHTMLPath);
19 | assert.ok(//.test(html));
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/test-html-renderer.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import fs from "fs";
3 | import helper from "./lib/helper";
4 | import HTMLRenderer from "../src/frontend/html-renderer";
5 | import http from "http";
6 | import MarkdownSocket from "../src/markdown-socket";
7 | import React from "react";
8 | import ReactTestUtils from "react-dom/test-utils";
9 |
10 | function getRenderedHTML(rendered) {
11 | const div = ReactTestUtils.findRenderedDOMComponentWithTag(rendered, "div");
12 | return div.innerHTML.replace(/ data-react[-\w]+="[^"]+"/g, "");
13 | }
14 |
15 | describe("HTMLRenderer", () => {
16 | let server;
17 | let mdSocket;
18 |
19 | beforeEach(done => {
20 | helper.makeDirectory("md-root");
21 | helper.createFile("md-root/test.md", "# hello");
22 | server = http.createServer((req, res) => res.end("hello"));
23 | mdSocket = new MarkdownSocket(helper.path("md-root"));
24 | mdSocket.listenTo(server);
25 | server.listen(1234, done);
26 | });
27 |
28 | afterEach(done => {
29 | helper.clean();
30 | mdSocket.close();
31 | server.close(done);
32 | });
33 |
34 | it("renders HTML parsed from Markdown with using Virtual DOM", done => {
35 | let rendered;
36 | let renderer = React.createElement(HTMLRenderer, {
37 | location: {
38 | host: "localhost:1234",
39 | pathname: "/test.md"
40 | },
41 | onUpdate() {
42 | assert.equal(getRenderedHTML(rendered), 'hello
\n');
43 | done();
44 | }
45 | });
46 | rendered = ReactTestUtils.renderIntoDocument(renderer);
47 | });
48 |
49 | it("re-renders whenever the file is updated", done => {
50 | const callback = err => {
51 | if (err) {
52 | done(err);
53 | }
54 | };
55 |
56 | let called = 0;
57 | let rendered;
58 | let renderer = React.createElement(HTMLRenderer, {
59 | location: {
60 | host: "localhost:1234",
61 | pathname: "/test.md"
62 | },
63 | onUpdate() {
64 | let html = getRenderedHTML(rendered);
65 | switch (called) {
66 | case 0:
67 | assert.equal(html, 'hello
\n');
68 | fs.writeFile(
69 | helper.path("md-root/test.md"),
70 | "```js\nvar a=10;\n```",
71 | callback
72 | );
73 | break;
74 | case 1:
75 | assert.equal(
76 | html,
77 | 'var a=10;\n
\n'
78 | );
79 | fs.writeFile(
80 | helper.path("md-root/test.md"),
81 | "* nested\n * nnested\n * nnnested",
82 | callback
83 | );
84 | break;
85 | case 2:
86 | assert.equal(
87 | html,
88 | "\n"
89 | );
90 | done();
91 | break;
92 | }
93 | called += 1;
94 | }
95 | });
96 | rendered = ReactTestUtils.renderIntoDocument(renderer);
97 | });
98 | });
99 |
--------------------------------------------------------------------------------
/test/test-index.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import helper from "./lib/helper";
3 | import path from "path";
4 | import request from "request";
5 | import { spawn } from "child_process";
6 |
7 | describe("index", () => {
8 | let proc;
9 | const cwd = process.cwd();
10 | const indexScriptPath = path.join(cwd, "index.js");
11 |
12 | beforeEach(() => {
13 | helper.makeDirectory("server-root");
14 | helper.createFile("server-root/test1.txt", "hello");
15 | process.chdir(helper.path("server-root"));
16 | });
17 |
18 | afterEach(done => {
19 | proc.on("close", done);
20 | proc.kill();
21 | helper.clean();
22 | process.chdir(cwd);
23 | });
24 |
25 | it("runs a server listening to a port", done => {
26 | proc = spawn("node", [indexScriptPath]);
27 | proc.stdout.on("data", data => {
28 | assert.equal(data.toString(), "listening 6060 ...\n");
29 | request.get("http://localhost:6060/test1.txt", (err, res, body) => {
30 | if (err) {
31 | done(err);
32 | return;
33 | }
34 |
35 | assert.equal(res.statusCode, 200);
36 | assert.equal(body, "hello");
37 | done();
38 | });
39 | });
40 | });
41 |
42 | it("runs a server listening to a custom port", done => {
43 | proc = spawn("node", [indexScriptPath, "-p", "1234"]);
44 | proc.stdout.on("data", data => {
45 | assert.equal(data.toString(), "listening 1234 ...\n");
46 | request.get("http://localhost:1234/test1.txt", (err, res, body) => {
47 | if (err) {
48 | done(err);
49 | return;
50 | }
51 |
52 | assert.equal(res.statusCode, 200);
53 | assert.equal(body, "hello");
54 | done();
55 | });
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/test/test-markdown-socket.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import fs from "fs";
3 | import helper from "./lib/helper";
4 | import http from "http";
5 | import MarkdownSocket from "../src/markdown-socket";
6 | import { w3cwebsocket as WebSocket } from "websocket";
7 |
8 | describe("MarkdownSocket", () => {
9 | let server;
10 | let mdSocket;
11 |
12 | beforeEach(done => {
13 | helper.makeDirectory("md-root");
14 | helper.createFile("md-root/test.md", "# hello");
15 | server = http.createServer((req, res) => {
16 | res.end("hello");
17 | });
18 | mdSocket = new MarkdownSocket(helper.path("md-root"));
19 | mdSocket.listenTo(server);
20 | server.listen(1234, done);
21 | });
22 |
23 | afterEach(done => {
24 | helper.clean();
25 | mdSocket.close();
26 | server.close(done);
27 | });
28 |
29 | it("handles a websocket connection", done => {
30 | let client = new WebSocket("ws://localhost:1234/test.md");
31 |
32 | client.onopen = () => {
33 | done();
34 | };
35 | });
36 |
37 | it("cannot handle a non markdown connection", done => {
38 | let client = new WebSocket("ws://localhost:1234");
39 |
40 | client.onerror = () => {
41 | done();
42 | };
43 | });
44 |
45 | it("opens a Markdown file and sends the parsed HTML", done => {
46 | let client = new WebSocket("ws://localhost:1234/test.md");
47 |
48 | client.onmessage = message => {
49 | assert.equal(message.data, 'hello
\n');
50 | done();
51 | };
52 | });
53 |
54 | it("sends parsed HTML data again when the file is updated", done => {
55 | const callback = err => {
56 | if (err) {
57 | done(err);
58 | }
59 | };
60 |
61 | let called = 0;
62 | let client = new WebSocket("ws://localhost:1234/test.md");
63 | client.onmessage = message => {
64 | switch (called) {
65 | case 0:
66 | assert.equal(message.data, 'hello
\n');
67 | fs.writeFile(
68 | helper.path("md-root/test.md"),
69 | "```js\nvar a=10;\n```",
70 | callback
71 | );
72 | break;
73 | case 1:
74 | assert.equal(
75 | message.data,
76 | 'var a=10;\n
\n'
77 | );
78 | fs.writeFile(
79 | helper.path("md-root/test.md"),
80 | "* nested\n * nnested\n * nnnested",
81 | callback
82 | );
83 | break;
84 | case 2:
85 | assert.equal(
86 | message.data,
87 | "\n"
88 | );
89 | done();
90 | break;
91 | }
92 | called += 1;
93 | };
94 | });
95 |
96 | it("ignores when there is no file for the path", done => {
97 | let client = new WebSocket("ws://localhost:1234/no-file.md");
98 | client.onmessage = message => {
99 | assert.equal(message.data, "Not found");
100 | done();
101 | };
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/test/test-markdown-watcher.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import fs from "fs";
3 | import helper from "./lib/helper";
4 | import MarkdownWatcher from "../src/markdown-watcher";
5 |
6 | describe("MarkdownWatcher", () => {
7 | let watcher;
8 |
9 | beforeEach(() => {
10 | helper.createFile("watcher-temp.md", "# hello");
11 | });
12 |
13 | afterEach(() => {
14 | watcher.stop();
15 | helper.clean();
16 | });
17 |
18 | it("reads a Markdown file and send parsed HTML data", done => {
19 | watcher = new MarkdownWatcher(helper.path("watcher-temp.md"));
20 | watcher
21 | .onData(data => {
22 | assert.equal(data, 'hello
\n');
23 | done();
24 | })
25 | .onError(done);
26 | });
27 |
28 | it("send parsed HTML data again when the file is updated", done => {
29 | const callback = err => {
30 | if (err) {
31 | done(err);
32 | }
33 | };
34 |
35 | let called = 0;
36 | watcher = new MarkdownWatcher(helper.path("watcher-temp.md"));
37 | watcher
38 | .onData(data => {
39 | switch (called) {
40 | case 0:
41 | assert.equal(data, 'hello
\n');
42 | fs.writeFile(
43 | helper.path("watcher-temp.md"),
44 | "```js\nvar a=10;\n```",
45 | callback
46 | );
47 | break;
48 | case 1:
49 | assert.equal(
50 | data,
51 | 'var a=10;\n
\n'
52 | );
53 | fs.writeFile(
54 | helper.path("watcher-temp.md"),
55 | "* nested\n * nnested\n * nnnested",
56 | callback
57 | );
58 | break;
59 | case 2:
60 | assert.equal(
61 | data,
62 | "\n"
63 | );
64 | done();
65 | break;
66 | }
67 | called += 1;
68 | })
69 | .onError(done);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/test-server.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import helper from "./lib/helper";
3 | import request from "request";
4 | import Server from "../src/server";
5 | import { w3cwebsocket as WebSocket } from "websocket";
6 |
7 | const TestPort = 1234;
8 |
9 | describe("Server", () => {
10 | let server;
11 |
12 | beforeEach(() => {
13 | helper.makeDirectory("server-root");
14 | helper.createFile("server-root/test1.txt", "hello");
15 | helper.createFile("server-root/test2.txt", "world");
16 | helper.createFile("server-root/test.md", "# hello");
17 | helper.createFile("server-root/test2.md", "# hello");
18 | helper.createFile("server-root/test3.MD", "# hello");
19 | helper.createFile("server-root/test4.markdown", "# hello");
20 | });
21 |
22 | afterEach(() => {
23 | server.close();
24 | helper.clean();
25 | });
26 |
27 | it("creates a file server on a given path", done => {
28 | server = new Server(helper.path("server-root"));
29 | server.listen(TestPort);
30 |
31 | let url = `http://localhost:${TestPort}/test1.txt`;
32 | request.get(url, (err, res, body) => {
33 | if (err) {
34 | done(err);
35 | return;
36 | }
37 |
38 | assert.equal(res.statusCode, 200);
39 | assert.equal(body, "hello");
40 |
41 | let url = `http://localhost:${TestPort}/test2.txt`;
42 | request.get(url, (err, res, body) => {
43 | if (err) {
44 | done(err);
45 | return;
46 | }
47 |
48 | assert.equal(res.statusCode, 200);
49 | assert.equal(body, "world");
50 | done();
51 | });
52 | });
53 | });
54 |
55 | it("fails when there is no file", done => {
56 | server = new Server(helper.path("server-root"));
57 | server.listen(TestPort);
58 |
59 | let url = `http://localhost:${TestPort}/test3.txt`;
60 | request.get(url, (err, res, body) => {
61 | if (err) {
62 | done(err);
63 | return;
64 | }
65 |
66 | assert.equal(res.statusCode, 404);
67 | assert.equal(body, "Not found");
68 | done();
69 | });
70 | });
71 |
72 | it("shows a list of Markdown files for directories", done => {
73 | server = new Server(helper.path("server-root"));
74 | server.listen(TestPort);
75 |
76 | let url = `http://localhost:${TestPort}/`;
77 | request.get(url, (err, res, body) => {
78 | if (err) {
79 | done(err);
80 | return;
81 | }
82 |
83 | assert.equal(res.statusCode, 200);
84 | assert.equal(
85 | body,
86 | "test.md test2.md test3.MD test4.markdown"
87 | );
88 | done();
89 | });
90 | });
91 |
92 | function previewTest(filename) {
93 | return new Promise((resolve, reject) => {
94 | request.get(
95 | `http://localhost:${TestPort}/${filename}`,
96 | (err, res, body) => {
97 | if (err) {
98 | reject(err);
99 | return;
100 | }
101 |
102 | assert.equal(res.statusCode, 200);
103 | assert.equal(res.headers["content-type"], "text/html");
104 | assert.ok(//.test(body));
105 | assert.ok(//.test(body));
106 | resolve();
107 | }
108 | );
109 | });
110 | }
111 |
112 | it("shows a preview page for Markdown files", async () => {
113 | server = new Server(helper.path("server-root"));
114 | server.listen(TestPort);
115 |
116 | await previewTest("test.md");
117 | await previewTest("test2.md");
118 | await previewTest("test3.MD");
119 | await previewTest("test4.markdown");
120 | });
121 |
122 | it("receives a websocket connection", done => {
123 | server = new Server(helper.path("server-root"));
124 | server.listen(TestPort);
125 |
126 | let url = `ws://localhost:${TestPort}/test.md`;
127 | let ws = new WebSocket(url);
128 | ws.onmessage = message => {
129 | assert.equal(message.data, 'hello
\n');
130 | done();
131 | };
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/test/test-socket-client.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import fs from "fs";
3 | import helper from "./lib/helper";
4 | import http from "http";
5 | import MarkdownSocket from "../src/markdown-socket";
6 | import SocketClient from "../src/frontend/socket-client";
7 |
8 | describe("SocketClient", () => {
9 | let server;
10 | let mdSocket;
11 |
12 | beforeEach(done => {
13 | helper.makeDirectory("md-root");
14 | helper.createFile("md-root/test.md", "# hello");
15 | server = http.createServer((req, res) => res.end("hello"));
16 | mdSocket = new MarkdownSocket(helper.path("md-root"));
17 | mdSocket.listenTo(server);
18 | server.listen(1234, done);
19 | });
20 |
21 | afterEach(done => {
22 | helper.clean();
23 | mdSocket.close();
24 | server.close(done);
25 | });
26 |
27 | it("receives HTML data sent from a Markdown socket server", done => {
28 | let client = new SocketClient({
29 | host: "localhost:1234",
30 | pathname: "/test.md"
31 | });
32 | client.onData(html => {
33 | assert.equal(html, 'hello
\n');
34 | done();
35 | });
36 | });
37 |
38 | it("receives the data whenever the file is updated", done => {
39 | const callback = err => {
40 | if (err) {
41 | done(err);
42 | }
43 | };
44 |
45 | let called = 0;
46 | let client = new SocketClient({
47 | host: "localhost:1234",
48 | pathname: "/test.md"
49 | });
50 | client.onData(html => {
51 | switch (called) {
52 | case 0:
53 | assert.equal(html, 'hello
\n');
54 | fs.writeFile(
55 | helper.path("md-root/test.md"),
56 | "```js\nvar a=10;\n```",
57 | callback
58 | );
59 | break;
60 | case 1:
61 | assert.equal(
62 | html,
63 | 'var a=10;\n
\n'
64 | );
65 | fs.writeFile(
66 | helper.path("md-root/test.md"),
67 | "* nested\n * nnested\n * nnnested",
68 | callback
69 | );
70 | break;
71 | case 2:
72 | assert.equal(
73 | html,
74 | "\n"
75 | );
76 | done();
77 | break;
78 | }
79 | called += 1;
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/test-watcher.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import fs from "fs";
3 | import helper from "./lib/helper";
4 | import Watcher from "../src/watcher";
5 |
6 | describe("Watcher", () => {
7 | let watcher;
8 |
9 | beforeEach(() => {
10 | helper.createFile("test테스트テスト.txt", "hello");
11 | });
12 |
13 | afterEach(() => {
14 | watcher.stop();
15 | helper.clean();
16 | });
17 |
18 | it("reads a file", done => {
19 | watcher = new Watcher(helper.path("test테스트テスト.txt"));
20 | watcher
21 | .onData(data => {
22 | assert.equal(data.toString(), "hello");
23 | done();
24 | })
25 | .onError(done);
26 | });
27 |
28 | it("cannot read a wrong file", done => {
29 | let watcher = new Watcher(helper.path("watcher-wrong-temp"));
30 | watcher
31 | .onData(() => {
32 | done("there shouldn't be a file!");
33 | })
34 | .onError(error => {
35 | assert.equal(error.code, "ENOENT");
36 | done();
37 | });
38 | });
39 |
40 | it("send the data again when the file is updated", done => {
41 | const callback = err => {
42 | if (err) {
43 | done(err);
44 | }
45 | };
46 |
47 | let called = 0;
48 | watcher = new Watcher(helper.path("test테스트テスト.txt"));
49 | watcher
50 | .onData(data => {
51 | switch (called) {
52 | case 0:
53 | assert.equal(data.toString(), "hello");
54 | fs.writeFile(
55 | helper.path("test테스트テスト.txt"),
56 | "world",
57 | callback
58 | );
59 | break;
60 | case 1:
61 | assert.equal(data.toString(), "world");
62 | fs.writeFile(helper.path("test테스트テスト.txt"), "pen!", callback);
63 | break;
64 | case 2:
65 | assert.equal(data.toString(), "pen!");
66 | done();
67 | break;
68 | }
69 | called += 1;
70 | })
71 | .onError(done);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | const ExtractTextPlugin = require("extract-text-webpack-plugin");
3 | const failPlugin = require("webpack-fail-plugin");
4 | const HTMLInlineSourcePlugin = require("html-webpack-inline-source-plugin");
5 | const HTMLPlugin = require("html-webpack-plugin");
6 | const path = require("path");
7 | const webpack = require("webpack");
8 |
9 | // Always enabled plugins
10 | const plugins = [
11 | // Extract CSS files to the 'bundle.css'.
12 | new ExtractTextPlugin("build.css"),
13 | new HTMLPlugin({
14 | title: "Pen",
15 | inlineSource: ".(js|css)$"
16 | }),
17 | new HTMLInlineSourcePlugin(),
18 | // This plugin should be always required. See https://github.com/webpack/webpack/issues/708
19 | failPlugin
20 | ];
21 |
22 | // Production only plugins
23 | if (process.env.NODE_ENV === "production") {
24 | plugins.push(
25 | // Pass the 'NODE_ENV=production' environment variable to the child processes.
26 | new webpack.DefinePlugin({
27 | "process.env": { NODE_ENV: JSON.stringify("production") }
28 | })
29 | );
30 | }
31 |
32 | // Configs
33 | module.exports = {
34 | entry: "./main.js",
35 | context: path.resolve(__dirname, "src/frontend"),
36 | output: {
37 | filename: "build.js",
38 | path: path.resolve(__dirname, "dist")
39 | },
40 | module: {
41 | loaders: [
42 | {
43 | test: /\.css$/,
44 | use: ExtractTextPlugin.extract({
45 | fallback: "style-loader",
46 | use: "css-loader"
47 | })
48 | },
49 | {
50 | test: /\.json$/,
51 | use: "json-loader"
52 | },
53 | {
54 | test: /\.js$/,
55 | exclude: /node_modules/,
56 | use: "babel-loader"
57 | }
58 | ]
59 | },
60 | plugins
61 | };
62 |
--------------------------------------------------------------------------------