diff --git a/docs/src/content/docs/components/file-tree.mdx b/docs/src/content/docs/components/file-tree.mdx
index bede1bb1732..652fdb32b29 100644
--- a/docs/src/content/docs/components/file-tree.mdx
+++ b/docs/src/content/docs/components/file-tree.mdx
@@ -218,6 +218,49 @@ import { FileTree } from '@astrojs/starlight/components';
+### Escape white-space in file names
+
+To include white-space in a file or directory name, escape it with double quotes.
+
+
+
+```mdx {5,6}
+import { FileTree } from '@astrojs/starlight/components';
+
+
+
+- "main folder/" file container
+- "main folder/" file container
+ - "The Header.astro" main file
+ - …
+
+
+```
+
+
+
+```markdoc {2,3}
+{% filetree %}
+- "main folder/" file container
+- "main folder/" file container
+ - "The Header.astro" main file
+ - …
+{% /filetree %}
+```
+
+
+
+
+
+- "main folder/" file container
+- "main folder/" file container
+ - "The Header.astro" main file
+ - …
+
+
+
+
+
## `` Props
**Implementation:** [`FileTree.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/user-components/FileTree.astro)
diff --git a/packages/starlight/__tests__/remark-rehype/rehype-file-tree.test.ts b/packages/starlight/__tests__/remark-rehype/rehype-file-tree.test.ts
index dcbe62f59ab..c1c31f07aa2 100644
--- a/packages/starlight/__tests__/remark-rehype/rehype-file-tree.test.ts
+++ b/packages/starlight/__tests__/remark-rehype/rehype-file-tree.test.ts
@@ -84,6 +84,78 @@ describe('processor', () => {
expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-comment-nodes.html');
});
+ test('double quotes allows to add white-space to file names', () => {
+ const html = processTestFileTree(``);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-file.html');
+ });
+
+ test('double quotes file name with text comments', () => {
+ const html = processTestFileTree(`- "file name" with some comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-file-text.html');
+ });
+
+ test('double quotes file name with node comments', () => {
+ const html = processTestFileTree(`- "file name" with important comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-file-node.html');
+ });
+
+ test('double quotes allows to add white-space to folder names', () => {
+ const html = processTestFileTree(``);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-folder.html');
+ });
+
+ test('double quotes folder name with text comments', () => {
+ const html = processTestFileTree(`- "folder name/" with some comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-folder-text.html');
+ });
+
+ test('double quotes folder name with node comments', () => {
+ const html = processTestFileTree(`- "folder name/" with important comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-folder-node.html');
+ });
+
+ test('special quotes allows to add white-space to file names', () => {
+ const html = processTestFileTree(``);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-file.html');
+ });
+
+ test('special quotes file name with text comments', () => {
+ const html = processTestFileTree(`- “file name” with some comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-file-text.html');
+ });
+
+ test('special quotes file name with node comments', () => {
+ const html = processTestFileTree(`- “file name” with important comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-file-node.html');
+ });
+
+ test('special quotes allows to add white-space to folder names', () => {
+ const html = processTestFileTree(``);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-folder.html');
+ });
+
+ test('special quotes folder name with text comments', () => {
+ const html = processTestFileTree(`- “folder name/” with some comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-folder-text.html');
+ });
+
+ test('special quotes folder name with node comments', () => {
+ const html = processTestFileTree(`- “folder name/” with important comments
`);
+
+ expect(extractFileTree(html)).toMatchFileSnapshot('./snapshots/file-tree-double-quotes-folder-node.html');
+ });
+
test('identifies directory with either a file name ending with a slash or a nested list', () => {
const html = processTestFileTree(`
- directory/
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file-node.html b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file-node.html
new file mode 100644
index 00000000000..7b60a8a75fc
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file-node.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file-text.html b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file-text.html
new file mode 100644
index 00000000000..3ee929c95a5
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file-text.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file.html b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file.html
new file mode 100644
index 00000000000..c8085eab49d
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-file.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder-node.html b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder-node.html
new file mode 100644
index 00000000000..438e5acd043
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder-node.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder-text.html b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder-text.html
new file mode 100644
index 00000000000..1ec24c220aa
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder-text.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder.html b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder.html
new file mode 100644
index 00000000000..133ab517538
--- /dev/null
+++ b/packages/starlight/__tests__/remark-rehype/snapshots/file-tree-double-quotes-folder.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/starlight/user-components/rehype-file-tree.ts b/packages/starlight/user-components/rehype-file-tree.ts
index 1bdb19072a5..3ba0a19fc10 100644
--- a/packages/starlight/user-components/rehype-file-tree.ts
+++ b/packages/starlight/user-components/rehype-file-tree.ts
@@ -56,11 +56,10 @@ const fileTreeProcessor = rehype()
// Extract text comment that follows the file name, e.g. `README.md This is a comment`
if (firstChild?.type === 'text') {
- const [filename, ...fragments] = firstChild.value.split(' ');
- firstChild.value = filename || '';
- const textComment = fragments.join(' ').trim();
- if (textComment.length > 0) {
- comment.push(fragments.join(' '));
+ const [filename, textComment] = splitFileNameAndTextComments(firstChild.value);
+ firstChild.value = filename;
+ if (textComment.trim().length > 0) {
+ comment.push(textComment);
}
}
@@ -244,6 +243,25 @@ function throwFileTreeValidationError(message: string): never {
);
}
+function splitFileNameAndTextComments(value: string): [string, string] {
+ if (!value || value.trim() === '') return ['', ''];
+
+ // Handles the case where the file name is wrapped in double quotes, e.g. `"READ ME.md" This is a comment`.
+ if (value.startsWith('"')) {
+ const match = value.match(/^"([^"]+)"\s*(.*)$/);
+ if (match) return [match[1]!, match[2]!];
+ }
+
+ if (value.startsWith('“')) {
+ const match = value.match(/^“([^”]+)”\s*(.*)$/);
+ if (match) return [match[1]!, match[2]!];
+ }
+
+ const [filename, ...fragments] = value.split(' ');
+ const textComments = fragments.join(' ');
+ return [filename!, textComments];
+}
+
export interface Definitions {
files: Record;
extensions: Record;