diff --git a/.changeset/kind-boats-train.md b/.changeset/kind-boats-train.md new file mode 100644 index 00000000000..83267d880ed --- /dev/null +++ b/.changeset/kind-boats-train.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': patch +--- + +Add Slack social link icon diff --git a/.changeset/polite-colts-turn.md b/.changeset/polite-colts-turn.md deleted file mode 100644 index 0e46da7f828..00000000000 --- a/.changeset/polite-colts-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/starlight': patch ---- - -Internal: fix import issue in translation string loading mechanism diff --git a/.changeset/quiet-humans-lie.md b/.changeset/quiet-humans-lie.md deleted file mode 100644 index 08611098bcc..00000000000 --- a/.changeset/quiet-humans-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/starlight': patch ---- - -Fix last updated dates for pages displaying fallback content diff --git a/.changeset/thick-boxes-fail.md b/.changeset/thick-boxes-fail.md new file mode 100644 index 00000000000..18b7aa870b6 --- /dev/null +++ b/.changeset/thick-boxes-fail.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": minor +--- + +Add i18n support for default aside labels diff --git a/.gitignore b/.gitignore index 9f880cb7098..e790eb4de4e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ pnpm-debug.log* # Vitest __coverage__/ + +# Vercel output +.vercel \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 8b16453a2c2..871a65eb470 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,4 +10,7 @@ .changeset # Files -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml + +# Test snapshots +**/__tests__/**/snapshots diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 37ecec8bd16..2405fd09de4 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -16,7 +16,13 @@ export const locales = { ru: { label: 'Русский', lang: 'ru' }, }; -const site = 'https://starlight.astro.build/'; +/* https://vercel.com/docs/projects/environment-variables/system-environment-variables#system-environment-variables */ +const VERCEL_PREVIEW_SITE = + process.env.VERCEL_ENV !== 'production' && + process.env.VERCEL_URL && + `https://${process.env.VERCEL_URL}`; + +const site = VERCEL_PREVIEW_SITE || 'https://starlight.astro.build/'; export default defineConfig({ site, @@ -175,7 +181,6 @@ export default defineConfig({ autogenerate: { directory: 'reference' }, }, ], - lastUpdated: true, }), ], }); diff --git a/docs/public/_headers b/docs/public/_headers deleted file mode 100644 index aaa51ef2a6e..00000000000 --- a/docs/public/_headers +++ /dev/null @@ -1,4 +0,0 @@ -/_astro/* - Cache-Control: public - Cache-Control: max-age=604800 - Cache-Control: immutable diff --git a/docs/public/_redirects b/docs/public/_redirects deleted file mode 100644 index a7aa40820f3..00000000000 --- a/docs/public/_redirects +++ /dev/null @@ -1,3 +0,0 @@ -/ph/* https://astro-houston-ph.pages.dev/ph/:splat 200 -/zh/* /zh-cn/:splat -/:lang/* /:lang/404/ 404 diff --git a/docs/src/assets/showcase/docs.ethfollow.xyz.png b/docs/src/assets/showcase/docs.ethfollow.xyz.png new file mode 100644 index 00000000000..c62f659b0b4 Binary files /dev/null and b/docs/src/assets/showcase/docs.ethfollow.xyz.png differ diff --git a/docs/src/components/showcase-sites.astro b/docs/src/components/showcase-sites.astro index 6301da3453f..50f606f050f 100644 --- a/docs/src/components/showcase-sites.astro +++ b/docs/src/components/showcase-sites.astro @@ -70,4 +70,9 @@ import FluidGrid from './fluid-grid.astro'; href="https://react-awesome-reveal.morello.dev/" thumbnail="react-awesome-reveal.morello.dev.png" /> + diff --git a/docs/src/content/docs/de/guides/authoring-content.md b/docs/src/content/docs/de/guides/authoring-content.md index e376af0fa08..88ffcf85b6f 100644 --- a/docs/src/content/docs/de/guides/authoring-content.md +++ b/docs/src/content/docs/de/guides/authoring-content.md @@ -207,3 +207,7 @@ Lange, einzeilige Codeblöcke sollten nicht umgebrochen werden. Sie sollten hori ## Andere allgemeine Markdown-Funktionen Starlight unterstützt alle anderen Markdown-Autorensyntaxen, wie Listen und Tabellen. Einen schnellen Überblick über alle Markdown-Syntaxelemente findest du im [Markdown Cheat Sheet von The Markdown Guide](https://www.markdownguide.org/cheat-sheet/). + +## Erweiterte Markdown- und MDX-Konfiguration + +Starlight verwendet Astros Markdown- und MDX-Renderer, der auf remark und rehype aufbaut. Du kannst eine Unterstützung für eigene Syntax und Verhalten hinzufügen, indem du `remarkPlugins` oder `rehypePlugins` in deiner Astro-Konfigurationsdatei hinzufügst. Weitere Informationen findest du unter ["Markdown konfigurieren"] (https://docs.astro.build/de/guides/markdown-content/#markdown-konfigurieren) in der Astro-Dokumentation. diff --git a/docs/src/content/docs/de/guides/i18n.mdx b/docs/src/content/docs/de/guides/i18n.mdx index 46478e5d25b..40ffae09356 100644 --- a/docs/src/content/docs/de/guides/i18n.mdx +++ b/docs/src/content/docs/de/guides/i18n.mdx @@ -143,7 +143,7 @@ Wenn für eine Sprache noch keine Übersetzung verfügbar ist, zeigt Starlight d Starlight bietet nicht nur übersetzte Inhaltsdateien, sondern auch die Möglichkeit, die Standard-Benutzeroberfläche zu übersetzen (z.B. die Überschrift "Auf dieser Seite" im Inhaltsverzeichnis), so dass deine Leser deine Website vollständig in der ausgewählten Sprache erleben können. -Englisch, Tschechisch, Französisch, Deutsch, Italienisch, Japanisch, Portugiesisch, Niederländisch, Dänisch, Spanisch, Türkisch, Arabisch, Norwegisch, Farsi, Hebräisch, Chinesisch (vereinfacht), Koreanisch, Indonesisch, Russisch, Schwedisch, Ukrainisch und Vietnamesisch werden standardmäßig übersetzt, und wir freuen uns über [Beiträge zur Aufnahme weiterer Standardsprachen](https://github.com/withastro/starlight/blob/main/CONTRIBUTING.md). +Englisch, Tschechisch, Französisch, Deutsch, Italienisch, Japanisch, Portugiesisch, Niederländisch, Dänisch, Spanisch, Türkisch, Arabisch, Norwegisch, Farsi, Hebräisch, Chinesisch (vereinfacht), Koreanisch, Indonesisch, Russisch, Schwedisch, Ukrainisch, Vietnamesisch und Galizisch werden standardmäßig übersetzt, und wir freuen uns über [Beiträge zur Aufnahme weiterer Standardsprachen](https://github.com/withastro/starlight/blob/main/CONTRIBUTING.md). Du kannst Übersetzungen für zusätzliche Sprachen, die du unterstützt, über die `i18n` Datensammlung zur Verfügung stellen - oder unsere Standardbezeichnungen überschreiben. diff --git a/docs/src/content/docs/de/reference/configuration.mdx b/docs/src/content/docs/de/reference/configuration.mdx index 306ed7dce4b..abc6711b866 100644 --- a/docs/src/content/docs/de/reference/configuration.mdx +++ b/docs/src/content/docs/de/reference/configuration.mdx @@ -181,10 +181,10 @@ sidebar: [ type SidebarItem = { label: string; translations?: Record; + badge?: string | BadgeConfig; } & ( | { link: string; - badge?: string | BadgeConfig; attrs?: Record; } | { items: SidebarItem[]; collapsed?: boolean } diff --git a/docs/src/content/docs/es/guides/i18n.mdx b/docs/src/content/docs/es/guides/i18n.mdx index 0a359b4ef5a..7274ab75045 100644 --- a/docs/src/content/docs/es/guides/i18n.mdx +++ b/docs/src/content/docs/es/guides/i18n.mdx @@ -140,7 +140,11 @@ Si no hay una traducción disponible para un idioma, Starlight mostrará a los l ## Traduce la UI de Starlight -Starlight proporciona de forma predeterminada traducciones para los textos utilizados en la UI al inglés, checo, francés, alemán, italiano, japonés, holandés, portugués, danés, español, turco, árabe, noruego, farsi, hebreo, chino simplificado, coreano, indonesio, ruso, sueco, ucraniano y vietnamita. También damos la bienvenida a [contribuciones para agregar más idiomas predeterminados](https://github.com/withastro/starlight/blob/main/CONTRIBUTING.md). +Además de alojar archivos de contenido traducidos, Starlight te permite traducir las etiquetas de UI predeterminadas (p. ej. el encabezado "En esta página" en la tabla de contenidos) para que tus lectores puedan experimentar tu sitio completamente en el idioma seleccionado. + +Inglés, checo, francés, alemán, italiano, japonés, portugués, holandés, danés, español, turco, árabe, noruego, farsi, hebreo, chino simplificado, coreano, indonesio, ruso, sueco, ucraniano, vietnamita y gallego se proporcionan de forma predeterminada, y damos la bienvenida a [contribuciones para agregar más idiomas predeterminados](https://github.com/withastro/starlight/blob/main/CONTRIBUTING.md). + +Puedes proprocionar traducciones para idiomas adicionales, o editar nuestras etiquetas predeterminadas, a través de la colección de datos `i18n`. 1. Configura la colección de datos `i18n` en `src/content/config.ts` si aún no está configurada: diff --git a/docs/src/content/docs/es/reference/frontmatter.md b/docs/src/content/docs/es/reference/frontmatter.md index 61a528327ec..b2a8cb6581b 100644 --- a/docs/src/content/docs/es/reference/frontmatter.md +++ b/docs/src/content/docs/es/reference/frontmatter.md @@ -111,20 +111,44 @@ hero: --- ``` +Puedes mostrar diferentes versiones de la imagen hero en los modos claro y oscuro. + +```md +--- +hero: + image: + alt: Un logotipo brillante, de colores brillantes + dark: ../../assets/logo-dark.png + light: ../../assets/logo-light.png +--- +``` + #### `HeroConfig` ```ts interface HeroConfig { title?: string; tagline?: string; - image?: { - alt?: string; - // Ruta relativa a una imagen en tu repositorio. - file?: string; - // HTML crudo para usar en el espacio de la imagen. - // Podría ser una etiqueta `` personalizada o un `` en línea. - html?: string; - }; + image?: + | { + // Ruta relativa a una imagen en tu repositorio. + file: string; + // Texto alternativo para hacer que la imagen sea accesible a la tecnología de asistencia + alt?: string; + } + | { + // Ruta relativa a una imagen en tu repositorio para usar en el modo oscuro. + dark: string; + // Ruta relativa a una imagen en tu repositorio para usar en el modo claro. + light: string; + // Texto alternativo para hacer que la imagen sea accesible a la tecnología de asistencia + alt?: string; + } + | { + // HTML crudo para usar en el espacio de la imagen. + // Podría ser una etiqueta `` personalizada o un `` en línea. + html: string; + }; actions?: Array<{ text: string; link: string; diff --git a/docs/src/content/docs/es/reference/overrides.md b/docs/src/content/docs/es/reference/overrides.md index a787b93a426..055d6f20627 100644 --- a/docs/src/content/docs/es/reference/overrides.md +++ b/docs/src/content/docs/es/reference/overrides.md @@ -344,6 +344,8 @@ La implementación predeterminada muestra un título grande, un lema y enlaces d Componente renderizado alrededor del contenido principal de cada página. La implementación predeterminada configura estilos básicos para aplicar al contenido de Markdown. +Los estilos de contenido Markdown también están expuestos en `@astrojs/starlight/style/markdown.css` y están limitados al ámbito de la clase CSS `.sl-markdown-content`. + --- ### Pie de página diff --git a/docs/src/content/docs/ko/reference/overrides.md b/docs/src/content/docs/ko/reference/overrides.md index 2e8334db69a..2aa40464f54 100644 --- a/docs/src/content/docs/ko/reference/overrides.md +++ b/docs/src/content/docs/ko/reference/overrides.md @@ -337,6 +337,8 @@ Starlight의 페이지 사이드바는 현재 페이지의 하위 제목을 간 각 페이지의 메인 콘텐츠 주위에 렌더링되는 컴포넌트입니다. 기본적으로 마크다운 콘텐츠에 적용할 기본 스타일을 설정합니다. +Markdown 콘텐츠 스타일은 `@astrojs/starlight/style/markdown.css`에도 노출되며 `.sl-markdown-content` CSS 클래스로 범위가 지정됩니다. + --- ### 바닥글 diff --git a/docs/src/content/docs/pt-br/reference/configuration.mdx b/docs/src/content/docs/pt-br/reference/configuration.mdx index 390cbb3dfd9..df932ec3346 100644 --- a/docs/src/content/docs/pt-br/reference/configuration.mdx +++ b/docs/src/content/docs/pt-br/reference/configuration.mdx @@ -183,10 +183,10 @@ sidebar: [ type SidebarItem = { label: string; translations?: Record; + badge?: string | BadgeConfig; } & ( | { link: string; - badge?: string | BadgeConfig; attrs?: Record; } | { items: SidebarItem[]; collapsed?: boolean } diff --git a/docs/src/content/docs/pt-br/reference/frontmatter.md b/docs/src/content/docs/pt-br/reference/frontmatter.md index ed8d73d07e4..1e38930a6df 100644 --- a/docs/src/content/docs/pt-br/reference/frontmatter.md +++ b/docs/src/content/docs/pt-br/reference/frontmatter.md @@ -111,20 +111,44 @@ hero: --- ``` +Você pode exibir diferentes versões da imagem hero no modo claro e escuro. + +```md +--- +hero: + image: + alt: Um logo brilhante e colorido + dark: ../../assets/logo-escuro.png + light: ../../assets/logo-claro.png +--- +``` + #### `HeroConfig` ```ts interface HeroConfig { title?: string; tagline?: string; - image?: { - alt?: string; - // Caminho relativo a uma imagem no seu repositório. - file?: string; - // HTML bruto para utilizar no slot de imagem. - // Pode ser uma tag `` customizada ou um `` inline. - html?: string; - }; + image?: + | { + // Caminho relativo de uma imagem no seu repositório. + file: string; + // Texto alternativo para tornar a imagem acessível à tecnologia assistiva + alt?: string; + } + | { + // Caminho relativo de uma imagem em seu repositório para ser usada no modo escuro. + dark: string; + // Caminho relativo de uma imagem em seu repositório para ser usada no modo claro. + light: string; + // Texto alternativo para tornar a imagem acessível à tecnologia assistiva + alt?: string; + } + | { + // HTML bruto para utilizar no slot de imagem. + // Pode ser uma tag `` personalizada ou um `` inline. + html: string; + }; actions?: Array<{ text: string; link: string; diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md index b3d95be95dd..a9938ad7f0d 100644 --- a/docs/src/content/docs/reference/overrides.md +++ b/docs/src/content/docs/reference/overrides.md @@ -342,6 +342,8 @@ The default implementation shows a large title, tagline, and call-to-action link Component rendered around each page’s main content. The default implementation sets up basic styles to apply to Markdown content. +The Markdown content styles are also exposed in `@astrojs/starlight/style/markdown.css` and scoped to the `.sl-markdown-content` CSS class. + --- ### Footer diff --git a/docs/vercel.json b/docs/vercel.json new file mode 100644 index 00000000000..e5259e57302 --- /dev/null +++ b/docs/vercel.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + + "routes": [ + { + "src": "^/_astro/(.*)$", + "headers": { "cache-control": "public, max-age=31536000, immutable" }, + "continue": true + }, + + { "src": "/(ph$|ph/)(.*)", "dest": "https://astro-houston-ph.pages.dev/ph/$2" }, + + { "src": "(.*)/([^./]+)$", "dest": "$1/$2/", "status": 301 }, + { "src": "(.*)/index.html$", "dest": "$1/", "status": 301 }, + + { "handle": "filesystem" }, + + { "src": "/zh/(.*)", "dest": "/zh-cn/$1", "status": 301 }, + + { "src": "/(?[^/]*)/(.*)", "dest": "/$lang/404/", "status": 404 } + ] +} diff --git a/examples/basics/package.json b/examples/basics/package.json index b7a528788f1..926d71d4b80 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/starlight": "^0.12.0", + "@astrojs/starlight": "^0.12.1", "astro": "^3.2.3", "sharp": "^0.32.5" } diff --git a/examples/tailwind/package.json b/examples/tailwind/package.json index 25851c56001..35727482b5b 100644 --- a/examples/tailwind/package.json +++ b/examples/tailwind/package.json @@ -11,7 +11,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/starlight": "^0.12.0", + "@astrojs/starlight": "^0.12.1", "@astrojs/starlight-tailwind": "^2.0.1", "@astrojs/tailwind": "^5.0.0", "astro": "^3.2.3", diff --git a/packages/starlight/CHANGELOG.md b/packages/starlight/CHANGELOG.md index 901e1a813cb..01931083f14 100644 --- a/packages/starlight/CHANGELOG.md +++ b/packages/starlight/CHANGELOG.md @@ -1,5 +1,17 @@ # @astrojs/starlight +## 0.12.1 + +### Patch Changes + +- [#1069](https://github.com/withastro/starlight/pull/1069) [`b86f360`](https://github.com/withastro/starlight/commit/b86f3608f03be9455ec1d5ba11820c9bf601ad1e) Thanks [@Genteure](https://github.com/Genteure)! - Fix sidebar highlighting and navigation buttons for pages with path containing non-ASCII characters + +- [#1025](https://github.com/withastro/starlight/pull/1025) [`0d1e75e`](https://github.com/withastro/starlight/commit/0d1e75e17269ddac3eb15b7dfb4480da1bb01c6c) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Internal: fix import issue in translation string loading mechanism + +- [#1044](https://github.com/withastro/starlight/pull/1044) [`a5a9754`](https://github.com/withastro/starlight/commit/a5a9754f111b97abfd277d99759e9857aa0fb22b) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fix last updated dates for pages displaying fallback content + +- [#1049](https://github.com/withastro/starlight/pull/1049) [`c27495d`](https://github.com/withastro/starlight/commit/c27495da61f9376236519ed3f08a169f245a189c) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Expose Markdown content styles in `@astrojs/starlight/style/markdown.css` + ## 0.12.0 ### Minor Changes diff --git a/packages/starlight/__tests__/basics/sitemap.test.ts b/packages/starlight/__tests__/basics/sitemap.test.ts new file mode 100644 index 00000000000..d33820402be --- /dev/null +++ b/packages/starlight/__tests__/basics/sitemap.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'vitest'; +import { getSitemapConfig, starlightSitemap } from '../../integrations/sitemap'; +import type { StarlightConfig } from '../../types'; +import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config'; + +describe('starlightSitemap', () => { + test('returns @astrojs/sitemap integration', () => { + const integration = starlightSitemap({} as StarlightConfig); + expect(integration.name).toBe('@astrojs/sitemap'); + }); +}); + +describe('getSitemapConfig', () => { + test('configures i18n config', () => { + const config = getSitemapConfig( + StarlightConfigSchema.parse({ + title: 'i18n test', + locales: { root: { lang: 'en', label: 'English' }, fr: { label: 'French' } }, + } satisfies StarlightUserConfig) + ); + expect(config).toMatchInlineSnapshot(` + { + "i18n": { + "defaultLocale": "root", + "locales": { + "fr": "fr", + "root": "en", + }, + }, + } + `); + }); + + test('no config for monolingual sites', () => { + const config = getSitemapConfig( + StarlightConfigSchema.parse({ title: 'i18n test' } satisfies StarlightUserConfig) + ); + expect(config).toMatchInlineSnapshot('{}'); + }); +}); diff --git a/packages/starlight/__tests__/remark-rehype/asides.test.ts b/packages/starlight/__tests__/remark-rehype/asides.test.ts new file mode 100644 index 00000000000..b3f792166ba --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/asides.test.ts @@ -0,0 +1,144 @@ +import { createMarkdownProcessor } from '@astrojs/markdown-remark'; +import { describe, expect, test } from 'vitest'; +import { starlightAsides } from '../../integrations/asides'; +import { createTranslationSystemFromFs } from '../../utils/translations-fs'; +import { StarlightConfigSchema, type StarlightUserConfig } from '../../utils/user-config'; + +const starlightConfig = StarlightConfigSchema.parse({ + title: 'Asides Tests', + locales: { en: { label: 'English' }, fr: { label: 'French' } }, + defaultLocale: 'en', +} satisfies StarlightUserConfig); + +const useTranslations = createTranslationSystemFromFs( + starlightConfig, + // Using non-existent `_src/` to ignore custom files in this test fixture. + { srcDir: new URL('./_src/', import.meta.url) } +); + +const processor = await createMarkdownProcessor({ + remarkPlugins: [ + ...starlightAsides({ + starlightConfig, + astroConfig: { root: new URL(import.meta.url), srcDir: new URL('./_src/', import.meta.url) }, + useTranslations, + }), + ], +}); + +test('generates ${label}

`); + }); +}); + +describe('custom labels', () => { + test.each(['note', 'tip', 'caution', 'danger'])('%s with custom label', async (type) => { + const label = 'Custom Label'; + const res = await processor.render(` +:::${type}[${label}] +Some text +::: + `); + expect(res.code).includes(`aria-label="${label}"`); + expect(res.code).includes(`${label}

`); + }); +}); + +test('ignores unknown directive variants', async () => { + const res = await processor.render(` +:::unknown +Some text +::: +`); + expect(res.code).toMatchInlineSnapshot('"

Some text

"'); +}); + +test('handles complex children', async () => { + const res = await processor.render(` +:::note +Paragraph [link](/href/). + +![alt](/img.jpg) + +
+See more + +More. + +
+::: +`); + expect(res.code).toMatchFileSnapshot('./snapshots/handles-complex-children.html'); +}); + +test('nested asides', async () => { + const res = await processor.render(` +::::note +Note contents. + +:::tip +Nested tip. +::: + +:::: +`); + expect(res.code).toMatchFileSnapshot('./snapshots/nested-asides.html'); +}); + +describe('translated labels in French', () => { + test.each([ + ['note', 'Note'], + ['tip', 'Astuce'], + ['caution', 'Attention'], + ['danger', 'Danger'], + ])('%s has label %s', async (type, label) => { + const res = await processor.render( + ` +:::${type} +Some text +::: +`, + { fileURL: new URL('./_src/content/docs/fr/index.md', import.meta.url) } + ); + expect(res.code).includes(`aria-label="${label}"`); + expect(res.code).includes(`${label}

`); + }); +}); + +test('runs without locales config', async () => { + const processor = await createMarkdownProcessor({ + remarkPlugins: [ + ...starlightAsides({ + starlightConfig: { locales: undefined }, + astroConfig: { + root: new URL(import.meta.url), + srcDir: new URL('./_src/', import.meta.url), + }, + useTranslations, + }), + ], + }); + const res = await processor.render(':::note\nTest\n::'); + expect(res.code.includes('aria-label=Note"')); +}); diff --git a/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts b/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts new file mode 100644 index 00000000000..f1f3b1c3155 --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts @@ -0,0 +1,91 @@ +import { expect, test } from 'vitest'; +import { processPanels, TabItemTagname } from '../../user-components/rehype-tabs'; + +const TabItem = ({ label, slot }: { label: string; slot: string }) => + `<${TabItemTagname} data-label="${label}">${slot}`; + +/** Get an array of HTML strings, one for each `
` created by rehype-tabs for each tab item. */ +const extractSections = (html: string) => + [...html.matchAll(//g)].map(([section]) => section); + +test('empty component returns no html or panels', () => { + const { panels, html } = processPanels(''); + expect(html).toEqual(''); + expect(panels).toEqual([]); +}); + +test('non-tab-item content is passed unchanged', () => { + const input = '

Random paragraph

'; + const { panels, html } = processPanels(input); + expect(html).toEqual(input); + expect(panels).toEqual([]); +}); + +test('tab items are processed', () => { + const label = 'Test'; + const slot = '

Random paragraph

'; + const input = TabItem({ label, slot }); + const { panels, html } = processPanels(input); + + expect(html).toMatchInlineSnapshot( + '"

Random paragraph

"' + ); + expect(panels).toHaveLength(1); + expect(panels?.[0]?.label).toBe(label); + expect(panels?.[0]?.panelId).toMatchInlineSnapshot('"tab-panel-0"'); + expect(panels?.[0]?.tabId).toMatchInlineSnapshot('"tab-0"'); +}); + +test('only first item is not hidden', () => { + const labels = ['One', 'Two', 'Three']; + const input = labels.map((label) => TabItem({ label, slot: `
${label}
` })).join(''); + const { panels, html } = processPanels(input); + + expect(panels).toHaveLength(3); + expect(html).toMatchInlineSnapshot( + '"
One
"' + ); + const sections = extractSections(html); + expect(sections).toMatchInlineSnapshot(` + [ + "
One
", + "", + "", + ] + `); + expect(sections.map((section) => section.includes('hidden'))).toEqual([false, true, true]); +}); + +test('applies incrementing ID and aria-labelledby to each tab item', () => { + const labels = ['One', 'Two', 'Three']; + const input = labels.map((label) => TabItem({ label, slot: `
${label}
` })).join(''); + const { panels, html } = processPanels(input); + + // IDs are incremented globally to ensure they are unique, so we need to extract from the panel data. + const firstTabIdMatches = panels?.[0]?.tabId.match(/^tab-(\d)+$/); + const firstTabId = parseInt(firstTabIdMatches![1]!, 10); + + extractSections(html).forEach((section, index) => { + expect(section).includes(`id="tab-panel-${firstTabId + index}"`); + expect(section).includes(`aria-labelledby="tab-${firstTabId + index}"`); + }); +}); + +test('applies tabindex="0" to tab items without focusable content', () => { + const input = [ + TabItem({ label: 'Focusable', slot: `` }), + TabItem({ label: 'Not Focusable', slot: `
Plain text
` }), + TabItem({ + label: 'Focusable Nested', + slot: `

`, + }), + ].join(''); + const { html } = processPanels(input); + expect(html).toMatchInlineSnapshot( + '"
"' + ); + const sections = extractSections(html); + expect(sections[0]).not.includes('tabindex="0"'); + expect(sections[1]).includes('tabindex="0"'); + expect(sections[2]).not.includes('tabindex="0"'); +}); diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside.html b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside.html new file mode 100644 index 00000000000..fb95a152382 --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/snapshots/generates-aside.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/handles-complex-children.html b/packages/starlight/__tests__/remark-rehype/snapshots/handles-complex-children.html new file mode 100644 index 00000000000..8cb4524903b --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/snapshots/handles-complex-children.html @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/packages/starlight/__tests__/remark-rehype/snapshots/nested-asides.html b/packages/starlight/__tests__/remark-rehype/snapshots/nested-asides.html new file mode 100644 index 00000000000..51da5fcc4b3 --- /dev/null +++ b/packages/starlight/__tests__/remark-rehype/snapshots/nested-asides.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts new file mode 100644 index 00000000000..5e280af7ddf --- /dev/null +++ b/packages/starlight/__tests__/sidebar/navigation-unicode.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, test, vi } from 'vitest'; +import { getSidebar } from '../../utils/navigation'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['environmental-impact.md', { title: 'Eco-friendly docs' }], + ['reference/configuration.mdx', { title: 'Config Reference' }], + ['reference/frontmatter.md', { title: 'Frontmatter Reference' }], + // @ts-expect-error — Using a slug not present in Starlight docs site + ['api/v1/用户.md', { title: 'Path with non-ASCII characters' }], + ['guides/components.mdx', { title: 'Components' }], + ], + }) +); + +describe('getSidebar', () => { + test('matches current page when path contains non-ascii characters', () => { + expect(getSidebar('/api/v1/%E7%94%A8%E6%88%B7', undefined)).toMatchInlineSnapshot(` + [ + { + "attrs": {}, + "badge": undefined, + "href": "/", + "isCurrent": false, + "label": "Home", + "type": "link", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": { + "text": "New", + "variant": "success", + }, + "href": "/intro/", + "isCurrent": false, + "label": "Introduction", + "type": "link", + }, + { + "attrs": {}, + "badge": { + "text": "Deprecated", + "variant": "default", + }, + "href": "/next-steps/", + "isCurrent": false, + "label": "Next Steps", + "type": "link", + }, + { + "attrs": { + "class": "showcase-link", + "target": "_blank", + }, + "badge": undefined, + "href": "/showcase/", + "isCurrent": false, + "label": "Showcase", + "type": "link", + }, + ], + "label": "Start Here", + "type": "group", + }, + { + "badge": { + "text": "Experimental", + "variant": "default", + }, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/reference/configuration/", + "isCurrent": false, + "label": "Config Reference", + "type": "link", + }, + { + "attrs": {}, + "badge": undefined, + "href": "/reference/frontmatter/", + "isCurrent": false, + "label": "Frontmatter Reference", + "type": "link", + }, + ], + "label": "Reference", + "type": "group", + }, + { + "badge": undefined, + "collapsed": false, + "entries": [ + { + "attrs": {}, + "badge": undefined, + "href": "/api/v1/用户/", + "isCurrent": true, + "label": "Path with non-ASCII characters", + "type": "link", + }, + ], + "label": "API v1", + "type": "group", + }, + ] + `); + }); +}); diff --git a/packages/starlight/__tests__/test-utils.ts b/packages/starlight/__tests__/test-utils.ts index b9b0e8991af..96ff4f649e9 100644 --- a/packages/starlight/__tests__/test-utils.ts +++ b/packages/starlight/__tests__/test-utils.ts @@ -17,6 +17,7 @@ const frontmatterSchema = docsSchema()({ z.literal('webp'), z.literal('gif'), z.literal('svg'), + z.literal('avif'), ]), }), }); diff --git a/packages/starlight/components/Icons.ts b/packages/starlight/components/Icons.ts index f6910737782..6446a5ae8a0 100644 --- a/packages/starlight/components/Icons.ts +++ b/packages/starlight/components/Icons.ts @@ -99,4 +99,6 @@ export const Icons = { '', patreon: '', + slack: + '', }; diff --git a/packages/starlight/components/MarkdownContent.astro b/packages/starlight/components/MarkdownContent.astro index ffb0108a6d0..0d40cd47ed2 100644 --- a/packages/starlight/components/MarkdownContent.astro +++ b/packages/starlight/components/MarkdownContent.astro @@ -1,127 +1,6 @@ --- import type { Props } from '../props'; +import '../style/markdown.css'; --- -
- - +
diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts index 53ffb2e20cc..01dc25f1e6c 100644 --- a/packages/starlight/index.ts +++ b/packages/starlight/index.ts @@ -49,7 +49,13 @@ export default function StarlightIntegration(opts: StarlightUserConfig): AstroIn plugins: [vitePluginStarlightUserConfig(userConfig, config)], }, markdown: { - remarkPlugins: [...starlightAsides()], + remarkPlugins: [ + ...starlightAsides({ + starlightConfig: userConfig, + astroConfig: config, + useTranslations, + }), + ], rehypePlugins: [rehypeRtlCodeSupport()], shikiConfig: // Configure Shiki theme if the user is using the default github-dark theme. diff --git a/packages/starlight/integrations/asides.ts b/packages/starlight/integrations/asides.ts index 5d2107ad79e..a97a610ae0a 100644 --- a/packages/starlight/integrations/asides.ts +++ b/packages/starlight/integrations/asides.ts @@ -1,10 +1,45 @@ -import type { AstroUserConfig } from 'astro'; +import type { AstroConfig, AstroUserConfig } from 'astro'; import { h as _h, s as _s, type Properties } from 'hastscript'; import type { Paragraph as P, Root } from 'mdast'; import remarkDirective from 'remark-directive'; import type { Plugin, Transformer } from 'unified'; import { remove } from 'unist-util-remove'; import { visit } from 'unist-util-visit'; +import type { StarlightConfig } from '../types'; +import type { createTranslationSystemFromFs } from '../utils/translations-fs'; + +interface AsidesOptions { + starlightConfig: { locales: StarlightConfig['locales'] }; + astroConfig: { root: AstroConfig['root']; srcDir: AstroConfig['srcDir'] }; + useTranslations: ReturnType; +} + +function pathToLocale( + slug: string | undefined, + config: AsidesOptions['starlightConfig'] +): string | undefined { + const locales = Object.keys(config.locales || {}); + const baseSegment = slug?.split('/')[0]; + if (baseSegment && locales.includes(baseSegment)) return baseSegment; + return undefined; +} + +/** get current lang from file full path */ +function getLocaleFromPath( + unformattedPath: string | undefined, + { starlightConfig, astroConfig }: AsidesOptions +): string | undefined { + const srcDir = new URL(astroConfig.srcDir, astroConfig.root); + const docsDir = new URL('content/docs/', srcDir); + const path = unformattedPath + // Format path to unix style path. + ?.replace(/\\/g, '/') + // Strip docs path leaving only content collection file ID. + // Example: /Users/houston/repo/src/content/docs/en/guide.md => en/guide.md + .replace(docsDir.pathname, ''); + const locale = pathToLocale(path, starlightConfig); + return locale; +} /** Hacky function that generates an mdast HTML tree ready for conversion to HTML by rehype. */ function h(el: string, attrs: Properties = {}, children: any[] = []): P { @@ -50,19 +85,11 @@ function s(el: string, attrs: Properties = {}, children: any[] = []): P { * * ``` */ -function remarkAsides(): Plugin<[], Root> { +function remarkAsides(options: AsidesOptions): Plugin<[], Root> { type Variant = 'note' | 'tip' | 'caution' | 'danger'; const variants = new Set(['note', 'tip', 'caution', 'danger']); const isAsideVariant = (s: string): s is Variant => variants.has(s); - // TODO: hook these up for i18n once the design for translating strings is ready - const defaultTitles = { - note: 'Note', - tip: 'Tip', - caution: 'Caution', - danger: 'Danger', - }; - const iconPaths = { // Information icon note: [ @@ -95,7 +122,9 @@ function remarkAsides(): Plugin<[], Root> { ], }; - const transformer: Transformer = (tree) => { + const transformer: Transformer = (tree, file) => { + const locale = getLocaleFromPath(file.history[0], options); + const t = options.useTranslations(locale); visit(tree, (node, index, parent) => { if (!parent || index === null || node.type !== 'containerDirective') { return; @@ -107,7 +136,7 @@ function remarkAsides(): Plugin<[], Root> { // its children, but we want to pass it as the title prop to