From 8239abec2a582d7e337e06be8320fb21de4fb397 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Tue, 10 Apr 2018 22:28:24 +0800 Subject: [PATCH] support code block MathJax preview --- src/resources/markdown_template.js | 7 +- src/resources/mathjax_preview.js | 48 ++ src/resources/mathjax_preview_template.html | 15 + src/src.pro | 8 +- src/utils/dom-to-image/README.md | 3 + src/utils/dom-to-image/dom-to-image.js | 779 ++++++++++++++++++++ src/utils/vutils.cpp | 25 + src/utils/vutils.h | 3 + src/vconstants.h | 3 + src/veditarea.cpp | 7 +- src/veditarea.h | 9 + src/vlivepreviewhelper.cpp | 53 +- src/vlivepreviewhelper.h | 33 + src/vmathjaxpreviewhelper.cpp | 64 ++ src/vmathjaxpreviewhelper.h | 62 ++ src/vmathjaxwebdocument.cpp | 23 + src/vmathjaxwebdocument.h | 31 + src/vnote.cpp | 23 +- src/vnote.h | 2 + src/vnote.qrc | 3 + 20 files changed, 1180 insertions(+), 21 deletions(-) create mode 100644 src/resources/mathjax_preview.js create mode 100644 src/resources/mathjax_preview_template.html create mode 100644 src/utils/dom-to-image/README.md create mode 100644 src/utils/dom-to-image/dom-to-image.js create mode 100644 src/vmathjaxpreviewhelper.cpp create mode 100644 src/vmathjaxpreviewhelper.h create mode 100644 src/vmathjaxwebdocument.cpp create mode 100644 src/vmathjaxwebdocument.h diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 934979ec..e4576c4a 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -1419,6 +1419,11 @@ var previewCodeBlock = function(id, lang, text, isLivePreview) { var setPreviewContent = function(lang, html) { previewDiv.innerHTML = html; + // Treat plantUML and graphviz the same. - previewDiv.classList = VPlantUMLDivClass; + if (lang == "puml" || lang == "dot") { + previewDiv.classList = VPlantUMLDivClass; + } else { + previewDiv.className = ''; + } }; diff --git a/src/resources/mathjax_preview.js b/src/resources/mathjax_preview.js new file mode 100644 index 00000000..26070367 --- /dev/null +++ b/src/resources/mathjax_preview.js @@ -0,0 +1,48 @@ +var channelInitialized = false; + +var contentDiv = document.getElementById('content-div'); + +var content; + +new QWebChannel(qt.webChannelTransport, + function(channel) { + content = channel.objects.content; + + content.requestPreviewMathJax.connect(previewMathJax); + + channelInitialized = true; + }); + +var previewMathJax = function(identifier, id, text) { + if (text.length == 0) { + return; + } + + var p = document.createElement('p'); + p.id = identifier + '_' + id; + p.textContent = text; + contentDiv.appendChild(p); + + try { + MathJax.Hub.Queue(["Typeset", + MathJax.Hub, + p, + postProcessMathJax.bind(undefined, identifier, id, p)]); + } catch (err) { + console.log("err: " + err); + contentDiv.removeChild(p); + delete p; + } +}; + +var postProcessMathJax = function(identifier, id, container) { + domtoimage.toPng(container, { height: container.clientHeight * 1.5 }).then(function (dataUrl) { + var png = dataUrl.substring(dataUrl.indexOf(',') + 1); + content.mathjaxResultReady(identifier, id, 'png', png); + + contentDiv.removeChild(container); + delete container; + }).catch(function (err) { + console.log("err: " + err); + }); +}; diff --git a/src/resources/mathjax_preview_template.html b/src/resources/mathjax_preview_template.html new file mode 100644 index 00000000..76e74e75 --- /dev/null +++ b/src/resources/mathjax_preview_template.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + +
+ + diff --git a/src/src.pro b/src/src.pro index 5e648f18..38ff2e02 100644 --- a/src/src.pro +++ b/src/src.pro @@ -128,7 +128,9 @@ SOURCES += main.cpp\ dialog/vfixnotebookdialog.cpp \ vplantumlhelper.cpp \ vgraphvizhelper.cpp \ - vlivepreviewhelper.cpp + vlivepreviewhelper.cpp \ + vmathjaxpreviewhelper.cpp \ + vmathjaxwebdocument.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -247,7 +249,9 @@ HEADERS += vmainwindow.h \ dialog/vfixnotebookdialog.h \ vplantumlhelper.h \ vgraphvizhelper.h \ - vlivepreviewhelper.h + vlivepreviewhelper.h \ + vmathjaxpreviewhelper.h \ + vmathjaxwebdocument.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/dom-to-image/README.md b/src/utils/dom-to-image/README.md new file mode 100644 index 00000000..b91f227b --- /dev/null +++ b/src/utils/dom-to-image/README.md @@ -0,0 +1,3 @@ +# [dom-to-image](https://github.com/tsayen/dom-to-image) +v2.6.0 +Anatolii Saienko, Paul Bakaus (original idea) diff --git a/src/utils/dom-to-image/dom-to-image.js b/src/utils/dom-to-image/dom-to-image.js new file mode 100644 index 00000000..29b6edce --- /dev/null +++ b/src/utils/dom-to-image/dom-to-image.js @@ -0,0 +1,779 @@ +(function (global) { + 'use strict'; + + var util = newUtil(); + var inliner = newInliner(); + var fontFaces = newFontFaces(); + var images = newImages(); + + // Default impl options + var defaultOptions = { + // Default is to fail on error, no placeholder + imagePlaceholder: undefined, + // Default cache bust is false, it will use the cache + cacheBust: false + }; + + var domtoimage = { + toSvg: toSvg, + toPng: toPng, + toJpeg: toJpeg, + toBlob: toBlob, + toPixelData: toPixelData, + impl: { + fontFaces: fontFaces, + images: images, + util: util, + inliner: inliner, + options: {} + } + }; + + if (typeof module !== 'undefined') + module.exports = domtoimage; + else + global.domtoimage = domtoimage; + + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options + * @param {Function} options.filter - Should return true if passed node should be included in the output + * (excluding node means excluding it's children as well). Not called on the root node. + * @param {String} options.bgcolor - color for the background, any valid CSS color value. + * @param {Number} options.width - width to be applied to node before rendering. + * @param {Number} options.height - height to be applied to node before rendering. + * @param {Object} options.style - an object whose properties to be copied to node's style before rendering. + * @param {Number} options.quality - a Number between 0 and 1 indicating image quality (applicable to JPEG only), + defaults to 1.0. + * @param {String} options.imagePlaceholder - dataURL to use as a placeholder for failed images, default behaviour is to fail fast on images we can't fetch + * @param {Boolean} options.cacheBust - set to true to cache bust by appending the time to the request url + * @return {Promise} - A promise that is fulfilled with a SVG image data URL + * */ + function toSvg(node, options) { + options = options || {}; + copyOptions(options); + return Promise.resolve(node) + .then(function (node) { + return cloneNode(node, options.filter, true); + }) + .then(embedFonts) + .then(inlineImages) + .then(applyOptions) + .then(function (clone) { + return makeSvgDataUri(clone, + options.width || util.width(node), + options.height || util.height(node) + ); + }); + + function applyOptions(clone) { + if (options.bgcolor) clone.style.backgroundColor = options.bgcolor; + + if (options.width) clone.style.width = options.width + 'px'; + if (options.height) clone.style.height = options.height + 'px'; + + if (options.style) + Object.keys(options.style).forEach(function (property) { + clone.style[property] = options.style[property]; + }); + + return clone; + } + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a Uint8Array containing RGBA pixel data. + * */ + function toPixelData(node, options) { + return draw(node, options || {}) + .then(function (canvas) { + return canvas.getContext('2d').getImageData( + 0, + 0, + util.width(node), + util.height(node) + ).data; + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a PNG image data URL + * */ + function toPng(node, options) { + return draw(node, options || {}) + .then(function (canvas) { + return canvas.toDataURL(); + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a JPEG image data URL + * */ + function toJpeg(node, options) { + options = options || {}; + return draw(node, options) + .then(function (canvas) { + return canvas.toDataURL('image/jpeg', options.quality || 1.0); + }); + } + + /** + * @param {Node} node - The DOM Node object to render + * @param {Object} options - Rendering options, @see {@link toSvg} + * @return {Promise} - A promise that is fulfilled with a PNG image blob + * */ + function toBlob(node, options) { + return draw(node, options || {}) + .then(util.canvasToBlob); + } + + function copyOptions(options) { + // Copy options to impl options for use in impl + if(typeof(options.imagePlaceholder) === 'undefined') { + domtoimage.impl.options.imagePlaceholder = defaultOptions.imagePlaceholder; + } else { + domtoimage.impl.options.imagePlaceholder = options.imagePlaceholder; + } + + if(typeof(options.cacheBust) === 'undefined') { + domtoimage.impl.options.cacheBust = defaultOptions.cacheBust; + } else { + domtoimage.impl.options.cacheBust = options.cacheBust; + } + } + + function draw(domNode, options) { + return toSvg(domNode, options) + .then(util.makeImage) + .then(util.delay(100)) + .then(function (image) { + var canvas = newCanvas(domNode); + canvas.getContext('2d').drawImage(image, 0, 0); + return canvas; + }); + + function newCanvas(domNode) { + var canvas = document.createElement('canvas'); + canvas.width = options.width || util.width(domNode); + canvas.height = options.height || util.height(domNode); + + if (options.bgcolor) { + var ctx = canvas.getContext('2d'); + ctx.fillStyle = options.bgcolor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + return canvas; + } + } + + function cloneNode(node, filter, root) { + if (!root && filter && !filter(node)) return Promise.resolve(); + + return Promise.resolve(node) + .then(makeNodeCopy) + .then(function (clone) { + return cloneChildren(node, clone, filter); + }) + .then(function (clone) { + return processClone(node, clone); + }); + + function makeNodeCopy(node) { + if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL()); + return node.cloneNode(false); + } + + function cloneChildren(original, clone, filter) { + var children = original.childNodes; + if (children.length === 0) return Promise.resolve(clone); + + return cloneChildrenInOrder(clone, util.asArray(children), filter) + .then(function () { + return clone; + }); + + function cloneChildrenInOrder(parent, children, filter) { + var done = Promise.resolve(); + children.forEach(function (child) { + done = done + .then(function () { + return cloneNode(child, filter); + }) + .then(function (childClone) { + if (childClone) parent.appendChild(childClone); + }); + }); + return done; + } + } + + function processClone(original, clone) { + if (!(clone instanceof Element)) return clone; + + return Promise.resolve() + .then(cloneStyle) + .then(clonePseudoElements) + .then(copyUserInput) + .then(fixSvg) + .then(function () { + return clone; + }); + + function cloneStyle() { + // Fix MathJax issue. + if (!clone.style) { + return; + } + + copyStyle(window.getComputedStyle(original), clone.style); + + function copyStyle(source, target) { + if (source.cssText) target.cssText = source.cssText; + else copyProperties(source, target); + + function copyProperties(source, target) { + util.asArray(source).forEach(function (name) { + target.setProperty( + name, + source.getPropertyValue(name), + source.getPropertyPriority(name) + ); + }); + } + } + } + + function clonePseudoElements() { + [':before', ':after'].forEach(function (element) { + clonePseudoElement(element); + }); + + function clonePseudoElement(element) { + var style = window.getComputedStyle(original, element); + var content = style.getPropertyValue('content'); + + if (content === '' || content === 'none') return; + + var className = util.uid(); + clone.className = clone.className + ' ' + className; + var styleElement = document.createElement('style'); + styleElement.appendChild(formatPseudoElementStyle(className, element, style)); + clone.appendChild(styleElement); + + function formatPseudoElementStyle(className, element, style) { + var selector = '.' + className + ':' + element; + var cssText = style.cssText ? formatCssText(style) : formatCssProperties(style); + return document.createTextNode(selector + '{' + cssText + '}'); + + function formatCssText(style) { + var content = style.getPropertyValue('content'); + return style.cssText + ' content: ' + content + ';'; + } + + function formatCssProperties(style) { + + return util.asArray(style) + .map(formatProperty) + .join('; ') + ';'; + + function formatProperty(name) { + return name + ': ' + + style.getPropertyValue(name) + + (style.getPropertyPriority(name) ? ' !important' : ''); + } + } + } + } + } + + function copyUserInput() { + if (original instanceof HTMLTextAreaElement) clone.innerHTML = original.value; + if (original instanceof HTMLInputElement) clone.setAttribute("value", original.value); + } + + function fixSvg() { + if (!(clone instanceof SVGElement)) return; + clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + + if (!(clone instanceof SVGRectElement)) return; + ['width', 'height'].forEach(function (attribute) { + var value = clone.getAttribute(attribute); + if (!value) return; + + clone.style.setProperty(attribute, value); + }); + } + } + } + + function embedFonts(node) { + return fontFaces.resolveAll() + .then(function (cssText) { + var styleNode = document.createElement('style'); + node.appendChild(styleNode); + styleNode.appendChild(document.createTextNode(cssText)); + return node; + }); + } + + function inlineImages(node) { + return images.inlineAll(node) + .then(function () { + return node; + }); + } + + function makeSvgDataUri(node, width, height) { + return Promise.resolve(node) + .then(function (node) { + node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + return new XMLSerializer().serializeToString(node); + }) + .then(util.escapeXhtml) + .then(function (xhtml) { + return '' + xhtml + ''; + }) + .then(function (foreignObject) { + return '' + + foreignObject + ''; + }) + .then(function (svg) { + return 'data:image/svg+xml;charset=utf-8,' + svg; + }); + } + + function newUtil() { + return { + escape: escape, + parseExtension: parseExtension, + mimeType: mimeType, + dataAsUrl: dataAsUrl, + isDataUrl: isDataUrl, + canvasToBlob: canvasToBlob, + resolveUrl: resolveUrl, + getAndEncode: getAndEncode, + uid: uid(), + delay: delay, + asArray: asArray, + escapeXhtml: escapeXhtml, + makeImage: makeImage, + width: width, + height: height + }; + + function mimes() { + /* + * Only WOFF and EOT mime types for fonts are 'real' + * see http://www.iana.org/assignments/media-types/media-types.xhtml + */ + var WOFF = 'application/font-woff'; + var JPEG = 'image/jpeg'; + + return { + 'woff': WOFF, + 'woff2': WOFF, + 'ttf': 'application/font-truetype', + 'eot': 'application/vnd.ms-fontobject', + 'png': 'image/png', + 'jpg': JPEG, + 'jpeg': JPEG, + 'gif': 'image/gif', + 'tiff': 'image/tiff', + 'svg': 'image/svg+xml' + }; + } + + function parseExtension(url) { + var match = /\.([^\.\/]*?)$/g.exec(url); + if (match) return match[1]; + else return ''; + } + + function mimeType(url) { + var extension = parseExtension(url).toLowerCase(); + return mimes()[extension] || ''; + } + + function isDataUrl(url) { + return url.search(/^(data:)/) !== -1; + } + + function toBlob(canvas) { + return new Promise(function (resolve) { + var binaryString = window.atob(canvas.toDataURL().split(',')[1]); + var length = binaryString.length; + var binaryArray = new Uint8Array(length); + + for (var i = 0; i < length; i++) + binaryArray[i] = binaryString.charCodeAt(i); + + resolve(new Blob([binaryArray], { + type: 'image/png' + })); + }); + } + + function canvasToBlob(canvas) { + if (canvas.toBlob) + return new Promise(function (resolve) { + canvas.toBlob(resolve); + }); + + return toBlob(canvas); + } + + function resolveUrl(url, baseUrl) { + var doc = document.implementation.createHTMLDocument(); + var base = doc.createElement('base'); + doc.head.appendChild(base); + var a = doc.createElement('a'); + doc.body.appendChild(a); + base.href = baseUrl; + a.href = url; + return a.href; + } + + function uid() { + var index = 0; + + return function () { + return 'u' + fourRandomChars() + index++; + + function fourRandomChars() { + /* see http://stackoverflow.com/a/6248722/2519373 */ + return ('0000' + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4); + } + }; + } + + function makeImage(uri) { + return new Promise(function (resolve, reject) { + var image = new Image(); + image.onload = function () { + resolve(image); + }; + image.onerror = reject; + image.src = uri; + }); + } + + function getAndEncode(url) { + var TIMEOUT = 30000; + if(domtoimage.impl.options.cacheBust) { + // Cache bypass so we dont have CORS issues with cached images + // Source: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache + url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime(); + } + + return new Promise(function (resolve) { + var request = new XMLHttpRequest(); + + request.onreadystatechange = done; + request.ontimeout = timeout; + request.responseType = 'blob'; + request.timeout = TIMEOUT; + request.open('GET', url, true); + request.send(); + + var placeholder; + if(domtoimage.impl.options.imagePlaceholder) { + var split = domtoimage.impl.options.imagePlaceholder.split(/,/); + if(split && split[1]) { + placeholder = split[1]; + } + } + + function done() { + if (request.readyState !== 4) return; + + if (request.status !== 200) { + if(placeholder) { + resolve(placeholder); + } else { + fail('cannot fetch resource: ' + url + ', status: ' + request.status); + } + + return; + } + + var encoder = new FileReader(); + encoder.onloadend = function () { + var content = encoder.result.split(/,/)[1]; + resolve(content); + }; + encoder.readAsDataURL(request.response); + } + + function timeout() { + if(placeholder) { + resolve(placeholder); + } else { + fail('timeout of ' + TIMEOUT + 'ms occured while fetching resource: ' + url); + } + } + + function fail(message) { + console.error(message); + resolve(''); + } + }); + } + + function dataAsUrl(content, type) { + return 'data:' + type + ';base64,' + content; + } + + function escape(string) { + return string.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1'); + } + + function delay(ms) { + return function (arg) { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(arg); + }, ms); + }); + }; + } + + function asArray(arrayLike) { + var array = []; + var length = arrayLike.length; + for (var i = 0; i < length; i++) array.push(arrayLike[i]); + return array; + } + + function escapeXhtml(string) { + return string.replace(/#/g, '%23').replace(/\n/g, '%0A'); + } + + function width(node) { + var leftBorder = px(node, 'border-left-width'); + var rightBorder = px(node, 'border-right-width'); + return node.scrollWidth + leftBorder + rightBorder; + } + + function height(node) { + var topBorder = px(node, 'border-top-width'); + var bottomBorder = px(node, 'border-bottom-width'); + return node.scrollHeight + topBorder + bottomBorder; + } + + function px(node, styleProperty) { + var value = window.getComputedStyle(node).getPropertyValue(styleProperty); + return parseFloat(value.replace('px', '')); + } + } + + function newInliner() { + var URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g; + + return { + inlineAll: inlineAll, + shouldProcess: shouldProcess, + impl: { + readUrls: readUrls, + inline: inline + } + }; + + function shouldProcess(string) { + return string.search(URL_REGEX) !== -1; + } + + function readUrls(string) { + var result = []; + var match; + while ((match = URL_REGEX.exec(string)) !== null) { + result.push(match[1]); + } + return result.filter(function (url) { + return !util.isDataUrl(url); + }); + } + + function inline(string, url, baseUrl, get) { + return Promise.resolve(url) + .then(function (url) { + return baseUrl ? util.resolveUrl(url, baseUrl) : url; + }) + .then(get || util.getAndEncode) + .then(function (data) { + return util.dataAsUrl(data, util.mimeType(url)); + }) + .then(function (dataUrl) { + return string.replace(urlAsRegex(url), '$1' + dataUrl + '$3'); + }); + + function urlAsRegex(url) { + return new RegExp('(url\\([\'"]?)(' + util.escape(url) + ')([\'"]?\\))', 'g'); + } + } + + function inlineAll(string, baseUrl, get) { + if (nothingToInline()) return Promise.resolve(string); + + return Promise.resolve(string) + .then(readUrls) + .then(function (urls) { + var done = Promise.resolve(string); + urls.forEach(function (url) { + done = done.then(function (string) { + return inline(string, url, baseUrl, get); + }); + }); + return done; + }); + + function nothingToInline() { + return !shouldProcess(string); + } + } + } + + function newFontFaces() { + return { + resolveAll: resolveAll, + impl: { + readAll: readAll + } + }; + + function resolveAll() { + return readAll(document) + .then(function (webFonts) { + return Promise.all( + webFonts.map(function (webFont) { + return webFont.resolve(); + }) + ); + }) + .then(function (cssStrings) { + return cssStrings.join('\n'); + }); + } + + function readAll() { + return Promise.resolve(util.asArray(document.styleSheets)) + .then(getCssRules) + .then(selectWebFontRules) + .then(function (rules) { + return rules.map(newWebFont); + }); + + function selectWebFontRules(cssRules) { + return cssRules + .filter(function (rule) { + return rule.type === CSSRule.FONT_FACE_RULE; + }) + .filter(function (rule) { + return inliner.shouldProcess(rule.style.getPropertyValue('src')); + }); + } + + function getCssRules(styleSheets) { + var cssRules = []; + styleSheets.forEach(function (sheet) { + try { + util.asArray(sheet.cssRules || []).forEach(cssRules.push.bind(cssRules)); + } catch (e) { + console.log('Error while reading CSS rules from ' + sheet.href, e.toString()); + } + }); + return cssRules; + } + + function newWebFont(webFontRule) { + return { + resolve: function resolve() { + var baseUrl = (webFontRule.parentStyleSheet || {}).href; + return inliner.inlineAll(webFontRule.cssText, baseUrl); + }, + src: function () { + return webFontRule.style.getPropertyValue('src'); + } + }; + } + } + } + + function newImages() { + return { + inlineAll: inlineAll, + impl: { + newImage: newImage + } + }; + + function newImage(element) { + return { + inline: inline + }; + + function inline(get) { + if (util.isDataUrl(element.src)) return Promise.resolve(); + + return Promise.resolve(element.src) + .then(get || util.getAndEncode) + .then(function (data) { + return util.dataAsUrl(data, util.mimeType(element.src)); + }) + .then(function (dataUrl) { + return new Promise(function (resolve, reject) { + element.onload = resolve; + element.onerror = reject; + element.src = dataUrl; + }); + }); + } + } + + function inlineAll(node) { + if (!(node instanceof Element)) return Promise.resolve(node); + + return inlineBackground(node) + .then(function () { + if (node instanceof HTMLImageElement) + return newImage(node).inline(); + else + return Promise.all( + util.asArray(node.childNodes).map(function (child) { + return inlineAll(child); + }) + ); + }); + + function inlineBackground(node) { + // Fix MathJax issue. + if (!node.style) { + return Promise.resolve(node); + } + + var background = node.style.getPropertyValue('background'); + + if (!background) return Promise.resolve(node); + + return inliner.inlineAll(background) + .then(function (inlined) { + node.style.setProperty( + 'background', + inlined, + node.style.getPropertyPriority('background') + ); + }) + .then(function () { + return node; + }); + } + } + } +})(this); diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index eaa568fa..8520c9f1 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -815,6 +815,31 @@ QString VUtils::generateExportHtmlTemplate(const QString &p_renderBg, bool p_inc return templ; } +QString VUtils::generateMathJaxPreviewTemplate() +{ + QString templ = VNote::generateMathJaxPreviewTemplate(); + QString mj = g_config->getMathjaxJavascript(); + // Chante MathJax to be rendered as SVG. + QRegExp reg("(Mathjax\\.js\\?config=)\\S+", Qt::CaseInsensitive); + // mj.replace(reg, QString("\\1%1").arg("TeX-MML-AM_SVG")); + + templ.replace(HtmlHolder::c_JSHolder, mj); + + QString extraFile; + extraFile += "\n"; + + templ.replace(HtmlHolder::c_extraHolder, extraFile); + + return templ; +} + QString VUtils::getFileNameWithSequence(const QString &p_directory, const QString &p_baseFileName, bool p_completeBaseName) diff --git a/src/utils/vutils.h b/src/utils/vutils.h index b43e388c..7ae5ec34 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -186,6 +186,9 @@ public: static QString generateSimpleHtmlTemplate(const QString &p_body); + // Generate template for MathJax preview. + static QString generateMathJaxPreviewTemplate(); + // Get an available file name in @p_directory with base @p_baseFileName. // If there already exists a file named @p_baseFileName, try to add sequence // suffix to the name, such as _001. diff --git a/src/vconstants.h b/src/vconstants.h index 4e463e4f..69315849 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -36,6 +36,9 @@ static const int c_tabSequenceBase = 1; namespace HtmlHolder { static const QString c_JSHolder = "JS_PLACE_HOLDER"; + static const QString c_cssHolder = "CSS_PLACE_HOLDER"; + static const QString c_codeBlockCssHolder = "HIGHLIGHTJS_CSS_PLACE_HOLDER"; + static const QString c_globalStyleHolder = "/* BACKGROUND_PLACE_HOLDER */"; static const QString c_extraHolder = ""; static const QString c_bodyHolder = ""; static const QString c_headHolder = ""; diff --git a/src/veditarea.cpp b/src/veditarea.cpp index 44d840d6..9c6f6ad8 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -1,5 +1,7 @@ -#include #include "veditarea.h" + +#include + #include "veditwindow.h" #include "vedittab.h" #include "vnote.h" @@ -11,6 +13,7 @@ #include "vmainwindow.h" #include "vcaptain.h" #include "vfilelist.h" +#include "vmathjaxpreviewhelper.h" extern VConfigManager *g_config; @@ -64,6 +67,8 @@ VEditArea::VEditArea(QWidget *parent) timer->start(); m_autoSave = g_config->getEnableAutoSave(); + + m_mathPreviewHelper = new VMathJaxPreviewHelper(this, this); } void VEditArea::setupUI() diff --git a/src/veditarea.h b/src/veditarea.h index 4fd9bea0..17c1ffd3 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -20,6 +20,7 @@ class VDirectory; class VFindReplaceDialog; class QLabel; class VVim; +class VMathJaxPreviewHelper; class VEditArea : public QWidget, public VNavigationMode { @@ -86,6 +87,8 @@ public: // Return the rect not containing the tab bar. QRect editAreaRect() const; + VMathJaxPreviewHelper *getMathJaxPreviewHelper() const; + signals: // Emit when current window's tab status updated. void tabStatusUpdated(const VEditTabInfo &p_info); @@ -224,6 +227,8 @@ private: // Whether auto save files. bool m_autoSave; + + VMathJaxPreviewHelper *m_mathPreviewHelper; }; inline VEditWindow* VEditArea::getWindow(int windowIndex) const @@ -242,4 +247,8 @@ inline VFindReplaceDialog *VEditArea::getFindReplaceDialog() const return m_findReplace; } +inline VMathJaxPreviewHelper *VEditArea::getMathJaxPreviewHelper() const +{ + return m_mathPreviewHelper; +} #endif // VEDITAREA_H diff --git a/src/vlivepreviewhelper.cpp b/src/vlivepreviewhelper.cpp index fed0d1e0..15488c37 100644 --- a/src/vlivepreviewhelper.cpp +++ b/src/vlivepreviewhelper.cpp @@ -1,6 +1,7 @@ #include "vlivepreviewhelper.h" #include +#include #include "veditor.h" #include "vdocument.h" @@ -8,12 +9,18 @@ #include "vgraphvizhelper.h" #include "vplantumlhelper.h" #include "vcodeblockhighlighthelper.h" +#include "vmainwindow.h" +#include "veditarea.h" +#include "vmathjaxpreviewhelper.h" extern VConfigManager *g_config; +extern VMainWindow *g_mainWin; + // Use the highest 4 bits (31-28) to indicate the lang. #define LANG_PREFIX_GRAPHVIZ 0x10000000UL #define LANG_PREFIX_PLANTUML 0x20000000UL +#define LANG_PREFIX_MATHJAX 0x30000000UL #define LANG_PREFIX_MASK 0xf0000000UL // Use th 27th bit to indicate the preview type. @@ -105,6 +112,11 @@ VLivePreviewHelper::VLivePreviewHelper(VEditor *p_editor, m_plantUMLMode = g_config->getPlantUMLMode(); m_graphvizEnabled = g_config->getEnableGraphviz(); m_mathjaxEnabled = g_config->getEnableMathjax(); + + m_mathJaxHelper = g_mainWin->getEditArea()->getMathJaxPreviewHelper(); + m_mathJaxID = m_mathJaxHelper->registerIdentifier(); + connect(m_mathJaxHelper, &VMathJaxPreviewHelper::mathjaxPreviewResultReady, + this, &VLivePreviewHelper::mathjaxPreviewResultReady); } bool VLivePreviewHelper::isPreviewLang(const QString &p_lang) const @@ -246,7 +258,8 @@ void VLivePreviewHelper::updateLivePreview() } else { m_document->setPreviewContent(vcb.m_lang, cb.imageData()); } - } else if (vcb.m_lang != "puml") { + } else if (vcb.m_lang != "mathjax") { + // No need to live preview MathJax. m_document->previewCodeBlock(m_cbIndex, vcb.m_lang, removeFence(vcb.m_text), @@ -365,6 +378,10 @@ void VLivePreviewHelper::processForInplacePreview(int p_idx) vcb.m_lang, removeFence(vcb.m_text), false); + } else if (vcb.m_lang == "mathjax") { + m_mathJaxHelper->previewMathJax(m_mathJaxID, + p_idx, + removeFence(vcb.m_text)); } } @@ -373,12 +390,17 @@ void VLivePreviewHelper::updateInplacePreview() QVector > images; for (int i = 0; i < m_codeBlocks.size(); ++i) { CodeBlockPreviewInfo &cb = m_codeBlocks[i]; - if (cb.inplacePreviewReady() && cb.hasImageData()) { - Q_ASSERT(!cb.inplacePreview().isNull()); + if (cb.inplacePreviewReady()) { // Generate the image. - cb.inplacePreview()->m_image.loadFromData(cb.imageData().toUtf8(), - cb.imageFormat().toLocal8Bit().data()); - images.append(cb.inplacePreview()); + if (cb.hasImageData()) { + cb.inplacePreview()->m_image.loadFromData(cb.imageData().toUtf8(), + cb.imageFormat().toLocal8Bit().data()); + images.append(cb.inplacePreview()); + } else if (cb.hasImageDataBa()) { + cb.inplacePreview()->m_image.loadFromData(cb.imageDataBa(), + cb.imageFormat().toLocal8Bit().data()); + images.append(cb.inplacePreview()); + } } } @@ -387,7 +409,8 @@ void VLivePreviewHelper::updateInplacePreview() // Clear image. for (int i = 0; i < m_codeBlocks.size(); ++i) { CodeBlockPreviewInfo &cb = m_codeBlocks[i]; - if (cb.inplacePreviewReady() && cb.hasImageData()) { + if (cb.inplacePreviewReady() + && (cb.hasImageData() || cb.hasImageDataBa())) { cb.inplacePreview()->m_image = QPixmap(); } } @@ -407,3 +430,19 @@ void VLivePreviewHelper::webAsyncResultReady(int p_id, cb.updateInplacePreview(m_editor, m_doc); updateInplacePreview(); } + +void VLivePreviewHelper::mathjaxPreviewResultReady(int p_identitifer, + int p_id, + const QString &p_format, + const QByteArray &p_data) +{ + if (p_identitifer != m_mathJaxID + || (p_id >= m_codeBlocks.size() || p_data.isEmpty())) { + return; + } + + CodeBlockPreviewInfo &cb = m_codeBlocks[p_id]; + cb.setImageDataBa(p_format, p_data); + cb.updateInplacePreview(m_editor, m_doc); + updateInplacePreview(); +} diff --git a/src/vlivepreviewhelper.h b/src/vlivepreviewhelper.h index d23bf015..3c609c68 100644 --- a/src/vlivepreviewhelper.h +++ b/src/vlivepreviewhelper.h @@ -11,6 +11,7 @@ class VEditor; class VDocument; class VGraphvizHelper; class VPlantUMLHelper; +class VMathJaxPreviewHelper; class CodeBlockPreviewInfo { @@ -57,6 +58,16 @@ public: return m_imgData; } + bool hasImageDataBa() const + { + return !m_imgDataBa.isEmpty(); + } + + const QByteArray &imageDataBa() const + { + return m_imgDataBa; + } + const QString &imageFormat() const { return m_imgFormat; @@ -64,10 +75,20 @@ public: void setImageData(const QString &p_format, const QString &p_data) { + m_imgDataBa.clear(); + m_imgFormat = p_format; m_imgData = p_data; } + void setImageDataBa(const QString &p_format, const QByteArray &p_data) + { + m_imgData.clear(); + + m_imgFormat = p_format; + m_imgDataBa = p_data; + } + const QSharedPointer inplacePreview() const { return m_inplacePreview; @@ -84,6 +105,8 @@ private: QString m_imgData; + QByteArray m_imgDataBa; + QString m_imgFormat; QSharedPointer m_inplacePreview; @@ -120,6 +143,11 @@ private slots: void localAsyncResultReady(int p_id, const QString &p_format, const QString &p_result); + void mathjaxPreviewResultReady(int p_identitifer, + int p_id, + const QString &p_format, + const QByteArray &p_data); + private: bool isPreviewLang(const QString &p_lang) const; @@ -154,6 +182,11 @@ private: VGraphvizHelper *m_graphvizHelper; VPlantUMLHelper *m_plantUMLHelper; + + VMathJaxPreviewHelper *m_mathJaxHelper; + + // Identification for VMathJaxPreviewHelper. + int m_mathJaxID; }; inline bool VLivePreviewHelper::isPreviewEnabled() const diff --git a/src/vmathjaxpreviewhelper.cpp b/src/vmathjaxpreviewhelper.cpp new file mode 100644 index 00000000..3ac4307e --- /dev/null +++ b/src/vmathjaxpreviewhelper.cpp @@ -0,0 +1,64 @@ +#include "vmathjaxpreviewhelper.h" + +#include +#include +#include + +#include "utils/vutils.h" +#include "vmathjaxwebdocument.h" + +VMathJaxPreviewHelper::VMathJaxPreviewHelper(QWidget *p_parentWidget, QObject *p_parent) + : QObject(p_parent), + m_parentWidget(p_parentWidget), + m_initialized(false), + m_nextID(0), + m_webView(NULL), + m_webReady(false) +{ +} + +VMathJaxPreviewHelper::~VMathJaxPreviewHelper() +{ +} + +void VMathJaxPreviewHelper::doInit() +{ + Q_ASSERT(!m_initialized); + + m_webView = new QWebEngineView(m_parentWidget); + connect(m_webView, &QWebEngineView::loadFinished, + this, [this]() { + m_webReady = true; + }); + + m_webView->hide(); + m_webView->setFocusPolicy(Qt::NoFocus); + + m_webDoc = new VMathJaxWebDocument(m_webView); + connect(m_webDoc, &VMathJaxWebDocument::mathjaxPreviewResultReady, + this, [this](int p_identifier, int p_id, const QString &p_format, const QString &p_data) { + QByteArray ba = QByteArray::fromBase64(p_data.toUtf8()); + emit mathjaxPreviewResultReady(p_identifier, p_id, p_format, ba); + }); + + QWebChannel *channel = new QWebChannel(m_webView); + channel->registerObject(QStringLiteral("content"), m_webDoc); + m_webView->page()->setWebChannel(channel); + + m_webView->setHtml(VUtils::generateMathJaxPreviewTemplate(), QUrl("qrc:/resources")); + + while (!m_webReady) { + VUtils::sleepWait(100); + } + + m_initialized = true; +} + +void VMathJaxPreviewHelper::previewMathJax(int p_identifier, + int p_id, + const QString &p_text) +{ + init(); + + m_webDoc->previewMathJax(p_identifier, p_id, p_text); +} diff --git a/src/vmathjaxpreviewhelper.h b/src/vmathjaxpreviewhelper.h new file mode 100644 index 00000000..60345653 --- /dev/null +++ b/src/vmathjaxpreviewhelper.h @@ -0,0 +1,62 @@ +#ifndef VMATHJAXPREVIEWHELPER_H +#define VMATHJAXPREVIEWHELPER_H + +#include + +class QWebEngineView; +class VMathJaxWebDocument; +class QWidget; + +class VMathJaxPreviewHelper : public QObject +{ + Q_OBJECT +public: + explicit VMathJaxPreviewHelper(QWidget *p_parentWidget, QObject *p_parent = nullptr); + + ~VMathJaxPreviewHelper(); + + // Get an ID for identification. + int registerIdentifier(); + + // Preview @p_text and return SVG data asynchronously. + // @p_identifier: identifier the caller registered; + // @p_id: internal id for each caller; + // @p_text: raw text of the MathJax script. + void previewMathJax(int p_identifier, int p_id, const QString &p_text); + +signals: + void mathjaxPreviewResultReady(int p_identifier, + int p_id, + const QString &p_format, + const QByteArray &p_data); + +private: + void init(); + + void doInit(); + + QWidget *m_parentWidget; + + int m_initialized; + + int m_nextID; + + QWebEngineView *m_webView; + + VMathJaxWebDocument *m_webDoc; + + bool m_webReady; +}; + +inline int VMathJaxPreviewHelper::registerIdentifier() +{ + return m_nextID++; +} + +inline void VMathJaxPreviewHelper::init() +{ + if (!m_initialized) { + doInit(); + } +} +#endif // VMATHJAXPREVIEWHELPER_H diff --git a/src/vmathjaxwebdocument.cpp b/src/vmathjaxwebdocument.cpp new file mode 100644 index 00000000..a5599c15 --- /dev/null +++ b/src/vmathjaxwebdocument.cpp @@ -0,0 +1,23 @@ +#include "vmathjaxwebdocument.h" + +#include + +VMathJaxWebDocument::VMathJaxWebDocument(QObject *p_parent) + : QObject(p_parent) +{ +} + +void VMathJaxWebDocument::previewMathJax(int p_identifier, + int p_id, + const QString &p_text) +{ + emit requestPreviewMathJax(p_identifier, p_id, p_text); +} + +void VMathJaxWebDocument::mathjaxResultReady(int p_identifier, + int p_id, + const QString &p_format, + const QString &p_data) +{ + emit mathjaxPreviewResultReady(p_identifier, p_id, p_format, p_data); +} diff --git a/src/vmathjaxwebdocument.h b/src/vmathjaxwebdocument.h new file mode 100644 index 00000000..687933ca --- /dev/null +++ b/src/vmathjaxwebdocument.h @@ -0,0 +1,31 @@ +#ifndef VMATHJAXWEBDOCUMENT_H +#define VMATHJAXWEBDOCUMENT_H + +#include + +class VMathJaxWebDocument : public QObject +{ + Q_OBJECT +public: + explicit VMathJaxWebDocument(QObject *p_parent = nullptr); + + void previewMathJax(int p_identifier, int p_id, const QString &p_text); + +public slots: + // Will be called in the HTML side + + void mathjaxResultReady(int p_identifier, + int p_id, + const QString &p_format, + const QString &p_data); + +signals: + void requestPreviewMathJax(int p_identifier, int p_id, const QString &p_text); + + void mathjaxPreviewResultReady(int p_identifier, + int p_id, + const QString &p_format, + const QString &p_data); +}; + +#endif // VMATHJAXWEBDOCUMENT_H diff --git a/src/vnote.cpp b/src/vnote.cpp index fcd351a1..50ee0df3 100644 --- a/src/vnote.cpp +++ b/src/vnote.cpp @@ -111,16 +111,12 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg, cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; } - const QString styleHolder("/* BACKGROUND_PLACE_HOLDER */"); - const QString cssHolder("CSS_PLACE_HOLDER"); - const QString codeBlockCssHolder("HIGHLIGHTJS_CSS_PLACE_HOLDER"); - QString templ = VUtils::readFileFromDisk(c_markdownTemplatePath); g_palette->fillStyle(templ); // Must replace the code block holder first. - templ.replace(codeBlockCssHolder, p_codeBlockStyleUrl); - templ.replace(cssHolder, p_renderStyleUrl); + templ.replace(HtmlHolder::c_codeBlockCssHolder, p_codeBlockStyleUrl); + templ.replace(HtmlHolder::c_cssHolder, p_renderStyleUrl); if (p_isPDF) { // Shoudl not display scrollbar in PDF. @@ -143,7 +139,7 @@ QString VNote::generateHtmlTemplate(const QString &p_renderBg, } if (!cssStyle.isEmpty()) { - templ.replace(styleHolder, cssStyle); + templ.replace(HtmlHolder::c_globalStyleHolder, cssStyle); } return templ; @@ -163,18 +159,25 @@ QString VNote::generateExportHtmlTemplate(const QString &p_renderBg) cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; } - const QString styleHolder("/* BACKGROUND_PLACE_HOLDER */"); - QString templ = VUtils::readFileFromDisk(c_exportTemplatePath); g_palette->fillStyle(templ); if (!cssStyle.isEmpty()) { - templ.replace(styleHolder, cssStyle); + templ.replace(HtmlHolder::c_globalStyleHolder, cssStyle); } return templ; } +QString VNote::generateMathJaxPreviewTemplate() +{ + const QString c_templatePath(":/resources/mathjax_preview_template.html"); + QString templ = VUtils::readFileFromDisk(c_templatePath); + g_palette->fillStyle(templ); + templ.replace(HtmlHolder::c_cssHolder, g_config->getCssStyleUrl()); + return templ; +} + void VNote::updateTemplate() { QString renderBg = g_config->getRenderBackgroundColor(g_config->getCurRenderBackgroundColor()); diff --git a/src/vnote.h b/src/vnote.h index 3d131eff..aefa6bab 100644 --- a/src/vnote.h +++ b/src/vnote.h @@ -114,6 +114,8 @@ public: // @p_renderBg: background color, empty to not specify given color. static QString generateExportHtmlTemplate(const QString &p_renderBg); + static QString generateMathJaxPreviewTemplate(); + public slots: void updateTemplate(); diff --git a/src/vnote.qrc b/src/vnote.qrc index 08d7eac8..5dea4f18 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -210,5 +210,8 @@ resources/themes/v_moonlight/radiobutton_checked_disabled.svg resources/themes/v_moonlight/radiobutton_unchecked_disabled.svg resources/icons/universal_entry_tb.svg + resources/mathjax_preview.js + resources/mathjax_preview_template.html + utils/dom-to-image/dom-to-image.js