mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Optimised markdown rendering for mobile and electron
This commit is contained in:
parent
5024dcf668
commit
9e3af21e66
@ -1,7 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { Note } = require('lib/models/note.js');
|
const { Note } = require('lib/models/note.js');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { MdToHtml } = require('lib/markdown-utils.js');
|
const MdToHtml = require('lib/MdToHtml');
|
||||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||||
|
|
||||||
class NoteTextComponent extends React.Component {
|
class NoteTextComponent extends React.Component {
|
||||||
|
335
ReactNativeClient/lib/MdToHtml.js
Normal file
335
ReactNativeClient/lib/MdToHtml.js
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
const MarkdownIt = require('markdown-it');
|
||||||
|
const Entities = require('html-entities').AllHtmlEntities;
|
||||||
|
const htmlentities = (new Entities()).encode;
|
||||||
|
const { Resource } = require('lib/models/resource.js');
|
||||||
|
const { shim } = require('lib/shim.js');
|
||||||
|
const md5 = require('md5');
|
||||||
|
|
||||||
|
class MdToHtml {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadedResources_ = {};
|
||||||
|
this.cachedContent_ = null;
|
||||||
|
this.cachedContentKey_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeContentKey(resources, body, style, options) {
|
||||||
|
let k = [];
|
||||||
|
for (let n in resources) {
|
||||||
|
if (!resources.hasOwnProperty(n)) continue;
|
||||||
|
const r = resources[n];
|
||||||
|
k.push(r.id);
|
||||||
|
}
|
||||||
|
k.push(md5(body));
|
||||||
|
k.push(md5(JSON.stringify(style)));
|
||||||
|
k.push(md5(JSON.stringify(options)));
|
||||||
|
return k.join('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAttrs_(attrs) {
|
||||||
|
if (!attrs) return '';
|
||||||
|
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < attrs.length; i++) {
|
||||||
|
const n = attrs[i][0];
|
||||||
|
const v = attrs[i].length >= 2 ? attrs[i][1] : null;
|
||||||
|
|
||||||
|
if (n === 'alt' && !v) {
|
||||||
|
continue;
|
||||||
|
} else if (n === 'src') {
|
||||||
|
output.push('src="' + htmlentities(v) + '"');
|
||||||
|
} else {
|
||||||
|
output.push(n + '="' + (v ? htmlentities(v) : '') + '"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAttr_(attrs, name) {
|
||||||
|
for (let i = 0; i < attrs.length; i++) {
|
||||||
|
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAttr_(attrs, name, value) {
|
||||||
|
for (let i = 0; i < attrs.length; i++) {
|
||||||
|
if (attrs[i][0] === name) {
|
||||||
|
attrs[i][1] = value;
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attrs.push([name, value]);
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderImage_(attrs, options) {
|
||||||
|
const loadResource = async (id) => {
|
||||||
|
console.info('Loading resource: ' + id);
|
||||||
|
|
||||||
|
// Initially set to to an empty object to make
|
||||||
|
// it clear that it is being loaded. Otherwise
|
||||||
|
// it sometimes results in multiple calls to
|
||||||
|
// loadResource() for the same resource.
|
||||||
|
this.loadedResources_[id] = {};
|
||||||
|
|
||||||
|
const resource = await Resource.load(id);
|
||||||
|
resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource));
|
||||||
|
|
||||||
|
let newResources = Object.assign({}, this.loadedResources_);
|
||||||
|
newResources[id] = resource;
|
||||||
|
this.loadedResources_ = newResources;
|
||||||
|
|
||||||
|
console.info('Resource loaded: ', resource.title);
|
||||||
|
|
||||||
|
if (options.onResourceLoaded) options.onResourceLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = this.getAttr_(attrs, 'title');
|
||||||
|
const href = this.getAttr_(attrs, 'src');
|
||||||
|
|
||||||
|
if (!Resource.isResourceUrl(href)) {
|
||||||
|
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceId = Resource.urlToId(href);
|
||||||
|
if (!this.loadedResources_[resourceId]) {
|
||||||
|
loadResource(resourceId);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = this.loadedResources_[resourceId];
|
||||||
|
if (!r.base64) return '';
|
||||||
|
|
||||||
|
const mime = r.mime.toLowerCase();
|
||||||
|
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
|
||||||
|
const src = 'data:' + r.mime + ';base64,' + r.base64;
|
||||||
|
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
|
||||||
|
}
|
||||||
|
|
||||||
|
renderOpenLink_(attrs, options) {
|
||||||
|
const href = this.getAttr_(attrs, 'href');
|
||||||
|
const title = this.getAttr_(attrs, 'title');
|
||||||
|
const text = this.getAttr_(attrs, 'text');
|
||||||
|
|
||||||
|
if (Resource.isResourceUrl(href)) {
|
||||||
|
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
|
||||||
|
// Ideally they should be opened in the user's browser.
|
||||||
|
return '[Resource not yet supported: ' + htmlentities(text) + ']';
|
||||||
|
} else {
|
||||||
|
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
||||||
|
//let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
|
||||||
|
let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>";
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCloseLink_(attrs, options) {
|
||||||
|
const href = this.getAttr_(attrs, 'href');
|
||||||
|
|
||||||
|
if (Resource.isResourceUrl(href)) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTokens_(tokens, options) {
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const t = tokens[i];
|
||||||
|
|
||||||
|
let tag = t.tag;
|
||||||
|
let openTag = null;
|
||||||
|
let closeTag = null;
|
||||||
|
let attrs = t.attrs ? t.attrs : [];
|
||||||
|
|
||||||
|
if (t.map) attrs.push(['data-map', t.map.join(':')]);
|
||||||
|
|
||||||
|
if (tag && t.type.indexOf('_open') >= 0) {
|
||||||
|
openTag = tag;
|
||||||
|
} else if (tag && t.type.indexOf('_close') >= 0) {
|
||||||
|
closeTag = tag;
|
||||||
|
} else if (tag && t.type.indexOf('inline') >= 0) {
|
||||||
|
openTag = tag;
|
||||||
|
} else if (t.type === 'link_open') {
|
||||||
|
openTag = 'a';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openTag) {
|
||||||
|
if (openTag === 'a') {
|
||||||
|
output.push(this.renderOpenLink_(attrs, options));
|
||||||
|
} else {
|
||||||
|
const attrsHtml = attrs ? this.renderAttrs_(attrs) : '';
|
||||||
|
output.push('<' + openTag + (attrsHtml ? ' ' + attrsHtml : '') + '>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.type === 'image') {
|
||||||
|
if (t.content) attrs.push(['title', t.content]);
|
||||||
|
output.push(this.renderImage_(attrs, options));
|
||||||
|
} else {
|
||||||
|
if (t.children) {
|
||||||
|
const parsedChildren = this.renderTokens_(t.children, options);
|
||||||
|
output = output.concat(parsedChildren);
|
||||||
|
} else {
|
||||||
|
if (t.content) {
|
||||||
|
output.push(htmlentities(t.content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.type === 'link_close') {
|
||||||
|
closeTag = 'a';
|
||||||
|
} else if (tag && t.type.indexOf('inline') >= 0) {
|
||||||
|
closeTag = openTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeTag) {
|
||||||
|
if (closeTag === 'a') {
|
||||||
|
output.push(this.renderCloseLink_(attrs, options));
|
||||||
|
} else {
|
||||||
|
output.push('</' + closeTag + '>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
render(body, style, options = null) {
|
||||||
|
if (!options) options = {};
|
||||||
|
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
||||||
|
|
||||||
|
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
|
||||||
|
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;
|
||||||
|
|
||||||
|
const md = new MarkdownIt();
|
||||||
|
const env = {};
|
||||||
|
|
||||||
|
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
|
||||||
|
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
|
||||||
|
// that it can be removed and replaced later on.
|
||||||
|
const HORRIBLE_HACK = true;
|
||||||
|
|
||||||
|
if (HORRIBLE_HACK) {
|
||||||
|
let counter = -1;
|
||||||
|
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
||||||
|
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
||||||
|
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
||||||
|
counter++;
|
||||||
|
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = md.parse(body, env);
|
||||||
|
|
||||||
|
// console.info(body);
|
||||||
|
// console.info(tokens);
|
||||||
|
|
||||||
|
let renderedBody = this.renderTokens_(tokens, options);
|
||||||
|
|
||||||
|
if (HORRIBLE_HACK) {
|
||||||
|
let loopCount = 0;
|
||||||
|
while (renderedBody.indexOf('mJOPm') >= 0) {
|
||||||
|
renderedBody = renderedBody.replace(/mJOPmCHECKBOXm([A-Z]+)m(\d+)m/, function(v, type, index) {
|
||||||
|
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
|
||||||
|
return '<a href="#" onclick="' + js + '" class="checkbox">' + (type == 'NOTICK' ? '☐' : '☑') + '</a>';
|
||||||
|
});
|
||||||
|
if (loopCount++ >= 9999) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://necolas.github.io/normalize.css/
|
||||||
|
const normalizeCss = `
|
||||||
|
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
|
||||||
|
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
body {
|
||||||
|
font-size: ` + style.htmlFontSize + `;
|
||||||
|
color: ` + style.htmlColor + `;
|
||||||
|
line-height: 1.5em;
|
||||||
|
background-color: ` + style.htmlBackgroundColor + `;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: ` + style.htmlLinkColor + `
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
a.checkbox {
|
||||||
|
font-size: 1.6em;
|
||||||
|
position: relative;
|
||||||
|
top: 0.1em;
|
||||||
|
text-decoration: none;
|
||||||
|
color: ` + style.htmlColor + `;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
td, th {
|
||||||
|
border: 1px solid silver;
|
||||||
|
padding: .5em 1em .5em 1em;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 1px solid ` + style.htmlDividerColor + `;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
|
||||||
|
|
||||||
|
const output = styleHtml + renderedBody;
|
||||||
|
this.cachedContent_ = output;
|
||||||
|
this.cachedContentKey_ = cacheKey;
|
||||||
|
return this.cachedContent_;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTickAt(body, index) {
|
||||||
|
let counter = -1;
|
||||||
|
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
||||||
|
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
||||||
|
if (index == counter) {
|
||||||
|
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';
|
||||||
|
}
|
||||||
|
return '°°JOP°CHECKBOX°' + s + '°°';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]');
|
||||||
|
body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]');
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckboxClick(msg, noteBody) {
|
||||||
|
msg = msg.split(':');
|
||||||
|
let index = Number(msg[msg.length - 1]);
|
||||||
|
let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway
|
||||||
|
return this.toggleTickAt(noteBody, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MdToHtml;
|
@ -3,7 +3,7 @@ const { WebView, View, Linking } = require('react-native');
|
|||||||
const { globalStyle } = require('lib/components/global-style.js');
|
const { globalStyle } = require('lib/components/global-style.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const { Resource } = require('lib/models/resource.js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { MdToHtml } = require('lib/markdown-utils.js');
|
const MdToHtml = require('lib/MdToHtml.js');
|
||||||
|
|
||||||
class NoteBodyViewer extends Component {
|
class NoteBodyViewer extends Component {
|
||||||
|
|
||||||
|
@ -1,445 +1,3 @@
|
|||||||
const marked = require('lib/marked.js');
|
|
||||||
const Entities = require('html-entities').AllHtmlEntities;
|
|
||||||
const htmlentities = (new Entities()).encode;
|
|
||||||
|
|
||||||
class MdToHtml {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.loadedResources_ = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAttrs_(attrs) {
|
|
||||||
if (!attrs) return '';
|
|
||||||
|
|
||||||
let output = [];
|
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
|
||||||
const n = attrs[i][0];
|
|
||||||
const v = attrs[i].length >= 2 ? attrs[i][1] : null;
|
|
||||||
|
|
||||||
if (n === 'alt' && !v) {
|
|
||||||
continue;
|
|
||||||
} else if (n === 'src') {
|
|
||||||
output.push('src="' + htmlentities(v) + '"');
|
|
||||||
} else {
|
|
||||||
output.push(n + '="' + (v ? htmlentities(v) : '') + '"');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttr_(attrs, name) {
|
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
|
||||||
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttr_(attrs, name, value) {
|
|
||||||
for (let i = 0; i < attrs.length; i++) {
|
|
||||||
if (attrs[i][0] === name) {
|
|
||||||
attrs[i][1] = value;
|
|
||||||
return attrs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attrs.push([name, value]);
|
|
||||||
return attrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderImage_(attrs, options) {
|
|
||||||
const { Resource } = require('lib/models/resource.js');
|
|
||||||
const { shim } = require('lib/shim.js');
|
|
||||||
|
|
||||||
const loadResource = async (id) => {
|
|
||||||
const resource = await Resource.load(id);
|
|
||||||
resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource));
|
|
||||||
|
|
||||||
let newResources = Object.assign({}, this.loadedResources_);
|
|
||||||
newResources[id] = resource;
|
|
||||||
this.loadedResources_ = newResources;
|
|
||||||
|
|
||||||
if (options.onResourceLoaded) options.onResourceLoaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = this.getAttr_(attrs, 'title');
|
|
||||||
const href = this.getAttr_(attrs, 'src');
|
|
||||||
|
|
||||||
if (!Resource.isResourceUrl(href)) {
|
|
||||||
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceId = Resource.urlToId(href);
|
|
||||||
if (!this.loadedResources_[resourceId]) {
|
|
||||||
loadResource(resourceId);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = this.loadedResources_[resourceId];
|
|
||||||
const mime = r.mime.toLowerCase();
|
|
||||||
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
|
|
||||||
const src = 'data:' + r.mime + ';base64,' + r.base64;
|
|
||||||
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLink_(attrs, options) {
|
|
||||||
const { Resource } = require('lib/models/resource.js');
|
|
||||||
|
|
||||||
const href = this.getAttr_(attrs, 'href');
|
|
||||||
const title = this.getAttr_(attrs, 'title');
|
|
||||||
const text = this.getAttr_(attrs, 'text');
|
|
||||||
|
|
||||||
// TODO:
|
|
||||||
|
|
||||||
// if (Resource.isResourceUrl(href)) {
|
|
||||||
// return '[Resource not yet supported: ' + htmlentities(text) + ']';
|
|
||||||
// } else {
|
|
||||||
// const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
|
||||||
// let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
|
|
||||||
// return output;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTokens_(tokens, options) {
|
|
||||||
let output = [];
|
|
||||||
for (let i = 0; i < tokens.length; i++) {
|
|
||||||
const t = tokens[i];
|
|
||||||
|
|
||||||
let tag = t.tag;
|
|
||||||
let openTag = null;
|
|
||||||
let closeTag = null;
|
|
||||||
let attrs = t.attrs ? t.attrs : [];
|
|
||||||
|
|
||||||
if (t.map) attrs.push(['data-map', t.map.join(':')]);
|
|
||||||
|
|
||||||
if (tag && t.type.indexOf('_open') >= 0) {
|
|
||||||
openTag = tag;
|
|
||||||
} else if (tag && t.type.indexOf('_close') >= 0) {
|
|
||||||
closeTag = tag;
|
|
||||||
} else if (tag && t.type.indexOf('inline') >= 0) {
|
|
||||||
openTag = tag;
|
|
||||||
} else if (t.type === 'link_open') {
|
|
||||||
openTag = 'a';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openTag) {
|
|
||||||
const attrsHtml = attrs ? this.renderAttrs_(attrs) : '';
|
|
||||||
output.push('<' + openTag + (attrsHtml ? ' ' + attrsHtml : '') + '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.type === 'image') {
|
|
||||||
if (t.content) attrs.push(['title', t.content]);
|
|
||||||
output.push(this.renderImage_(attrs, options));
|
|
||||||
} else {
|
|
||||||
if (t.children) {
|
|
||||||
const parsedChildren = this.renderTokens_(t.children, options);
|
|
||||||
output = output.concat(parsedChildren);
|
|
||||||
} else {
|
|
||||||
if (t.content) {
|
|
||||||
output.push(htmlentities(t.content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.type === 'link_close') {
|
|
||||||
closeTag = 'a';
|
|
||||||
} else if (tag && t.type.indexOf('inline') >= 0) {
|
|
||||||
closeTag = openTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeTag) {
|
|
||||||
output.push('</' + closeTag + '>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIt(body, style, options = null) {
|
|
||||||
if (!options) options = {};
|
|
||||||
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
|
||||||
|
|
||||||
const MarkdownIt = require('markdown-it');
|
|
||||||
const md = new MarkdownIt();
|
|
||||||
const env = {};
|
|
||||||
|
|
||||||
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
|
|
||||||
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
|
|
||||||
// that it can be removed and replaced later on.
|
|
||||||
const HORRIBLE_HACK = true;
|
|
||||||
|
|
||||||
if (HORRIBLE_HACK) {
|
|
||||||
let counter = -1;
|
|
||||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
|
||||||
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
|
||||||
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
|
||||||
counter++;
|
|
||||||
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = md.parse(body, env);
|
|
||||||
|
|
||||||
console.info(body);
|
|
||||||
console.info(tokens);
|
|
||||||
|
|
||||||
let renderedBody = this.renderTokens_(tokens, options);
|
|
||||||
|
|
||||||
if (HORRIBLE_HACK) {
|
|
||||||
let loopCount = 0;
|
|
||||||
while (renderedBody.indexOf('mJOPm') >= 0) {
|
|
||||||
renderedBody = renderedBody.replace(/mJOPmCHECKBOXm([A-Z]+)m(\d+)m/, function(v, type, index) {
|
|
||||||
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
|
|
||||||
return '<a href="#" onclick="' + js + '" class="checkbox">' + (type == 'NOTICK' ? '☐' : '☑') + '</a>';
|
|
||||||
});
|
|
||||||
if (loopCount++ >= 9999) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://necolas.github.io/normalize.css/
|
|
||||||
const normalizeCss = `
|
|
||||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
|
||||||
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
|
|
||||||
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
|
|
||||||
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const css = `
|
|
||||||
body {
|
|
||||||
font-size: ` + style.htmlFontSize + `;
|
|
||||||
color: ` + style.htmlColor + `;
|
|
||||||
line-height: 1.5em;
|
|
||||||
background-color: ` + style.htmlBackgroundColor + `;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: ` + style.htmlLinkColor + `
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
a.checkbox {
|
|
||||||
font-size: 1.6em;
|
|
||||||
position: relative;
|
|
||||||
top: 0.1em;
|
|
||||||
text-decoration: none;
|
|
||||||
color: ` + style.htmlColor + `;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
border: 1px solid silver;
|
|
||||||
padding: .5em 1em .5em 1em;
|
|
||||||
}
|
|
||||||
hr {
|
|
||||||
border: 1px solid ` + style.htmlDividerColor + `;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
|
|
||||||
|
|
||||||
return styleHtml + renderedBody;
|
|
||||||
|
|
||||||
|
|
||||||
//return md.render(body);
|
|
||||||
// var result = md.render('# markdown-it rulezz!');
|
|
||||||
|
|
||||||
// // node.js, the same, but with sugar:
|
|
||||||
// var md = require('markdown-it')();
|
|
||||||
// var result = md.render('# markdown-it rulezz!');
|
|
||||||
|
|
||||||
// // browser without AMD, added to "window" on script load
|
|
||||||
// // Note, there is no dash in "markdownit".
|
|
||||||
// var md = window.markdownit();
|
|
||||||
// var result = md.render('# markdown-it rulezz!');
|
|
||||||
}
|
|
||||||
|
|
||||||
render(body, style, options = null) {
|
|
||||||
return this.renderIt(body, style, options);
|
|
||||||
|
|
||||||
if (!options) options = {};
|
|
||||||
|
|
||||||
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
|
||||||
|
|
||||||
// ipcRenderer.sendToHost('pong')
|
|
||||||
|
|
||||||
const { Resource } = require('lib/models/resource.js');
|
|
||||||
// const Entities = require('html-entities').AllHtmlEntities;
|
|
||||||
// const htmlentities = (new Entities()).encode;
|
|
||||||
const { shim } = require('lib/shim.js');
|
|
||||||
|
|
||||||
const loadResource = async (id) => {
|
|
||||||
const resource = await Resource.load(id);
|
|
||||||
resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource));
|
|
||||||
|
|
||||||
let newResources = Object.assign({}, this.loadedResources_);
|
|
||||||
newResources[id] = resource;
|
|
||||||
this.loadedResources_ = newResources;
|
|
||||||
|
|
||||||
if (options.onResourceLoaded) options.onResourceLoaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://necolas.github.io/normalize.css/
|
|
||||||
const normalizeCss = `
|
|
||||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
|
||||||
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
|
|
||||||
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
|
|
||||||
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const css = `
|
|
||||||
body {
|
|
||||||
font-size: ` + style.htmlFontSize + `;
|
|
||||||
color: ` + style.htmlColor + `;
|
|
||||||
line-height: 1.5em;
|
|
||||||
background-color: ` + style.htmlBackgroundColor + `;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: ` + style.htmlLinkColor + `
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
a.checkbox {
|
|
||||||
font-size: 1.6em;
|
|
||||||
position: relative;
|
|
||||||
top: 0.1em;
|
|
||||||
text-decoration: none;
|
|
||||||
color: ` + style.htmlColor + `;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
td, th {
|
|
||||||
border: 1px solid silver;
|
|
||||||
padding: .5em 1em .5em 1em;
|
|
||||||
}
|
|
||||||
hr {
|
|
||||||
border: 1px solid ` + style.htmlDividerColor + `;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
let counter = -1;
|
|
||||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
|
||||||
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
|
||||||
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
|
||||||
counter++;
|
|
||||||
return '°°JOP°CHECKBOX°' + s + '°' + counter + '°°';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
|
||||||
|
|
||||||
renderer.link = function (href, title, text) {
|
|
||||||
if (Resource.isResourceUrl(href)) {
|
|
||||||
return '[Resource not yet supported: ' + htmlentities(text) + ']';
|
|
||||||
} else {
|
|
||||||
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
|
||||||
let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.image = (href, title, text) => {
|
|
||||||
if (!Resource.isResourceUrl(href)) {
|
|
||||||
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceId = Resource.urlToId(href);
|
|
||||||
if (!this.loadedResources_[resourceId]) {
|
|
||||||
loadResource(resourceId);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = this.loadedResources_[resourceId];
|
|
||||||
const mime = r.mime.toLowerCase();
|
|
||||||
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
|
|
||||||
const src = 'data:' + r.mime + ';base64,' + r.base64;
|
|
||||||
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
|
|
||||||
}
|
|
||||||
|
|
||||||
let styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
|
|
||||||
|
|
||||||
let html = body ? styleHtml + marked(body, {
|
|
||||||
gfm: true,
|
|
||||||
breaks: true,
|
|
||||||
renderer: renderer,
|
|
||||||
sanitize: true,
|
|
||||||
}) : styleHtml;
|
|
||||||
|
|
||||||
while (html.indexOf('°°JOP°') >= 0) {
|
|
||||||
html = html.replace(/°°JOP°CHECKBOX°([A-Z]+)°(\d+)°°/, function(v, type, index) {
|
|
||||||
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
|
|
||||||
return '<a href="#" onclick="' + js + '" class="checkbox">' + (type == 'NOTICK' ? '☐' : '☑') + '</a>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//let scriptHtml = '<script>document.body.scrollTop = ' + this.bodyScrollTop_ + ';</script>';
|
|
||||||
let scriptHtml = '';
|
|
||||||
|
|
||||||
//html = '<body onscroll="postMessage(\'bodyscroll:\' + document.body.scrollTop);">' + html + scriptHtml + '</body>';
|
|
||||||
html = '<body>' + html + scriptHtml + '</body>';
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTickAt(body, index) {
|
|
||||||
let counter = -1;
|
|
||||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
|
||||||
counter++;
|
|
||||||
|
|
||||||
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
|
||||||
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
|
||||||
if (index == counter) {
|
|
||||||
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';
|
|
||||||
}
|
|
||||||
return '°°JOP°CHECKBOX°' + s + '°°';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]');
|
|
||||||
body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]');
|
|
||||||
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCheckboxClick(msg, noteBody) {
|
|
||||||
msg = msg.split(':');
|
|
||||||
let index = Number(msg[msg.length - 1]);
|
|
||||||
let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway
|
|
||||||
return this.toggleTickAt(noteBody, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const markdownUtils = {
|
const markdownUtils = {
|
||||||
|
|
||||||
// Not really escaping because that's not supported by marked.js
|
// Not really escaping because that's not supported by marked.js
|
||||||
@ -455,4 +13,4 @@ const markdownUtils = {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { markdownUtils, MdToHtml };
|
module.exports = { markdownUtils };
|
@ -68,7 +68,7 @@ class Resource extends BaseItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static isResourceUrl(url) {
|
static isResourceUrl(url) {
|
||||||
return url.length === 34 && url[0] === ':' && url[1] === '/';
|
return url && url.length === 34 && url[0] === ':' && url[1] === '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
static urlToId(url) {
|
static urlToId(url) {
|
||||||
|
59
ReactNativeClient/package-lock.json
generated
59
ReactNativeClient/package-lock.json
generated
@ -141,7 +141,6 @@
|
|||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
||||||
"integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
|
"integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"sprintf-js": "1.0.3"
|
"sprintf-js": "1.0.3"
|
||||||
}
|
}
|
||||||
@ -1182,6 +1181,11 @@
|
|||||||
"supports-color": "2.0.0"
|
"supports-color": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"charenc": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||||
|
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||||
|
},
|
||||||
"ci-info": {
|
"ci-info": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.1.tgz",
|
||||||
@ -1488,6 +1492,11 @@
|
|||||||
"which": "1.3.0"
|
"which": "1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"crypt": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||||
|
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||||
|
},
|
||||||
"cryptiles": {
|
"cryptiles": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
|
||||||
@ -1657,6 +1666,11 @@
|
|||||||
"iconv-lite": "0.4.19"
|
"iconv-lite": "0.4.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"entities": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
|
||||||
|
},
|
||||||
"envinfo": {
|
"envinfo": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-3.4.2.tgz",
|
||||||
@ -3395,6 +3409,14 @@
|
|||||||
"type-check": "0.3.2"
|
"type-check": "0.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"linkify-it": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
|
||||||
|
"requires": {
|
||||||
|
"uc.micro": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"load-json-file": {
|
"load-json-file": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||||
@ -3586,6 +3608,33 @@
|
|||||||
"tmpl": "1.0.4"
|
"tmpl": "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"markdown-it": {
|
||||||
|
"version": "8.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.0.tgz",
|
||||||
|
"integrity": "sha512-tNuOCCfunY5v5uhcO2AUMArvKAyKMygX8tfup/JrgnsDqcCATQsAExBq7o5Ml9iMmO82bk6jYNLj6khcrl0JGA==",
|
||||||
|
"requires": {
|
||||||
|
"argparse": "1.0.9",
|
||||||
|
"entities": "1.1.1",
|
||||||
|
"linkify-it": "2.0.3",
|
||||||
|
"mdurl": "1.0.1",
|
||||||
|
"uc.micro": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"md5": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||||
|
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
|
||||||
|
"requires": {
|
||||||
|
"charenc": "0.0.2",
|
||||||
|
"crypt": "0.0.2",
|
||||||
|
"is-buffer": "1.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mdurl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||||
|
},
|
||||||
"media-typer": {
|
"media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@ -5404,8 +5453,7 @@
|
|||||||
"sprintf-js": {
|
"sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"sshpk": {
|
"sshpk": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
@ -5692,6 +5740,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
|
||||||
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g=="
|
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g=="
|
||||||
},
|
},
|
||||||
|
"uc.micro": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-ftUNXg+an7ClczeSWfKndFjVAZI="
|
||||||
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "2.7.5",
|
"version": "2.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz",
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"form-data": "^2.1.4",
|
"form-data": "^2.1.4",
|
||||||
"html-entities": "^1.2.1",
|
"html-entities": "^1.2.1",
|
||||||
|
"markdown-it": "^8.4.0",
|
||||||
|
"md5": "^2.2.1",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"prop-types": "^15.6.0",
|
"prop-types": "^15.6.0",
|
||||||
"query-string": "4.3.4",
|
"query-string": "4.3.4",
|
||||||
|
Loading…
Reference in New Issue
Block a user