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( ... + ['' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '' ... + '%s' ... + '%s' ... + '%s' ... + '%s' ... + ''], ... + 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, " - try - matbox.py.pipUninstall('pybadges') - catch ME - disp('Pybadges was not installed') - end - end - end +%PyNamespaceTest Unit tests for the matbox.py namespace utilities. methods (TestMethodSetup) % Setup for each test - function setupMethod(testCase) + function setupMethod(testCase) %#ok end end - - methods (Test) - - function testPipUninstall(testCase) - try - matbox.py.pipUninstall('pybadges') - catch - disp('Pybadges was not installed') - end - end - function testPipInstall(testCase) - matbox.py.pipInstall('pybadges') - end + methods (Test) function testPipInstallUnknownPackage(testCase) testCase.assertError(@(name) matbox.py.pipInstall('pybaldges'), ... 'MatBox:UnableToInstallPythonPackage') end - function testGetPythonPackageInfo(testCase) - info = matbox.py.getPackageInfo('pybadges'); - testCase.verifyClass(info, 'struct') - - location = matbox.py.getPackageInfo('pybadges', "Field", "Location"); - testCase.verifyClass(location, 'string') - end end end