Merge pull request #31838 from karthikjeeyar/mkdocs-patch

feat(techdocs): add app-config option to disable external font download
This commit is contained in:
Andre Wanlin
2026-04-29 12:47:12 -05:00
committed by GitHub
14 changed files with 257 additions and 6 deletions
@@ -37,6 +37,7 @@ import {
patchMkdocsYmlPreBuild,
patchMkdocsYmlWithPlugins,
sanitizeMkdocsYml,
patchMkdocsYmlWithFontDisabled,
} from './mkdocsPatchers';
import yaml from 'js-yaml';
@@ -468,6 +469,119 @@ describe('helpers', () => {
});
});
describe('patchMkdocsYmlWithFontDisabled', () => {
beforeEach(() => {
mockDir.setContent({
'mkdocs_without_theme.yml': `site_name: Test Site
docs_dir: docs
`,
'mkdocs_with_theme_no_font.yml': `site_name: Test Site
docs_dir: docs
theme:
name: material
`,
'mkdocs_with_theme_font_true.yml': `site_name: Test Site
docs_dir: docs
theme:
name: material
font: true
`,
'mkdocs_with_theme_font_false.yml': `site_name: Test Site
docs_dir: docs
theme:
name: material
font: false
`,
'mkdocs_with_theme_non_material.yml': `site_name: Test Site
docs_dir: docs
theme:
name: test-theme
`,
});
mockLogger.debug.mockClear();
});
it('should create theme section with font disabled when no theme exists', async () => {
await patchMkdocsYmlWithFontDisabled(
mockDir.resolve('mkdocs_without_theme.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_without_theme.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
theme?: { name?: string; font?: boolean };
};
expect(parsedYml.theme).toBeDefined();
expect(parsedYml.theme?.name).toBe('material');
expect(parsedYml.theme?.font).toBe(false);
});
it('should add font: false when theme exists but font is not configured', async () => {
await patchMkdocsYmlWithFontDisabled(
mockDir.resolve('mkdocs_with_theme_no_font.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_theme_no_font.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
theme?: { name?: string; font?: boolean };
};
expect(parsedYml.theme).toBeDefined();
expect(parsedYml.theme?.name).toBe('material');
expect(parsedYml.theme?.font).toBe(false);
});
it('should not override font when font is already set to true', async () => {
await patchMkdocsYmlWithFontDisabled(
mockDir.resolve('mkdocs_with_theme_font_true.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_theme_font_true.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
theme?: { name?: string; font?: boolean };
};
expect(parsedYml.theme).toBeDefined();
expect(parsedYml.theme?.name).toBe('material');
expect(parsedYml.theme?.font).toBe(true);
});
it('should not override font when font is already set to false', async () => {
await patchMkdocsYmlWithFontDisabled(
mockDir.resolve('mkdocs_with_theme_font_false.yml'),
mockLogger,
);
const updatedMkdocsYml = await fs.readFile(
mockDir.resolve('mkdocs_with_theme_font_false.yml'),
);
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
theme?: { name?: string; font?: boolean };
};
expect(parsedYml.theme).toBeDefined();
expect(parsedYml.theme?.name).toBe('material');
expect(parsedYml.theme?.font).toBe(false);
});
it('should not patch when theme name is not material', async () => {
const fixturePath = mockDir.resolve('mkdocs_with_theme_non_material.yml');
const before = await fs.readFile(fixturePath, 'utf8');
await patchMkdocsYmlWithFontDisabled(fixturePath, mockLogger);
await expect(fs.readFile(fixturePath, 'utf8')).resolves.toEqual(before);
expect(mockLogger.debug).toHaveBeenCalledWith(
'mkdocs.yml theme is not "material"; skipping font disabling patch',
);
});
});
describe('patchIndexPreBuild', () => {
afterEach(() => {
warn.mockClear();
@@ -25,11 +25,23 @@ import { toError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { LoggerService } from '@backstage/backend-plugin-api';
const MATERIAL_THEME = 'material';
type MkDocsThemeObject = {
name?: string;
font?: boolean;
};
function isThemeObject(theme: unknown): theme is MkDocsThemeObject {
return typeof theme === 'object' && theme !== null && !Array.isArray(theme);
}
type MkDocsObject = {
plugins?: string[];
docs_dir: string;
repo_url?: string;
edit_uri?: string;
theme?: MkDocsThemeObject;
};
const patchMkdocsFile = async (
@@ -188,6 +200,43 @@ export const patchMkdocsYmlWithPlugins = async (
});
};
/**
* Disable external font download for the material theme.
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
* @param logger
*/
export const patchMkdocsYmlWithFontDisabled = async (
mkdocsYmlPath: string,
logger: LoggerService,
) => {
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
if (!('theme' in mkdocsYml)) {
// No theme section exists, create it with font disabled
mkdocsYml.theme = {
name: MATERIAL_THEME,
font: false,
};
return true;
}
const theme = mkdocsYml.theme;
if (isThemeObject(theme)) {
// Theme section exists. Only modify it when the configured theme is Material
if (theme.name === MATERIAL_THEME && !('font' in theme)) {
theme.font = false;
return true;
}
if (theme.name !== MATERIAL_THEME) {
logger.debug(
'mkdocs.yml theme is not "material"; skipping font disabling patch',
);
}
}
return false;
});
};
/**
* Sanitize mkdocs.yml by keeping only allowed configuration keys.
*
@@ -32,6 +32,7 @@ import {
import {
patchMkdocsYmlPreBuild,
patchMkdocsYmlWithFontDisabled,
patchMkdocsYmlWithPlugins,
sanitizeMkdocsYml,
} from './mkdocsPatchers';
@@ -150,6 +151,9 @@ export class TechdocsGenerator implements GeneratorBase {
}
await patchMkdocsYmlWithPlugins(mkdocsYmlPath, childLogger, defaultPlugins);
if (this.options.disableExternalFonts) {
await patchMkdocsYmlWithFontDisabled(mkdocsYmlPath, childLogger);
}
// Directories to bind on container
const mountDirs = {
@@ -264,5 +268,8 @@ export function readGeneratorConfig(
dangerouslyAllowAdditionalKeys: config.getOptionalStringArray(
'techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys',
),
disableExternalFonts: config.getOptionalBoolean(
'techdocs.generator.mkdocs.disableExternalFonts',
),
};
}
@@ -46,6 +46,7 @@ export type GeneratorConfig = {
legacyCopyReadmeMdToIndexMd?: boolean;
defaultPlugins?: string[];
dangerouslyAllowAdditionalKeys?: string[];
disableExternalFonts?: boolean;
};
/**