Repository: paularmstrong/build-tracker (ts)

Issue:
`~` in Markdown table is not escaped
# Problem

<!--
Add a short explanation of your problem or use-case

Example:
When I click the drawer link, nothing happens!
-->

For example, a file named:

```
collection-route~collections-feed-route~editorial-route~explore-route~following-route~photos-route~s~0690aa50
```

**Steps to Reproduce:**

<!--
Include a list of steps that anyone can take to reproduce the bug
If you have a code sample, this is a great place to include it

Example:
1. Navigate to the main Build Tracker page
2. Click the menu button to open the side drawer
3. Click "Documentation" link
-->

**Expected Result**

<!--
Write out what you expect to see or have happen when running the previous steps

Example:
The link should open a new page to the documentation
-->

Should be escaped, e.g.

```
collection-route\~collections-feed-route\~editorial-route\~explore-route\~following-route\~photos-route\~s\~0690aa50
```

which appears as

collection-route\~collections-feed-route\~editorial-route\~explore-route\~following-route\~photos-route\~s\~0690aa50

**Actual Result**

(In Markdown, `~` is used to begin a strikethrough.)

Not escaped, e.g.

collection-route~collections-feed-route~editorial-route~explore-route~following-route~photos-route~s~0690aa50

<!--
Write out what *actually* happens when running the previous steps

Example:
Nothing happens. But I do see an error in the browser's console. It looks like this:
> TypeError: cannot read property href of undefined
-->

![image](https://user-images.githubusercontent.com/921609/77089016-2d8bcb00-69fd-11ea-88d6-d01c1446c3d5.png)

Pull request description:
fix(comparator): ensure artifact names have tildes escaped for markdown
<!--
Thank you so much for contributing to open source and the Build Tracker project!
-->

# Problem

When outputting markdown tables from the comparator `toMarkdown` function, when artifact names have more than one tilde (~) in them, it strikes through some of the text.

For example:
I expect `foo~bar~baz` to have two tildes, but instead, it looks like this:
foo~bar~baz

# Solution

For now, just escape those single characters both in `toMarkdown()` as well as `toSummary()`

fixes #179

# TODO

- [x] 🤓 Add & update tests (always try to _increase_ test coverage)
- [x] 🔬 Ensure CI is passing (`yarn lint:ci`, `yarn test`, `yarn tsc:ci`)
- [x] 📖 Update relevant documentation


Patch:
diff --git a/docs/docs/packages/comparator.md b/docs/docs/packages/comparator.md
index 8e5fb86..a7daff3 100644
--- a/docs/docs/packages/comparator.md
+++ b/docs/docs/packages/comparator.md
@@ -130,6 +130,7 @@ interface ComparatorOptions {
 
 ```ts
 interface FormattingOptions {
+  formatCellText?: CellTextStringFormatter;
   formatRevision?: RevisionStringFormatter;
   formatRevisionDelta?: RevisionDeltaStringFormatter;
   formatTotal?: TotalStringFormatter;
@@ -142,6 +143,7 @@ interface FormattingOptions {
 ### Formatters
 
 ```ts
+type CellTextStringFormatter = (cell: TextCell | GroupCell | ArtifactCell) => string;
 type RevisionStringFormatter = (cell: RevisionCell) => string;
 type RevisionDeltaStringFormatter = (cell: RevisionDeltaCell) => string;
 type TotalStringFormatter = (cell: TotalCell, sizeKey: string) => string;
diff --git a/src/comparator/src/index.ts b/src/comparator/src/index.ts
index bd114c0..3fc9685 100644
--- a/src/comparator/src/index.ts
+++ b/src/comparator/src/index.ts
@@ -91,6 +91,7 @@ export interface ComparisonMatrix {
   artifacts: Array<ArtifactRow>;
 }
 
+export type CellTextStringFormatter = (cell: TextCell | GroupCell | ArtifactCell) => string;
 export type RevisionStringFormatter = (cell: RevisionCell) => string;
 export type RevisionDeltaStringFormatter = (cell: RevisionDeltaCell) => string;
 export type TotalStringFormatter = (cell: TotalCell, sizeKey: string) => string;
@@ -98,6 +99,7 @@ export type DeltaStringFormatter = (cell: DeltaCell | TotalDeltaCell, sizeKey: s
 export type ArtifactFilter = (row: ArtifactRow) => boolean;
 
 export interface FormattingOptions {
+  formatCellText?: CellTextStringFormatter;
   formatRevision?: RevisionStringFormatter;
   formatRevisionDelta?: RevisionDeltaStringFormatter;
   formatTotal?: TotalStringFormatter;
@@ -112,6 +114,8 @@ const emptyObject = Object.freeze({});
 const flatten = (arrays: Array<any>): Array<any> => arrays.reduce((memo: Array<any>, b: any) => memo.concat(b), []);
 /* eslint-enable @typescript-eslint/no-explicit-any */
 
+const defaultFormatCellText = (cell: TextCell | GroupCell | ArtifactCell): string => cell.text;
+const formatCellTextMarkdown = (cell: TextCell | GroupCell | ArtifactCell): string => cell.text.replace(/~/g, '\\~');
 const defaultFormatRevision = (cell: RevisionCell): string => formatSha(cell.revision);
 const defaultFormatRevisionDelta = (cell: RevisionDeltaCell): string => `Δ${cell.deltaIndex}`;
 const defaultFormatTotal = (cell: TotalCell, sizeKey: string): string => formatBytes(cell.sizes[sizeKey] || 0);
@@ -379,6 +383,7 @@ export default class BuildComparator {
   }
 
   public getStringFormattedGroups(
+    formatCellText: CellTextStringFormatter = defaultFormatCellText,
     formatTotal: TotalStringFormatter = defaultFormatTotal,
     formatDelta: DeltaStringFormatter = defaultFormatDelta,
     sizeKey = 'gzip'
@@ -388,7 +393,7 @@ export default class BuildComparator {
         (cell): string => {
           switch (cell.type) {
             case CellType.GROUP:
-              return cell.text;
+              return formatCellText(cell as GroupCell);
             case CellType.TOTAL_DELTA:
               return formatDelta(cell as TotalDeltaCell, sizeKey);
             case CellType.TOTAL:
@@ -400,6 +405,7 @@ export default class BuildComparator {
   }
 
   public getStringFormattedRows(
+    formatCellText: CellTextStringFormatter = defaultFormatCellText,
     formatTotal: TotalStringFormatter = defaultFormatTotal,
     formatDelta: DeltaStringFormatter = defaultFormatDelta,
     sizeKey = 'gzip',
@@ -411,7 +417,7 @@ export default class BuildComparator {
           (cell): string => {
             switch (cell.type) {
               case CellType.ARTIFACT:
-                return cell.text;
+                return formatCellText(cell as ArtifactCell);
               case CellType.DELTA:
                 return formatDelta(cell as DeltaCell, sizeKey);
               case CellType.TOTAL:
@@ -432,6 +438,7 @@ export default class BuildComparator {
   }
 
   public toMarkdown({
+    formatCellText = formatCellTextMarkdown,
     formatRevision,
     formatRevisionDelta,
     formatTotal,
@@ -440,8 +447,8 @@ export default class BuildComparator {
     sizeKey
   }: FormattingOptions = {}): string {
     const header = this.getStringFormattedHeader(formatRevision, formatRevisionDelta);
-    const groups = this.getStringFormattedGroups(formatTotal, formatDelta, sizeKey);
-    const rows = this.getStringFormattedRows(formatTotal, formatDelta, sizeKey, artifactFilter);
+    const groups = this.getStringFormattedGroups(formatCellText, formatTotal, formatDelta, sizeKey);
+    const rows = this.getStringFormattedRows(formatCellText, formatTotal, formatDelta, sizeKey, artifactFilter);
 
     const allRows = [header, ...groups, ...rows];
 
@@ -449,6 +456,7 @@ export default class BuildComparator {
   }
 
   public toCsv({
+    formatCellText,
     formatRevision,
     formatRevisionDelta,
     formatTotal,
@@ -457,8 +465,8 @@ export default class BuildComparator {
     sizeKey
   }: FormattingOptions = {}): string {
     const header = this.getStringFormattedHeader(formatRevision, formatRevisionDelta);
-    const groups = this.getStringFormattedGroups(formatTotal, formatDelta, sizeKey);
-    const rows = this.getStringFormattedRows(formatTotal, formatDelta, sizeKey, artifactFilter);
+    const groups = this.getStringFormattedGroups(formatCellText, formatTotal, formatDelta, sizeKey);
+    const rows = this.getStringFormattedRows(formatCellText, formatTotal, formatDelta, sizeKey, artifactFilter);
 
     return [header, ...groups, ...rows].map(row => `${row.join(',')}`).join(`\r\n`);
   }
diff --git a/src/formatting/src/index.ts b/src/formatting/src/index.ts
index a235405..a5f328a 100644
--- a/src/formatting/src/index.ts
+++ b/src/formatting/src/index.ts
@@ -46,7 +46,7 @@ export const levelToEmoji = {
 };
 
 function getPrefix(artifactName: string, level: string, useEmoji = false): string {
-  return `${(useEmoji ? levelToEmoji : levelToString)[level]}: \`${artifactName}\``;
+  return `${(useEmoji ? levelToEmoji : levelToString)[level]}: *${artifactName.replace(/~/g, '\\~')}*`;
 }
 
 export function formatBudgetResult(budgetResult: BudgetResult, artifactName: string, useEmoji = false): string {


Test patch:
diff --git a/src/app/src/components/ComparisonTable/__tests__/DeltaCell.test.tsx b/src/app/src/components/ComparisonTable/__tests__/DeltaCell.test.tsx
index be9e901..8d509cc 100644
--- a/src/app/src/components/ComparisonTable/__tests__/DeltaCell.test.tsx
+++ b/src/app/src/components/ComparisonTable/__tests__/DeltaCell.test.tsx
@@ -85,7 +85,7 @@ describe('DeltaCell', () => {
         />
       );
       expect(queryAllByType(HashIcon)).toHaveLength(1);
-      expect(getByType(Td).props.accessibilityLabel).toEqual('Hash: `tacos` hash changed without any file size change');
+      expect(getByType(Td).props.accessibilityLabel).toEqual('Hash: *tacos* hash changed without any file size change');
     });
 
     test('shows a warning icon if warning budget fails', () => {
diff --git a/src/comparator/src/__tests__/index.test.ts b/src/comparator/src/__tests__/index.test.ts
index 9da6dfe..17aead3 100644
--- a/src/comparator/src/__tests__/index.test.ts
+++ b/src/comparator/src/__tests__/index.test.ts
@@ -33,7 +33,7 @@ const build2 = new Build(
   { branch: 'master', revision: '8901234abcdef', parentRevision: 'abcdef', timestamp: 8901234 },
   [
     { name: 'tacos', hash: 'abc', sizes: { stat: 123, gzip: 43 } },
-    { name: 'churros', hash: 'def', sizes: { stat: 469, gzip: 120 } }
+    { name: 'churros~burritos~tacos', hash: 'def', sizes: { stat: 469, gzip: 120 } }
   ]
 );
 // Alternative to build2 with burritos
@@ -114,12 +114,12 @@ describe('BuildComparator', () => {
   describe('artifactNames', () => {
     test('includes all artifact names', () => {
       const comparator = new BuildComparator({ builds: [build1, build2] });
-      expect(comparator.artifactNames).toEqual(expect.arrayContaining(['burritos', 'churros', 'tacos']));
+      expect(comparator.artifactNames).toEqual(expect.arrayContaining(['burritos', 'churros~burritos~tacos', 'tacos']));
     });
 
     test('sorts artifacts by average size', () => {
       const comparator = new BuildComparator({ builds: [build1, build2] });
-      expect(comparator.artifactNames).toEqual(['churros', 'burritos', 'tacos']);
+      expect(comparator.artifactNames).toEqual(['churros~burritos~tacos', 'burritos', 'tacos']);
     });
 
     test('returns an empty array if there are no builds', () => {
@@ -211,7 +211,7 @@ describe('BuildComparator', () => {
     test('includes a row for each artifact', () => {
       const comparator = new BuildComparator({ builds: [build1, build2] });
       expect(comparator.matrixArtifacts.map(r => r[0])).toEqual([
-        { type: 'artifact', text: 'churros' },
+        { type: 'artifact', text: 'churros~burritos~tacos' },
         { type: 'artifact', text: 'burritos' },
         { type: 'artifact', text: 'tacos' }
       ]);
@@ -249,7 +249,7 @@ describe('BuildComparator', () => {
       expect(comparator.matrixGroups[0][0]).toEqual({
         type: 'group',
         text: 'All',
-        artifactNames: ['churros', 'burritos', 'tacos']
+        artifactNames: ['churros~burritos~tacos', 'burritos', 'tacos']
       });
     });
 
@@ -283,7 +283,7 @@ describe('BuildComparator', () => {
     test('includes totals for each group', () => {
       const comparator = new BuildComparator({
         builds: [build1, build2],
-        groups: [{ name: 'stuff', artifactNames: ['churros', 'burritos'] }]
+        groups: [{ name: 'stuff', artifactNames: ['churros~burritos~tacos', 'burritos'] }]
       });
       expect(comparator.matrixGroups[1][1]).toEqual({
         type: 'total',
@@ -300,7 +300,7 @@ describe('BuildComparator', () => {
     test('includes deltas for each group', () => {
       const comparator = new BuildComparator({
         builds: [build1, build2],
-        groups: [{ name: 'stuff', artifactNames: ['churros', 'burritos'] }]
+        groups: [{ name: 'stuff', artifactNames: ['churros~burritos~tacos', 'burritos'] }]
       });
       expect(comparator.matrixGroups[1][3]).toEqual({
         type: 'totalDelta',
@@ -364,12 +364,12 @@ describe('BuildComparator', () => {
     test('gets a markdown-formatted table', () => {
       const comparator = new BuildComparator({ builds: [build1, build2] });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                  Δ1 |
-        | :------- | -------: | -------: | ------------------: |
-        | All      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
-        | churros  |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
-        | burritos | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-        | tacos    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
+        "|                          |  1234567 |  8901234 |                  Δ1 |
+        | :----------------------- | -------: | -------: | ------------------: |
+        | All                      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
+        | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
+        | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
+        | tacos                    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
       `);
     });
 
@@ -385,12 +385,12 @@ describe('BuildComparator', () => {
     test('can specify a different size key', () => {
       const comparator = new BuildComparator({ builds: [build1, build2] });
       expect(comparator.toMarkdown({ sizeKey: 'stat' })).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                  Δ1 |
-        | :------- | -------: | -------: | ------------------: |
-        | All      | 0.57 KiB | 0.58 KiB |     0.01 KiB (2.2%) |
-        | churros  |    0 KiB | 0.46 KiB |   0.46 KiB (100.0%) |
-        | burritos | 0.45 KiB |    0 KiB | -0.45 KiB (-100.0%) |
-        | tacos    | 0.12 KiB | 0.12 KiB |        0 KiB (0.0%) |"
+        "|                          |  1234567 |  8901234 |                  Δ1 |
+        | :----------------------- | -------: | -------: | ------------------: |
+        | All                      | 0.57 KiB | 0.58 KiB |     0.01 KiB (2.2%) |
+        | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.46 KiB |   0.46 KiB (100.0%) |
+        | burritos                 | 0.45 KiB |    0 KiB | -0.45 KiB (-100.0%) |
+        | tacos                    | 0.12 KiB | 0.12 KiB |        0 KiB (0.0%) |"
       `);
     });
 
@@ -405,11 +405,11 @@ describe('BuildComparator', () => {
         });
       };
       expect(comparator.toMarkdown({ artifactFilter })).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                  Δ1 |
-        | :------- | -------: | -------: | ------------------: |
-        | All      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
-        | churros  |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
-        | burritos | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |"
+        "|                          |  1234567 |  8901234 |                  Δ1 |
+        | :----------------------- | -------: | -------: | ------------------: |
+        | All                      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
+        | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
+        | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |"
       `);
     });
 
@@ -435,11 +435,11 @@ describe('BuildComparator', () => {
       const artifactFilter = (row: ArtifactRow): boolean => row[0].text !== 'burritos';
       expect(comparator.toMarkdown({ formatRevision, formatRevisionDelta, formatTotal, formatDelta, artifactFilter }))
         .toMatchInlineSnapshot(`
-        "|         |  12 |  89 |    d1 |
-        | :------ | --: | --: | ----: |
-        | All     | 579 | 592 |  0.21 |
-        | churros |   0 | 469 |  1.00 |
-        | tacos   | 123 | 123 | -0.04 |"
+        "|                          |  12 |  89 |    d1 |
+        | :----------------------- | --: | --: | ----: |
+        | All                      | 579 | 592 |  0.21 |
+        | churros\\\\~burritos\\\\~tacos |   0 | 469 |  1.00 |
+        | tacos                    | 123 | 123 | -0.04 |"
       `);
     });
 
@@ -455,12 +455,12 @@ describe('BuildComparator', () => {
         }
       });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                   Δ1 |
-        | :------- | -------: | -------: | -------------------: |
-        | All      | 0.13 KiB | 0.16 KiB |     0.03 KiB (20.7%) |
-        | churros  |    0 KiB | 0.12 KiB | ⚠️ 0.12 KiB (100.0%) |
-        | burritos | 0.09 KiB |    0 KiB |  -0.09 KiB (-100.0%) |
-        | tacos    | 0.04 KiB | 0.04 KiB |     🚨 0 KiB (-4.4%) |"
+        "|                          |  1234567 |  8901234 |                  Δ1 |
+        | :----------------------- | -------: | -------: | ------------------: |
+        | All                      | 0.13 KiB | 0.16 KiB |    0.03 KiB (20.7%) |
+        | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
+        | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
+        | tacos                    | 0.04 KiB | 0.04 KiB |    🚨 0 KiB (-4.4%) |"
       `);
     });
 
@@ -473,12 +473,12 @@ describe('BuildComparator', () => {
         ]
       });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                  Δ1 |
-        | :------- | -------: | -------: | ------------------: |
-        | All      | 0.13 KiB | 0.16 KiB | 🚨 0.03 KiB (20.7%) |
-        | churros  |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
-        | burritos | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-        | tacos    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
+        "|                          |  1234567 |  8901234 |                  Δ1 |
+        | :----------------------- | -------: | -------: | ------------------: |
+        | All                      | 0.13 KiB | 0.16 KiB | 🚨 0.03 KiB (20.7%) |
+        | churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
+        | burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
+        | tacos                    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
       `);
     });
 
@@ -488,13 +488,13 @@ describe('BuildComparator', () => {
         budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }]
       });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                  Δ1 |
-        | :------- | -------: | -------: | ------------------: |
-        | All      | 0.13 KiB | 0.16 KiB | ⚠️ 0.03 KiB (20.7%) |
-        | churros  |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
-        | burritos | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
-        | tacos    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
-      `);
+"|                          |  1234567 |  8901234 |                  Δ1 |
+| :----------------------- | -------: | -------: | ------------------: |
+| All                      | 0.13 KiB | 0.16 KiB | ⚠️ 0.03 KiB (20.7%) |
+| churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |   0.12 KiB (100.0%) |
+| burritos                 | 0.09 KiB |    0 KiB | -0.09 KiB (-100.0%) |
+| tacos                    | 0.04 KiB | 0.04 KiB |       0 KiB (-4.4%) |"
+`);
     });
 
     test('includes hash emoji for unexpected hash changes', () => {
@@ -502,12 +502,12 @@ describe('BuildComparator', () => {
         builds: [build1, build2b]
       });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-"|          |  1234567 |  8901234 |               Δ1 |
-| :------- | -------: | -------: | ---------------: |
-| All      | 0.13 KiB | 0.13 KiB | #️⃣ 0 KiB (0.0%) |
-| burritos | 0.09 KiB | 0.09 KiB | #️⃣ 0 KiB (0.0%) |
-| tacos    | 0.04 KiB | 0.04 KiB |     0 KiB (0.0%) |"
-`);
+        "|          |  1234567 |  8901234 |               Δ1 |
+        | :------- | -------: | -------: | ---------------: |
+        | All      | 0.13 KiB | 0.13 KiB | #️⃣ 0 KiB (0.0%) |
+        | burritos | 0.09 KiB | 0.09 KiB | #️⃣ 0 KiB (0.0%) |
+        | tacos    | 0.04 KiB | 0.04 KiB |     0 KiB (0.0%) |"
+      `);
     });
 
     test('includes emoji for failing group budgets', () => {
@@ -515,27 +515,27 @@ describe('BuildComparator', () => {
         builds: [build1, build2],
         groups: [
           {
-            artifactNames: ['churros', 'burritos'],
+            artifactNames: ['churros~burritos~tacos', 'burritos'],
             budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
             name: 'warning'
           },
           {
-            artifactNames: ['churros', 'tacos'],
+            artifactNames: ['churros~burritos~tacos', 'tacos'],
             budgets: [{ level: BudgetLevel.ERROR, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
             name: 'error'
           }
         ]
       });
       expect(comparator.toMarkdown()).toMatchInlineSnapshot(`
-        "|          |  1234567 |  8901234 |                   Δ1 |
-        | :------- | -------: | -------: | -------------------: |
-        | All      | 0.13 KiB | 0.16 KiB |     0.03 KiB (20.7%) |
-        | warning  | 0.09 KiB | 0.12 KiB |  ⚠️ 0.03 KiB (33.3%) |
-        | error    | 0.04 KiB | 0.16 KiB | 🚨 0.12 KiB (262.2%) |
-        | churros  |    0 KiB | 0.12 KiB |    0.12 KiB (100.0%) |
-        | burritos | 0.09 KiB |    0 KiB |  -0.09 KiB (-100.0%) |
-        | tacos    | 0.04 KiB | 0.04 KiB |        0 KiB (-4.4%) |"
-      `);
+"|                          |  1234567 |  8901234 |                   Δ1 |
+| :----------------------- | -------: | -------: | -------------------: |
+| All                      | 0.13 KiB | 0.16 KiB |     0.03 KiB (20.7%) |
+| warning                  | 0.09 KiB | 0.12 KiB |  ⚠️ 0.03 KiB (33.3%) |
+| error                    | 0.04 KiB | 0.16 KiB | 🚨 0.12 KiB (262.2%) |
+| churros\\\\~burritos\\\\~tacos |    0 KiB | 0.12 KiB |    0.12 KiB (100.0%) |
+| burritos                 | 0.09 KiB |    0 KiB |  -0.09 KiB (-100.0%) |
+| tacos                    | 0.04 KiB | 0.04 KiB |        0 KiB (-4.4%) |"
+`);
     });
   });
 
@@ -545,7 +545,7 @@ describe('BuildComparator', () => {
       expect(comparator.toCsv()).toMatchInlineSnapshot(`
         ",1234567,8901234,Δ1
         All,0.13 KiB,0.16 KiB,0.03 KiB (20.7%)
-        churros,0 KiB,0.12 KiB,0.12 KiB (100.0%)
+        churros~burritos~tacos,0 KiB,0.12 KiB,0.12 KiB (100.0%)
         burritos,0.09 KiB,0 KiB,-0.09 KiB (-100.0%)
         tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
       `);
@@ -556,7 +556,7 @@ describe('BuildComparator', () => {
       expect(comparator.toCsv({ sizeKey: 'stat' })).toMatchInlineSnapshot(`
         ",1234567,8901234,Δ1
         All,0.57 KiB,0.58 KiB,0.01 KiB (2.2%)
-        churros,0 KiB,0.46 KiB,0.46 KiB (100.0%)
+        churros~burritos~tacos,0 KiB,0.46 KiB,0.46 KiB (100.0%)
         burritos,0.45 KiB,0 KiB,-0.45 KiB (-100.0%)
         tacos,0.12 KiB,0.12 KiB,0 KiB (0.0%)"
       `);
@@ -573,11 +573,11 @@ describe('BuildComparator', () => {
         });
       };
       expect(comparator.toCsv({ artifactFilter })).toMatchInlineSnapshot(`
-        ",1234567,8901234,Δ1
-        All,0.13 KiB,0.16 KiB,0.03 KiB (20.7%)
-        churros,0 KiB,0.12 KiB,0.12 KiB (100.0%)
-        burritos,0.09 KiB,0 KiB,-0.09 KiB (-100.0%)"
-      `);
+",1234567,8901234,Δ1
+All,0.13 KiB,0.16 KiB,0.03 KiB (20.7%)
+churros~burritos~tacos,0 KiB,0.12 KiB,0.12 KiB (100.0%)
+burritos,0.09 KiB,0 KiB,-0.09 KiB (-100.0%)"
+`);
     });
 
     test('does not include filtered artifacts', () => {
@@ -601,11 +601,11 @@ tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
       const artifactFilter = (row: ArtifactRow): boolean => row[0].text !== 'burritos';
       expect(comparator.toCsv({ formatRevision, formatRevisionDelta, formatTotal, formatDelta, artifactFilter }))
         .toMatchInlineSnapshot(`
-        ",12,89,d1
-        All,579,592,0.21
-        churros,0,469,1.00
-        tacos,123,123,-0.04"
-      `);
+",12,89,d1
+All,579,592,0.21
+churros~burritos~tacos,0,469,1.00
+tacos,123,123,-0.04"
+`);
     });
   });
 
@@ -622,9 +622,9 @@ tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
         }
       });
       expect(comparator.toSummary().join('\n')).toMatchInlineSnapshot(`
-"#️⃣: \`burritos\` hash changed without any file size change
-⚠️: \`tacos\` failed the gzip budget size limit of 0 KiB by 0.04 KiB
-🚨: \`tacos\` failed the gzip budget size limit of 0.03 KiB by 0.01 KiB"
+"#️⃣: *burritos* hash changed without any file size change
+⚠️: *tacos* failed the gzip budget size limit of 0 KiB by 0.04 KiB
+🚨: *tacos* failed the gzip budget size limit of 0.03 KiB by 0.01 KiB"
 `);
     });
 
@@ -634,22 +634,22 @@ tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
         budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
         groups: [
           {
-            artifactNames: ['churros', 'burritos'],
+            artifactNames: ['churros~burritos~tacos', 'burritos'],
             budgets: [{ level: BudgetLevel.WARN, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
             name: 'warning'
           },
           {
-            artifactNames: ['churros', 'tacos'],
+            artifactNames: ['churros~burritos~tacos', 'tacos'],
             budgets: [{ level: BudgetLevel.ERROR, type: BudgetType.SIZE, maximum: 1, sizeKey: 'gzip' }],
             name: 'error'
           }
         ]
       });
       expect(comparator.toSummary().join('\n')).toMatchInlineSnapshot(`
-        "⚠️: \`Group \\"All\\"\` failed the gzip budget size limit of 0 KiB by 0.16 KiB
-        ⚠️: \`Group \\"warning\\"\` failed the gzip budget size limit of 0 KiB by 0.12 KiB
-        🚨: \`Group \\"error\\"\` failed the gzip budget size limit of 0 KiB by 0.16 KiB"
-      `);
+"⚠️: *Group \\"All\\"* failed the gzip budget size limit of 0 KiB by 0.16 KiB
+⚠️: *Group \\"warning\\"* failed the gzip budget size limit of 0 KiB by 0.12 KiB
+🚨: *Group \\"error\\"* failed the gzip budget size limit of 0 KiB by 0.16 KiB"
+`);
     });
 
     test('can render budgets without emoji', () => {
@@ -662,9 +662,9 @@ tacos,0.04 KiB,0.04 KiB,0 KiB (-4.4%)"
       });
 
       expect(comparator.toSummary(false).join('\n')).toMatchInlineSnapshot(`
-        "Warning: \`Group \\"All\\"\` failed the gzip budget size limit of 0 KiB by 0.16 KiB
-        Error: \`tacos\` failed the gzip budget size limit of 0.03 KiB by 0.01 KiB"
-      `);
+"Warning: *Group \\"All\\"* failed the gzip budget size limit of 0 KiB by 0.16 KiB
+Error: *tacos* failed the gzip budget size limit of 0.03 KiB by 0.01 KiB"
+`);
     });
 
     test('returns success if no failing budgets', () => {
diff --git a/src/formatting/src/__tests__/index.test.ts b/src/formatting/src/__tests__/index.test.ts
index 5b1a84d..6a0b817 100644
--- a/src/formatting/src/__tests__/index.test.ts
+++ b/src/formatting/src/__tests__/index.test.ts
@@ -60,7 +60,24 @@ describe('formatBudgetResult', () => {
           'tacos',
           true
         )
-      ).toEqual('🚨: `tacos` failed the stat budget size limit of 2 KiB by 2 KiB');
+      ).toEqual('🚨: *tacos* failed the stat budget size limit of 2 KiB by 2 KiB');
+    });
+
+    test('escapes ~ in artifact names', () => {
+      expect(
+        formatBudgetResult(
+          {
+            sizeKey: 'stat',
+            passing: false,
+            expected: 2048,
+            actual: 4096,
+            type: BudgetType.SIZE,
+            level: BudgetLevel.ERROR
+          },
+          'churros~burritos~tacos',
+          true
+        )
+      ).toEqual('🚨: *churros\\~burritos\\~tacos* failed the stat budget size limit of 2 KiB by 2 KiB');
     });
 
     test('size', () => {
@@ -76,7 +93,7 @@ describe('formatBudgetResult', () => {
           },
           'tacos'
         )
-      ).toEqual('Error: `tacos` failed the stat budget size limit of 2 KiB by 2 KiB');
+      ).toEqual('Error: *tacos* failed the stat budget size limit of 2 KiB by 2 KiB');
     });
 
     test('delta', () => {
@@ -93,7 +110,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Error: `tacos` failed the stat budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
+        'Error: *tacos* failed the stat budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
       );
     });
 
@@ -111,7 +128,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Error: `tacos` failed the stat budget percent change limit. Expected to not increase by more than 10%, but increased by 22.4%'
+        'Error: *tacos* failed the stat budget percent change limit. Expected to not increase by more than 10%, but increased by 22.4%'
       );
     });
   });
@@ -131,7 +148,7 @@ describe('formatBudgetResult', () => {
           'tacos',
           true
         )
-      ).toEqual('⚠️: `tacos` failed the stat budget size limit of 2 KiB by 2 KiB');
+      ).toEqual('⚠️: *tacos* failed the stat budget size limit of 2 KiB by 2 KiB');
     });
 
     test('size', () => {
@@ -147,7 +164,7 @@ describe('formatBudgetResult', () => {
           },
           'tacos'
         )
-      ).toEqual('Warning: `tacos` failed the brotli budget size limit of 2 KiB by 2 KiB');
+      ).toEqual('Warning: *tacos* failed the brotli budget size limit of 2 KiB by 2 KiB');
     });
 
     test('delta', () => {
@@ -164,7 +181,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Warning: `tacos` failed the gzip budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
+        'Warning: *tacos* failed the gzip budget delta limit. Expected to increase no more than 2 KiB, but increased by 4 KiB'
       );
     });
 
@@ -182,7 +199,7 @@ describe('formatBudgetResult', () => {
           'tacos'
         )
       ).toEqual(
-        'Warning: `tacos` failed the stat budget percent change limit. Expected to not increase by more than 10%, but increased by 163.6%'
+        'Warning: *tacos* failed the stat budget percent change limit. Expected to not increase by more than 10%, but increased by 163.6%'
       );
     });
   });