Skip to content

Commit f969eb3

Browse files
comnam90claude
andauthored
fix(scraper): ignore <table class="Note"> admonitions when parsing regions (#99)
Scope row extraction in parseRegionTable to <table class="Blue_Table"> so adjacent admonition tables on the M365 source page no longer leak through as bogus regions. Closes #98. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9fd9eb9 commit f969eb3

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

scripts/scrape-veeam-regions.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,24 @@ async function fetchWithRetry(url, maxRetries = 3) {
7777
*/
7878
function parseRegionTable(html, serviceKey) {
7979
const regions = [];
80-
81-
// Extract table rows
80+
81+
// Veeam help-center pages wrap region data in <table class="Blue_Table">.
82+
// Other tables on the page (notably class="Note" admonitions on the M365 page)
83+
// must be ignored, or their <tr>s leak in as bogus regions — see #95, #96.
84+
const dataTablePattern = /<table[^>]*class="[^"]*\bBlue_Table\b[^"]*"[^>]*>([\s\S]*?)<\/table>/gi;
85+
const dataTables = [...html.matchAll(dataTablePattern)].map(m => m[1]);
86+
87+
if (dataTables.length === 0) {
88+
console.warn(` ⚠️ No <table class="Blue_Table"> found for ${serviceKey}; page structure may have changed`);
89+
return regions;
90+
}
91+
92+
const scopedHtml = dataTables.join('\n');
93+
8294
const tableRowPattern = /<tr[^>]*>(.*?)<\/tr>/gis;
8395
const cellPattern = /<t[dh][^>]*>(.*?)<\/t[dh]>/gis;
84-
85-
const rows = [...html.matchAll(tableRowPattern)];
96+
97+
const rows = [...scopedHtml.matchAll(tableRowPattern)];
8698

8799
for (let i = 0; i < rows.length; i++) {
88100
const rowHtml = rows[i][1];

scripts/test-scraper.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,47 @@ async function runTests() {
102102
assert(regions.some(r => r.regionName === 'Central US'), 'Should have Central US');
103103
});
104104

105+
// Regression: M365 page wraps a "note" admonition in <table class="Note">
106+
// immediately before the data table. Those rows must not leak through as regions.
107+
// See issues #95, #96.
108+
await test('parseRegionTable ignores <table class="Note"> admonitions', async () => {
109+
const sampleHtml = `
110+
<p>Intro paragraph.</p>
111+
<div><table border="0" cellspacing="0" cellpadding="0" class="Note">
112+
<tr style="vertical-align:top">
113+
<td><p class="T_NoteType"><span class="T_NoteType">note</span></p></td>
114+
</tr>
115+
<tr style="vertical-align:top">
116+
<td><p class="Notes"><span class="Notes">Express backup data is stored in the Microsoft-specific region based on your configuration.</span></p></td>
117+
</tr>
118+
</table></div>
119+
<div><table border="0" cellspacing="0" cellpadding="0" class="Blue_Table">
120+
<caption>Backup Storage Regions</caption>
121+
<tr><th><p>Global Region</p></th><th><p>Azure Region</p></th></tr>
122+
<tr><td rowspan="2"><p>AMER</p></td><td><p>East US</p></td></tr>
123+
<tr><td><p>West US</p></td></tr>
124+
</table></div>
125+
`;
126+
127+
const regions = parseRegionTable(sampleHtml, 'vdc_m365');
128+
129+
assertEqual(regions.length, 2, `Should parse exactly 2 regions, got ${regions.length}`);
130+
assert(regions.some(r => r.regionName === 'East US'), 'Should have East US');
131+
assert(regions.some(r => r.regionName === 'West US'), 'Should have West US');
132+
assert(!regions.some(r => r.regionName === 'note'), 'Must not parse "note" as a region');
133+
assert(!regions.some(r => /Express backup/.test(r.regionName)), 'Must not parse note body as a region');
134+
});
135+
136+
// Defensive: if the page structure changes and no Blue_Table is found, return [].
137+
await test('parseRegionTable returns empty when no Blue_Table is present', async () => {
138+
const sampleHtml = `
139+
<table class="Note"><tr><td>note</td></tr></table>
140+
<table class="SomeOtherClass"><tr><td>East US</td></tr></table>
141+
`;
142+
const regions = parseRegionTable(sampleHtml, 'vdc_m365');
143+
assertEqual(regions.length, 0, 'Should return no regions when no Blue_Table is present');
144+
});
145+
105146
// Test 3: findMatchingRegion function
106147
await test('findMatchingRegion finds regions by ID', async () => {
107148
const scrapedRegion = {

0 commit comments

Comments
 (0)