From 4cf3a7da1a9829219a7fdfffd93badfc1912866c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 11:49:58 +0000
Subject: [PATCH 1/2] Initial plan
From dd26bde7aa515c7f4d7883bca1309b7c8a22e4be Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Mar 2026 12:05:19 +0000
Subject: [PATCH 2/2] Replace deprecated pybadges Python dependency with pure
MATLAB SVG badge generator
Co-authored-by: ehennestad <17237719+ehennestad@users.noreply.github.com>
---
code/+matbox/+utility/createBadgeSvg.m | 270 +++++++++++++++++-
.../+unittest/CreateBadgeSvgTest.m | 114 ++++++++
.../+matboxtools/+unittest/PyNamespaceTest.m | 35 +--
3 files changed, 377 insertions(+), 42 deletions(-)
create mode 100644 tools/tests/+matboxtools/+unittest/CreateBadgeSvgTest.m
diff --git a/code/+matbox/+utility/createBadgeSvg.m b/code/+matbox/+utility/createBadgeSvg.m
index 2f5efa0..e92e961 100644
--- a/code/+matbox/+utility/createBadgeSvg.m
+++ b/code/+matbox/+utility/createBadgeSvg.m
@@ -1,5 +1,39 @@
function createBadgeSvg(label, message, color, projectRootDirectory, options)
-
+%createBadgeSvg Create an SVG badge and save it to a file.
+%
+% createBadgeSvg(label, message, color) creates a flat-style SVG badge
+% file with the given label on the left section and the message on the
+% right section. The badge is written to the ".github/badges" folder of
+% the current project root directory.
+%
+% Arguments:
+% label (string) - The label text on the left side of the badge.
+%
+% message (string) - The message text on the right side of the badge.
+%
+% color (string) - Background color of the right (message) section.
+% Must be one of: "red", "green", "blue",
+% "orange", or "yellow".
+%
+% Optional arguments:
+% projectRootDirectory (string) - Root directory of the project. Used
+% to derive the default output folder (".github/badges"). Required
+% when OutputFolder is not specified.
+%
+% Optional name/value arguments:
+% OutputFolder (string) - Path to the folder where the SVG will be
+% saved. Overrides the default derived from projectRootDirectory.
+%
+% FileName (string) - Base file name (without extension) for the
+% output SVG. Defaults to the label with spaces replaced by
+% underscores.
+%
+% Example:
+%
+% createBadgeSvg("tests", "21 passed", "green", "/path/to/project")
+%
+% creates "tests.svg" in "/path/to/project/.github/badges/".
+
arguments
label (1,1) string
message (1,1) string
@@ -9,17 +43,12 @@ function createBadgeSvg(label, message, color, projectRootDirectory, options)
options.FileName (1,1) string = missing
end
- try
- matbox.py.getPackageInfo('pybadges');
- catch
- matbox.py.pipInstall('pybadges')
- end
-
- badgeSvg = py.pybadges.badge(left_text=label, right_text=message, right_color=color);
+ badgeSvg = generateBadgeSvg(label, message, color);
if ismissing(options.OutputFolder)
if ismissing(projectRootDirectory)
- error("Please specify project root directory or output folder")
+ error("MatBox:createBadgeSvg:MissingOutputPath", ...
+ "Please specify project root directory or output folder")
end
options.OutputFolder = fullfile(projectRootDirectory, ".github", "badges");
end
@@ -37,7 +66,228 @@ function createBadgeSvg(label, message, color, projectRootDirectory, options)
filePath = fullfile(options.OutputFolder, name + ".svg");
fid = fopen(filePath, "wt");
fileCleanup = onCleanup(@() fclose(fid));
-
+
fwrite(fid, char(badgeSvg));
fprintf('Saved badge to %s\n', filePath)
end
+
+% -------------------------------------------------------------------------
+
+function svgStr = generateBadgeSvg(label, message, color)
+%generateBadgeSvg Build the SVG markup string for a flat-style badge.
+
+ colorHexMap = containers.Map( ...
+ ["red", "green", "blue", "orange", "yellow"], ...
+ ["#e05d44", "#97CA00", "#007ec6", "#fe7d37", "#dfb317"]);
+ colorHex = colorHexMap(color);
+
+ labelTextLength = computeTextWidth(label);
+ messageTextLength = computeTextWidth(message);
+
+ % Section widths in px (5 px padding each side)
+ labelWidth = labelTextLength / 10 + 10;
+ messageWidth = messageTextLength / 10 + 10;
+ totalWidth = labelWidth + messageWidth;
+
+ % Text x-positions in scale(0.1) coordinate space
+ labelTextX = labelTextLength / 2 + 60;
+ messageTextX = labelTextLength + messageTextLength / 2 + 140;
+
+ labelEsc = xmlEscape(label);
+ messageEsc = xmlEscape(message);
+
+ svgStr = sprintf( ...
+ [''], ...
+ totalWidth, ... % svg width
+ totalWidth, ... % clipPath rect width
+ labelWidth, ... % left rect width
+ labelWidth, messageWidth, ... % right rect x + width
+ colorHex, ... % right rect color
+ totalWidth, ... % gradient rect width
+ labelTextX, labelTextLength, labelEsc, ... % label shadow text
+ labelTextX, labelTextLength, labelEsc, ... % label text
+ messageTextX, messageTextLength, messageEsc, ... % message shadow text
+ messageTextX, messageTextLength, messageEsc); % message text
+end
+
+% -------------------------------------------------------------------------
+
+function width = computeTextWidth(text)
+%computeTextWidth Compute total text width in SVG textLength units.
+%
+% Width values are based on Verdana 11 px font metrics, scaled so that
+% dividing by 10 gives the rendered pixel width at the badge font size.
+
+ charWidths = getCharWidths();
+ chars = double(char(text));
+ width = 0;
+ for k = 1:numel(chars)
+ idx = chars(k) - 31; % Maps ASCII 32 (space) -> index 1, ..., ASCII 126 (~) -> index 95
+ if idx >= 1 && idx <= numel(charWidths)
+ width = width + charWidths(idx);
+ else
+ width = width + 70; % Fallback width; approximates the mean Verdana 11 px glyph width
+ end
+ end
+end
+
+% -------------------------------------------------------------------------
+
+function widths = getCharWidths()
+%getCharWidths Return per-character width table for Verdana 11 px.
+%
+% Values are in SVG textLength units (10x the rendered pixel width).
+% Index 1 corresponds to ASCII 32 (space); index 95 to ASCII 126 (~).
+
+ widths = zeros(1, 95);
+
+ % --- Space and basic punctuation (ASCII 32-47) ---
+ widths(1) = 33; % ' '
+ widths(2) = 38; % '!'
+ widths(3) = 45; % '"'
+ widths(4) = 80; % '#'
+ widths(5) = 62; % '$'
+ widths(6) = 93; % '%'
+ widths(7) = 78; % '&'
+ widths(8) = 25; % '''
+ widths(9) = 38; % '('
+ widths(10) = 38; % ')'
+ widths(11) = 55; % '*'
+ widths(12) = 80; % '+'
+ widths(13) = 37; % ','
+ widths(14) = 42; % '-'
+ widths(15) = 35; % '.'
+ widths(16) = 40; % '/'
+
+ % --- Digits (ASCII 48-57) ---
+ widths(17) = 73; % '0'
+ widths(18) = 64; % '1'
+ widths(19) = 70; % '2'
+ widths(20) = 70; % '3'
+ widths(21) = 73; % '4'
+ widths(22) = 70; % '5'
+ widths(23) = 73; % '6'
+ widths(24) = 70; % '7'
+ widths(25) = 73; % '8'
+ widths(26) = 73; % '9'
+
+ % --- Punctuation (ASCII 58-64) ---
+ widths(27) = 38; % ':'
+ widths(28) = 38; % ';'
+ widths(29) = 80; % '<'
+ widths(30) = 80; % '='
+ widths(31) = 80; % '>'
+ widths(32) = 63; % '?'
+ widths(33) = 125; % '@'
+
+ % --- Uppercase letters (ASCII 65-90) ---
+ widths(34) = 76; % 'A'
+ widths(35) = 73; % 'B'
+ widths(36) = 73; % 'C'
+ widths(37) = 83; % 'D'
+ widths(38) = 66; % 'E'
+ widths(39) = 62; % 'F'
+ widths(40) = 80; % 'G'
+ widths(41) = 83; % 'H'
+ widths(42) = 28; % 'I'
+ widths(43) = 45; % 'J'
+ widths(44) = 76; % 'K'
+ widths(45) = 62; % 'L'
+ widths(46) = 89; % 'M'
+ widths(47) = 83; % 'N'
+ widths(48) = 86; % 'O'
+ widths(49) = 69; % 'P'
+ widths(50) = 86; % 'Q'
+ widths(51) = 76; % 'R'
+ widths(52) = 65; % 'S'
+ widths(53) = 66; % 'T'
+ widths(54) = 83; % 'U'
+ widths(55) = 76; % 'V'
+ widths(56) = 102; % 'W'
+ widths(57) = 71; % 'X'
+ widths(58) = 71; % 'Y'
+ widths(59) = 68; % 'Z'
+
+ % --- Punctuation (ASCII 91-96) ---
+ widths(60) = 38; % '['
+ widths(61) = 43; % '\'
+ widths(62) = 38; % ']'
+ widths(63) = 80; % '^'
+ widths(64) = 55; % '_'
+ widths(65) = 55; % '`'
+
+ % --- Lowercase letters (ASCII 97-122) ---
+ widths(66) = 73; % 'a'
+ widths(67) = 73; % 'b'
+ widths(68) = 60; % 'c'
+ widths(69) = 73; % 'd'
+ widths(70) = 66; % 'e'
+ widths(71) = 38; % 'f'
+ widths(72) = 73; % 'g'
+ widths(73) = 73; % 'h'
+ widths(74) = 28; % 'i'
+ widths(75) = 28; % 'j'
+ widths(76) = 66; % 'k'
+ widths(77) = 28; % 'l'
+ widths(78) = 103; % 'm'
+ widths(79) = 73; % 'n'
+ widths(80) = 73; % 'o'
+ widths(81) = 73; % 'p'
+ widths(82) = 73; % 'q'
+ widths(83) = 43; % 'r'
+ widths(84) = 56; % 's'
+ widths(85) = 45; % 't'
+ widths(86) = 73; % 'u'
+ widths(87) = 66; % 'v'
+ widths(88) = 93; % 'w'
+ widths(89) = 64; % 'x'
+ widths(90) = 66; % 'y'
+ widths(91) = 58; % 'z'
+
+ % --- Remaining punctuation (ASCII 123-126) ---
+ widths(92) = 42; % '{'
+ widths(93) = 40; % '|'
+ widths(94) = 42; % '}'
+ widths(95) = 80; % '~'
+end
+
+% -------------------------------------------------------------------------
+
+function escaped = xmlEscape(text)
+%xmlEscape Replace special XML characters with entity references.
+ escaped = char(text);
+ escaped = strrep(escaped, '&', '&');
+ escaped = strrep(escaped, '<', '<');
+ escaped = strrep(escaped, '>', '>');
+ escaped = strrep(escaped, '"', '"');
+ escaped = strrep(escaped, '''', ''');
+end
diff --git a/tools/tests/+matboxtools/+unittest/CreateBadgeSvgTest.m b/tools/tests/+matboxtools/+unittest/CreateBadgeSvgTest.m
new file mode 100644
index 0000000..272cb4f
--- /dev/null
+++ b/tools/tests/+matboxtools/+unittest/CreateBadgeSvgTest.m
@@ -0,0 +1,114 @@
+classdef CreateBadgeSvgTest < matlab.unittest.TestCase
+%CreateBadgeSvgTest Unit tests for matbox.utility.createBadgeSvg.
+
+ properties (Constant)
+ OutputFolder = fullfile(tempdir, "matbox_badge_test")
+ end
+
+ methods (TestClassSetup)
+ function setupClass(testCase) %#ok
+ if ~isfolder(CreateBadgeSvgTest.OutputFolder)
+ mkdir(CreateBadgeSvgTest.OutputFolder)
+ end
+ end
+ end
+
+ methods (TestClassTeardown)
+ function tearDownClass(testCase) %#ok
+ if isfolder(CreateBadgeSvgTest.OutputFolder)
+ rmdir(CreateBadgeSvgTest.OutputFolder, 's')
+ end
+ end
+ end
+
+ methods (Test)
+
+ function testBadgeFileIsCreated(testCase)
+ matbox.utility.createBadgeSvg("tests", "21 passed", "green", ...
+ "OutputFolder", testCase.OutputFolder);
+ filePath = fullfile(testCase.OutputFolder, "tests.svg");
+ testCase.verifyTrue(isfile(filePath), ...
+ "Expected SVG file was not created.")
+ end
+
+ function testBadgeFileHasSvgContent(testCase)
+ matbox.utility.createBadgeSvg("build", "passing", "blue", ...
+ "OutputFolder", testCase.OutputFolder);
+ filePath = fullfile(testCase.OutputFolder, "build.svg");
+ content = fileread(filePath);
+ testCase.verifySubstring(content, "