diff --git a/.oxlintrc.json b/.oxlintrc.json index 3e0b31f..a639dd3 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,7 +8,11 @@ } }, "rules": { + "complexity": ["error", { "max": 10 }], "import/no-cycle": "error", + "max-depth": ["error", { "max": 2 }], + "max-lines": ["error", { "max": 500 }], + "max-params": ["error", { "max": 3 }], "typescript/await-thenable": "error", "typescript/no-floating-promises": "error", "typescript/no-misused-promises": "error" diff --git a/src/data/article.ts b/src/data/article.ts index 7653dbd..98e9b5e 100644 --- a/src/data/article.ts +++ b/src/data/article.ts @@ -137,27 +137,27 @@ function transformOutsideFences(markdown: string, transform: (prose: string) => return restoreFencedBlocks(transform(protectedMarkdown), fencedBlocks); } +type Fence = { character: string; length: number; lines: string[] }; + function protectFencedBlocks(markdown: string, fencedBlocks: string[]): string { const lines = markdown.split("\n"); const protectedLines: string[] = []; - let fence: { character: string; length: number; lines: string[] } | undefined; + let fence: Fence | undefined; for (const line of lines) { - const marker = /^(?:\s*)(`{3,}|~{3,})/u.exec(line)?.[1]; - if (fence === undefined && marker !== undefined) { + const marker = fence === undefined ? /^(?:\s*)(`{3,}|~{3,})/u.exec(line)?.[1] : undefined; + if (marker !== undefined) { fence = { character: marker[0], length: marker.length, lines: [line] }; continue; } - if (fence !== undefined) { - fence.lines.push(line); - const closing = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/u.exec(line)?.[1]; - if (closing?.[0] === fence.character && closing.length >= fence.length) { - protectedLines.push(storeFencedBlock(fence.lines, fencedBlocks)); - fence = undefined; - } + if (fence === undefined) { + protectedLines.push(line); continue; } - protectedLines.push(line); + fence.lines.push(line); + if (!closesFence(line, fence)) continue; + protectedLines.push(storeFencedBlock(fence.lines, fencedBlocks)); + fence = undefined; } if (fence !== undefined) { @@ -166,6 +166,11 @@ function protectFencedBlocks(markdown: string, fencedBlocks: string[]): string { return protectedLines.join("\n"); } +function closesFence(line: string, fence: Fence): boolean { + const closing = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/u.exec(line)?.[1]; + return closing?.[0] === fence.character && closing.length >= fence.length; +} + function storeFencedBlock(lines: string[], fencedBlocks: string[]): string { const index = fencedBlocks.push(lines.join("\n")) - 1; return `@@FUREDEA_FENCED_BLOCK_${index}@@`; @@ -180,7 +185,8 @@ function restoreFencedBlocks(markdown: string, fencedBlocks: string[]): string { function convertCaptions(markdown: string): string { return markdown.replace( /^!\[([^\]]*)\]\(([^)\s]+)\)\n([_*])(.+)\3$/gmu, - (_match, alt: string, url: string, _delimiter: string, caption: string) => { + (_match, ...captures: string[]) => { + const [alt, url, , caption] = captures; return [ "
", `${escapeHtml(alt)}`, diff --git a/src/integrations/esa.test.ts b/src/integrations/esa.test.ts index 48cea43..101621a 100644 --- a/src/integrations/esa.test.ts +++ b/src/integrations/esa.test.ts @@ -215,6 +215,40 @@ test.each([0, -1, 1, 1.5, "2"])( }, ); +test.each([ + ["name", 1], + ["body_md", null], + ["tags", [1]], + ["category", 1], + ["wip", "false"], +])("refuses to publish when a searched post has an invalid %s", async (field, value) => { + const methods: string[] = []; + const fetcher: typeof fetch = async (input, options) => { + methods.push(new Request(input, options).method); + return Response.json({ + posts: [ + { + number: 42, + url: "https://example-team.esa.io/posts/42", + ...PAYLOAD.post, + [field]: value, + }, + ], + }); + }; + + await expect( + upsertEsaPost({ + team: "example-team", + accessToken: ACCESS_TOKEN, + canonicalUrl: "https://furedea.com/ja/blog/article-publishing/", + payload: PAYLOAD, + fetcher, + }), + ).rejects.toThrow("esa API returned an invalid post search response."); + expect(methods.filter((method) => method !== "GET")).toEqual([]); +}); + test("does not publish when a later search page fails", async () => { const methods: string[] = []; const fetcher: typeof fetch = async (input, options) => { diff --git a/src/integrations/esa.ts b/src/integrations/esa.ts index 9eda65b..9efa31f 100644 --- a/src/integrations/esa.ts +++ b/src/integrations/esa.ts @@ -173,12 +173,20 @@ function isEsaPostSearchResult(value: unknown): value is EsaPostSearchResult { } function isEsaPostSummary(value: unknown): value is EsaPostSummary { + return isEsaPostResult(value) && hasEsaPostContent(value) && hasEsaPostMetadata(value); +} + +function hasEsaPostContent(value: object): boolean { return ( - isEsaPostResult(value) && "name" in value && typeof value.name === "string" && "body_md" in value && - typeof value.body_md === "string" && + typeof value.body_md === "string" + ); +} + +function hasEsaPostMetadata(value: object): boolean { + return ( "tags" in value && Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string") && diff --git a/tests/performance/performance_report.ts b/tests/performance/performance_report.ts index 706036f..e881a09 100644 --- a/tests/performance/performance_report.ts +++ b/tests/performance/performance_report.ts @@ -68,8 +68,7 @@ function renderRow(entry: PerformanceReportEntry): string { (value) => value.toFixed(3), ), unitWithRange( - metrics.blockingTime, - samples.map(({ blockingTime }) => blockingTime), + { median: metrics.blockingTime, samples: samples.map(({ blockingTime }) => blockingTime) }, "ms", Math.round, ), @@ -89,12 +88,11 @@ function seconds(milliseconds: number): string { } function secondsWithRange(median: number, samples: number[]): string { - return unitWithRange(median, samples, "s", seconds); + return unitWithRange({ median, samples }, "s", seconds); } function unitWithRange( - median: number, - samples: number[], + { median, samples }: { median: number; samples: number[] }, unit: string, format: (value: number) => string | number, ): string {