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
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,53 @@ describe("enum list", () => {
]);
});

test("should normalize emojis missing VS16 (U+FE0F) for consistent terminal alignment", () => {
const ENUM_RULE_LIST = ["build", "revert", "ci"];
setRules({
"type-enum": [RuleConfigSeverity.Error, "always", ENUM_RULE_LIST],
} as any);

setPromptConfig({
questions: {
type: {
enum: {
build: {
description: "Build system changes",
emoji: "🛠",
},
revert: {
description: "Reverts a commit",
emoji: "🗑",
},
ci: {
description: "CI config changes",
emoji: "⚙️",
},
},
},
},
});

const enumList = getRuleQuestionConfig("type")?.enumList;
expect(enumList).toEqual([
{
name: "🛠\uFE0F build: Build system changes",
value: "build",
short: "build",
},
{
name: "🗑\uFE0F revert: Reverts a commit",
value: "revert",
short: "revert",
},
{
name: "⚙️ ci: CI config changes",
value: "ci",
short: "ci",
},
]);
});

test("should handle no enums having emojis correctly", () => {
const ENUM_RULE_LIST = ["feat", "fix", "chore"];
setRules({
Expand Down Expand Up @@ -438,7 +485,7 @@ describe("enum list", () => {
});

test("should include the emoji in the value when `emojiInHeader` is true", () => {
const ENUM_RULE_LIST = ["feat", "fix"];
const ENUM_RULE_LIST = ["feat", "fix", "build", "revert"];
setRules({
"type-enum": [RuleConfigSeverity.Error, "always", ENUM_RULE_LIST],
} as any);
Expand All @@ -456,23 +503,46 @@ describe("enum list", () => {
description: "Bug fixes",
emoji: "🐛",
},
build: {
description: "Build changes",
emoji: "🛠",
},
revert: {
description: "Revert commit",
emoji: "🗑",
},
},
},
},
});

const enumList = getRuleQuestionConfig("type")?.enumList;

expect(enumList).toEqual([
{
name: "✨ feat: Features",
// ✨ is Emoji_Presentation (width 2). No \uFE0F added.
name: "✨ feat: Features",
value: "✨ feat",
short: "feat",
},
{
name: "🐛 fix: Bug fixes",
// 🐛 is Emoji_Presentation (width 2). No \uFE0F added.
name: "🐛 fix: Bug fixes",
value: "🐛 fix",
short: "fix",
},
{
// 🛠 is NOT presentation-default. \uFE0F IS added.
name: "🛠\uFE0F build: Build changes",
value: "🛠\uFE0F build",
short: "build",
},
{
// 🗑 is NOT presentation-default. \uFE0F IS added.
name: "🗑\uFE0F revert: Revert commit",
value: "🗑\uFE0F revert",
short: "revert",
},
]);
});

Expand Down
42 changes: 41 additions & 1 deletion @commitlint/cz-commitlint/src/services/getRuleQuestionConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference lib="es2023.intl" />
import { RuleField } from "@commitlint/types";
import { QuestionConfig } from "../Question.js";
import { getPromptMessages, getPromptQuestions } from "../store/prompts.js";
Expand All @@ -14,6 +15,44 @@ import {
ruleIsDisabled,
} from "../utils/rules.js";

const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const isPresentation = /^\p{Emoji_Presentation}/u;
const isEmojiBase = /^\p{Emoji}/u;
Comment thread
mrt181 marked this conversation as resolved.

/**
* Appends Unicode Variation Selector 16 (U+FE0F) to emojis missing it,
* forcing emoji-width (2 col, like ✨) presentation in terminals. Without VS16,
* emojis like 🛠 (U+1F6E0) and 🗑 (U+1F5D1) render at text-width (1 col),
* breaking column alignment in interactive menus.
*/
function normalizeEmoji(emoji: string): string {
const trimmed = emoji.replace(/\s+$/, "");
const trailing = emoji.slice(trimmed.length);

if (trimmed.length === 0) return emoji;

const segments = Array.from(segmenter.segment(trimmed));

if (segments.length === 1) {
const char = segments[0].segment;

switch (true) {
case char.includes("\uFE0F"):
case char.includes("\uFE0E"):
return emoji;

case isPresentation.test(char):
return emoji;

// Is it a "Text-style" emoji base and not a number? Add VS16!
case isEmojiBase.test(char) && !/^[0-9#*]$/.test(char):
return trimmed + "\uFE0F" + trailing;
}
}

return emoji;
}

export default function (rulePrefix: RuleField): QuestionConfig | null {
const questions = getPromptQuestions();
const questionSettings = questions[rulePrefix];
Expand Down Expand Up @@ -54,7 +93,8 @@ export default function (rulePrefix: RuleField): QuestionConfig | null {
.map((enumName) => {
const enumDescription = enumDescriptions[enumName]?.description;
if (enumDescription) {
const emoji = enumDescriptions[enumName]?.emoji;
const rawEmoji = enumDescriptions[enumName]?.emoji;
const emoji = rawEmoji ? normalizeEmoji(rawEmoji) : rawEmoji;

const emojiPrefix = emoji
? `${emoji} `
Expand Down