Skip to content

Commit 20c44e9

Browse files
fix: splice badges in readme during migration (#847)
## PR Checklist - [x] Addresses an existing open issue: fixes #751 - [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22) - [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CONTRIBUTING.md) were taken ## Overview - [x] Detecting existing badges - [x] Splicing new badges alongside existing equivalents and other badges - [x] Inject into an existing `<p>...</p>` instead of prepending a new one `writeReadme`'s logic is complex enough now that I gave it its own `writeReadme/index.ts`. Verified with unit tests in the PR as well as running JoshuaKGoldberg/all-contributors-auto-action#68 locally.
1 parent 02f819d commit 20c44e9

11 files changed

+594
-142
lines changed

src/create/createWithOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { addToolAllContributors } from "../steps/addToolAllContributors.js";
88
import { finalizeDependencies } from "../steps/finalizeDependencies.js";
99
import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/index.js";
1010
import { runCommands } from "../steps/runCommands.js";
11-
import { writeReadme } from "../steps/writeReadme.js";
11+
import { writeReadme } from "../steps/writeReadme/index.js";
1212
import { writeStructure } from "../steps/writing/writeStructure.js";
1313

1414
export async function createWithOptions({ github, options }: GitHubAndOptions) {

src/migrate/migrateWithOptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { initializeGitHubRepository } from "../steps/initializeGitHubRepository/
77
import { runCommands } from "../steps/runCommands.js";
88
import { updateAllContributorsTable } from "../steps/updateAllContributorsTable.js";
99
import { updateLocalFiles } from "../steps/updateLocalFiles.js";
10-
import { writeReadme } from "../steps/writeReadme.js";
10+
import { writeReadme } from "../steps/writeReadme/index.js";
1111
import { writeStructure } from "../steps/writing/writeStructure.js";
1212

1313
export async function migrateWithOptions({

src/steps/writeReadme.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { describe, expect, it, test } from "vitest";
2+
3+
import { findExistingBadges } from "./findExistingBadges.js";
4+
5+
describe("findExistingBadges", () => {
6+
describe("no result cases", () => {
7+
test.each([
8+
"",
9+
"abc123",
10+
"# Test Title",
11+
"[]",
12+
"[][]",
13+
"[]()",
14+
"[][]()()",
15+
`<img />`,
16+
])("%j", (input) => {
17+
expect(findExistingBadges(input)).toEqual([]);
18+
});
19+
});
20+
21+
describe("single result cases", () => {
22+
test.each([
23+
`[![GitHub CI](https://github.com/JoshuaKGoldberg/console-fail-test/actions/workflows/compile.yml/badge.svg)](https://github.com/JoshuaKGoldberg/console-fail-test/actions/workflows/compile.yml)`,
24+
`[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io)`,
25+
`![TypeScript: Strict](https://img.shields.io/badge/typescript-strict-brightgreen.svg)`,
26+
`[![NPM version](https://badge.fury.io/js/console-fail-test.svg)](http://badge.fury.io/js/console-fail-test)`,
27+
`[![Downloads](http://img.shields.io/npm/dm/console-fail-test.svg)](https://npmjs.org/package/console-fail-test)`,
28+
"<a>badge</a>",
29+
"<a >badge</a>",
30+
"<a \t>badge</a>",
31+
"<a href='abc'>badge</a>",
32+
` <a href="#contributors" target="_blank">
33+
<!-- prettier-ignore-start -->
34+
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
35+
<img alt="All Contributors: 1" src="https://img.shields.io/badge/all_contributors-1-21bb42.svg" />
36+
<!-- ALL-CONTRIBUTORS-BADGE:END -->
37+
<!-- prettier-ignore-end -->
38+
</a>`,
39+
` <a href="https://codecov.io/gh/JoshuaKGoldberg/all-contributors-auto-action" target="_blank">
40+
<img alt="Codecov Test Coverage" src="https://codecov.io/gh/JoshuaKGoldberg/all-contributors-auto-action/branch/main/graph/badge.svg"/>
41+
</a>`,
42+
` <a href="https://github.com/JoshuaKGoldberg/all-contributors-auto-action/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank">
43+
<img alt="Contributor Covenant" src="https://img.shields.io/badge/code_of_conduct-enforced-21bb42" />
44+
</a>`,
45+
`
46+
<a href="https://github.com/JoshuaKGoldberg/all-contributors-auto-action/blob/main/LICENSE.md" target="_blank">
47+
<img alt="License: MIT" src="https://img.shields.io/github/license/JoshuaKGoldberg/all-contributors-auto-action?color=21bb42">
48+
</a>`,
49+
`
50+
<a href="https://github.com/sponsors/JoshuaKGoldberg" target="_blank">
51+
<img alt="Sponsor: On GitHub" src="https://img.shields.io/badge/sponsor-on_github-21bb42.svg" />
52+
</a>`,
53+
`<img alt="Style: Prettier" src="https://img.shields.io/badge/style-prettier-21bb42.svg" />`,
54+
`<img alt="TypeScript: Strict" src="https://img.shields.io/badge/typescript-strict-21bb42.svg" />`,
55+
])("%s", (contents) => {
56+
expect(findExistingBadges(contents)).toEqual([contents.trim()]);
57+
});
58+
});
59+
60+
it("doesn't collect badges after a ##", () => {
61+
expect(
62+
findExistingBadges(`
63+
<img alt="test badge a" src="test-a.jpg" />
64+
65+
## Usage
66+
67+
<img alt="test badge b" src="test-b.jpg" />
68+
`),
69+
).toEqual([`<img alt="test badge a" src="test-a.jpg" />`]);
70+
});
71+
72+
it("doesn't collect badges after an h2", () => {
73+
expect(
74+
findExistingBadges(`
75+
<img alt="test badge a" src="test-a.jpg" />
76+
77+
<h2 align="left">Usage</h2>
78+
79+
<img alt="test badge b" src="test-b.jpg" />
80+
`),
81+
).toEqual([`<img alt="test badge a" src="test-a.jpg" />`]);
82+
});
83+
84+
test("real-world usage", () => {
85+
expect(
86+
findExistingBadges(`
87+
<p align="center">
88+
<a href="#contributors" target="_blank">
89+
<!-- prettier-ignore-start -->
90+
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
91+
<img alt="All Contributors: 1" src="https://img.shields.io/badge/all_contributors-1-21bb42.svg" />
92+
<!-- ALL-CONTRIBUTORS-BADGE:END -->
93+
<!-- prettier-ignore-end -->
94+
</a>
95+
<a href="https://codecov.io/gh/JoshuaKGoldberg/all-contributors-auto-action" target="_blank">
96+
<img alt="Codecov Test Coverage" src="https://codecov.io/gh/JoshuaKGoldberg/all-contributors-auto-action/branch/main/graph/badge.svg?token=eVIFY4MhfQ"/>
97+
</a>
98+
<a href="https://github.com/JoshuaKGoldberg/all-contributors-auto-action/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank">
99+
<img alt="Contributor Covenant" src="https://img.shields.io/badge/code_of_conduct-enforced-21bb42" />
100+
</a>
101+
<a href="https://github.com/JoshuaKGoldberg/all-contributors-auto-action/blob/main/LICENSE.md" target="_blank">
102+
<img alt="License: MIT" src="https://img.shields.io/github/license/JoshuaKGoldberg/all-contributors-auto-action?color=21bb42">
103+
</a>
104+
<a href="https://github.com/sponsors/JoshuaKGoldberg" target="_blank">
105+
<img alt="Sponsor: On GitHub" src="https://img.shields.io/badge/sponsor-on_github-21bb42.svg" />
106+
</a>
107+
<img alt="Style: Prettier" src="https://img.shields.io/badge/style-prettier-21bb42.svg" />
108+
<img alt="TypeScript: Strict" src="https://img.shields.io/badge/typescript-strict-21bb42.svg" />
109+
</p>
110+
`),
111+
).toMatchInlineSnapshot(`
112+
[
113+
"<a href=\\"#contributors\\" target=\\"_blank\\">
114+
<!-- prettier-ignore-start -->
115+
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
116+
<img alt=\\"All Contributors: 1\\" src=\\"https://img.shields.io/badge/all_contributors-1-21bb42.svg\\" />
117+
<!-- ALL-CONTRIBUTORS-BADGE:END -->
118+
<!-- prettier-ignore-end -->
119+
</a>",
120+
"<a href=\\"https://github.com/JoshuaKGoldberg/all-contributors-auto-action/blob/main/LICENSE.md\\" target=\\"_blank\\">
121+
<img alt=\\"License: MIT\\" src=\\"https://img.shields.io/github/license/JoshuaKGoldberg/all-contributors-auto-action?color=21bb42\\">
122+
</a>",
123+
"<img alt=\\"Codecov Test Coverage\\" src=\\"https://codecov.io/gh/JoshuaKGoldberg/all-contributors-auto-action/branch/main/graph/badge.svg?token=eVIFY4MhfQ\\"/>",
124+
"<img alt=\\"Sponsor: On GitHub\\" src=\\"https://img.shields.io/badge/sponsor-on_github-21bb42.svg\\" />",
125+
"<img alt=\\"TypeScript: Strict\\" src=\\"https://img.shields.io/badge/typescript-strict-21bb42.svg\\" />",
126+
]
127+
`);
128+
});
129+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const existingBadgeMatcherCreators = [
2+
() => /\[!\[.+\]\(.+\)\]\(.+\)/g,
3+
() => /!\[.+\]\(.+\)/g,
4+
() => /^\s*<a[ \tA-Za-z_\-=#&;?./:'"]*>[\s\S]+?<\/a>/gm,
5+
() => /<img.+src.+\/>/g,
6+
];
7+
8+
export function findExistingBadges(contents: string): string[] {
9+
const badges: string[] = [];
10+
let remaining = contents.split(/<\s*h2.*>|##/)[0];
11+
12+
for (const createMatcher of existingBadgeMatcherCreators) {
13+
const matcher = createMatcher();
14+
15+
while (true) {
16+
const matched = matcher.exec(remaining);
17+
18+
if (!matched) {
19+
break;
20+
}
21+
22+
const [badge] = matched;
23+
24+
badges.push(badge.trim());
25+
remaining = [
26+
remaining.slice(0, matched.index),
27+
remaining.slice(matched.index + badge.length),
28+
].join("");
29+
}
30+
}
31+
32+
return badges;
33+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { findIntroSectionClose } from "./findIntroSectionClose.js";
4+
5+
describe("findIntroSectionClose", () => {
6+
it.each([
7+
[
8+
`# First
9+
## Second`,
10+
6,
11+
],
12+
[
13+
`# First
14+
<h2>Second</h2>`,
15+
6,
16+
],
17+
[
18+
`# First
19+
<h2>Second</h2>`,
20+
6,
21+
],
22+
[
23+
`# First
24+
\`\`\`js
25+
...
26+
\`\`\``,
27+
6,
28+
],
29+
[
30+
`# First
31+
32+
[![](https://img.shields.io/badge/abc-ffcc00.svg)](image.jpg)
33+
34+
[![](https://img.shields.io/badge/abc-ffcc00.svg)](image.jpg)
35+
`,
36+
135,
37+
],
38+
[
39+
`Plain heading
40+
41+
Next line.
42+
`,
43+
13,
44+
],
45+
])("%s", (contents, expected) => {
46+
expect(findIntroSectionClose(contents)).toEqual(expected);
47+
});
48+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { existingBadgeMatcherCreators } from "./findExistingBadges.js";
2+
3+
export function findIntroSectionClose(contents: string) {
4+
// Highest priority match: an h2, presumably following badges
5+
const indexOfH2OrCodeBlock = contents.search(/## |<\s*h2|```/);
6+
7+
if (indexOfH2OrCodeBlock !== -1) {
8+
return indexOfH2OrCodeBlock - 2;
9+
}
10+
11+
// Failing that, if any badges are found, go after the last of them
12+
for (const createMatcher of existingBadgeMatcherCreators) {
13+
const lastMatch = [...contents.matchAll(createMatcher())].at(-1);
14+
15+
if (lastMatch?.index) {
16+
return lastMatch.index + lastMatch[0].length + 2;
17+
}
18+
}
19+
20+
// Lastly, go for the second line altogether
21+
return contents.indexOf("\n", 2);
22+
}

0 commit comments

Comments
 (0)