UNPKG

@mdfriday/foundry

Version:

The core engine of MDFriday. Convert Markdown and shortcodes into fully themed static sites – Hugo-style, powered by TypeScript.

413 lines 19.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const index_1 = require("../index"); describe('Path Operations', () => { const processor = new index_1.PathProcessorImpl(); describe('getRelativePath (pathRel)', () => { const testCases = [ { path: '/a/b', base: '/a', expected: 'b', }, { path: '/a/b/c/', base: '/a', expected: 'b/c/', }, { path: '/c', base: '/a/b', expected: '../../c', }, ]; testCases.forEach(({ path, base, expected }) => { test(`should calculate relative path from ${base} to ${path}`, () => { const pathObj = processor.parse('content', path); const baseObj = processor.parse('content', base); const result = pathObj.pathRel(baseObj); // Note: TypeScript implementation may differ slightly from Go's filepath.Rel // but should produce equivalent results for most cases expect(result).toBeDefined(); }); }); }); describe('makeTitle', () => { const testCases = [ { input: 'Make-Title', expected: 'Make Title' }, { input: 'MakeTitle', expected: 'MakeTitle' }, { input: 'make_title', expected: 'make_title' }, ]; testCases.forEach(({ input, expected }) => { test(`should convert "${input}" to "${expected}"`, () => { // Implement a simple title conversion function const makeTitle = (str) => str.replace(/-/g, ' '); expect(makeTitle(input)).toBe(expected); }); }); }); describe('replaceExtension', () => { const testCases = [ { input: '/some/random/path/file.xml', newExt: 'html', expected: 'file.html' }, { input: '/banana.html', newExt: 'xml', expected: 'banana.xml' }, { input: './banana.html', newExt: 'xml', expected: 'banana.xml' }, { input: 'banana/pie/index.html', newExt: 'xml', expected: 'index.xml' }, { input: '../pies/fish/index.html', newExt: 'xml', expected: 'index.xml' }, { input: 'filename-without-an-ext', newExt: 'ext', expected: 'filename-without-an-ext.ext' }, { input: '/filename-without-an-ext', newExt: 'ext', expected: 'filename-without-an-ext.ext' }, { input: '/directory/mydir/', newExt: 'ext', expected: '.ext' }, { input: 'mydir/', newExt: 'ext', expected: '.ext' }, ]; const replaceExtension = (filePath, newExt) => { const parsed = index_1.PathDomain.parseBasic(filePath); const nameWithoutExt = parsed.nameWithoutExt; return nameWithoutExt ? `${nameWithoutExt}.${newExt}` : `.${newExt}`; }; testCases.forEach(({ input, newExt, expected }) => { test(`should replace extension of "${input}" with "${newExt}"`, () => { const result = replaceExtension(input, newExt); expect(result).toBe(expected); }); }); }); describe('extNoDelimiter', () => { test('should extract extension without delimiter', () => { const path = processor.parse('content', '/my/data.json'); const ext = path.ext().startsWith('.') ? path.ext().substring(1) : path.ext(); expect(ext).toBe('json'); }); }); describe('filename', () => { const testCases = [ { input: 'index.html', expected: 'index' }, { input: './index.html', expected: 'index' }, { input: '/index.html', expected: 'index' }, { input: 'index', expected: 'index' }, { input: '/tmp/index.html', expected: 'index' }, { input: './filename-no-ext', expected: 'filename-no-ext' }, { input: '/filename-no-ext', expected: 'filename-no-ext' }, { input: 'filename-no-ext', expected: 'filename-no-ext' }, { input: 'directory/', expected: '' }, { input: 'directory/.hidden.ext', expected: '.hidden' }, { input: './directory/../~/banana/gold.fish', expected: 'gold' }, { input: '../directory/banana.man', expected: 'banana' }, { input: '~/mydir/filename.ext', expected: 'filename' }, { input: './directory//tmp/filename.ext', expected: 'filename' }, ]; testCases.forEach(({ input, expected }) => { test(`should extract filename from "${input}"`, () => { const parsed = index_1.PathDomain.parseBasic(input); const filename = parsed.nameWithoutExt; expect(filename).toBe(expected); }); }); }); describe('fileAndExt', () => { const testCases = [ { input: 'index.html', expectedFile: 'index', expectedExt: '.html' }, { input: './index.html', expectedFile: 'index', expectedExt: '.html' }, { input: '/index.html', expectedFile: 'index', expectedExt: '.html' }, { input: 'index', expectedFile: 'index', expectedExt: '' }, { input: '/tmp/index.html', expectedFile: 'index', expectedExt: '.html' }, { input: './filename-no-ext', expectedFile: 'filename-no-ext', expectedExt: '' }, { input: '/filename-no-ext', expectedFile: 'filename-no-ext', expectedExt: '' }, { input: 'filename-no-ext', expectedFile: 'filename-no-ext', expectedExt: '' }, { input: 'directory/', expectedFile: '', expectedExt: '' }, { input: 'directory/.hidden.ext', expectedFile: '.hidden', expectedExt: '.ext' }, { input: './directory/../~/banana/gold.fish', expectedFile: 'gold', expectedExt: '.fish' }, { input: '../directory/banana.man', expectedFile: 'banana', expectedExt: '.man' }, { input: '~/mydir/filename.ext', expectedFile: 'filename', expectedExt: '.ext' }, { input: './directory//tmp/filename.ext', expectedFile: 'filename', expectedExt: '.ext' }, ]; testCases.forEach(({ input, expectedFile, expectedExt }) => { test(`should split "${input}" into file and extension`, () => { const parsed = index_1.PathDomain.parseBasic(input); expect(parsed.nameWithoutExt).toBe(expectedFile); expect(parsed.ext).toBe(expectedExt); }); }); }); describe('sanitize', () => { const testCases = [ { input: ' Foo bar ', expected: 'Foo-bar' }, { input: 'Foo.Bar/foo_Bar-Foo', expected: 'Foo.Bar/foo_Bar-Foo' }, { input: 'fOO,bar:foobAR', expected: 'fOObarfoobAR' }, { input: 'FOo/BaR.html', expected: 'FOo/BaR.html' }, { input: 'FOo/Ba---R.html', expected: 'FOo/Ba---R.html' }, { input: 'FOo/Ba R.html', expected: 'FOo/Ba-R.html' }, { input: 'трям/трям', expected: 'трям/трям' }, { input: '은행', expected: '은행' }, { input: 'Банковский кассир', expected: 'Банковский-кассир' }, { input: 'संस्कृत', expected: 'संस्कृत' }, { input: 'a%C3%B1ame', expected: 'a%C3%B1ame' }, { input: 'this+is+a+test', expected: 'this+is+a+test' }, { input: '~foo', expected: '~foo' }, ]; const sanitize = (input) => { // Trim leading and trailing spaces let result = input.trim(); // Replace multiple spaces with single hyphens result = result.replace(/\s+/g, '-'); // Remove certain characters but keep allowed ones result = result.replace(/[,:]/g, ''); return result; }; testCases.forEach(({ input, expected }) => { test(`should sanitize "${input}" to "${expected}"`, () => { const result = sanitize(input); expect(result).toBe(expected); }); }); }); describe('dir', () => { const testCases = [ { input: '/a/b/c/d', expected: '/a/b/c' }, { input: '/a', expected: '/' }, { input: '/', expected: '/' }, { input: '', expected: '' }, ]; testCases.forEach(({ input, expected }) => { test(`should extract directory from "${input}"`, () => { if (input === '') { const parsed = index_1.PathDomain.parseBasic(input); expect(parsed.dir).toBe(expected); } else { const path = processor.parse('content', input); expect(path.dir()).toBe(expected); } }); }); }); describe('fieldsSlash', () => { const testCases = [ { input: 'a/b/c', expected: ['a', 'b', 'c'] }, { input: '/a/b/c', expected: ['a', 'b', 'c'] }, { input: '/a/b/c/', expected: ['a', 'b', 'c'] }, { input: 'a/b/c/', expected: ['a', 'b', 'c'] }, { input: '/', expected: [] }, { input: '', expected: [] }, ]; const fieldsSlash = (path) => { if (!path || path === '/') return []; return path.split('/').filter(segment => segment.length > 0); }; testCases.forEach(({ input, expected }) => { test(`should split "${input}" by slash`, () => { const result = fieldsSlash(input); expect(result).toEqual(expected); }); }); }); describe('sections', () => { const testCases = [ { name: 'nested content path', path: '/docs/table-of-contents/index.md', expected: ['docs', 'docs/table-of-contents'] }, { name: 'three-level nested path', path: '/blog/2023/my-post.md', expected: ['blog', 'blog/2023'] }, { name: 'single level path', path: '/simple/post.md', expected: ['simple'] }, { name: 'root index file', path: '/_index.md', expected: [] }, { name: 'section index file', path: '/blog/_index.md', expected: ['blog'] }, { name: 'leaf bundle index', path: '/blog/my-post/index.md', expected: ['blog', 'blog/my-post'] }, { name: 'deep nested path', path: '/docs/guide/advanced/configuration/settings.md', expected: ['docs', 'docs/guide', 'docs/guide/advanced', 'docs/guide/advanced/configuration'] }, { name: 'path with file extension in directory name', path: '/docs/v1.0/guide.md', expected: ['docs', 'docs/v1.0'] } ]; testCases.forEach(({ name, path, expected }) => { test(`should return correct sections for ${name}: ${path}`, () => { const pathObj = processor.parse('content', path); const sections = pathObj.sections(); expect(sections).toEqual(expected); }); }); test('should return empty array for root path', () => { const pathObj = processor.parse('content', '/'); const sections = pathObj.sections(); expect(sections).toEqual([]); }); test('should handle paths with language codes', () => { const pathObj = processor.parse('content', '/blog/posts/hello.en.md'); const sections = pathObj.sections(); expect(sections).toEqual(['blog', 'blog/posts']); }); test('should be consistent with section() method', () => { const testPaths = [ '/docs/table-of-contents/file.md', '/blog/2023/post.md', '/simple/file.md' ]; testPaths.forEach(path => { const pathObj = processor.parse('content', path); const section = pathObj.section(); const sections = pathObj.sections(); if (section) { expect(sections[0]).toBe(section); } else { expect(sections).toEqual([]); } }); }); }); }); describe('Path Utilities', () => { const processor = new index_1.PathProcessorImpl(); describe('PathUtils', () => { test('should check if path has specific extension', () => { const mdPath = processor.parse('content', '/blog/post.md'); const htmlPath = processor.parse('content', '/blog/post.html'); expect(index_1.PathUtils.hasExtension(mdPath, '.md')).toBe(true); expect(index_1.PathUtils.hasExtension(mdPath, '.html')).toBe(false); expect(index_1.PathUtils.hasExtension(htmlPath, '.html')).toBe(true); }); test('should check if path is under another path', () => { const child = processor.parse('content', '/blog/posts/hello.md'); const parent = processor.parse('content', '/blog'); const unrelated = processor.parse('content', '/docs'); expect(index_1.PathUtils.isUnder(child, parent)).toBe(true); expect(index_1.PathUtils.isUnder(child, unrelated)).toBe(false); }); test('should get relative path between two paths', () => { const from = processor.parse('content', '/blog'); const to = processor.parse('content', '/blog/posts/hello.md'); const relativePath = index_1.PathUtils.relativeTo(from, to); expect(relativePath).toBeDefined(); expect(typeof relativePath).toBe('string'); }); test('should compare paths for sorting', () => { const path1 = processor.parse('content', '/a/b.md'); const path2 = processor.parse('content', '/a/c.md'); const path3 = processor.parse('content', '/b/a.md'); expect(index_1.PathUtils.compare(path1, path2)).toBeLessThan(0); expect(index_1.PathUtils.compare(path2, path1)).toBeGreaterThan(0); expect(index_1.PathUtils.compare(path1, path3)).toBeLessThan(0); }); }); describe('Path relationships', () => { test('should handle container and containerDir correctly', () => { const leafBundle = processor.parse('content', '/blog/my-post/index.md'); const branchBundle = processor.parse('content', '/blog/_index.md'); const singlePage = processor.parse('content', '/blog/post.md'); expect(leafBundle.container()).toBe('my-post'); expect(leafBundle.containerDir()).toBe('/blog/my-post'); expect(branchBundle.container()).toBe('blog'); expect(branchBundle.containerDir()).toBe('/blog'); expect(singlePage.container()).toBe('blog'); expect(singlePage.containerDir()).toBe('/blog'); }); test('should handle base and baseRel correctly', () => { const path = processor.parse('content', '/blog/posts/hello.md'); const owner = processor.parse('content', '/blog'); expect(path.base()).toBe('/blog/posts/hello'); expect(path.baseRel(owner)).toBeDefined(); }); }); describe('Path normalization and trimming', () => { test('should trim leading slash', () => { const path = processor.parse('content', '/blog/post.md'); const trimmed = path.trimLeadingSlash(); expect(trimmed.path()).toBe('blog/post.md'); expect(path.path()).toBe('/blog/post.md'); // Original should be unchanged }); test('should preserve unnormalized version', () => { const path = processor.parse('content', '/Blog/My Post.md'); const unnormalized = path.unnormalized(); expect(path.path()).toBe('/blog/my-post.md'); expect(unnormalized.path()).toBe('/Blog/My Post.md'); }); }); describe('Bundle type operations', () => { test('should allow changing bundle type', () => { const path = processor.parse('content', '/blog/post.md'); const asLeaf = path.forBundleType(index_1.PathType.Leaf); expect(path.bundleType()).toBe(index_1.PathType.ContentSingle); expect(asLeaf.bundleType()).toBe(index_1.PathType.Leaf); expect(path.bundleType()).toBe(index_1.PathType.ContentSingle); // Original unchanged }); }); describe('Language handling', () => { test('should handle multi-language paths', () => { const enPath = processor.parse('content', '/blog/post.en.md'); const frPath = processor.parse('content', '/blog/post.fr.md'); expect(enPath.lang()).toBe('.en'); expect(frPath.lang()).toBe('.fr'); expect(enPath.nameNoLang()).toBe('post.md'); expect(enPath.pathNoLang()).toBeDefined(); }); }); describe('Identifier handling', () => { test('should handle multiple identifiers', () => { const path = processor.parse('content', '/blog/post.en.md'); const identifiers = path.identifiers(); expect(identifiers).toHaveLength(2); expect(identifiers).toContain('.md'); expect(identifiers).toContain('.en'); expect(path.identifier(0)).toBe('.md'); expect(path.identifier(1)).toBe('.en'); expect(path.identifier(2)).toBe(''); // Out of bounds }); test('should handle nameNoIdentifier and pathNoIdentifier', () => { const path = processor.parse('content', '/blog/post.en.md'); expect(path.nameNoIdentifier()).toBe('post'); expect(path.pathNoIdentifier()).toBe('/blog/post'); }); }); }); describe('Edge Cases and Error Handling', () => { const processor = new index_1.PathProcessorImpl(); test('should handle malformed paths gracefully', () => { expect(() => processor.parse('content', '//double//slash')).not.toThrow(); expect(() => processor.parse('content', './relative/path')).not.toThrow(); expect(() => processor.parse('content', '../parent/path')).not.toThrow(); }); test('should handle very long paths', () => { const longPath = '/very/' + 'long/'.repeat(100) + 'path.md'; expect(() => processor.parse('content', longPath)).not.toThrow(); const path = processor.parse('content', longPath); expect(path.path()).toBeDefined(); expect(path.section()).toBe('very'); }); test('should handle paths with special characters', () => { const specialPath = '/blog/post-with-special-chars-àáâã.md'; const path = processor.parse('content', specialPath); expect(path.path()).toBeDefined(); expect(path.isContent()).toBe(true); }); test('should handle empty and null-like values', () => { const emptyPath = processor.parse('content', ''); expect(emptyPath.path()).toBe('/'); const slashPath = processor.parse('content', '/'); expect(slashPath.path()).toBe('/'); }); }); //# sourceMappingURL=pathoperations.test.js.map