1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-11-25 22:32:52 +02:00

Improve blogs handling and also add tests #898 #1001

This commit is contained in:
Patrik J. Braun
2025-10-18 15:21:01 +02:00
parent 3ff3457ba0
commit c266fd92ce
4 changed files with 668 additions and 73 deletions

View File

@@ -5,6 +5,6 @@ export interface MDFileDTO extends FileDTO {
id: number;
name: string;
directory: DirectoryPathDTO;
date: number;
date: number; // same date as the youngest photo in a folder
}

View File

@@ -21,9 +21,11 @@ import { FileDTOToRelativePathPipe } from '../../../pipes/FileDTOToRelativePathP
})
export class GalleryBlogComponent implements OnChanges {
@Input() open: boolean;
/**
* Blog is inserted to the top (when date is null) and to all date groups, when date is set
*/
@Input() date: Date;
@Output() openChange = new EventEmitter<boolean>();
public markdowns: string[] = [];
mkObservable: Observable<GroupedMarkdown[]>;
constructor(public blogService: BlogService) {

View File

@@ -0,0 +1,497 @@
import {TestBed} from '@angular/core/testing';
import {BlogService, GroupedMarkdown} from './blog.service';
import {NetworkService} from '../../../model/network/network.service';
import {ContentService} from '../content.service';
import {MDFilesFilterPipe} from '../../../pipes/MDFilesFilterPipe';
import {of} from 'rxjs';
import {MDFileDTO} from '../../../../../common/entities/MDFileDTO';
import {DirectoryPathDTO} from '../../../../../common/entities/DirectoryDTO';
describe('BlogService', () => {
let service: BlogService;
let mockNetworkService: jasmine.SpyObj<NetworkService>;
let mockGalleryService: jasmine.SpyObj<ContentService>;
let mockMdFilesFilterPipe: jasmine.SpyObj<MDFilesFilterPipe>;
const createMockDirectory = (path: string, name: string): DirectoryPathDTO => ({
path: path,
name: name
} as DirectoryPathDTO);
const createMockMDFile = (name: string, date: number, directory?: DirectoryPathDTO): MDFileDTO => ({
id: 1,
name: name,
directory: directory || createMockDirectory('/test', 'folder'),
date: date
} as MDFileDTO);
beforeEach(() => {
mockNetworkService = jasmine.createSpyObj('NetworkService', ['getText']);
mockGalleryService = jasmine.createSpyObj('ContentService', [], {
sortedFilteredContent: of(null)
});
mockMdFilesFilterPipe = jasmine.createSpyObj('MDFilesFilterPipe', ['transform']);
TestBed.configureTestingModule({
providers: [
BlogService,
{provide: NetworkService, useValue: mockNetworkService},
{provide: ContentService, useValue: mockGalleryService},
{provide: MDFilesFilterPipe, useValue: mockMdFilesFilterPipe}
]
});
service = TestBed.inject(BlogService);
});
describe('splitMarkDown', () => {
it('should return empty array for empty markdown', async () => {
const file = createMockMDFile('test.md', Date.UTC(2025, 1, 1));
mockNetworkService.getText.and.returnValue(Promise.resolve(''));
const result = await (service as any).splitMarkDown(file, [], 0);
expect(result).toEqual([]);
});
it('should return entire markdown with date=null when no date groups exist', async () => {
const file = createMockMDFile('test.md', Date.UTC(2025, 1, 1));
const markdown = '# Test Markdown\n\nSome content here.';
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, [], 0);
expect(result.length).toBe(1);
expect(result[0].text).toBe(markdown);
expect(result[0].date).toBeNull();
expect(result[0].file).toBe(file);
expect(result[0].textShort).toBe(markdown.substring(0, 200));
});
it('should place undated markdown at top (date=null) when not a search result', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '# Test Markdown\n\nNo date tags here.';
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBeNull();
expect(result[0].text).toBe(markdown);
});
it('should place undated markdown at first media date group when search result', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const fileDate = Date.UTC(2025, 1, 5);
const file = createMockMDFile('test.md', fileDate);
const markdown = '# Test Markdown\n\nNo date tags here.';
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 5));
expect(result[0].text).toBe(markdown);
});
it('should assign dated section to exact matching date group', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '<!-- @pg-date 2025-02-03 -->\n## Day 1\n\nContent for Feb 3rd.';
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 3)); // Feb 3rd matches exactly
});
it('should assign dated section to closest younger date group', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '<!-- @pg-date 2025-02-02 -->\n## Day 1\n\nContent for Feb 2nd.';
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1)); // Feb 2nd → Feb 1st (closest younger)
});
it('should handle multiple dated sections', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = `<!-- @pg-date 2025-02-01 -->
## Day 1
Content for Feb 1st.
<!-- @pg-date 2025-02-03 -->
## Day 2
Content for Feb 3rd.`;
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(2);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1));
expect(result[0].text).toContain('Day 1');
expect(result[1].date).toBe(Date.UTC(2025, 1, 3));
expect(result[1].text).toContain('Day 2');
});
it('should concatenate sections with same date group', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = `<!-- @pg-date 2025-02-01 -->
## Part 1
First part.
<!-- @pg-date 2025-02-02 -->
## Part 2
Second part (goes to Feb 1).`;
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 5)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1));
expect(result[0].text).toContain('Part 1');
expect(result[0].text).toContain('Part 2');
});
it('should handle mixed undated and dated sections', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = `# Intro
This is undated.
<!-- @pg-date 2025-02-03 -->
## Day 1
This is dated.`;
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(2);
expect(result[0].date).toBeNull(); // Undated section goes to top
expect(result[0].text).toContain('Intro');
expect(result[1].date).toBe(Date.UTC(2025, 1, 3));
expect(result[1].text).toContain('Day 1');
});
it('should handle date tag with colon syntax', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '<!-- @pg-date: 2025-02-01 -->\n## Day 1\n\nContent.';
const dates = [Date.UTC(2025, 1, 1)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1));
});
it('should handle whitespace variations in date tags', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = ' <!-- @pg-date 2025-02-01 --> \n## Day 1\n\nContent.';
const dates = [Date.UTC(2025, 1, 1)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1));
});
it('should generate textShort preview for all sections', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const longText = 'A'.repeat(300);
const markdown = `<!-- @pg-date 2025-02-01 -->\n${longText}`;
const dates = [Date.UTC(2025, 1, 1)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result[0].textShort).toBe(result[0].text.substring(0, 200));
expect(result[0].textShort.length).toBe(200);
});
it('should handle dates in different day of same group correctly', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '<!-- @pg-date 2025-02-03 -->\n## Content at 23:59\n\nLate in the day.';
const dates = [Date.UTC(2025, 1, 3)];
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 3)); // Same day group
});
it('should handle dates in original order (ascending)', async () => {
const firstMedia = Date.UTC(2025, 1, 1);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '<!-- @pg-date 2025-02-02 -->\n## Content\n\nTest.';
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)]; // Ascending
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1)); // Should find closest correctly
});
it('should handle dates in descending order', async () => {
const firstMedia = Date.UTC(2025, 1, 5);
const file = createMockMDFile('test.md', firstMedia);
const markdown = '<!-- @pg-date 2025-02-02 -->\n## Content\n\nTest.';
const dates = [Date.UTC(2025, 1, 5), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 1)]; // Descending
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
const result = await (service as any).splitMarkDown(file, dates, firstMedia);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1)); // Should find closest correctly
});
});
describe('parseMarkdownSections', () => {
it('should return single section for markdown without date tags', () => {
const markdown = '# Test\n\nNo dates here.';
const sections = (service as any).parseMarkdownSections(markdown);
expect(sections.length).toBe(1);
expect(sections[0].date).toBeNull();
expect(sections[0].text).toBe(markdown);
});
it('should parse markdown with one date tag', () => {
const markdown = '<!-- @pg-date 2025-02-01 -->\n## Day 1\n\nContent.';
const sections = (service as any).parseMarkdownSections(markdown);
expect(sections.length).toBe(1);
expect(sections[0].date).toEqual(new Date('2025-02-01'));
expect(sections[0].text).toContain('Day 1');
});
it('should parse markdown with undated section before first date tag', () => {
const markdown = '# Intro\n\nUndated.\n\n<!-- @pg-date 2025-02-01 -->\n## Day 1\n\nDated.';
const sections = (service as any).parseMarkdownSections(markdown);
expect(sections.length).toBe(2);
expect(sections[0].date).toBeNull();
expect(sections[0].text).toContain('Intro');
expect(sections[1].date).toEqual(new Date('2025-02-01'));
expect(sections[1].text).toContain('Day 1');
});
it('should parse markdown with multiple date tags', () => {
const markdown = `<!-- @pg-date 2025-02-01 -->
## Day 1
<!-- @pg-date 2025-02-03 -->
## Day 2`;
const sections = (service as any).parseMarkdownSections(markdown);
expect(sections.length).toBe(2);
expect(sections[0].date).toEqual(new Date('2025-02-01'));
expect(sections[1].date).toEqual(new Date('2025-02-03'));
});
it('should handle empty sections gracefully', () => {
const markdown = '<!-- @pg-date 2025-02-01 -->\n\n\n<!-- @pg-date 2025-02-02 -->\n## Content';
const sections = (service as any).parseMarkdownSections(markdown);
expect(sections.length).toBe(1); // Empty sections are filtered out
expect(sections[0].text).toBe('## Content');
expect(sections[0].date).toEqual(new Date('2025-02-02'));
});
it('should trim whitespace from sections', () => {
const markdown = ' \n\n# Content\n\n ';
const sections = (service as any).parseMarkdownSections(markdown);
expect(sections[0].text).toBe('# Content');
});
});
describe('findClosestDateGroup', () => {
it('should find exact matching date group', () => {
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
const timestamp = Date.UTC(2025, 1, 3);
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 3));
});
it('should find closest younger date group', () => {
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
const timestamp = Date.UTC(2025, 1, 2); // Feb 2nd
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 1)); // Feb 1st is closest younger
});
it('should return last date group for dates older than all groups', () => {
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
const timestamp = Date.UTC(2025, 0, 1); // Jan 1st, before all groups
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 5)); // Last group (oldest behavior)
});
it('should return last date group for dates newer than all groups', () => {
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 5)];
const timestamp = Date.UTC(2025, 1, 10); // Feb 10th, after all groups
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 5)); // Last group
});
it('should handle single date group', () => {
const dates = [Date.UTC(2025, 1, 1)];
const timestamp = Date.UTC(2025, 1, 5);
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 1));
});
it('should find exact matching date group in descending order', () => {
const dates = [Date.UTC(2025, 1, 5), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 1)];
const timestamp = Date.UTC(2025, 1, 3);
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 3));
});
it('should find closest younger date group in descending order', () => {
const dates = [Date.UTC(2025, 1, 5), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 1)];
const timestamp = Date.UTC(2025, 1, 2); // Feb 2nd
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 1)); // Feb 1st is closest younger
});
it('should return last date group for dates older than all groups in descending order', () => {
const dates = [Date.UTC(2025, 1, 5), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 1)];
const timestamp = Date.UTC(2025, 0, 1); // Jan 1st, before all groups
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 1)); // Last group (oldest in descending)
});
it('should return last date group for dates newer than all groups in descending order', () => {
const dates = [Date.UTC(2025, 1, 5), Date.UTC(2025, 1, 3), Date.UTC(2025, 1, 1)];
const timestamp = Date.UTC(2025, 1, 10); // Feb 10th, after all groups
const result = (service as any).findClosestDateGroup(timestamp, dates);
expect(result).toBe(Date.UTC(2025, 1, 5)); // First group in descending (newest)
});
});
describe('groupSectionsByDate', () => {
const file = createMockMDFile('test.md', Date.UTC(2025, 1, 1));
const dates = [Date.UTC(2025, 1, 1), Date.UTC(2025, 1, 3)];
it('should place undated section at top when not search result', () => {
const sections: Array<{ date: Date | null; text: string }> = [{date: null, text: '# Test'}];
const firstMedia = Date.UTC(2025, 1, 1);
const result = (service as any).groupSectionsByDate(sections, file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBeNull();
});
it('should place undated section at file date group when search result', () => {
const sections: Array<{ date: Date | null; text: string }> = [{date: null, text: '# Test'}];
const firstMedia = Date.UTC(2025, 0, 1); // Different from file.date
const result = (service as any).groupSectionsByDate(sections, file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1));
});
it('should assign dated sections to correct date groups', () => {
const sections = [
{date: new Date('2025-02-01'), text: '# Day 1'},
{date: new Date('2025-02-03'), text: '# Day 2'}
];
const firstMedia = Date.UTC(2025, 1, 1);
const result = (service as any).groupSectionsByDate(sections, file, dates, firstMedia);
expect(result.length).toBe(2);
expect(result[0].date).toBe(Date.UTC(2025, 1, 1));
expect(result[1].date).toBe(Date.UTC(2025, 1, 3));
});
it('should concatenate sections with same date group', () => {
const sections = [
{date: new Date('2025-02-01'), text: '# Part 1'},
{date: new Date('2025-02-01'), text: '# Part 2'}
];
const firstMedia = Date.UTC(2025, 1, 1);
const result = (service as any).groupSectionsByDate(sections, file, dates, firstMedia);
expect(result.length).toBe(1);
expect(result[0].text).toContain('Part 1');
expect(result[0].text).toContain('Part 2');
expect(result[0].text).toContain('\n\n'); // Concatenated with double newline
});
it('should generate textShort for all groups', () => {
const longText = 'A'.repeat(300);
const sections = [{date: new Date('2025-02-01'), text: longText}];
const firstMedia = Date.UTC(2025, 1, 1);
const result = (service as any).groupSectionsByDate(sections, file, dates, firstMedia);
expect(result[0].textShort).toBe(longText.substring(0, 200));
});
});
describe('getMarkDown caching', () => {
it('should cache markdown content', async () => {
const file = createMockMDFile('test.md', Date.UTC(2025, 1, 1));
const markdown = '# Test Content';
mockNetworkService.getText.and.returnValue(Promise.resolve(markdown));
await (service as any).getMarkDown(file);
await (service as any).getMarkDown(file);
expect(mockNetworkService.getText).toHaveBeenCalledTimes(1);
});
it('should build correct file path', async () => {
const directory = createMockDirectory('/photos/2025', 'vacation');
const file = createMockMDFile('index.md', Date.UTC(2025, 1, 1), directory);
mockNetworkService.getText.and.returnValue(Promise.resolve('# Test'));
await (service as any).getMarkDown(file);
expect(mockNetworkService.getText).toHaveBeenCalledWith('/gallery/content//photos/2025/vacation/index.md');
});
});
});

View File

@@ -10,8 +10,8 @@ import {Config} from '../../../../../common/config/public/Config';
@Injectable()
export class BlogService {
cache: { [key: string]: Promise<string> | string } = {};
public groupedMarkdowns: Observable<GroupedMarkdown[]>;
private cache: { [key: string]: string } = {};
constructor(private networkService: NetworkService,
private galleryService: ContentService,
@@ -39,23 +39,47 @@ export class BlogService {
}), shareReplay(1));
}
public getMarkDown(file: FileDTO): Promise<string> {
/**
* Loads a Markdown file from the server and caches it
* @param file
* @private
*/
private async getMarkDown(file: FileDTO): Promise<string> {
const filePath = Utils.concatUrls(
file.directory.path,
file.directory.name,
file.name
);
if (!this.cache[filePath]) {
this.cache[filePath] = this.networkService.getText(
this.cache[filePath] = await this.networkService.getText(
'/gallery/content/' + filePath
);
(this.cache[filePath] as Promise<string>).then((val: string) => {
this.cache[filePath] = val;
});
}
return Promise.resolve(this.cache[filePath]);
return this.cache[filePath];
}
/**
* Splits a markdown file into date groups.
*
* Rules:
* - If a MD part does not have a date tag, it goes to the top (date=null)
* UNLESS file.date !== firstMedia, then it goes
* around the date group of the first media of the MD file's folder.
* - If a MD part has a date tag, it goes to the closest date group.
* - Date groups span a full day (UTC midnight to midnight).
*
* Example with date groups: [2025-02-01, 2025-02-03, 2025-02-04, 2025-02-10]
* - MD part: 2025-02-01 00:00 → goes to 2025-02-01
* - MD part: 2025-02-02 00:00 → goes to 2025-02-01 (closest younger)
* - MD part: 2025-02-03 00:00 → goes to 2025-02-03
* - MD part: 2025-02-03 23:59 → goes to 2025-02-03
* - MD part: 2025-02-04 00:00 → goes to 2025-02-04
*
* @param file The markdown file to split
* @param dates Array of date group timestamps (in milliseconds, UTC midnight)
* @param firstMedia The timestamp of the first media in the current gallery view
* @private
*/
private async splitMarkDown(file: MDFileDTO, dates: number[], firstMedia: number): Promise<GroupedMarkdown[]> {
const markdown = (await this.getMarkDown(file)).trim();
@@ -63,8 +87,8 @@ export class BlogService {
return [];
}
// there is no date group by
if (dates.length == 0) {
// No date groups exist - return entire markdown with no date
if (dates.length === 0) {
return [{
text: markdown,
file: file,
@@ -73,83 +97,155 @@ export class BlogService {
}];
}
dates.sort((a, b) => a - b);
// Keep dates in their original order from mediaGroups (respects sorting direction)
const splitterRgx = new RegExp(/^\s*<!--\s*@pg-date:?\s*\d{4}-\d{1,2}-\d{1,2}\s*-->/, 'gim');
const dateRgx = new RegExp(/\d{4}-\d{1,2}-\d{1,2}/);
// Parse markdown for date-tagged sections
const sections = this.parseMarkdownSections(markdown);
const ret: GroupedMarkdown[] = [];
const matches = Array.from(markdown.matchAll(splitterRgx));
const getDateGroup = (date: Date) => {
// get UTC midnight date
const dateNum = Utils.makeUTCMidnight(date, undefined).getTime();
let groupDate = dates.find((d, i) => i > dates.length - 1 ? false : dates[i + 1] > dateNum); //dates are sorted
// cant find the date. put to the last group (as it was later)
if (groupDate === undefined) {
groupDate = dates[dates.length - 1];
}
return groupDate;
};
// There is no splits
if (matches.length == 0) {
// No date tags found - treat entire markdown as one section
if (sections.length === 1 && sections[0].date === null) {
const beforeFirstMedia = file.date === firstMedia;
return [{
text: markdown,
text: sections[0].text,
file: file,
textShort: markdown.substring(0, 200),
date: getDateGroup(new Date(file.date))
textShort: sections[0].text.substring(0, 200),
date: beforeFirstMedia ? null : this.findClosestDateGroup(file.date, dates)
}];
}
// Group sections by date and concatenate sections with the same date group
return this.groupSectionsByDate(sections, file, dates, firstMedia);
}
const baseText = markdown.substring(0, matches[0].index).trim();
/**
* Parses a markdown file into sections based on date tags.
* Returns an array where each section has optional date and text content.
*
* @param markdown The raw markdown content
* @private
*/
private parseMarkdownSections(markdown: string): Array<{ date: Date | null; text: string }> {
const splitterRgx = /^\s*<!--\s*@pg-date:?\s*\d{4}-\d{1,2}-\d{1,2}\s*-->/gim;
const dateRgx = /\d{4}-\d{1,2}-\d{1,2}/;
// don't show empty
if (baseText) {
if (file.date === firstMedia) {
ret.push({
text: baseText,
file: file,
date: null
});
const matches = Array.from(markdown.matchAll(splitterRgx));
if (matches.length === 0) {
// No date tags - return entire markdown as one undated section
return [{date: null, text: markdown.trim()}];
}
const sections: Array<{ date: Date | null; text: string }> = [];
// Extract the section before the first date tag (if any)
const preFirstTagText = markdown.substring(0, matches[0].index).trim();
if (preFirstTagText) {
sections.push({date: null, text: preFirstTagText});
}
// Extract each dated section
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
const dateStr = match[0].match(dateRgx)?.[0];
const sectionDate = dateStr ? new Date(dateStr) : null;
// Get text from after the date tag to next match (or end of markdown)
const startIdx = match.index! + match[0].length; // Start after the date tag
const endIdx = i + 1 < matches.length ? matches[i + 1].index! : markdown.length;
const sectionText = markdown.substring(startIdx, endIdx).trim();
// Only add section if it has actual content (not just whitespace)
if (sectionText) {
sections.push({date: sectionDate, text: sectionText});
}
}
return sections;
}
/**
* Finds the closest date group that is on or before the given timestamp.
* This is the "closest younger" date group as per the specification.
* Works with both ascending and descending date arrays.
*
* @param timestamp The timestamp to find a date group for (in milliseconds)
* @param dates Array of date group timestamps (in original order from mediaGroups)
* @private
*/
private findClosestDateGroup(timestamp: number, dates: number[]): number {
const targetMidnight = Utils.makeUTCMidnight(new Date(timestamp), undefined).getTime();
// Detect if dates are in ascending or descending order
const isAscending = dates.length < 2 || dates[0] <= dates[dates.length - 1];
// Find the closest date group that is <= targetMidnight
let closestGroup = dates[dates.length - 1]; // Default to last
if (isAscending) {
for (let i = 1; i < dates.length; i++) {
if (dates[i] > targetMidnight) {
closestGroup = dates[i-1];
break;
}
}
} else {
for (let i = 1; i < dates.length; i++) {
if (dates[i] < targetMidnight) {
closestGroup = dates[i-1];
break;
}
}
}
return closestGroup;
}
/**
* Groups markdown sections by their appropriate date groups and concatenates
* sections that belong to the same date group.
*
* @param sections Parsed markdown sections with optional dates
* @param file The markdown file being processed
* @param sortedDates Array of date group timestamps, sorted ascending
* @param firstMedia The timestamp of the first media in the current gallery view
* @private
*/
private groupSectionsByDate(
sections: Array<{ date: Date | null; text: string }>,
file: MDFileDTO,
sortedDates: number[],
firstMedia: number
): GroupedMarkdown[] {
const grouped: GroupedMarkdown[] = [];
const beforeFirestMedia = file.date === firstMedia;
for (const section of sections) {
let targetDateGroup: number | null;
if (section.date === null) {
// Undated section: goes to top (null) unless it's a search result
targetDateGroup = beforeFirestMedia ? null : this.findClosestDateGroup(file.date, sortedDates);
} else {
ret.push({
text: baseText,
file: file,
date: getDateGroup(new Date(file.date))
// Dated section: find closest date group on or before this date
targetDateGroup = this.findClosestDateGroup(section.date.getTime(), sortedDates);
}
// Try to find existing group with the same date and concatenate
const existingGroup = grouped.find(g => g.date === targetDateGroup);
if (existingGroup) {
existingGroup.text += '\n\n' + section.text;
} else {
grouped.push({
date: targetDateGroup,
text: section.text,
file: file
});
}
}
for (let i = 0; i < matches.length; ++i) {
const matchedStr = matches[i][0];
const dateNum = new Date(matchedStr.match(dateRgx)[0]);
// Generate short text previews for all groups
grouped.forEach(md => md.textShort = md.text.substring(0, 200));
const groupDate = getDateGroup(dateNum);
const text = (i + 1 >= matches.length ? markdown.substring(matches[i].index) : markdown.substring(matches[i].index, matches[i + 1].index)).trim();
// don't show empty
if (!text) {
continue;
}
// if it would be in the same group. Concatenate it
const sameGroup = ret.find(g => g.date == groupDate);
if (sameGroup) {
sameGroup.text += text;
continue;
}
ret.push({
date: groupDate,
text: text,
file: file
});
}
ret.forEach(md => md.textShort = md.text.substring(0, 200));
return ret;
return grouped;
}
}