You've already forked pigallery2
mirror of
https://github.com/bpatrik/pigallery2.git
synced 2025-11-25 22:32:52 +02:00
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
497
src/frontend/app/ui/gallery/blog/blog.service.spec.ts
Normal file
497
src/frontend/app/ui/gallery/blog/blog.service.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user