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

Issue:
🐛 Bug: Transition removes footnotes from `README.md`
### 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 footnotes like:

```md
[^1]: Abc def.
```

...then they should be preserved during `npx create-typescript-app` migration.

### Actual

https://github.com/JoshuaKGoldberg/ts-function-inliner/pull/206/files#r2027202825 shows one being removed.

### Additional Info

🎁 

Pull request description:
fix: account for footnotes in README.md
## PR Checklist

- [x] Addresses an existing open issue: fixes #2129
- [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

🎁 

Patch:
diff --git a/src/base.ts b/src/base.ts
index b35dc66..e07a1b1 100644
--- a/src/base.ts
+++ b/src/base.ts
@@ -31,6 +31,7 @@ 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 { readReadmeFootnotes } from "./options/readReadmeFootnotes.js";
 import { readReadmeUsage } from "./options/readReadmeUsage.js";
 import { readRepository } from "./options/readRepository.js";
 import { readRulesetId } from "./options/readRulesetId.js";
@@ -197,6 +198,7 @@ export const base = createBase({
 					getDevelopmentDocumentation,
 					getReadmeAdditional,
 					getReadmeExplainer,
+					getReadmeFootnotes,
 					getReadmeUsage,
 				),
 		);
@@ -281,6 +283,10 @@ export const base = createBase({
 			async () => await readReadmeExplainer(getReadme),
 		);
 
+		const getReadmeFootnotes = lazyValue(
+			async () => await readReadmeFootnotes(getReadme),
+		);
+
 		const getReadmeUsage = lazyValue(
 			async () => await readReadmeUsage(getEmoji, getReadme, getRepository),
 		);
diff --git a/src/blocks/blockREADME.ts b/src/blocks/blockREADME.ts
index b030095..a6c9692 100644
--- a/src/blocks/blockREADME.ts
+++ b/src/blocks/blockREADME.ts
@@ -43,6 +43,11 @@ export const blockREADME = base.createBlock({
 			options.logo &&
 			`\n<img ${printAttributes({ align: "right", ...options.logo })}>\n`;
 
+		const suffixes = [
+			...notices,
+			options.documentation.readme.footnotes,
+		].filter((suffix) => typeof suffix === "string");
+
 		return {
 			files: {
 				"README.md": `<h1 align="center">${options.title}</h1>
@@ -65,7 +70,7 @@ ${[...sections, options.documentation.readme.additional]
 	.filter(Boolean)
 	.map((section) => `\n${section}`)
 	.join("")}
-${notices.length ? `\n${notices.map((notice) => notice.trim()).join("\n\n")}` : ""}`,
+${suffixes.length ? `\n${suffixes.map((suffix) => suffix.trim()).join("\n\n")}` : ""}`,
 			},
 		};
 	},
diff --git a/src/options/readDocumentation.ts b/src/options/readDocumentation.ts
index fc1eb70..d828f34 100644
--- a/src/options/readDocumentation.ts
+++ b/src/options/readDocumentation.ts
@@ -4,20 +4,24 @@ export async function readDocumentation(
 	getDevelopmentDocumentation: () => Promise<string | undefined>,
 	getReadmeAdditional: () => Promise<string | undefined>,
 	getReadmeExplainer: () => Promise<string | undefined>,
+	getReadmeFootnotes: () => Promise<string | undefined>,
 	getReadmeUsage: () => Promise<string>,
 ): Promise<Documentation> {
-	const [additional, explainer, development, usage] = await Promise.all([
-		getReadmeAdditional(),
-		getReadmeExplainer(),
-		getDevelopmentDocumentation(),
-		getReadmeUsage(),
-	]);
+	const [additional, explainer, footnotes, development, usage] =
+		await Promise.all([
+			getReadmeAdditional(),
+			getReadmeExplainer(),
+			getReadmeFootnotes(),
+			getDevelopmentDocumentation(),
+			getReadmeUsage(),
+		]);
 
 	return {
 		development,
 		readme: {
 			additional,
 			explainer,
+			footnotes,
 			usage,
 		},
 	};
diff --git a/src/options/readReadmeAdditional.ts b/src/options/readReadmeAdditional.ts
index a06eb58..9aed6af 100644
--- a/src/options/readReadmeAdditional.ts
+++ b/src/options/readReadmeAdditional.ts
@@ -1,10 +1,9 @@
+import { indicatorTemplatedBy } from "./readReadmeFootnotes.js";
+
 const indicatorAfterAllContributors = /<!--\s*ALL-CONTRIBUTORS-LIST:END\s*-->/;
 const indicatorAfterAllContributorsSpellCheck =
 	/<!--\s*spellchecker:\s*enable\s*-->/;
 
-const indicatorBeforeTemplatedBy =
-	/> .* This package (?:is|was) (?:based|build|templated) (?:on|with) |<!-- You can remove this notice/;
-
 export async function readReadmeAdditional(getReadme: () => Promise<string>) {
 	const readme = await getReadme();
 	if (!readme) {
@@ -18,7 +17,7 @@ export async function readReadmeAdditional(getReadme: () => Promise<string>) {
 		return undefined;
 	}
 
-	const templatedByMatch = indicatorBeforeTemplatedBy.exec(readme);
+	const templatedByMatch = indicatorTemplatedBy.exec(readme);
 
 	return readme
 		.slice(
diff --git a/src/options/readReadmeFootnotes.ts b/src/options/readReadmeFootnotes.ts
new file mode 100644
index 0000000..1211e6b
--- /dev/null
+++ b/src/options/readReadmeFootnotes.ts
@@ -0,0 +1,18 @@
+export const indicatorTemplatedBy =
+	/> .* This package (?:is|was) (?:based|build|templated) (?:on|with) |<!-- You can remove this notice/;
+
+export async function readReadmeFootnotes(getReadme: () => Promise<string>) {
+	const readme = await getReadme();
+	if (!readme) {
+		return undefined;
+	}
+
+	const indexOfTemplatedBy = indicatorTemplatedBy.exec(readme)?.index;
+	if (!indexOfTemplatedBy) {
+		return undefined;
+	}
+
+	const indexOfNextLine = readme.indexOf("\n", indexOfTemplatedBy);
+
+	return readme.slice(indexOfNextLine).trim() || undefined;
+}
diff --git a/src/schemas.ts b/src/schemas.ts
index 508ba0c..8415021 100644
--- a/src/schemas.ts
+++ b/src/schemas.ts
@@ -13,6 +13,7 @@ export type Contributor = z.infer<typeof zContributor>;
 export const zReadme = z.object({
 	additional: z.string().optional(),
 	explainer: z.string().optional(),
+	footnotes: z.string().optional(),
 	usage: z.string(),
 });
 


Test patch:
diff --git a/src/blocks/blockREADME.test.ts b/src/blocks/blockREADME.test.ts
index ad38c95..d45a9e5 100644
--- a/src/blocks/blockREADME.test.ts
+++ b/src/blocks/blockREADME.test.ts
@@ -117,7 +117,7 @@ describe("blockREADME", () => {
 		`);
 	});
 
-	test("options.explainer", () => {
+	test("options.documentation.readme.explainer", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
 				...optionsBase,
@@ -125,7 +125,7 @@ describe("blockREADME", () => {
 					...optionsBase.documentation,
 					readme: {
 						...optionsBase.documentation.readme,
-						explainer: "\nAnd a one.\nAnd a two.\n",
+						explainer: "And a one.\nAnd a two.",
 					},
 				},
 			},
@@ -142,11 +142,9 @@ 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.
@@ -162,6 +160,48 @@ describe("blockREADME", () => {
 		`);
 	});
 
+	test("options.documentation.readme.footnotes", () => {
+		const creation = testBlock(blockREADME, {
+			options: {
+				...optionsBase,
+				documentation: {
+					...optionsBase.documentation,
+					readme: {
+						...optionsBase.documentation.readme,
+						footnotes: "And a one.\nAnd a two.",
+					},
+				},
+			},
+		});
+
+		expect(creation).toMatchInlineSnapshot(`
+			{
+			  "files": {
+			    "README.md": "<h1 align="center">Test Title</h1>
+
+			<p align="center">Test description</p>
+
+			<p align="center">
+				<img alt="💪 TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
+			</p>
+
+			## Usage
+
+			Test usage.
+
+			## Development
+
+			See [\`.github/CONTRIBUTING.md\`](./.github/CONTRIBUTING.md), then [\`.github/DEVELOPMENT.md\`](./.github/DEVELOPMENT.md).
+			Thanks! 💖
+
+
+			And a one.
+			And a two.",
+			  },
+			}
+		`);
+	});
+
 	test("options.logo without sizing", () => {
 		const creation = testBlock(blockREADME, {
 			options: {
diff --git a/src/options/readReadmeFootnotes.test.ts b/src/options/readReadmeFootnotes.test.ts
new file mode 100644
index 0000000..373eefe
--- /dev/null
+++ b/src/options/readReadmeFootnotes.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, it } from "vitest";
+
+import { readReadmeFootnotes } from "./readReadmeFootnotes.js";
+
+describe(readReadmeFootnotes, () => {
+	it("resolves undefined when there is no existing readme content", async () => {
+		const getReadme = () => Promise.resolve("");
+
+		const result = await readReadmeFootnotes(getReadme);
+
+		expect(result).toBeUndefined();
+	});
+
+	it("resolves undefined when there is no templated by notice", async () => {
+		const getReadme = () => Promise.resolve(`# My Package`);
+
+		const result = await readReadmeFootnotes(getReadme);
+
+		expect(result).toBeUndefined();
+	});
+
+	it("resolves undefined when there is no content after a templated by notice", async () => {
+		const getReadme = () =>
+			Promise.resolve(`# My Package
+			
+> 💖 This package was templated with etc. etc.
+
+`);
+
+		const result = await readReadmeFootnotes(getReadme);
+
+		expect(result).toBeUndefined();
+	});
+
+	it("resolves the content when there plain text content after a templated by notice", async () => {
+		const getReadme = () =>
+			Promise.resolve(`# My Package
+			
+> 💖 This package was templated with etc. etc.
+
+After.
+`);
+
+		const result = await readReadmeFootnotes(getReadme);
+
+		expect(result).toBe("After.");
+	});
+
+	it("resolves the content when there are footnotes after a templated by notice", async () => {
+		const getReadme = () =>
+			Promise.resolve(`# My Package
+			
+> 💖 This package was templated with etc. etc.
+
+[^1]: After.
+`);
+
+		const result = await readReadmeFootnotes(getReadme);
+
+		expect(result).toBe("[^1]: After.");
+	});
+});