jrnl-render
Version:
Render a jrnl (jrnl.sh) file as a webpage.
320 lines (295 loc) • 13.8 kB
JavaScript
;
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;