Skip to content
Open
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
90 changes: 50 additions & 40 deletions src/util/rgb/Decode.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
function hexToHSL(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return { h: 100, s: 100, l: 100 };

const r = parseInt(result[1], 16) / 255;
const g = parseInt(result[2], 16) / 255;
const b = parseInt(result[3], 16) / 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);

let h = 0;
let s,
l = (max + min) / 2;
let s = 0;
let l = (max + min) / 2;

if (max === min) {
h = s = 0; // achromatic
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
Expand All @@ -36,43 +39,39 @@ function hexToHSL(hex: string) {
}

export function getSignificantPoints(gradient: string[], threshold: number) {
// Convert all colors to HSL
const hslColors = gradient.map(hexToHSL);

// Calculate differences between consecutive colors
const differences = [];
for (let i = 1; i < hslColors.length; i++) {
const hDiff = Math.abs(hslColors[i].h - hslColors[i - 1].h);
const sDiff = Math.abs(hslColors[i].s - hslColors[i - 1].s);
const lDiff = Math.abs(hslColors[i].l - hslColors[i - 1].l);

// Weight hue, saturation, and lightness changes
differences.push({
index: i,
change: hDiff * 2 + sDiff + lDiff, // Hue changes weighted more heavily
change: hDiff * 2 + sDiff + lDiff,
});
}

// Identify significant points based on notable changes
const significantPoints = [gradient[0]]; // Always include the first color

// Iterate over differences to capture significant transitions
for (let i = 1; i < differences.length; i++) {
if (differences[i - 1].change > threshold) {
// Dynamic threshold based on gradient characteristics
significantPoints.push(gradient[differences[i - 1].index]);
}
}

significantPoints.push(gradient[gradient.length - 1]); // Always include the last color
significantPoints.push(gradient[gradient.length - 1]);

return significantPoints;
}

export function decodeLegacy(rgbtext: string) {
const legacyCodeRegex =
/(?:(?:[&§]|\\u00a7)x(?:(?:[&§]|\\u00a7)[0-9A-Fa-f]){6}|&#[0-9A-Fa-f]{6}|(?:[&§]|\\u00a7)[l-orL-ORkK])/g;
const matches = [...rgbtext.matchAll(legacyCodeRegex)];
if (!rgbtext || !rgbtext.trim()) return null;
const codeRegex =
/(?:(?:[&§]|\\u00a7)x(?:(?:[&§]|\\u00a7)[0-9A-Fa-f]){6}|[&#§]\b[0-9A-Fa-f]{6}\b|&#[0-9A-Fa-f]{6}|<span[^>]*style=["']([^"']*)["'][^>]*>|<\/span>|(?:[&§]|\\u00a7)[l-orL-ORkK])/gi;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two real decode failures come from this regex:

  1. §#RRGGBB never matches. There's a dedicated &#[0-9A-Fa-f]{6} alternative for the &-prefixed modern hex, but no equivalent for §. decodeLegacy("§#ff0055Hello") returns null even though the PR description explicitly lists §#ff0055 as supported.
  2. Raw #RRGGBB (and any hex reached only via the generic [&#§]\b...\b alternative) breaks whenever it's directly followed by text — the common case, e.g. "#ff0055Hello". The trailing \b requires a non-word character right after the 6th hex digit, so it only works if a space (or end of string) follows.
Suggested change
/(?:(?:[&§]|\\u00a7)x(?:(?:[&§]|\\u00a7)[0-9A-Fa-f]){6}|[&#§]\b[0-9A-Fa-f]{6}\b|&#[0-9A-Fa-f]{6}|<span[^>]*style=["']([^"']*)["'][^>]*>|<\/span>|(?:[&§]|\\u00a7)[l-orL-ORkK])/gi;
/(?:(?:[&§]|\\u00a7)x(?:(?:[&§]|\\u00a7)[0-9A-Fa-f]){6}|(?:&|§)#[0-9A-Fa-f]{6}|[&#§][0-9A-Fa-f]{6}|<span[^>]*style=["']([^"']*)["'][^>]*>|<\/span>|(?:[&§]|\\u00a7)[l-orL-ORkK])/gi;

This adds a §# alternative (mirroring the existing &# one) and drops the trailing \b from the generic single-prefix hex alternative so it matches regardless of what follows.

Verified with a battery of manual test cases (§#ff0055Hello, #ff0055Hello, etc.) — before this change both return null; after, both decode correctly with no regressions on the existing passing cases (legacy &x/§x hex, HTML spans, formatting codes, malformed/blank input).


Generated by Claude Code


const matches = [...rgbtext.matchAll(codeRegex)];
if (matches.length === 0) return null;

const colors: Array<{ hex: string; pos: number }> = [];
Expand All @@ -98,38 +97,49 @@ export function decodeLegacy(rgbtext: string) {
const match = matches[i];
const codeStr = match[0];

const lastChar = codeStr.charAt(codeStr.length - 1).toLowerCase();
if (codeStr.length === 2 || codeStr.startsWith('\\u00a7')) {
if (lastChar === 'r') {
currentColor = '#ffffff';
currentFmts.bold = false;
currentFmts.italic = false;
currentFmts.underline = false;
currentFmts.strikethrough = false;
currentFmts.obfuscate = false;
} else if (lastChar === 'l') {
currentFmts.bold = true;
} else if (lastChar === 'o') {
currentFmts.italic = true;
} else if (lastChar === 'n') {
if (codeStr.toLowerCase().startsWith('<span')) {
const styleAttr = match[1] || '';
const colorMatch = styleAttr.match(/color:\s*(#[0-9a-fA-F]{6})/i);
if (colorMatch) currentColor = colorMatch[1];
if (/font-weight:\s*bold/i.test(styleAttr)) currentFmts.bold = true;
if (/font-style:\s*italic/i.test(styleAttr)) currentFmts.italic = true;
if (/text-decoration:[^;]*underline/i.test(styleAttr)) {
currentFmts.underline = true;
} else if (lastChar === 'm') {
}
if (/text-decoration:[^;]*line-through/i.test(styleAttr)) {
currentFmts.strikethrough = true;
} else if (lastChar === 'k') {
currentFmts.obfuscate = true;
}
} else if (codeStr.toLowerCase() === '</span>') {
// closing tag
} else {
Comment on lines +104 to 114

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)Decode\.ts$|rgb|HTML|Parser' || true

echo
echo "Decode outline:"
if [ -f src/util/rgb/Decode.ts ]; then
  ast-grep outline src/util/rgb/Decode.ts --view compact || true
  echo
  echo "Relevant Decode.ts 1-180:"
  sed -n '1,180p' src/util/rgb/Decode.ts | cat -n
fi

echo
echo "Search for Decode/rich/span usage:"
rg -n "Decode|\\$<r|r>|currentFmts|font-weight|font-style|text-decoration|span" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -200

Repository: birdflop/web

Length of output: 27745


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "MiniMessageDecode relevant sections:"
sed -n '1,380p' src/util/rgb/MiniMessageDecode.ts | cat -n

echo
echo "Decode tests:"
if [ -f src/util/rgb/__tests__ 2>/dev/null ]; then
  fd . src/util/rgb/__tests__ -t f -x sh -c 'echo "--- $1"; cat -n "$1"' sh {}
fi
fd './Decode.*\.test\.(ts|tsx|js)$|rgbUtils.test.ts' src packages --exec sh -c 'echo "--- $1"; cat -n "$1"' sh {}

echo
echo "HTML/span examples in rgb package docs/source:"
rg -n "<span[^>]+style=|font-(weight|style)|text-decoration|text:<color>|<color|<span" packages/src packages packages/rgbirdflop --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -250

Repository: birdflop/web

Length of output: 19310


🌐 Web query:

RGBirdflop Decode.ts span HTML style decode legacy miniMessage output <span style="font-weight:bold color:#...

💡 Result:

The specific output format <span style="font-weight:bold color:#... is not a standard MiniMessage or legacy Minecraft format, nor is it a native output format of the RGBirdflop toolset [1][2]. RGBirdflop generates Minecraft-compatible formats like MiniMessage (<gradient:...>), legacy section codes (§), or ampersand codes (&) [1][3][2]. If you are encountering <span style="..."> HTML-like tags, this indicates that the text has likely been processed by an HTML converter, an web-based UI component, or a specialized Minecraft plugin intended to bridge web-based CSS styling with game text [4][5]. To resolve or decode this: 1. Identification: The string structure suggests it is attempting to translate web-style CSS (bold, hex colors) into Minecraft components [4]. This is often used by web dashboards that display server chat or status messages. 2. Conversion Path: If your goal is to get this into a format compatible with Minecraft (MiniMessage), you should use a deserializer that can handle HTML, or perform a string replace/regex to map these CSS styles to standard MiniMessage tags [4][5]. - Example mapping: Replace <span style="font-weight:bold color:#([0-9a-fA-F]{6})"> with <color:#$1><bold>. 3. Authorized Tools: For standard Minecraft conversions, utilize the LegacyComponentSerializer or MiniMessage APIs provided by the Adventure library, which is the authoritative standard for handling these formats in modern Minecraft development [4][5][6]. If you are attempting to use RGBirdflop specifically, ensure you are selecting the correct output format (e.g., MiniMessage or Legacy) in its configuration, as RGBirdflop does not natively output HTML <span> tags [1][3][2].

Citations:


🌐 Web query:

MiniTranslator <span style="font-weight:bold color:#ffffff"/> text decoration underline line-through Decode.ts

💡 Result:

The term MiniTranslator is commonly associated with a Java utility used to convert Minecraft legacy formatting codes (like &n for underline and &m for strikethrough) into MiniMessage tags [1][2][3]. While there is no single authoritative library named "MiniTranslator" that contains a specific "Decode.ts" file implementing text-decoration logic, technical searches reveal a relevant implementation in the context of converting macOS Notes data to Markdown [4][5]. Specifically, in the macos-ts library, the file src/notes/protobuf/decode.ts handles the decoding of attributes (such as underlined or strikethrough state) from protobuf data [4], and the corresponding src/notes/conversion/proto-to-markdown.ts file processes these decoded attributes to apply text decorations like strikethrough (text) or underline (via HTML tags) during conversion [5]. For Minecraft-related projects, MiniTranslator [1][3] and related tools like FlectonePulse [6][7] map legacy codes to MiniMessage tags as follows: - Underline: &n maps to [3][6] - Strikethrough: &m maps to [3][6] If you are working with a TypeScript codebase and looking for a "Decode.ts" file, it is likely part of a specific project (such as a parser or converter) where you must manually map state flags (e.g., isUnderlined, isStrikethrough) to the desired output format (like HTML or Markdown) [5].

Citations:


🌐 Web query:

site:github.com/imDaniX/MiniTranslator Decode.ts span style font-weight bold

💡 Result:

I couldn’t find Decode.ts or the exact span style="font-weight: bold" text in imDaniX/MiniTranslator. The repository is Java-based, with source files under src, and maps Minecraft bold formatting (&l) to MiniMessage’s <b> tag. (github.com)

Citations:


🌐 Web query:

"font-weight:\s*bold" "text-decoration:[^;]*underline" "currentFmts.bold"

💡 Result:

The text pattern provided appears to be a composite of CSS properties (font-weight, text-decoration) and a programmatic object reference (currentFmts.bold) often used in text-processing or editor-related software. 1. CSS Properties: The segments font-weight: bold and text-decoration: underline are standard CSS rules used to style text [1][2]. In rich text editors like CKEditor or Lexical, these CSS properties are frequently used to identify or apply formatting styles during document parsing or transformation [3][4][5]. 2. currentFmts.bold: This is not a standard web API. Based on common coding patterns in text-formatting libraries, it is likely a property or object within an editor's internal state management (such as TinyMCE's formatter engine [6][7], a custom extension [4], or a formatting utility). It represents a flag or boolean configuration indicating that the currently selected text or node should be, or is currently, formatted as bold [4][6]. In summary, the string reflects an internal software operation where a system checks if a specific text range has bold formatting (currentFmts.bold) and applies corresponding CSS styles (font-weight: bold; text-decoration: underline;) when rendering or exporting that content [3][4].

Citations:


🌐 Web query:

"parseMiniMessage" "decodeMiniMessage"

💡 Result:

In the context of the Adventure library's MiniMessage implementation, which is the industry standard for parsing MiniMessage, the terms parseMiniMessage and decodeMiniMessage are not part of the official public API [1][2][3]. The Adventure MiniMessage API uses the following primary methods to process strings [1][2][4]: deserialize(String input): This is the standard method used to convert a MiniMessage string into a Component object [1][2][3]. serialize(Component component): This is used to convert a Component object back into a MiniMessage string [1][2]. If you have encountered parseMiniMessage or decodeMiniMessage in your codebase, they are likely either: 1. Custom wrapper utility methods defined within your own project or a third-party plugin/library (such as AdventureUtils) to simplify common tasks [5][6]. 2. Legacy method names from older or alternative MiniMessage implementations [7]. For example, some third-party utilities define a parseMiniMessage method that might combine deserialization with other logic, such as legacy color code conversion or applying custom TagResolvers [5][6]. You should check the source code or the documentation of the specific utility class where these methods are defined to understand their exact behavior [5][6].

Citations:


Reset span formatting flags on each style change.

currentFmts.bold, .italic, .underline, and .strikethrough are set only when a style is present, and </span> is a no-op. A second sibling <span> that omits a previous style keeps that formatting until &r/§r. Assign each flag only when the span’s style declares it, unless nested span inheritance is the required behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/rgb/Decode.ts` around lines 109 - 115, Update the span style
handling in the Decode formatting parser so currentFmts.bold, italic, underline,
and strikethrough are reset for each new span before applying declarations from
styleAttr. Preserve flags only when the current span explicitly declares the
corresponding style, while leaving the existing reset-code behavior unchanged.

if (codeStr.startsWith('&#')) {
const lastChar = codeStr.charAt(codeStr.length - 1).toLowerCase();
if (codeStr.length === 2 || codeStr.startsWith('\\u00a7')) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This condition conflates two unrelated matches. Any regex match starting with the literal § escape sequence — including a full escaped-unicode legacy hex sequence like §x§f§f§0§0§5§5 — gets routed into this "single formatting code" branch instead of the hex-extraction branch below. It only checks lastChar against r/l/o/n/m/k, so the parsed color is silently discarded and the text defaults to white (#ffffff) instead of the actual decoded hex.

Suggested change
if (codeStr.length === 2 || codeStr.startsWith('\\u00a7')) {
const isFormatCode = /^(?:[&§]|\\u00a7)[l-orL-ORkK]$/i.test(codeStr);
if (isFormatCode) {

This precisely identifies only the standalone formatting-code alternative (regardless of prefix), so escaped-unicode legacy hex sequences correctly fall through to the hex-extraction branch and keep their color. Confirmed via manual testing: decodeLegacy("\\u00a7x\\u00a7f\\u00a7f\\u00a70\\u00a70\\u00a75\\u00a75Hello") now returns #ff0055 instead of #ffffff, with no change to normal formatting-code handling (&l, &r, etc.).


Generated by Claude Code

if (lastChar === 'r') {
currentColor = '#ffffff';
currentFmts.bold = false;
currentFmts.italic = false;
currentFmts.underline = false;
currentFmts.strikethrough = false;
currentFmts.obfuscate = false;
Comment on lines +115 to +123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

\u00a7-prefixed Legacy Hex codes are misrouted to the formatting branch and lose their color.

The condition codeStr.length === 2 || codeStr.startsWith('\\u00a7') is meant to isolate single formatting/reset codes (&l, §r, or their \u00a7-escaped forms). But a full Legacy Hex match ((?:[&§]|\\u00a7)x(?:(?:[&§]|\\u00a7)[0-9A-Fa-f]){6}) that happens to use the \u00a7 prefix also starts with that same literal text, so it enters this branch too. Inside, lastChar is one of the trailing hex digits, which never equals r, l, o, n, m, or k, so none of the branches in Lines 118-135 execute. The color encoded in that Legacy Hex sequence is silently dropped, and currentColor retains its prior value.

Detect formatting/reset codes by their actual shape instead of length/prefix heuristics, so a \u00a7-prefixed Legacy Hex match is never confused with a formatting code.

🐛 Proposed fix
       const lastChar = codeStr.charAt(codeStr.length - 1).toLowerCase();
-      if (codeStr.length === 2 || codeStr.startsWith('\\u00a7')) {
+      if (/^(?:[&§]|\\u00a7)[l-orL-ORkK]$/i.test(codeStr)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lastChar = codeStr.charAt(codeStr.length - 1).toLowerCase();
if (codeStr.length === 2 || codeStr.startsWith('\\u00a7')) {
if (lastChar === 'r') {
currentColor = '#ffffff';
currentFmts.bold = false;
currentFmts.italic = false;
currentFmts.underline = false;
currentFmts.strikethrough = false;
currentFmts.obfuscate = false;
const lastChar = codeStr.charAt(codeStr.length - 1).toLowerCase();
if (/^(?:[&§]|\\u00a7)[l-orL-ORkK]$/i.test(codeStr)) {
if (lastChar === 'r') {
currentColor = '`#ffffff`';
currentFmts.bold = false;
currentFmts.italic = false;
currentFmts.underline = false;
currentFmts.strikethrough = false;
currentFmts.obfuscate = false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/rgb/Decode.ts` around lines 116 - 124, Update the
formatting/reset-code condition near lastChar in the decoder to match only
actual single formatting or reset code shapes, rather than any value beginning
with the escaped section-sign prefix. Ensure full Legacy Hex sequences,
including those using \u00a7, bypass this branch and continue through color
decoding while preserving existing handling for valid formatting codes.

} else if (lastChar === 'l') {
currentFmts.bold = true;
} else if (lastChar === 'o') {
currentFmts.italic = true;
} else if (lastChar === 'n') {
currentFmts.underline = true;
} else if (lastChar === 'm') {
currentFmts.strikethrough = true;
} else if (lastChar === 'k') {
currentFmts.obfuscate = true;
}
} else if (codeStr.startsWith('&#') || codeStr.startsWith('§#')) {
currentColor = '#' + codeStr.slice(2);
} else if (codeStr.startsWith('#')) {
currentColor = codeStr;
} else {
const hexDigits = codeStr.replace(/(?:[&§]|\\u00a7|x)/g, '');
const hexDigits = codeStr.replace(/(?:[&§]|\\u00a7|x)/gi, '');
currentColor = '#' + hexDigits;
}
currentFmts.bold = false;
currentFmts.italic = false;
currentFmts.underline = false;
currentFmts.strikethrough = false;
currentFmts.obfuscate = false;
}

const startIdx = match.index + codeStr.length;
Expand Down