Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/components/icon-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const rows = (entries: Entry[], { skipDefaults = false } = {}) =>
icons: resolvers.map((icons, i) => {
const set = fileIconSets[i] ?? 'seti';
const scale = set === 'seti' ? 1 : iconSetScale[set];
const name = names[i] ?? '';
const name = names[i] || '';
const svgs = icons.svgs(name);
const html = svgs
.map(({ svg, set: from }, variant) => {
Expand Down
13 changes: 13 additions & 0 deletions docs/src/content/docs/features/file-icons.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ codeblocks({
- In the Seti set, the full name comes first, such as `Dockerfile`. Then the extension, such as `.test.ts` and then `.ts`.
- Then the language of the code block, such as `py` or `python`.
- Then the default file icon.
- A `false` in `fileIcons.files` or `fileIcons.languages` stops the search, and the block gets no icon. The `icon` attribute still sets one.
- When two keys of `fileIcons.files` match a title, the later key wins.
- Where icons show:
- In the title of an editor frame. Terminal frames and code blocks with no title get no icon.
- [Code tabs](/starlight-codeblocks/features/code-tabs/) show the icon of each file on its tab. The code tabs menu shows the icon of each language, custom icons too.
Expand Down Expand Up @@ -282,6 +284,17 @@ codeblocks({
});
```

To show no icon for a file type or a language, use `false`. Here, `.mmd` files and `metro` blocks get no icon, but `overview.mmd` gets the Markdown icon. It comes after `.mmd`, so it wins:

```js title="astro.config.mjs"
codeblocks({
fileIcons: {
files: { '.mmd': false, 'overview.mmd': 'markdown' },
languages: { metro: { icon: false } },
},
});
```

An icon in `fileIcons.icons` is SVG markup, or the path data of a 24 by 24 icon. An icon with no `fill` takes the colour of the icon. An icon with its own colours keeps them.

The size and the default colours are style settings in the `codeblocksFileIcons` group.
Expand Down
29 changes: 16 additions & 13 deletions packages/starlight-codeblocks/src/expressive-code/file-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const fileIconSets = ['seti', 'material', 'vscode-icons', 'catppuccin'] a
export type FileIconSet = (typeof fileIconSets)[number];

export interface FileIconLanguage {
icon?: string;
/** `false` for no icon. */
icon?: string | false;
colour?: string;
style?: FileIconStyle;
}
Expand All @@ -22,7 +23,8 @@ export interface FileIconSettings {
set: FileIconSet;
style: FileIconStyle;
languages: Record<string, FileIconLanguage>;
files: Record<string, string>;
/** `false` for no icon. */
files: Record<string, string | false>;
icons: Record<string, string>;
}

Expand Down Expand Up @@ -279,26 +281,26 @@ export async function fileIconResolver({
const byLanguage = new Map(
Object.entries(languages).map(([lang, settings]) => {
const id = languageId(lang);
if (!settings.icon?.trim().startsWith('<')) return [id, settings];
if (typeof settings.icon !== 'string' || !settings.icon.trim().startsWith('<')) return [id, settings];
icons[`language:${id}`] = settings.icon;
return [id, { ...settings, icon: `language:${id}` }];
}),
);

const markup = (name: string) => icons[name] ?? iconSet.markup(name) ?? seti.markup(name);

/** The icon name for the file path in a title, or `undefined`. */
function forFileName(title: string) {
/** The icon name for the file path in a title, `false` for no icon, or `undefined`. */
function forFileName(title: string): string | false | undefined {
const path = title.trim().replaceAll('\\', '/');
if (!path) return undefined;
return rules.find(([matches]) => matches(path))?.[1] ?? iconSet.forPath(path);
}

/** The icon name for a code block language, or `undefined`. */
function forLanguage(lang: string) {
/** The icon name for a code block language, `false` for no icon, or `undefined`. */
function forLanguage(lang: string): string | false | undefined {
const id = languageId(lang.toLowerCase());
const own = byLanguage.get(id)?.icon;
if (own) return own;
if (own !== undefined) return own;
return iconSet.forLanguage([...new Set([lang.toLowerCase(), id, ...(bundledLanguage(id)?.aliases ?? [])])]);
}

Expand All @@ -315,7 +317,7 @@ export async function fileIconResolver({
return svg ? [{ svg, coloured: isColoured(source), set: fromSet ? set : undefined }] : [];
});
},
/** The icon name for a block, from its title, then its language, then the default of the set. */
/** The icon name for a block, from its title, then its language, then the default of the set. `false` stops the search. */
nameFor: (title: string, language: string) => forFileName(title) ?? forLanguage(language) ?? iconSet.fallback,
svg: (name: string) => {
const source = markup(name);
Expand All @@ -340,7 +342,7 @@ export function pluginFileIcons(settings: FileIconSettings): CodeblocksPlugin {
...Object.values(settings.files),
...Object.keys(settings.languages).map((lang) => icons.languageSettings(lang)?.icon),
];
const unknown = named.find((name) => name !== undefined && !icons.has(name));
const unknown = named.find((name) => typeof name === 'string' && !icons.has(name));
if (unknown) {
throw new Error(
`starlight-codeblocks: \`fileIcons\` names the icon "${unknown}", which is not in the \`${set}\` set or in \`fileIcons.icons\`.`,
Expand Down Expand Up @@ -427,8 +429,9 @@ ${Object.entries(iconSetScale)
warn(context, `\`icon="${name}"\` is not a known icon. The block uses the icon of its title or language.`);
name = undefined;
}
name ??= icons.nameFor(String(codeBlock.props.title ?? ''), codeBlock.language);
const variants = icons.svgs(name);
const resolved = name ?? icons.nameFor(String(codeBlock.props.title ?? ''), codeBlock.language);
if (resolved === false) return;
const variants = icons.svgs(resolved);
if (variants.length === 0) return;

const language = icons.languageSettings(codeBlock.language);
Expand All @@ -451,7 +454,7 @@ ${Object.entries(iconSetScale)
svg.properties = {
class: ICON,
dataScbFileIcon: style,
dataScbFileIconName: name.replace(/^seti:/, ''),
dataScbFileIconName: resolved.replace(/^seti:/, ''),
...(variantNames[i] && { dataScbFileIconVariant: variantNames[i] }),
...(coloured && { dataScbFileIconColoured: '' }),
...(set && { dataScbFileIconSet: set }),
Expand Down
14 changes: 7 additions & 7 deletions packages/starlight-codeblocks/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export interface CodeblocksOptions {
set?: FileIconSet;
style?: FileIconStyle;
languages?: Record<string, FileIconLanguage>;
files?: Record<string, string>;
files?: Record<string, string | false>;
icons?: Record<string, string>;
};
codeLinks?: false;
Expand Down Expand Up @@ -388,25 +388,25 @@ export const optionsReference: Record<keyof CodeblocksOptions, Feature> = {
valid: oneOf('plain', 'tile'),
},
languages: {
type: "Record<string, { icon?: string; colour?: string; style?: 'plain' | 'tile' }>",
type: "Record<string, { icon?: string | false; colour?: string; style?: 'plain' | 'tile' }>",
default: {},
description:
'Settings for the blocks of each language: the icon when the title gives none, as an icon name or SVG markup, a CSS colour for the icon or the tile, and the style.',
'Settings for the blocks of each language: the icon when the title gives none, as an icon name or SVG markup, or `false` for none, a CSS colour for the icon or the tile, and the style.',
valid: isRecordOf(
(language) =>
isObject(language) &&
Object.keys(language).every((key) => ['icon', 'colour', 'style'].includes(key)) &&
(language.icon === undefined || isString(language.icon)) &&
(language.icon === undefined || language.icon === false || isString(language.icon)) &&
(language.colour === undefined || (isString(language.colour) && isCssColour(language.colour))) &&
(language.style === undefined || oneOf('plain', 'tile')(language.style)),
),
},
files: {
type: 'Record<string, string>',
type: 'Record<string, string | false>',
default: {},
description:
'Icon names by file name, such as `nextflow.config`, by extension, such as `.nf`, or by path pattern, such as `.github/**`. A `*` matches within one folder, and `**` across folders. They come before the built-in rules.',
valid: isRecordOf(isString),
'Icon names, or `false` for no icon, by file name, such as `nextflow.config`, by extension, such as `.nf`, or by path pattern, such as `.github/**`. A `*` matches within one folder, and `**` across folders. They come before the built-in rules.',
valid: isRecordOf((icon) => icon === false || isString(icon)),
},
icons: {
type: 'Record<string, string>',
Expand Down
27 changes: 27 additions & 0 deletions packages/starlight-codeblocks/test/file-icons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,34 @@ test('custom icons, file names and languages', async () => {
).rejects.toThrow('"missing"');
});

test('`false` in `files` or `languages` gives no icon, and `icon="<name>"` still sets one', async () => {
const fileIcons = {
files: { '.mmd': false as const, 'special.mmd': 'markdown' },
languages: { metro: { icon: false as const, colour: '#f00' } },
};
const iconOf = async (meta: string) => icon((await render(block(meta, 'x'), { fileIcons })).html);
expect(await iconOf('txt title="flow.mmd"')).toBeUndefined();
// A `false` rule stops the search, so the language gives no icon either.
expect(await iconOf('js title="flow.mmd"')).toBeUndefined();
expect(iconName((await render(block('txt title="special.mmd"', 'x'), { fileIcons })).html)).toBe('markdown');
expect(await iconOf('metro title="Example"')).toBeUndefined();
expect(iconName((await render(block('metro title="flow.js"', 'x'), { fileIcons })).html)).toBe('javascript');
for (const meta of ['txt title="flow.mmd" icon="react"', 'metro title="Example" icon="react"']) {
expect(iconName((await render(block(meta, 'x'), { fileIcons })).html), meta).toBe('react');
}
expect(await iconOf('js title="a.js" icon=false')).toBeUndefined();
expect(iconName((await render(block('js title="a.js"', 'x'), { fileIcons })).html)).toBe('javascript');
expect(iconName((await render(block('txt title="flow.mmd"', 'x'))).html)).toBe('default');
});

test('options are validated', () => {
expect(
resolveOptions({ fileIcons: { files: { '.mmd': false }, languages: { metro: { icon: false } } } }).fileIcons,
).toMatchObject({ files: { '.mmd': false }, languages: { metro: { icon: false } } });
expect(() => resolveOptions({ fileIcons: { files: { '.mmd': true as never } } })).toThrow('fileIcons.files');
expect(() => resolveOptions({ fileIcons: { languages: { metro: { icon: true as never } } } })).toThrow(
'fileIcons.languages',
);
expect(resolveOptions().fileIcons).toEqual({
set: 'vscode-icons',
style: 'plain',
Expand Down
2 changes: 1 addition & 1 deletion skills/starlight-codeblocks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ These features need no attribute. Know them, because they can change a block tha
- Smart shell copy applies to every block with a terminal frame and a line that starts with a prompt (`$ ` or `> ` by default). It also applies to every `pycon` block with a line that starts with `>>> `. It applies to a `python` or `py` block if the first line starts with `>>> `.
- Colourised brackets apply to every block in the languages in `brackets.languages`, if the site sets that option.
- Colour swatches apply to every block with a CSS colour in it. Turn them off for a block with `swatches=false`.
- File icons apply to every block with a title in an editor frame. Turn them off for a block with `icon=false`.
- File icons apply to every block with a title in an editor frame. Turn them off for a block with `icon=false`, or for a file type or a language with `false` in `fileIcons.files` or `fileIcons.languages`.
- Expandable blocks apply to every block longer than `expandable.auto` lines, if the site sets that option. Turn it off for a block with `expandable=false`.
- Inline code highlighting applies to all inline code, if the site sets `inlineHighlighting.defaultLanguage`. Keep one piece plain with `{:txt}`.

Expand Down
2 changes: 1 addition & 1 deletion skills/starlight-codeblocks/references/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ codeblocks({
| `whitespace` | None | Visible whitespace |
| `brackets` | `languages`: default `[]` | Colourised brackets |
| `swatches` | `languages`: default `'all'`. `formats`: default every format. `shape`: `'square'`, `'rounded'` or `'circle'`, default `'rounded'`. `size`: default `'0.8em'`. `hover`, `copy`: default `true`. `prose`: default `false` | Colour swatches |
| `fileIcons` | `set`: `'vscode-icons'`, `'material'`, `'catppuccin'` or `'seti'`, default `'vscode-icons'`. Material and Catppuccin need their package, such as `@iconify-json/catppuccin`. `style`: `'plain'` or `'tile'`, default `'plain'`. `languages`: `icon`, `colour` and `style` for each language. `files`: icon names by file name, extension or path pattern. `icons`: custom icons by name | File icons |
| `fileIcons` | `set`: `'vscode-icons'`, `'material'`, `'catppuccin'` or `'seti'`, default `'vscode-icons'`. Material and Catppuccin need their package, such as `@iconify-json/catppuccin`. `style`: `'plain'` or `'tile'`, default `'plain'`. `languages`: `icon`, `colour` and `style` for each language. `files`: icon names, or `false` for no icon, by file name, extension or path pattern. `icons`: custom icons by name | File icons |
| `codeLinks` | None | Code links |
| `apiLinks` | `adapters`: default `[python(), nextflow()]` | API auto-linking |
| `expandable` | `lines`: default `12`. `auto`: a line count, default `false` | Expandable blocks |
Expand Down
3 changes: 2 additions & 1 deletion skills/starlight-codeblocks/references/readability.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ Starts on its own in every block with a title in an editor frame. An icon of the
- A coloured icon keeps its colours, on a neutral square in a tile. A Seti icon has the colour of the title, and its tile has the accent colour. A tile in a custom colour gets a black or white icon, whichever reads on it.
- Options: `fileIcons.style` (default `'plain'`). `fileIcons.languages` sets `icon`, `colour` and `style` for each language, such as `{ python: { colour: '#3776ab' } }`.
- Options: `fileIcons.languages.<lang>.icon` is an icon name or SVG markup, such as `siNextflow.svg` from `simple-icons`.
- Options: `fileIcons.icons` adds icons by name, as SVG markup or 24 by 24 path data. `fileIcons.files` maps a file name, an extension such as `.nf`, or a path pattern such as `docs/**/*.md` to an icon name.
- Options: `false` in `fileIcons.files` or as `fileIcons.languages.<lang>.icon` gives no icon, such as `{ '.mmd': false }`. `icon="<name>"` on the fence line still sets one.
- Options: `fileIcons.icons` adds icons by name, as SVG markup or 24 by 24 path data. `fileIcons.files` maps a file name, an extension such as `.nf`, or a path pattern such as `docs/**/*.md` to an icon name. When two keys match, the later key wins.
- Code tabs show the icon of each file on its tab. A tab with only a `label` gets an icon only from `icon="..."`. The code tabs menu uses the same icon for each language, custom icons too.
- A docs example about another feature with a title: add `icon=false` if the icon distracts from the feature.

Expand Down
Loading