From a374a7d7bc27fa8f2603c024ac8e695cbe0cf0cb Mon Sep 17 00:00:00 2001 From: Surya Paolo Date: Fri, 16 May 2025 18:52:21 +0200 Subject: [PATCH] =?UTF-8?q?-=20estrazione=20dei=20dati=20del=20libro=20sul?= =?UTF-8?q?=20sito=20di=20Amazon.=20-=20possibilit=C3=A0=20di=20visualizza?= =?UTF-8?q?re=20i=20dati=20estratti=20e=20di=20aggiornare=20i=20dati,=20si?= =?UTF-8?q?a=20solo=20se=20vuoti,=20che=20sovrascrivere=20tutti=20i=20dati?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/server/models/product.js | 14 +- src/server/models/productInfo.js | 7 + src/server/modules/Macro.js | 2 +- src/server/modules/Scraping.js | 421 +++++++++++++++++++++++++----- src/server/router/admin_router.js | 21 +- src/server/tools/general.js | 10 + yarn.lock | 61 ++++- 8 files changed, 458 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index cbb3bf3..a27a412 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "bcryptjs": "^3.0.2", "bluebird": "^3.7.2", "body-parser": "^1.20.3", + "cheerio": "^1.0.0", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "country-codes-list": "^2.0.0", diff --git a/src/server/models/product.js b/src/server/models/product.js index 626ccee..f088173 100755 --- a/src/server/models/product.js +++ b/src/server/models/product.js @@ -238,6 +238,12 @@ const productSchema = new Schema({ date_updated: { type: Date, }, + scraped: { + type: Boolean, + }, + scraped_date: { + type: Date, + }, validaprod: { esito: { type: Number, @@ -422,6 +428,9 @@ module.exports.executeQueryPickup = async function (idapp, params) { return [...risexact, ...ris]; }; +module.exports.getProductByIsbn = function (idapp, isbn) { + return Product.findAllIdApp(idapp, null, null, null, isbn); +}; module.exports.getProductByCode = function (idapp, code) { return Product.findAllIdApp(idapp, code); }; @@ -454,7 +463,7 @@ module.exports.isLowQuantityInStockById = async function (id) { return false; }; -module.exports.findAllIdApp = async function (idapp, code, id, all) { +module.exports.findAllIdApp = async function (idapp, code, id, all, isbn) { let myfind = {}; let myqueryadd = {}; let query = []; @@ -478,6 +487,9 @@ module.exports.findAllIdApp = async function (idapp, code, id, all) { if (code) { myfind = { ...myfind, code }; } + if (isbn) { + myfind = { ...myfind, sbn }; + } if (id) { myqueryadd = { $addFields: { diff --git a/src/server/models/productInfo.js b/src/server/models/productInfo.js index 532b303..07dccb5 100755 --- a/src/server/models/productInfo.js +++ b/src/server/models/productInfo.js @@ -183,6 +183,13 @@ const productInfoSchema = new Schema({ sottotitolo: String, link_macro: String, + scraped: { + type: Boolean, + }, + scraped_date: { + type: Date, + }, + }); var productInfo = module.exports = mongoose.model('ProductInfo', productInfoSchema); diff --git a/src/server/modules/Macro.js b/src/server/modules/Macro.js index b850771..97f6138 100644 --- a/src/server/modules/Macro.js +++ b/src/server/modules/Macro.js @@ -587,7 +587,7 @@ class Macro { setta = false, importa = true; - let product = { ...productInput }; + let product = { ...productInput, deleted: false }; if (options.inputdaGM) diff --git a/src/server/modules/Scraping.js b/src/server/modules/Scraping.js index 7d7c0db..140ddcc 100644 --- a/src/server/modules/Scraping.js +++ b/src/server/modules/Scraping.js @@ -1,13 +1,18 @@ -import axios from 'axios'; -import cheerio from 'cheerio'; +const axios = require('axios'); +const cheerio = require('cheerio'); + +const Product = require('../models/product'); +const ProductInfo = require('../models/productInfo'); + +const tools = require('../tools/general'); class AmazonBookScraper { constructor() { this.baseUrl = 'https://www.amazon.it/dp/'; } - async fetchPage(isbn) { - const url = `${this.baseUrl}${isbn}`; + async fetchPageISBN10(isbn10) { + const url = `${this.baseUrl}${isbn10}`; try { const { data } = await axios.get(url, { headers: { @@ -20,106 +25,380 @@ class AmazonBookScraper { }); return data; } catch (err) { - console.error(`Errore fetching ISBN ${isbn}:`, err.message); + console.error(`Errore fetching ISBN ${isbn10}:`, err.message); + return null; + } + } + isbn13to10(isbn13) { + try { + if (!isbn13.startsWith('978') || isbn13.length !== 13) return null; + const core = isbn13.slice(3, 12); // i 9 numeri centrali + + let sum = 0; + for (let i = 0; i < 9; i++) { + sum += (10 - i) * parseInt(core[i], 10); + } + let check = 11 - (sum % 11); + if (check === 10) check = 'X'; + else if (check === 11) check = '0'; + else check = check.toString(); + return core + check; + } catch (err) { + console.error('Errore calcolando ISBN-10 da ISBN-13:', err.message); return null; } } - extractData(html) { + getTitleByProductInfo(productInfo) { + try { + return productInfo?.name.trim(); + } catch (e) { + return ''; + } + } + + async extractData(myproduct, html) { const $ = cheerio.load(html); + const productInfo = await ProductInfo.findOne({ _id: myproduct.idProductInfo }).lean(); + + const title_ondb = this.getTitleByProductInfo(productInfo); + // Titolo let title = $('#productTitle').text().trim() || null; + // Sottotitolo (Amazon lo mette nel titolo) - // Sottotitolo (Amazon spesso lo mette in #productSubtitle o nel titolo, proveremo) - let subtitle = $('#productSubtitle').text().trim() || null; + let dettagli = $('#productSubtitle').text().trim() || null; - // Numero pagine, formato, edizione - // Questi dati spesso sono nella tabella dettagli prodotto con id #detailBullets_feature_div o #productDetailsTable - // Proviamo a estrarre da #detailBullets_feature_div + const ris = this.extractSubtitle(title, title_ondb); - let pages = null; - let format = null; - let edition = null; + let sottotitolo = title_ondb ? ris?.subtitle : ''; - $('#detailBullets_feature_div li').each((i, el) => { - const label = $(el).find('span.a-text-bold').text().trim().toLowerCase(); - const value = $(el).find('span').last().text().trim(); + let titoloOriginale = ''; - if (label.includes('pagine') || label.includes('pagine stampate')) { - pages = value; - } else if (label.includes('formato')) { - format = value; - } else if (label.includes('edizione')) { - edition = value; - } - }); - - // fallback su #productDetailsTable (altro possibile layout) - if (!pages || !format || !edition) { - $('#productDetailsTable .content tr').each((i, el) => { - const label = $(el).find('th').text().trim().toLowerCase(); - const value = $(el).find('td').text().trim(); - - if (!pages && (label.includes('pagine') || label.includes('pagine stampate'))) { - pages = value; - } else if (!format && label.includes('formato')) { - format = value; - } else if (!edition && label.includes('edizione')) { - edition = value; - } - }); + if (ris?.titoloOriginale) { + // E' presente il vero titolo del Libro ! + titoloOriginale = ris.titoloOriginale; } - return { title, subtitle, pages, format, edition }; + if (sottotitolo && title_ondb) { + // significa che il titolo è dentro al sottotitolo, quindi prendi per buono quello nostro + title = title_ondb; + } + + let numpagine = null; + let misure = null; + let edizione = null; + let publisher = null; + let data_pubblicazione = null; + + numpagine = this.extractNumeroDiPagine($); + const dim = this.extractDimensions($); + misure = this.convertDimensionsToMisureMacro(dim); + const data_pubb = this.extractDataPubblicazione($); + data_pubblicazione = this.parseItalianDate(data_pubb); + + publisher = this.extractEditore($); + + if (data_pubb) { + edizione = this.extractMonthYear(data_pubb); + } + + return { + titolo: title, + ...(titoloOriginale ? { titoloOriginale } : {}), + ...(sottotitolo ? { sottotitolo } : {sottotitolo: ''}), + numpagine, + misure, + edizione, + data_pubblicazione, + editore: publisher, + }; } - async scrapeISBN(isbn) { - const html = await this.fetchPage(isbn); + async scrapeISBN(myproduct, isbn, options) { + const isbn10 = this.isbn13to10(isbn); + const html = await this.fetchPageISBN10(isbn10); if (!html) return null; - const data = this.extractData(html); + const data = await this.extractData(myproduct, html); + + if (!options?.update) return data; + + const arrvariazioni = myproduct.arrvariazioni || []; + let index = -1; + + if (arrvariazioni.length === 1) { + index = 0; + } else { + index = arrvariazioni.findIndex((v) => v.versione === shared_consts.PRODUCTTYPE.NUOVO); + if (index < 0) index = 0; + } + + const productInfo = {}; + let aggiornaDataPubb = false; + + let aggiornaPages = false; + let aggiornaMisure = false; + let aggiornaEdizione = false; + + if (index !== -1) { + const variante = arrvariazioni[index]; + + // Determina se aggiornare pagine + aggiornaPages = !options.aggiornasoloSeVuoti || !variante.pagine || variante.pagine === 0; + if (aggiornaPages) variante.pagine = Number(data.numpagine); + + // Determina se aggiornare misure + aggiornaMisure = !options.aggiornasoloSeVuoti || !variante.misure; + if (aggiornaMisure) variante.misure = data.misure; + + // Determina se aggiornare edizione + aggiornaEdizione = !options.aggiornasoloSeVuoti || !variante.edizione; + if (aggiornaEdizione) variante.edizione = data.edizione; + } + + // Determina se aggiornare data pubblicazione + const currentDatePub = productInfo.date_pub; + aggiornaDataPubb = !options.aggiornasoloSeVuoti || !tools.isDateValid(currentDatePub); + if (aggiornaDataPubb && data.data_pubblicazione) { + productInfo.date_pub = new Date(data.data_pubblicazione); + } + + // Aggiorna arrvariazioni se pagine o misure sono cambiati + const aggiornadati = aggiornaPages || aggiornaMisure || aggiornaEdizione; + + if (aggiornadati) { + await Product.findOneAndUpdate( + { _id: myproduct._id }, + { $set: { arrvariazioni, scraped: true, scraped_date: new Date() } }, + { upsert: true, new: true, includeResultMetadata: true } + ); + } + + // Aggiorna productInfo se contiene dati + if (!tools.isObjectEmpty(productInfo)) { + const risupdate = await ProductInfo.findOneAndUpdate( + { _id: myproduct.idProductInfo }, + { $set: productInfo, scraped: true, scraped_date: new Date() }, + { new: true, upsert: true, returnOriginal: false } + ).lean(); + console.log('risupdate', risupdate) ; + } + return data; } - async scrapeMultiple(isbnList) { + async scrapeMultiple(isbnList, options) { const results = []; for (const isbn of isbnList) { console.log(`Scraping ISBN: ${isbn}`); - const data = await this.scrapeISBN(isbn); + const myproduct = null; + /// myproduct... + const data = await this.scrapeISBN(myproduct, isbn, options); results.push({ isbn, ...data }); // Per evitare blocchi, metti una pausa (es. 2 secondi) await new Promise((r) => setTimeout(r, 2000)); } return results; } -} -export async function ScraperDataAmazon(idapp, options) { - const scraper = new AmazonBookScraper(); - const isbn = options.isbn; + generateHtmlTableFromObject(obj) { + if (!obj || typeof obj !== 'object') return ''; - try { - const data = await scraper.scrapeISBN(isbn); - console.log(data); - return data; - } catch (e) { - console.error(e); - return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' }); + let html = ''; + html += ''; + + for (const [key, value] of Object.entries(obj)) { + // Se il valore è un oggetto o array, lo converto in JSON stringa + const displayValue = value && typeof value === 'object' ? JSON.stringify(value) : String(value); + + html += ``; + } + + html += '
ChiaveValore
${key}${displayValue}
'; + return html; + } + + async ScraperDataAmazon(idapp, options) { + const scraper = new AmazonBookScraper(); + const isbn = options.isbn; + + try { + const myproduct = await Product.getProductByIsbn(isbn); + const data = await scraper.scrapeISBN(myproduct, isbn, options); + + // let html = this.generateHtmlTableFromObject(data); + + console.log(data); + return res.status(200).send({ code: server_constants.RIS_CODE_OK, data, html }); + } catch (e) { + console.error(e); + return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' }); + } + } + + async ScraperMultipleDataAmazon(idapp, options) { + const scraper = new AmazonBookScraper(); + const isbnList = ['8850224248']; // metti i tuoi ISBN qui + + try { + const books = await scraper.scrapeMultiple(isbnList); + console.log(books); + } catch (e) { + console.error(e); + return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' }); + } + } + + extractDataPubblicazione($) { + // Seleziona il div con id specifico per la data di pubblicazione + const publicationDate = $('#rpi-attribute-book_details-publication_date .rpi-attribute-value span').text().trim(); + + // Se non trova la data, ritorna null + return publicationDate || null; + } + + extractNumeroDiPagine($) { + // Seleziona il div con id specifico per pagine + const pagesText = $('#rpi-attribute-book_details-fiona_pages .rpi-attribute-value span').text().trim(); + + // pagesText dovrebbe essere tipo "184 pagine" + if (!pagesText) return null; + + // Estrai solo il numero (facoltativo) + const match = pagesText.match(/(\d+)/); + return match ? match[1] : pagesText; // ritorna solo il numero o il testo intero + } + + extractDimensions($) { + // Seleziona il div con id specifico per dimensioni + const dimText = $('#rpi-attribute-book_details-dimensions .rpi-attribute-value span').text().trim(); + + // Se non trova niente ritorna null + return dimText || null; + } + + convertDimensionsToMisureMacro(dimString) { + if (!dimString) return null; + + // Estrai tutti i numeri (compresi decimali) + const numbers = dimString.match(/[\d.]+/g); + if (!numbers || numbers.length < 2) return null; + + // Converti in numeri float e ordina decrescente + const sortedNums = numbers + .map((num) => parseFloat(num)) + .filter((n) => !isNaN(n)) + .sort((a, b) => b - a); + + if (sortedNums.length < 2) return null; + + // Prendi i due più grandi + const [first, second] = sortedNums; + + return `cm ${first}x${second}`; + } + + parseItalianDate(dateStr) { + if (!dateStr) return null; + + // Mappa mesi in italiano a numeri (0-based per Date) + const months = { + gennaio: 0, + febbraio: 1, + marzo: 2, + aprile: 3, + maggio: 4, + giugno: 5, + luglio: 6, + agosto: 7, + settembre: 8, + ottobre: 9, + novembre: 10, + dicembre: 11, + }; + + // Divido la stringa (es. "14 maggio 2025") + const parts = dateStr.toLowerCase().split(' '); + if (parts.length !== 3) return null; + + const day = parseInt(parts[0], 10); + const month = months[parts[1]]; + const year = parseInt(parts[2], 10); + + if (isNaN(day) || month === undefined || isNaN(year)) return null; + + return new Date(year, month, day); + } + extractSubtitle(fullTitle, baseTitle) { + if (!fullTitle || !baseTitle) return null; + + let mybaseTitle = ''; + let numCharRemoved = 0; + let titoloOriginale = ''; + + // Se il fullTitle non contiene il baseTitle, ritorna null + const coniniziali = fullTitle.trim().toLowerCase().startsWith(baseTitle.trim().toLowerCase()); + if (coniniziali) { + mybaseTitle = baseTitle; + } + + let senzainiziali = false; + + if (!coniniziali) { + // torna la posizione in cui l'ho trovato + const posizione = fullTitle.toLowerCase().indexOf(baseTitle.trim().toLowerCase()); + if (posizione < 0 || posizione > 3) { + return null; + } + // torna il nome del titolo, compreso degli articoli, partendo da zero, fino a posizione + baseTitle + titoloOriginale = fullTitle.substring(0, posizione + baseTitle.length); + + numCharRemoved = posizione; + senzainiziali = true; + } + + if (!coniniziali && !senzainiziali) { + return null; + } + + // Rimuovi il baseTitle dall'inizio + let remainder = fullTitle.slice(baseTitle.length + numCharRemoved).trim(); + + // Se la rimanenza inizia con ":" o "-" o ".", rimuovila + if (remainder.startsWith(':') || remainder.startsWith('-') || remainder.startsWith('.')) { + remainder = remainder.slice(1).trim(); + } + + // Se resta una stringa non vuota, è il sottotitolo + return { subtitle: remainder.length > 0 ? remainder : null, titoloOriginale }; + } + + extractMonthYear(dateStr) { + if (!dateStr) return null; + + // Divide la stringa in parole + const parts = dateStr.trim().split(' '); + + // Se ha almeno 3 parti (giorno, mese, anno) + if (parts.length >= 3) { + // Restituisce mese + anno + return parts + .slice(1) + .map((part, idx) => (idx === 0 ? part[0].toUpperCase() + part.slice(1) : part)) + .join(' '); + } + + return null; + } + + extractEditore($) { + // Seleziona il testo dentro il div id rpi-attribute-book_details-publisher + const publisher = $('#rpi-attribute-book_details-publisher .rpi-attribute-value span').text().trim(); + + return publisher || null; } } -export async function ScraperMultipleDataAmazon(idapp, options) { - const scraper = new AmazonBookScraper(); - const isbnList = ['8850224248']; // metti i tuoi ISBN qui - - try { - const books = await scraper.scrapeMultiple(isbnList); - console.log(books); - } catch (e) { - console.error(e); - return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' }); - } -} - -export default AmazonBookScraper; +module.exports = AmazonBookScraper; diff --git a/src/server/router/admin_router.js b/src/server/router/admin_router.js index f62d813..244096a 100755 --- a/src/server/router/admin_router.js +++ b/src/server/router/admin_router.js @@ -27,7 +27,7 @@ const Gasordine = require('../models/gasordine'); const { User } = require('../models/user'); -const AmazonBookScraper = require('../modules/Scraping'); +const AmazonBookScraper = require('../modules/scraping'); const { Catalog } = require('../models/catalog'); const { RaccoltaCataloghi } = require('../models/raccoltacataloghi'); @@ -2363,13 +2363,24 @@ router.post('/cloudflare', authenticate, async (req, res) => { }); router.post('/scraper', authenticate, async (req, res) => { + idapp = req.body.idapp; + idProduct = req.body.product_id; + options = req.body.options; + const scraper = new AmazonBookScraper(); - const isbn = req.data.options.isbn; + + const product = await Product.getProductById(idProduct); + let isbn = ''; + if (product) { + isbn = product.isbn; + } try { - const data = await scraper.scrapeISBN(isbn); - console.log(data); - + let data = null; + if (isbn) { + data = await scraper.scrapeISBN(product, isbn, options); + console.log(data); + } return res.send(data); } catch (e) { console.error(e); diff --git a/src/server/tools/general.js b/src/server/tools/general.js index da1aa69..e58a231 100755 --- a/src/server/tools/general.js +++ b/src/server/tools/general.js @@ -6299,5 +6299,15 @@ module.exports = { return filename; }, + isDateValid(mydate) { + try { + return ( + mydate instanceof Date && isFinite(mydate.getTime()) && mydate.toISOString().split('T')[0] !== '1970-01-01' + ); + } catch { + return false; + } + }, + }; diff --git a/yarn.lock b/yarn.lock index 8b6ffca..627fe02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2102,6 +2102,23 @@ cheerio@^0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" +cheerio@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" + integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.1.0" + encoding-sniffer "^0.2.0" + htmlparser2 "^9.1.0" + parse5 "^7.1.2" + parse5-htmlparser2-tree-adapter "^7.0.0" + parse5-parser-stream "^7.1.2" + undici "^6.19.5" + whatwg-mimetype "^4.0.0" + chokidar@^3.5.1, chokidar@^3.5.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -2874,7 +2891,7 @@ domutils@^2.4.2: domelementtype "^2.2.0" domhandler "^4.2.0" -domutils@^3.0.1: +domutils@^3.0.1, domutils@^3.1.0: version "3.2.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== @@ -3011,6 +3028,14 @@ encoding-japanese@2.2.0: resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz#0ef2d2351250547f432a2dd155453555c16deb59" integrity sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A== +encoding-sniffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5" + integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== + dependencies: + iconv-lite "^0.6.3" + whatwg-encoding "^3.1.1" + end-of-stream@^1.1.0, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -3064,6 +3089,11 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.0.tgz#09c9e29cb79b0a6459a9b9db9efb418ac5bb8e51" + integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -4175,6 +4205,16 @@ htmlparser2@^8.0.0, htmlparser2@^8.0.1, htmlparser2@^8.0.2: domutils "^3.0.1" entities "^4.4.0" +htmlparser2@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" + integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -6662,6 +6702,13 @@ parse5-htmlparser2-tree-adapter@^7.0.0: domhandler "^5.0.3" parse5 "^7.0.0" +parse5-parser-stream@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1" + integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + dependencies: + parse5 "^7.0.0" + parse5@^7.0.0, parse5@^7.2.1: version "7.2.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.2.1.tgz#8928f55915e6125f430cc44309765bf17556a33a" @@ -6669,6 +6716,13 @@ parse5@^7.0.0, parse5@^7.2.1: dependencies: entities "^4.5.0" +parse5@^7.1.2: + version "7.3.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + parseley@^0.12.0: version "0.12.1" resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef" @@ -8632,6 +8686,11 @@ undici-types@~6.20.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== +undici@^6.19.5: + version "6.21.3" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a" + integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw== + unicode-emoji-modifier-base@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459"