From f9de5f6faab903d9a3876470137a942aa9b85889 Mon Sep 17 00:00:00 2001 From: Phil Ewels Date: Mon, 5 Oct 2026 12:11:06 +0200 Subject: [PATCH] file-icons: accept false in files and languages to turn icons off Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/src/components/icon-sets.ts | 2 +- docs/src/content/docs/features/file-icons.mdx | 13 +++++++++ .../src/expressive-code/file-icons.ts | 29 ++++++++++--------- packages/starlight-codeblocks/src/options.ts | 14 ++++----- .../test/file-icons.test.ts | 27 +++++++++++++++++ skills/starlight-codeblocks/SKILL.md | 2 +- .../references/configuration.md | 2 +- .../references/readability.md | 3 +- 8 files changed, 68 insertions(+), 24 deletions(-) diff --git a/docs/src/components/icon-sets.ts b/docs/src/components/icon-sets.ts index bc6833f..bf7dcfb 100644 --- a/docs/src/components/icon-sets.ts +++ b/docs/src/components/icon-sets.ts @@ -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) => { diff --git a/docs/src/content/docs/features/file-icons.mdx b/docs/src/content/docs/features/file-icons.mdx index ccad6fb..e465f3f 100644 --- a/docs/src/content/docs/features/file-icons.mdx +++ b/docs/src/content/docs/features/file-icons.mdx @@ -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. @@ -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. diff --git a/packages/starlight-codeblocks/src/expressive-code/file-icons.ts b/packages/starlight-codeblocks/src/expressive-code/file-icons.ts index 4acd053..30b6452 100644 --- a/packages/starlight-codeblocks/src/expressive-code/file-icons.ts +++ b/packages/starlight-codeblocks/src/expressive-code/file-icons.ts @@ -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; } @@ -22,7 +23,8 @@ export interface FileIconSettings { set: FileIconSet; style: FileIconStyle; languages: Record; - files: Record; + /** `false` for no icon. */ + files: Record; icons: Record; } @@ -279,7 +281,7 @@ 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}` }]; }), @@ -287,18 +289,18 @@ export async function fileIconResolver({ 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 ?? [])])]); } @@ -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); @@ -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\`.`, @@ -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); @@ -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 }), diff --git a/packages/starlight-codeblocks/src/options.ts b/packages/starlight-codeblocks/src/options.ts index 30db140..f33ac12 100644 --- a/packages/starlight-codeblocks/src/options.ts +++ b/packages/starlight-codeblocks/src/options.ts @@ -100,7 +100,7 @@ export interface CodeblocksOptions { set?: FileIconSet; style?: FileIconStyle; languages?: Record; - files?: Record; + files?: Record; icons?: Record; }; codeLinks?: false; @@ -388,25 +388,25 @@ export const optionsReference: Record = { valid: oneOf('plain', 'tile'), }, languages: { - type: "Record", + type: "Record", 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', + type: 'Record', 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', diff --git a/packages/starlight-codeblocks/test/file-icons.test.ts b/packages/starlight-codeblocks/test/file-icons.test.ts index f337bbe..7aaeded 100644 --- a/packages/starlight-codeblocks/test/file-icons.test.ts +++ b/packages/starlight-codeblocks/test/file-icons.test.ts @@ -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=""` 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', diff --git a/skills/starlight-codeblocks/SKILL.md b/skills/starlight-codeblocks/SKILL.md index ae2e997..91b242b 100644 --- a/skills/starlight-codeblocks/SKILL.md +++ b/skills/starlight-codeblocks/SKILL.md @@ -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}`. diff --git a/skills/starlight-codeblocks/references/configuration.md b/skills/starlight-codeblocks/references/configuration.md index f46d79b..845e944 100644 --- a/skills/starlight-codeblocks/references/configuration.md +++ b/skills/starlight-codeblocks/references/configuration.md @@ -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 | diff --git a/skills/starlight-codeblocks/references/readability.md b/skills/starlight-codeblocks/references/readability.md index 8800304..050bb4a 100644 --- a/skills/starlight-codeblocks/references/readability.md +++ b/skills/starlight-codeblocks/references/readability.md @@ -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..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..icon` gives no icon, such as `{ '.mmd': false }`. `icon=""` 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.