mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Improving markdown renderer
This commit is contained in:
parent
26d9b57923
commit
5024dcf668
5
CliClient/package-lock.json
generated
5
CliClient/package-lock.json
generated
@ -490,6 +490,11 @@
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
|
||||
"integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
|
||||
},
|
||||
"html-entities": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
|
||||
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
|
@ -30,6 +30,7 @@
|
||||
"follow-redirects": "^1.2.4",
|
||||
"form-data": "^2.1.4",
|
||||
"fs-extra": "^3.0.1",
|
||||
"html-entities": "^1.2.1",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
"lodash": "^4.17.4",
|
||||
|
@ -2,6 +2,6 @@
|
||||
set -e
|
||||
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
bash $CLIENT_DIR/build.sh && node build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
||||
bash $CLIENT_DIR/build.sh && node $CLIENT_DIR/build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
||||
|
||||
#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
@ -19,6 +19,8 @@ class NoteTextComponent extends React.Component {
|
||||
isLoading: true,
|
||||
webviewReady: false,
|
||||
};
|
||||
|
||||
this.lastLoadedNoteId_ = null;
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
@ -36,8 +38,15 @@ class NoteTextComponent extends React.Component {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.noteId) this.reloadNote(nextProps.noteId);
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
if ('noteId' in nextProps) {
|
||||
const noteId = nextProps.noteId;
|
||||
this.lastLoadedNoteId_ = noteId;
|
||||
const note = noteId ? await Note.load(noteId) : null;
|
||||
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
|
||||
|
||||
this.setState({ note: note });
|
||||
}
|
||||
}
|
||||
|
||||
isModified() {
|
||||
@ -77,7 +86,7 @@ class NoteTextComponent extends React.Component {
|
||||
webviewReady: true,
|
||||
});
|
||||
|
||||
// this.webview_.openDevTools();
|
||||
this.webview_.openDevTools();
|
||||
|
||||
this.webview_.addEventListener('ipc-message', (event) => {
|
||||
const msg = event.channel;
|
||||
@ -89,17 +98,8 @@ class NoteTextComponent extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
async reloadNote(noteId) {
|
||||
const note = noteId ? await Note.load(noteId) : null;
|
||||
|
||||
this.setState({
|
||||
note: note,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const note = this.state.note;
|
||||
const body = note ? note.body : 'no note';
|
||||
|
||||
if (this.state.webviewReady) {
|
||||
const mdOptions = {
|
||||
@ -132,7 +132,7 @@ class NoteTextComponent extends React.Component {
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
noteId: state.selectedNoteId,
|
||||
notes: state.notes,
|
||||
//notes: state.notes,
|
||||
folderId: state.selectedFolderId,
|
||||
itemType: state.selectedItemType,
|
||||
folders: state.folders,
|
||||
|
39
ElectronClient/app/package-lock.json
generated
39
ElectronClient/app/package-lock.json
generated
@ -158,7 +158,6 @@
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
|
||||
"integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"sprintf-js": "1.0.3"
|
||||
},
|
||||
@ -166,8 +165,7 @@
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"dev": true
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1524,6 +1522,11 @@
|
||||
"once": "1.4.0"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz",
|
||||
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA="
|
||||
},
|
||||
"env-paths": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz",
|
||||
@ -2471,6 +2474,14 @@
|
||||
"invert-kv": "1.0.0"
|
||||
}
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
|
||||
"integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
|
||||
"requires": {
|
||||
"uc.micro": "1.0.3"
|
||||
}
|
||||
},
|
||||
"load-json-file": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||
@ -2609,11 +2620,28 @@
|
||||
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.0.tgz",
|
||||
"integrity": "sha512-tNuOCCfunY5v5uhcO2AUMArvKAyKMygX8tfup/JrgnsDqcCATQsAExBq7o5Ml9iMmO82bk6jYNLj6khcrl0JGA==",
|
||||
"requires": {
|
||||
"argparse": "1.0.9",
|
||||
"entities": "1.1.1",
|
||||
"linkify-it": "2.0.3",
|
||||
"mdurl": "1.0.1",
|
||||
"uc.micro": "1.0.3"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz",
|
||||
"integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc="
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||
},
|
||||
"mem": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
|
||||
@ -4824,6 +4852,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
|
||||
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g=="
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.3.tgz",
|
||||
"integrity": "sha1-ftUNXg+an7ClczeSWfKndFjVAZI="
|
||||
},
|
||||
"unique-string": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
|
||||
|
@ -29,6 +29,7 @@
|
||||
"fs-extra": "^4.0.2",
|
||||
"html-entities": "^1.2.1",
|
||||
"lodash": "^4.17.4",
|
||||
"markdown-it": "^8.4.0",
|
||||
"marked": "^0.3.6",
|
||||
"moment": "^2.19.1",
|
||||
"node-fetch": "^1.7.3",
|
||||
|
@ -107,6 +107,8 @@ shared.initState = async function(comp) {
|
||||
folder: folder,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
comp.lastLoadedNoteId_ = note ? note.id : null;
|
||||
}
|
||||
|
||||
shared.showMetadata_onPress = function(comp) {
|
||||
|
@ -1,4 +1,6 @@
|
||||
const marked = require('lib/marked.js');
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = (new Entities()).encode;
|
||||
|
||||
class MdToHtml {
|
||||
|
||||
@ -6,7 +8,268 @@ class MdToHtml {
|
||||
this.loadedResources_ = [];
|
||||
}
|
||||
|
||||
renderAttrs_(attrs) {
|
||||
if (!attrs) return '';
|
||||
|
||||
let output = [];
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
const n = attrs[i][0];
|
||||
const v = attrs[i].length >= 2 ? attrs[i][1] : null;
|
||||
|
||||
if (n === 'alt' && !v) {
|
||||
continue;
|
||||
} else if (n === 'src') {
|
||||
output.push('src="' + htmlentities(v) + '"');
|
||||
} else {
|
||||
output.push(n + '="' + (v ? htmlentities(v) : '') + '"');
|
||||
}
|
||||
}
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
getAttr_(attrs, name) {
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setAttr_(attrs, name, value) {
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
if (attrs[i][0] === name) {
|
||||
attrs[i][1] = value;
|
||||
return attrs;
|
||||
}
|
||||
}
|
||||
attrs.push([name, value]);
|
||||
return attrs;
|
||||
}
|
||||
|
||||
renderImage_(attrs, options) {
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
const loadResource = async (id) => {
|
||||
const resource = await Resource.load(id);
|
||||
resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource));
|
||||
|
||||
let newResources = Object.assign({}, this.loadedResources_);
|
||||
newResources[id] = resource;
|
||||
this.loadedResources_ = newResources;
|
||||
|
||||
if (options.onResourceLoaded) options.onResourceLoaded();
|
||||
}
|
||||
|
||||
const title = this.getAttr_(attrs, 'title');
|
||||
const href = this.getAttr_(attrs, 'src');
|
||||
|
||||
if (!Resource.isResourceUrl(href)) {
|
||||
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
||||
}
|
||||
|
||||
const resourceId = Resource.urlToId(href);
|
||||
if (!this.loadedResources_[resourceId]) {
|
||||
loadResource(resourceId);
|
||||
return '';
|
||||
}
|
||||
|
||||
const r = this.loadedResources_[resourceId];
|
||||
const mime = r.mime.toLowerCase();
|
||||
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
|
||||
const src = 'data:' + r.mime + ';base64,' + r.base64;
|
||||
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
|
||||
return output;
|
||||
}
|
||||
|
||||
return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
|
||||
}
|
||||
|
||||
renderLink_(attrs, options) {
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
|
||||
const href = this.getAttr_(attrs, 'href');
|
||||
const title = this.getAttr_(attrs, 'title');
|
||||
const text = this.getAttr_(attrs, 'text');
|
||||
|
||||
// TODO:
|
||||
|
||||
// if (Resource.isResourceUrl(href)) {
|
||||
// return '[Resource not yet supported: ' + htmlentities(text) + ']';
|
||||
// } else {
|
||||
// const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
||||
// let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
|
||||
// return output;
|
||||
// }
|
||||
}
|
||||
|
||||
renderTokens_(tokens, options) {
|
||||
let output = [];
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const t = tokens[i];
|
||||
|
||||
let tag = t.tag;
|
||||
let openTag = null;
|
||||
let closeTag = null;
|
||||
let attrs = t.attrs ? t.attrs : [];
|
||||
|
||||
if (t.map) attrs.push(['data-map', t.map.join(':')]);
|
||||
|
||||
if (tag && t.type.indexOf('_open') >= 0) {
|
||||
openTag = tag;
|
||||
} else if (tag && t.type.indexOf('_close') >= 0) {
|
||||
closeTag = tag;
|
||||
} else if (tag && t.type.indexOf('inline') >= 0) {
|
||||
openTag = tag;
|
||||
} else if (t.type === 'link_open') {
|
||||
openTag = 'a';
|
||||
}
|
||||
|
||||
if (openTag) {
|
||||
const attrsHtml = attrs ? this.renderAttrs_(attrs) : '';
|
||||
output.push('<' + openTag + (attrsHtml ? ' ' + attrsHtml : '') + '>');
|
||||
}
|
||||
|
||||
if (t.type === 'image') {
|
||||
if (t.content) attrs.push(['title', t.content]);
|
||||
output.push(this.renderImage_(attrs, options));
|
||||
} else {
|
||||
if (t.children) {
|
||||
const parsedChildren = this.renderTokens_(t.children, options);
|
||||
output = output.concat(parsedChildren);
|
||||
} else {
|
||||
if (t.content) {
|
||||
output.push(htmlentities(t.content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (t.type === 'link_close') {
|
||||
closeTag = 'a';
|
||||
} else if (tag && t.type.indexOf('inline') >= 0) {
|
||||
closeTag = openTag;
|
||||
}
|
||||
|
||||
if (closeTag) {
|
||||
output.push('</' + closeTag + '>');
|
||||
}
|
||||
}
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
renderIt(body, style, options = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
||||
|
||||
const MarkdownIt = require('markdown-it');
|
||||
const md = new MarkdownIt();
|
||||
const env = {};
|
||||
|
||||
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
|
||||
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
|
||||
// that it can be removed and replaced later on.
|
||||
const HORRIBLE_HACK = true;
|
||||
|
||||
if (HORRIBLE_HACK) {
|
||||
let counter = -1;
|
||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
||||
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
||||
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
||||
counter++;
|
||||
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const tokens = md.parse(body, env);
|
||||
|
||||
console.info(body);
|
||||
console.info(tokens);
|
||||
|
||||
let renderedBody = this.renderTokens_(tokens, options);
|
||||
|
||||
if (HORRIBLE_HACK) {
|
||||
let loopCount = 0;
|
||||
while (renderedBody.indexOf('mJOPm') >= 0) {
|
||||
renderedBody = renderedBody.replace(/mJOPmCHECKBOXm([A-Z]+)m(\d+)m/, function(v, type, index) {
|
||||
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
|
||||
return '<a href="#" onclick="' + js + '" class="checkbox">' + (type == 'NOTICK' ? '☐' : '☑') + '</a>';
|
||||
});
|
||||
if (loopCount++ >= 9999) break;
|
||||
}
|
||||
}
|
||||
|
||||
// https://necolas.github.io/normalize.css/
|
||||
const normalizeCss = `
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
|
||||
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
||||
`;
|
||||
|
||||
const css = `
|
||||
body {
|
||||
font-size: ` + style.htmlFontSize + `;
|
||||
color: ` + style.htmlColor + `;
|
||||
line-height: 1.5em;
|
||||
background-color: ` + style.htmlBackgroundColor + `;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
a {
|
||||
color: ` + style.htmlLinkColor + `
|
||||
}
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
a.checkbox {
|
||||
font-size: 1.6em;
|
||||
position: relative;
|
||||
top: 0.1em;
|
||||
text-decoration: none;
|
||||
color: ` + style.htmlColor + `;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
td, th {
|
||||
border: 1px solid silver;
|
||||
padding: .5em 1em .5em 1em;
|
||||
}
|
||||
hr {
|
||||
border: 1px solid ` + style.htmlDividerColor + `;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
|
||||
|
||||
return styleHtml + renderedBody;
|
||||
|
||||
|
||||
//return md.render(body);
|
||||
// var result = md.render('# markdown-it rulezz!');
|
||||
|
||||
// // node.js, the same, but with sugar:
|
||||
// var md = require('markdown-it')();
|
||||
// var result = md.render('# markdown-it rulezz!');
|
||||
|
||||
// // browser without AMD, added to "window" on script load
|
||||
// // Note, there is no dash in "markdownit".
|
||||
// var md = window.markdownit();
|
||||
// var result = md.render('# markdown-it rulezz!');
|
||||
}
|
||||
|
||||
render(body, style, options = null) {
|
||||
return this.renderIt(body, style, options);
|
||||
|
||||
if (!options) options = {};
|
||||
|
||||
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
||||
@ -14,8 +277,8 @@ class MdToHtml {
|
||||
// ipcRenderer.sendToHost('pong')
|
||||
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = (new Entities()).encode;
|
||||
// const Entities = require('html-entities').AllHtmlEntities;
|
||||
// const htmlentities = (new Entities()).encode;
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
const loadResource = async (id) => {
|
||||
@ -132,7 +395,6 @@ class MdToHtml {
|
||||
sanitize: true,
|
||||
}) : styleHtml;
|
||||
|
||||
let elementId = 1;
|
||||
while (html.indexOf('°°JOP°') >= 0) {
|
||||
html = html.replace(/°°JOP°CHECKBOX°([A-Z]+)°(\d+)°°/, function(v, type, index) {
|
||||
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
|
||||
|
Loading…
Reference in New Issue
Block a user