2019-09-23 23:18:30 +02:00
const stringToStream = require ( 'string-to-stream' ) ;
2020-10-21 13:02:06 +02:00
// const cleanHtml = require('clean-html');
2020-11-05 18:58:23 +02:00
const resourceUtils = require ( './resourceUtils.js' ) ;
2022-12-27 17:17:42 +02:00
const { cssValue } = require ( './import-enex-md-gen' ) ;
2021-01-30 13:08:11 +02:00
const htmlUtils = require ( './htmlUtils' ) . default ;
2020-06-15 18:10:51 +02:00
const Entities = require ( 'html-entities' ) . AllHtmlEntities ;
const htmlentities = new Entities ( ) . encode ;
2019-09-23 23:18:30 +02:00
function addResourceTag ( lines , resource , attributes ) {
// Note: refactor to use Resource.markdownTag
if ( ! attributes . alt ) attributes . alt = resource . title ;
if ( ! attributes . alt ) attributes . alt = resource . filename ;
if ( ! attributes . alt ) attributes . alt = '' ;
const src = ` :/ ${ resource . id } ` ;
if ( resourceUtils . isImageMimeType ( resource . mime ) ) {
2020-02-05 00:09:34 +02:00
lines . push ( resourceUtils . imgElement ( { src , attributes } ) ) ;
2019-09-23 23:18:30 +02:00
} else if ( resource . mime === 'audio/x-m4a' ) {
2022-07-23 09:31:32 +02:00
// TODO: once https://github.com/laurent22/joplin/issues/1794 is resolved,
// come back to this and make sure it works.
2019-09-23 23:18:30 +02:00
lines . push ( resourceUtils . audioElement ( {
src ,
alt : attributes . alt ,
id : resource . id ,
} ) ) ;
} else {
// TODO: figure out what other mime types can be handled more gracefully
lines . push ( resourceUtils . attachmentElement ( {
src ,
attributes ,
id : resource . id ,
} ) ) ;
}
return lines ;
}
function attributeToLowerCase ( node ) {
if ( ! node . attributes ) return { } ;
2020-03-14 01:46:14 +02:00
const output = { } ;
for ( const n in node . attributes ) {
2019-09-23 23:18:30 +02:00
if ( ! node . attributes . hasOwnProperty ( n ) ) continue ;
output [ n . toLowerCase ( ) ] = node . attributes [ n ] ;
}
return output ;
}
function enexXmlToHtml _ ( stream , resources ) {
2020-03-14 01:46:14 +02:00
const remainingResources = resources . slice ( ) ;
2019-09-23 23:18:30 +02:00
2020-05-21 10:14:33 +02:00
const removeRemainingResource = id => {
2019-09-23 23:18:30 +02:00
for ( let i = 0 ; i < remainingResources . length ; i ++ ) {
const r = remainingResources [ i ] ;
if ( r . id === id ) {
remainingResources . splice ( i , 1 ) ;
}
}
} ;
2020-06-15 18:10:51 +02:00
return new Promise ( ( resolve ) => {
2019-09-23 23:18:30 +02:00
const options = { } ;
const strict = false ;
2020-11-07 17:59:37 +02:00
const saxStream = require ( '@joplin/fork-sax' ) . createStream ( strict , options ) ;
2019-09-23 23:18:30 +02:00
2020-03-14 01:46:14 +02:00
const section = {
2019-09-23 23:18:30 +02:00
type : 'text' ,
lines : [ ] ,
parent : null ,
} ;
2023-02-20 17:02:29 +02:00
saxStream . on ( 'error' , ( e ) => {
2019-09-23 23:18:30 +02:00
console . warn ( e ) ;
} ) ;
2023-02-20 17:02:29 +02:00
saxStream . on ( 'text' , ( text ) => {
2020-06-15 18:10:51 +02:00
section . lines . push ( htmlentities ( text ) ) ;
2019-09-23 23:18:30 +02:00
} ) ;
saxStream . on ( 'opentag' , function ( node ) {
const tagName = node . name . toLowerCase ( ) ;
const attributesStr = resourceUtils . attributesToStr ( node . attributes ) ;
2022-12-27 17:17:42 +02:00
const nodeAttributes = attributeToLowerCase ( node ) ;
2019-09-23 23:18:30 +02:00
if ( tagName === 'en-media' ) {
const nodeAttributes = attributeToLowerCase ( node ) ;
const hash = nodeAttributes . hash ;
let resource = null ;
for ( let i = 0 ; i < resources . length ; i ++ ) {
2020-03-14 01:46:14 +02:00
const r = resources [ i ] ;
2022-07-23 09:31:32 +02:00
if ( r . id === hash ) {
2019-09-23 23:18:30 +02:00
resource = r ;
removeRemainingResource ( r . id ) ;
break ;
}
}
if ( ! resource ) {
// TODO: Extract this duplicate of code in ./import-enex-md-gen.js
let found = false ;
for ( let i = 0 ; i < remainingResources . length ; i ++ ) {
2020-03-14 01:46:14 +02:00
const r = remainingResources [ i ] ;
2019-09-23 23:18:30 +02:00
if ( ! r . id ) {
2023-06-01 13:02:36 +02:00
resource = { ... r } ;
2019-09-23 23:18:30 +02:00
resource . id = hash ;
remainingResources . splice ( i , 1 ) ;
found = true ;
break ;
}
}
if ( ! found ) {
2020-06-15 18:10:51 +02:00
// console.warn(`Hash with no associated resource: ${hash}`);
2019-09-23 23:18:30 +02:00
}
}
// If the resource does not appear among the note's resources, it
// means it's an attachement. It will be appended along with the
// other remaining resources at the bottom of the markdown text.
if ( resource && ! ! resource . id ) {
section . lines = addResourceTag ( section . lines , resource , nodeAttributes ) ;
}
2022-07-23 09:31:32 +02:00
} else if ( tagName === 'en-todo' ) {
const checkedHtml = nodeAttributes . checked && nodeAttributes . checked . toLowerCase ( ) === 'true' ? ' checked="checked" ' : ' ' ;
2020-06-28 19:55:47 +02:00
section . lines . push ( ` <input ${ checkedHtml } type="checkbox" onclick="return false;" /> ` ) ;
2022-12-27 17:17:42 +02:00
} else if ( tagName === 'li' && cssValue ( this , nodeAttributes . style , '--en-checked' ) ) {
const checkedHtml = cssValue ( this , nodeAttributes . style , '--en-checked' ) === 'true' ? ' checked="checked" ' : ' ' ;
section . lines . push ( ` < ${ tagName } ${ attributesStr } > <input ${ checkedHtml } type="checkbox" onclick="return false;" /> ` ) ;
2021-01-30 13:08:11 +02:00
} else if ( htmlUtils . isSelfClosingTag ( tagName ) ) {
2020-06-15 18:10:51 +02:00
section . lines . push ( ` < ${ tagName } ${ attributesStr } /> ` ) ;
2019-09-23 23:18:30 +02:00
} else {
2020-06-15 18:10:51 +02:00
section . lines . push ( ` < ${ tagName } ${ attributesStr } > ` ) ;
2019-09-23 23:18:30 +02:00
}
} ) ;
2023-02-20 17:02:29 +02:00
saxStream . on ( 'closetag' , ( node ) => {
2020-06-15 18:10:51 +02:00
const tagName = node ? node . toLowerCase ( ) : node ;
2021-01-30 13:08:11 +02:00
if ( ! htmlUtils . isSelfClosingTag ( tagName ) ) section . lines . push ( ` </ ${ tagName } > ` ) ;
2019-09-23 23:18:30 +02:00
} ) ;
2023-02-20 17:02:29 +02:00
saxStream . on ( 'attribute' , ( ) => { } ) ;
2019-09-23 23:18:30 +02:00
2023-02-20 17:02:29 +02:00
saxStream . on ( 'end' , ( ) => {
2019-09-23 23:18:30 +02:00
resolve ( {
content : section ,
resources : remainingResources ,
} ) ;
} ) ;
stream . pipe ( saxStream ) ;
} ) ;
}
async function enexXmlToHtml ( xmlString , resources , options = { } ) {
const stream = stringToStream ( xmlString ) ;
2020-03-14 01:46:14 +02:00
const result = await enexXmlToHtml _ ( stream , resources , options ) ;
2019-09-23 23:18:30 +02:00
2020-06-15 18:10:51 +02:00
const preCleaning = result . content . lines . join ( '' ) ;
const final = await beautifyHtml ( preCleaning ) ;
return final . join ( '' ) ;
2019-09-23 23:18:30 +02:00
}
const beautifyHtml = ( html ) => {
2020-10-21 13:02:06 +02:00
// The clean-html package doesn't appear to be robust enough to deal with the crazy HTML that Evernote can generate.
// In the best case scenario it will throw an error but in some cases it will go into an infinite loop, so
// for that reason we need to disable it.
//
// Fixed https://github.com/laurent22/joplin/issues/3958
return [ html ] ;
// return new Promise((resolve) => {
// try {
// cleanHtml.clean(html, { wrap: 0 }, (...cleanedHtml) => resolve(cleanedHtml));
// } catch (error) {
// console.warn(`Could not clean HTML - the "unclean" version will be used: ${error.message}: ${html.trim().substr(0, 512).replace(/[\n\r]/g, ' ')}...`);
// resolve([html]);
// }
// });
2019-09-23 23:18:30 +02:00
} ;
2020-02-05 00:09:34 +02:00
module . exports = { enexXmlToHtml } ;