2026-09-22 14:12:13 +02:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { parseJournalMarkdown, serializeJournalMarkdown } from '../journal';
|
2026-09-23 12:09:08 +02:00
|
|
|
import type { JournalBlock } from '../journal';
|
2026-09-22 14:12:13 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* serializeJournalMarkdown must be the exact inverse of the inline renderer
|
|
|
|
|
* for the constructs it produces, or a save silently rewrites the file (#628).
|
|
|
|
|
*/
|
|
|
|
|
const roundTrip = (source: string) => serializeJournalMarkdown(parseJournalMarkdown(source));
|
|
|
|
|
|
|
|
|
|
describe('journal round trip (#628)', () => {
|
|
|
|
|
it('keeps a link through save', () => {
|
|
|
|
|
const source = 'Read more at [Immich](https://immich.app) today.';
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
expect(roundTrip(once)).toBe(once);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps emphasis inside a quote as markdown, not literal tags', () => {
|
|
|
|
|
const source = '> **Yes** he said -- Someone';
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
expect(once).not.toContain('<strong>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps emphasis inside a photo caption as markdown', () => {
|
|
|
|
|
const source = '';
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not double-encode an apostrophe in a photo-pair caption', () => {
|
|
|
|
|
const source = "";
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
expect(roundTrip(once)).toBe(once);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('leaves intraword underscores alone', () => {
|
|
|
|
|
const source = 'See my_photo_2024.jpg for details.';
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('still renders underscore emphasis with word boundaries either side', () => {
|
|
|
|
|
const source = 'This is _emphasised_ text.';
|
|
|
|
|
const parsed = parseJournalMarkdown(source);
|
|
|
|
|
expect(parsed.blocks[0]).toMatchObject({
|
|
|
|
|
type: 'paragraph',
|
|
|
|
|
html: 'This is <em>emphasised</em> text.',
|
|
|
|
|
});
|
|
|
|
|
// `_` and `*` both parse to <em>, so a `_`-emphasised word comes back with
|
|
|
|
|
// `*` — that normalisation is expected. What must hold is idempotence.
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(roundTrip(once)).toBe(once);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('round-trips a link nested inside bold text', () => {
|
|
|
|
|
const source = '**[Immich](https://immich.app)** is great.';
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('round-trips bold text nested inside a link label', () => {
|
|
|
|
|
const source = '[**Immich**](https://immich.app) is great.';
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
expect(once).toBe(source);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('is idempotent on a full multi-block entry', () => {
|
|
|
|
|
const source = [
|
|
|
|
|
'---',
|
|
|
|
|
'title: "A trip"',
|
|
|
|
|
'---',
|
|
|
|
|
'',
|
|
|
|
|
'# Heading',
|
|
|
|
|
'',
|
|
|
|
|
'Read more at [Immich](https://immich.app), it is **great** and *fun*.',
|
|
|
|
|
'',
|
|
|
|
|
'> **Yes** he said -- Someone',
|
|
|
|
|
'',
|
|
|
|
|
'',
|
|
|
|
|
'',
|
|
|
|
|
"",
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
|
|
|
|
const once = roundTrip(source);
|
|
|
|
|
const twice = roundTrip(once);
|
|
|
|
|
expect(twice).toBe(once);
|
|
|
|
|
expect(once).toContain('[Immich](https://immich.app)');
|
|
|
|
|
expect(once).toContain("Anna's photos");
|
|
|
|
|
});
|
2026-09-23 12:09:08 +02:00
|
|
|
|
|
|
|
|
it('round-trips an unfilled photo placeholder as a photo block, not text (#template placeholders)', () => {
|
|
|
|
|
for (const layout of ['contained', 'wide', 'fullbleed'] as const) {
|
|
|
|
|
const block: JournalBlock = { type: 'photo', assetId: '', caption: 'Pick a photo', layout };
|
|
|
|
|
const serialized = serializeJournalMarkdown({
|
|
|
|
|
frontmatter: {},
|
|
|
|
|
blocks: [block],
|
|
|
|
|
referencedAssetIds: [],
|
|
|
|
|
});
|
|
|
|
|
const parsed = parseJournalMarkdown(serialized);
|
|
|
|
|
expect(parsed.blocks).toEqual([block]);
|
|
|
|
|
expect(parsed.referencedAssetIds).toEqual([]);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('round-trips an unfilled photo-pair placeholder without referencing empty ids', () => {
|
|
|
|
|
const block: JournalBlock = { type: 'photo-pair', assetIds: ['', ''], caption: 'Two to pick' };
|
|
|
|
|
const serialized = serializeJournalMarkdown({
|
|
|
|
|
frontmatter: {},
|
|
|
|
|
blocks: [block],
|
|
|
|
|
referencedAssetIds: [],
|
|
|
|
|
});
|
|
|
|
|
const parsed = parseJournalMarkdown(serialized);
|
|
|
|
|
expect(parsed.blocks).toEqual([block]);
|
|
|
|
|
expect(parsed.referencedAssetIds).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps a filled id in a half-filled pair as the only reference', () => {
|
|
|
|
|
const block: JournalBlock = {
|
|
|
|
|
type: 'photo-pair',
|
|
|
|
|
assetIds: ['abc123', ''],
|
|
|
|
|
caption: undefined,
|
|
|
|
|
};
|
|
|
|
|
const parsed = parseJournalMarkdown(
|
|
|
|
|
serializeJournalMarkdown({ frontmatter: {}, blocks: [block], referencedAssetIds: [] }),
|
|
|
|
|
);
|
|
|
|
|
expect(parsed.blocks).toEqual([block]);
|
|
|
|
|
expect(parsed.referencedAssetIds).toEqual(['abc123']);
|
|
|
|
|
});
|
2026-09-22 14:12:13 +02:00
|
|
|
});
|