1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

All: Refactor Markdown rendering (#1315)

* Refactoring MdToHtml to avoid manually rendering tokens

* Minor fix

* Fixed loading of resources

* Handle clicking on checkboxes

* Added back Katex support

* Fixed issues with Katex and note rendering

* Added back support for links

* Restored code block highlighting support

* clean up

* Applying update to mobile

* Fixed handling of links and cleaned up to share more code between mobile and desktop

* Restored content caching and improved handling of additional assets

* Clean up and a few fixes

* Applied more updates to mobile and added code highlighting support
This commit is contained in:
Laurent Cozic 2019-03-08 17:14:17 +00:00 committed by GitHub
parent 5719ae495a
commit 9289dbdf77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1223 additions and 1468 deletions

View File

@ -43,6 +43,7 @@ fs.readdirSync(guiPath).forEach((filename) => {
const libContent = [
fs.readFileSync(basePath + '/ReactNativeClient/lib/string-utils-common.js', 'utf8'),
fs.readFileSync(basePath + '/ReactNativeClient/lib/markJsUtils.js', 'utf8'),
fs.readFileSync(basePath + '/ReactNativeClient/lib/MdToHtml/webviewLib.js', 'utf8'),
];
fs.writeFileSync(__dirname + '/gui/note-viewer/lib.js', libContent.join('\n'), 'utf8');

View File

@ -36,6 +36,7 @@ const ResourceFetcher = require('lib/services/ResourceFetcher');
const { toSystemSlashes, safeFilename } = require('lib/path-utils');
const { clipboard } = require('electron');
const SearchEngine = require('lib/services/SearchEngine');
const ModelCache = require('lib/services/ModelCache');
const NoteTextViewer = require('./NoteTextViewer.min');
require('brace/mode/markdown');
@ -75,6 +76,7 @@ class NoteTextComponent extends React.Component {
// to automatically set the title.
newAndNoTitleChangeNoteId: null,
bodyHtml: '',
lastRenderCssFiles: [],
lastKeys: [],
showLocalSearch: false,
localSearch: Object.assign({}, this.localSearchDefaultState),
@ -226,9 +228,10 @@ class NoteTextComponent extends React.Component {
if (!this.state.note || !this.state.note.body) return;
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
if (resourceIds.indexOf(resource.id) >= 0) {
this.mdToHtml().clearCache();
// this.mdToHtml().clearCache();
this.lastSetHtml_ = '';
this.updateHtml(this.state.note.body);
this.scheduleHtmlUpdate();
//this.updateHtml(this.state.note.body);
}
}
@ -563,6 +566,8 @@ class NoteTextComponent extends React.Component {
}
}
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
this.updateHtml(newState.note ? newState.note.body : '');
}
@ -635,7 +640,7 @@ class NoteTextComponent extends React.Component {
const arg0 = args && args.length >= 1 ? args[0] : null;
const arg1 = args && args.length >= 2 ? args[1] : null;
reg.logger().debug('Got ipc-message: ' + msg, args);
console.info('Got ipc-message: ' + msg, args);
if (msg.indexOf('checkboxclick:') === 0) {
// Ugly hack because setting the body here will make the scrollbar
@ -644,7 +649,7 @@ class NoteTextComponent extends React.Component {
// "afterRender" event has been called.
this.restoreScrollTop_ = this.editorScrollTop();
const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.state.note.body);
const newBody = shared.toggleCheckbox(msg, this.state.note.body);
this.saveOneProperty('body', newBody);
} else if (msg === 'setMarkerCount') {
const ls = Object.assign({}, this.state.localSearch);
@ -724,6 +729,7 @@ class NoteTextComponent extends React.Component {
// When using the file:// protocol, openExternal doesn't work (does nothing) with URL-encoded paths
require('electron').shell.openExternal(urlDecode(msg));
} else {
console.info('OPEN URL');
require('electron').shell.openExternal(msg);
}
} else if (msg.indexOf('#') === 0) {
@ -794,7 +800,7 @@ class NoteTextComponent extends React.Component {
});
if (Setting.value('env') === 'dev') {
// this.webviewRef_.current.wrappedInstance.openDevTools();
this.webviewRef_.current.wrappedInstance.openDevTools();
}
}
@ -891,31 +897,22 @@ class NoteTextComponent extends React.Component {
}
}
updateHtml(body = null, options = null) {
async updateHtml(body = null, options = null) {
if (!options) options = {};
if (!('useCustomCss' in options)) options.useCustomCss = true;
const mdOptions = {
onResourceLoaded: () => {
if (this.resourceLoadedTimeoutId_) {
clearTimeout(this.resourceLoadedTimeoutId_);
this.resourceLoadedTimeoutId_ = null;
}
this.resourceLoadedTimeoutId_ = setTimeout(() => {
this.resourceLoadedTimeoutId_ = null;
this.updateHtml();
this.forceUpdate();
}, 100);
},
postMessageSyntax: 'ipcProxySendToHost',
userCss: options.useCustomCss ? this.props.customCss : '',
};
let bodyToRender = body;
if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
const theme = themeStyle(this.props.theme);
let bodyToRender = body;
if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
const mdOptions = {
codeTheme: theme.codeThemeCss,
postMessageSyntax: 'ipcProxySendToHost',
userCss: options.useCustomCss ? this.props.customCss : '',
resources: await shared.attachedResources(bodyToRender),
};
let bodyHtml = '';
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
@ -925,9 +922,12 @@ class NoteTextComponent extends React.Component {
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*';
}
bodyHtml = this.mdToHtml().render(bodyToRender, theme, mdOptions);
const result = this.mdToHtml().render(bodyToRender, theme, mdOptions);
this.setState({ bodyHtml: bodyHtml });
this.setState({
bodyHtml: result.html,
lastRenderCssFiles: result.cssFiles,
});
}
titleField_keyDown(event) {
@ -1061,7 +1061,7 @@ class NoteTextComponent extends React.Component {
});
}
printTo_(target, options) {
async printTo_(target, options) {
if (this.props.selectedNoteIds.length !== 1 || !this.webviewRef_.current) {
throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
}
@ -1072,13 +1072,13 @@ class NoteTextComponent extends React.Component {
const previousTheme = Setting.value('theme');
Setting.setValue('theme', Setting.THEME_LIGHT);
this.lastSetHtml_ = '';
this.updateHtml(tempBody, { useCustomCss: false });
await this.updateHtml(tempBody, { useCustomCss: false });
this.forceUpdate();
const restoreSettings = () => {
const restoreSettings = async () => {
Setting.setValue('theme', previousTheme);
this.lastSetHtml_ = '';
this.updateHtml(previousBody);
await this.updateHtml(previousBody);
this.forceUpdate();
}
@ -1100,7 +1100,7 @@ class NoteTextComponent extends React.Component {
}, 100);
}
commandSavePdf() {
async commandSavePdf() {
try {
if (!this.state.note) throw new Error(_('Only one note can be printed or exported to PDF at a time.'));
@ -1111,15 +1111,15 @@ class NoteTextComponent extends React.Component {
if (!path) return;
this.printTo_('pdf', { path: path });
await this.printTo_('pdf', { path: path });
} catch (error) {
bridge().showErrorMessageBox(error.message);
}
}
commandPrint() {
async commandPrint() {
try {
this.printTo_('printer');
await this.printTo_('printer');
} catch (error) {
bridge().showErrorMessageBox(error.message);
}
@ -1711,7 +1711,9 @@ class NoteTextComponent extends React.Component {
const htmlHasChanged = this.lastSetHtml_ !== html;
if (htmlHasChanged) {
let options = {codeTheme: theme.codeThemeCss};
let options = {
cssFiles: this.state.lastRenderCssFiles,
};
this.webviewRef_.current.wrappedInstance.send('setHtml', html, options);
this.lastSetHtml_ = html;
}

View File

@ -40,6 +40,20 @@ class NoteTextViewerComponent extends React.Component {
const fn = this.webviewListeners_[n];
wv.addEventListener(n, fn);
}
let isAlreadyReady = false;
try {
isAlreadyReady = !this.webviewRef_.current.isLoading()
} catch (error) {
// Ignore - it means the view has not started loading, and the DOM ready event has not been emitted yet
// Error is "The WebView must be attached to the DOM and the dom-ready event emitted before this method can be called."
}
// Edge-case - the webview was already ready by the time initWebview was
// called - so manually call the domReady event to notify caller.
if (isAlreadyReady) {
this.webview_domReady({});
}
}
destroyWebview() {

View File

@ -28,14 +28,11 @@
ul ul, ul ol, ol ul, ol ol {
margin-bottom: 0px;
}
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
</style>
</head>
<body id="body">
<div id="hlScriptContainer"></div>
<div id="mermaidScriptContainer"></div>
<div id="styleContainer"></div>
<div id="markScriptContainer"></div>
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
<script src="./lib.js"></script>
@ -59,62 +56,18 @@
}
});
// ----------------------------------------------------------------------
// Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
let hljsScriptAdded = false;
let hljsLoaded = false;
function loadHljs(options) {
hljsScriptAdded = true;
const script = document.createElement('script');
script.onload = function () {
hljsLoaded = true;
applyHljs();
};
script.src = 'highlight/highlight.pack.js';
document.getElementById('hlScriptContainer').appendChild(script);
const link = document.createElement('link');
link.rel = 'stylesheet';
// https://ace.c9.io/build/kitchen-sink.html
// https://highlightjs.org/static/demo/
link.href = 'highlight/styles/' + options.codeTheme;
document.getElementById('hlScriptContainer').appendChild(link);
}
function loadAndApplyHljs(options) {
var codeElements = document.getElementsByClassName('code');
if (!codeElements.length) return;
if (!hljsScriptAdded) {
this.loadHljs(options);
return;
}
// If HLJS is not loaded yet, no need to do anything. When it loads
// it will automatically apply the style to all the code elements.
if (hljsLoaded) applyHljs(codeElements);
}
function applyHljs(codeElements) {
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
for (var i = 0; i < codeElements.length; i++) {
try {
hljs.highlightBlock(codeElements[i]);
} catch (error) {
console.warn('Cannot highlight code', error);
}
const loadedCssFiles_ = {};
function loadCssFiles(cssFiles) {
for (let i = 0; i < cssFiles.length; i++) {
const f = cssFiles[i];
if (loadedCssFiles_[f]) continue;
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = f;
document.getElementById('styleContainer').appendChild(link);
}
}
// ----------------------------------------------------------------------
// / Handle dynamically loading HLJS when a code element is present
// ----------------------------------------------------------------------
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
@ -142,23 +95,6 @@
setPercentScroll(percentScroll_);
}
const loadedScripts_ = {};
function lazyLoadScript(url, containerId, onReady) {
if (loadedScripts_[url]) {
onReady();
return;
}
console.info('Loading script: ' + url);
const script = document.createElement('script');
script.onload = onReady;
script.src = url;
document.getElementById(containerId).appendChild(script);
loadedScripts_[url] = url;
}
ipc.setHtml = (event) => {
const html = event.html;
@ -168,21 +104,6 @@
contentElement.innerHTML = html;
loadAndApplyHljs(event.options);
// Remove the bullet from "ul" for checkbox lists and extra padding
// const checkboxes = document.getElementsByClassName('checkbox');
// for (let i = 0; i < checkboxes.length; i++) {
// const cb = checkboxes[i];
// const ul = cb.parentElement.parentElement;
// if (!ul) {
// console.warn('Unexpected layout for checkbox');
// continue;
// }
// ul.style.listStyleType = 'none';
// ul.style.paddingLeft = 0;
// }
let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now();
ignoreNextScrollEvent = true;
@ -203,12 +124,7 @@
}, 1);
}
if (document.getElementsByClassName('mermaid').length) {
console.warn('Mermaid support is currently disabled');
// lazyLoadScript('../../node_modules/mermaid/dist/mermaid.min.js', 'mermaidScriptContainer', () => {
// mermaid.init();
// });
}
loadCssFiles(event.options.cssFiles);
}
let ignoreNextScrollEvent = false;
@ -372,49 +288,8 @@
}
});
function handleInternalLink(event, anchorNode) {
const href = anchorNode.getAttribute('href');
if (href.indexOf('#') === 0) {
event.preventDefault();
location.hash = href;
return true;
}
return false;
}
function getParentAnchorElement(element) {
let counter = 0;
while (true) {
if (counter++ >= 10000) {
console.warn('been looping for too long - exiting')
return null;
}
if (!element) return null;
if (element.nodeName === 'A') return element;
element = element.parentElement;
}
}
document.addEventListener('click', function(event) {
const anchor = getParentAnchorElement(event.target);
if (!anchor) return;
// Prevent URLs added via <a> tags from being opened within the application itself
// otherwise it would open the whole website within the WebView.
if (!anchor.hasAttribute('data-from-md')) {
if (handleInternalLink(event, anchor)) return;
event.preventDefault();
ipcProxySendToHost(anchor.getAttribute('href'));
return;
}
// If this is an internal link, jump to the anchor directly
if (anchor.hasAttribute('data-from-md')) {
if (handleInternalLink(event, anchor)) return;
}
webviewLib.initialize({
postMessage: ipcProxySendToHost,
});
// Disable drag and drop otherwise it's possible to drop a URL

View File

@ -1418,7 +1418,8 @@
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
"dev": true
},
"compare-version": {
"version": "0.1.2",
@ -1561,288 +1562,6 @@
"resolved": "https://registry.npmjs.org/currify/-/currify-2.0.6.tgz",
"integrity": "sha512-F0lbcoBkA2FMcejFeHJkDEhQ1AvVkTpkn9PMzJch+7mHy5WdteZ9t+nhT6cOdga4uRay3rjvprgp8tUkixFy8w=="
},
"d3": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.9.1.tgz",
"integrity": "sha512-JceuBn5VVWySPQc9EA0gfq0xQVgEQXGokHhe+359bmgGeUITLK2r2b9idMzquQne9DKxb7JDCE1gDRXe9OIF2Q==",
"requires": {
"d3-array": "1",
"d3-axis": "1",
"d3-brush": "1",
"d3-chord": "1",
"d3-collection": "1",
"d3-color": "1",
"d3-contour": "1",
"d3-dispatch": "1",
"d3-drag": "1",
"d3-dsv": "1",
"d3-ease": "1",
"d3-fetch": "1",
"d3-force": "1",
"d3-format": "1",
"d3-geo": "1",
"d3-hierarchy": "1",
"d3-interpolate": "1",
"d3-path": "1",
"d3-polygon": "1",
"d3-quadtree": "1",
"d3-random": "1",
"d3-scale": "2",
"d3-scale-chromatic": "1",
"d3-selection": "1",
"d3-shape": "1",
"d3-time": "1",
"d3-time-format": "2",
"d3-timer": "1",
"d3-transition": "1",
"d3-voronoi": "1",
"d3-zoom": "1"
}
},
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
},
"d3-axis": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
},
"d3-brush": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz",
"integrity": "sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"d3-chord": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
"integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
"requires": {
"d3-array": "1",
"d3-path": "1"
}
},
"d3-collection": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
},
"d3-color": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz",
"integrity": "sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw=="
},
"d3-contour": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
"integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
"requires": {
"d3-array": "^1.1.1"
}
},
"d3-dispatch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz",
"integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g=="
},
"d3-drag": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz",
"integrity": "sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg==",
"requires": {
"d3-dispatch": "1",
"d3-selection": "1"
}
},
"d3-dsv": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz",
"integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==",
"requires": {
"commander": "2",
"iconv-lite": "0.4",
"rw": "1"
}
},
"d3-ease": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz",
"integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ=="
},
"d3-fetch": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
"integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
"requires": {
"d3-dsv": "1"
}
},
"d3-force": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.0.tgz",
"integrity": "sha512-PFLcDnRVANHMudbQlIB87gcfQorEsDIAvRpZ2bNddfM/WxdsEkyrEaOIPoydhH1I1V4HPjNLGOMLXCA0AuGQ9w==",
"requires": {
"d3-collection": "1",
"d3-dispatch": "1",
"d3-quadtree": "1",
"d3-timer": "1"
}
},
"d3-format": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz",
"integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ=="
},
"d3-geo": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.3.tgz",
"integrity": "sha512-n30yN9qSKREvV2fxcrhmHUdXP9TNH7ZZj3C/qnaoU0cVf/Ea85+yT7HY7i8ySPwkwjCNYtmKqQFTvLFngfkItQ==",
"requires": {
"d3-array": "1"
}
},
"d3-hierarchy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz",
"integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w=="
},
"d3-interpolate": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
"integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
"requires": {
"d3-color": "1"
}
},
"d3-path": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz",
"integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA=="
},
"d3-polygon": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz",
"integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w=="
},
"d3-quadtree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz",
"integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA=="
},
"d3-random": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
"integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
},
"d3-scale": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
"requires": {
"d3-array": "^1.2.0",
"d3-collection": "1",
"d3-format": "1",
"d3-interpolate": "1",
"d3-time": "1",
"d3-time-format": "2"
}
},
"d3-scale-chromatic": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz",
"integrity": "sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw==",
"requires": {
"d3-color": "1",
"d3-interpolate": "1"
}
},
"d3-selection": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz",
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
},
"d3-shape": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.4.tgz",
"integrity": "sha512-izaz4fOpOnY3CD17hkZWNxbaN70sIGagLR/5jb6RS96Y+6VqX+q1BQf1av6QSBRdfULi3Gb8Js4CzG4+KAPjMg==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
"integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
},
"d3-time-format": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz",
"integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==",
"requires": {
"d3-time": "1"
}
},
"d3-timer": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz",
"integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg=="
},
"d3-transition": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz",
"integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==",
"requires": {
"d3-color": "1",
"d3-dispatch": "1",
"d3-ease": "1",
"d3-interpolate": "1",
"d3-selection": "^1.1.0",
"d3-timer": "1"
}
},
"d3-voronoi": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
},
"d3-zoom": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.7.3.tgz",
"integrity": "sha512-xEBSwFx5Z9T3/VrwDkMt+mr0HCzv7XjpGURJ8lWmIC8wxe32L39eWHIasEe/e7Ox8MPU4p1hvH8PKN2olLzIBg==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"dagre-d3-renderer": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz",
"integrity": "sha512-XH2a86isUHRxzIYbjQVEuZtJnWEufb64H5DuXIUmn8esuB40jgLEbUUclulWOW62/ZoXlj2ZDyL8SJ+YRxs+jQ==",
"requires": {
"dagre-layout": "^0.8.8",
"lodash": "^4.17.5"
}
},
"dagre-layout": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/dagre-layout/-/dagre-layout-0.8.8.tgz",
"integrity": "sha512-ZNV15T9za7X+fV8Z07IZquUKugCxm5owoiPPxfEx6OJRD331nkiIaF3vSt0JEY5FkrY0KfRQxcpQ3SpXB7pLPQ==",
"requires": {
"graphlibrary": "^2.2.0",
"lodash": "^4.17.5"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -2415,11 +2134,6 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"escaper": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz",
"integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ=="
},
"escodegen": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz",
@ -3553,14 +3267,6 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"graphlibrary": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/graphlibrary/-/graphlibrary-2.2.0.tgz",
"integrity": "sha512-XTcvT55L8u4MBZrM37zXoUxsgxs/7sow7YSygd9CIwfWTVO8RVu7AYXhhCiTuFEf+APKgx6Jk4SuQbYR0vYKmQ==",
"requires": {
"lodash": "^4.17.5"
}
},
"growly": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
@ -3647,15 +3353,10 @@
}
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"highlight.js": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
"version": "9.15.6",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz",
"integrity": "sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ=="
},
"hoist-non-react-statics": {
"version": "2.5.0",
@ -4027,11 +3728,6 @@
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
"dev": true
},
"is-regexp": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk="
},
"is-retry-allowed": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
@ -4563,33 +4259,6 @@
}
}
},
"mermaid": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.0.0.tgz",
"integrity": "sha512-vUQRykev0A6RtxIVqQT3a9TDxcSbdZbQF5JDyKgidnYuJy8BE8jp6LM+HKDSQuroKm6buu4NlpMO+qhxIP/cTg==",
"requires": {
"d3": "^5.7.0",
"dagre-d3-renderer": "^0.5.8",
"dagre-layout": "^0.8.8",
"graphlibrary": "^2.2.0",
"he": "^1.2.0",
"lodash": "^4.17.11",
"moment": "^2.23.0",
"scope-css": "^1.2.1"
},
"dependencies": {
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
}
}
},
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@ -5993,11 +5662,6 @@
}
}
},
"rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
},
"rxjs": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
@ -6039,16 +5703,6 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"scope-css": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/scope-css/-/scope-css-1.2.1.tgz",
"integrity": "sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==",
"requires": {
"escaper": "^2.5.3",
"slugify": "^1.3.1",
"strip-css-comments": "^3.0.0"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
@ -6149,11 +5803,6 @@
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
},
"slugify": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz",
"integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw=="
},
"smalltalk": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/smalltalk/-/smalltalk-2.5.1.tgz",
@ -6485,14 +6134,6 @@
"is-utf8": "^0.2.0"
}
},
"strip-css-comments": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-css-comments/-/strip-css-comments-3.0.0.tgz",
"integrity": "sha1-elYl7/iisibPiUehElTaluE9rok=",
"requires": {
"is-regexp": "^1.0.0"
}
},
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",

View File

@ -96,7 +96,7 @@
"form-data": "^2.3.2",
"formatcoords": "^1.1.3",
"fs-extra": "^5.0.0",
"highlight.js": "^9.12.0",
"highlight.js": "^9.15.6",
"html-entities": "^1.2.1",
"image-type": "^3.0.0",
"joplin-turndown": "^4.0.11",
@ -109,7 +109,6 @@
"markdown-it": "^8.4.1",
"markdown-it-katex": "^2.0.3",
"md5": "^2.2.1",
"mermaid": "^8.0.0",
"mime": "^2.3.1",
"moment": "^2.22.2",
"multiparty": "^4.2.1",

View File

@ -1,11 +0,0 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"diacritics": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
"integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E="
}
}
}

View File

@ -1,17 +0,0 @@
#!/bin/bash
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
APP_DIR="$ROOT_DIR/app"
cd "$APP_DIR"
VERSION="$(npm version patch)"
git add -A
git commit -m "Electron release $VERSION"
git tag $VERSION
git push && git push --tags
echo ""
echo "Create a draft release at: https://github.com/laurent22/joplin/releases/tag/$VERSION"
echo ""
echo "Then run:"
echo ""
echo "git pull && node $APP_DIR/update-readme-download.js && git add -A && git commit -m 'Update website' && git push && git push --tags"

View File

@ -195,6 +195,17 @@ class BaseModel {
return this.modelSelectAll(q.sql);
}
static async byIds(ids, options = null) {
if (!ids.length) return [];
if (!options) options = {};
if (!options.fields) options.fields = '*';
let sql = 'SELECT ' + this.db().escapeFields(options.fields) + ' FROM `' + this.tableName() + '`';
sql += ' WHERE id IN ("' + ids.join('","') + '")';
let q = this.applySqlOptions(options, sql);
return this.modelSelectAll(q.sql);
}
static async search(options = null) {
if (!options) options = {};
if (!options.fields) options.fields = '*';

View File

@ -2,407 +2,32 @@ const MarkdownIt = require('markdown-it');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const ObjectUtils = require('lib/ObjectUtils');
const { shim } = require('lib/shim.js');
const { _ } = require('lib/locale');
const md5 = require('md5');
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
const MdToHtml_Mermaid = require('lib/MdToHtml_Mermaid');
const StringUtils = require('lib/string-utils.js');
const noteStyle = require('./MdToHtml/noteStyle');
const rules = {
image: require('./MdToHtml/rules/image'),
checkbox: require('./MdToHtml/rules/checkbox'),
katex: require('./MdToHtml/rules/katex'),
link_open: require('./MdToHtml/rules/link_open'),
html_block: require('./MdToHtml/rules/html_block'),
html_inline: require('./MdToHtml/rules/html_inline'),
highlight_keywords: require('./MdToHtml/rules/highlight_keywords'),
};
const setupLinkify = require('./MdToHtml/setupLinkify');
const hljs = require('highlight.js');
class MdToHtml {
constructor(options = null) {
if (!options) options = {};
this.loadedResources_ = {};
this.cachedContent_ = null;
this.cachedContentKey_ = null;
this.extraCssBlocks_ = [];
this.lastExecutedPlugins_ = [];
// Must include last "/"
this.resourceBaseUrl_ = ('resourceBaseUrl' in options) ? options.resourceBaseUrl : 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(escape(body))); // https://github.com/pvorb/node-md5/issues/41
k.push(md5(JSON.stringify(style)));
k.push(md5(JSON.stringify(options)));
return k.join('_');
}
clearCache() {
this.cachedContent_ = null;
this.cachedContentKey_ = null;
}
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, defaultValue = null) {
for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
}
return defaultValue;
}
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;
}
async loadResource(id, options) {
// 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);
if (!resource) {
// Can happen for example if an image is attached to a note, but the resource hasn't
// been downloaded from the sync target yet.
console.info('Cannot load resource: ' + id);
delete this.loadedResources_[id];
return;
}
const localState = await Resource.localState(resource);
if (localState.fetch_status !== Resource.FETCH_STATUS_DONE) {
delete this.loadedResources_[id];
console.info('Resource not yet fetched: ' + id);
return;
}
this.loadedResources_[id] = resource;
if (options.onResourceLoaded) options.onResourceLoaded();
}
renderImage_(attrs, options) {
const title = this.getAttr_(attrs, 'title');
const href = this.getAttr_(attrs, 'src');
if (!Resource.isResourceUrl(href)) {
return '<img data-from-md title="' + htmlentities(title) + '" src="' + href + '"/>';
}
const resourceId = Resource.urlToId(href);
const resource = this.loadedResources_[resourceId];
if (!resource) {
this.loadResource(resourceId, options);
return '';
}
if (!resource.id) return ''; // Resource is being loaded
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let src = './' + Resource.filename(resource);
if (this.resourceBaseUrl_ !== null) src = this.resourceBaseUrl_ + src;
let output = '<img data-from-md data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + src + '"/>';
return output;
}
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
}
renderImageHtml_(before, src, after, options) {
const resourceId = Resource.urlToId(src);
const resource = this.loadedResources_[resourceId];
if (!resource) {
this.loadResource(resourceId, options);
return '';
}
if (!resource.id) return ''; // Resource is being loaded
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let newSrc = './' + Resource.filename(resource);
if (this.resourceBaseUrl_ !== null) newSrc = this.resourceBaseUrl_ + newSrc;
let output = '<img ' + before + ' data-resource-id="' + resource.id + '" src="' + newSrc + '" ' + after + '/>';
return output;
}
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
}
renderOpenLink_(attrs, options) {
let href = this.getAttr_(attrs, 'href');
const text = this.getAttr_(attrs, 'text');
const isResourceUrl = Resource.isResourceUrl(href);
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
let resourceIdAttr = "";
let icon = "";
let hrefAttr = '#';
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} else {
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
let output = "<a data-from-md " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
return output;
}
renderCloseLink_(attrs, options) {
return '</a>';
}
rendererPlugin_(language) {
if (!language) return null;
if (!this.rendererPlugins_) {
this.rendererPlugins_ = {};
this.rendererPlugins_['katex'] = new MdToHtml_Katex();
this.rendererPlugins_['mermaid'] = new MdToHtml_Mermaid();
}
const plugin = this.rendererPlugins_[language];
return plugin && plugin.enabled() ? plugin : null;
}
parseInlineCodeLanguage_(content) {
const m = content.match(/^\{\.([a-zA-Z0-9]+)\}/);
if (m && m.length >= 2) {
const language = m[1];
return {
language: language,
newContent: content.substr(language.length + 3),
};
}
return null;
}
urldecode_(str) {
try {
return decodeURIComponent((str+'').replace(/\+/g, '%20'));
} catch (error) {
// decodeURIComponent can throw if the string contains non-encoded data (for example "100%")
// so in this case just return the non encoded string.
return str;
}
}
renderTokens_(markdownIt, tokens, options) {
let output = [];
let previousToken = null;
let anchorAttrs = [];
let extraCssBlocks = {};
let anchorHrefs = [];
for (let i = 0; i < tokens.length; i++) {
let t = tokens[i];
const nextToken = i < tokens.length ? tokens[i+1] : null;
let tag = t.tag;
let openTag = null;
let closeTag = null;
let attrs = t.attrs ? t.attrs : [];
let tokenContent = t.content ? t.content : '';
const isCodeBlock = tag === 'code' && t.block;
const isInlineCode = t.type === 'code_inline';
const codeBlockLanguage = t && t.info ? t.info : null;
let rendererPlugin = null;
let rendererPluginOptions = { tagType: 'inline' };
let linkHref = null;
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
if (rendererPlugin && this.lastExecutedPlugins_.indexOf(codeBlockLanguage) < 0) {
this.lastExecutedPlugins_.push(codeBlockLanguage);
}
if (isInlineCode) {
openTag = null;
} else if (tag && (t.type.indexOf('html_inline') >= 0 || t.type.indexOf('html_block') >= 0)) {
openTag = null;
} else 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';
} else if (isCodeBlock) {
if (rendererPlugin) {
openTag = null;
} else {
openTag = 'pre';
}
}
if (openTag) {
if (openTag === 'a') {
anchorAttrs.push(attrs);
anchorHrefs.push(this.getAttr_(attrs, 'href'));
output.push(this.renderOpenLink_(attrs, options));
} else {
const attrsHtml = this.renderAttrs_(attrs);
output.push('<' + openTag + (attrsHtml ? ' ' + attrsHtml : '') + '>');
}
}
if (isCodeBlock) {
const codeAttrs = ['code'];
if (!rendererPlugin) {
if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
}
} else if (isInlineCode) {
const result = this.parseInlineCodeLanguage_(tokenContent);
if (result) {
rendererPlugin = this.rendererPlugin_(result.language);
tokenContent = result.newContent;
}
if (!rendererPlugin) {
output.push('<code class="inline-code">');
}
}
if (t.type === 'math_inline' || t.type === 'math_block') {
rendererPlugin = this.rendererPlugin_('katex');
rendererPluginOptions = { tagType: t.type === 'math_block' ? 'block' : 'inline' };
}
if (rendererPlugin) {
rendererPlugin.loadAssets().catch((error) => {
console.warn('MdToHtml: Error loading assets for ' + rendererPlugin.name() + ': ', error.message);
});
}
if (t.type === 'image') {
if (tokenContent) attrs.push(['title', tokenContent]);
output.push(this.renderImage_(attrs, options));
} else if (t.type === 'html_inline' || t.type === 'html_block') {
output.push(t.content);
} else if (t.type === 'softbreak') {
output.push('<br/>');
} else if (t.type === 'hardbreak') {
output.push('<br/>');
} else if (t.type === 'hr') {
output.push('<hr/>');
} else {
if (t.children) {
const parsedChildren = this.renderTokens_(markdownIt, t.children, options);
output = output.concat(parsedChildren);
} else {
if (tokenContent) {
if ((isCodeBlock || isInlineCode) && rendererPlugin) {
output = rendererPlugin.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline');
} else if (rendererPlugin) {
output = rendererPlugin.processContent(output, tokenContent, rendererPluginOptions.tagType);
} else {
output.push(htmlentities(tokenContent));
}
}
}
}
if (nextToken && nextToken.tag === 'li' && t.tag === 'p') {
closeTag = null;
} else if (t.type === 'link_close') {
closeTag = 'a';
} else if (tag && t.type.indexOf('inline') >= 0) {
closeTag = openTag;
} else if (isCodeBlock) {
if (!rendererPlugin) closeTag = openTag;
}
if (isCodeBlock) {
if (!rendererPlugin) {
output.push('</code>');
}
} else if (isInlineCode) {
if (!rendererPlugin) {
output.push('</code>');
}
}
if (closeTag) {
if (closeTag === 'a') {
const currentAnchorAttrs = anchorAttrs.pop();
output.push(this.renderCloseLink_(currentAnchorAttrs, options));
} else {
output.push('</' + closeTag + '>');
}
}
if (rendererPlugin) {
const extraCss = rendererPlugin.extraCss();
const name = rendererPlugin.name();
if (extraCss && !(name in extraCssBlocks)) {
extraCssBlocks[name] = extraCss;
}
}
previousToken = t;
}
// Insert the extra CSS at the top of the HTML
if (!ObjectUtils.isEmpty(extraCssBlocks)) {
const temp = [];//['<style>'];
for (let n in extraCssBlocks) {
if (!extraCssBlocks.hasOwnProperty(n)) continue;
temp.push(extraCssBlocks[n]);
}
// temp.push('</style>');
if (temp.length) this.extraCssBlocks_.push(temp.join('\n'));
//output = temp.concat(output);
}
return output.join('');
}
applyHighlightedKeywords_(body, keywords) {
if (!keywords.length) return body;
return StringUtils.surroundKeywords(keywords, body, '<span class="highlighted-keyword">', '</span>');
this.cachedOutputs_ = {};
}
render(body, style, options = null) {
@ -411,361 +36,90 @@ class MdToHtml {
if (!options.paddingBottom) options.paddingBottom = '0';
if (!options.highlightedKeywords) options.highlightedKeywords = [];
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;
const cacheKey = md5(escape(body + JSON.stringify(options) + JSON.stringify(style)));
const cachedOutput = this.cachedOutputs_[cacheKey];
if (cachedOutput) return cachedOutput;
this.lastExecutedPlugins_ = [];
const context = {
css: {},
cssFiles: {},
assetLoaders: {},
};
const md = new MarkdownIt({
const markdownIt = new MarkdownIt({
breaks: true,
linkify: true,
html: true,
});
highlight: function(str, lang) {
try {
let hlCode = '';
if (lang && hljs.getLanguage(lang)) {
hlCode = hljs.highlight(lang, str, true).value;
} else {
hlCode = hljs.highlightAuto(str).value;
}
body = this.applyHighlightedKeywords_(body, options.highlightedKeywords);
if (shim.isReactNative()) {
context.css['hljs'] = shim.loadCssFromJs(options.codeTheme);
} else {
context.cssFiles['hljs'] = 'highlight/styles/' + options.codeTheme;
}
// Add `file:` protocol in linkify to allow text in the format of "file://..." to translate into
// file-URL links in html view
md.linkify.add('file:', {
validate: function (text, pos, self) {
var tail = text.slice(pos);
if (!self.re.file) {
// matches all local file URI on Win/Unix/MacOS systems including reserved characters in some OS (i.e. no OS specific sanity check)
self.re.file = new RegExp('^[\\/]{2,3}[\\S]+');
}
if (self.re.file.test(tail)) {
return tail.match(self.re.file)[0].length;
}
return 0;
}
});
// enable file link URLs in MarkdownIt. Keeps other URL restrictions of MarkdownIt untouched.
// Format [link name](file://...)
md.validateLink = function (url) {
var BAD_PROTO_RE = /^(vbscript|javascript|data):/;
var GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/;
// url should be normalized at this point, and existing entities are decoded
var str = url.trim().toLowerCase();
return BAD_PROTO_RE.test(str) ? (GOOD_DATA_RE.test(str) ? true : false) : true;
}
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
// to math_inline and math_block blocks. These blocks are then processed directly with the Katex
// library. It is better this way as then it is possible to conditionally load the CSS required by
// Katex and use an up-to-date version of Katex (as of 2018, the plugin is still using 0.6, which is
// buggy instead of 0.9).
md.use(require('markdown-it-katex'));
// 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.indexOf('- [x]') >= 0) {
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
counter++;
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
});
}
}
const env = {};
const tokens = md.parse(body, env);
this.extraCssBlocks_ = [];
let renderedBody = this.renderTokens_(md, tokens, options);
// console.info(body);
// console.info(tokens);
// console.info(renderedBody);
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.classList.contains('tick') ? this.classList.remove('tick') : this.classList.add('tick'); return false;";
return '<a data-from-md href="#" onclick="' + js + '" class="checkbox ' + (type == 'NOTICK' ? '' : 'tick') + '"><span>' + '' + '</span></a>';
});
if (loopCount++ >= 9999) break;
}
}
renderedBody = renderedBody.replace(/<img(.*?)src=["'](.*?)["'](.*?)\/>/g, (v, before, src, after) => {
if (!Resource.isResourceUrl(src)) return '<img ' + before + ' src="' + src + '" ' + after + '/>';
return this.renderImageHtml_(before, src, after, options);
});
// To disable meta tags that would refresh the page - eg "<meta http-equiv="refresh" content="5; url=/">"
// Also disable a few other tags that are likely not meant to be rendered.
// https://github.com/laurent22/joplin/issues/769
renderedBody = renderedBody.replace(/<(meta|title|body|html|script)/, '&lt;$1');
// 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 fontFamily = "'Avenir', 'Arial', sans-serif";
const css = `
body {
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
line-height: ` + style.htmlLineHeight + `;
background-color: ` + style.htmlBackgroundColor + `;
font-family: ` + fontFamily + `;
padding-bottom: ` + options.paddingBottom + `;
}
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-corner {
background: none;
}
::-webkit-scrollbar-track {
border: none;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
}
::-webkit-scrollbar-track:hover {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
}
/* Remove top padding and margin from first child so that top of rendered text is aligned to top of text editor text */
#rendered-md h1:first-child,
#rendered-md h2:first-child,
#rendered-md h3:first-child,
#rendered-md h4:first-child,
#rendered-md ul:first-child,
#rendered-md ol:first-child,
#rendered-md table:first-child,
#rendered-md blockquote:first-child,
#rendered-md img:first-child,
#rendered-md p:first-child {
margin-top: 0;
padding-top: 0;
}
p, h1, h2, h3, h4, h5, h6, ul, table {
margin-top: .6em;
margin-bottom: .65em;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.5em;
}
h1 {
font-size: 1.5em;
font-weight: bold;
border-bottom: 1px solid ` + style.htmlDividerColor + `;
padding-bottom: .3em;
}
h2 {
font-size: 1.3em;
font-weight: bold;
padding-bottom: .1em; */
}
h3 {
font-size: 1.1em;
}
h4, h5, h6 {
font-size: 1em;
font-weight: bold;
}
a {
color: ` + style.htmlLinkColor + `;
}
ul, ol {
padding-left: 0;
margin-left: 1.7em;
}
li {
margin-bottom: .4em;
}
li p {
margin-top: 0.2em;
margin-bottom: 0;
}
.resource-icon {
display: inline-block;
position: relative;
top: .5em;
text-decoration: none;
width: 1.15em;
height: 1.45em;
margin-right: 0.4em;
background-color: ` + style.htmlColor + `;
/* Awesome Font file */
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1536 1892' xmlns='http://www.w3.org/2000/svg'><path d='M288 128C129 128 0 257 0 416v960c0 159 129 288 288 288h960c159 0 288-129 288-288V416c0-159-129-288-288-288H288zm449.168 236.572l263.434.565 263.431.562.584 73.412.584 73.412-42.732 1.504c-23.708.835-47.002 2.774-52.322 4.36-14.497 4.318-23.722 12.902-29.563 27.51l-5.12 12.802-1.403 291.717c-1.425 295.661-1.626 302.586-9.936 343.043-15.2 74-69.604 150.014-142.197 198.685-58.287 39.08-121.487 60.47-208.155 70.45-22.999 2.648-122.228 2.636-141.976-.024l-.002.006c-69.785-9.377-108.469-20.202-154.848-43.332-85.682-42.73-151.778-116.991-177.537-199.469-10.247-32.81-11.407-40.853-11.375-78.754.026-31.257.76-39.15 5.024-54.043 8.94-31.228 20.912-51.733 43.56-74.62 27.312-27.6 55.812-40.022 95.524-41.633 37.997-1.542 63.274 5.024 87.23 22.66 15.263 11.235 30.828 33.238 39.537 55.884 5.52 14.355 5.949 18.31 7.549 69.569 1.675 53.648 3.05 63.99 11.674 87.785 11.777 32.499 31.771 55.017 61.46 69.22 26.835 12.838 47.272 16.785 80.56 15.56 21.646-.798 30.212-2.135 43.208-6.741 38.682-13.708 70.96-44.553 86.471-82.635 16.027-39.348 15.995-38.647 15.947-361.595-.042-283.26-.09-286.272-4.568-296.153-10.958-24.171-22.488-28.492-81.074-30.377l-42.969-1.38v-147.95z'/></svg>");
}
blockquote {
border-left: 4px solid ` + style.htmlCodeBorderColor + `;
padding-left: 1.2em;
margin-left: 0;
opacity: .7;
}
table {
text-align: left-align;
border-collapse: collapse;
border: 1px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlBackgroundColor + `;
}
td, th {
padding: .5em 1em .5em 1em;
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
font-family: ` + fontFamily + `;
}
td {
border: 1px solid ` + style.htmlCodeBorderColor + `;
}
th {
border: 1px solid ` + style.htmlCodeBorderColor + `;
border-bottom: 2px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlTableBackgroundColor + `;
}
tr:nth-child(even) {
background-color: ` + style.htmlTableBackgroundColor + `;
}
tr:hover {
background-color: ` + style.raisedBackgroundColor + `;
}
hr {
border: none;
border-bottom: 2px solid ` + style.htmlDividerColor + `;
}
img {
max-width: 100%;
height: auto;
}
.inline-code {
border: 1px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlCodeBackgroundColor + `;
padding-right: .2em;
padding-left: .2em;
border-radius: .25em;
color: ` + style.htmlCodeColor + `;
font-size: ` + style.htmlCodeFontSize + `;
}
.highlighted-keyword {
background-color: #F3B717;
color: black;
}
/*
This is to fix https://github.com/laurent22/joplin/issues/764
Without this, the tag attached to an equation float at an absolute position of the page,
instead of a position relative to the container.
*/
.katex-display>.katex>.katex-html {
position: relative;
}
a.checkbox {
border: 1pt solid ` + style.htmlColor + `;
border-radius: 2pt;
width: 1.1em;
height: 1.1em;
background-color: rgba(0,0,0,0);
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
top: -0.3em;
margin-right: 0.3em;
}
a.checkbox.tick:after {
content: "✓";
}
a.checkbox.tick {
color: ` + style.htmlColor + `;
}
@media print {
body {
height: auto !important;
}
pre {
white-space: pre-wrap;
}
.code, .inline-code {
border: 1px solid #CBCBCB;
return '<pre class="hljs"><code>' + hlCode + '</code></pre>';
} catch (error) {
return '<pre class="hljs"><code>' + markdownIt.utils.escapeHtml(str) + '</code></pre>';
}
}
`;
});
const ruleOptions = Object.assign({}, options, { resourceBaseUrl: this.resourceBaseUrl_ });
markdownIt.use(rules.image(context, ruleOptions));
markdownIt.use(rules.checkbox(context, ruleOptions));
markdownIt.use(rules.link_open(context, ruleOptions));
markdownIt.use(rules.html_block(context, ruleOptions));
markdownIt.use(rules.katex(context, ruleOptions));
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
setupLinkify(markdownIt);
const renderedBody = markdownIt.render(body);
const cssStrings = noteStyle(style, options);
for (let k in context.css) {
if (!context.css.hasOwnProperty(k)) continue;
cssStrings.push(context.css[k]);
}
for (let k in context.assetLoaders) {
if (!context.assetLoaders.hasOwnProperty(k)) continue;
context.assetLoaders[k]().catch(error => {
console.warn('MdToHtml: Error loading assets for ' + k + ': ', error.message);
});
}
let cssStrings = [normalizeCss, css];
if (this.extraCssBlocks_) cssStrings = cssStrings.concat(this.extraCssBlocks_);
if (options.userCss) cssStrings.push(options.userCss);
const styleHtml = '<style>' + cssStrings.join('\n') + '</style>';
const output = styleHtml + '<div id="rendered-md">' + renderedBody + '</div>';
const html = styleHtml + '<div id="rendered-md">' + renderedBody + '</div>';
// console.info('<!DOCTYPE html><html><head><meta charset="UTF-8">' + output + '</body></html>');
const output = {
html: html,
cssFiles: Object.keys(context.cssFiles).map(k => context.cssFiles[k]),
};
this.cachedContent_ = output;
this.cachedContentKey_ = cacheKey;
return this.cachedContent_;
}
// Fow now, we keep only the last entry in the cache
this.cachedOutputs_ = {};
this.cachedOutputs_[cacheKey] = output;
toggleTickAt(body, index) {
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
counter++;
body = body.replace(/- \[(X| |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;
return output;
}
injectedJavaScript() {
const output = [];
for (let i = 0; i < this.lastExecutedPlugins_.length; i++) {
const name = this.lastExecutedPlugins_[i];
const plugin = this.rendererPlugin_(name);
if (!plugin.injectedJavaScript) continue;
output.push(plugin.injectedJavaScript());
}
return output.join('\n');
}
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);
return '';
}
}

View File

@ -0,0 +1,221 @@
module.exports = function(style, options) {
// 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 fontFamily = "'Avenir', 'Arial', sans-serif";
const listMarginLeft = '1.7em';
const css = `
body {
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
line-height: ` + style.htmlLineHeight + `;
background-color: ` + style.htmlBackgroundColor + `;
font-family: ` + fontFamily + `;
padding-bottom: ` + options.paddingBottom + `;
}
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-corner {
background: none;
}
::-webkit-scrollbar-track {
border: none;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
}
::-webkit-scrollbar-track:hover {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
}
/* Remove top padding and margin from first child so that top of rendered text is aligned to top of text editor text */
#rendered-md h1:first-child,
#rendered-md h2:first-child,
#rendered-md h3:first-child,
#rendered-md h4:first-child,
#rendered-md ul:first-child,
#rendered-md ol:first-child,
#rendered-md table:first-child,
#rendered-md blockquote:first-child,
#rendered-md img:first-child,
#rendered-md p:first-child {
margin-top: 0;
padding-top: 0;
}
p, h1, h2, h3, h4, h5, h6, ul, table {
margin-top: .6em;
margin-bottom: .65em;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.5em;
}
h1 {
font-size: 1.5em;
font-weight: bold;
border-bottom: 1px solid ` + style.htmlDividerColor + `;
padding-bottom: .3em;
}
h2 {
font-size: 1.3em;
font-weight: bold;
padding-bottom: .1em; */
}
h3 {
font-size: 1.1em;
}
h4, h5, h6 {
font-size: 1em;
font-weight: bold;
}
a {
color: ` + style.htmlLinkColor + `;
}
ul, ol {
padding-left: 0;
margin-left: ` + listMarginLeft + `;
}
li {
margin-bottom: .4em;
}
li p {
margin-top: 0.2em;
margin-bottom: 0;
}
.resource-icon {
display: inline-block;
position: relative;
top: .5em;
text-decoration: none;
width: 1.15em;
height: 1.45em;
margin-right: 0.4em;
background-color: ` + style.htmlColor + `;
/* Awesome Font file */
-webkit-mask: url("data:image/svg+xml;utf8,<svg viewBox='0 0 1536 1892' xmlns='http://www.w3.org/2000/svg'><path d='M288 128C129 128 0 257 0 416v960c0 159 129 288 288 288h960c159 0 288-129 288-288V416c0-159-129-288-288-288H288zm449.168 236.572l263.434.565 263.431.562.584 73.412.584 73.412-42.732 1.504c-23.708.835-47.002 2.774-52.322 4.36-14.497 4.318-23.722 12.902-29.563 27.51l-5.12 12.802-1.403 291.717c-1.425 295.661-1.626 302.586-9.936 343.043-15.2 74-69.604 150.014-142.197 198.685-58.287 39.08-121.487 60.47-208.155 70.45-22.999 2.648-122.228 2.636-141.976-.024l-.002.006c-69.785-9.377-108.469-20.202-154.848-43.332-85.682-42.73-151.778-116.991-177.537-199.469-10.247-32.81-11.407-40.853-11.375-78.754.026-31.257.76-39.15 5.024-54.043 8.94-31.228 20.912-51.733 43.56-74.62 27.312-27.6 55.812-40.022 95.524-41.633 37.997-1.542 63.274 5.024 87.23 22.66 15.263 11.235 30.828 33.238 39.537 55.884 5.52 14.355 5.949 18.31 7.549 69.569 1.675 53.648 3.05 63.99 11.674 87.785 11.777 32.499 31.771 55.017 61.46 69.22 26.835 12.838 47.272 16.785 80.56 15.56 21.646-.798 30.212-2.135 43.208-6.741 38.682-13.708 70.96-44.553 86.471-82.635 16.027-39.348 15.995-38.647 15.947-361.595-.042-283.26-.09-286.272-4.568-296.153-10.958-24.171-22.488-28.492-81.074-30.377l-42.969-1.38v-147.95z'/></svg>");
}
blockquote {
border-left: 4px solid ` + style.htmlCodeBorderColor + `;
padding-left: 1.2em;
margin-left: 0;
opacity: .7;
}
table {
text-align: left-align;
border-collapse: collapse;
border: 1px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlBackgroundColor + `;
}
td, th {
padding: .5em 1em .5em 1em;
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
font-family: ` + fontFamily + `;
}
td {
border: 1px solid ` + style.htmlCodeBorderColor + `;
}
th {
border: 1px solid ` + style.htmlCodeBorderColor + `;
border-bottom: 2px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlTableBackgroundColor + `;
}
tr:nth-child(even) {
background-color: ` + style.htmlTableBackgroundColor + `;
}
tr:hover {
background-color: ` + style.raisedBackgroundColor + `;
}
hr {
border: none;
border-bottom: 2px solid ` + style.htmlDividerColor + `;
}
img {
max-width: 100%;
height: auto;
}
.inline-code {
border: 1px solid ` + style.htmlCodeBorderColor + `;
background-color: ` + style.htmlCodeBackgroundColor + `;
padding-right: .2em;
padding-left: .2em;
border-radius: .25em;
color: ` + style.htmlCodeColor + `;
font-size: ` + style.htmlCodeFontSize + `;
}
.highlighted-keyword {
background-color: #F3B717;
color: black;
}
/*
This is to fix https://github.com/laurent22/joplin/issues/764
Without this, the tag attached to an equation float at an absolute position of the page,
instead of a position relative to the container.
*/
.katex-display>.katex>.katex-html {
position: relative;
}
li.md-checkbox {
list-style-type: none;
margin-left: -` + listMarginLeft + `;
}
li.md-checkbox input[type=checkbox] {
margin-right: 1em;
}
a.checkbox {
border: 1pt solid ` + style.htmlColor + `;
border-radius: 2pt;
width: 1.1em;
height: 1.1em;
background-color: rgba(0,0,0,0);
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
top: -0.3em;
margin-right: 0.3em;
}
a.checkbox.tick:after {
content: "✓";
}
a.checkbox.tick {
color: ` + style.htmlColor + `;
}
@media print {
body {
height: auto !important;
}
pre {
white-space: pre-wrap;
}
.code, .inline-code {
border: 1px solid #CBCBCB;
}
}
`;
return [normalizeCss, css];
}

View File

@ -0,0 +1,105 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
let checkboxIndex_ = -1;
function createPrefixTokens(Token, id, checked, label, postMessageSyntax, sourceToken) {
let token = null;
const tokens = [];
// A bit hard to handle errors here and it's unlikely that the token won't have a valid
// map parameter, but if it does set it to a very high value, which will be more easy to notice
// in calling code.
const lineIndex = sourceToken.map && sourceToken.map.length ? sourceToken.map[0] : 99999999;
const checkedString = checked ? 'checked' : 'unchecked';
const js = postMessageSyntax + "('checkboxclick:" + checkedString + ':' + lineIndex + "'); return true;";
token = new Token('checkbox_input', 'input', 0);
token.attrs = [
['type', 'checkbox'],
['id', id],
['onclick', js],
];
if (checked) token.attrs.push(['checked', 'true']);
tokens.push(token);
token = new Token('label_open', 'label', 1);
token.attrs = [['for', id]];
tokens.push(token);
if (label) {
token = new Token('text', '', 0);
token.content = label;
tokens.push(token);
}
return tokens;
}
function createSuffixTokens(Token) {
return [new Token('label_close', 'label', -1)];
}
function installRule(markdownIt, mdOptions, ruleOptions) {
markdownIt.core.ruler.push('checkbox', state => {
const tokens = state.tokens;
const Token = state.Token;
const checkboxPattern = /^\[([x|X| ])\] (.*)$/
let currentListItem = null;
let processedFirstInline = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'list_item_open') {
currentListItem = token;
processedFirstInline = false;
continue;
}
if (token.type === 'list_item_close') {
currentListItem = null;
processedFirstInline = false;
continue;
}
if (currentListItem && !processedFirstInline && token.type === 'inline') {
processedFirstInline = true;
const firstChild = token.children && token.children.length ? token.children[0] : null;
if (!firstChild) continue;
const matches = checkboxPattern.exec(firstChild.content);
if (!matches || matches.length < 2) continue;
checkboxIndex_++;
const checked = matches[1] !== ' ';
const id = 'md-checkbox-' + checkboxIndex_;
const label = matches.length >= 3 ? matches[2] : '';
// Prepend the text content with the checkbox markup and the opening <label> tag
// then append the </label> tag at the end of the text content.
const prefix = createPrefixTokens(Token, id, checked, label, ruleOptions.postMessageSyntax, token);
const suffix = createSuffixTokens(Token);
token.children = markdownIt.utils.arrayReplaceAt(token.children, 0, prefix);
token.children = token.children.concat(suffix);
// Add a class to the <li> container so that it can be targetted with CSS.
let itemClass = currentListItem.attrGet('class');
if (!itemClass) itemClass = '';
itemClass += ' md-checkbox';
currentListItem.attrSet('class', itemClass.trim());
}
}
});
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -0,0 +1,71 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
const StringUtils = require('lib/string-utils.js');
const md5 = require('md5');
function createHighlightedTokens(Token, splitted) {
let token;
const output = [];
for (let i = 0; i < splitted.length; i++) {
const text = splitted[i];
if (!text) continue;
if (i % 2 === 0) {
token = new Token('text', '', 0);
token.content = text;
output.push(token);
} else {
token = new Token('highlighted_keyword_open', 'span', 1);
token.attrs = [['class', 'highlighted-keyword']];
output.push(token);
token = new Token('text', '', 0);
token.content = text;
output.push(token);
token = new Token('highlighted_keyword_close', 'span', -1);
output.push(token);
}
}
return output;
}
function installRule(markdownIt, mdOptions, ruleOptions) {
const divider = md5(Date.now().toString() + Math.random().toString());
markdownIt.core.ruler.push('highlight_keywords', state => {
const keywords = ruleOptions.highlightedKeywords;
if (!keywords || !keywords.length) return;
const tokens = state.tokens;
const Token = state.Token;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type !== 'inline') continue;
for (let j = 0; j < token.children.length; j++) {
const child = token.children[j];
if (child.type !== 'text') continue;
const splitted = StringUtils.surroundKeywords(keywords, child.content, divider, divider).split(divider);
const splittedTokens = createHighlightedTokens(Token, splitted);
if (splittedTokens.length <= 1) continue;
token.children = markdownIt.utils.arrayReplaceAt(token.children, j, splittedTokens);
j += splittedTokens.length - 1;
}
}
});
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -0,0 +1,45 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
function renderImageHtml(before, src, after, ruleOptions) {
const resourceId = Resource.urlToId(src);
const resource = ruleOptions.resources[resourceId];
if (!resource) return '<div>' + utils.loaderImage() + '</div>';
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let newSrc = './' + Resource.filename(resource);
if (ruleOptions.resourceBaseUrl) newSrc = ruleOptions.resourceBaseUrl + newSrc;
return '<img ' + before + ' data-resource-id="' + resource.id + '" src="' + newSrc + '" ' + after + '/>';
}
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
}
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.html_block || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
const imageRegex = /<img(.*?)src=["'](.*?)["'](.*?)\/>/
markdownIt.renderer.rules.html_block = function(tokens, idx, options, env, self) {
const token = tokens[idx];
const content = token.content;
if (!content.match(imageRegex)) return defaultRender(tokens, idx, options, env, self);
return content.replace(imageRegex, (v, before, src, after) => {
if (!Resource.isResourceUrl(src)) return defaultRender(tokens, idx, options, env, self);
return renderImageHtml(before, src, after, ruleOptions);
});
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -0,0 +1,39 @@
// This rule is no longer needed because HTML anchors (as opposed to those generated from Markdown)
// are handled in webviewLib. Keeping it here for reference.
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.html_block || function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
const anchorRegex = /<a (.*)>/
markdownIt.renderer.rules.html_inline = function(tokens, idx, options, env, self) {
const token = tokens[idx];
const content = token.content;
if (!content.match(anchorRegex)) return defaultRender(tokens, idx, options, env, self);
return content.replace(anchorRegex, (v, content) => {
let js = `
var href = this.getAttribute('href');
if (!href || href.indexOf('http') < 0) return true;
` + ruleOptions.postMessageSyntax + `(href);
return false;
`;
js = js.split('\n').join(' ').replace(/\t/g, '');
return '<a onclick="' + js + '" ' + content + '>';
});
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -0,0 +1,36 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.image;
markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx];
const src = utils.getAttr(token.attrs, 'src');
const title = utils.getAttr(token.attrs, 'title');
if (!Resource.isResourceUrl(src)) return defaultRender(tokens, idx, options, env, self);
const resourceId = Resource.urlToId(src);
const resource = ruleOptions.resources[resourceId];
if (!resource) return '<div>' + utils.loaderImage() + '</div>';
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let realSrc = './' + Resource.filename(resource);
if (ruleOptions.resourceBaseUrl) realSrc = ruleOptions.resourceBaseUrl + realSrc;
let output = '<img data-from-md data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + realSrc + '"/>';
return output;
}
return defaultRender(tokens, idx, options, env, self);
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -0,0 +1,244 @@
// Based on https://github.com/waylonflinn/markdown-it-katex
'use strict';
const { shim } = require('lib/shim');
const Setting = require('lib/models/Setting');
var katex = require('katex');
const katexCss = require('lib/csstojs/katex.css.js');
const md5 = require('md5');
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
function isValidDelim(state, pos) {
var prevChar, nextChar,
max = state.posMax,
can_open = true,
can_close = true;
prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;
nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;
// Check non-whitespace conditions for opening and closing, and
// check that closing delimeter isn't followed by a number
if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
(nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
can_close = false;
}
if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
can_open = false;
}
return {
can_open: can_open,
can_close: can_close
};
}
function math_inline(state, silent) {
var start, match, token, res, pos, esc_count;
if (state.src[state.pos] !== "$") { return false; }
res = isValidDelim(state, state.pos);
if (!res.can_open) {
if (!silent) { state.pending += "$"; }
state.pos += 1;
return true;
}
// First check for and bypass all properly escaped delimieters
// This loop will assume that the first leading backtick can not
// be the first character in state.src, which is known since
// we have found an opening delimieter already.
start = state.pos + 1;
match = start;
while ( (match = state.src.indexOf("$", match)) !== -1) {
// Found potential $, look for escapes, pos will point to
// first non escape when complete
pos = match - 1;
while (state.src[pos] === "\\") { pos -= 1; }
// Even number of escapes, potential closing delimiter found
if ( ((match - pos) % 2) == 1 ) { break; }
match += 1;
}
// No closing delimter found. Consume $ and continue.
if (match === -1) {
if (!silent) { state.pending += "$"; }
state.pos = start;
return true;
}
// Check if we have empty content, ie: $$. Do not parse.
if (match - start === 0) {
if (!silent) { state.pending += "$$"; }
state.pos = start + 1;
return true;
}
// Check for valid closing delimiter
res = isValidDelim(state, match);
if (!res.can_close) {
if (!silent) { state.pending += "$"; }
state.pos = start;
return true;
}
if (!silent) {
token = state.push('math_inline', 'math', 0);
token.markup = "$";
token.content = state.src.slice(start, match);
}
state.pos = match + 1;
return true;
}
function math_block(state, start, end, silent){
var firstLine, lastLine, next, lastPos, found = false, token,
pos = state.bMarks[start] + state.tShift[start],
max = state.eMarks[start]
if(pos + 2 > max){ return false; }
if(state.src.slice(pos,pos+2)!=='$$'){ return false; }
pos += 2;
firstLine = state.src.slice(pos,max);
if(silent){ return true; }
if(firstLine.trim().slice(-2)==='$$'){
// Single line expression
firstLine = firstLine.trim().slice(0, -2);
found = true;
}
for(next = start; !found; ){
next++;
if(next >= end){ break; }
pos = state.bMarks[next]+state.tShift[next];
max = state.eMarks[next];
if(pos < max && state.tShift[next] < state.blkIndent){
// non-empty line with negative indent should stop the list:
break;
}
if(state.src.slice(pos,max).trim().slice(-2)==='$$'){
lastPos = state.src.slice(0,max).lastIndexOf('$$');
lastLine = state.src.slice(pos,lastPos);
found = true;
}
}
state.line = next + 1;
token = state.push('math_block', 'math', 0);
token.block = true;
token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '')
+ state.getLines(start + 1, next, state.tShift[start], true)
+ (lastLine && lastLine.trim() ? lastLine : '');
token.map = [ start, state.line ];
token.markup = '$$';
return true;
}
let assetsLoaded_ = false;
let cache_ = {};
module.exports = function(context, ruleOptions) {
// Keep macros that persist across Katex blocks to allow defining a macro
// in one block and re-using it later in other blocks.
// https://github.com/laurent22/joplin/issues/1105
context.__katex = { macros: {} };
const addContextAssets = () => {
context.css['katex'] = katexCss;
context.assetLoaders['katex'] = async () => {
if (assetsLoaded_) return;
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
// them as an asset and copy them from there (or load them from there by modifying Katex CSS), but for now that will do.
if (shim.isReactNative()) {
// Fonts must go under the resourceDir directory because this is the baseUrl of NoteBodyViewer
const baseDir = Setting.value('resourceDir');
await shim.fsDriver().mkdir(baseDir + '/fonts');
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Main-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Main-Regular.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
}
assetsLoaded_ = true;
};
}
function renderToStringWithCache(latex, options) {
const cacheKey = md5(escape(latex) + escape(JSON.stringify(options)));
if (cacheKey in cache_) {
return cache_[cacheKey];
} else {
const beforeMacros = JSON.stringify(options.macros);
const output = katex.renderToString(latex, options);
const afterMacros = JSON.stringify(options.macros);
// Don't cache the formulas that add macros, otherwise
// they won't be added on second run.
if (beforeMacros === afterMacros) cache_[cacheKey] = output;
return output;
}
}
return function(md, options) {
// Default options
options = options || {};
options.macros = context.__katex.macros;
// set KaTeX as the renderer for markdown-it-simplemath
var katexInline = function(latex){
options.displayMode = false;
try{
return renderToStringWithCache(latex, options);
} catch(error){
if(options.throwOnError){ console.log(error); }
return latex;
}
};
var inlineRenderer = function(tokens, idx){
addContextAssets();
return katexInline(tokens[idx].content);
};
var katexBlock = function(latex){
options.displayMode = true;
try{
return "<p>" + renderToStringWithCache(latex, options) + "</p>";
} catch(error){
if(options.throwOnError){ console.log(error); }
return latex;
}
}
var blockRenderer = function(tokens, idx){
addContextAssets();
return katexBlock(tokens[idx].content) + '\n';
}
md.inline.ruler.after('escape', 'math_inline', math_inline);
md.block.ruler.after('blockquote', 'math_block', math_block, {
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
};

View File

@ -0,0 +1,45 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
const loaderImage = '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="16px" height="16px" viewBox="0 0 128 128" xml:space="preserve"><g><circle cx="16" cy="64" r="16" fill="#000000" fill-opacity="1"/><circle cx="16" cy="64" r="16" fill="#555555" fill-opacity="0.67" transform="rotate(45,64,64)"/><circle cx="16" cy="64" r="16" fill="#949494" fill-opacity="0.42" transform="rotate(90,64,64)"/><circle cx="16" cy="64" r="16" fill="#cccccc" fill-opacity="0.2" transform="rotate(135,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(180,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(225,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(270,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(315,64,64)"/><animateTransform attributeName="transform" type="rotate" values="0 64 64;315 64 64;270 64 64;225 64 64;180 64 64;135 64 64;90 64 64;45 64 64" calcMode="discrete" dur="720ms" repeatCount="indefinite"></animateTransform></g></svg>';
function installRule(markdownIt, mdOptions, ruleOptions) {
markdownIt.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const token = tokens[idx];
let href = utils.getAttr(token.attrs, 'href');
const text = utils.getAttr(token.attrs, 'text');
const isResourceUrl = Resource.isResourceUrl(href);
const title = isResourceUrl ? utils.getAttr(token.attrs, 'title') : href;
console.info(href, isResourceUrl);
let resourceIdAttr = "";
let icon = "";
let hrefAttr = '#';
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} else {
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
let js = ruleOptions.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
if (js) hrefAttr = '#';
let output = "<a data-from-md " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
console.info(output);
return output;
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -0,0 +1,29 @@
module.exports = function(markdownIt) {
// Add `file:` protocol in linkify to allow text in the format of "file://..." to translate into
// file-URL links in html view
markdownIt.linkify.add('file:', {
validate: function (text, pos, self) {
var tail = text.slice(pos);
if (!self.re.file) {
// matches all local file URI on Win/Unix/MacOS systems including reserved characters in some OS (i.e. no OS specific sanity check)
self.re.file = new RegExp('^[\\/]{2,3}[\\S]+');
}
if (self.re.file.test(tail)) {
return tail.match(self.re.file)[0].length;
}
return 0;
}
});
// enable file link URLs in MarkdownIt. Keeps other URL restrictions of MarkdownIt untouched.
// Format [link name](file://...)
markdownIt.validateLink = function (url) {
var BAD_PROTO_RE = /^(vbscript|javascript|data):/;
var GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/;
// url should be normalized at this point, and existing entities are decoded
var str = url.trim().toLowerCase();
return BAD_PROTO_RE.test(str) ? (GOOD_DATA_RE.test(str) ? true : false) : true;
}
}

View File

@ -0,0 +1,14 @@
module.exports = {
getAttr: function(attrs, name, defaultValue = null) {
for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
}
return defaultValue;
},
loaderImage: function() {
return '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="16px" height="16px" viewBox="0 0 128 128" xml:space="preserve"><g><circle cx="16" cy="64" r="16" fill="#000000" fill-opacity="1"/><circle cx="16" cy="64" r="16" fill="#555555" fill-opacity="0.67" transform="rotate(45,64,64)"/><circle cx="16" cy="64" r="16" fill="#949494" fill-opacity="0.42" transform="rotate(90,64,64)"/><circle cx="16" cy="64" r="16" fill="#cccccc" fill-opacity="0.2" transform="rotate(135,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(180,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(225,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(270,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(315,64,64)"/><animateTransform attributeName="transform" type="rotate" values="0 64 64;315 64 64;270 64 64;225 64 64;180 64 64;135 64 64;90 64 64;45 64 64" calcMode="discrete" dur="720ms" repeatCount="indefinite"></animateTransform></g></svg>';
},
};

View File

@ -0,0 +1,54 @@
const webviewLib = {};
webviewLib.handleInternalLink = function(event, anchorNode) {
const href = anchorNode.getAttribute('href');
if (href.indexOf('#') === 0) {
event.preventDefault();
location.hash = href;
return true;
}
return false;
}
webviewLib.getParentAnchorElement = function(element) {
let counter = 0;
while (true) {
if (counter++ >= 10000) {
console.warn('been looping for too long - exiting')
return null;
}
if (!element) return null;
if (element.nodeName === 'A') return element;
element = element.parentElement;
}
}
webviewLib.initialize = function(options) {
webviewLib.options_ = options;
}
document.addEventListener('click', function(event) {
const anchor = webviewLib.getParentAnchorElement(event.target);
if (!anchor) return;
// Prevent URLs added via <a> tags from being opened within the application itself
// otherwise it would open the whole website within the WebView.
// Note that we already handle some links in html_inline.js, however not all of them
// go through this plugin, in particular links coming from third-party packages such
// as Katex.
if (!anchor.hasAttribute('data-from-md')) {
if (webviewLib.handleInternalLink(event, anchor)) return;
event.preventDefault();
webviewLib.options_.postMessage(anchor.getAttribute('href'));
return;
}
// If this is an internal link, jump to the anchor directly
if (anchor.hasAttribute('data-from-md')) {
if (webviewLib.handleInternalLink(event, anchor)) return;
}
});

View File

@ -1,75 +0,0 @@
const { shim } = require('lib/shim');
const katex = require('katex');
const katexCss = require('lib/csstojs/katex.css.js');
const Setting = require('lib/models/Setting');
const MdToHtml_PluginBase = require('./MdToHtml_PluginBase');
class MdToHtml_Katex extends MdToHtml_PluginBase {
constructor() {
super();
this.cache_ = {};
this.assetsLoaded_ = false;
// Keep macros that persist across Katex blocks to allow defining a macro
// in one block and re-using it later in other blocks.
// https://github.com/laurent22/joplin/issues/1105
this.macros_ = {};
}
name() {
return 'katex';
}
processContent(renderedTokens, content, tagType) {
try {
const cacheKey = tagType + '_' + content;
let renderered = null;
if (this.cache_[cacheKey]) {
renderered = this.cache_[cacheKey];
} else {
renderered = katex.renderToString(content, {
displayMode: tagType === 'block',
macros: this.macros_,
});
this.cache_[cacheKey] = renderered;
}
if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
renderedTokens.push(renderered);
} catch (error) {
renderedTokens.push('Cannot render Katex content: ' + error.message);
}
return renderedTokens;
}
extraCss() {
return katexCss;
}
async loadAssets() {
if (this.assetsLoaded_) return;
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
// them as an asset and copy them from there (or load them from there by modifying Katex CSS), but for now that will do.
if (shim.isReactNative()) {
// Fonts must go under the resourceDir directory because this is the baseUrl of NoteBodyViewer
const baseDir = Setting.value('resourceDir');
await shim.fsDriver().mkdir(baseDir + '/fonts');
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Main-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Main-Regular.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
}
this.assetsLoaded_ = true;
}
}
module.exports = MdToHtml_Katex;

View File

@ -1,36 +0,0 @@
const { shim } = require('lib/shim');
const MdToHtml_PluginBase = require('./MdToHtml_PluginBase');
class MdToHtml_Mermaid extends MdToHtml_PluginBase {
constructor() {
super();
this.setEnabled(false);
}
name() {
return 'mermaid';
}
processContent(renderedTokens, content, tagType) {
renderedTokens.push('<div class="mermaid">' + content + '</div>');
return renderedTokens;
}
extraCss() {
// Force a white background because the graphs can have various colours
// that may not be compatible with the current theme. Also make it
// inline-block so that the div is the same size as the content.
return '.mermaid { width: 100%; } .mermaid svg { background-color: white; }';
}
injectedJavaScript() {
const js = shim.injectedJs('mermaid');
return js ? js + '\n' + 'mermaid.init();' : '';
}
async loadAssets() {}
}
module.exports = MdToHtml_Mermaid;

View File

@ -1,21 +0,0 @@
class MdToHtml_PluginBase {
constructor() {
this.enabled_ = true;
}
setEnabled(v) {
this.enabled_ = v;
}
enabled() {
return this.enabled_;
}
disabled() {
return !this.enabled();
}
}
module.exports = MdToHtml_PluginBase;

View File

@ -36,6 +36,8 @@ const globalStyle = {
htmlCodeBackgroundColor: 'rgb(243, 243, 243)',
htmlCodeBorderColor: 'rgb(220, 220, 220)',
htmlCodeColor: 'rgb(0,0,0)',
codeThemeCss: 'hljs-atom-one-light.css',
};
globalStyle.marginRight = globalStyle.margin;
@ -118,6 +120,8 @@ function themeStyle(theme) {
output.htmlCodeBackgroundColor = 'rgb(47, 48, 49)';
output.htmlCodeBorderColor = 'rgb(70, 70, 70)';
output.codeThemeCss = 'hljs-atom-one-dark-reasonable.css';
output.colorUrl = '#7B81FF';
themeCache_[theme] = output;

View File

@ -1,10 +1,12 @@
const React = require('react'); const Component = React.Component;
const { Platform, WebView, View } = require('react-native');
const { globalStyle } = require('lib/components/global-style.js');
const { themeStyle } = require('lib/components/global-style.js');
const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js');
const { reg } = require('lib/registry.js');
const { shim } = require('lib/shim');
const MdToHtml = require('lib/MdToHtml.js');
const shared = require('lib/components/shared/note-screen-shared.js');
class NoteBodyViewer extends Component {
@ -69,7 +71,7 @@ class NoteBodyViewer extends Component {
}
rebuildMd() {
this.mdToHtml_.clearCache();
// this.mdToHtml_.clearCache();
this.forceUpdate();
}
@ -77,6 +79,9 @@ class NoteBodyViewer extends Component {
const note = this.props.note;
const style = this.props.style;
const onCheckboxChange = this.props.onCheckboxChange;
const theme = themeStyle(this.props.theme);
const bodyToRender = note ? note.body : '';
const mdOptions = {
onResourceLoaded: () => {
@ -92,11 +97,18 @@ class NoteBodyViewer extends Component {
},
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
highlightedKeywords: this.props.highlightedKeywords,
resources: this.props.noteResources,//await shared.attachedResources(bodyToRender),
codeTheme: theme.codeThemeCss,
};
let html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
let result = this.mdToHtml_.render(bodyToRender, this.props.webViewStyle, mdOptions);
let html = result.html;
const injectedJs = this.mdToHtml_.injectedJavaScript();
const injectedJs = [this.mdToHtml_.injectedJavaScript()];
injectedJs.push(shim.injectedJs('webviewLib'));
injectedJs.push('webviewLib.initialize({ postMessage: postMessage });');
console.info(injectedJs);
html = `
<!DOCTYPE html>
@ -148,7 +160,7 @@ class NoteBodyViewer extends Component {
scalesPageToFit={Platform.OS !== 'ios'}
style={webViewStyle}
source={source}
injectedJavaScript={injectedJs}
injectedJavaScript={injectedJs.join('\n')}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode="always"
allowFileAccess={true}
@ -158,8 +170,8 @@ class NoteBodyViewer extends Component {
let msg = event.nativeEvent.data;
if (msg.indexOf('checkboxclick:') === 0) {
const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.props.note.body);
if (onCheckboxChange) onCheckboxChange(newBody);
const newBody = shared.toggleCheckbox(msg, this.props.note.body);
if (this.props.onCheckboxChange) this.props.onCheckboxChange(newBody);
} else if (msg.indexOf('bodyscroll:') === 0) {
//msg = msg.split(':');
//this.bodyScrollTop_ = Number(msg[1]);

View File

@ -63,6 +63,7 @@ class NoteScreenComponent extends BaseScreenComponent {
noteTagDialogShown: false,
fromShare: false,
showCamera: false,
noteResources: {},
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
@ -167,7 +168,10 @@ class NoteScreenComponent extends BaseScreenComponent {
if (!this.state.note || !this.state.note.body) return;
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
if (resourceIds.indexOf(resource.id) >= 0 && this.refs.noteBodyViewer) {
this.refs.noteBodyViewer.rebuildMd();
const attachedResources = await shared.attachedResources(this.state.note.body);
this.setState({ noteResources: attachedResources }, () => {
this.refs.noteBodyViewer.rebuildMd();
});
}
}
@ -612,7 +616,9 @@ class NoteScreenComponent extends BaseScreenComponent {
style={this.styles().noteBodyViewer}
webViewStyle={theme}
note={note}
noteResources={this.state.noteResources}
highlightedKeywords={keywords}
theme={this.props.theme}
onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}
onLoadEnd={() => {
setTimeout(() => {

View File

@ -2,6 +2,7 @@ const { reg } = require('lib/registry.js');
const Folder = require('lib/models/Folder.js');
const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js');
const Mutex = require('async-mutex').Mutex;
@ -79,6 +80,8 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
comp.setState(newState);
// await shared.refreshAttachedResources(comp, newState.note.body);
if (isNew) {
Note.updateGeolocation(note.id).then((geoNote) => {
const stateNote = comp.state.note;
@ -151,6 +154,29 @@ shared.noteComponent_change = function(comp, propName, propValue) {
comp.setState(newState);
}
const resourceCache_ = {};
shared.attachedResources = async function(noteBody) {
if (!noteBody) return {};
const resourceIds = await Note.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, noteBody);
const output = {};
for (let i = 0; i < resourceIds.length; i++) {
const id = resourceIds[i];
if (resourceCache_[id]) {
output[id] = resourceCache_[id];
} else {
const resource = await Resource.load(id);
const isReady = await Resource.isReady(resource);
if (!isReady) continue;
resourceCache_[id] = resource;
output[id] = resource;
}
}
return output;
}
shared.refreshNoteMetadata = async function(comp, force = null) {
if (force !== true && !comp.state.showNoteMetadata) return;
@ -184,6 +210,7 @@ shared.initState = async function(comp) {
folder: folder,
isLoading: false,
fromShare: comp.props.sharedData ? true : false,
noteResources: await shared.attachedResources(note ? note.body : ''),
});
if (comp.props.sharedData) {
@ -204,4 +231,28 @@ shared.toggleIsTodo_onPress = function(comp) {
comp.setState(newState);
}
shared.toggleCheckbox = function(ipcMessage, noteBody) {
let newBody = noteBody.split('\n');
const p = ipcMessage.split(':');
const lineIndex = Number(p[p.length - 1]);
if (lineIndex >= newBody.length) {
reg.logger().warn('Checkbox line out of bounds: ', ipcMessage, args);
return newBody.join('\n');
}
let line = newBody[lineIndex];
if (line.trim().indexOf('- [ ] ') === 0) {
line = line.replace(/- \[ \] /, '- [x] ');
} else if (line.trim().indexOf('- [x] ') === 0 || line.trim().indexOf('- [X] ') === 0) {
line = line.replace(/- \[x\] /i, '- [ ] ');
} else {
reg.logger().warn('Could not find matching checkbox for message: ', ipcMessage, args);
return newBody.join('\n');
}
newBody[lineIndex] = line;
return newBody.join('\n')
}
module.exports = shared;

View File

@ -42,6 +42,10 @@ class Resource extends BaseItem {
return r ? r['total'] : 0;
}
static async resetStartedFetchStatus() {
return await this.db().exec('UPDATE resource_local_states SET fetch_status = ? WHERE fetch_status = ?', [Resource.FETCH_STATUS_IDLE, Resource.FETCH_STATUS_STARTED]);
}
static fsDriver() {
if (!Resource.fsDriver_) Resource.fsDriver_ = new FsDriverDummy();
return Resource.fsDriver_;

View File

@ -27,10 +27,6 @@ class ResourceLocalState extends BaseModel {
return result;
}
static resetStartedFetchStatus() {
return this.db().exec('UPDATE resource_local_states SET fetch_status = ? WHERE fetch_status = ?', [Resource.FETCH_STATUS_IDLE, Resource.FETCH_STATUS_STARTED]);
}
static async save(o) {
const queries = [
{ sql: 'DELETE FROM resource_local_states WHERE resource_id = ?', params: [o.resource_id] },

View File

@ -6,6 +6,7 @@ const EventEmitter = require('events');
const { splitCommandString } = require('lib/string-utils');
const { fileExtension } = require('lib/path-utils');
const spawn = require('child_process').spawn;
const chokidar = require('chokidar');
class ExternalEditWatcher {
@ -15,6 +16,7 @@ class ExternalEditWatcher {
this.watcher_ = null;
this.eventEmitter_ = new EventEmitter();
this.skipNextChangeEvent_ = {};
this.chokidar_ = chokidar;
}
static instance() {
@ -39,14 +41,16 @@ class ExternalEditWatcher {
return this.logger_;
}
async preload() {
// Chokidar is extremely slow to load since Electron 4 - it takes over 4 seconds
// on my computer. So load it in the background.
setTimeout(() => {
if (this.chokidar_) return;
this.chokidar_ = require('chokidar');
}, 1000);
}
// async preload() {
// // Chokidar is extremely slow to load since Electron 4 - it takes over 4 seconds
// // on my computer. So load it in the background.
// setTimeout(() => {
// if (this.chokidar_) return;
// const startTime = Date.now();
// this.chokidar_ = require('chokidar');
// console.info('Chokidar load time:', Date.now() - startTime);
// }, 1000);
// }
watch(fileToWatch) {
if (!this.chokidar_) return;

View File

@ -0,0 +1,38 @@
const BaseItem = require('lib/models/BaseItem');
class ModelCache {
constructor() {
this.cache_ = {};
}
async byIds(itemType, ids) {
const ModelClass = BaseItem.getClassByItemType(itemType);
const output = [];
const remainingIds = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (!this.cache_[id]) {
remainingIds.push(id);
} else {
output.push(this.cache_[id].model);
}
}
const models = await ModelClass.byIds(remainingIds);
for (let i = 0; i < models.length; i++) {
this.cache_[models[i].id] = {
model: models[i],
timestamp: Date.now(),
}
output.push(models[i]);
}
return output;
}
}
module.exports = ModelCache;

View File

@ -1,5 +1,4 @@
const Resource = require('lib/models/Resource');
const ResourceLocalState = require('lib/models/ResourceLocalState');
const BaseService = require('lib/services/BaseService');
const BaseSyncTarget = require('lib/BaseSyncTarget');
const { Logger } = require('lib/logger.js');
@ -176,7 +175,7 @@ class ResourceFetcher extends BaseService {
}
async start() {
await ResourceLocalState.resetStartedFetchStatus();
await Resource.resetStartedFetchStatus();
this.autoAddResources(10);
}
@ -193,7 +192,7 @@ class ResourceFetcher extends BaseService {
}
async fetchAll() {
await ResourceLocalState.resetStartedFetchStatus();
await Resource.resetStartedFetchStatus();
this.autoAddResources(null);
}

View File

@ -13,7 +13,12 @@ const { uuid } = require('lib/uuid.js');
const Resource = require('lib/models/Resource');
const injectedJs = {
mermaid: require('lib/rnInjectedJs/mermaid'),
webviewLib: require('lib/rnInjectedJs/webviewLib'),
};
const cssToJs = {
'hljs-atom-one-dark-reasonable.css': require('lib/csstojs/hljs-atom-one-dark-reasonable.css.js'),
'hljs-atom-one-light.css': require('lib/csstojs/hljs-atom-one-light.css.js'),
};
function shimInit() {
@ -169,8 +174,13 @@ function shimInit() {
}
shim.injectedJs = function(name) {
if (name in injectedJs) return injectedJs[name];
return '';
if (!(name in injectedJs)) throw new Error('Cannot find injectedJs file (add it to "injectedJs" object): ' + name);
return injectedJs[name];
}
shim.loadCssFromJs = function(name) {
if (!(name in cssToJs)) throw new Error('Cannot find csstojs file (add it to "cssToJs" object): ' + name);
return cssToJs[name];
}
}

View File

@ -150,5 +150,6 @@ shim.Buffer = null;
shim.openUrl = () => { throw new Error('Not implemented'); }
shim.waitForFrame = () => { throw new Error('Not implemented'); }
shim.injectedJs = name => '';
shim.loadCssFromJs = name => { throw new Error('Not implemented'); }
module.exports = { shim };

View File

@ -6,14 +6,6 @@
// So there's basically still a one way flux: React => SQLite => Redux => React
// To disable warnings internal to React Native for componentWillMount and
// componentWillReceiveProps. Should be fixed at some point and at that
// time this code could be removed.
// https://github.com/facebook/react-native/issues/18165#issuecomment-369907978
// require("ReactFeatureFlags").warnAboutDeprecatedLifecycles = false;
// console.disableYellowBox = true
const { AppRegistry } = require('react-native');

View File

@ -3297,6 +3297,11 @@
}
}
},
"highlight.js": {
"version": "9.15.6",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz",
"integrity": "sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ=="
},
"hoist-non-react-statics": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",

View File

@ -15,6 +15,7 @@
"diacritics": "^1.3.0",
"events": "^1.1.1",
"form-data": "^2.1.4",
"highlight.js": "^9.15.6",
"html-entities": "^1.2.1",
"katex": "^0.10.0",
"markdown-it": "^8.4.0",

View File

@ -7,6 +7,7 @@ const fs = require('fs-extra');
const cwd = process.cwd();
const outputDir = cwd + '/lib/rnInjectedJs';
const rnDir = __dirname + '/../ReactNativeClient';
async function copyJs(name, filePath) {
const js = await fs.readFile(filePath, 'utf-8');
@ -17,7 +18,7 @@ async function copyJs(name, filePath) {
async function main(argv) {
await fs.mkdirp(outputDir);
await copyJs('mermaid', __dirname + '/node_modules/mermaid/dist/mermaid.min.js');
await copyJs('webviewLib', rnDir + '/lib/MdToHtml/webviewLib.js');
}
main(process.argv).catch((error) => {

View File

@ -15,6 +15,8 @@ async function createJsFromCss(name, filePath) {
async function main(argv) {
await fs.mkdirp(outputDir);
await createJsFromCss('katex', cwd + '/node_modules/katex/dist/katex.min.css');
await createJsFromCss('hljs-atom-one-light', cwd + '/node_modules/highlight.js/styles/atom-one-light.css');
await createJsFromCss('hljs-atom-one-dark-reasonable', cwd + '/node_modules/highlight.js/styles/atom-one-dark-reasonable.css');
if (argv.indexOf('--copy-fonts') >= 0) {
await fs.copy(cwd + '/node_modules/katex/dist/fonts', cwd + '/gui/note-viewer/fonts');

View File

@ -14,7 +14,6 @@
"gettext-parser": "^1.3.0",
"markdown-it": "^8.4.1",
"marked": "^0.3.7",
"mermaid": "^8.0.0",
"mustache": "^2.3.0",
"node-fetch": "^1.7.3",
"request": "^2.85.0",

View File

@ -52,16 +52,6 @@ f(x) = \int_{-\infty}^\infty
\,d\xi
$$
And graphs can be added using the [Mermaid syntax](https://mermaidjs.github.io/):
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
Various other tricks are possible, such as using HTML, or customising the CSS. See the Markdown documentation for more info - https://joplin.cozic.net#markdown
## Community and further help