Repository: utatti/pen Branch: master Commit: 5b1e0123c25c Files: 34 Total size: 34.6 KB Directory structure: gitextract_h5t0zom2/ ├── .babelrc ├── .eslintignore ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin/ │ └── pen ├── index.js ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["env"] } ================================================ FILE: .eslintignore ================================================ # Built files dist ================================================ FILE: .eslintrc.yml ================================================ root: true parser: "babel-eslint" extends: - "eslint:recommended" - "plugin:prettier/recommended" rules: no-console: 0 env: node: true es6: true ================================================ FILE: .gitignore ================================================ # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules # Temporary files for tests /test/temp # Built files /dist ================================================ FILE: .npmignore ================================================ .DS_Store node_modules /.eslintignore /.eslintrc /.gitignore /.travis.yml /test/temp /resource # Built JS and CSS files /dist/build.js /dist/build.css ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 6 - 8 - node ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-2018 Jun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

logo

> We need a better Markdown previewer. [![travis](https://travis-ci.org/utatti/pen.svg)](https://travis-ci.org/utatti/pen) `pen` is a Markdown previewer written in JavaScript, aiming to *just work*. There are literally tons of Markdown previewers out there. I love some of them, I even made [one](https://github.com/utatti/orange-cat) of them. Nevertheless, we always need a better one, don't we? Using `pen` is super simple, we don't need to install any special editor or launch any GUI application. `pen` is just a tidy command-line tool. You can use your favourite editor and browser. No manual refresh is even needed. Also, the previewer renders the content using [React](https://facebook.github.io/react/). It means that it will not re-render entire DOM when the document is updated. This is a huge advantage because images or other media won't be reloaded for the DOM update. I personally love to use `pen`, and I hope you love it too. :black_nib: ## Demo Here is a short demo showing how awesome `pen` is. ![demo](https://cloud.githubusercontent.com/assets/1013641/9977359/21b79f66-5f3f-11e5-860a-cf19b2287009.gif) The following demo shows `pen` incrementally updates only modified part using [React](https://facebook.github.io/react/) and its [Reconciliation](https://reactjs.org/docs/reconciliation.html). ![incremental update](https://cloud.githubusercontent.com/assets/1013641/11914823/896591ba-a6cd-11e5-94ee-05e3ab50413b.gif) ## Requirement `pen` uses [Node.js >= 4.0](https://nodejs.org/en/docs/es6/). It may not work on earlier versions. ## Install Using [npm](http://npmjs.com): ```shell npm i -g pen ``` You can try using `pen` with `npx`: ```shell npx pen ``` ## Usage To use `pen`, simply run the `pen` command. ```shell pen README.md ``` The command above will launch a `pen` server and open the file in your default browser. The server will listen to a 6060 port by default. To be honest, you don't even need to launch it with a filename. You can manually open http://localhost:6060/README.md, or any other files in the same directory. To stop the server, enter `^C`. For the further details of the `pen` command, please enter `pen -h` or `pen --help`. ### Pandoc Pen uses [markdown-it](https://github.com/markdown-it/markdown-it) as Markdown parser by default, but it also supports Pandoc. Please provide [a proper Pandoc format](http://pandoc.org/MANUAL.html#general-options) for the value. ```shell pen --pandoc gfm README.md ``` ## Contribution I welcome every contribution on `pen`. You may start from forking and cloning this repo. ```shell git clone git@github.com:your_username/pen.git cd pen # Install dependencies npm i # Lint, build, and test pen codes at once npm test ``` To build frontend scripts: ```shell npm run build ``` To lint with [ESLint](http://eslint.org): ```shell npm run lint ``` To test with [Mocha](http://mochajs.org) ```shell npm run mocha ``` ## License Pen is released under the [MIT License](LICENSE). ================================================ FILE: bin/pen ================================================ #!/usr/bin/env node require('../index'); ================================================ FILE: index.js ================================================ "use strict"; const open = require("opn"); const Server = require("./src/server"); const argv = require("./src/argv"); let server = new Server(process.cwd()); server.listen(argv.port, () => { console.log(`listening ${argv.port} ...`); argv._.forEach(file => open(`http://localhost:${argv.port}/${file}`).catch(() => {}) ); }); ================================================ FILE: package.json ================================================ { "name": "pen", "version": "2.2.0", "description": "A better Markdown previewer", "main": "index.js", "scripts": { "test": "npm run lint && npm run build && npm run mocha", "lint": "eslint --ext .js --ignore-path .gitignore .", "lintfix": "eslint --fix --ext .js --ignore-path .gitignore .", "build": "NODE_ENV=production webpack -p", "mocha": "mocha -r babel-core/register -r babel-polyfill -r test/setup.js test/**/test-*", "release": "npm run build && npm publish --access=public" }, "repository": { "type": "git", "url": "git+https://github.com/utatti/pen.git" }, "keywords": [ "markdown", "previewer" ], "author": "Jun ", "license": "MIT", "bugs": { "url": "https://github.com/utatti/pen/issues" }, "homepage": "https://github.com/utatti/pen", "devDependencies": { "babel-core": "^6.26.3", "babel-eslint": "^8.2.3", "babel-loader": "^7.1.2", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.7.0", "css-loader": "^0.28.7", "eslint": "^4.19.1", "eslint-config-prettier": "^2.9.0", "eslint-plugin-prettier": "^2.6.0", "eslint-plugin-react": "^7.7.0", "extract-text-webpack-plugin": "^3.0.2", "github-markdown-css": "^2.9.0", "html-webpack-inline-source-plugin": "^0.0.9", "html-webpack-plugin": "^2.30.1", "jsdom": "^9.4.2", "json-loader": "^0.5.7", "mocha": "^5.2.0", "prettier": "^1.12.1", "react": "^16.2.0", "react-dom": "^16.2.0", "react-render-html": "^0.6.0", "request": "^2.87.0", "rimraf": "^2.5.4", "style-loader": "^0.19.0", "webpack": "^3.8.1", "webpack-fail-plugin": "^2.0.0" }, "dependencies": { "highlight.js": "^9.9.0", "markdown-it": "^8.2.2", "markdown-it-anchor": "^5.0.2", "markdown-it-checkbox": "^1.1.0", "markdown-it-emoji": "^1.2.0", "markdown-it-highlightjs": "^2.0.0", "opn": "^4.0.2", "prop-types": "^15.6.0", "simple-pandoc": "^0.1.0", "websocket": "^1.0.26", "yargs": "^6.6.0" }, "preferGlobal": true, "bin": { "pen": "./bin/pen" } } ================================================ FILE: src/argv.js ================================================ "use strict"; const DEFAULT_PORT = 6060; module.exports = require("yargs") .usage("Usage: $0 [options] [file]") .option("p", { alias: "port", default: DEFAULT_PORT, describe: "Set a custom port" }) .option("pandoc", { type: "string", describe: "Use local pandoc as markdown converter. Provide target format as the value." }) .help("h") .alias("h", "help").argv; ================================================ FILE: src/frontend/.eslintrc.yml ================================================ parserOptions: sourceType: module extends: - "eslint:recommended" - "plugin:react/recommended" env: browser: true ================================================ FILE: src/frontend/html-renderer.js ================================================ import React from "react"; import renderHTML from "react-render-html"; import SocketClient from "./socket-client"; import PropTypes from "prop-types"; class HTMLRenderer extends React.Component { constructor(props) { super(props); this.state = { html: "" }; } componentDidMount() { this.socketClient = new SocketClient(this.props.location); this.socketClient.onData(html => this.setState({ html })); } componentDidUpdate() { if (this.props.onUpdate) { this.props.onUpdate(); } } render() { return React.createElement("div", null, renderHTML(this.state.html)); } } HTMLRenderer.propTypes = { location: PropTypes.shape({ host: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired }).isRequired, onUpdate: PropTypes.func }; export default HTMLRenderer; ================================================ FILE: src/frontend/main.js ================================================ // Non-js dependencies import "github-markdown-css/github-markdown.css"; import "highlight.js/styles/foundation.css"; import "./style.css"; import React from "react"; import ReactDOM from "react-dom"; import HTMLRenderer from "./html-renderer"; const app = document.createElement("div"); app.setAttribute("id", "app"); app.setAttribute("class", "markdown-body"); document.body.appendChild(app); // Set title to Markdown filename const pathTokens = location.pathname.split("/"); document.title = pathTokens[pathTokens.length - 1]; ReactDOM.render(React.createElement(HTMLRenderer, { location }), app); ================================================ FILE: src/frontend/socket-client.js ================================================ import { w3cwebsocket as WebSocket } from "websocket"; export default class SocketClient { constructor(location) { this.host = location.host; this.pathname = location.pathname; const url = `ws://${this.host}${this.pathname}`; this._socket = new WebSocket(url); this._socket.onmessage = event => { this.triggerOnData(event.data); }; this._dataCallback = null; } onData(callback) { this._dataCallback = callback; return this; } triggerOnData(data) { if (this._dataCallback) { this._dataCallback(data); } } } ================================================ FILE: src/frontend/style.css ================================================ #app { max-width: 790px; margin: 30px auto; } /* checkbox */ input[type='checkbox'] { vertical-align: middle; margin: 0 0.5em 0.25em 0; } li:has(> input[type='checkbox']) { list-style-type:none; } li>input[type='checkbox'] { margin: 0 0.7em 0.25em -1.6em; } ================================================ FILE: src/markdown-socket.js ================================================ "use strict"; const MarkdownWatcher = require("./markdown-watcher"); const path = require("path"); const WebSocketServer = require("websocket").server; class MarkdownSocket { constructor(rootPath) { this.rootPath = rootPath; this._server = null; this.pathname = null; } listenTo(httpServer) { this._server = new WebSocketServer(); this._server.mount({ httpServer: httpServer }); this._server.on("request", this.onRequest.bind(this)); this._server.on("connect", this.onConnect.bind(this)); } onRequest(request) { const extname = path.extname(request.resource); if (extname !== ".md" && extname !== ".markdown") { request.reject(); return; } this.pathname = request.resource; request.accept(null, request.origin); } onConnect(connection) { const decodedPath = decodeURIComponent(this.pathname); const watcher = new MarkdownWatcher(path.join(this.rootPath, decodedPath)); watcher.onData(data => connection.send(data)); watcher.onError(err => { if (err.code === "ENOENT") { // if there is no file, ignore and send 'no file' connection.send("Not found"); return; } throw err; }); connection.on("close", () => { watcher.stop(); }); } close() { this._server.closeAllConnections(); } } module.exports = MarkdownSocket; ================================================ FILE: src/markdown-watcher.js ================================================ "use strict"; const convert = require("./markdown"); const Watcher = require("./watcher"); class MarkdownWatcher extends Watcher { onData(callback) { this._dataCallback = data => convert(data.toString()).then(callback); return this; } } module.exports = MarkdownWatcher; ================================================ FILE: src/markdown.js ================================================ "use strict"; const argv = require("./argv"); const mdit = require("markdown-it"); const pandoc = require("simple-pandoc"); const singleton = creator => { let obj; return () => obj || (obj = creator()); }; const md = singleton(() => mdit({ html: true, linkify: true }) .use(require("markdown-it-highlightjs")) .use(require("markdown-it-emoji")) .use(require("markdown-it-checkbox")) .use(require("markdown-it-anchor")) ); const pd = singleton(() => pandoc(argv.pandoc, "html")); module.exports = markdown => argv.pandoc ? pd()(markdown) : Promise.resolve(md().render(markdown)); ================================================ FILE: src/server.js ================================================ "use strict"; const fs = require("fs"); const http = require("http"); const path = require("path"); const urllib = require("url"); const MarkdownSocket = require("./markdown-socket"); function isMarkdown(path) { const lowerCasedPath = path.toLowerCase(); return lowerCasedPath.endsWith(".md") || lowerCasedPath.endsWith(".markdown"); } class Server { constructor(rootPath) { this.rootPath = rootPath; this._server = http.createServer(this.handler.bind(this)); this._ws = new MarkdownSocket(this.rootPath); this._ws.listenTo(this._server); } listen(port, cb) { this._server.listen(port, cb); } close(cb) { this._ws.close(); this._server.close(cb); } handler(req, res) { const url = urllib.parse(req.url); if (isMarkdown(url.pathname)) { this.handleAsMarkdown(res); } else { this.handleAsStatic(url.pathname, res); } } handleAsMarkdown(res) { res.setHeader("Content-Type", "text/html"); const indexHTMLPath = path.join(__dirname, "../dist/index.html"); fs.createReadStream(indexHTMLPath).pipe(res); } handleAsStatic(pathname, res) { const fullPath = path.join(this.rootPath, pathname); try { const stat = fs.statSync(fullPath); if (stat.isDirectory()) { if (!pathname.endsWith("/")) { res.writeHead(302, { Location: pathname + "/" }); res.end(); return; } const fileList = fs.readdirSync(fullPath).filter(isMarkdown); res.setHeader("Content-Type", "text/html"); res.end(fileList.map(f => `${f}`).join(" ")); } else { fs.createReadStream(fullPath).pipe(res); } } catch (err) { if (err.code === "ENOENT") { res.statusCode = 404; res.end("Not found"); } else { throw err; } } } } module.exports = Server; ================================================ FILE: src/watcher.js ================================================ "use strict"; const fs = require("fs"); const WatchInterval = 200; // milliseconds class Watcher { constructor(p) { this.path = p; this._watchLoop = null; this._dataCallback = null; this._errorCallback = null; this._previousData = null; this.start(); } start() { if (this._watchLoop) { clearInterval(this._watchLoop); } setTimeout(() => this.watch(), 0); // for the first execution this._watchLoop = setInterval(() => this.watch(), WatchInterval); } stop() { clearInterval(this._watchLoop); this._watchLoop = null; } watch() { fs.readFile(this.path, (error, data) => { if (error) { this.triggerOnError(error); this.stop(); } else { if (!this._previousData || data.compare(this._previousData) !== 0) { this.triggerOnData(data); this._previousData = data; } } }); } onData(callback) { this._dataCallback = callback; return this; } onError(callback) { this._errorCallback = callback; return this; } triggerOnData(data) { if (this._dataCallback) { this._dataCallback(data); } } triggerOnError(error) { if (this._errorCallback) { this._errorCallback(error); } else { throw error; } } } module.exports = Watcher; ================================================ FILE: test/.eslintrc.yml ================================================ parserOptions: sourceType: module extends: - "eslint:recommended" - "plugin:react/recommended" rules: no-console: 1 env: mocha: true ================================================ FILE: test/lib/helper.js ================================================ import fs from "fs"; import path from "path"; import rimraf from "rimraf"; const root = path.join(__dirname, "../temp"); function filePath(p) { return path.join(root, p); } const helper = { createFile(p, initialContent) { fs.writeFileSync(filePath(p), initialContent); }, makeDirectory(p) { try { fs.mkdirSync(filePath(p)); } catch (e) { if (e.code !== "EEXIST") { throw e; } } }, path(p) { return path.join(root, p); }, createRootDirectory() { try { fs.mkdirSync(root); } catch (e) { // do nothing } }, clean() { rimraf.sync(path.join(root, "*")); } }; helper.createRootDirectory(); helper.clean(); export default helper; ================================================ FILE: test/setup.js ================================================ import jsdom from "jsdom"; // test setup for browser mocking global.document = jsdom.jsdom(""); global.window = global.document.defaultView; global.navigator = { userAgent: "node.js" }; ================================================ FILE: test/test-build-html.js ================================================ import assert from "assert"; import path from "path"; import fs from "fs"; describe("built HTML", () => { const indexHTMLPath = path.join(__dirname, "../dist/index.html"); it("exists", () => { assert.ok(fs.readFileSync(indexHTMLPath)); }); it("contains a style tag", () => { const html = fs.readFileSync(indexHTMLPath); assert.ok(//.test(html)); }); it("contains a script tag", () => { const html = fs.readFileSync(indexHTMLPath); assert.ok(//.test(html)); }); }); ================================================ FILE: test/test-html-renderer.js ================================================ import assert from "assert"; import fs from "fs"; import helper from "./lib/helper"; import HTMLRenderer from "../src/frontend/html-renderer"; import http from "http"; import MarkdownSocket from "../src/markdown-socket"; import React from "react"; import ReactTestUtils from "react-dom/test-utils"; function getRenderedHTML(rendered) { const div = ReactTestUtils.findRenderedDOMComponentWithTag(rendered, "div"); return div.innerHTML.replace(/ data-react[-\w]+="[^"]+"/g, ""); } describe("HTMLRenderer", () => { let server; let mdSocket; beforeEach(done => { helper.makeDirectory("md-root"); helper.createFile("md-root/test.md", "# hello"); server = http.createServer((req, res) => res.end("hello")); mdSocket = new MarkdownSocket(helper.path("md-root")); mdSocket.listenTo(server); server.listen(1234, done); }); afterEach(done => { helper.clean(); mdSocket.close(); server.close(done); }); it("renders HTML parsed from Markdown with using Virtual DOM", done => { let rendered; let renderer = React.createElement(HTMLRenderer, { location: { host: "localhost:1234", pathname: "/test.md" }, onUpdate() { assert.equal(getRenderedHTML(rendered), '

hello

\n'); done(); } }); rendered = ReactTestUtils.renderIntoDocument(renderer); }); it("re-renders whenever the file is updated", done => { const callback = err => { if (err) { done(err); } }; let called = 0; let rendered; let renderer = React.createElement(HTMLRenderer, { location: { host: "localhost:1234", pathname: "/test.md" }, onUpdate() { let html = getRenderedHTML(rendered); switch (called) { case 0: assert.equal(html, '

hello

\n'); fs.writeFile( helper.path("md-root/test.md"), "```js\nvar a=10;\n```", callback ); break; case 1: assert.equal( html, '
var a=10;\n
\n' ); fs.writeFile( helper.path("md-root/test.md"), "* nested\n * nnested\n * nnnested", callback ); break; case 2: assert.equal( html, "
    \n
  • nested\n
      \n
    • nnested\n
        \n
      • nnnested
      • \n
      \n
    • \n
    \n
  • \n
\n" ); done(); break; } called += 1; } }); rendered = ReactTestUtils.renderIntoDocument(renderer); }); }); ================================================ FILE: test/test-index.js ================================================ import assert from "assert"; import helper from "./lib/helper"; import path from "path"; import request from "request"; import { spawn } from "child_process"; describe("index", () => { let proc; const cwd = process.cwd(); const indexScriptPath = path.join(cwd, "index.js"); beforeEach(() => { helper.makeDirectory("server-root"); helper.createFile("server-root/test1.txt", "hello"); process.chdir(helper.path("server-root")); }); afterEach(done => { proc.on("close", done); proc.kill(); helper.clean(); process.chdir(cwd); }); it("runs a server listening to a port", done => { proc = spawn("node", [indexScriptPath]); proc.stdout.on("data", data => { assert.equal(data.toString(), "listening 6060 ...\n"); request.get("http://localhost:6060/test1.txt", (err, res, body) => { if (err) { done(err); return; } assert.equal(res.statusCode, 200); assert.equal(body, "hello"); done(); }); }); }); it("runs a server listening to a custom port", done => { proc = spawn("node", [indexScriptPath, "-p", "1234"]); proc.stdout.on("data", data => { assert.equal(data.toString(), "listening 1234 ...\n"); request.get("http://localhost:1234/test1.txt", (err, res, body) => { if (err) { done(err); return; } assert.equal(res.statusCode, 200); assert.equal(body, "hello"); done(); }); }); }); }); ================================================ FILE: test/test-markdown-socket.js ================================================ import assert from "assert"; import fs from "fs"; import helper from "./lib/helper"; import http from "http"; import MarkdownSocket from "../src/markdown-socket"; import { w3cwebsocket as WebSocket } from "websocket"; describe("MarkdownSocket", () => { let server; let mdSocket; beforeEach(done => { helper.makeDirectory("md-root"); helper.createFile("md-root/test.md", "# hello"); server = http.createServer((req, res) => { res.end("hello"); }); mdSocket = new MarkdownSocket(helper.path("md-root")); mdSocket.listenTo(server); server.listen(1234, done); }); afterEach(done => { helper.clean(); mdSocket.close(); server.close(done); }); it("handles a websocket connection", done => { let client = new WebSocket("ws://localhost:1234/test.md"); client.onopen = () => { done(); }; }); it("cannot handle a non markdown connection", done => { let client = new WebSocket("ws://localhost:1234"); client.onerror = () => { done(); }; }); it("opens a Markdown file and sends the parsed HTML", done => { let client = new WebSocket("ws://localhost:1234/test.md"); client.onmessage = message => { assert.equal(message.data, '

hello

\n'); done(); }; }); it("sends parsed HTML data again when the file is updated", done => { const callback = err => { if (err) { done(err); } }; let called = 0; let client = new WebSocket("ws://localhost:1234/test.md"); client.onmessage = message => { switch (called) { case 0: assert.equal(message.data, '

hello

\n'); fs.writeFile( helper.path("md-root/test.md"), "```js\nvar a=10;\n```", callback ); break; case 1: assert.equal( message.data, '
var a=10;\n
\n' ); fs.writeFile( helper.path("md-root/test.md"), "* nested\n * nnested\n * nnnested", callback ); break; case 2: assert.equal( message.data, "
    \n
  • nested\n
      \n
    • nnested\n
        \n
      • nnnested
      • \n
      \n
    • \n
    \n
  • \n
\n" ); done(); break; } called += 1; }; }); it("ignores when there is no file for the path", done => { let client = new WebSocket("ws://localhost:1234/no-file.md"); client.onmessage = message => { assert.equal(message.data, "Not found"); done(); }; }); }); ================================================ FILE: test/test-markdown-watcher.js ================================================ import assert from "assert"; import fs from "fs"; import helper from "./lib/helper"; import MarkdownWatcher from "../src/markdown-watcher"; describe("MarkdownWatcher", () => { let watcher; beforeEach(() => { helper.createFile("watcher-temp.md", "# hello"); }); afterEach(() => { watcher.stop(); helper.clean(); }); it("reads a Markdown file and send parsed HTML data", done => { watcher = new MarkdownWatcher(helper.path("watcher-temp.md")); watcher .onData(data => { assert.equal(data, '

hello

\n'); done(); }) .onError(done); }); it("send parsed HTML data again when the file is updated", done => { const callback = err => { if (err) { done(err); } }; let called = 0; watcher = new MarkdownWatcher(helper.path("watcher-temp.md")); watcher .onData(data => { switch (called) { case 0: assert.equal(data, '

hello

\n'); fs.writeFile( helper.path("watcher-temp.md"), "```js\nvar a=10;\n```", callback ); break; case 1: assert.equal( data, '
var a=10;\n
\n' ); fs.writeFile( helper.path("watcher-temp.md"), "* nested\n * nnested\n * nnnested", callback ); break; case 2: assert.equal( data, "
    \n
  • nested\n
      \n
    • nnested\n
        \n
      • nnnested
      • \n
      \n
    • \n
    \n
  • \n
\n" ); done(); break; } called += 1; }) .onError(done); }); }); ================================================ FILE: test/test-server.js ================================================ import assert from "assert"; import helper from "./lib/helper"; import request from "request"; import Server from "../src/server"; import { w3cwebsocket as WebSocket } from "websocket"; const TestPort = 1234; describe("Server", () => { let server; beforeEach(() => { helper.makeDirectory("server-root"); helper.createFile("server-root/test1.txt", "hello"); helper.createFile("server-root/test2.txt", "world"); helper.createFile("server-root/test.md", "# hello"); helper.createFile("server-root/test2.md", "# hello"); helper.createFile("server-root/test3.MD", "# hello"); helper.createFile("server-root/test4.markdown", "# hello"); }); afterEach(() => { server.close(); helper.clean(); }); it("creates a file server on a given path", done => { server = new Server(helper.path("server-root")); server.listen(TestPort); let url = `http://localhost:${TestPort}/test1.txt`; request.get(url, (err, res, body) => { if (err) { done(err); return; } assert.equal(res.statusCode, 200); assert.equal(body, "hello"); let url = `http://localhost:${TestPort}/test2.txt`; request.get(url, (err, res, body) => { if (err) { done(err); return; } assert.equal(res.statusCode, 200); assert.equal(body, "world"); done(); }); }); }); it("fails when there is no file", done => { server = new Server(helper.path("server-root")); server.listen(TestPort); let url = `http://localhost:${TestPort}/test3.txt`; request.get(url, (err, res, body) => { if (err) { done(err); return; } assert.equal(res.statusCode, 404); assert.equal(body, "Not found"); done(); }); }); it("shows a list of Markdown files for directories", done => { server = new Server(helper.path("server-root")); server.listen(TestPort); let url = `http://localhost:${TestPort}/`; request.get(url, (err, res, body) => { if (err) { done(err); return; } assert.equal(res.statusCode, 200); assert.equal( body, "test.md test2.md test3.MD test4.markdown" ); done(); }); }); function previewTest(filename) { return new Promise((resolve, reject) => { request.get( `http://localhost:${TestPort}/${filename}`, (err, res, body) => { if (err) { reject(err); return; } assert.equal(res.statusCode, 200); assert.equal(res.headers["content-type"], "text/html"); assert.ok(//.test(body)); assert.ok(//.test(body)); resolve(); } ); }); } it("shows a preview page for Markdown files", async () => { server = new Server(helper.path("server-root")); server.listen(TestPort); await previewTest("test.md"); await previewTest("test2.md"); await previewTest("test3.MD"); await previewTest("test4.markdown"); }); it("receives a websocket connection", done => { server = new Server(helper.path("server-root")); server.listen(TestPort); let url = `ws://localhost:${TestPort}/test.md`; let ws = new WebSocket(url); ws.onmessage = message => { assert.equal(message.data, '

hello

\n'); done(); }; }); }); ================================================ FILE: test/test-socket-client.js ================================================ import assert from "assert"; import fs from "fs"; import helper from "./lib/helper"; import http from "http"; import MarkdownSocket from "../src/markdown-socket"; import SocketClient from "../src/frontend/socket-client"; describe("SocketClient", () => { let server; let mdSocket; beforeEach(done => { helper.makeDirectory("md-root"); helper.createFile("md-root/test.md", "# hello"); server = http.createServer((req, res) => res.end("hello")); mdSocket = new MarkdownSocket(helper.path("md-root")); mdSocket.listenTo(server); server.listen(1234, done); }); afterEach(done => { helper.clean(); mdSocket.close(); server.close(done); }); it("receives HTML data sent from a Markdown socket server", done => { let client = new SocketClient({ host: "localhost:1234", pathname: "/test.md" }); client.onData(html => { assert.equal(html, '

hello

\n'); done(); }); }); it("receives the data whenever the file is updated", done => { const callback = err => { if (err) { done(err); } }; let called = 0; let client = new SocketClient({ host: "localhost:1234", pathname: "/test.md" }); client.onData(html => { switch (called) { case 0: assert.equal(html, '

hello

\n'); fs.writeFile( helper.path("md-root/test.md"), "```js\nvar a=10;\n```", callback ); break; case 1: assert.equal( html, '
var a=10;\n
\n' ); fs.writeFile( helper.path("md-root/test.md"), "* nested\n * nnested\n * nnnested", callback ); break; case 2: assert.equal( html, "
    \n
  • nested\n
      \n
    • nnested\n
        \n
      • nnnested
      • \n
      \n
    • \n
    \n
  • \n
\n" ); done(); break; } called += 1; }); }); }); ================================================ FILE: test/test-watcher.js ================================================ import assert from "assert"; import fs from "fs"; import helper from "./lib/helper"; import Watcher from "../src/watcher"; describe("Watcher", () => { let watcher; beforeEach(() => { helper.createFile("test테스트テスト.txt", "hello"); }); afterEach(() => { watcher.stop(); helper.clean(); }); it("reads a file", done => { watcher = new Watcher(helper.path("test테스트テスト.txt")); watcher .onData(data => { assert.equal(data.toString(), "hello"); done(); }) .onError(done); }); it("cannot read a wrong file", done => { let watcher = new Watcher(helper.path("watcher-wrong-temp")); watcher .onData(() => { done("there shouldn't be a file!"); }) .onError(error => { assert.equal(error.code, "ENOENT"); done(); }); }); it("send the data again when the file is updated", done => { const callback = err => { if (err) { done(err); } }; let called = 0; watcher = new Watcher(helper.path("test테스트テスト.txt")); watcher .onData(data => { switch (called) { case 0: assert.equal(data.toString(), "hello"); fs.writeFile( helper.path("test테스트テスト.txt"), "world", callback ); break; case 1: assert.equal(data.toString(), "world"); fs.writeFile(helper.path("test테스트テスト.txt"), "pen!", callback); break; case 2: assert.equal(data.toString(), "pen!"); done(); break; } called += 1; }) .onError(done); }); }); ================================================ FILE: webpack.config.js ================================================ "use strict"; const ExtractTextPlugin = require("extract-text-webpack-plugin"); const failPlugin = require("webpack-fail-plugin"); const HTMLInlineSourcePlugin = require("html-webpack-inline-source-plugin"); const HTMLPlugin = require("html-webpack-plugin"); const path = require("path"); const webpack = require("webpack"); // Always enabled plugins const plugins = [ // Extract CSS files to the 'bundle.css'. new ExtractTextPlugin("build.css"), new HTMLPlugin({ title: "Pen", inlineSource: ".(js|css)$" }), new HTMLInlineSourcePlugin(), // This plugin should be always required. See https://github.com/webpack/webpack/issues/708 failPlugin ]; // Production only plugins if (process.env.NODE_ENV === "production") { plugins.push( // Pass the 'NODE_ENV=production' environment variable to the child processes. new webpack.DefinePlugin({ "process.env": { NODE_ENV: JSON.stringify("production") } }) ); } // Configs module.exports = { entry: "./main.js", context: path.resolve(__dirname, "src/frontend"), output: { filename: "build.js", path: path.resolve(__dirname, "dist") }, module: { loaders: [ { test: /\.css$/, use: ExtractTextPlugin.extract({ fallback: "style-loader", use: "css-loader" }) }, { test: /\.json$/, use: "json-loader" }, { test: /\.js$/, exclude: /node_modules/, use: "babel-loader" } ] }, plugins };