Merge pull request #275 from getmaxun/iframe

feat: support `iframe` extraction
This commit is contained in:
Rohit
2025-01-05 00:34:48 +05:30
committed by GitHub
6 changed files with 1416 additions and 629 deletions

View File

@@ -189,111 +189,145 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
* @returns {Array.<Object.<string, string>>} * @returns {Array.<Object.<string, string>>}
*/ */
window.scrapeSchema = function(lists) { window.scrapeSchema = function(lists) {
// Utility functions remain the same
function omap(object, f, kf = (x) => x) { function omap(object, f, kf = (x) => x) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(object) Object.entries(object)
.map(([k, v]) => [kf(k), f(v)]), .map(([k, v]) => [kf(k), f(v)]),
); );
} }
function ofilter(object, f) { function ofilter(object, f) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(object) Object.entries(object)
.filter(([k, v]) => f(k, v)), .filter(([k, v]) => f(k, v)),
); );
} }
function findAllElements(config) { function findAllElements(config) {
if (!config.shadow || !config.selector.includes('>>')) { // Regular DOM query if no special delimiters
return Array.from(document.querySelectorAll(config.selector)); if (!config.selector.includes('>>') && !config.selector.includes(':>>')) {
} return Array.from(document.querySelectorAll(config.selector));
}
// For shadow DOM, we'll get all possible combinations // Split by both types of delimiters
const parts = config.selector.split('>>').map(s => s.trim()); const parts = config.selector.split(/(?:>>|:>>)/).map(s => s.trim());
let currentElements = [document]; const delimiters = config.selector.match(/(?:>>|:>>)/g) || [];
let currentElements = [document];
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
const part = parts[i]; const part = parts[i];
const nextElements = []; const nextElements = [];
const isLast = i === parts.length - 1;
const delimiter = delimiters[i] || '';
const isIframeTraversal = delimiter === ':>>';
for (const element of currentElements) { for (const element of currentElements) {
let targets; try {
if (i === 0) { let targets;
// First selector is queried from document
targets = Array.from(element.querySelectorAll(part))
.filter(el => {
// Only include elements that either:
// 1. Have an open shadow root
// 2. Don't need shadow root (last part of selector)
if (i === parts.length - 1) return true;
const shadowRoot = el.shadowRoot;
return shadowRoot && shadowRoot.mode === 'open';
});
} else {
// For subsequent selectors, only use elements with open shadow roots
const shadowRoot = element.shadowRoot;
if (!shadowRoot || shadowRoot.mode !== 'open') continue;
targets = Array.from(shadowRoot.querySelectorAll(part)); if (i === 0) {
// First selector is queried from main document
targets = Array.from(element.querySelectorAll(part))
.filter(el => {
if (isLast) return true;
// For iframe traversal, only include iframes
if (isIframeTraversal) return el.tagName === 'IFRAME';
// For shadow DOM traversal, only include elements with shadow root
return el.shadowRoot && el.shadowRoot.mode === 'open';
});
} else {
if (isIframeTraversal) {
// Handle iframe traversal
const iframeDocument = element.contentDocument || element.contentWindow?.document;
if (!iframeDocument) continue;
targets = Array.from(iframeDocument.querySelectorAll(part));
if (!isLast) {
targets = targets.filter(el => el.tagName === 'IFRAME');
} }
nextElements.push(...targets); } else {
// Handle shadow DOM traversal
const shadowRoot = element.shadowRoot;
if (!shadowRoot || shadowRoot.mode !== 'open') continue;
targets = Array.from(shadowRoot.querySelectorAll(part));
if (!isLast) {
targets = targets.filter(el => el.shadowRoot && el.shadowRoot.mode === 'open');
}
}
} }
if (nextElements.length === 0) return []; nextElements.push(...targets);
currentElements = nextElements; } catch (error) {
console.warn('Cannot access content:', error);
continue;
}
} }
return currentElements; if (nextElements.length === 0) return [];
currentElements = nextElements;
}
return currentElements;
} }
// Modified to handle iframe context for URL resolution
function getElementValue(element, attribute) { function getElementValue(element, attribute) {
if (!element) return null; if (!element) return null;
switch (attribute) { // Get the base URL for resolving relative URLs
case 'href': { const baseURL = element.ownerDocument?.location?.href || window.location.origin;
const relativeHref = element.getAttribute('href');
return relativeHref ? new URL(relativeHref, window.location.origin).href : null; switch (attribute) {
} case 'href': {
case 'src': { const relativeHref = element.getAttribute('href');
const relativeSrc = element.getAttribute('src'); return relativeHref ? new URL(relativeHref, baseURL).href : null;
return relativeSrc ? new URL(relativeSrc, window.location.origin).href : null;
}
case 'innerText':
return element.innerText?.trim();
case 'textContent':
return element.textContent?.trim();
default:
return element.getAttribute(attribute) || element.innerText?.trim();
} }
case 'src': {
const relativeSrc = element.getAttribute('src');
return relativeSrc ? new URL(relativeSrc, baseURL).href : null;
}
case 'innerText':
return element.innerText?.trim();
case 'textContent':
return element.textContent?.trim();
default:
return element.getAttribute(attribute) || element.innerText?.trim();
}
} }
// Get the seed key based on the maximum number of elements found // Rest of the functions remain largely the same
function getSeedKey(listObj) { function getSeedKey(listObj) {
const maxLength = Math.max(...Object.values( const maxLength = Math.max(...Object.values(
omap(listObj, (x) => findAllElements(x).length) omap(listObj, (x) => findAllElements(x).length)
)); ));
return Object.keys( return Object.keys(
ofilter(listObj, (_, v) => findAllElements(v).length === maxLength) ofilter(listObj, (_, v) => findAllElements(v).length === maxLength)
)[0]; )[0];
} }
// Find minimal bounding elements // Find minimal bounding elements
function getMBEs(elements) { function getMBEs(elements) {
return elements.map((element) => { return elements.map((element) => {
let candidate = element; let candidate = element;
const isUniqueChild = (e) => elements const isUniqueChild = (e) => elements
.filter((elem) => e.parentNode?.contains(elem)) .filter((elem) => {
.length === 1; // Handle both iframe and shadow DOM boundaries
const sameContext = elem.getRootNode() === e.getRootNode() &&
elem.ownerDocument === e.ownerDocument;
return sameContext && e.parentNode?.contains(elem);
})
.length === 1;
while (candidate && isUniqueChild(candidate)) { while (candidate && isUniqueChild(candidate)) {
candidate = candidate.parentNode; candidate = candidate.parentNode;
} }
return candidate; return candidate;
}); });
} }
// First try the MBE approach
const seedName = getSeedKey(lists); const seedName = getSeedKey(lists);
const seedElements = findAllElements(lists[seedName]); const seedElements = findAllElements(lists[seedName]);
const MBEs = getMBEs(seedElements); const MBEs = getMBEs(seedElements);
@@ -347,164 +381,210 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
* @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list * @returns {Array.<Array.<Object>>} Array of arrays of scraped items, one sub-array per list
*/ */
window.scrapeList = async function ({ listSelector, fields, limit = 10 }) { window.scrapeList = async function ({ listSelector, fields, limit = 10 }) {
// Shadow DOM query functions remain unchanged // Enhanced query function to handle both iframe and shadow DOM
const queryShadowDOM = (rootElement, selector) => { const queryElement = (rootElement, selector) => {
if (!selector.includes('>>')) { if (!selector.includes('>>') && !selector.includes(':>>')) {
return rootElement.querySelector(selector); return rootElement.querySelector(selector);
} }
const parts = selector.split('>>').map(part => part.trim()); const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
let currentElement = rootElement; let currentElement = rootElement;
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (!currentElement) return null; if (!currentElement) return null;
if (!currentElement.querySelector && !currentElement.shadowRoot) { // Handle iframe traversal
currentElement = document.querySelector(parts[i]); if (currentElement.tagName === 'IFRAME') {
continue; try {
} const iframeDoc = currentElement.contentDocument || currentElement.contentWindow.document;
currentElement = iframeDoc.querySelector(parts[i]);
continue;
} catch (e) {
console.warn('Cannot access iframe content:', e);
return null;
}
}
let nextElement = currentElement.querySelector(parts[i]); // Try regular DOM first
let nextElement = currentElement.querySelector(parts[i]);
if (!nextElement && currentElement.shadowRoot) { // Try shadow DOM if not found
nextElement = currentElement.shadowRoot.querySelector(parts[i]); if (!nextElement && currentElement.shadowRoot) {
} nextElement = currentElement.shadowRoot.querySelector(parts[i]);
}
if (!nextElement) { // Check children's shadow roots if still not found
const allChildren = Array.from(currentElement.children || []); if (!nextElement) {
for (const child of allChildren) { const children = Array.from(currentElement.children || []);
if (child.shadowRoot) { for (const child of children) {
nextElement = child.shadowRoot.querySelector(parts[i]); if (child.shadowRoot) {
if (nextElement) break; nextElement = child.shadowRoot.querySelector(parts[i]);
} if (nextElement) break;
} }
} }
}
currentElement = nextElement; currentElement = nextElement;
} }
return currentElement; return currentElement;
}; };
const queryShadowDOMAll = (rootElement, selector) => { // Enhanced query all function for both contexts
if (!selector.includes('>>')) { const queryElementAll = (rootElement, selector) => {
if (!selector.includes('>>') && !selector.includes(':>>')) {
return rootElement.querySelectorAll(selector); return rootElement.querySelectorAll(selector);
} }
const parts = selector.split('>>').map(part => part.trim()); const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
let currentElements = [rootElement]; let currentElements = [rootElement];
for (const part of parts) { for (const part of parts) {
const nextElements = []; const nextElements = [];
for (const element of currentElements) { for (const element of currentElements) {
if (element.querySelectorAll) { // Handle iframe traversal
nextElements.push(...element.querySelectorAll(part)); if (element.tagName === 'IFRAME') {
} try {
const iframeDoc = element.contentDocument || element.contentWindow.document;
nextElements.push(...iframeDoc.querySelectorAll(part));
} catch (e) {
console.warn('Cannot access iframe content:', e);
continue;
}
} else {
// Regular DOM elements
if (element.querySelectorAll) {
nextElements.push(...element.querySelectorAll(part));
}
if (element.shadowRoot) { // Shadow DOM elements
nextElements.push(...element.shadowRoot.querySelectorAll(part)); if (element.shadowRoot) {
} nextElements.push(...element.shadowRoot.querySelectorAll(part));
}
const children = Array.from(element.children || []); // Check children's shadow roots
for (const child of children) { const children = Array.from(element.children || []);
if (child.shadowRoot) { for (const child of children) {
nextElements.push(...child.shadowRoot.querySelectorAll(part)); if (child.shadowRoot) {
} nextElements.push(...child.shadowRoot.querySelectorAll(part));
} }
} }
}
}
currentElements = nextElements; currentElements = nextElements;
} }
return currentElements; return currentElements;
}; };
// Enhanced table processing helper functions with shadow DOM support // Enhanced value extraction with context awareness
function extractValue(element, attribute) { function extractValue(element, attribute) {
if (!element) return null; if (!element) return null;
// Check for shadow root first // Get context-aware base URL
if (element.shadowRoot) { const baseURL = element.ownerDocument?.location?.href || window.location.origin;
const shadowContent = element.shadowRoot.textContent;
if (shadowContent && shadowContent.trim()) {
return shadowContent.trim();
}
}
if (attribute === 'innerText') { // Check shadow root first
return element.innerText.trim(); if (element.shadowRoot) {
} else if (attribute === 'innerHTML') { const shadowContent = element.shadowRoot.textContent;
return element.innerHTML.trim(); if (shadowContent?.trim()) {
} else if (attribute === 'src' || attribute === 'href') { return shadowContent.trim();
const attrValue = element.getAttribute(attribute); }
return attrValue ? new URL(attrValue, window.location.origin).href : null; }
}
return element.getAttribute(attribute); if (attribute === 'innerText') {
return element.innerText.trim();
} else if (attribute === 'innerHTML') {
return element.innerHTML.trim();
} else if (attribute === 'src' || attribute === 'href') {
const attrValue = element.getAttribute(attribute);
return attrValue ? new URL(attrValue, baseURL).href : null;
}
return element.getAttribute(attribute);
} }
// Enhanced table ancestor finding with context support
function findTableAncestor(element) { function findTableAncestor(element) {
let currentElement = element; let currentElement = element;
const MAX_DEPTH = 5; const MAX_DEPTH = 5;
let depth = 0; let depth = 0;
while (currentElement && depth < MAX_DEPTH) { while (currentElement && depth < MAX_DEPTH) {
// Check if current element is in shadow DOM // Handle shadow DOM
if (currentElement.getRootNode() instanceof ShadowRoot) { if (currentElement.getRootNode() instanceof ShadowRoot) {
currentElement = currentElement.getRootNode().host; currentElement = currentElement.getRootNode().host;
continue; continue;
} }
if (currentElement.tagName === 'TD') { if (currentElement.tagName === 'TD') {
return { type: 'TD', element: currentElement }; return { type: 'TD', element: currentElement };
} else if (currentElement.tagName === 'TR') { } else if (currentElement.tagName === 'TR') {
return { type: 'TR', element: currentElement }; return { type: 'TR', element: currentElement };
} }
currentElement = currentElement.parentElement;
depth++; // Handle iframe crossing
} if (currentElement.tagName === 'IFRAME') {
return null; try {
currentElement = currentElement.contentDocument.body;
} catch (e) {
return null;
}
} else {
currentElement = currentElement.parentElement;
}
depth++;
}
return null;
} }
// Helper function to get cell index
function getCellIndex(td) { function getCellIndex(td) {
let index = 0; if (td.getRootNode() instanceof ShadowRoot) {
let sibling = td; const shadowRoot = td.getRootNode();
const allCells = Array.from(shadowRoot.querySelectorAll('td'));
return allCells.indexOf(td);
}
// Handle shadow DOM case let index = 0;
if (td.getRootNode() instanceof ShadowRoot) { let sibling = td;
const shadowRoot = td.getRootNode(); while (sibling = sibling.previousElementSibling) {
const allCells = Array.from(shadowRoot.querySelectorAll('td')); index++;
return allCells.indexOf(td); }
} return index;
while (sibling = sibling.previousElementSibling) {
index++;
}
return index;
} }
// Helper function to check for TH elements
function hasThElement(row, tableFields) { function hasThElement(row, tableFields) {
for (const [label, { selector }] of Object.entries(tableFields)) { for (const [_, { selector }] of Object.entries(tableFields)) {
const element = queryShadowDOM(row, selector); const element = queryElement(row, selector);
if (element) { if (element) {
let current = element; let current = element;
while (current && current !== row) { while (current && current !== row) {
// Check if we're in shadow DOM if (current.getRootNode() instanceof ShadowRoot) {
if (current.getRootNode() instanceof ShadowRoot) { current = current.getRootNode().host;
current = current.getRootNode().host; continue;
continue; }
}
if (current.tagName === 'TH') { if (current.tagName === 'TH') return true;
return true;
} if (current.tagName === 'IFRAME') {
current = current.parentElement; try {
} current = current.contentDocument.body;
} } catch (e) {
} break;
return false; }
} else {
current = current.parentElement;
}
}
}
}
return false;
} }
// Helper function to filter rows
function filterRowsBasedOnTag(rows, tableFields) { function filterRowsBasedOnTag(rows, tableFields) {
for (const row of rows) { for (const row of rows) {
if (hasThElement(row, tableFields)) { if (hasThElement(row, tableFields)) {
@@ -520,7 +600,7 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
}); });
} }
// Class similarity functions remain unchanged // Class similarity comparison functions
function calculateClassSimilarity(classList1, classList2) { function calculateClassSimilarity(classList1, classList2) {
const set1 = new Set(classList1); const set1 = new Set(classList1);
const set2 = new Set(classList2); const set2 = new Set(classList2);
@@ -529,189 +609,237 @@ function scrapableHeuristics(maxCountPerPage = 50, minArea = 20000, scrolls = 3,
return intersection.size / union.size; return intersection.size / union.size;
} }
// Enhanced similar elements finding with context support
function findSimilarElements(baseElement, similarityThreshold = 0.7) { function findSimilarElements(baseElement, similarityThreshold = 0.7) {
const baseClasses = Array.from(baseElement.classList); const baseClasses = Array.from(baseElement.classList);
if (baseClasses.length === 0) return []; if (baseClasses.length === 0) return [];
const potentialElements = document.getElementsByTagName(baseElement.tagName);
return Array.from(potentialElements).filter(element => { const allElements = [];
if (element === baseElement) return false;
const similarity = calculateClassSimilarity( // Get elements from main document
baseClasses, allElements.push(...document.getElementsByTagName(baseElement.tagName));
Array.from(element.classList)
); // Get elements from shadow DOM
return similarity >= similarityThreshold; if (baseElement.getRootNode() instanceof ShadowRoot) {
}); const shadowHost = baseElement.getRootNode().host;
allElements.push(...shadowHost.getElementsByTagName(baseElement.tagName));
}
// Get elements from iframes
const iframes = document.getElementsByTagName('iframe');
for (const iframe of iframes) {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
allElements.push(...iframeDoc.getElementsByTagName(baseElement.tagName));
} catch (e) {
console.warn('Cannot access iframe content:', e);
}
}
return allElements.filter(element => {
if (element === baseElement) return false;
const similarity = calculateClassSimilarity(
baseClasses,
Array.from(element.classList)
);
return similarity >= similarityThreshold;
});
} }
// Main scraping logic with shadow DOM support // Main scraping logic with context support
let containers = queryShadowDOMAll(document, listSelector); let containers = queryElementAll(document, listSelector);
containers = Array.from(containers); containers = Array.from(containers);
if (containers.length === 0) return []; if (containers.length === 0) return [];
if (limit > 1 && containers.length === 1) { if (limit > 1 && containers.length === 1) {
const baseContainer = containers[0]; const baseContainer = containers[0];
const similarContainers = findSimilarElements(baseContainer); const similarContainers = findSimilarElements(baseContainer);
if (similarContainers.length > 0) { if (similarContainers.length > 0) {
const newContainers = similarContainers.filter(container => const newContainers = similarContainers.filter(container =>
!container.matches(listSelector) !container.matches(listSelector)
); );
containers = [...containers, ...newContainers]; containers = [...containers, ...newContainers];
} }
} }
const containerFields = containers.map(() => ({ const containerFields = containers.map(() => ({
tableFields: {}, tableFields: {},
nonTableFields: {} nonTableFields: {}
})); }));
// Classify fields // Classify fields
containers.forEach((container, containerIndex) => { containers.forEach((container, containerIndex) => {
for (const [label, field] of Object.entries(fields)) { for (const [label, field] of Object.entries(fields)) {
const sampleElement = queryShadowDOM(container, field.selector); const sampleElement = queryElement(container, field.selector);
if (sampleElement) { if (sampleElement) {
const ancestor = findTableAncestor(sampleElement); const ancestor = findTableAncestor(sampleElement);
if (ancestor) { if (ancestor) {
containerFields[containerIndex].tableFields[label] = { containerFields[containerIndex].tableFields[label] = {
...field, ...field,
tableContext: ancestor.type, tableContext: ancestor.type,
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1 cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
}; };
} else {
containerFields[containerIndex].nonTableFields[label] = field;
}
} else { } else {
containerFields[containerIndex].nonTableFields[label] = field; containerFields[containerIndex].nonTableFields[label] = field;
} }
} else {
containerFields[containerIndex].nonTableFields[label] = field;
} }
}
}); });
const tableData = []; const tableData = [];
const nonTableData = []; const nonTableData = [];
// Process table data with shadow DOM support // Process table data with both iframe and shadow DOM support
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) { for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
const container = containers[containerIndex]; const container = containers[containerIndex];
const { tableFields } = containerFields[containerIndex]; const { tableFields } = containerFields[containerIndex];
if (Object.keys(tableFields).length > 0) { if (Object.keys(tableFields).length > 0) {
const firstField = Object.values(tableFields)[0]; const firstField = Object.values(tableFields)[0];
const firstElement = queryShadowDOM(container, firstField.selector); const firstElement = queryElement(container, firstField.selector);
let tableContext = firstElement; let tableContext = firstElement;
// Find table context including shadow DOM // Find table context including both iframe and shadow DOM
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) { while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
if (tableContext.getRootNode() instanceof ShadowRoot) { if (tableContext.getRootNode() instanceof ShadowRoot) {
tableContext = tableContext.getRootNode().host; tableContext = tableContext.getRootNode().host;
} else { continue;
tableContext = tableContext.parentElement; }
}
}
if (tableContext) { if (tableContext.tagName === 'IFRAME') {
// Get rows from both regular DOM and shadow DOM try {
const rows = []; tableContext = tableContext.contentDocument.body;
if (tableContext.shadowRoot) { } catch (e) {
rows.push(...tableContext.shadowRoot.getElementsByTagName('TR')); break;
} }
rows.push(...tableContext.getElementsByTagName('TR')); } else {
tableContext = tableContext.parentElement;
}
}
const processedRows = filterRowsBasedOnTag(rows, tableFields); if (tableContext) {
// Get rows from all contexts
const rows = [];
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) { // Get rows from regular DOM
const record = {}; rows.push(...tableContext.getElementsByTagName('TR'));
const currentRow = processedRows[rowIndex];
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) { // Get rows from shadow DOM
let element = null; if (tableContext.shadowRoot) {
rows.push(...tableContext.shadowRoot.getElementsByTagName('TR'));
}
if (cellIndex >= 0) { // Get rows from iframes
let td = currentRow.children[cellIndex]; if (tableContext.tagName === 'IFRAME') {
try {
const iframeDoc = tableContext.contentDocument || tableContext.contentWindow.document;
rows.push(...iframeDoc.getElementsByTagName('TR'));
} catch (e) {
console.warn('Cannot access iframe rows:', e);
}
}
// Check shadow DOM for td const processedRows = filterRowsBasedOnTag(rows, tableFields);
if (!td && currentRow.shadowRoot) {
const shadowCells = currentRow.shadowRoot.children;
if (shadowCells && shadowCells.length > cellIndex) {
td = shadowCells[cellIndex];
}
}
if (td) { for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
element = queryShadowDOM(td, selector); const record = {};
const currentRow = processedRows[rowIndex];
if (!element && selector.split(">").pop().includes('td:nth-child')) { for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
element = td; let element = null;
}
if (!element) { if (cellIndex >= 0) {
const tagOnlySelector = selector.split('.')[0]; // Get TD element considering both contexts
element = queryShadowDOM(td, tagOnlySelector); let td = currentRow.children[cellIndex];
}
if (!element) { // Check shadow DOM for td
let currentElement = td; if (!td && currentRow.shadowRoot) {
while (currentElement && currentElement.children.length > 0) { const shadowCells = currentRow.shadowRoot.children;
let foundContentChild = false; if (shadowCells && shadowCells.length > cellIndex) {
for (const child of currentElement.children) { td = shadowCells[cellIndex];
if (extractValue(child, attribute)) { }
currentElement = child; }
foundContentChild = true;
break;
}
}
if (!foundContentChild) break;
}
element = currentElement;
}
}
} else {
element = queryShadowDOM(currentRow, selector);
}
if (element) { if (td) {
record[label] = extractValue(element, attribute); element = queryElement(td, selector);
}
}
if (Object.keys(record).length > 0) { if (!element && selector.split(/(?:>>|:>>)/).pop().includes('td:nth-child')) {
tableData.push(record); element = td;
} }
}
} if (!element) {
} const tagOnlySelector = selector.split('.')[0];
element = queryElement(td, tagOnlySelector);
}
if (!element) {
let currentElement = td;
while (currentElement && currentElement.children.length > 0) {
let foundContentChild = false;
for (const child of currentElement.children) {
if (extractValue(child, attribute)) {
currentElement = child;
foundContentChild = true;
break;
}
}
if (!foundContentChild) break;
}
element = currentElement;
}
}
} else {
element = queryElement(currentRow, selector);
}
if (element) {
record[label] = extractValue(element, attribute);
}
}
if (Object.keys(record).length > 0) {
tableData.push(record);
}
}
}
}
} }
// Non-table data scraping remains unchanged // Process non-table data with both contexts support
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) { for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
if (nonTableData.length >= limit) break; if (nonTableData.length >= limit) break;
const container = containers[containerIndex]; const container = containers[containerIndex];
const { nonTableFields } = containerFields[containerIndex]; const { nonTableFields } = containerFields[containerIndex];
if (Object.keys(nonTableFields).length > 0) { if (Object.keys(nonTableFields).length > 0) {
const record = {}; const record = {};
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) { for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
const relativeSelector = selector.split('>>').slice(-1)[0]; // Get the last part of the selector after any context delimiter
const element = queryShadowDOM(container, relativeSelector); const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0];
const element = queryElement(container, relativeSelector);
if (element) { if (element) {
record[label] = extractValue(element, attribute); record[label] = extractValue(element, attribute);
} }
} }
if (Object.keys(record).length > 0) { if (Object.keys(record).length > 0) {
nonTableData.push(record); nonTableData.push(record);
} }
} }
} }
// Merge and limit the results // Merge and limit the results
const scrapedData = [...tableData, ...nonTableData]; const scrapedData = [...tableData, ...nonTableData];
return scrapedData; return scrapedData;
}; };
/** /**
* Gets all children of the elements matching the listSelector, * Gets all children of the elements matching the listSelector,

View File

@@ -680,11 +680,25 @@ export default class Interpreter extends EventEmitter {
return workflow; return workflow;
} }
private removeSpecialSelectors(workflow: Workflow) {
for (let actionId = workflow.length - 1; actionId >= 0; actionId--) {
const step = workflow[actionId];
if (step.where && Array.isArray(step.where.selectors)) {
// Filter out if selector has EITHER ":>>" OR ">>"
step.where.selectors = step.where.selectors.filter(selector =>
!(selector.includes(':>>') || selector.includes('>>'))
);
}
}
return workflow;
}
private async runLoop(p: Page, workflow: Workflow) { private async runLoop(p: Page, workflow: Workflow) {
let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow)); let workflowCopy: Workflow = JSON.parse(JSON.stringify(workflow));
// remove shadow selectors workflowCopy = this.removeSpecialSelectors(workflowCopy);
workflowCopy = this.removeShadowSelectors(workflowCopy);
// apply ad-blocker to the current page // apply ad-blocker to the current page
try { try {

View File

@@ -129,6 +129,12 @@ export interface BaseActionInfo {
hasOnlyText: boolean; hasOnlyText: boolean;
} }
interface IframeSelector {
full: string;
isIframe: boolean;
}
interface ShadowSelector { interface ShadowSelector {
full: string; full: string;
mode: string; mode: string;
@@ -148,7 +154,8 @@ export interface Selectors {
hrefSelector: string|null; hrefSelector: string|null;
accessibilitySelector: string|null; accessibilitySelector: string|null;
formSelector: string|null; formSelector: string|null;
shadowSelector: ShadowSelector | null; iframeSelector: IframeSelector|null;
shadowSelector: ShadowSelector|null;
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,10 @@ export const getBestSelectorForAction = (action: Action) => {
case ActionType.DragAndDrop: { case ActionType.DragAndDrop: {
const selectors = action.selectors; const selectors = action.selectors;
if (selectors?.iframeSelector?.full) {
return selectors.iframeSelector.full;
if (selectors?.shadowSelector?.full) { if (selectors?.shadowSelector?.full) {
return selectors.shadowSelector.full; return selectors.shadowSelector.full;
} }

View File

@@ -12,6 +12,7 @@ import { useTranslation } from 'react-i18next';
interface ElementInfo { interface ElementInfo {
tagName: string; tagName: string;
hasOnlyText?: boolean; hasOnlyText?: boolean;
isIframeContent?: boolean;
isShadowRoot?: boolean; isShadowRoot?: boolean;
innerText?: string; innerText?: string;
url?: string; url?: string;
@@ -117,24 +118,47 @@ export const BrowserWindow = () => {
}, [screenShot, canvasRef, socket, screencastHandler]); }, [screenShot, canvasRef, socket, screencastHandler]);
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => { const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
console.log("LIST SELECTOR", listSelector);
console.log("DATA SELECTOR", data.selector);
console.log("CHILD SELECTORS", data.childSelectors);
if (getList === true) { if (getList === true) {
if (listSelector) { if (listSelector) {
socket?.emit('listSelector', { selector: listSelector }); socket?.emit('listSelector', { selector: listSelector });
const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0; const hasValidChildSelectors = Array.isArray(data.childSelectors) && data.childSelectors.length > 0;
if (limitMode) { if (limitMode) {
setHighlighterData(null); setHighlighterData(null);
} else if (paginationMode) { } else if (paginationMode) {
// only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp' // Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) { if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) {
setHighlighterData(data); setHighlighterData(data);
} else { } else {
setHighlighterData(null); setHighlighterData(null);
} }
} else if (data.childSelectors && data.childSelectors.includes(data.selector)) { } else if (data.childSelectors && data.childSelectors.includes(data.selector)) {
// highlight only valid child elements within the listSelector // Highlight only valid child elements within the listSelector
setHighlighterData(data); setHighlighterData(data);
} else if (data.elementInfo?.isIframeContent && data.childSelectors) {
// Handle pure iframe elements - similar to previous shadow DOM logic but using iframe syntax
// Check if the selector matches any iframe child selectors
const isIframeChild = data.childSelectors.some(childSelector =>
data.selector.includes(':>>') && // Iframe uses :>> for traversal
childSelector.split(':>>').some(part =>
data.selector.includes(part.trim())
)
);
setHighlighterData(isIframeChild ? data : null);
} else if (data.selector.includes(':>>') && hasValidChildSelectors) {
// Handle mixed DOM cases with iframes
// Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split(':>>').map(part => part.trim());
const isValidMixedSelector = selectorParts.some(part =>
// We know data.childSelectors is defined due to hasValidChildSelectors check
data.childSelectors!.some(childSelector =>
childSelector.includes(part)
)
);
setHighlighterData(isValidMixedSelector ? data : null);
} else if (data.elementInfo?.isShadowRoot && data.childSelectors) { } else if (data.elementInfo?.isShadowRoot && data.childSelectors) {
// New case: Handle pure Shadow DOM elements // New case: Handle pure Shadow DOM elements
// Check if the selector matches any shadow root child selectors // Check if the selector matches any shadow root child selectors
@@ -145,7 +169,7 @@ export const BrowserWindow = () => {
) )
); );
setHighlighterData(isShadowChild ? data : null); setHighlighterData(isShadowChild ? data : null);
} else if (data.selector.includes('>>') && hasValidChildSelectors) { } else if (data.selector.includes('>>') && hasValidChildSelectors) {
// New case: Handle mixed DOM cases // New case: Handle mixed DOM cases
// Split the selector into parts and check each against child selectors // Split the selector into parts and check each against child selectors
const selectorParts = data.selector.split('>>').map(part => part.trim()); const selectorParts = data.selector.split('>>').map(part => part.trim());
@@ -156,19 +180,18 @@ export const BrowserWindow = () => {
) )
); );
setHighlighterData(isValidMixedSelector ? data : null); setHighlighterData(isValidMixedSelector ? data : null);
} } else {
else {
// if !valid child in normal mode, clear the highlighter // if !valid child in normal mode, clear the highlighter
setHighlighterData(null); setHighlighterData(null);
} }
} else { } else {
// set highlighterData for the initial listSelector selection // Set highlighterData for the initial listSelector selection
setHighlighterData(data); setHighlighterData(data);
} }
} else { } else {
// for non-list steps // For non-list steps
setHighlighterData(data); setHighlighterData(data);
} }
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]); }, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);