help-beta: Merge lists of same type adjacent to each other.

Fixes #31252.
One of our major use cases for file imports is to have bullet points as
partials to import at different places in the project. But when
importing the file with Astro, it creates its own lists. So we merge
lists together if they have nothing but whitespace between them.
There were some talks to use a component called FlattenList that would
flatten the list inside it, but that would also flatten lists that were
nested on purpose. This approach while feeling a bit hacky would not
flatten nested lists.
This commit is contained in:
Shubham Padia
2025-04-15 10:54:36 +00:00
committed by Tim Abbott
parent c0a2b2a31d
commit b813d868a7
5 changed files with 99 additions and 1 deletions

View File

@@ -12,7 +12,10 @@
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.3", "@astrojs/check": "^0.9.3",
"@astrojs/starlight": "^0.33.0", "@astrojs/starlight": "^0.33.0",
"@types/hast": "^3.0.4",
"astro": "^5.1.2", "astro": "^5.1.2",
"hast-util-from-html": "^2.0.3",
"hast-util-to-html": "^9.0.5",
"sharp": "^0.34.1", "sharp": "^0.34.1",
"typescript": "^5.4.5" "typescript": "^5.4.5"
} }

View File

@@ -0,0 +1,82 @@
import {defineMiddleware} from "astro:middleware";
import type {Element, Root, RootContent} from "hast";
import {fromHtml} from "hast-util-from-html";
import {toHtml} from "hast-util-to-html";
function isList(node: Element): boolean {
return node.tagName === "ol" || node.tagName === "ul";
}
// This function traverses the HTML tree and merges lists of the same
// type if they are adjacent to each other. This is kinda a hack to
// make file imports work within lists. One of our major use cases
// for file imports is to have bullet points as partials to import at
// different places in the project. But when importing the file with
// Astro, it creates its own lists. So we merge lists together if they
// have nothing but whitespace between them.
function mergeAdjacentListsOfSameType(tree: Root): Root {
function recursiveMergeAdjacentLists(node: Element | Root): void {
if (!node.children) {
return;
}
const modifiedChildren: RootContent[] = [];
let currentIndex = 0;
while (currentIndex < node.children.length) {
const currentChild = node.children[currentIndex]!;
if (currentChild.type === "element" && isList(currentChild)) {
const mergedList = structuredClone(currentChild);
let lookaheadIndex = currentIndex + 1;
while (lookaheadIndex < node.children.length) {
const lookaheadChild = node.children[lookaheadIndex]!;
if (lookaheadChild.type === "element" && isList(lookaheadChild)) {
if (lookaheadChild.tagName === currentChild.tagName) {
mergedList.children.push(...lookaheadChild.children);
}
lookaheadIndex += 1;
} else if (
lookaheadChild.type === "text" &&
/^\s*$/.test(lookaheadChild.value)
) {
// Whitespace should be allowed in between lists.
lookaheadIndex += 1;
} else {
break;
}
}
modifiedChildren.push(mergedList);
currentIndex = lookaheadIndex;
} else {
modifiedChildren.push(currentChild);
currentIndex += 1;
}
}
node.children = modifiedChildren;
for (const child of node.children) {
if (child.type === "element") {
recursiveMergeAdjacentLists(child);
}
}
}
recursiveMergeAdjacentLists(tree);
return tree;
}
export const onRequest = defineMiddleware(async (_context, next) => {
const response = await next();
const html = await response.text();
const tree = fromHtml(html);
const result = toHtml(mergeAdjacentListsOfSameType(tree));
return new Response(result, {
status: 200,
headers: response.headers,
});
});

9
pnpm-lock.yaml generated
View File

@@ -503,9 +503,18 @@ importers:
'@astrojs/starlight': '@astrojs/starlight':
specifier: ^0.33.0 specifier: ^0.33.0
version: 0.33.0(astro@5.6.1(@types/node@22.14.0)(jiti@1.21.7)(rollup@4.39.0)(sass@1.86.3)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.1)) version: 0.33.0(astro@5.6.1(@types/node@22.14.0)(jiti@1.21.7)(rollup@4.39.0)(sass@1.86.3)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.1))
'@types/hast':
specifier: ^3.0.4
version: 3.0.4
astro: astro:
specifier: ^5.1.2 specifier: ^5.1.2
version: 5.6.1(@types/node@22.14.0)(jiti@1.21.7)(rollup@4.39.0)(sass@1.86.3)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.1) version: 5.6.1(@types/node@22.14.0)(jiti@1.21.7)(rollup@4.39.0)(sass@1.86.3)(terser@5.39.0)(typescript@5.8.3)(yaml@2.7.1)
hast-util-from-html:
specifier: ^2.0.3
version: 2.0.3
hast-util-to-html:
specifier: ^9.0.5
version: 9.0.5
sharp: sharp:
specifier: ^0.34.1 specifier: ^0.34.1
version: 0.34.1 version: 0.34.1

View File

@@ -176,6 +176,10 @@ def run() -> None:
include_source_dir = os.path.join(BASE_DIR, "help/include") include_source_dir = os.path.join(BASE_DIR, "help/include")
include_destination_dir = os.path.join(BASE_DIR, "help-beta/src/content/docs/include") include_destination_dir = os.path.join(BASE_DIR, "help-beta/src/content/docs/include")
shutil.copytree(include_source_dir, include_destination_dir) shutil.copytree(include_source_dir, include_destination_dir)
# We do not want Astro to render these include files as standalone
# files, prefixing them with an underscore accomplishes that.
# https://docs.astro.build/en/guides/routing/#excluding-pages
for name in os.listdir(include_destination_dir): for name in os.listdir(include_destination_dir):
os.rename( os.rename(
os.path.join(include_destination_dir, name), os.path.join(include_destination_dir, name),

View File

@@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 378
# historical commits sharing the same major version, in which case a # historical commits sharing the same major version, in which case a
# minor version bump suffices. # minor version bump suffices.
PROVISION_VERSION = (325, 0) # bumped 2025-04-09 to upgrade JavaScript dependencies PROVISION_VERSION = (325, 1) # bumped 2025-04-16 to add hast dependencies to help-beta