From 93a17cc79740eb0589aafcc7d7b9e193dc1f0bc3 Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:24:43 +0100 Subject: [PATCH] Enhance DOM extension methods with additional pseudo-selector handlers and improve nth-child logic --- lib/utils/extensions/dom_extensions.dart | 243 ++++++++++++++++++++++- 1 file changed, 234 insertions(+), 9 deletions(-) diff --git a/lib/utils/extensions/dom_extensions.dart b/lib/utils/extensions/dom_extensions.dart index 65a5199b..b7d3b3ba 100644 --- a/lib/utils/extensions/dom_extensions.dart +++ b/lib/utils/extensions/dom_extensions.dart @@ -4,14 +4,86 @@ import 'package:mangayomi/utils/reg_exp_matcher.dart'; import 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart'; void _initPseudoSelector() { - bool nthChild(Element element, String? args) { - if (int.tryParse(args!) != null) { - final parent = element.parentNode; - return parent != null && - (int.parse(args) as num) > 0 && - parent.nodes.indexOf(element) == int.parse(args); + (int, int) parseNth(String arg) { + arg = arg.toLowerCase().replaceAll(' ', ''); + if (arg == 'odd') return (2, 1); + if (arg == 'even') return (2, 0); + final reg = RegExp(r'^(\d*)n([+-]?\d+)?$'); + final match = reg.firstMatch(arg); + if (match != null) { + final aStr = match.group(1); + final a = aStr == null || aStr.isEmpty ? 1 : int.parse(aStr); + final bStr = match.group(2); + final b = bStr == null ? 0 : int.parse(bStr); + return (a, b); } - return true; + final n = int.tryParse(arg); + if (n != null) return (0, n); + return (0, 0); + } + + bool matchesNth(int index, int a, int b) { + if (a == 0) return index == b; + final diff = index - b; + return diff % a == 0 && diff ~/ a >= 0; + } + + String getWholeText(Element element) { + return element.nodes.map((node) { + if (node is Text) return node.text; + if (node is Element) return getWholeText(node); + return ''; + }).join(); + } + + String getWholeOwnText(Element element) { + return element.nodes.whereType().map((t) => t.text).join(); + } + + bool nthChild(Element element, String? args) { + if (args == null) return false; + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children; + final index = siblings.indexOf(element) + 1; // 1-based + final (a, b) = parseNth(args); + return matchesNth(index, a, b); + } + + bool nthLastChild(Element element, String? args) { + if (args == null) return false; + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children; + final index = + siblings.length - siblings.indexOf(element); // 1-based from end + final (a, b) = parseNth(args); + return matchesNth(index, a, b); + } + + bool nthOfType(Element element, String? args) { + if (args == null) return false; + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children + .where((e) => e.localName == element.localName) + .toList(); + final index = siblings.indexOf(element) + 1; // 1-based + final (a, b) = parseNth(args); + return matchesNth(index, a, b); + } + + bool nthLastOfType(Element element, String? args) { + if (args == null) return false; + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children + .where((e) => e.localName == element.localName) + .toList(); + final index = + siblings.length - siblings.indexOf(element); // 1-based from end + final (a, b) = parseNth(args); + return matchesNth(index, a, b); } bool has(Element element, String? args) { @@ -34,7 +106,74 @@ void _initPseudoSelector() { bool contains(Element element, String? args) { final text = args ?? ''; - return element.text.contains(text); + return element.text.toLowerCase().contains(text.toLowerCase()); + } + + bool containsOwn(Element element, String? args) { + final text = args ?? ''; + final ownText = element.nodes.whereType().map((t) => t.text).join(); + return ownText.toLowerCase().contains(text.toLowerCase()); + } + + bool matches(Element element, String? args) { + if (args == null) return false; + try { + final reg = RegExp(args, caseSensitive: false); + return reg.hasMatch(element.text); + } catch (e) { + return false; + } + } + + bool containsData(Element element, String? args) { + final data = args ?? ''; + // For script and style elements + if (element.localName == 'script' || element.localName == 'style') { + return element.text.toLowerCase().contains(data.toLowerCase()); + } + return false; + } + + bool containsWholeText(Element element, String? args) { + final text = args ?? ''; + return getWholeText(element).contains(text); + } + + bool containsWholeOwnText(Element element, String? args) { + final text = args ?? ''; + return getWholeOwnText(element).contains(text); + } + + bool matchesWholeText(Element element, String? args) { + if (args == null) return false; + try { + final reg = RegExp(args); + return reg.hasMatch(getWholeText(element)); + } catch (e) { + return false; + } + } + + bool matchesWholeOwnText(Element element, String? args) { + if (args == null) return false; + try { + final reg = RegExp(args); + return reg.hasMatch(getWholeOwnText(element)); + } catch (e) { + return false; + } + } + + bool isSelector(Element element, String? args) { + if (args == null) return false; + final selectors = args.split(',').map((s) => s.trim()).toList(); + for (final sel in selectors) { + try { + final parsed = pseudom.parse(sel); + if (parsed.selectFirst(element) != null) return true; + } catch (_) {} + } + return false; } bool firstChild(Element element, String? args) { @@ -45,17 +184,103 @@ void _initPseudoSelector() { return element.nextElementSibling == null; } + bool firstOfType(Element element, String? args) { + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children.where( + (e) => e.localName == element.localName, + ); + return siblings.first == element; + } + + bool lastOfType(Element element, String? args) { + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children.where( + (e) => e.localName == element.localName, + ); + return siblings.last == element; + } + bool onlyChild(Element element, String? args) { - return element.nextElementSibling == null; + return element.previousElementSibling == null && + element.nextElementSibling == null; + } + + bool onlyOfType(Element element, String? args) { + final parent = element.parent; + if (parent == null) return false; + final siblings = parent.children.where( + (e) => e.localName == element.localName, + ); + return siblings.length == 1; + } + + bool empty(Element element, String? args) { + return element.children.isEmpty && element.text.trim().isEmpty; + } + + bool root(Element element, String? args) { + return element.parent == null; + } + + bool lt(Element element, String? args) { + if (args == null) return false; + final n = int.tryParse(args); + if (n == null) return false; + final parent = element.parent; + if (parent == null) return false; + final index = parent.children.indexOf(element); + return index < n; + } + + bool gt(Element element, String? args) { + if (args == null) return false; + final n = int.tryParse(args); + if (n == null) return false; + final parent = element.parent; + if (parent == null) return false; + final index = parent.children.indexOf(element); + return index > n; + } + + bool eq(Element element, String? args) { + if (args == null) return false; + final n = int.tryParse(args); + if (n == null) return false; + final parent = element.parent; + if (parent == null) return false; + final index = parent.children.indexOf(element); + return index == n; } pseudom.PseudoSelector.handlers['nth-child'] = nthChild; + pseudom.PseudoSelector.handlers['nth-last-child'] = nthLastChild; + pseudom.PseudoSelector.handlers['nth-of-type'] = nthOfType; + pseudom.PseudoSelector.handlers['nth-last-of-type'] = nthLastOfType; pseudom.PseudoSelector.handlers['has'] = has; pseudom.PseudoSelector.handlers['inot'] = inot; pseudom.PseudoSelector.handlers['contains'] = contains; + pseudom.PseudoSelector.handlers['containsOwn'] = containsOwn; + pseudom.PseudoSelector.handlers['containsData'] = containsData; + pseudom.PseudoSelector.handlers['containsWholeText'] = containsWholeText; + pseudom.PseudoSelector.handlers['containsWholeOwnText'] = + containsWholeOwnText; + pseudom.PseudoSelector.handlers['matches'] = matches; + pseudom.PseudoSelector.handlers['matchesWholeText'] = matchesWholeText; + pseudom.PseudoSelector.handlers['matchesWholeOwnText'] = matchesWholeOwnText; + pseudom.PseudoSelector.handlers['is'] = isSelector; pseudom.PseudoSelector.handlers['last-child'] = lastChild; pseudom.PseudoSelector.handlers['first-child'] = firstChild; + pseudom.PseudoSelector.handlers['first-of-type'] = firstOfType; + pseudom.PseudoSelector.handlers['last-of-type'] = lastOfType; pseudom.PseudoSelector.handlers['only-child'] = onlyChild; + pseudom.PseudoSelector.handlers['only-of-type'] = onlyOfType; + pseudom.PseudoSelector.handlers['empty'] = empty; + pseudom.PseudoSelector.handlers['root'] = root; + pseudom.PseudoSelector.handlers['lt'] = lt; + pseudom.PseudoSelector.handlers['gt'] = gt; + pseudom.PseudoSelector.handlers['eq'] = eq; } String _fixSelector(String selector) {