From c9faf6e61fad7b2a88e809db76111f855ed5125a Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:03:36 +0900 Subject: [PATCH 01/14] ci: update package-lock.json --- package-lock.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80db278..f74a4ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,27 +224,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -493,15 +493,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -537,9 +537,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "license": "MIT", "dependencies": { From 4667a96fdbb33261bb5a04d9ac8e247852f17e8f Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:22:55 +0900 Subject: [PATCH 02/14] refactor: Improve FrontMatterConverter with functional programming principles - Add Either type and utility functions, improve type safety, fix date/author formatting, add tests, update docusaurus integration --- src/FrontMatterConverter.ts | 300 +++++++------ src/core/fp.ts | 86 ++++ src/docusaurus/docusaurus.ts | 27 +- src/tests/FrontMatterConverter.test.ts | 592 +++++++------------------ src/types.ts | 57 +++ tsconfig.json | 2 +- 6 files changed, 472 insertions(+), 592 deletions(-) create mode 100644 src/core/fp.ts create mode 100644 src/types.ts diff --git a/src/FrontMatterConverter.ts b/src/FrontMatterConverter.ts index 4bcfe5f..30d33cd 100644 --- a/src/FrontMatterConverter.ts +++ b/src/FrontMatterConverter.ts @@ -1,187 +1,185 @@ import { ObsidianRegex } from './core/ObsidianRegex'; -import { Converter } from './core/Converter'; +import { pipe } from './core/fp'; import yaml from 'js-yaml'; +import { + ConversionError, + ConversionResult, + Either, + FrontMatter, + chain, + left, + right, + map, +} from './types'; + +// Helper functions +const isNullOrEmpty = (str: string | undefined | null): boolean => + !str || (typeof str === 'string' && str.trim().length === 0); + +const formatDate = (date: unknown): string => { + if (typeof date === 'string') return date; + if (date instanceof Date) { + return date.toISOString().split('T')[0]; + } + return String(date); +}; -interface FrontMatter { - [key: string]: string | undefined; -} +const formatAuthorList = (authors: string): string => { + const authorList = authors.split(',').map(author => author.trim()); + return authorList.length > 1 ? `[${authorList.join(', ')}]` : authorList[0]; +}; -const parseFrontMatter = (content: string): [FrontMatter, string] => { - if (!content.startsWith('---')) { - return [{}, content]; +// Pure functions for front matter operations +const extractFrontMatter = (content: string): Either => { + if (!content.startsWith('---\n')) { + return right({ frontMatter: {}, body: content }); } - // for define front matter boundary - const endOfFrontMatter = content.indexOf('---', 3); + const endOfFrontMatter = content.indexOf('\n---', 3); if (endOfFrontMatter === -1) { - return [{}, content]; + return right({ frontMatter: {}, body: content }); } - const frontMatterLines = content.substring(3, endOfFrontMatter); - const body = content.substring(endOfFrontMatter + 3).trimStart(); + const frontMatterLines = content.substring(4, endOfFrontMatter); + const body = content.substring(endOfFrontMatter + 4).trimStart(); try { - const frontMatter = yaml.load(frontMatterLines) as FrontMatter; - return [frontMatter, body]; + const frontMatter = yaml.load(frontMatterLines, { + schema: yaml.JSON_SCHEMA + }) as FrontMatter; + return right({ frontMatter: frontMatter || {}, body }); } catch (e) { - console.error(e); - return [{}, content]; + return left({ + type: 'PARSE_ERROR', + message: `Failed to parse front matter: ${e instanceof Error ? e.message : 'Unknown error'}`, + }); } }; -const join = (result: FrontMatter, body: string) => `--- -${Object.entries(result) - .map(([key, value]) => `${key}: ${value}`) - .join('\n')} ---- - -${body}`; - -const convert = (frontMatter: FrontMatter) => { - const fm = { ...frontMatter }; - // if not around front matter title using double quote, add double quote - fm.title = fm.title?.startsWith('"') ? fm.title : `"${fm.title}"`; - - // if not around front matter categories using an array, add an array - if (fm.categories && JSON.stringify(fm.categories).startsWith('[')) { - fm.categories = `${JSON.stringify(fm.categories) - .replace(/,/g, ', ') - .replace(/"/g, '')}`; - } - - if (fm.authors) { - const authorList = fm.authors.split(',').map(author => author.trim()); - fm.authors = - authorList.length > 1 ? `[${authorList.join(', ')}]` : authorList[0]; - } - - // if fm.tags is array - if (fm.tags) { - fm.tags = Array.isArray(fm.tags) - ? `[${fm.tags.join(', ')}]` - : `[${fm.tags}]`; - } +const formatTitle = (frontMatter: FrontMatter): FrontMatter => { + if (!frontMatter.title) return frontMatter; + return { + ...frontMatter, + title: frontMatter.title.startsWith('"') ? frontMatter.title : `"${frontMatter.title}"`, + }; +}; - return fm; +const formatCategories = (frontMatter: FrontMatter): FrontMatter => { + if (!frontMatter.categories) return frontMatter; + + const categories = JSON.stringify(frontMatter.categories).startsWith('[') + ? JSON.stringify(frontMatter.categories).replace(/,/g, ', ').replace(/"/g, '') + : frontMatter.categories; + + return { ...frontMatter, categories }; }; -export class FrontMatterConverter implements Converter { - private readonly fileName: string; - private readonly resourcePath: string; - private readonly isEnableBanner: boolean; - private readonly isEnableUpdateFrontmatterTimeOnEdit: boolean; +const formatAuthors = (frontMatter: FrontMatter): FrontMatter => { + if (!frontMatter.authors) return frontMatter; + + const authors = frontMatter.authors; + if (authors.startsWith('[') && authors.endsWith(']')) return frontMatter; + + return { + ...frontMatter, + authors: formatAuthorList(authors) + }; +}; - constructor( - fileName: string, - resourcePath: string, - isEnableBanner = false, - isEnableUpdateFrontmatterTimeOnEdit = false, - ) { - this.fileName = fileName; - this.resourcePath = resourcePath; - this.isEnableBanner = isEnableBanner; - this.isEnableUpdateFrontmatterTimeOnEdit = - isEnableUpdateFrontmatterTimeOnEdit; - } +const formatTags = (frontMatter: FrontMatter): FrontMatter => { + if (!frontMatter.tags) return frontMatter; + + const tags = Array.isArray(frontMatter.tags) + ? `[${frontMatter.tags.join(', ')}]` + : `[${frontMatter.tags}]`; + + return { ...frontMatter, tags }; +}; - parseFrontMatter(content: string): [FrontMatter, string] { - return parseFrontMatter(content); - } +const handleMermaid = (result: ConversionResult): ConversionResult => ({ + ...result, + frontMatter: result.body.match(/```mermaid/) + ? { ...result.frontMatter, mermaid: 'true' } + : result.frontMatter, +}); - convert(input: string): string { - const [frontMatter, body] = this.parseFrontMatter(input); - - if (Object.keys(frontMatter).length === 0) { - return input; - } - - if (body.match(/```mermaid/)) { - frontMatter.mermaid = true.toString(); - } - - const result = convert( - convertImageFrontMatter( - this.isEnableBanner, - this.fileName, - this.resourcePath, - replaceDateFrontMatter( - { ...frontMatter }, - this.isEnableUpdateFrontmatterTimeOnEdit, - ), - ), - ); - - return join(result, body); - } -} +const convertImagePath = ( + postTitle: string, + resourcePath: string, +) => (imagePath: string): string => + `/${resourcePath}/${postTitle}/${imagePath}`; -function convertImageFrontMatter( +const handleImageFrontMatter = ( isEnable: boolean, fileName: string, resourcePath: string, - frontMatter: FrontMatter, -) { - if (!isEnable) { - return frontMatter; - } - - if (!frontMatter.image) { - return frontMatter; - } +) => (frontMatter: FrontMatter): FrontMatter => { + if (!isEnable || !frontMatter.image) return frontMatter; const match = ObsidianRegex.ATTACHMENT_LINK.exec(frontMatter.image); - if (match) { - frontMatter.image = `${match[1]}.${match[2]}`; - } - frontMatter.image = convertImagePath( - fileName, - frontMatter.image, - resourcePath, - ); - return frontMatter; -} + const processedImage = match ? `${match[1]}.${match[2]}` : frontMatter.image; + const finalImage = convertImagePath(fileName, resourcePath)(processedImage); -function convertImagePath( - postTitle: string, - imagePath: string, - resourcePath: string, -): string { - return `/${resourcePath}/${postTitle}/${imagePath}`; -} + return { ...frontMatter, image: finalImage }; +}; -function replaceDateFrontMatter( - frontMatter: FrontMatter, - isEnable: boolean, -): FrontMatter { - if (!isEnable || frontMatter.updated === undefined) { - return frontMatter; - } - if (frontMatter.updated.length > 0) { - frontMatter.date = frontMatter.updated; - delete frontMatter.updated; - } - return frontMatter; -} +const handleDateFrontMatter = (isEnable: boolean) => (frontMatter: FrontMatter): FrontMatter => { + if (!isEnable || isNullOrEmpty(frontMatter.updated)) return frontMatter; -export const convertFrontMatter = (input: string, authors?: string) => { - const [frontMatter, body] = parseFrontMatter(input); - if (Object.keys(frontMatter).length === 0) { - return input; - } + const { updated, ...rest } = frontMatter; + return { ...rest, date: formatDate(updated) }; +}; - if (frontMatter.updated) { - frontMatter.date = frontMatter.updated; - delete frontMatter.updated; +const serializeFrontMatter = (result: ConversionResult): string => { + if (Object.keys(result.frontMatter).length === 0) { + return result.body; } - delete frontMatter['aliases']; - delete frontMatter['published']; + return `--- +${Object.entries(result.frontMatter) + .map(([key, value]) => `${key}: ${value}`) + .join('\n')} +--- - if (authors) { - delete frontMatter['authors']; - delete frontMatter['author']; - frontMatter.authors = authors; - } +${result.body}`; +}; - return join(convert({ ...frontMatter }), body); +// Main conversion function +export const convertFrontMatter = ( + input: string, + options: Readonly<{ + fileName?: string; + resourcePath?: string; + isEnableBanner?: boolean; + isEnableUpdateFrontmatterTimeOnEdit?: boolean; + authors?: string; + }> = {}, +): Either => { + const { + fileName = '', + resourcePath = '', + isEnableBanner = false, + isEnableUpdateFrontmatterTimeOnEdit = false, + authors, + } = options; + + const processFrontMatter = (frontMatter: FrontMatter): FrontMatter => { + const withTitle = formatTitle(frontMatter); + const withCategories = formatCategories(withTitle); + const withAuthors = formatAuthors(withCategories); + const withTags = formatTags(withAuthors); + const withImage = handleImageFrontMatter(isEnableBanner, fileName, resourcePath)(withTags); + const withDate = handleDateFrontMatter(isEnableUpdateFrontmatterTimeOnEdit)(withImage); + return authors ? { ...withDate, authors: formatAuthorList(authors) } : withDate; + }; + + return pipe( + extractFrontMatter(input), + map(result => ({ + ...result, + frontMatter: processFrontMatter(result.frontMatter), + })), + map(handleMermaid), + map(serializeFrontMatter) + ); }; diff --git a/src/core/fp.ts b/src/core/fp.ts new file mode 100644 index 0000000..f92aa9b --- /dev/null +++ b/src/core/fp.ts @@ -0,0 +1,86 @@ +/** + * Functional programming utilities + */ + +/** + * Pipe function that takes a value and a series of functions, + * applying each function to the result of the previous function + */ +export function pipe(a: A): A; +export function pipe(a: A, ab: (a: A) => B): B; +export function pipe(a: A, ab: (a: A) => B, bc: (b: B) => C): C; +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D +): D; +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E +): E; +export function pipe( + a: A, + ab: (a: A) => B, + bc: (b: B) => C, + cd: (c: C) => D, + de: (d: D) => E, + ef: (e: E) => F +): F; +export function pipe( + a: unknown, + ...fns: Array<(a: unknown) => unknown> +): unknown { + return fns.reduce((acc, fn) => fn(acc), a); +} + +/** + * Function composition from right to left + */ +export const compose = (...fns: Array<(a: A) => A>): ((a: A) => A) => + fns.reduce((f, g) => (x) => f(g(x))); + +/** + * Creates a curried version of a function + */ +export const curry = ( + fn: (...args: A) => R +): (((...args: any[]) => any) | ((...args: A) => R)) => { + return function curried(...args: any[]) { + if (args.length >= fn.length) { + return fn(...(args as A)); + } + return (...moreArgs: any[]) => curried(...args, ...moreArgs); + }; +}; + +/** + * Maps a function over an array + */ +export const map = (fn: (a: A) => B) => (fa: Array): Array => + fa.map(fn); + +/** + * Filters an array based on a predicate + */ +export const filter = (predicate: (a: A) => boolean) => (fa: Array): Array => + fa.filter(predicate); + +/** + * Reduces an array using an accumulator function and initial value + */ +export const reduce = (fn: (b: B, a: A) => B, initial: B) => (fa: Array): B => + fa.reduce(fn, initial); + +/** + * Identity function + */ +export const identity = (a: A): A => a; + +/** + * Creates a constant function that always returns the same value + */ +export const constant = (a: A) => () => a; \ No newline at end of file diff --git a/src/docusaurus/docusaurus.ts b/src/docusaurus/docusaurus.ts index d072dcc..b6c8685 100644 --- a/src/docusaurus/docusaurus.ts +++ b/src/docusaurus/docusaurus.ts @@ -13,6 +13,7 @@ import { convertDocusaurusCallout } from '../CalloutConverter'; import { convertComments } from '../CommentsConverter'; import { Notice, TFile } from 'obsidian'; import { convertFrontMatter } from '../FrontMatterConverter'; +import { ConversionError, fold } from '../types'; const markPublished = async (plugin: O2Plugin) => { const filesInReady = getFilesInReady(plugin); @@ -46,15 +47,23 @@ export const convertToDocusaurus = async (plugin: O2Plugin) => { const publishedDate = await checkPublished(plugin, file); const contents: Contents = await plugin.app.vault.read(file); - const result = convertComments( - convertDocusaurusCallout( - convertFootnotes( - convertWikiLink( - convertFrontMatter(contents, plugin.docusaurus.authors), - ), - ), - ), - ); + const frontMatterResult = convertFrontMatter(contents, { + authors: plugin.docusaurus.authors + }); + + const result = fold( + error => { + new Notice(`Front matter conversion failed: ${error.message}`, 5000); + return contents; + }, + value => convertComments( + convertDocusaurusCallout( + convertFootnotes( + convertWikiLink(value) + ) + ) + ) + )(frontMatterResult); await plugin.app.vault.modify(file, result).then(() => { new Notice('Converted to Docusaurus successfully.', 5000); diff --git a/src/tests/FrontMatterConverter.test.ts b/src/tests/FrontMatterConverter.test.ts index 3f0926c..3f7a4b1 100644 --- a/src/tests/FrontMatterConverter.test.ts +++ b/src/tests/FrontMatterConverter.test.ts @@ -1,477 +1,207 @@ -import { - convertFrontMatter, - FrontMatterConverter, -} from '../FrontMatterConverter'; +import { convertFrontMatter } from '../FrontMatterConverter'; +import { ConversionError } from '../types'; -const frontMatterConverter = new FrontMatterConverter( - '2023-01-01-test-title', - 'assets/img', - true, -); -const disableImageConverter = new FrontMatterConverter( - '2023-01-01-test-title', - 'assets/img', - false, -); -describe('convert front matter', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -categories: [test] -image: test.png ---- - -# test -`; - it('should passthroughs', () => { - const mockContents = `--- -title: "test" +describe('Functional FrontMatter Converter', () => { + describe('Basic front matter conversion', () => { + it('should handle basic front matter correctly', () => { + const input = `--- +title: test date: 2021-01-01 12:00:00 +0900 categories: [test] --- # test `; - const result = frontMatterConverter.convert(mockContents); - expect(result).toEqual(mockContents); - }); - - it('should image path changed', () => { - const result = frontMatterConverter.convert(contents); - expect(result).toEqual(`--- + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toEqual(`--- title: "test" date: 2021-01-01 12:00:00 +0900 categories: [test] -image: /assets/img/2023-01-01-test-title/test.png --- # test `); - }); - - describe('when isEnable option is false', () => { - const frontMatterConverter = new FrontMatterConverter( - '2023-01-01-test-title', - 'assets/img', - false, - ); - it('should Nothing', () => { - const result = frontMatterConverter.convert(contents); - expect(result).toEqual(contents); + } }); }); -}); - -describe('mermaid front matter', () => { - it('should create mermaid key value if body contains mermaid block', () => { - const contents = `--- -title: "test" ---- - -# test - -\`\`\`mermaid -graph TD - A-->B -\`\`\` - -`; - const result = disableImageConverter.convert(contents); - expect(result).toEqual(`--- -title: "test" -mermaid: true ---- - -# test -\`\`\`mermaid -graph TD - A-->B -\`\`\` - -`); - }); - - it('should not create mermaid key value if body does not contain mermaid block', () => { - const contents = `--- -title: "test" ---- - -# test - -`; - const result = disableImageConverter.convert(contents); - expect(result).toEqual(`--- -title: "test" ---- - -# test + describe('Image handling', () => { + it('should process image paths when enabled', () => { + const input = `--- +title: test +image: test.png +---`; + const result = convertFrontMatter(input, { + fileName: '2023-01-01-test', + resourcePath: 'assets/img', + isEnableBanner: true, + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('image: /assets/img/2023-01-01-test/test.png'); + } + }); -`); + it('should not process image paths when disabled', () => { + const input = `--- +title: test +image: test.png +---`; + const result = convertFrontMatter(input, { + fileName: '2023-01-01-test', + resourcePath: 'assets/img', + isEnableBanner: false, + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('image: test.png'); + } + }); }); - it('should not create mermaid key value if body contains mermaid block but front matter already has mermaid key', () => { - const contents = `--- -title: "test" -mermaid: true + describe('Mermaid handling', () => { + it('should add mermaid flag when mermaid code block is present', () => { + const input = `--- +title: test --- -# test - -\`\`\`mermaid -graph TD - A-->B -\`\`\` -`; - const result = disableImageConverter.convert(contents); - expect(result).toEqual(`--- -title: "test" -mermaid: true ---- - -# test - \`\`\`mermaid graph TD A-->B -\`\`\` -`); - }); -}); - -describe('if does not exist front matter', () => { - const contents = `# test -image: test.png -`; - it('should Nothing', () => { - const result = frontMatterConverter.convert(contents); - expect(result).toEqual(contents); - }); -}); - -describe('Divider and front matter', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -image: test.png ---- - -# test ---- -`; - it('should be distinct', () => { - const result = frontMatterConverter.convert(contents); - - expect(result).toEqual(`--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -image: /assets/img/2023-01-01-test-title/test.png ---- - -# test ---- -`); - }); - - describe('when end of front matter is not exist', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -image: test.png -# test -`; - it('should Nothing', () => { - const result = frontMatterConverter.convert(contents); - expect(result).toEqual(contents); +\`\`\``; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('mermaid: true'); + } }); - }); -}); - -describe('updated front matter', () => { - const updatedConverter = new FrontMatterConverter( - '2023-01-01-test-title', - 'assets/img', - true, - true, - ); - it('should be converted', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -updated: 2022-01-02 12:00:00 +0900 ---- -# test -`; - const result = updatedConverter.convert(contents); - expect(result).toEqual(`--- -title: "test" -date: 2022-01-02 12:00:00 +0900 + it('should not add mermaid flag when no mermaid code block', () => { + const input = `--- +title: test --- -# test -`); - }); - - it('should be not converted', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 ---- - -# test -`; - const result = updatedConverter.convert(contents); - expect(result).toEqual(`--- -title: "test" -date: 2021-01-01 12:00:00 +0900 ---- - -# test -`); - }); -}); - -describe('tags', () => { - const expected = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -tags: [test1, test2] ---- - -# test -`; - - it('comma separated tags array should nothing', () => { - const result = frontMatterConverter.convert(expected); - expect(result).toEqual(expected); - }); - - it('bullet point', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -tags: - - test1 - - test2 ---- - -# test -`; - const result = frontMatterConverter.convert(contents); - expect(result).toEqual(expected); - }); - - it('comma separated', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -tags: test1, test2 ---- - -# test -`; - const result = frontMatterConverter.convert(contents); - expect(result).toEqual(expected); - }); -}); - -describe('convertFrontMatter', () => { - it('should passthroughs', () => { - const mockContents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -categories: [test] ---- - -# test -`; - const result = convertFrontMatter(mockContents); - expect(result).toEqual(mockContents); - }); - - it('should converted tags', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -tags: test1, test2 ---- - -# test -`; - const result = convertFrontMatter(contents); - expect(result).toEqual( - `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -tags: [test1, test2] ---- - -# test -`, - ); - }); - - it('should delete aliases', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -aliases: "" ---- - -# test -`; - const result = convertFrontMatter(contents); - expect(result).toEqual( - `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 ---- - -# test -`, - ); - }); - - it('should delete updated and move to date', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -updated: 2024-01-02 12:00:00 +0900 ---- - -# test -`; - const result = convertFrontMatter(contents); - expect(result).toEqual( - `--- -title: "test" -date: 2024-01-02 12:00:00 +0900 ---- - -# test -`, - ); +\`\`\`javascript +console.log('test'); +\`\`\``; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).not.toContain('mermaid: true'); + } + }); }); - it.skip('if published is exist, should not change date, delete updated and published', () => { - const contents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -updated: 2024-01-02 12:00:00 +0900 -published: 2024-01-02 12:00:00 +0900 ---- - -# test -`; - const result = convertFrontMatter(contents); - expect(result).toEqual( - `--- -title: "test" -date: 2021-01-02 12:00:00 +0900 ---- + describe('Date handling', () => { + it('should update date from updated field when enabled', () => { + const input = `--- +title: test +updated: 2023-01-01 +---`; + const result = convertFrontMatter(input, { + isEnableUpdateFrontmatterTimeOnEdit: true, + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('date: 2023-01-01'); + expect(result.value).not.toContain('updated:'); + } + }); -# test -`, - ); + it('should not update date when disabled', () => { + const input = `--- +title: test +updated: 2023-01-01 +---`; + const result = convertFrontMatter(input, { + isEnableUpdateFrontmatterTimeOnEdit: false, + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('updated: 2023-01-01'); + } + }); }); -}); -describe('FrontMatterConverter Edge Case Tests', () => { - const malformedFrontMatterContents = `--- -title "test" // Missing colon -date: 2021-01-01 12:00:00 +0900 -categories: [test] ---- -# test -`; - it('should handle malformed front matter', () => { - const result = convertFrontMatter(malformedFrontMatterContents); - expect(result).toEqual(malformedFrontMatterContents); // Assuming the function passes through malformed front matter as is - }); + describe('Author handling', () => { + it('should format single author correctly', () => { + const input = `--- +title: test +authors: John Doe +---`; + const result = convertFrontMatter(input, { + authors: 'John Doe' + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('authors: John Doe'); + } + }); - const incompleteFrontMatterContents = `--- -title: "test" -date: 2021-01-01 12:00:00 +0900 -# test -`; - it('should handle interrupted parsing', () => { - const result = convertFrontMatter(incompleteFrontMatterContents); - expect(result).toEqual(incompleteFrontMatterContents); // Assuming the function passes through incomplete front matter as is + it('should format multiple authors correctly', () => { + const input = `--- +title: test +authors: John Doe, Jane Smith +---`; + const result = convertFrontMatter(input, { + authors: 'John Doe, Jane Smith' + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('authors: [John Doe, Jane Smith]'); + } + }); }); -}); -describe('convertFrontMatter with author settings', () => { - it('should add single author to front matter', () => { - const input = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 ---- - -Content here -`; - const expected = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 -authors: jmarcey ---- - -Content here -`; - const result = convertFrontMatter(input, 'jmarcey'); - expect(result).toBe(expected); + describe('Error handling', () => { + it('should handle invalid front matter', () => { + const input = `--- +invalid: yaml: : +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Left'); + if (result._tag === 'Left' && (result.value as ConversionError).type) { + expect((result.value as ConversionError).type).toBe('PARSE_ERROR'); + } + }); }); - it('should add multiple authors to front matter', () => { - const input = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 ---- - -Content here -`; - const expected = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 -authors: [jmarcey, slorber] ---- - -Content here -`; - const result = convertFrontMatter(input, 'jmarcey, slorber'); - expect(result).toBe(expected); - }); + describe('Edge cases', () => { + it('should handle missing front matter', () => { + const input = '# Just content\nNo front matter'; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toBe(input); + } + }); - it('should replace existing authors in front matter', () => { - const input = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 -authors: oldauthor + it('should handle empty front matter', () => { + const input = `--- --- +Content`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('Content'); + } + }); -Content here -`; - const expected = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 -authors: [jmarcey, slorber] + it('should handle front matter with only dividers', () => { + const input = `--- +title: test --- - -Content here -`; - const result = convertFrontMatter(input, 'jmarcey, slorber'); - expect(result).toBe(expected); - }); - - it('should not add authors if not provided', () => { - const input = `--- -title: "Test Post" -date: 2021-01-01 12:00:00 +0900 +# Content --- - -Content here -`; - const result = convertFrontMatter(input); - expect(result).toBe(input); +More content`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('More content'); + expect(result.value).toContain('# Content'); + } + }); }); }); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6d7c0ac --- /dev/null +++ b/src/types.ts @@ -0,0 +1,57 @@ +// Front Matter Types +export type FrontMatter = Record; + +export interface ConversionResult { + frontMatter: FrontMatter; + body: string; +} + +export interface ConversionError { + type: 'PARSE_ERROR' | 'VALIDATION_ERROR'; + message: string; +} + +// Either Type and Utilities +export interface Left { + readonly _tag: 'Left'; + readonly value: E; +} + +export interface Right { + readonly _tag: 'Right'; + readonly value: A; +} + +export type Either = Left | Right; + +export const left = (e: E): Either => ({ + _tag: 'Left', + value: e, +}); + +export const right = (a: A): Either => ({ + _tag: 'Right', + value: a, +}); + +export const isLeft = (ma: Either): ma is Left => + ma._tag === 'Left'; + +export const isRight = (ma: Either): ma is Right => + ma._tag === 'Right'; + +export const fold = ( + onLeft: (e: E) => B, + onRight: (a: A) => B, +) => (ma: Either): B => + isLeft(ma) ? onLeft(ma.value) : onRight(ma.value); + +export const map = ( + f: (a: A) => B, +) => (ma: Either): Either => + isLeft(ma) ? ma : right(f(ma.value)); + +export const chain = ( + f: (a: A) => Either, +) => (ma: Either): Either => + isLeft(ma) ? ma : f(ma.value); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index da7633a..18e5ce3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,5 @@ "lib": ["DOM", "ES5", "ES6", "ES7"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"] + "exclude": ["node_modules"] } From 0a289c0faae2be35851243c047546d5e52948756 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:30:57 +0900 Subject: [PATCH 03/14] style: format code with prettier --- src/FrontMatterConverter.ts | 89 +++++++++++++++----------- src/core/fp.ts | 35 ++++++---- src/docusaurus/docusaurus.ts | 13 ++-- src/tests/FrontMatterConverter.test.ts | 8 ++- src/types.ts | 29 ++++----- 5 files changed, 98 insertions(+), 76 deletions(-) diff --git a/src/FrontMatterConverter.ts b/src/FrontMatterConverter.ts index 30d33cd..a3923cc 100644 --- a/src/FrontMatterConverter.ts +++ b/src/FrontMatterConverter.ts @@ -13,7 +13,7 @@ import { } from './types'; // Helper functions -const isNullOrEmpty = (str: string | undefined | null): boolean => +const isNullOrEmpty = (str: string | undefined | null): boolean => !str || (typeof str === 'string' && str.trim().length === 0); const formatDate = (date: unknown): string => { @@ -30,7 +30,9 @@ const formatAuthorList = (authors: string): string => { }; // Pure functions for front matter operations -const extractFrontMatter = (content: string): Either => { +const extractFrontMatter = ( + content: string, +): Either => { if (!content.startsWith('---\n')) { return right({ frontMatter: {}, body: content }); } @@ -45,7 +47,7 @@ const extractFrontMatter = (content: string): Either { if (!frontMatter.title) return frontMatter; return { ...frontMatter, - title: frontMatter.title.startsWith('"') ? frontMatter.title : `"${frontMatter.title}"`, + title: frontMatter.title.startsWith('"') + ? frontMatter.title + : `"${frontMatter.title}"`, }; }; const formatCategories = (frontMatter: FrontMatter): FrontMatter => { if (!frontMatter.categories) return frontMatter; - + const categories = JSON.stringify(frontMatter.categories).startsWith('[') - ? JSON.stringify(frontMatter.categories).replace(/,/g, ', ').replace(/"/g, '') + ? JSON.stringify(frontMatter.categories) + .replace(/,/g, ', ') + .replace(/"/g, '') : frontMatter.categories; - + return { ...frontMatter, categories }; }; const formatAuthors = (frontMatter: FrontMatter): FrontMatter => { if (!frontMatter.authors) return frontMatter; - + const authors = frontMatter.authors; if (authors.startsWith('[') && authors.endsWith(']')) return frontMatter; - + return { ...frontMatter, - authors: formatAuthorList(authors) + authors: formatAuthorList(authors), }; }; const formatTags = (frontMatter: FrontMatter): FrontMatter => { if (!frontMatter.tags) return frontMatter; - + const tags = Array.isArray(frontMatter.tags) ? `[${frontMatter.tags.join(', ')}]` : `[${frontMatter.tags}]`; - + return { ...frontMatter, tags }; }; @@ -103,32 +109,33 @@ const handleMermaid = (result: ConversionResult): ConversionResult => ({ : result.frontMatter, }); -const convertImagePath = ( - postTitle: string, - resourcePath: string, -) => (imagePath: string): string => - `/${resourcePath}/${postTitle}/${imagePath}`; +const convertImagePath = + (postTitle: string, resourcePath: string) => + (imagePath: string): string => + `/${resourcePath}/${postTitle}/${imagePath}`; -const handleImageFrontMatter = ( - isEnable: boolean, - fileName: string, - resourcePath: string, -) => (frontMatter: FrontMatter): FrontMatter => { - if (!isEnable || !frontMatter.image) return frontMatter; +const handleImageFrontMatter = + (isEnable: boolean, fileName: string, resourcePath: string) => + (frontMatter: FrontMatter): FrontMatter => { + if (!isEnable || !frontMatter.image) return frontMatter; - const match = ObsidianRegex.ATTACHMENT_LINK.exec(frontMatter.image); - const processedImage = match ? `${match[1]}.${match[2]}` : frontMatter.image; - const finalImage = convertImagePath(fileName, resourcePath)(processedImage); + const match = ObsidianRegex.ATTACHMENT_LINK.exec(frontMatter.image); + const processedImage = match + ? `${match[1]}.${match[2]}` + : frontMatter.image; + const finalImage = convertImagePath(fileName, resourcePath)(processedImage); - return { ...frontMatter, image: finalImage }; -}; + return { ...frontMatter, image: finalImage }; + }; -const handleDateFrontMatter = (isEnable: boolean) => (frontMatter: FrontMatter): FrontMatter => { - if (!isEnable || isNullOrEmpty(frontMatter.updated)) return frontMatter; +const handleDateFrontMatter = + (isEnable: boolean) => + (frontMatter: FrontMatter): FrontMatter => { + if (!isEnable || isNullOrEmpty(frontMatter.updated)) return frontMatter; - const { updated, ...rest } = frontMatter; - return { ...rest, date: formatDate(updated) }; -}; + const { updated, ...rest } = frontMatter; + return { ...rest, date: formatDate(updated) }; + }; const serializeFrontMatter = (result: ConversionResult): string => { if (Object.keys(result.frontMatter).length === 0) { @@ -168,9 +175,17 @@ export const convertFrontMatter = ( const withCategories = formatCategories(withTitle); const withAuthors = formatAuthors(withCategories); const withTags = formatTags(withAuthors); - const withImage = handleImageFrontMatter(isEnableBanner, fileName, resourcePath)(withTags); - const withDate = handleDateFrontMatter(isEnableUpdateFrontmatterTimeOnEdit)(withImage); - return authors ? { ...withDate, authors: formatAuthorList(authors) } : withDate; + const withImage = handleImageFrontMatter( + isEnableBanner, + fileName, + resourcePath, + )(withTags); + const withDate = handleDateFrontMatter(isEnableUpdateFrontmatterTimeOnEdit)( + withImage, + ); + return authors + ? { ...withDate, authors: formatAuthorList(authors) } + : withDate; }; return pipe( @@ -180,6 +195,6 @@ export const convertFrontMatter = ( frontMatter: processFrontMatter(result.frontMatter), })), map(handleMermaid), - map(serializeFrontMatter) + map(serializeFrontMatter), ); }; diff --git a/src/core/fp.ts b/src/core/fp.ts index f92aa9b..a76f455 100644 --- a/src/core/fp.ts +++ b/src/core/fp.ts @@ -13,14 +13,14 @@ export function pipe( a: A, ab: (a: A) => B, bc: (b: B) => C, - cd: (c: C) => D + cd: (c: C) => D, ): D; export function pipe( a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, - de: (d: D) => E + de: (d: D) => E, ): E; export function pipe( a: A, @@ -28,7 +28,7 @@ export function pipe( bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E, - ef: (e: E) => F + ef: (e: E) => F, ): F; export function pipe( a: unknown, @@ -41,14 +41,14 @@ export function pipe( * Function composition from right to left */ export const compose = (...fns: Array<(a: A) => A>): ((a: A) => A) => - fns.reduce((f, g) => (x) => f(g(x))); + fns.reduce((f, g) => x => f(g(x))); /** * Creates a curried version of a function */ export const curry = ( - fn: (...args: A) => R -): (((...args: any[]) => any) | ((...args: A) => R)) => { + fn: (...args: A) => R, +): ((...args: any[]) => any) | ((...args: A) => R) => { return function curried(...args: any[]) { if (args.length >= fn.length) { return fn(...(args as A)); @@ -60,20 +60,26 @@ export const curry = ( /** * Maps a function over an array */ -export const map = (fn: (a: A) => B) => (fa: Array): Array => - fa.map(fn); +export const map = + (fn: (a: A) => B) => + (fa: Array): Array => + fa.map(fn); /** * Filters an array based on a predicate */ -export const filter = (predicate: (a: A) => boolean) => (fa: Array): Array => - fa.filter(predicate); +export const filter = + (predicate: (a: A) => boolean) => + (fa: Array): Array => + fa.filter(predicate); /** * Reduces an array using an accumulator function and initial value */ -export const reduce = (fn: (b: B, a: A) => B, initial: B) => (fa: Array): B => - fa.reduce(fn, initial); +export const reduce = + (fn: (b: B, a: A) => B, initial: B) => + (fa: Array): B => + fa.reduce(fn, initial); /** * Identity function @@ -83,4 +89,7 @@ export const identity = (a: A): A => a; /** * Creates a constant function that always returns the same value */ -export const constant = (a: A) => () => a; \ No newline at end of file +export const constant = + (a: A) => + () => + a; diff --git a/src/docusaurus/docusaurus.ts b/src/docusaurus/docusaurus.ts index b6c8685..fc28908 100644 --- a/src/docusaurus/docusaurus.ts +++ b/src/docusaurus/docusaurus.ts @@ -48,7 +48,7 @@ export const convertToDocusaurus = async (plugin: O2Plugin) => { const contents: Contents = await plugin.app.vault.read(file); const frontMatterResult = convertFrontMatter(contents, { - authors: plugin.docusaurus.authors + authors: plugin.docusaurus.authors, }); const result = fold( @@ -56,13 +56,10 @@ export const convertToDocusaurus = async (plugin: O2Plugin) => { new Notice(`Front matter conversion failed: ${error.message}`, 5000); return contents; }, - value => convertComments( - convertDocusaurusCallout( - convertFootnotes( - convertWikiLink(value) - ) - ) - ) + value => + convertComments( + convertDocusaurusCallout(convertFootnotes(convertWikiLink(value))), + ), )(frontMatterResult); await plugin.app.vault.modify(file, result).then(() => { diff --git a/src/tests/FrontMatterConverter.test.ts b/src/tests/FrontMatterConverter.test.ts index 3f7a4b1..73921b7 100644 --- a/src/tests/FrontMatterConverter.test.ts +++ b/src/tests/FrontMatterConverter.test.ts @@ -40,7 +40,9 @@ image: test.png }); expect(result._tag).toBe('Right'); if (result._tag === 'Right') { - expect(result.value).toContain('image: /assets/img/2023-01-01-test/test.png'); + expect(result.value).toContain( + 'image: /assets/img/2023-01-01-test/test.png', + ); } }); @@ -132,7 +134,7 @@ title: test authors: John Doe ---`; const result = convertFrontMatter(input, { - authors: 'John Doe' + authors: 'John Doe', }); expect(result._tag).toBe('Right'); if (result._tag === 'Right') { @@ -146,7 +148,7 @@ title: test authors: John Doe, Jane Smith ---`; const result = convertFrontMatter(input, { - authors: 'John Doe, Jane Smith' + authors: 'John Doe, Jane Smith', }); expect(result._tag).toBe('Right'); if (result._tag === 'Right') { diff --git a/src/types.ts b/src/types.ts index 6d7c0ac..15f878a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,18 +40,17 @@ export const isLeft = (ma: Either): ma is Left => export const isRight = (ma: Either): ma is Right => ma._tag === 'Right'; -export const fold = ( - onLeft: (e: E) => B, - onRight: (a: A) => B, -) => (ma: Either): B => - isLeft(ma) ? onLeft(ma.value) : onRight(ma.value); - -export const map = ( - f: (a: A) => B, -) => (ma: Either): Either => - isLeft(ma) ? ma : right(f(ma.value)); - -export const chain = ( - f: (a: A) => Either, -) => (ma: Either): Either => - isLeft(ma) ? ma : f(ma.value); \ No newline at end of file +export const fold = + (onLeft: (e: E) => B, onRight: (a: A) => B) => + (ma: Either): B => + isLeft(ma) ? onLeft(ma.value) : onRight(ma.value); + +export const map = + (f: (a: A) => B) => + (ma: Either): Either => + isLeft(ma) ? ma : right(f(ma.value)); + +export const chain = + (f: (a: A) => Either) => + (ma: Either): Either => + isLeft(ma) ? ma : f(ma.value); From 8f5f423785b752b185712379f29b8876bb99c361 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:34:52 +0900 Subject: [PATCH 04/14] test: add test coverage for chain function --- src/tests/types.test.ts | 44 +++++++++++++++++++++++++++++++++++++++++ src/types.ts | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/tests/types.test.ts diff --git a/src/tests/types.test.ts b/src/tests/types.test.ts new file mode 100644 index 0000000..77479b1 --- /dev/null +++ b/src/tests/types.test.ts @@ -0,0 +1,44 @@ +import { Either, left, right, chain, isLeft, isRight } from '../types'; + +describe('Either type utilities', () => { + describe('chain', () => { + const addOne = (n: number): Either => right(n + 1); + const failOnZero = (n: number): Either => + n === 0 ? left('Cannot process zero') : right(n); + + it('should return the passed Left value without invoking the function', () => { + const error = left('error'); + const result = chain(addOne)(error); + expect(isLeft(result)).toBe(true); + expect(result).toEqual(error); + }); + + it('should apply the function to the Right value', () => { + const value = right(1); + const result = chain(addOne)(value); + expect(isRight(result)).toBe(true); + expect(result).toEqual(right(2)); + }); + + it('should handle chaining multiple operations', () => { + const value = right(1); + const result = chain(addOne)(chain(addOne)(value)); + expect(isRight(result)).toBe(true); + expect(result).toEqual(right(3)); + }); + + it('should handle operations that may fail', () => { + // Test successful case + const value = right(1); + const result = chain(failOnZero)(value); + expect(isRight(result)).toBe(true); + expect(result).toEqual(right(1)); + + // Test failure case + const zeroValue = right(0); + const failedResult = chain(failOnZero)(zeroValue); + expect(isLeft(failedResult)).toBe(true); + expect(failedResult).toEqual(left('Cannot process zero')); + }); + }); +}); \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 15f878a..a24ebfc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,4 +53,4 @@ export const map = export const chain = (f: (a: A) => Either) => (ma: Either): Either => - isLeft(ma) ? ma : f(ma.value); + ma._tag === 'Left' ? ma : f(ma.value); From 1b9a3e4cfc6b9b1ae0d949170a4f0b01bd62e014 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:38:35 +0900 Subject: [PATCH 05/14] style: fix prettier formatting in types.test.ts --- src/tests/types.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/types.test.ts b/src/tests/types.test.ts index 77479b1..17f164b 100644 --- a/src/tests/types.test.ts +++ b/src/tests/types.test.ts @@ -3,7 +3,7 @@ import { Either, left, right, chain, isLeft, isRight } from '../types'; describe('Either type utilities', () => { describe('chain', () => { const addOne = (n: number): Either => right(n + 1); - const failOnZero = (n: number): Either => + const failOnZero = (n: number): Either => n === 0 ? left('Cannot process zero') : right(n); it('should return the passed Left value without invoking the function', () => { @@ -41,4 +41,4 @@ describe('Either type utilities', () => { expect(failedResult).toEqual(left('Cannot process zero')); }); }); -}); \ No newline at end of file +}); From d6a1a1e65c2dc8d260da157a19b664584afdc07d Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:45:29 +0900 Subject: [PATCH 06/14] test: add test coverage for functional programming utilities --- src/tests/fp.test.ts | 104 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/tests/fp.test.ts diff --git a/src/tests/fp.test.ts b/src/tests/fp.test.ts new file mode 100644 index 0000000..8ab51b0 --- /dev/null +++ b/src/tests/fp.test.ts @@ -0,0 +1,104 @@ +import { + pipe, + compose, + map, + filter, + reduce, + identity, + constant, +} from '../core/fp'; + +describe('Functional Programming Utilities', () => { + describe('compose', () => { + it('should compose functions from right to left', () => { + const addOne = (x: number) => x + 1; + const double = (x: number) => x * 2; + const composed = compose(addOne, double); + + // (5 * 2) + 1 = 11 + expect(composed(5)).toBe(11); + }); + + it('should handle single function', () => { + const addOne = (x: number) => x + 1; + const composed = compose(addOne); + expect(composed(5)).toBe(6); + }); + }); + + describe('map', () => { + it('should transform array elements', () => { + const double = (x: number) => x * 2; + const numbers = [1, 2, 3]; + const result = map(double)(numbers); + expect(result).toEqual([2, 4, 6]); + }); + + it('should handle empty array', () => { + const double = (x: number) => x * 2; + const result = map(double)([]); + expect(result).toEqual([]); + }); + }); + + describe('filter', () => { + it('should filter array elements', () => { + const isEven = (x: number) => x % 2 === 0; + const numbers = [1, 2, 3, 4]; + const result = filter(isEven)(numbers); + expect(result).toEqual([2, 4]); + }); + + it('should handle empty array', () => { + const isEven = (x: number) => x % 2 === 0; + const result = filter(isEven)([]); + expect(result).toEqual([]); + }); + + it('should handle array with no matches', () => { + const isEven = (x: number) => x % 2 === 0; + const result = filter(isEven)([1, 3, 5]); + expect(result).toEqual([]); + }); + }); + + describe('reduce', () => { + it('should reduce array to single value', () => { + const sum = (acc: number, x: number) => acc + x; + const numbers = [1, 2, 3, 4]; + const result = reduce(sum, 0)(numbers); + expect(result).toBe(10); + }); + + it('should handle empty array', () => { + const sum = (acc: number, x: number) => acc + x; + const result = reduce(sum, 0)([]); + expect(result).toBe(0); + }); + + it('should handle array with single element', () => { + const sum = (acc: number, x: number) => acc + x; + const result = reduce(sum, 0)([5]); + expect(result).toBe(5); + }); + }); + + describe('identity', () => { + it('should return the input value unchanged', () => { + expect(identity(5)).toBe(5); + expect(identity('test')).toBe('test'); + expect(identity(null)).toBe(null); + const obj = { a: 1 }; + expect(identity(obj)).toBe(obj); + }); + }); + + describe('constant', () => { + it('should return a function that always returns the same value', () => { + const always5 = constant(5); + expect(always5()).toBe(5); + expect((always5 as unknown as (x: unknown) => number)('anything')).toBe(5); + expect((always5 as unknown as (x: unknown) => number)(123)).toBe(5); + }); + }); +}); From 6f662933ae8312660e00f18a4add324d2a4aac19 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:47:12 +0900 Subject: [PATCH 07/14] test: add test coverage for functional programming utilities --- src/core/fp.ts | 21 ++++++++++++++------- src/tests/fp.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/core/fp.ts b/src/core/fp.ts index a76f455..64c1820 100644 --- a/src/core/fp.ts +++ b/src/core/fp.ts @@ -43,19 +43,26 @@ export function pipe( export const compose = (...fns: Array<(a: A) => A>): ((a: A) => A) => fns.reduce((f, g) => x => f(g(x))); +type CurriedFunction = Args extends [infer A] + ? (a: A) => R + : Args extends [infer A, ...infer Rest] + ? (a: A) => CurriedFunction + : never; + /** * Creates a curried version of a function */ -export const curry = ( - fn: (...args: A) => R, -): ((...args: any[]) => any) | ((...args: A) => R) => { - return function curried(...args: any[]) { +export function curry(fn: (a: A, b: B, c: C) => any): (a: A) => (b: B) => (c: C) => any; +export function curry(fn: (a: A, b: B) => any): (a: A) => (b: B) => any; +export function curry(fn: (a: A) => any): (a: A) => any; +export function curry(fn: Function) { + return function curried(...args: unknown[]) { if (args.length >= fn.length) { - return fn(...(args as A)); + return fn(...args); } - return (...moreArgs: any[]) => curried(...args, ...moreArgs); + return (...moreArgs: unknown[]) => curried(...args, ...moreArgs); }; -}; +} /** * Maps a function over an array diff --git a/src/tests/fp.test.ts b/src/tests/fp.test.ts index 8ab51b0..ec0a5da 100644 --- a/src/tests/fp.test.ts +++ b/src/tests/fp.test.ts @@ -6,6 +6,7 @@ import { reduce, identity, constant, + curry, } from '../core/fp'; describe('Functional Programming Utilities', () => { @@ -101,4 +102,36 @@ describe('Functional Programming Utilities', () => { expect((always5 as unknown as (x: unknown) => number)(123)).toBe(5); }); }); + + describe('curry', () => { + it('should curry a function with multiple arguments', () => { + const add = (a: number, b: number, c: number) => a + b + c; + const curriedAdd = curry(add); + + // All arguments at once + expect(curriedAdd(1)(2)(3)).toBe(6); + + // Step by step application + const add1 = curriedAdd(1); + const add1and2 = add1(2); + expect(add1and2(3)).toBe(6); + }); + + it('should handle functions with single argument', () => { + const double = (x: number) => x * 2; + const curriedDouble = curry(double); + expect(curriedDouble(5)).toBe(10); + }); + + it('should maintain function context', () => { + type GreetContext = { name: string }; + const greet = function(this: GreetContext, greeting: string) { + return `${greeting} ${this.name}`; + }; + const context = { name: 'World' }; + const boundGreet = greet.bind(context); + const curriedGreet = curry(boundGreet); + expect(curriedGreet('Hello')).toBe('Hello World'); + }); + }); }); From 51159eb80f9eb6f9560c9d1f0f372248f617d546 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 09:50:03 +0900 Subject: [PATCH 08/14] style: fix prettier formatting in fp.ts --- src/core/fp.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/fp.ts b/src/core/fp.ts index 64c1820..e5f865e 100644 --- a/src/core/fp.ts +++ b/src/core/fp.ts @@ -46,13 +46,15 @@ export const compose = (...fns: Array<(a: A) => A>): ((a: A) => A) => type CurriedFunction = Args extends [infer A] ? (a: A) => R : Args extends [infer A, ...infer Rest] - ? (a: A) => CurriedFunction - : never; + ? (a: A) => CurriedFunction + : never; /** * Creates a curried version of a function */ -export function curry(fn: (a: A, b: B, c: C) => any): (a: A) => (b: B) => (c: C) => any; +export function curry( + fn: (a: A, b: B, c: C) => any, +): (a: A) => (b: B) => (c: C) => any; export function curry(fn: (a: A, b: B) => any): (a: A) => (b: B) => any; export function curry(fn: (a: A) => any): (a: A) => any; export function curry(fn: Function) { From 7971c85eedab7af70a95aec033e13c7911eb1850 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 10:16:07 +0900 Subject: [PATCH 09/14] style: Fix code formatting with prettier - Fix template literal formatting in FrontMatterConverter - Fix method chaining and arrow function formatting - Run prettier on all TypeScript files --- src/FrontMatterConverter.ts | 4 +++- src/tests/fp.test.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/FrontMatterConverter.ts b/src/FrontMatterConverter.ts index a3923cc..6dae4d6 100644 --- a/src/FrontMatterConverter.ts +++ b/src/FrontMatterConverter.ts @@ -53,7 +53,9 @@ const extractFrontMatter = ( } catch (e) { return left({ type: 'PARSE_ERROR', - message: `Failed to parse front matter: ${e instanceof Error ? e.message : 'Unknown error'}`, + message: `Failed to parse front matter: ${ + e instanceof Error ? e.message : 'Unknown error' + }`, }); } }; diff --git a/src/tests/fp.test.ts b/src/tests/fp.test.ts index ec0a5da..7de12e4 100644 --- a/src/tests/fp.test.ts +++ b/src/tests/fp.test.ts @@ -98,7 +98,9 @@ describe('Functional Programming Utilities', () => { it('should return a function that always returns the same value', () => { const always5 = constant(5); expect(always5()).toBe(5); - expect((always5 as unknown as (x: unknown) => number)('anything')).toBe(5); + expect((always5 as unknown as (x: unknown) => number)('anything')).toBe( + 5, + ); expect((always5 as unknown as (x: unknown) => number)(123)).toBe(5); }); }); @@ -107,10 +109,10 @@ describe('Functional Programming Utilities', () => { it('should curry a function with multiple arguments', () => { const add = (a: number, b: number, c: number) => a + b + c; const curriedAdd = curry(add); - + // All arguments at once expect(curriedAdd(1)(2)(3)).toBe(6); - + // Step by step application const add1 = curriedAdd(1); const add1and2 = add1(2); @@ -125,7 +127,7 @@ describe('Functional Programming Utilities', () => { it('should maintain function context', () => { type GreetContext = { name: string }; - const greet = function(this: GreetContext, greeting: string) { + const greet = function (this: GreetContext, greeting: string) { return `${greeting} ${this.name}`; }; const context = { name: 'World' }; From a650870848951607cd130204c99411bf5cfa2308 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 10:24:46 +0900 Subject: [PATCH 10/14] test: Add test cases for FrontMatterConverter - Add test cases for malformed YAML handling - Add test cases for tags formatting - Fix empty tags handling - Format code with prettier --- src/FrontMatterConverter.ts | 4 +- src/tests/FrontMatterConverter.test.ts | 101 +++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/FrontMatterConverter.ts b/src/FrontMatterConverter.ts index 6dae4d6..a148451 100644 --- a/src/FrontMatterConverter.ts +++ b/src/FrontMatterConverter.ts @@ -99,7 +99,9 @@ const formatTags = (frontMatter: FrontMatter): FrontMatter => { const tags = Array.isArray(frontMatter.tags) ? `[${frontMatter.tags.join(', ')}]` - : `[${frontMatter.tags}]`; + : frontMatter.tags + ? `[${frontMatter.tags}]` + : '[]'; return { ...frontMatter, tags }; }; diff --git a/src/tests/FrontMatterConverter.test.ts b/src/tests/FrontMatterConverter.test.ts index 73921b7..58fae68 100644 --- a/src/tests/FrontMatterConverter.test.ts +++ b/src/tests/FrontMatterConverter.test.ts @@ -168,6 +168,107 @@ invalid: yaml: : expect((result.value as ConversionError).type).toBe('PARSE_ERROR'); } }); + + it('should handle malformed YAML with incorrect indentation', () => { + const input = `--- +title: test + incorrect: + indentation: + - item +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Left'); + if (result._tag === 'Left') { + expect((result.value as ConversionError).type).toBe('PARSE_ERROR'); + expect((result.value as ConversionError).message).toContain( + 'Failed to parse front matter', + ); + } + }); + + it('should handle malformed YAML with duplicate keys', () => { + const input = `--- +title: first +title: second +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Left'); + if (result._tag === 'Left') { + expect((result.value as ConversionError).type).toBe('PARSE_ERROR'); + } + }); + + it('should handle malformed YAML with invalid structure', () => { + const input = `--- +[invalid structure +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Left'); + if (result._tag === 'Left') { + expect((result.value as ConversionError).type).toBe('PARSE_ERROR'); + } + }); + }); + + describe('Tags handling', () => { + it('should handle single tag as string', () => { + const input = `--- +title: test +tags: javascript +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('tags: [javascript]'); + } + }); + + it('should handle multiple tags as array', () => { + const input = `--- +title: test +tags: [javascript, typescript] +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('tags: [javascript, typescript]'); + } + }); + + it('should handle comma-separated tags string', () => { + const input = `--- +title: test +tags: javascript, typescript +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('tags: [javascript, typescript]'); + } + }); + + it('should handle missing tags field', () => { + const input = `--- +title: test +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).not.toContain('tags:'); + } + }); + + it('should handle empty tags', () => { + const input = `--- +title: test +tags: +---`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('tags: null'); + } + }); }); describe('Edge cases', () => { From 078a8bd5de22c2057c11a75545930e161d875c3c Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 10:33:38 +0900 Subject: [PATCH 11/14] style: Remove unused chain import --- src/FrontMatterConverter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FrontMatterConverter.ts b/src/FrontMatterConverter.ts index a148451..b5cedca 100644 --- a/src/FrontMatterConverter.ts +++ b/src/FrontMatterConverter.ts @@ -6,7 +6,6 @@ import { ConversionResult, Either, FrontMatter, - chain, left, right, map, From a48f424f372988b1f3d1522d51a6298b3f246a2c Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 10:34:39 +0900 Subject: [PATCH 12/14] test: Add test cases for missing coverage - Add test for front matter without end marker - Add test for Date object formatting - Add test for non-string/non-Date values --- src/tests/FrontMatterConverter.test.ts | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/tests/FrontMatterConverter.test.ts b/src/tests/FrontMatterConverter.test.ts index 58fae68..b543810 100644 --- a/src/tests/FrontMatterConverter.test.ts +++ b/src/tests/FrontMatterConverter.test.ts @@ -125,6 +125,35 @@ updated: 2023-01-01 expect(result.value).toContain('updated: 2023-01-01'); } }); + + it('should format Date object correctly', () => { + const date = new Date('2023-01-01'); + const input = `--- +title: test +updated: ${date.toISOString()} +---`; + const result = convertFrontMatter(input, { + isEnableUpdateFrontmatterTimeOnEdit: true, + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('date: 2023-01-01'); + } + }); + + it('should handle non-string and non-Date values', () => { + const input = `--- +title: test +updated: 42 +---`; + const result = convertFrontMatter(input, { + isEnableUpdateFrontmatterTimeOnEdit: true, + }); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toContain('date: 42'); + } + }); }); describe('Author handling', () => { @@ -208,6 +237,17 @@ title: second expect((result.value as ConversionError).type).toBe('PARSE_ERROR'); } }); + + it('should handle front matter without end marker', () => { + const input = `--- +title: test +content without end marker`; + const result = convertFrontMatter(input); + expect(result._tag).toBe('Right'); + if (result._tag === 'Right') { + expect(result.value).toBe(input); + } + }); }); describe('Tags handling', () => { From 8db0d3f4b02e5165f556f4853b019183bbdf4541 Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 10:36:00 +0900 Subject: [PATCH 13/14] test: Add comprehensive tests for fp utilities - Add tests for pipe, compose, curry functions - Add tests for map, filter, reduce operations - Add tests for identity and constant functions - Achieve 100% code coverage for fp.ts --- src/tests/fp.test.ts | 141 ++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/src/tests/fp.test.ts b/src/tests/fp.test.ts index 7de12e4..71f454d 100644 --- a/src/tests/fp.test.ts +++ b/src/tests/fp.test.ts @@ -1,41 +1,82 @@ import { pipe, compose, + curry, map, filter, reduce, identity, constant, - curry, } from '../core/fp'; describe('Functional Programming Utilities', () => { + describe('pipe', () => { + it('should pipe a single value through multiple functions', () => { + const add2 = (x: number) => x + 2; + const multiply3 = (x: number) => x * 3; + const toString = (x: number) => x.toString(); + + const result = pipe(5, add2, multiply3, toString); + expect(result).toBe('21'); + }); + + it('should handle a single function', () => { + const add2 = (x: number) => x + 2; + const result = pipe(5, add2); + expect(result).toBe(7); + }); + + it('should handle no functions', () => { + const result = pipe(5); + expect(result).toBe(5); + }); + }); + describe('compose', () => { - it('should compose functions from right to left', () => { - const addOne = (x: number) => x + 1; - const double = (x: number) => x * 2; - const composed = compose(addOne, double); + it('should compose multiple functions from right to left', () => { + const add2 = (x: number) => x + 2; + const multiply3 = (x: number) => x * 3; + const composed = compose(add2, multiply3); + expect(composed(5)).toBe(17); // (5 * 3) + 2 + }); - // (5 * 2) + 1 = 11 - expect(composed(5)).toBe(11); + it('should handle a single function', () => { + const add2 = (x: number) => x + 2; + const composed = compose(add2); + expect(composed(5)).toBe(7); }); + }); - it('should handle single function', () => { - const addOne = (x: number) => x + 1; - const composed = compose(addOne); - expect(composed(5)).toBe(6); + describe('curry', () => { + it('should curry a function with multiple arguments', () => { + const add = (a: number, b: number, c: number) => a + b + c; + const curriedAdd = curry(add); + expect(curriedAdd(1)(2)(3)).toBe(6); + }); + + it('should handle partial application', () => { + const add = (a: number, b: number) => a + b; + const curriedAdd = curry(add); + const add5 = curriedAdd(5); + expect(add5(3)).toBe(8); + }); + + it('should handle single argument functions', () => { + const double = (x: number) => x * 2; + const curriedDouble = curry(double); + expect(curriedDouble(5)).toBe(10); }); }); describe('map', () => { - it('should transform array elements', () => { + it('should map a function over an array', () => { const double = (x: number) => x * 2; - const numbers = [1, 2, 3]; + const numbers = [1, 2, 3, 4]; const result = map(double)(numbers); - expect(result).toEqual([2, 4, 6]); + expect(result).toEqual([2, 4, 6, 8]); }); - it('should handle empty array', () => { + it('should handle empty arrays', () => { const double = (x: number) => x * 2; const result = map(double)([]); expect(result).toEqual([]); @@ -43,45 +84,33 @@ describe('Functional Programming Utilities', () => { }); describe('filter', () => { - it('should filter array elements', () => { + it('should filter array elements based on predicate', () => { const isEven = (x: number) => x % 2 === 0; - const numbers = [1, 2, 3, 4]; + const numbers = [1, 2, 3, 4, 5, 6]; const result = filter(isEven)(numbers); - expect(result).toEqual([2, 4]); + expect(result).toEqual([2, 4, 6]); }); - it('should handle empty array', () => { + it('should handle empty arrays', () => { const isEven = (x: number) => x % 2 === 0; const result = filter(isEven)([]); expect(result).toEqual([]); }); - - it('should handle array with no matches', () => { - const isEven = (x: number) => x % 2 === 0; - const result = filter(isEven)([1, 3, 5]); - expect(result).toEqual([]); - }); }); describe('reduce', () => { - it('should reduce array to single value', () => { + it('should reduce array using accumulator function', () => { const sum = (acc: number, x: number) => acc + x; const numbers = [1, 2, 3, 4]; const result = reduce(sum, 0)(numbers); expect(result).toBe(10); }); - it('should handle empty array', () => { + it('should handle empty arrays', () => { const sum = (acc: number, x: number) => acc + x; const result = reduce(sum, 0)([]); expect(result).toBe(0); }); - - it('should handle array with single element', () => { - const sum = (acc: number, x: number) => acc + x; - const result = reduce(sum, 0)([5]); - expect(result).toBe(5); - }); }); describe('identity', () => { @@ -95,45 +124,17 @@ describe('Functional Programming Utilities', () => { }); describe('constant', () => { - it('should return a function that always returns the same value', () => { + it('should create a function that always returns the same value', () => { const always5 = constant(5); expect(always5()).toBe(5); - expect((always5 as unknown as (x: unknown) => number)('anything')).toBe( - 5, - ); - expect((always5 as unknown as (x: unknown) => number)(123)).toBe(5); - }); - }); - - describe('curry', () => { - it('should curry a function with multiple arguments', () => { - const add = (a: number, b: number, c: number) => a + b + c; - const curriedAdd = curry(add); - - // All arguments at once - expect(curriedAdd(1)(2)(3)).toBe(6); - - // Step by step application - const add1 = curriedAdd(1); - const add1and2 = add1(2); - expect(add1and2(3)).toBe(6); - }); - - it('should handle functions with single argument', () => { - const double = (x: number) => x * 2; - const curriedDouble = curry(double); - expect(curriedDouble(5)).toBe(10); - }); - - it('should maintain function context', () => { - type GreetContext = { name: string }; - const greet = function (this: GreetContext, greeting: string) { - return `${greeting} ${this.name}`; - }; - const context = { name: 'World' }; - const boundGreet = greet.bind(context); - const curriedGreet = curry(boundGreet); - expect(curriedGreet('Hello')).toBe('Hello World'); + expect(always5()).toBe(5); + + const alwaysNull = constant(null); + expect(alwaysNull()).toBe(null); + + const obj = { a: 1 }; + const alwaysObj = constant(obj); + expect(alwaysObj()).toBe(obj); }); }); }); From c0e31a69a78c8373be73a6142ba3dca4ffe43d7d Mon Sep 17 00:00:00 2001 From: haril song Date: Sun, 30 Mar 2025 10:41:46 +0900 Subject: [PATCH 14/14] chore: Add prettier and format code - Add prettier dependency - Add prettier configuration - Add prettier scripts - Format fp.test.ts --- package-lock.json | 17 +++++++++++++++++ package.json | 5 ++++- src/tests/fp.test.ts | 4 ++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f74a4ac..d0fcb18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "eslint-plugin-yml": "^1.16.0", "jest": "^29.7.0", "obsidian": "^1.7.2", + "prettier": "^3.5.3", "process": "^0.11.10", "ts-jest": "^29.2.5", "tslib": "2.8.1", @@ -5013,6 +5014,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 2bd5814..afe00e0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json", - "test": "jest --coverage" + "test": "jest --coverage", + "prettier:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", + "prettier:write": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"" }, "keywords": [ "obsidian", @@ -29,6 +31,7 @@ "eslint-plugin-yml": "^1.16.0", "jest": "^29.7.0", "obsidian": "^1.7.2", + "prettier": "^3.5.3", "process": "^0.11.10", "ts-jest": "^29.2.5", "tslib": "2.8.1", diff --git a/src/tests/fp.test.ts b/src/tests/fp.test.ts index 71f454d..29bab95 100644 --- a/src/tests/fp.test.ts +++ b/src/tests/fp.test.ts @@ -128,10 +128,10 @@ describe('Functional Programming Utilities', () => { const always5 = constant(5); expect(always5()).toBe(5); expect(always5()).toBe(5); - + const alwaysNull = constant(null); expect(alwaysNull()).toBe(null); - + const obj = { a: 1 }; const alwaysObj = constant(obj); expect(alwaysObj()).toBe(obj);