Skip to content

Commit

Permalink
Fix quoting issues with negative numbers
Browse files Browse the repository at this point in the history
Fix arbitrary date format import
Add extra tests
  • Loading branch information
CalebJohn committed Sep 27, 2021
1 parent 5893bb7 commit 785d6d0
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 14 deletions.
8 changes: 8 additions & 0 deletions packages/app-cli/tests/support/test_notes/yaml/unquoted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
Title: Unquoted
Longitude: -94.51350100
Completed?: No
DUE: 2022-04-04 13:00
---

note body
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ describe('interop/InteropService_Exporter_Yaml', function() {

test('should export without additional quotes', (async () => {
const folder1 = await Folder.save({ title: 'folder1' });
await Note.save({ title: '60', body: '**ma note**', parent_id: folder1.id });
await Note.save({ title: '-60', body: '**ma note**', parent_id: folder1.id });

const content = await exportAndLoad(`${exportDir()}/folder1/60.md`);
expect(content).toContain('Title: 60');
const content = await exportAndLoad(`${exportDir()}/folder1/-60.md`);
expect(content).toContain('Title: -60');
expect(content).toContain('Latitude: 0.00000000');
}));

Expand All @@ -77,7 +77,7 @@ describe('interop/InteropService_Exporter_Yaml', function() {

const content = await exportAndLoad(`${exportDir()}/folder1/Todo.md`);
expect(content).toContain(`Due: ${time.formatMsToLocal(1)}`);
expect(content).toContain('Completed?: False');
expect(content).toContain('Completed?: No');
}));

test('should export author', (async () => {
Expand Down
32 changes: 28 additions & 4 deletions packages/lib/services/interop/InteropService_Exporter_Yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import { YamlExportMetaData } from './types';

import * as yaml from 'js-yaml';

// There is a special case (negative numbers) where the yaml library will force quotations
// These need to be stripped
function trimQuotes(rawOutput: string): string {
return rawOutput.split('\n').map(line => {
const index = line.indexOf(': \'-');
if (index >= 0) {
// The plus 2 eats the : and space characters
const start = line.substring(0, index + 2);
// The plus 3 eats the quote character
const end = line.substring(index + 3, line.length - 1);
return start + end;
}
return line;
}).join('\n');


}

export const fieldOrder = ['Title', 'Updated', 'Created', 'Source', 'Author', 'Latitude', 'Longitude', 'Altitude', 'Completed?', 'Due', 'Tags'];

export default class InteropService_Exporter_Yaml extends InteropService_Exporter_Md {
Expand Down Expand Up @@ -81,7 +99,7 @@ export default class InteropService_Exporter_Yaml extends InteropService_Exporte
// todo
if (note.is_todo) {
// boolean is not support by the yaml FAILSAFE_SCHEMA
md['Completed?'] = note.todo_completed ? 'True' : 'False';
md['Completed?'] = note.todo_completed ? 'Yes' : 'No';
}
if (note.todo_due) { md['Due'] = this.convertDate(note.todo_due); }

Expand All @@ -103,11 +121,17 @@ export default class InteropService_Exporter_Yaml extends InteropService_Exporte
return fieldOrder.indexOf(a) - fieldOrder.indexOf(b);
};

// The FAILSAFE_SCHEMA allows this to export strings that look like numbers without
// the added '' quotes around the text
return yaml.dump(md, { sortKeys: sort, schema: yaml.FAILSAFE_SCHEMA });
// The FAILSAFE_SCHEMA along with noCompatMode allows this to export strings that look
// like numbers (or yes/no) without the added '' quotes around the text
const rawOutput = yaml.dump(md, { sortKeys: sort, noCompatMode: true, schema: yaml.FAILSAFE_SCHEMA });
// The additional trimming is the unfortunate result of the yaml library insisting on
// quoting negative numbers.
// For now the trimQuotes function only trims quotes associated with a negative number
// but it can be extended to support more special cases in the future if necessary.
return trimQuotes(rawOutput);
}


public async getNoteExportContent_(modNote: NoteEntity) {
const noteContent = await Note.replaceResourceInternalToExternalLinks(await Note.serialize(modNote, ['body']));
const metadata = this.extractMetadata(modNote);
Expand Down
10 changes: 10 additions & 0 deletions packages/lib/services/interop/InteropService_Importer_Yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,14 @@ describe('InteropService_Importer_Yaml: importMetadata', function() {
const tags = await Tag.tagsByNoteId(note.id);
expect(tags.length).toBe(3);
});
it('should load unquoted special forms correctl', async function() {
const note = await importNote(`${supportDir}/test_notes/yaml/unquoted.md`);

expect(note.title).toBe('Unquoted');
expect(note.body).toBe('note body\n');

expect(note.longitude).toBe('-94.51350100');
expect(note.is_todo).toBe(1);
expect(note.todo_completed).toBeUndefined();
});
});
9 changes: 6 additions & 3 deletions packages/lib/services/interop/InteropService_Importer_Yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export default class InteropService_Importer_Yaml extends InteropService_Importe
is_todo: ('completed?' in md) ? 1 : 0,
};

if (md['created']) { metadata['user_created_time'] = time.anythingToMs(md['created']); }
if (md['updated']) { metadata['user_updated_time'] = time.anythingToMs(md['updated']); }
if (md['created']) { metadata['user_created_time'] = time.anythingToMs(md['created'], Date.now()); }
if (md['updated']) { metadata['user_updated_time'] = time.anythingToMs(md['updated'], Date.now()); }

if ('latitude' in md) { metadata['latitude'] = md['latitude']; }
if ('longitude' in md) { metadata['longitude'] = md['longitude']; }
Expand All @@ -83,7 +83,10 @@ export default class InteropService_Importer_Yaml extends InteropService_Importe
// Completed time isn't preserved, so we use a sane choice here
metadata['todo_completed'] = metadata['user_updated_time'];
}
if ('due' in md) { metadata['todo_due'] = time.anythingToMs(md['due']); }
if ('due' in md) {
const due_date = time.anythingToMs(md['due'], null);
if (due_date) { metadata['todo_due'] = due_date; }
}
}

// Tags are handled seperately from typical metadata
Expand Down
13 changes: 10 additions & 3 deletions packages/lib/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,18 @@ class Time {
return m.isValid() ? m.toDate() : defaultValue;
}

public anythingToMs(o: any, defaultValue: Date = null) {
public anythingToMs(o: any, defaultValue: number = null) {
if (o && o.toDate) return o.toDate();
if (!o) return defaultValue;
const m = moment(o);
return m.isValid() ? m.toDate().getTime() : defaultValue;
// There are a few date formats supported by Joplin that are not supported by
// moment without an explicit format specifier. The typical case is that a user
// has a preferred data format. This means we should try the currently assigned
// date first, and then attempt to load a generic date string.
const m = moment(o, this.dateTimeFormat());
if (m.isValid()) return m.toDate().getTime();
// moment will fall back on Date.parse, so we might as well use it directly
const d = Date.parse(o);
return isNaN(d) ? defaultValue : d;
}

public msleep(ms: number) {
Expand Down

0 comments on commit 785d6d0

Please sign in to comment.