You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Optimised markdown rendering for mobile and electron
This commit is contained in:
		| @@ -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 { | ||||
|   | ||||
							
								
								
									
										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 { 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 { | ||||
|  | ||||
|   | ||||
| @@ -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 = { | ||||
|  | ||||
| 	// 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) { | ||||
| 		return url.length === 34 && url[0] === ':' && url[1] === '/'; | ||||
| 		return url && url.length === 34 && url[0] === ':' && url[1] === '/'; | ||||
| 	} | ||||
|  | ||||
| 	static urlToId(url) { | ||||
|   | ||||
							
								
								
									
										59
									
								
								ReactNativeClient/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								ReactNativeClient/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user