From 9e3af21e66a45a2cf470c9777430be4007ff2014 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 7 Nov 2017 18:39:11 +0000 Subject: [PATCH] Optimised markdown rendering for mobile and electron --- ElectronClient/app/gui/NoteText.jsx | 2 +- ReactNativeClient/lib/MdToHtml.js | 335 +++++++++++++ .../lib/components/note-body-viewer.js | 2 +- ReactNativeClient/lib/markdown-utils.js | 444 +----------------- ReactNativeClient/lib/models/resource.js | 2 +- ReactNativeClient/package-lock.json | 59 ++- ReactNativeClient/package.json | 2 + 7 files changed, 397 insertions(+), 449 deletions(-) create mode 100644 ReactNativeClient/lib/MdToHtml.js diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index 7cb113a7f..4755b752f 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -1,7 +1,7 @@ const React = require('react'); const { Note } = require('lib/models/note.js'); 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'); class NoteTextComponent extends React.Component { diff --git a/ReactNativeClient/lib/MdToHtml.js b/ReactNativeClient/lib/MdToHtml.js new file mode 100644 index 000000000..336a1f544 --- /dev/null +++ b/ReactNativeClient/lib/MdToHtml.js @@ -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 '' + 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 = ''; + 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 = "" + htmlentities(text) + ''; + let output = ""; + return output; + } + } + + renderCloseLink_(attrs, options) { + const href = this.getAttr_(attrs, 'href'); + + if (Resource.isResourceUrl(href)) { + return ''; + } else { + return ''; + } + } + + 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(''); + } + } + } + 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 '' + (type == 'NOTICK' ? '☐' : '☑') + ''; + }); + 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 = ''; + + 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; \ No newline at end of file diff --git a/ReactNativeClient/lib/components/note-body-viewer.js b/ReactNativeClient/lib/components/note-body-viewer.js index 82af62dda..4bb61b00f 100644 --- a/ReactNativeClient/lib/components/note-body-viewer.js +++ b/ReactNativeClient/lib/components/note-body-viewer.js @@ -3,7 +3,7 @@ const { WebView, View, Linking } = require('react-native'); const { globalStyle } = require('lib/components/global-style.js'); const { Resource } = require('lib/models/resource.js'); const { reg } = require('lib/registry.js'); -const { MdToHtml } = require('lib/markdown-utils.js'); +const MdToHtml = require('lib/MdToHtml.js'); class NoteBodyViewer extends Component { diff --git a/ReactNativeClient/lib/markdown-utils.js b/ReactNativeClient/lib/markdown-utils.js index 2df4685c5..22f83d73d 100644 --- a/ReactNativeClient/lib/markdown-utils.js +++ b/ReactNativeClient/lib/markdown-utils.js @@ -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 '' + 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 = ''; - 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 = "" + htmlentities(text) + ''; - // 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(''); - } - } - 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 '' + (type == 'NOTICK' ? '☐' : '☑') + ''; - }); - 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 = ''; - - 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 = "" + htmlentities(text) + ''; - return output; - } - } - - renderer.image = (href, title, text) => { - if (!Resource.isResourceUrl(href)) { - return '' + 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 = ''; - return output; - } - - return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]'; - } - - let styleHtml = ''; - - 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 '' + (type == 'NOTICK' ? '☐' : '☑') + ''; - }); - } - - //let scriptHtml = ''; - let scriptHtml = ''; - - //html = '' + html + scriptHtml + ''; - html = '' + html + scriptHtml + ''; - - 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 = { // Not really escaping because that's not supported by marked.js @@ -455,4 +13,4 @@ const markdownUtils = { }; -module.exports = { markdownUtils, MdToHtml }; \ No newline at end of file +module.exports = { markdownUtils }; \ No newline at end of file diff --git a/ReactNativeClient/lib/models/resource.js b/ReactNativeClient/lib/models/resource.js index 7e67fd46f..db9fca9a6 100644 --- a/ReactNativeClient/lib/models/resource.js +++ b/ReactNativeClient/lib/models/resource.js @@ -68,7 +68,7 @@ class Resource extends BaseItem { } static isResourceUrl(url) { - return url.length === 34 && url[0] === ':' && url[1] === '/'; + return url && url.length === 34 && url[0] === ':' && url[1] === '/'; } static urlToId(url) { diff --git a/ReactNativeClient/package-lock.json b/ReactNativeClient/package-lock.json index 7af70ae24..c6762108f 100644 --- a/ReactNativeClient/package-lock.json +++ b/ReactNativeClient/package-lock.json @@ -141,7 +141,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", - "dev": true, "requires": { "sprintf-js": "1.0.3" } @@ -1182,6 +1181,11 @@ "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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.1.tgz", @@ -1488,6 +1492,11 @@ "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": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", @@ -1657,6 +1666,11 @@ "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": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-3.4.2.tgz", @@ -3395,6 +3409,14 @@ "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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -3586,6 +3608,33 @@ "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": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5404,8 +5453,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.13.1", @@ -5692,6 +5740,11 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", "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": { "version": "2.7.5", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.5.tgz", diff --git a/ReactNativeClient/package.json b/ReactNativeClient/package.json index 8a93d6534..668ead3ad 100644 --- a/ReactNativeClient/package.json +++ b/ReactNativeClient/package.json @@ -11,6 +11,8 @@ "dependencies": { "form-data": "^2.1.4", "html-entities": "^1.2.1", + "markdown-it": "^8.4.0", + "md5": "^2.2.1", "moment": "^2.18.1", "prop-types": "^15.6.0", "query-string": "4.3.4",