render-markdown-codehighlight/generate-theme.cjs (151 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
* OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const fs = require('fs');
const path = require('path');
const stylesDir = path.resolve(__dirname, 'node_modules/highlight.js/styles'); // Path to Highlight.js styles directory
const jsOutputFile = path.resolve(__dirname, 'themeStyles.js'); // Path to output JavaScript file
const goOutputFile = path.resolve(__dirname, 'theme_list.go'); // Path to output Go file
const colorInfoOutputFile = path.resolve(__dirname, 'themeColors.js'); // Path to output color information file
// Read all CSS files from the styles directory
let themes = fs.readdirSync(stylesDir).filter(file => file.endsWith('.css'));
// Prioritize .min.css files
const minifiedFiles = new Set(themes.filter(file => file.endsWith('.min.css')).map(file => file.replace('.min.css', '')));
themes = themes.filter(file => {
const baseName = file.replace('.css', '').replace('.min', '');
// Skip unminified versions if corresponding .min.css file exists
return !minifiedFiles.has(baseName) || file.endsWith('.min.css');
});
// Group themes and classify by naming conventions
const themeMap = {};
let themeList = [];
const themeColors = [];
let defaultDarkTheme = null;
let defaultLightTheme = null;
const cssColorNames = {
black: '#000000',
white: '#ffffff',
navy: '#000080',
// Add more color names as needed if the background color in css is not defined in a standard method
};
// Convert color names (e.g., 'black', 'white') to hex values
function convertColorNameToHex(colorName) {
return cssColorNames[colorName.toLowerCase()] || null;
}
// Normalize hex color code (e.g., convert shorthand hex to full length)
function normalizeHexColor(hexColor) {
hexColor = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
if (hexColor.length === 3) {
hexColor = hexColor.split('').map(char => char + char).join('');
}
return hexColor;
}
// Determine if a theme is dark based on its background color
function isDarkTheme(color) {
if (!color.startsWith('#') && !color.startsWith('rgb')) {
const hexColor = convertColorNameToHex(color);
if (hexColor) {
color = hexColor;
} else {
return false;
}
}
const hexColor = normalizeHexColor(color);
const rgb = parseInt(hexColor, 16);
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
const brightness = (r * 299 + g * 587 + b * 114) / 1000; // Calculate brightness based on RGB values
return brightness < 128; // Dark theme if brightness is below the threshold
}
// Process each theme file
themes.forEach(file => {
const themeName = file.replace('.css', '').replace('.min', '');
const [base, ...variantParts] = themeName.split('-');
const variant = variantParts.join('-');
if (!themeMap[base]) {
themeMap[base] = {};
}
let isDark = false;
let backgroundColor = null;
// Identify light or dark themes based on the variant name
if (variant.includes('light')) {
if (!themeMap[base].light || themeMap[base].light.length > file.length) {
themeMap[base].light = `() => import('highlight.js/styles/${file}?inline')`;
if (!defaultLightTheme) {
defaultLightTheme = themeMap[base].light;
}
}
} else if (variant.includes('dark')) {
if (!themeMap[base].dark || themeMap[base].dark.length > file.length) {
themeMap[base].dark = `() => import('highlight.js/styles/${file}?inline')`;
if (!defaultDarkTheme) {
defaultDarkTheme = themeMap[base].dark;
}
}
} else {
// Extract background color from CSS content and determine if it's a dark theme
const cssContent = fs.readFileSync(path.resolve(stylesDir, file), 'utf-8');
const backgroundMatch = cssContent.match(/\.hljs\s*{[^}]*?\s*background(?:-color)?:\s*(#[0-9a-fA-F]{3,6}|rgb\([^)]+\)|[a-zA-Z]+|url\([^)]+\))/i);
backgroundColor = backgroundMatch ? backgroundMatch[1].trim() : null;
if (backgroundColor) {
if (backgroundColor.startsWith('url')) {
backgroundColor = null;
} else if (backgroundColor.startsWith('#')) {
isDark = isDarkTheme(backgroundColor);
} else if (backgroundColor.startsWith('rgb')) {
const rgbValues = backgroundColor.match(/\d+/g).map(Number);
const brightness = (rgbValues[0] * 299 + rgbValues[1] * 587 + rgbValues[2] * 114) / 1000;
isDark = brightness < 128;
} else {
isDark = isDarkTheme(backgroundColor);
}
}
// Assign the theme to light or dark based on the background color
if (isDark) {
if (!themeMap[base].dark || themeMap[base].dark.length > file.length) {
themeMap[base].dark = `() => import('highlight.js/styles/${file}?inline')`;
if (!defaultDarkTheme) {
defaultDarkTheme = themeMap[base].dark;
}
}
} else {
if (!themeMap[base].light || themeMap[base].light.length > file.length) {
themeMap[base].light = `() => import('highlight.js/styles/${file}?inline')`;
if (!defaultLightTheme) {
defaultLightTheme = themeMap[base].light;
}
}
}
}
// Add theme to the theme list
if (!themeList.includes(base)) {
themeList.push(base);
}
// Store theme color information
if (backgroundColor) {
themeColors.push({
theme: base,
variant: isDark ? 'dark' : 'light',
color: backgroundColor
});
}
});
// Classify themes based on the presence of light and dark variants
themeList = themeList.map(base => {
if (themeMap[base].light && !themeMap[base].dark) {
return `${base}-light`;
} else if (!themeMap[base].light && themeMap[base].dark) {
return `${base}-dark`;
} else if (themeMap[base].light && themeMap[base].dark) {
return `${base}-all`;
} else {
return base;
}
});
// Assign default light and dark themes if missing
Object.keys(themeMap).forEach(base => {
if (!themeMap[base].dark && defaultDarkTheme) {
themeMap[base].dark = defaultDarkTheme;
}
if (!themeMap[base].light && defaultLightTheme) {
themeMap[base].light = defaultLightTheme;
}
});
// Generate the JavaScript output for theme styles
const jsOutput = `export const themeStyles = {\n${Object.entries(themeMap)
.map(([theme, variants]) =>
` ${JSON.stringify(theme)}: {\n light: ${variants.light},\n dark: ${variants.dark}\n }`
).join(',\n')}\n};`;
fs.writeFileSync(jsOutputFile, jsOutput); // Write the theme styles to JavaScript file
// Generate the Go output for theme list
const goOutput = `
package render_markdown_codehighlight
var ThemeList = []string{
${themeList.map(theme => `"${theme}"`).join(",\n ")},
}
`;
fs.writeFileSync(goOutputFile, goOutput); // Write the theme list to Go file
console.log('Theme styles, Go theme list, and color information generated successfully!');