Skip to content

Commit

Permalink
Merge branch 'main' into hd-astro-v5
Browse files Browse the repository at this point in the history
* main:
  [ci] release (withastro#2634)
  ci: update file icons (withastro#2663)
  i18n(ru): update ru.json (withastro#2642)
  Improve error message with invalid configuration (withastro#2656)
  docs: use single listener in icons reference (withastro#2657)
  fix: use seti ui repo to generate file icons (withastro#2648)
  Fix favicon support for query and fragment in URLs (withastro#2645)
  • Loading branch information
HiDeoo committed Dec 13, 2024
2 parents 000efa1 + 0d1f8cb commit 3f41484
Show file tree
Hide file tree
Showing 17 changed files with 161 additions and 71 deletions.
5 changes: 0 additions & 5 deletions .changeset/serious-badgers-build.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/strange-icons-explode.md

This file was deleted.

27 changes: 14 additions & 13 deletions docs/src/components/icons-list.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,22 @@ const icons = Object.keys(Icons) as (keyof typeof Icons)[];
</div>

<script>
const iconGrid = document.querySelector<HTMLDivElement>('.icons-grid');
const copiedLabel = document.querySelector<HTMLDivElement>('.icons-grid')?.dataset.labelCopied!;
const icons = document.querySelectorAll<HTMLButtonElement>('.icon-preview');
icons.forEach((icon) => {
icon.addEventListener('click', () => {
const iconName = icon.dataset.name!;
navigator.clipboard.writeText(iconName);
iconGrid?.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return;
const icon = event.target.closest('.icon-preview');
if (!(icon instanceof HTMLButtonElement)) return;
const iconName = icon.dataset.name!;
navigator.clipboard.writeText(iconName);

const iconLabel = icon.querySelector('[aria-live]');
if (iconLabel) {
iconLabel.textContent = copiedLabel;
setTimeout(() => {
iconLabel.textContent = iconName;
}, 1000);
}
});
const iconLabel = icon.querySelector('[aria-live]');
if (iconLabel) {
iconLabel.textContent = copiedLabel;
setTimeout(() => {
iconLabel.textContent = iconName;
}, 1000);
}
});
</script>

Expand Down
2 changes: 1 addition & 1 deletion examples/basics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.29.2",
"@astrojs/starlight": "^0.29.3",
"astro": "^5.0.2",
"sharp": "^0.32.5"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/markdoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/markdoc": "^0.12.1",
"@astrojs/starlight": "^0.29.2",
"@astrojs/starlight": "^0.29.3",
"@astrojs/starlight-markdoc": "^0.1.0",
"astro": "^5.0.2",
"sharp": "^0.32.5"
Expand Down
2 changes: 1 addition & 1 deletion examples/tailwind/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.29.2",
"@astrojs/starlight": "^0.29.3",
"@astrojs/starlight-tailwind": "^2.0.3",
"@astrojs/tailwind": "^5.1.3",
"astro": "^5.0.2",
Expand Down
24 changes: 14 additions & 10 deletions packages/file-icons-generator/index.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { writeDefinitionsAndSVGs } from './utils/file';
import { getIconSvgPaths } from './utils/font';
import { fetchFont, fetchMapping, parseMapping } from './utils/seti';
import { deleteRepo, parseMapping, setupRepo } from './utils/seti';

/**
* Script generating definitions used by the Starlight `<FileTree>` component and associated SVGs.
*
* To do so, it fetches the Seti UI icon mapping file and font from GitHub, parses the mapping to
* generate the definitions and a list of icons to extract as SVGs, and finally extracts the SVGs
* from the font and writes the definitions and SVGs to the Starlight package in a file ready to be
* consumed by Starlight.
* To do so, it clones the Seti UI repository, installs dependencies, generates icons, parses the
* mapping to generate the definitions and a list of icons to extract as SVGs, and finally extracts
* the SVGs from the font and writes the definitions and SVGs to the Starlight package in a file
* ready to be consumed by Starlight.
*
* @see {@link file://./config.ts} for the configuration used by this script.
* @see {@link file://../starlight/user-components/file-tree-icons.ts} for the generated file.
* @see {@link https://opentype.js.org/glyph-inspector.html} for a font glyph inspector.
*/

const mapping = await fetchMapping();
const { definitions, icons } = parseMapping(mapping);
const repoPath = await setupRepo();

const font = await fetchFont();
const svgPaths = getIconSvgPaths(icons, definitions, font);
try {
const { definitions, icons } = await parseMapping(repoPath);

await writeDefinitionsAndSVGs(definitions, svgPaths);
const svgPaths = await getIconSvgPaths(repoPath, icons, definitions);

await writeDefinitionsAndSVGs(definitions, svgPaths);
} finally {
await deleteRepo(repoPath);
}
10 changes: 4 additions & 6 deletions packages/file-icons-generator/utils/font.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import opentype, { type Font, Glyph } from 'opentype.js';
import { seti, starlight } from '../config';
import type { Definitions } from '../../starlight/user-components/rehype-file-tree';
import { getSetiIconName } from './seti';
import { getFont, getSetiIconName } from './seti';

// This matches the default precision used by the SVGO default preset.
const pathDecimalPrecision = 3;

/** Extract SVG paths from the Seti UI icon font from a list of icon names matching font glyphs. */
export function getIconSvgPaths(
icons: string[],
definitions: Definitions,
fontBuffer: ArrayBuffer
) {
export async function getIconSvgPaths(repoPath: string, icons: string[], definitions: Definitions) {
const fontBuffer = await getFont(repoPath);

const iconSvgs: Record<string, string> = {};

let font: Font;
Expand Down
69 changes: 53 additions & 16 deletions packages/file-icons-generator/utils/seti.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { spawnSync, type SpawnSyncOptions } from 'node:child_process';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { seti, starlight } from '../config';
import type { Definitions } from '../../starlight/user-components/rehype-file-tree.ts';

Expand All @@ -7,33 +11,56 @@ import type { Definitions } from '../../starlight/user-components/rehype-file-tr
const mappingRegex =
/^\.icon-(?<type>(set|partial))\((?<quote>['"])(?<identifier>.+)\k<quote>, \k<quote>(?<lang>.+)\k<quote>, @.+\);$/;

/** Fetch the Seti UI icon mapping file from GitHub. */
export async function fetchMapping() {
/** Clone the Seti UI repository, install dependencies, and generate the Seti UI icons. */
export async function setupRepo() {
try {
const result = await fetch(getGitHubDownloadLink(seti.repo, seti.mapping));
return await result.text();
const repoPath = await fs.mkdtemp(path.join(os.tmpdir(), 'starlight-file-icons-'));

const spawnOptions: SpawnSyncOptions = {
cwd: repoPath,
encoding: 'utf8',
};

let result = spawnSync('git', ['clone', `https://github.com/${seti.repo}`, '.'], spawnOptions);
if (result.error) throw new Error('Failed to clone the Seti UI repository.');

result = spawnSync('npm', ['install'], spawnOptions);
if (result.error) throw new Error('Failed to install the Seti UI dependencies.');

result = spawnSync('npm', ['run', 'createIcons'], spawnOptions);
if (result.error) throw new Error('Failed to generate the Seti UI icons.');

return repoPath;
} catch (error) {
throw new Error(
'Failed to download Seti UI icon mapping file. Make sure the repository URL and mapping path are correct.',
'Failed to setup the Seti UI repo. Make sure the repository URL and font path are correct.',
{ cause: error }
);
}
}

/** Delete the Seti UI repository. */
export async function deleteRepo(repoPath: string) {
try {
await fs.rm(repoPath, { force: true, recursive: true });
} catch (error) {
throw new Error('Failed to remove the Seti UI repo.', { cause: error });
}
}

/**
* Fetch the Seti UI icon font from GitHub.
* Get the Seti UI icon font from a local repository.
* Note that the `woff` font format is used and not `woff2` as we would manually need to decompress
* it and we do not need the compression benefits for this use case.
*/
export async function fetchFont() {
export async function getFont(repoPath: string) {
try {
const result = await fetch(getGitHubDownloadLink(seti.repo, seti.font));
return await result.arrayBuffer();
const result = await fs.readFile(path.join(repoPath, seti.font));
return new Uint8Array(result).buffer;
} catch (error) {
throw new Error(
'Failed to download Seti UI font. Make sure the repository URL and font path are correct.',
{ cause: error }
);
throw new Error('Failed to read Seti UI font. Make sure the font path is correct.', {
cause: error,
});
}
}

Expand All @@ -42,7 +69,9 @@ export async function fetchFont() {
* component and a list of Seti UI icons to extract as SVGs.
* @see https://github.com/elviswolcott/seti-icons/blob/master/build/extract.ts
*/
export function parseMapping(mapping: string) {
export async function parseMapping(repoPath: string) {
const mapping = await getMapping(repoPath);

const lines = mapping.split('\n');
// Include the `folder` icon by default as it is not defined in the mapping file.
const icons = new Set<string>(['folder']);
Expand Down Expand Up @@ -87,6 +116,14 @@ export function getSetiIconName(icon: string) {
return `${starlight.prefix}${name}`;
}

function getGitHubDownloadLink(repo: string, path: string) {
return `https://raw.githubusercontent.com/${repo}/${seti.branch}/${path}`;
/** Get the Seti UI icon mapping file from a local repository. */
async function getMapping(repoPath: string) {
try {
return await fs.readFile(path.join(repoPath, seti.mapping), 'utf8');
} catch (error) {
throw new Error(
'Failed to read Seti UI icon mapping file. Make sure the mapping file path is correct.',
{ cause: error }
);
}
}
16 changes: 16 additions & 0 deletions packages/starlight/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# @astrojs/starlight

## 0.29.3

### Patch Changes

- [#2642](https://github.com/withastro/starlight/pull/2642) [`12750ae`](https://github.com/withastro/starlight/commit/12750ae1bc303f2c53efd25adf01428e54aced90) Thanks [@dragomano](https://github.com/dragomano)! - Updates Russian UI translations

- [#2656](https://github.com/withastro/starlight/pull/2656) [`4d543be`](https://github.com/withastro/starlight/commit/4d543bec280f3b5e00e21727d78f25756a1ced75) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Improves error message when an invalid configuration or no configuration is provided to the Starlight integration.

- [#2645](https://github.com/withastro/starlight/pull/2645) [`cf12beb`](https://github.com/withastro/starlight/commit/cf12beb91b4cb2f212dbcc0cc1ed56e79d055ff0) Thanks [@techfg](https://github.com/techfg)! - Fixes support for favicon URLs that contain a search query and/or hash

- [#2650](https://github.com/withastro/starlight/pull/2650) [`38db4ec`](https://github.com/withastro/starlight/commit/38db4ecfdb572b1f6362aca544f72f5128f5fe08) Thanks [@raviqqe](https://github.com/raviqqe)! - Moves `@types/js-yaml` package to non-dev dependencies

- [#2633](https://github.com/withastro/starlight/pull/2633) [`5adb720`](https://github.com/withastro/starlight/commit/5adb720afd354d99b3682d045b9dc8729a1ff274) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes a VoiceOver issue with Safari where the content of a `<script>` element could be read before the sidebar content.

- [#2663](https://github.com/withastro/starlight/pull/2663) [`34755f9`](https://github.com/withastro/starlight/commit/34755f9c5f2fa451e8a56aecf3ff5a6ff499767b) Thanks [@astrobot-houston](https://github.com/astrobot-houston)! - Adds a new `seti:vite` icon for Vite configuration files in the `<FileTree>` component

## 0.29.2

### Patch Changes
Expand Down
27 changes: 27 additions & 0 deletions packages/starlight/__tests__/basics/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ describe('FaviconSchema', () => {
expect(favicon.type).toBe('image/jpeg');
});

test('returns the proper href and type attributes when contains query', () => {
const icon = '/custom-icon.gif?v=123456&x=987654';

const favicon = FaviconSchema().parse(icon);

expect(favicon.href).toBe(icon);
expect(favicon.type).toBe('image/gif');
});

test('returns the proper href and type attributes when contains fragment', () => {
const icon = '/custom-icon.png#favicon';

const favicon = FaviconSchema().parse(icon);

expect(favicon.href).toBe(icon);
expect(favicon.type).toBe('image/png');
});

test('returns the proper href and type attributes when contains query and fragment', () => {
const icon = '/custom-icon.ico?v=123456&x=987654#favicon';

const favicon = FaviconSchema().parse(icon);

expect(favicon.href).toBe(icon);
expect(favicon.type).toBe('image/x-icon');
});

test('throws on invalid favicon extensions', () => {
expect(() => FaviconSchema().parse('/favicon.pdf')).toThrow();
});
Expand Down
15 changes: 11 additions & 4 deletions packages/starlight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import mdx from '@astrojs/mdx';
import type { AstroIntegration } from 'astro';
import { AstroError } from 'astro/errors';
import { spawn } from 'node:child_process';
import { dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
Expand All @@ -27,10 +28,16 @@ import {
import { processI18nConfig } from './utils/i18n';
import type { StarlightConfig } from './types';

export default function StarlightIntegration({
plugins,
...opts
}: StarlightUserConfigWithPlugins): AstroIntegration {
export default function StarlightIntegration(
userOpts: StarlightUserConfigWithPlugins
): AstroIntegration {
if (typeof userOpts !== 'object' || userOpts === null || Array.isArray(userOpts))
throw new AstroError(
'Invalid config passed to starlight integration',
`The Starlight integration expects a configuration object with at least a \`title\` property.\n\n` +
`See more details in the [Starlight configuration reference](https://starlight.astro.build/reference/configuration/)\n`
);
const { plugins, ...opts } = userOpts;
let userConfig: StarlightConfig;
let pluginTranslations: PluginTranslations = {};
return {
Expand Down
2 changes: 1 addition & 1 deletion packages/starlight/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@astrojs/starlight",
"version": "0.29.2",
"version": "0.29.3",
"description": "Build beautiful, high-performance documentation websites with Astro",
"scripts": {
"test": "vitest",
Expand Down
4 changes: 3 additions & 1 deletion packages/starlight/schemas/favicon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export const FaviconSchema = () =>
.string()
.default('/favicon.svg')
.transform((favicon, ctx) => {
const ext = extname(favicon).toLowerCase();
// favicon can be absolute or relative url
const { pathname } = new URL(favicon, 'https://example.com');
const ext = extname(pathname).toLowerCase();

if (!isFaviconExt(ext)) {
ctx.addIssue({
Expand Down
4 changes: 2 additions & 2 deletions packages/starlight/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"search.label": "Поиск",
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "Отменить",
"search.devWarning": "Поиск доступен только в производственных сборках. \nПопробуйте выполнить сборку и просмотреть сайт, чтобы протестировать его локально.",
"search.devWarning": "Поиск доступен только в продакшен-сборках. \nВыполните сборку и запустите превью, чтобы протестировать поиск локально.",
"themeSelect.accessibleLabel": "Выберите тему",
"themeSelect.dark": "Тёмная",
"themeSelect.light": "Светлая",
Expand All @@ -18,7 +18,7 @@
"page.lastUpdated": "Последнее обновление:",
"page.previousLink": "Предыдущая",
"page.nextLink": "Следующая",
"page.draft": "Этот контент является черновиком и не будет добавлен в производственные сборки.",
"page.draft": "Этот контент является черновиком и не будет добавлен в продакшен-сборки.",
"404.text": "Страница не найдена. Проверьте URL или используйте поиск по сайту.",
"aside.note": "Заметка",
"aside.tip": "Совет",
Expand Down
Loading

0 comments on commit 3f41484

Please sign in to comment.