1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Refactor to re-use md rendering code in Electron app

This commit is contained in:
Laurent Cozic 2017-11-05 15:35:38 +00:00
parent cc5bd12ba1
commit 09db5ff1d8
6 changed files with 204 additions and 175 deletions

View File

@ -32,12 +32,8 @@ class NoteListComponent extends React.Component {
}
const mapStateToProps = (state) => {
//let notes = [];
//for (let i = 0; i < 100; i++) notes.push({ title: "Note " + i });
return {
notes: state.notes,
//notes: notes,
};
};

View File

@ -65,7 +65,7 @@ class ReactRootComponent extends React.Component {
return (
<div style={style}>
<NoteList itemHeight={40} style={noteListStyle}></NoteList>
{/*<NoteText style={noteTextStyle}></NoteText>*/}
<NoteText style={noteTextStyle}></NoteText>
</div>
);
}

View File

@ -7,16 +7,19 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
for JSX_FILE in "$BUILD_DIR"/gui/*.jsx; do
JS_FILE="${JSX_FILE::-4}.min.js"
"$ROOT_DIR/app/node_modules/.bin/babel" --presets react "$JSX_FILE" > "$JS_FILE"
if [[ $? != 0 ]]; then
exit 1
if [ $JSX_FILE -nt $JS_FILE ]; then
echo "Compile $JS_FILE..."
"$ROOT_DIR/app/node_modules/.bin/babel" --presets react "$JSX_FILE" > "$JS_FILE"
if [[ $? != 0 ]]; then
exit 1
fi
fi
done
TRANSLATION_BUILD_SCRIPT="$ROOT_DIR/../CliClient/build/build-translation.js"
if [[ ! -f $TRANSLATION_BUILD_SCRIPT ]]; then
echo "Build the CLI app first ($TRANSLATION_BUILD_SCRIPT missing)"
exit 1
fi
# TRANSLATION_BUILD_SCRIPT="$ROOT_DIR/../CliClient/build/build-translation.js"
# if [[ ! -f $TRANSLATION_BUILD_SCRIPT ]]; then
# echo "Build the CLI app first ($TRANSLATION_BUILD_SCRIPT missing)"
# exit 1
# fi
node "$TRANSLATION_BUILD_SCRIPT" --silent
# node "$TRANSLATION_BUILD_SCRIPT" --silent

View File

@ -3,4 +3,4 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$ROOT_DIR"
./build.sh || exit 1
cd "$ROOT_DIR/app"
node_modules/.bin/electron . --env dev --log-level debug "$@"
./node_modules/.bin/electron . --env dev --log-level debug "$@"

View File

@ -2,11 +2,8 @@ const React = require('react'); const Component = React.Component;
const { WebView, View, Linking } = require('react-native');
const { globalStyle } = require('lib/components/global-style.js');
const { Resource } = require('lib/models/resource.js');
const { shim } = require('lib/shim.js');
const { reg } = require('lib/registry.js');
const marked = require('lib/marked.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const { markdownUtils, MdToHtml } = require('lib/markdown-utils.js');
class NoteBodyViewer extends Component {
@ -20,162 +17,16 @@ class NoteBodyViewer extends Component {
this.isMounted_ = false;
}
async loadResource(id) {
const resource = await Resource.load(id);
resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource));
let newResources = Object.assign({}, this.state.resources);
newResources[id] = resource;
this.setState({ resources: newResources });
}
componentWillMount() {
this.mdToHtml_ = new MdToHtml();
this.isMounted_ = true;
}
componentWillUnmount() {
this.mdToHtml_ = null;
this.isMounted_ = false;
}
toggleTickAt(body, index) {
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
counter++;
body = body.replace(/- \[(X| )\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
if (index == counter) {
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';
}
return '°°JOP°CHECKBOX°' + s + '°°';
});
}
body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]');
body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]');
return body;
}
markdownToHtml(body, style) {
// https://necolas.github.io/normalize.css/
const normalizeCss = `
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
`;
const css = `
body {
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
line-height: 1.5em;
background-color: ` + style.htmlBackgroundColor + `;
}
h1 {
font-size: 1.2em;
font-weight: bold;
}
h2 {
font-size: 1em;
font-weight: bold;
}
a {
color: ` + style.htmlLinkColor + `
}
ul {
padding-left: 1em;
}
a.checkbox {
font-size: 1.6em;
position: relative;
top: 0.1em;
text-decoration: none;
color: ` + style.htmlColor + `;
}
table {
border-collapse: collapse;
}
td, th {
border: 1px solid silver;
padding: .5em 1em .5em 1em;
}
hr {
border: 1px solid ` + style.htmlDividerColor + `;
}
img {
width: 100%;
}
`;
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
body = body.replace(/- \[(X| )\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
counter++;
return '°°JOP°CHECKBOX°' + s + '°' + counter + '°°';
});
}
const renderer = new marked.Renderer();
renderer.link = function (href, title, text) {
if (Resource.isResourceUrl(href)) {
return '[Resource not yet supported: ' + htmlentities(text) + ']';
} else {
const js = "postMessage(" + JSON.stringify(href) + "); return false;";
let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
return output;
}
}
renderer.image = (href, title, text) => {
if (!Resource.isResourceUrl(href)) {
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
}
const resourceId = Resource.urlToId(href);
if (!this.state.resources[resourceId]) {
this.loadResource(resourceId);
return '';
}
const r = this.state.resources[resourceId];
const mime = r.mime.toLowerCase();
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
const src = 'data:' + r.mime + ';base64,' + r.base64;
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
return output;
}
return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
}
let styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
let html = body ? styleHtml + marked(body, {
gfm: true,
breaks: true,
renderer: renderer,
sanitize: true,
}) : styleHtml;
let elementId = 1;
while (html.indexOf('°°JOP°') >= 0) {
html = html.replace(/°°JOP°CHECKBOX°([A-Z]+)°(\d+)°°/, function(v, type, index) {
const js = "postMessage('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
return '<a href="#" onclick="' + js + '" class="checkbox">' + (type == 'NOTICK' ? '☐' : '☑') + '</a>';
});
}
let scriptHtml = '<script>document.body.scrollTop = ' + this.bodyScrollTop_ + ';</script>';
html = '<body onscroll="postMessage(\'bodyscroll:\' + document.body.scrollTop);">' + html + scriptHtml + '</body>';
return html;
}
onLoadEnd() {
if (this.state.webViewLoaded) return;
@ -191,7 +42,14 @@ class NoteBodyViewer extends Component {
const note = this.props.note;
const style = this.props.style;
const onCheckboxChange = this.props.onCheckboxChange;
const html = this.markdownToHtml(note ? note.body : '', this.props.webViewStyle);
const mdOptions = {
onResourceLoaded: function() {
this.forceUpdated();
},
};
const html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
let webViewStyle = {}
webViewStyle.opacity = this.state.webViewLoaded ? 1 : 0.01;
@ -207,14 +65,11 @@ class NoteBodyViewer extends Component {
let msg = event.nativeEvent.data;
if (msg.indexOf('checkboxclick:') === 0) {
msg = msg.split(':');
let index = Number(msg[msg.length - 1]);
let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway
const newBody = this.toggleTickAt(note.body, index);
const newBody = this.mdToHtml_.handleCheckboxClick(msg, note.body);
if (onCheckboxChange) onCheckboxChange(newBody);
} else if (msg.indexOf('bodyscroll:') === 0) {
msg = msg.split(':');
this.bodyScrollTop_ = Number(msg[1]);
//msg = msg.split(':');
//this.bodyScrollTop_ = Number(msg[1]);
} else {
Linking.openURL(msg);
}

View File

@ -1,3 +1,178 @@
const marked = require('lib/marked.js');
class MdToHtml {
constructor() {
this.loadedResources_ = [];
}
render(body, style, options = null) {
if (!options) options = {};
const { Resource } = require('lib/models/resource.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const { shim } = require('lib/shim.js');
const loadResource = async function(id) {
const resource = await Resource.load(id);
resource.base64 = await shim.readLocalFileBase64(Resource.fullPath(resource));
let newResources = Object.assign({}, this.loadedResources_);
newResources[id] = resource;
this.loadedResources_ = newResources;
if (options.onResourceLoaded) options.onResourceLoaded();
}
// https://necolas.github.io/normalize.css/
const normalizeCss = `
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}
pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
`;
const css = `
body {
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
line-height: 1.5em;
background-color: ` + style.htmlBackgroundColor + `;
}
h1 {
font-size: 1.2em;
font-weight: bold;
}
h2 {
font-size: 1em;
font-weight: bold;
}
a {
color: ` + style.htmlLinkColor + `
}
ul {
padding-left: 1em;
}
a.checkbox {
font-size: 1.6em;
position: relative;
top: 0.1em;
text-decoration: none;
color: ` + style.htmlColor + `;
}
table {
border-collapse: collapse;
}
td, th {
border: 1px solid silver;
padding: .5em 1em .5em 1em;
}
hr {
border: 1px solid ` + style.htmlDividerColor + `;
}
img {
width: 100%;
}
`;
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
body = body.replace(/- \[(X| )\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
counter++;
return '°°JOP°CHECKBOX°' + s + '°' + counter + '°°';
});
}
const renderer = new marked.Renderer();
renderer.link = function (href, title, text) {
if (Resource.isResourceUrl(href)) {
return '[Resource not yet supported: ' + htmlentities(text) + ']';
} else {
const js = "postMessage(" + JSON.stringify(href) + "); return false;";
let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
return output;
}
}
renderer.image = (href, title, text) => {
if (!Resource.isResourceUrl(href)) {
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
}
const resourceId = Resource.urlToId(href);
if (!this.loadedResources_[resourceId]) {
this.loadResource(resourceId);
return '';
}
const r = this.loadedResources_[resourceId];
const mime = r.mime.toLowerCase();
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
const src = 'data:' + r.mime + ';base64,' + r.base64;
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
return output;
}
return '[Image: ' + htmlentities(r.title) + ' (' + htmlentities(mime) + ')]';
}
let styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
let html = body ? styleHtml + marked(body, {
gfm: true,
breaks: true,
renderer: renderer,
sanitize: true,
}) : styleHtml;
let elementId = 1;
while (html.indexOf('°°JOP°') >= 0) {
html = html.replace(/°°JOP°CHECKBOX°([A-Z]+)°(\d+)°°/, function(v, type, index) {
const js = "postMessage('checkboxclick:" + type + ':' + index + "'); this.textContent = this.textContent == '☐' ? '☑' : '☐'; return false;";
return '<a href="#" onclick="' + js + '" class="checkbox">' + (type == 'NOTICK' ? '☐' : '☑') + '</a>';
});
}
//let scriptHtml = '<script>document.body.scrollTop = ' + this.bodyScrollTop_ + ';</script>';
let scriptHtml = '';
html = '<body onscroll="postMessage(\'bodyscroll:\' + document.body.scrollTop);">' + html + scriptHtml + '</body>';
return html;
}
toggleTickAt(body, index) {
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
counter++;
body = body.replace(/- \[(X| )\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
if (index == counter) {
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';
}
return '°°JOP°CHECKBOX°' + s + '°°';
});
}
body = body.replace(/°°JOP°CHECKBOX°NOTICK°°/g, '- [ ]');
body = body.replace(/°°JOP°CHECKBOX°TICK°°/g, '- [X]');
return body;
}
handleCheckboxClick(msg, noteBody) {
msg = msg.split(':');
let index = Number(msg[msg.length - 1]);
let currentState = msg[msg.length - 2]; // Not really needed but keep it anyway
return this.toggleTickAt(noteBody, index);
}
}
const markdownUtils = {
// Not really escaping because that's not supported by marked.js
@ -13,4 +188,4 @@ const markdownUtils = {
};
module.exports = { markdownUtils };
module.exports = { markdownUtils, MdToHtml };