UNPKG

jrnl-render

Version:

Render a jrnl (jrnl.sh) file as a webpage.

320 lines (295 loc) 13.8 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var React = _interopDefault(require('react')); var parse = _interopDefault(require('jrnl-parse')); var rehypeHighlight = _interopDefault(require('rehype-highlight')); var rehypeStringify = _interopDefault(require('rehype-stringify')); var remark = _interopDefault(require('remark')); var remarkPing = _interopDefault(require('remark-ping')); var remarkRehype = _interopDefault(require('remark-rehype')); var slugify = _interopDefault(require('slugify')); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; } || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function () { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } var checkStatus = function (response) { if (response.status >= 200 && response.status < 300) { return response; } throw new Error(response.statusText); }; var parseText = function (response) { return response.text(); }; var fetchTxt = function (url) { return fetch(url, { headers: { "Content-Type": "text/plain" } }) .then(checkStatus) .then(parseText); }; // https://stackoverflow.com/a/901144/1157536 function getQueryParam(name, url) { if (!url) { url = window.location.href; } name = name.replace(/[[\]]/g, "\\$&"); var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"); var results = regex.exec(url); if (!results) { return null; } if (!results[2]) { return ""; } return decodeURIComponent(results[2].replace(/\+/g, " ")); } // @ts-ignore function renderMarkdown(source, _a) { var _b = _a === void 0 ? {} : _a, _c = _b.simple, simple = _c === void 0 ? false : _c, _d = _b.tagURL, tagURL = _d === void 0 ? null : _d; // If simple is true, don't use remark-ping var plugins = (simple ? [] : [ [ // Use remark-ping to convert tags to links remarkPing, { pingUsername: function () { return true; }, userURL: tagURL } ] ]).concat([remarkRehype, rehypeStringify, rehypeHighlight]); var remarkInst = remark().use(plugins); return remarkInst.processSync(source).contents; } var Markdown = function (_a) { var source = _a.source, simple = _a.simple, tagURL = _a.tagURL, rest = __rest(_a, ["source", "simple", "tagURL"]); var rendered = renderMarkdown(source, { simple: simple, tagURL: tagURL }); return React.createElement("div", __assign({ dangerouslySetInnerHTML: { __html: rendered } }, rest)); }; var slugifyEntry = function (entry) { return formatDate(entry.date) + "-" + slugify(entry.title, { lower: true, remove: /[$*_+~.()'"!\-:@=]/g }); }; var formatDate = function (date) { return date.toISOString().slice(0, 10); }; // 2020-02-23 // NOTE: Defining this in ES5 due to // a peculiarity in Babel that breaks // instanceof usage with custom errors // https://stackoverflow.com/questions/33870684/why-doesnt-instanceof-work-on-instances-of-error-subclasses-under-babel-node/33877501#comment77815293_33877501 function TimeoutError() { var message = "Timeout expired"; Error.call(this, message); this.message = message; } var timeout = function (ms) { return new Promise(function (resolve, reject) { // @ts-ignore window.setTimeout(function () { return reject(new TimeoutError()); }, ms); }); }; /** * Call showLoaderFn if promise doesn't resolve in `delay` milliseconds. * * This is useful for showing a loading indicator only for slow HTTP * responses (to prevent flashing content). */ var delayedLoader = function (promise, showLoaderFn, delay) { return Promise.race([promise, timeout(delay)]).catch(function (err) { if (err instanceof TimeoutError) { return showLoaderFn(); } else { throw err; } }); }; var EntryBody = function (props) { var renderTag = function (tag) { return "?q=@" + tag; }; return (React.createElement("div", { className: "Entry-body f6 f5-l lh-copy" }, React.createElement(Markdown, { className: "u-markdown", source: props.body, tagURL: renderTag }))); }; var EntryContainer = function (_a) { var slug = _a.slug, date = _a.date, children = _a.children; return (React.createElement("article", { id: slug, className: "Entry bb b--black-10" }, React.createElement("div", { className: "db pb6 pt5 ph3 ph0-l" }, React.createElement("div", { className: "flex flex-column flex-row-ns" }, React.createElement("div", { className: "w-100" }, children))), React.createElement("time", { className: "Entry-date f7 code mb2 db ph3 ph0-l" }, React.createElement("a", { className: "gray no-underline", title: slug, href: "#" + slug }, formatDate(date))))); }; var Entry = function (_a) { var entry = _a.entry; return (React.createElement(EntryContainer, { slug: entry.slug, date: entry.date }, React.createElement("h1", { className: "Entry-title f4 f3-l fw7 mt0 lh-title" }, React.createElement("a", { className: "near-black no-underline", href: "#" + entry.slug }, entry.title + " ", React.createElement("span", { className: "Permalink silver" }))), React.createElement(EntryBody, { body: entry.body }))); }; var Empty = function (props) { return (React.createElement("div", { className: "Empty tc mt5 mh5 mt6-l mh6-l code gray vh-75" }, props.children)); }; var Loader = function (_a) { var message = _a.message; return (React.createElement("div", { className: "Loader tc mt5 mh5 mt6-l mh6-l code gray vh-75" }, React.createElement(Markdown, { source: message || "Loading entries…", simple: true }))); }; var SearchInput = function (props) { return (React.createElement("input", __assign({ className: "Search code f7 input-reset pa2 br2 ba b--black-20 bg-white hover-dark-blue" }, props))); }; var Footer = function (_a) { var copyright = _a.copyright; return (React.createElement("footer", { className: "Footer pv4 ph3 ph5-m ph6-l mid-gray" }, copyright && (React.createElement("small", { className: "Footer-copyright u-markdown f6 db tc" }, React.createElement(Markdown, { source: copyright, simple: true }))), React.createElement("div", { className: "tc mt3" }, React.createElement("small", { className: "Footer-postscript f6" }, "Written with", " ", React.createElement("a", { className: "link", href: "http://jrnl.sh", target: "_blank", rel: "noopener noreferrer" }, "jrnl"), ". Rendered with", " ", React.createElement("a", { className: "link", href: "https://github.com/sloria/jrnl-render", target: "_blank", rel: "noopener noreferrer" }, "jrnl-render"), ".")))); }; var Header = function (_a) { var title = _a.title, onInputChange = _a.onInputChange, filter = _a.filter; return (React.createElement("header", { className: "Header flex mt4 mb3 mw8 center" }, React.createElement("h2", { className: "Header-brand f4 mv0 pv2 ph3 ph0-l" }, React.createElement("a", { className: "no-underline hover-dark-pink near-black", href: "/" }, title || "JRNL")), React.createElement("div", { className: "flex-auto" }), React.createElement("div", { className: "Header-search tc lh-title flex mb1" }, React.createElement(SearchInput, { autoFocus: true, placeholder: "Search...", onChange: onInputChange, value: filter })))); }; var JRNL = function (_a) { var title = _a.title, source = _a.source, loaded = _a.loaded, loadingMessage = _a.loadingMessage, copyright = _a.copyright, filter = _a.filter, onInputChange = _a.onInputChange, onClickTag = _a.onClickTag; var parsed = source ? parse(source) : []; // Show entries in reverse chronological order var entries = parsed.reverse(); // Add slug field to each entry entries.forEach(function (entry) { entry.slug = slugifyEntry(entry); }); if (filter) { // Naive search. If all tokens are in the // title+body, it's a hit var entryFilter = function (entry) { var filterTokens = filter.split(/\s+/); var entryLower = (entry.title + "\n" + entry.body).toLowerCase(); for (var _i = 0, filterTokens_1 = filterTokens; _i < filterTokens_1.length; _i++) { var token = filterTokens_1[_i]; if (entryLower.indexOf(token) < 0) { return false; } } return true; }; entries = entries.filter(entryFilter); } return (React.createElement("div", { className: "App mw7 center sans-serif near-black" }, React.createElement(Header, { title: title || "", onInputChange: onInputChange, filter: filter }), React.createElement("section", { className: "min-vh-75" }, loaded === false ? (React.createElement(Loader, { message: loadingMessage || "" })) : loaded === null ? (React.createElement(Empty, null)) : entries.length ? (entries.map(function (entry) { return (React.createElement(Entry, { key: entry.slug, entry: entry, onClickTag: onClickTag })); })) : (React.createElement(Empty, null, React.createElement("p", null, "No entries to show. Try a different search.")))), React.createElement(Footer, { copyright: copyright || "" }))); }; JRNL.defaultProps = { loaded: true }; var App = /** @class */ (function (_super) { __extends(App, _super); function App(props) { var _this = _super.call(this, props) || this; _this.handleInputChange = function (e) { _this.setState({ filter: e.currentTarget.value }); }; _this.handleClickTag = function (tag) { _this.setState({ filter: tag }); }; _this.state = { // TODO: Error state filter: "", loaded: null, source: "" }; return _this; } App.prototype.componentDidMount = function () { var _this = this; var filter = getQueryParam("q") || ""; // Try to guess if a URL was passed if (this.props.url) { this.setState({ filter: filter }); var fetchPromise = fetchTxt(this.props.url).then(function (source) { _this.setState({ source: source, loaded: true }, function () { if (window.location.hash) { // setTimeout ensures that entries are fully // rendered before scrolling to the entry window.setTimeout(function () { var id = window.location.hash.slice(1); var elem = document.getElementById(id); if (elem) { elem.scrollIntoView(true); } }, 100); } }); }); // Don't show loading indicator if the request finishes // in < 300ms delayedLoader(fetchPromise, function () { return _this.setState({ loaded: false }); }, 300); } else if (this.props.source) { this.setState({ source: this.props.source }); } }; App.prototype.render = function () { return (React.createElement(JRNL, { loaded: this.state.loaded, loadingMessage: this.props.loadingMessage, title: this.props.title, copyright: this.props.copyright, source: this.state.source, filter: this.state.filter, onInputChange: this.handleInputChange, onClickTag: this.handleClickTag })); }; return App; }(React.Component)); module.exports = App;