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

All: Resolves #8684: Apply correct size to images imported from ENEX files

This commit is contained in:
Laurent Cozic 2023-08-25 15:13:36 +01:00
parent a14674aaa8
commit dcd3def942
4 changed files with 492 additions and 37 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
![](:/RESOURCE_ID_1)
#### Last Transfer
<img src=":/RESOURCE_ID_2" width="65" height="65" alt="bank.svg"/>
##### **Next Day Bank Deposit / USD**
###### **March 5, 2023 04:28AM**
* * *
Processing
**Confirmation**: ILbwHO5Z06p7meW
![](https://joplinapp.org/images/logo-text.svg)
<img src="https://joplinapp.org/images/logo-text.svg" width="100" height="50"/>

View File

@ -1,7 +1,7 @@
import { NoteEntity, ResourceEntity, TagEntity } from './services/database/types';
import shim from './shim';
const fs = require('fs-extra');
import { readFile, stat } from 'fs/promises';
const os = require('os');
const { filename } = require('./path-utils');
import { setupDatabaseAndSynchronizer, switchClient, expectNotThrow, supportDir, expectThrow } from './testing/test-utils';
@ -13,6 +13,16 @@ import Resource from './models/Resource';
const enexSampleBaseDir = `${supportDir}/../enex_to_md`;
const importEnexFile = async (filename: string) => {
const filePath = `${enexSampleBaseDir}/${filename}`;
await importEnex('', filePath);
};
const readExpectedFile = async (filename: string) => {
const filePath = `${enexSampleBaseDir}/${filename}`;
return readFile(filePath, 'utf8');
};
describe('import-enex-md-gen', () => {
beforeEach(async () => {
@ -65,8 +75,7 @@ describe('import-enex-md-gen', () => {
});
it('should import ENEX metadata', async () => {
const filePath = `${enexSampleBaseDir}/sample-enex.xml`;
await importEnex('', filePath);
await importEnexFile('sample-enex.xml');
const note: NoteEntity = (await Note.all())[0];
expect(note.title).toBe('Test Note for Export');
@ -87,37 +96,33 @@ describe('import-enex-md-gen', () => {
const resource: ResourceEntity = (await Resource.all())[0];
expect(resource.id).toBe('3d0f4d01abc02cf8c4dc1c796df8c4b2');
const stat = await fs.stat(Resource.fullPath(resource));
expect(stat.size).toBe(277);
const s = await stat(Resource.fullPath(resource));
expect(s.size).toBe(277);
});
it('should handle invalid dates', async () => {
const filePath = `${enexSampleBaseDir}/invalid_date.enex`;
await importEnex('', filePath);
await importEnexFile('invalid_date.enex');
const note: NoteEntity = (await Note.all())[0];
expect(note.created_time).toBe(1521822724000); // 20180323T163204Z
expect(note.updated_time).toBe(1521822724000); // Because this date was invalid, it is set to the created time instead
});
it('should handle empty resources', async () => {
const filePath = `${enexSampleBaseDir}/empty_resource.enex`;
await expectNotThrow(() => importEnex('', filePath));
await expectNotThrow(() => importEnexFile('empty_resource.enex'));
const all = await Resource.all();
expect(all.length).toBe(1);
expect(all[0].size).toBe(0);
});
it('should handle tasks', async () => {
const filePath = `${enexSampleBaseDir}/tasks.enex`;
await importEnex('', filePath);
await importEnexFile('tasks.enex');
const expectedMd = await shim.fsDriver().readFile(`${enexSampleBaseDir}/tasks.md`);
const note: NoteEntity = (await Note.all())[0];
expect(note.body).toEqual(expectedMd);
});
it('should handle empty note content', async () => {
const filePath = `${enexSampleBaseDir}/empty_content.enex`;
await expectNotThrow(() => importEnex('', filePath));
await importEnexFile('empty_content.enex');
const all = await Note.all();
expect(all.length).toBe(1);
expect(all[0].title).toBe('China and the case for stimulus.');
@ -131,8 +136,7 @@ describe('import-enex-md-gen', () => {
// type "application/octet-stream", which can later cause problems to
// open the file.
// https://discourse.joplinapp.org/t/importing-a-note-with-a-zip-file/12123?u=laurent
const filePath = `${enexSampleBaseDir}/WithInvalidMime.enex`;
await importEnex('', filePath);
await importEnexFile('WithInvalidMime.enex');
const all = await Resource.all();
expect(all.length).toBe(1);
expect(all[0].mime).toBe('application/zip');
@ -154,8 +158,26 @@ describe('import-enex-md-gen', () => {
});
it('should throw an error and stop if the outer XML is invalid', async () => {
const filePath = `${enexSampleBaseDir}/invalid_html.enex`;
await expectThrow(async () => importEnex('', filePath));
await expectThrow(async () => importEnexFile('invalid_html.enex'));
});
it('should import images with sizes', async () => {
await importEnexFile('images_with_and_without_size.enex');
let expected = await readExpectedFile('images_with_and_without_size.md');
const note: NoteEntity = (await Note.all())[0];
const all: ResourceEntity[] = await Resource.all();
expect(all.length).toBe(2);
const svgResource = all.find(r => r.mime === 'image/svg+xml');
const pngResource = all.find(r => r.mime === 'image/png');
expected = expected.replace(/RESOURCE_ID_1/, pngResource.id);
expected = expected.replace(/RESOURCE_ID_2/, svgResource.id);
expect(note.body).toBe(expected);
});
});

View File

@ -1,5 +1,6 @@
import markdownUtils from './markdownUtils';
import { ResourceEntity } from './services/database/types';
import { htmlentities } from '@joplin/utils/html';
const stringPadding = require('string-padding');
const stringToStream = require('string-to-stream');
const resourceUtils = require('./resourceUtils.js');
@ -368,26 +369,49 @@ function tagAttributeToMdText(attr: string): string {
return attr;
}
function addResourceTag(lines: string[], resource: ResourceEntity, alt = ''): string[] {
// Note: refactor to use Resource.markdownTag
if (!alt) alt = resource.title;
if (!alt) alt = resource.filename;
if (!alt) alt = '';
interface AddResourceOptions {
alt?: string;
width?: number;
height?: number;
}
alt = tagAttributeToMdText(alt);
if (resourceUtils.isImageMimeType(resource.mime)) {
lines.push('![');
lines.push(alt);
lines.push(`](:/${resource.id})`);
const addResourceTag = (lines: string[], src: string, mime: string, options: AddResourceOptions): string[] => {
const alt = options.alt ? tagAttributeToMdText(options.alt) : '';
if (resourceUtils.isImageMimeType(mime)) {
if (!!options.width || !!options.height) {
const attrs: Record<string, string> = { src };
if (options.width) attrs.width = options.width.toString();
if (options.height) attrs.height = options.height.toString();
if (alt) attrs.alt = alt;
const attrsHtml: string[] = [];
for (const [key, value] of Object.entries(attrs)) {
attrsHtml.push(`${key}="${htmlentities(value)}"`);
}
lines.push(`<img ${attrsHtml.join(' ')}/>`);
} else {
lines.push('![');
lines.push(alt);
lines.push(`](${markdownUtils.escapeLinkUrl(src)})`);
}
} else {
lines.push('[');
lines.push(alt);
lines.push(`](:/${resource.id})`);
lines.push(`](${markdownUtils.escapeLinkUrl(src)})`);
}
return lines;
}
};
const altFromResource = (resource: ResourceEntity): string => {
let alt = '';
if (!alt) alt = resource.title;
if (!alt) alt = resource.filename;
return alt;
};
function isBlockTag(n: string) {
return ['div', 'p', 'dl', 'dd', 'dt', 'center', 'address'].indexOf(n) >= 0;
@ -806,12 +830,14 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: Extra
} else if (n === 'q') {
section.lines.push('"');
} else if (n === 'img') {
// Many (most?) img tags don't have no source associated,
// especially when they were imported from HTML
if (nodeAttributes.src) {
// Many (most?) img tags don't have no source associated, especially when they were imported from HTML
let s = '![';
if (nodeAttributes.alt) s += tagAttributeToMdText(nodeAttributes.alt);
s += `](${markdownUtils.escapeLinkUrl(nodeAttributes.src)})`;
section.lines.push(s);
section.lines = addResourceTag(section.lines, nodeAttributes.src, 'image/png', {
width: nodeAttributes.width ? Number(nodeAttributes.width) : 0,
height: nodeAttributes.height ? Number(nodeAttributes.height) : 0,
alt: nodeAttributes.alt ? nodeAttributes.alt : '',
});
}
} else if (isAnchor(n)) {
state.anchorAttributes.push(nodeAttributes);
@ -928,7 +954,11 @@ function enexXmlToMdArray(stream: any, resources: ResourceEntity[], tasks: Extra
// 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.alt);
section.lines = addResourceTag(section.lines, `:/${resource.id}`, resource.mime, {
alt: nodeAttributes.alt ? nodeAttributes.alt : altFromResource(resource),
width: nodeAttributes.width ? Number(nodeAttributes.width) : 0,
height: nodeAttributes.height ? Number(nodeAttributes.height) : 0,
});
}
} else if (n === 'span') {
if (isSpanWithStyle(nodeAttributes)) {
@ -1411,7 +1441,9 @@ async function enexXmlToMd(xmlString: string, resources: ResourceEntity[], tasks
const r = result.resources[i];
if (firstAttachment) mdLines.push(NEWLINE);
mdLines.push(NEWLINE);
mdLines = addResourceTag(mdLines, r, r.filename);
mdLines = addResourceTag(mdLines, `:/${r.id}`, r.mime, {
alt: altFromResource(r),
});
firstAttachment = false;
}
@ -1422,4 +1454,4 @@ async function enexXmlToMd(xmlString: string, resources: ResourceEntity[], tasks
return output.join('\n');
}
export { enexXmlToMd, processMdArrayNewLines, NEWLINE, addResourceTag, cssValue };
export { enexXmlToMd, processMdArrayNewLines, NEWLINE, cssValue };