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

Issue:
🚀 Feature: Add options.explainer to blockREADME
### Bug Report Checklist

- [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.

### Overview

This repo has a couple lines of text in its `README.md` after the logo and before the `## Usage` heading. That should be an option for the template.

### Additional Info

Prototyped in #1839.

💖 

Pull request:
feat: add options.explainer for blockREADME
## PR Checklist

- [x] Addresses an existing open issue: fixes #1878
- [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

Also touches up some spacing and tests in `readDefaultsFromReadme`, while I'm in the area.

💖 

Patch:
diff --git a/src/next/base.ts b/src/next/base.ts
index cbab6de..11e592f 100644
--- a/src/next/base.ts
+++ b/src/next/base.ts
@@ -72,6 +72,10 @@ export const base = createBase({
 			.describe(
 				"email address to be listed as the point of contact in docs and packages",
 			),
+		explainer: z
+			.array(z.string())
+			.optional()
+			.describe("additional README.md sentence(s) describing the package"),
 		funding: z
 			.string()
 			.optional()
diff --git a/src/next/blocks/blockREADME.ts b/src/next/blocks/blockREADME.ts
index 8487363..70f3c9c 100644
--- a/src/next/blocks/blockREADME.ts
+++ b/src/next/blocks/blockREADME.ts
@@ -5,6 +5,7 @@ import { base } from "../base.js";
 function printAttributes(attributes: Record<string, number | string>) {
 	return Object.entries(attributes)
 		.map(([key, value]) => `${key}="${value}"`)
+		.sort()
 		.join(" ");
 }
 
@@ -20,9 +21,12 @@ export const blockREADME = base.createBlock({
 	produce({ addons, options }) {
 		const { badges, notices, sections } = addons;
 
-		const logo = options.logo
-			? `\n<img ${printAttributes({ align: "right", ...options.logo })}>\n`
-			: "";
+		const logo =
+			options.logo &&
+			`\n<img ${printAttributes({ align: "right", ...options.logo })}>\n`;
+
+		const explainer =
+			options.explainer && `\n${options.explainer.join("\n")}\n`;
 
 		return {
 			files: {
@@ -37,7 +41,7 @@ export const blockREADME = base.createBlock({
 	<a href="http://npmjs.com/package/${options.repository}"><img alt="📦 npm version" src="https://img.shields.io/npm/v/${options.repository}?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>
-${logo}
+${[logo, explainer].filter(Boolean).join("")}
 ## Usage
 
 ${options.usage}
@@ -58,5 +62,5 @@ function formatDescription(description: string) {
 		return description;
 	}
 
-	return "\n\t" + description.replaceAll(". ", ". \n\t") + "\n";
+	return "\n\t" + description.replaceAll(". ", ".\n\t") + "\n";
 }
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
index 41fb6ed..9b8aaf3 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.ts
@@ -13,6 +13,14 @@ export function readDefaultsFromReadme(
 	);
 
 	return {
+		explainer: async () => {
+			return />\n\n([\s\S]*?)\n\n## Usage/u
+				.exec(await readme())?.[1]
+				.split("\n")
+				.map((line) => line.trim())
+				.filter(Boolean);
+		},
+
 		logo: async () => {
 			const tag = await imageTag();
 
@@ -22,18 +30,22 @@ export function readDefaultsFromReadme(
 
 			const src = /src\s*=(.+)['"/]>/
 				.exec(tag)?.[1]
-				?.replaceAll(/^['"]|['"]$/g, "");
+				?.split(/\s*\w+=/)[0]
+				.replaceAll(/^['"]|['"]$/g, "");
 
 			if (!src) {
 				return undefined;
 			}
 
 			return {
-				alt: /alt=['"](.+)['"]\s*src=/.exec(tag)?.[1] ?? "Project logo",
+				alt:
+					/alt=['"](.+)['"]\s*src=/.exec(tag)?.[1].split(/['"]?\s*\w+=/)[0] ??
+					"Project logo",
 				src,
 				...readLogoSizing(src),
 			};
 		},
+
 		title: async () => {
 			const text = await readme();
 			const fromText = (/^<h1\s+align="center">(.+)<\/h1>/.exec(text) ??


Test patch:
diff --git a/src/next/blocks/blockREADME.test.ts b/src/next/blocks/blockREADME.test.ts
index 548215b..452d47c 100644
--- a/src/next/blocks/blockREADME.test.ts
+++ b/src/next/blocks/blockREADME.test.ts
@@ -62,7 +62,7 @@ describe("blockREADME", () => {
 			    "README.md": "<h1 align="center">Test Title</h1>
 
 			<p align="center">
-				First sentence. 
+				First sentence.
 				Second sentence.
 			</p>
 
@@ -89,6 +89,47 @@ describe("blockREADME", () => {
 		`);
 	});
 
+	test("options.explainer", () => {
+		const creation = testBlock(blockREADME, {
+			options: {
+				...options,
+				explainer: ["And a one.", "And a two."],
+			},
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "files": {
+			    "README.md": "<h1 align="center">Test Title</h1>
+
+			<p align="center">Test description</p>
+
+			<p align="center">
+				<a href="https://github.com/test-owner/test-repository/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/test-owner/test-repository" target="_blank"><img alt="🧪 Coverage" src="https://img.shields.io/codecov/c/github/test-owner/test-repository?label=%F0%9F%A7%AA%20coverage" /></a>
+				<a href="https://github.com/test-owner/test-repository/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/test-repository"><img alt="📦 npm version" src="https://img.shields.io/npm/v/test-repository?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>
+
+			And a one.
+			And a two.
+
+			## Usage
+
+			Use it.
+
+			## Development
+
+			See [\`.github/CONTRIBUTING.md\`](./.github/CONTRIBUTING.md), then [\`.github/DEVELOPMENT.md\`](./.github/DEVELOPMENT.md).
+			Thanks! 💖
+
+			",
+			  },
+			}
+		`);
+	});
+
 	test("options.logo without sizing", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
@@ -177,6 +218,55 @@ describe("blockREADME", () => {
 		`);
 	});
 
+	test("options.explainer and options.logo", () => {
+		const creation = testBlock(blockREADME, {
+			options: {
+				...options,
+				explainer: ["And a one.", "And a two."],
+				logo: {
+					alt: "My logo",
+					height: 100,
+					src: "img.jpg",
+					width: 128,
+				},
+			},
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "files": {
+			    "README.md": "<h1 align="center">Test Title</h1>
+
+			<p align="center">Test description</p>
+
+			<p align="center">
+				<a href="https://github.com/test-owner/test-repository/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/test-owner/test-repository" target="_blank"><img alt="🧪 Coverage" src="https://img.shields.io/codecov/c/github/test-owner/test-repository?label=%F0%9F%A7%AA%20coverage" /></a>
+				<a href="https://github.com/test-owner/test-repository/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/test-repository"><img alt="📦 npm version" src="https://img.shields.io/npm/v/test-repository?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="My logo" height="100" src="img.jpg" width="128">
+
+			And a one.
+			And a two.
+
+			## Usage
+
+			Use it.
+
+			## Development
+
+			See [\`.github/CONTRIBUTING.md\`](./.github/CONTRIBUTING.md), then [\`.github/DEVELOPMENT.md\`](./.github/DEVELOPMENT.md).
+			Thanks! 💖
+
+			",
+			  },
+			}
+		`);
+	});
+
 	test("without addons", () => {
 		const creation = testBlock(blockREADME, {
 			options,
diff --git a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
index c9559cc..82098f8 100644
--- a/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
+++ b/src/shared/options/createOptionDefaults/readDefaultsFromReadme.test.ts
@@ -19,6 +19,50 @@ vi.mock("./getUsageFromReadme.js", () => ({
 }));
 
 describe("readDefaultsFromReadme", () => {
+	describe("explainer", () => {
+		it("defaults to undefined when it cannot be found", async () => {
+			const explainer = await readDefaultsFromReadme(
+				() => Promise.resolve(`nothing.`),
+				() => Promise.resolve(undefined),
+			).explainer();
+
+			expect(explainer).toBeUndefined();
+		});
+
+		it("parses a line after badges", async () => {
+			const explainer = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
+</p>
+
+This is my project.
+
+## Usage
+					.`),
+				() => Promise.resolve(undefined),
+			).explainer();
+
+			expect(explainer).toEqual(["This is my project."]);
+		});
+
+		it("parses multiple line after badges", async () => {
+			const explainer = await readDefaultsFromReadme(
+				() =>
+					Promise.resolve(`
+</p>
+
+This is my project.
+It is good.
+
+## Usage
+					.`),
+				() => Promise.resolve(undefined),
+			).explainer();
+
+			expect(explainer).toEqual(["This is my project.", "It is good."]);
+		});
+	});
+
 	describe("logo", () => {
 		it("defaults to undefined when it cannot be found", async () => {
 			const logo = await readDefaultsFromReadme(
@@ -107,7 +151,7 @@ describe("readDefaultsFromReadme", () => {
 			const logo = await readDefaultsFromReadme(
 				() =>
 					Promise.resolve(`
-<img alt='Project logo: a fancy circle' src='abc/def.jpg'/>`),
+<img alt='Project logo: a fancy circle' height='117px' src='abc/def.jpg' width=' 117px'/>`),
 				() => Promise.resolve(undefined),
 			).logo();