fix: oprava parsování TechTower po změně HTML struktury
CI / Generate TypeScript types (push) Failing after 13m15s
CI / Server unit tests (push) Has been skipped
CI / Build server (push) Has been skipped
CI / Build client (push) Has been skipped
CI / Playwright E2E tests (push) Has been skipped
CI / Build and push Docker image (push) Has been skipped
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Failing after 13m15s
CI / Server unit tests (push) Has been skipped
CI / Build server (push) Has been skipped
CI / Build client (push) Has been skipped
CI / Playwright E2E tests (push) Has been skipped
CI / Build and push Docker image (push) Has been skipped
CI / Notify (push) Successful in 2s
This commit is contained in:
4 files changed
+445
-57
No files matched your search
+96
-56
@@ -283,7 +283,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
let thirdTry = false;
|
||||
// První pokus - varianta "Obědy"
|
||||
let fonts = $('font.wsw-41');
|
||||
let font = undefined;
|
||||
let font: any = undefined;
|
||||
fonts.each((i, f) => {
|
||||
if ($(f).text().trim().startsWith('Obědy')) {
|
||||
font = f;
|
||||
@@ -309,12 +309,83 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
}
|
||||
})
|
||||
}
|
||||
// Pátý pokus - formát z 07/2026: hlavička "Jídelní lístek" je v libovolném <font>
|
||||
// (typicky wsw-31), jednotlivé položky jsou v samostatných <p> uvnitř vnořeného
|
||||
// <div><font class="wsw-31">, den a jeho první položka mohou být ve stejném <p>
|
||||
// oddělené <br>. Třída wsw-41 nyní obsahuje otevírací dobu, nikoli menu.
|
||||
let fifthTry = false;
|
||||
if (!font) {
|
||||
$('font').each((_, f) => {
|
||||
if (font) return;
|
||||
if ($(f).text().trim().startsWith('Jídelní lístek')) {
|
||||
font = f;
|
||||
fifthTry = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!font) {
|
||||
throw new Error('Chyba: nenalezen <font> pro obědy v HTML Techtower.');
|
||||
}
|
||||
|
||||
const result: Food[][] = [];
|
||||
|
||||
// Z textu jedné položky odvodí název, cenu a alergeny (sdíleno napříč formáty).
|
||||
const buildFoodItem = (text: string): Food => {
|
||||
let price = 'na\xA0váhu';
|
||||
let nameRaw = text.replace('•', '');
|
||||
if (text.toLowerCase().endsWith('kč')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2));
|
||||
price = `${split.slice(1)[0]}\xA0Kč`;
|
||||
nameRaw = split[0].replace('•', '');
|
||||
} else if (text.toLowerCase().endsWith(',-')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -1).join(' ')].concat(tmp.slice(-1));
|
||||
price = `${split.slice(1)[0].replace(',-', '')}\xA0Kč`;
|
||||
nameRaw = split[0].replace('•', '');
|
||||
}
|
||||
if (nameRaw.endsWith('–') || nameRaw.endsWith('—')) {
|
||||
nameRaw = nameRaw.slice(0, -1).trim();
|
||||
}
|
||||
const parsed = parseAllergens(nameRaw);
|
||||
return {
|
||||
amount: '-',
|
||||
name: parsed.cleanName,
|
||||
price,
|
||||
isSoup: isTextSoupName(parsed.cleanName),
|
||||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Pátý pokus (parsing): projdeme všechny <p> ve společném kontejneru, každý rozdělíme
|
||||
// podle <br> na jednotlivé řádky. Řádek s názvem dne přepne aktuální den, ostatní řádky
|
||||
// jsou položky. Zvládne tak i den a první položku ve stejném <p>.
|
||||
if (fifthTry) {
|
||||
let parsing = false;
|
||||
let currentDayIndex = 0;
|
||||
const container = $(font).parent().parent();
|
||||
container.find('p').each((_, p) => {
|
||||
const pHtml = $(p).html() ?? '';
|
||||
const lines = pHtml.split(/<br\s*\/?>/i)
|
||||
.map(part => $(`<span>${part}</span>`).text().trim())
|
||||
.filter(line => line.length > 0);
|
||||
for (const text of lines) {
|
||||
const lower = text.toLocaleLowerCase();
|
||||
if (DAYS_IN_WEEK.includes(lower)) {
|
||||
currentDayIndex = DAYS_IN_WEEK.indexOf(lower);
|
||||
parsing = true;
|
||||
result[currentDayIndex] ??= [];
|
||||
continue;
|
||||
}
|
||||
if (!parsing) {
|
||||
continue;
|
||||
}
|
||||
result[currentDayIndex].push(buildFoodItem(text));
|
||||
}
|
||||
});
|
||||
return finalizeTechTower(result, $(font).text().trim(), firstDayOfWeek);
|
||||
}
|
||||
|
||||
// Čtvrtý pokus (detekce): thirdTry našel <font>, ale nový formát má každý den v jednom <p>
|
||||
// s položkami oddělenými <br> místo separátních <p> pro každou položku
|
||||
const fourthTry = thirdTry && $(font).parent().siblings('p').toArray().some(el => {
|
||||
@@ -345,31 +416,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
for (const text of itemLines) {
|
||||
let price = 'na\xA0váhu';
|
||||
let nameRaw = text.replace('•', '');
|
||||
if (text.toLowerCase().endsWith('kč')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2));
|
||||
price = `${split.slice(1)[0]}\xA0Kč`;
|
||||
nameRaw = split[0].replace('•', '');
|
||||
} else if (text.toLowerCase().endsWith(',-')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -1).join(' ')].concat(tmp.slice(-1));
|
||||
price = `${split.slice(1)[0].replace(',-', '')}\xA0Kč`;
|
||||
nameRaw = split[0].replace('•', '');
|
||||
}
|
||||
if (nameRaw.endsWith('–') || nameRaw.endsWith('—')) {
|
||||
nameRaw = nameRaw.slice(0, -1).trim();
|
||||
}
|
||||
|
||||
const parsed = parseAllergens(nameRaw);
|
||||
result[dayIndex].push({
|
||||
amount: '-',
|
||||
name: parsed.cleanName,
|
||||
price,
|
||||
isSoup: isTextSoupName(parsed.cleanName),
|
||||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||||
});
|
||||
result[dayIndex].push(buildFoodItem(text));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -389,47 +436,40 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
// Prázdná řádka - bývá na zcela náhodných místech ¯\_(ツ)_/¯
|
||||
continue;
|
||||
}
|
||||
let price = 'na\xA0váhu';
|
||||
let nameRaw = text.replace('•', '');
|
||||
if (text.toLowerCase().endsWith('kč')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2));
|
||||
price = `${split.slice(1)[0]}\xA0Kč`
|
||||
nameRaw = split[0].replace('•', '');
|
||||
} else if (text.toLowerCase().endsWith(',-')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -1).join(' ')].concat(tmp.slice(-1));
|
||||
price = `${split.slice(1)[0].replace(',-', '')}\xA0Kč`
|
||||
nameRaw = split[0].replace('•', '');
|
||||
}
|
||||
if (nameRaw.endsWith('–')|| nameRaw.endsWith('—')) {
|
||||
nameRaw = nameRaw.slice(0, -1).trim();
|
||||
}
|
||||
|
||||
const parsed = parseAllergens(nameRaw);
|
||||
result[currentDayIndex] ??= [];
|
||||
result[currentDayIndex].push({
|
||||
amount: '-',
|
||||
name: parsed.cleanName,
|
||||
price,
|
||||
isSoup: isTextSoupName(parsed.cleanName),
|
||||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||||
})
|
||||
result[currentDayIndex].push(buildFoodItem(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validace, zda text hlavičky obsahuje datum odpovídající požadovanému týdnu
|
||||
const headerText = $(font).text().trim();
|
||||
const dateMatch = headerText.match(/(\d{1,2})\.(\d{1,2})\./);
|
||||
return finalizeTechTower(result, $(font).text().trim(), firstDayOfWeek);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ověří, že datum v hlavičce TechTower spadá do požadovaného týdne, jinak vyhodí StaleWeekError.
|
||||
*
|
||||
* Datum v hlavičce nemusí být přesně pondělí (např. je-li pondělí svátek, začíná hlavička
|
||||
* úterým), proto stačí, aby první nalezené datum padlo do libovolného dne požadovaného týdne.
|
||||
* Toleruje i mezery za tečkami (formát "7. 7.-10. 7.").
|
||||
*/
|
||||
const finalizeTechTower = (result: Food[][], headerText: string, firstDayOfWeek: Date): Food[][] => {
|
||||
const dateMatch = headerText.match(/(\d{1,2})\.\s*(\d{1,2})\./);
|
||||
if (dateMatch) {
|
||||
const foundDay = parseInt(dateMatch[1]);
|
||||
const foundMonth = parseInt(dateMatch[2]) - 1; // JS months are 0-based
|
||||
if (foundDay !== firstDayOfWeek.getDate() || foundMonth !== firstDayOfWeek.getMonth()) {
|
||||
let matchesWeek = false;
|
||||
for (let d = 0; d < 7; d++) {
|
||||
const day = new Date(firstDayOfWeek);
|
||||
day.setDate(firstDayOfWeek.getDate() + d);
|
||||
if (day.getDate() === foundDay && day.getMonth() === foundMonth) {
|
||||
matchesWeek = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchesWeek) {
|
||||
throw new StaleWeekError(result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user