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
4 changes: 4 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import starlightGitHubAlerts from 'starlight-github-alerts';
import starlightBlog from 'starlight-blog';
import mermaid from 'astro-mermaid';
import { fileURLToPath } from 'node:url';
import remarkStripEmojis from './src/lib/remark/stripEmojis.js';

/**
* Creates blog authors config with GitHub profile pictures
Expand All @@ -32,6 +33,9 @@ function createAuthors(authors) {
export default defineConfig({
site: 'https://github.github.io',
base: '/gh-aw/',
markdown: {
remarkPlugins: [remarkStripEmojis],
},
vite: {
server: {
fs: {
Expand Down
132 changes: 132 additions & 0 deletions docs/src/lib/remark/stripEmojis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// @ts-check

/**
* Strip decorative emojis from rendered markdown for a more professional look.
*
* - Applies to regular text nodes.
* - Applies to code blocks and inline code so rendered pages contain no emojis.
* - Also applies to image/link metadata and raw HTML/MDX JSX attributes.
*/
export default function remarkStripEmojis() {
return function transform(tree) {
visit(tree);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transform function should return the tree after modifications. While remark plugins that mutate the tree in-place may work without an explicit return, it's a best practice to return the tree to follow the unified plugin specification and ensure compatibility with the remark ecosystem. Add return tree; after the visit call.

Suggested change
visit(tree);
visit(tree);
return tree;

Copilot uses AI. Check for mistakes.
};
}

/**
* @param {any} node
*/
function visit(node) {
if (!node || typeof node !== 'object') return;

if (node.type === 'text' && typeof node.value === 'string') {
node.value = stripEmojis(node.value);
}

if (node.type === 'inlineCode' && typeof node.value === 'string') {
node.value = stripEmojis(node.value);
}

if (node.type === 'code' && typeof node.value === 'string') {
node.value = stripEmojis(node.value);
}
Comment on lines +26 to +32
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stripping emojis from code blocks and inline code could break code examples that intentionally include emoji characters. For example, string literals, test cases, or documentation examples that demonstrate emoji handling would be modified. Consider whether code should be excluded from emoji stripping to preserve code examples as written.

Copilot uses AI. Check for mistakes.

if (node.type === 'html' && typeof node.value === 'string') {
node.value = stripEmojis(node.value);
}

if (node.type === 'image') {
if (typeof node.alt === 'string') node.alt = stripEmojis(node.alt);
if (typeof node.title === 'string') node.title = stripEmojis(node.title);
}

if (node.type === 'link' && typeof node.title === 'string') {
node.title = stripEmojis(node.title);
}

if (node.type === 'definition' && typeof node.title === 'string') {
node.title = stripEmojis(node.title);
}

// MDX JSX elements can carry emoji in string-valued attributes (e.g., alt/title).
// We keep this conservative and only touch string values.
if (
(node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
Array.isArray(node.attributes)
) {
for (const attr of node.attributes) {
if (!attr || typeof attr !== 'object') continue;
if (typeof attr.value === 'string') {
attr.value = stripEmojis(attr.value);
continue;
}
// Some MDX parsers represent attribute values as objects.
if (attr.value && typeof attr.value === 'object' && typeof attr.value.value === 'string') {
attr.value.value = stripEmojis(attr.value.value);
}
}
}

const { children } = node;
if (Array.isArray(children)) {
for (const child of children) visit(child);
}
}

const replacements = new Map([
// Prefer text symbols over emoji glyphs.
['✅', '✓'],
['❌', '✗'],
['⚠️', '!'],
['⚠', '!'],
// Common decorative prefixes.
['🚀', ''],
['🔍', ''],
['🤖', ''],
['🛡️', ''],
['🛡', ''],
['🔒', ''],
['🔐', ''],
['🔓', ''],
['📥', ''],
['📤', ''],
['🌐', ''],
['🚫', ''],
['🐳', ''],
['💰', ''],
['⚡', ''],
['🔗', ''],
['🏷️', ''],
['🏷', ''],
['📊', ''],
['🔬', ''],
['🏗️', ''],
['🏗', ''],
['🧪', ''],
['📋', ''],
['🧩', ''],
['🎯', ''],
['🎭', ''],
]);

/**
* @param {string} input
*/
function stripEmojis(input) {
let output = input;

for (const [from, to] of replacements.entries()) {
if (output.includes(from)) output = output.split(from).join(to);
}

Comment on lines +113 to +121
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string replacement approach using split().join() is inefficient for multiple replacements on the same string. Each replacement iterates through the entire string, resulting in O(n*m) complexity where n is string length and m is number of replacements. Consider using a single pass with a regex combining all patterns, or use replaceAll() which is more efficient and clearer in intent.

Suggested change
* @param {string} input
*/
function stripEmojis(input) {
let output = input;
for (const [from, to] of replacements.entries()) {
if (output.includes(from)) output = output.split(from).join(to);
}
* Escape special characters in a string for use in a RegExp.
* @param {string} value
* @returns {string}
*/
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Build a single regex that matches any of the replacement keys.
const replacementPattern = Array.from(replacements.keys())
.sort((a, b) => b.length - a.length) // longer first to prefer longer matches
.map((key) => escapeRegex(key))
.join('|');
const replacementsRegex = new RegExp(replacementPattern, 'gu');
/**
* @param {string} input
*/
function stripEmojis(input) {
let output = input.replace(
replacementsRegex,
(match) => replacements.get(match) ?? match,
);

Copilot uses AI. Check for mistakes.
// Remove leftover emoji presentation selectors.
output = output.replace(/\uFE0F/gu, '');

// Strip any remaining pictographic emoji characters.
// Node 20+ supports Unicode property escapes.
output = output.replace(/\p{Extended_Pictographic}+/gu, '');

// Collapse double spaces introduced by removals.
output = output.replace(/[ \t]{2,}/g, ' ');
return output;
}
Loading