Repository: JoshuaKGoldberg/create-typescript-app (ts)

Issue:
🐛 Bug: README.md h2 sections between badges and Usage shouldn't be removed
### Bug Report Checklist

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have pulled the latest `main` branch of the repository.
- [x] I have [searched for related issues](https://github.com/JoshuaKGoldberg/create-typescript-app/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

If a repo's README.md has an `##` h2 between its badges and `## Usage`, it shouldn't be removed. It should be preserved.

### Actual

https://github.com/JoshuaKGoldberg/tidelift-me-up/pull/684/files#r2027035918 shows one that is deleted.

### Additional Info

🎁 

Pull request description:
fix: allow README.md explainer to be a full section
## PR Checklist

- [x] Addresses an existing open issue: fixes #2127
- [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

Moves the `explainer` option into `documentation.readme` so it sits alongside `additional`.

🎁 

Patch:
diff --git a/docs/Configuration Files.md b/docs/Configuration Files.md
index c184283..1504edf 100644
--- a/docs/Configuration Files.md
+++ b/docs/Configuration Files.md
@@ -24,7 +24,6 @@ These options are generally only programmatically used internally, but can still
 | `contributors`      | AllContributors contributors to store in `.all-contributorsrc`           | Existing contributors in the file, or just your username  |
 | `documentation`     | additional docs to add to `.github/DEVELOPMENT.md` and/or `README.md`    | Extra content in those two files                          |
 | `existingLabels`    | existing labels to switch to the standard template labels                | Existing labels on the repository from the GitHub API     |
-| `explainer`         | additional `README.md` sentence(s) describing the package                | Extra content in `README.md` after badges and description |
 | `guide`             | link to a contribution guide to place at the top of development docs     | Block quote on top of `.github/DEVELOPMENT.md`            |
 | `logo`              | local image file and alt text to display near the top of the `README.md` | First non-badge image's `alt` and `src` in `README.md`    |
 | `node`              | Node.js engine version(s) to pin and require a minimum of                | Values from `.nvmrc` and `package.json`'s `"engines"`     |
diff --git a/src/base.ts b/src/base.ts
index 6d761b0..b35dc66 100644
--- a/src/base.ts
+++ b/src/base.ts
@@ -17,7 +17,6 @@ import { readEmailFromNpm } from "./options/readEmailFromNpm.js";
 import { readEmails } from "./options/readEmails.js";
 import { readEmoji } from "./options/readEmoji.js";
 import { readExistingLabels } from "./options/readExistingLabels.js";
-import { readExplainer } from "./options/readExplainer.js";
 import { readFileSafe } from "./options/readFileSafe.js";
 import { readFunding } from "./options/readFunding.js";
 import { readGitDefaults } from "./options/readGitDefaults.js";
@@ -31,6 +30,7 @@ import { readPackageAuthor } from "./options/readPackageAuthor.js";
 import { readPackageData } from "./options/readPackageData.js";
 import { readPnpm } from "./options/readPnpm.js";
 import { readReadmeAdditional } from "./options/readReadmeAdditional.js";
+import { readReadmeExplainer } from "./options/readReadmeExplainer.js";
 import { readReadmeUsage } from "./options/readReadmeUsage.js";
 import { readRepository } from "./options/readRepository.js";
 import { readRulesetId } from "./options/readRulesetId.js";
@@ -94,10 +94,6 @@ export const base = createBase({
 			)
 			.optional()
 			.describe("existing labels from the GitHub repository"),
-		explainer: z
-			.array(z.string())
-			.optional()
-			.describe("additional README.md sentence(s) describing the package"),
 		funding: z
 			.string()
 			.optional()
@@ -200,6 +196,7 @@ export const base = createBase({
 				await readDocumentation(
 					getDevelopmentDocumentation,
 					getReadmeAdditional,
+					getReadmeExplainer,
 					getReadmeUsage,
 				),
 		);
@@ -232,8 +229,6 @@ export const base = createBase({
 			async () => await readExistingLabels(take, getOwner, getRepository),
 		);
 
-		const getExplainer = lazyValue(async () => await readExplainer(getReadme));
-
 		const getFunding = lazyValue(async () => await readFunding(take));
 
 		const getGitDefaults = lazyValue(async () => await readGitDefaults(take));
@@ -282,6 +277,10 @@ export const base = createBase({
 			async () => await readReadmeAdditional(getReadme),
 		);
 
+		const getReadmeExplainer = lazyValue(
+			async () => await readReadmeExplainer(getReadme),
+		);
+
 		const getReadmeUsage = lazyValue(
 			async () => await readReadmeUsage(getEmoji, getReadme, getRepository),
 		);
@@ -318,7 +317,6 @@ export const base = createBase({
 			email: getEmail,
 			emoji: getEmoji,
 			existingLabels: getExistingLabels,
-			explainer: getExplainer,
 			funding: getFunding,
 			guide: getGuide,
 			keywords: getKeywords,
diff --git a/src/blocks/blockREADME.ts b/src/blocks/blockREADME.ts
index fd29bfd..b030095 100644
--- a/src/blocks/blockREADME.ts
+++ b/src/blocks/blockREADME.ts
@@ -36,7 +36,8 @@ export const blockREADME = base.createBlock({
 		const { badges, notices, sections } = addons;
 
 		const explainer =
-			options.explainer && `\n${options.explainer.join("\n")}\n`;
+			options.documentation.readme.explainer &&
+			`\n${options.documentation.readme.explainer}\n`;
 
 		const logo =
 			options.logo &&
diff --git a/src/options/readDocumentation.ts b/src/options/readDocumentation.ts
index b3802fa..fc1eb70 100644
--- a/src/options/readDocumentation.ts
+++ b/src/options/readDocumentation.ts
@@ -3,10 +3,12 @@ import { Documentation } from "../schemas.js";
 export async function readDocumentation(
 	getDevelopmentDocumentation: () => Promise<string | undefined>,
 	getReadmeAdditional: () => Promise<string | undefined>,
+	getReadmeExplainer: () => Promise<string | undefined>,
 	getReadmeUsage: () => Promise<string>,
 ): Promise<Documentation> {
-	const [additional, development, usage] = await Promise.all([
+	const [additional, explainer, development, usage] = await Promise.all([
 		getReadmeAdditional(),
+		getReadmeExplainer(),
 		getDevelopmentDocumentation(),
 		getReadmeUsage(),
 	]);
@@ -15,6 +17,7 @@ export async function readDocumentation(
 		development,
 		readme: {
 			additional,
+			explainer,
 			usage,
 		},
 	};
diff --git a/src/options/readExplainer.ts b/src/options/readExplainer.ts
deleted file mode 100644
index 57d5343..0000000
--- a/src/options/readExplainer.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-export async function readExplainer(getReadme: () => Promise<string>) {
-	const readme = await getReadme();
-
-	const indexOfH2 = readme.indexOf("##");
-	if (indexOfH2 === -1) {
-		return undefined;
-	}
-
-	const lastTag = readme.slice(0, indexOfH2).lastIndexOf(">");
-
-	return readme
-		.slice(lastTag + 1, indexOfH2)
-		.split("\n")
-		.map((line) => line.trim())
-		.filter(Boolean);
-}
diff --git a/src/options/readReadmeExplainer.ts b/src/options/readReadmeExplainer.ts
new file mode 100644
index 0000000..aff6a33
--- /dev/null
+++ b/src/options/readReadmeExplainer.ts
@@ -0,0 +1,36 @@
+export async function readReadmeExplainer(getReadme: () => Promise<string>) {
+	const readme = await getReadme();
+
+	const indexOfUsageH2 = /## Usage/.exec(readme)?.index ?? readme.indexOf("##");
+	if (indexOfUsageH2 === -1) {
+		return undefined;
+	}
+
+	const beforeUsageH2 = readme.slice(0, indexOfUsageH2);
+
+	const [indexOfLastTag, lastTagMatcher] = lastLastIndexOf(beforeUsageH2, [
+		`">`,
+		"/p>",
+		"/>",
+	]);
+	if (!lastTagMatcher) {
+		return undefined;
+	}
+
+	return readme
+		.slice(indexOfLastTag + lastTagMatcher.length, indexOfUsageH2)
+		.trim();
+}
+
+function lastLastIndexOf(text: string, matchers: string[]) {
+	let pair: [number, string | undefined] = [-1, undefined];
+
+	for (const matcher of matchers) {
+		const indexOf = text.lastIndexOf(matcher);
+		if (indexOf > pair[0]) {
+			pair = [indexOf, matcher];
+		}
+	}
+
+	return pair;
+}
diff --git a/src/schemas.ts b/src/schemas.ts
index e8804bb..508ba0c 100644
--- a/src/schemas.ts
+++ b/src/schemas.ts
@@ -12,6 +12,7 @@ export type Contributor = z.infer<typeof zContributor>;
 
 export const zReadme = z.object({
 	additional: z.string().optional(),
+	explainer: z.string().optional(),
 	usage: z.string(),
 });
 


Test patch:
diff --git a/src/base.test.ts b/src/base.test.ts
index 4b092a8..c6154a0 100644
--- a/src/base.test.ts
+++ b/src/base.test.ts
@@ -24,6 +24,10 @@ describe("base", () => {
 				development: expect.any(String),
 				readme: {
 					additional: expect.any(String),
+					explainer: [
+						`\`create-typescript-app\` is a one-stop-shop solution to set up a new or existing repository with the latest and greatest TypeScript tooling.`,
+						`It includes options not just for building and testing but also automated release management, contributor recognition, GitHub repository settings, and more.`,
+					].join("\n"),
 					usage: expect.any(String),
 				},
 			},
@@ -33,10 +37,6 @@ describe("base", () => {
 			},
 			emoji: "🎁",
 			existingLabels: expect.any(Array),
-			explainer: [
-				`\`create-typescript-app\` is a one-stop-shop solution to set up a new or existing repository with the latest and greatest TypeScript tooling.`,
-				`It includes options not just for building and testing but also automated release management, contributor recognition, GitHub repository settings, and more.`,
-			],
 			funding: "JoshuaKGoldberg",
 			guide: {
 				href: "https://www.joshuakgoldberg.com/blog/contributing-to-a-create-typescript-app-repository",
diff --git a/src/blocks/blockREADME.test.ts b/src/blocks/blockREADME.test.ts
index 3cc7b48..ad38c95 100644
--- a/src/blocks/blockREADME.test.ts
+++ b/src/blocks/blockREADME.test.ts
@@ -121,7 +121,13 @@ describe("blockREADME", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
 				...optionsBase,
-				explainer: ["And a one.", "And a two."],
+				documentation: {
+					...optionsBase.documentation,
+					readme: {
+						...optionsBase.documentation.readme,
+						explainer: "\nAnd a one.\nAnd a two.\n",
+					},
+				},
 			},
 		});
 
@@ -136,9 +142,11 @@ describe("blockREADME", () => {
 				<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
 			</p>
 
+
 			And a one.
 			And a two.
 
+
 			## Usage
 
 			Test usage.
@@ -238,7 +246,13 @@ describe("blockREADME", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
 				...optionsBase,
-				explainer: ["And a one.", "And a two."],
+				documentation: {
+					...optionsBase.documentation,
+					readme: {
+						...optionsBase.documentation.readme,
+						explainer: "And a one.\nAnd a two.",
+					},
+				},
 				logo: {
 					alt: "My logo",
 					height: 100,
diff --git a/src/options/readExplainer.test.ts b/src/options/readExplainer.test.ts
deleted file mode 100644
index d69c229..0000000
--- a/src/options/readExplainer.test.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { readExplainer } from "./readExplainer.js";
-
-describe(readExplainer, () => {
-	it("defaults to undefined when it cannot be found", async () => {
-		const actual = await readExplainer(() => Promise.resolve(`nothing.`));
-
-		expect(actual).toBeUndefined();
-	});
-
-	it("parses a line after badges", async () => {
-		const actual = await readExplainer(() =>
-			Promise.resolve(`
-</p>
-
-This is my project.
-
-## Usage
-					.`),
-		);
-
-		expect(actual).toEqual(["This is my project."]);
-	});
-
-	it("parses multiple lines after badges", async () => {
-		const actual = await readExplainer(() =>
-			Promise.resolve(`
-</p>
-
-This is my project.
-It is good.
-
-## Usage
-					.`),
-		);
-
-		expect(actual).toEqual(["This is my project.", "It is good."]);
-	});
-
-	it("parses multiple lines after full badges and a logo", async () => {
-		const actual = await readExplainer(() =>
-			Promise.resolve(`
-<p align="center">
-	<!-- prettier-ignore-start -->
-	<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
-	<a href="#contributors" target="_blank"><img alt="👪 All Contributors: 52" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-52-21bb42.svg" /></a>
-<!-- ALL-CONTRIBUTORS-BADGE:END -->
-	<!-- prettier-ignore-end -->
-	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="🤝 Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a>
-	<a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="🧪 Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a>
-	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="📝 License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a>
-	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
-	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
-</p>
-
-<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128">
-
-This is my project.
-It is good.
-
-## Usage
-					.`),
-		);
-
-		expect(actual).toEqual(["This is my project.", "It is good."]);
-	});
-});
diff --git a/src/options/readReadmeExplainer.test.ts b/src/options/readReadmeExplainer.test.ts
new file mode 100644
index 0000000..d545d13
--- /dev/null
+++ b/src/options/readReadmeExplainer.test.ts
@@ -0,0 +1,185 @@
+import { describe, expect, it } from "vitest";
+
+import { readReadmeExplainer } from "./readReadmeExplainer.js";
+
+describe(readReadmeExplainer, () => {
+	it("resolves with undefined when an h2 cannot be found", async () => {
+		const actual = await readReadmeExplainer(() => Promise.resolve(`nothing.`));
+
+		expect(actual).toBeUndefined();
+	});
+
+	it("resolves with undefined before h2 when a Usage h2 exists and there are no preceding tags", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`# Title
+			
+## Usage`),
+		);
+
+		expect(actual).toBeUndefined();
+	});
+
+	it("resolves with undefined before h2 when a non-Usage h2 exists and there are no preceding tags", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`# Title
+			
+## What?`),
+		);
+
+		expect(actual).toBeUndefined();
+	});
+
+	it("parses a line after badges", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+</p>
+
+This is my project.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual("This is my project.");
+	});
+
+	it("parses multiple lines after badges", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+</p>
+
+This is my project.
+It is good.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual("This is my project.\nIt is good.");
+	});
+
+	it("parses multiple lines after full badges and a logo", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+<p align="center">
+	<!-- prettier-ignore-start -->
+	<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
+	<a href="#contributors" target="_blank"><img alt="👪 All Contributors: 52" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-52-21bb42.svg" /></a>
+<!-- ALL-CONTRIBUTORS-BADGE:END -->
+	<!-- prettier-ignore-end -->
+	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="🤝 Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a>
+	<a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="🧪 Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a>
+	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="📝 License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a>
+	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
+	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+</p>
+
+<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128">
+
+This is my project.
+It is good.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual("This is my project.\nIt is good.");
+	});
+
+	it("parses a non-Usage h2 after full badges", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+<p align="center">
+	<!-- prettier-ignore-start -->
+	<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
+	<a href="#contributors" target="_blank"><img alt="👪 All Contributors: 52" src="https://img.shields.io/badge/%F0%9F%91%AA_all_contributors-52-21bb42.svg" /></a>
+<!-- ALL-CONTRIBUTORS-BADGE:END -->
+	<!-- prettier-ignore-end -->
+	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="🤝 Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a>
+	<a href="https://codecov.io/gh/JoshuaKGoldberg/create-typescript-app" target="_blank"><img alt="🧪 Coverage" src="https://img.shields.io/codecov/c/github/JoshuaKGoldberg/create-typescript-app?label=%F0%9F%A7%AA%20coverage" /></a>
+	<a href="https://github.com/JoshuaKGoldberg/create-typescript-app/blob/main/LICENSE.md" target="_blank"><img alt="📝 License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg"></a>
+	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
+	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+</p>
+
+## What?
+
+This is my project.
+It is good.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual("## What?\n\nThis is my project.\nIt is good.");
+	});
+
+	it("parses a non-Usage h2 with a block quote after full badges", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
+	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+</p>
+
+## What?
+
+This is my project.
+It is good.
+
+> See here.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual(
+			"## What?\n\nThis is my project.\nIt is good.\n\n> See here.",
+		);
+	});
+
+	it("parses a non-Usage h2 after full badges and a logo", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
+	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+</p>
+
+<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128">
+
+## What?
+
+This is my project.
+It is good.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual("## What?\n\nThis is my project.\nIt is good.");
+	});
+
+	it("parses a non-Usage h2 with a block quote after full badges and a logo", async () => {
+		const actual = await readReadmeExplainer(() =>
+			Promise.resolve(`
+	<a href="http://npmjs.com/package/create-typescript-app"><img alt="📦 npm version" src="https://img.shields.io/npm/v/create-typescript-app?color=21bb42&label=%F0%9F%93%A6%20npm" /></a>
+	<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+</p>
+
+<img align="right" alt="Project logo: the TypeScript blue square with rounded corners, but a plus sign instead of 'TS'" height="128" src="./docs/create-typescript-app.png" width="128">
+
+## What?
+
+This is my project.
+It is good.
+
+> See here.
+
+## Usage
+					.`),
+		);
+
+		expect(actual).toEqual(
+			"## What?\n\nThis is my project.\nIt is good.\n\n> See here.",
+		);
+	});
+});