ff-dom 3.0.3-beta.1 → 3.0.3-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cdn.js CHANGED
@@ -540,6 +540,22 @@
540
540
  String(attribute.value),
541
541
  ].join("::");
542
542
  const computeBlockedAttributeDecision = (attribute, targetElement, isTarget) => {
543
+ if (attribute.name === "class" &&
544
+ attribute.value
545
+ .split(/\s+/)
546
+ .some((token) => INTERNAL_CLASS_TOKENS.has(token))) {
547
+ const alreadyTracked = modifiedElementAttributes.some((modifiedAttribute) => modifiedAttribute.doc === targetElement.ownerDocument &&
548
+ modifiedAttribute.element === targetElement &&
549
+ modifiedAttribute.attributeName === "class");
550
+ if (!alreadyTracked) {
551
+ modifiedElementAttributes.push({
552
+ url: targetElement.baseURI,
553
+ attributeName: "class",
554
+ element: targetElement,
555
+ doc: targetElement.ownerDocument,
556
+ });
557
+ }
558
+ }
543
559
  const sanitizedValue = sanitizeAttributeValue(attribute.name, attribute.value);
544
560
  if (!sanitizedValue || typeof attribute?.value === "boolean") {
545
561
  return false;
@@ -662,6 +678,12 @@
662
678
  if (count === 1 && !isIndex) {
663
679
  return pattern;
664
680
  }
681
+ else {
682
+ const indexedXPath = findXpathWithIndex(pattern, element, docmt, count);
683
+ if (indexedXPath) {
684
+ return indexedXPath;
685
+ }
686
+ }
665
687
  }
666
688
  return;
667
689
  }
@@ -687,6 +709,12 @@
687
709
  if (count === 1 && !isIndex) {
688
710
  return pattern;
689
711
  }
712
+ else {
713
+ const indexedXPath = findXpathWithIndex(pattern, element, docmt, count);
714
+ if (indexedXPath) {
715
+ return indexedXPath;
716
+ }
717
+ }
690
718
  }
691
719
  if (isAttributeProp && hasNumericAttributeValue(value)) {
692
720
  // Strict numeric-attribute validation: do not split numeric attribute
@@ -1021,7 +1049,13 @@
1021
1049
  }
1022
1050
  // Combine all parts into the final XPath
1023
1051
  const finalXPath = `${xpathParts.join("")}`;
1024
- const xpathToReturn = getCountOfXPath(finalXPath, domNode, docmt) === 1 ? finalXPath : (getAbsoluteXPath(domNode, docmt));
1052
+ const finalXPathCount = getCountOfXPath(finalXPath, domNode, docmt);
1053
+ const indexedXPath = finalXPathCount > 1 && isIndex ?
1054
+ findXpathWithIndex(finalXPath, domNode, docmt, finalXPathCount)
1055
+ : undefined;
1056
+ const xpathToReturn = finalXPathCount === 1 ? finalXPath
1057
+ : indexedXPath ? indexedXPath
1058
+ : getAbsoluteXPath(domNode, docmt);
1025
1059
  // Cache the final XPath for this node
1026
1060
  nodeCache.set(cacheKey, xpathToReturn);
1027
1061
  return xpathToReturn;
@@ -1841,7 +1875,7 @@
1841
1875
  if (!xpaths1?.length) {
1842
1876
  const xpaths = addAttributeSplitCombineXpaths(domNode.attributes, domNode, docmt, isTarget);
1843
1877
  if (xpaths?.length) {
1844
- xpaths1.concat(xpaths);
1878
+ xpaths1.push(...xpaths);
1845
1879
  }
1846
1880
  }
1847
1881
  return xpaths1;
@@ -1974,7 +2008,12 @@
1974
2008
  createObserver,
1975
2009
  startObserver,
1976
2010
  stopObserver,
1977
- modifiedElementAttributes,
2011
+ get modifiedElementAttributes() {
2012
+ return modifiedElementAttributes;
2013
+ },
2014
+ set modifiedElementAttributes(attributes) {
2015
+ modifiedElementAttributes = Array.isArray(attributes) ? attributes : [];
2016
+ },
1978
2017
  cspEnabled,
1979
2018
  getXPathPattern,
1980
2019
  getNormalizedPropertyXPath,
@@ -2001,13 +2040,13 @@
2001
2040
  let referenceElementMode = false;
2002
2041
  let parentXpathCache = new WeakMap();
2003
2042
  let childRelativeXpathCache = new WeakMap();
2004
- const getElementSignature = (element) => element
2005
- ? [
2043
+ const getElementSignature = (element) => element ?
2044
+ [
2006
2045
  element.tagName,
2007
2046
  Array.from(element.attributes || [])
2008
2047
  .map((attribute) => `${attribute.name}=${attribute.value}`)
2009
2048
  .join("|"),
2010
- element.textContent || ""
2049
+ element.textContent || "",
2011
2050
  ].join("::")
2012
2051
  : "";
2013
2052
  const clearXPathModuleCaches = () => {
@@ -2025,7 +2064,7 @@
2025
2064
  axes: "axes",
2026
2065
  index: "index",
2027
2066
  hardCodedText: "text",
2028
- tillTag: "tag"
2067
+ tillTag: "tag",
2029
2068
  };
2030
2069
  const getStrategyName = (strategy) => {
2031
2070
  return STRATEGY_MAP[strategy] || strategy;
@@ -2034,9 +2073,9 @@
2034
2073
  const withStrategyKey = (strategy, entries) => {
2035
2074
  return entries.map((entry) => ({
2036
2075
  ...entry,
2037
- key: entry?.key?.includes(strategy)
2038
- ? entry.key
2039
- : `${strategy} ${entry.key}`.trim()
2076
+ key: entry?.key?.includes(strategy) ?
2077
+ entry.key
2078
+ : `${strategy} ${entry.key}`.trim(),
2040
2079
  }));
2041
2080
  };
2042
2081
  const getUniqueXpathEntries = (entries) => {
@@ -2077,9 +2116,7 @@
2077
2116
  }
2078
2117
  return false;
2079
2118
  };
2080
- const removePositionalIndexXpaths = (entries, isIndex) => isIndex
2081
- ? entries
2082
- : entries.filter((entry) => !hasPositionalIndex(entry.value));
2119
+ const removePositionalIndexXpaths = (entries, isIndex) => isIndex ? entries : (entries.filter((entry) => !hasPositionalIndex(entry.value)));
2083
2120
  const getNormalizedText = (element) => {
2084
2121
  return element?.textContent?.replace(/\s+/g, " ")?.trim() || "";
2085
2122
  };
@@ -2099,12 +2136,9 @@
2099
2136
  const xpaths = [];
2100
2137
  const attributes = Array.from(element.attributes || []).filter((attribute) => attribute?.value && checkBlockedAttributes(attribute, element, isTarget));
2101
2138
  const text = getNormalizedText(element);
2102
- const getStrategyXPath = strategy === "contains"
2103
- ? getContainsPropertyXPath
2104
- : strategy === "startsWith"
2105
- ? getStartsWithPropertyXPath
2106
- : strategy === "normalizeSpace"
2107
- ? getNormalizedPropertyXPath
2139
+ const getStrategyXPath = strategy === "contains" ? getContainsPropertyXPath
2140
+ : strategy === "startsWith" ? getStartsWithPropertyXPath
2141
+ : strategy === "normalizeSpace" ? getNormalizedPropertyXPath
2108
2142
  : null;
2109
2143
  if (!getStrategyXPath) {
2110
2144
  return xpaths;
@@ -2125,7 +2159,7 @@
2125
2159
  if (xpath) {
2126
2160
  xpaths.push({
2127
2161
  key: `xpath by ${strategy} class`,
2128
- value: xpath
2162
+ value: xpath,
2129
2163
  });
2130
2164
  }
2131
2165
  });
@@ -2135,7 +2169,7 @@
2135
2169
  if (xpath) {
2136
2170
  xpaths.push({
2137
2171
  key: `xpath by ${strategy} ${attribute.name}`,
2138
- value: xpath
2172
+ value: xpath,
2139
2173
  });
2140
2174
  }
2141
2175
  });
@@ -2148,7 +2182,7 @@
2148
2182
  if (xpath) {
2149
2183
  xpaths.push({
2150
2184
  key: `xpath by ${strategy} text`,
2151
- value: xpath
2185
+ value: xpath,
2152
2186
  });
2153
2187
  }
2154
2188
  }
@@ -2158,8 +2192,8 @@
2158
2192
  const attributes = Array.from(element.attributes || []).filter((attribute) => attribute?.value && checkBlockedAttributes(attribute, element, isTarget));
2159
2193
  if (strategy === "and") {
2160
2194
  const combinationXpath = getAttributeCombinationXpath(element, docmt, attributes, isTarget);
2161
- return combinationXpath
2162
- ? [{ key: "xpath by combination", value: combinationXpath }]
2195
+ return combinationXpath ?
2196
+ [{ key: "xpath by combination", value: combinationXpath }]
2163
2197
  : [];
2164
2198
  }
2165
2199
  if (strategy === "or" && attributes.length >= 1) {
@@ -2185,16 +2219,16 @@
2185
2219
  };
2186
2220
  const getSiblingNodes = (element, direction) => {
2187
2221
  const nodes = [];
2188
- let currentNode = direction === "previous"
2189
- ? element.previousElementSibling
2222
+ let currentNode = direction === "previous" ?
2223
+ element.previousElementSibling
2190
2224
  : element.nextElementSibling;
2191
2225
  while (currentNode) {
2192
2226
  if (!currentNode.classList?.contains("flntooltip")) {
2193
2227
  nodes.push(currentNode);
2194
2228
  }
2195
2229
  currentNode =
2196
- direction === "previous"
2197
- ? currentNode.previousElementSibling
2230
+ direction === "previous" ?
2231
+ currentNode.previousElementSibling
2198
2232
  : currentNode.nextElementSibling;
2199
2233
  }
2200
2234
  return nodes;
@@ -2202,20 +2236,20 @@
2202
2236
  const getDirectionalAxisSeedNodes = (element, direction) => {
2203
2237
  const nodes = [];
2204
2238
  for (let currentNode = element; currentNode?.parentElement; currentNode = currentNode.parentElement) {
2205
- let sibling = direction === "previous"
2206
- ? currentNode.previousElementSibling
2239
+ let sibling = direction === "previous" ?
2240
+ currentNode.previousElementSibling
2207
2241
  : currentNode.nextElementSibling;
2208
2242
  while (sibling) {
2209
2243
  if (!sibling.classList?.contains("flntooltip")) {
2210
2244
  nodes.push(sibling);
2211
2245
  nodes.push({
2212
2246
  node: sibling,
2213
- expand: () => collectSubtreeElements(sibling)
2247
+ expand: () => collectSubtreeElements(sibling),
2214
2248
  });
2215
2249
  }
2216
2250
  sibling =
2217
- direction === "previous"
2218
- ? sibling.previousElementSibling
2251
+ direction === "previous" ?
2252
+ sibling.previousElementSibling
2219
2253
  : sibling.nextElementSibling;
2220
2254
  }
2221
2255
  }
@@ -2249,8 +2283,8 @@
2249
2283
  };
2250
2284
  const buildAxisXpathsForNodes = (nodes, axis, targetElement, docmt, isTarget, key, isIndex) => {
2251
2285
  const textPredicate = getTextXpathFunction(targetElement);
2252
- const targetNodeTest = textPredicate
2253
- ? `${getAxisNodeTest(targetElement)}[${textPredicate}]`
2286
+ const targetNodeTest = textPredicate ?
2287
+ `${getAxisNodeTest(targetElement)}[${textPredicate}]`
2254
2288
  : getAxisNodeTest(targetElement);
2255
2289
  const MAX_INDEX_TRY = 3;
2256
2290
  const MAX_CANDIDATES = 5;
@@ -2286,7 +2320,7 @@
2286
2320
  }
2287
2321
  }
2288
2322
  return anchors;
2289
- }
2323
+ },
2290
2324
  ];
2291
2325
  for (let stageIndex = 0; stageIndex < anchorStages.length; stageIndex++) {
2292
2326
  let anchors = anchorStages[stageIndex]();
@@ -2396,8 +2430,8 @@
2396
2430
  };
2397
2431
  const getTargetPathFromScope = (scope, targetElement) => {
2398
2432
  const targetNodeTest = getAxisNodeTest(targetElement);
2399
- return targetElement.parentElement === scope
2400
- ? [`/${targetNodeTest}`, `/descendant::${targetNodeTest}`]
2433
+ return targetElement.parentElement === scope ?
2434
+ [`/${targetNodeTest}`, `/descendant::${targetNodeTest}`]
2401
2435
  : [`/descendant::${targetNodeTest}`];
2402
2436
  };
2403
2437
  const collectChainedAxisAnchorNodes = (element) => {
@@ -2420,7 +2454,7 @@
2420
2454
  for (let scope = element.parentElement, depth = 0; scope && depth < MAX_SCOPE_DEPTH && anchors.length < MAX_ANCHORS; scope = scope.parentElement, depth++) {
2421
2455
  const siblingGroups = [
2422
2456
  getSiblingNodes(scope, "previous").slice(0, MAX_SIBLINGS_PER_SCOPE),
2423
- getSiblingNodes(scope, "next").slice(0, MAX_SIBLINGS_PER_SCOPE)
2457
+ getSiblingNodes(scope, "next").slice(0, MAX_SIBLINGS_PER_SCOPE),
2424
2458
  ];
2425
2459
  for (const siblings of siblingGroups) {
2426
2460
  for (const sibling of siblings) {
@@ -2470,7 +2504,7 @@
2470
2504
  if (isExactUniqueXpath(xpath, element, docmt)) {
2471
2505
  candidates.push({
2472
2506
  key: "chained axes fallback",
2473
- value: xpath
2507
+ value: xpath,
2474
2508
  });
2475
2509
  if (candidates.length >= 2) {
2476
2510
  return candidates;
@@ -2570,7 +2604,7 @@
2570
2604
  // Keep existing unique attribute/text anchors as secondary options.
2571
2605
  return [
2572
2606
  ...anchors,
2573
- ...getUniqueNodeAnchorXpaths(anchorNode, docmt, isTarget)
2607
+ ...getUniqueNodeAnchorXpaths(anchorNode, docmt, isTarget),
2574
2608
  ];
2575
2609
  };
2576
2610
  const buildAncestorDescendantBridgeAxesXpaths = (element, docmt, isTarget) => {
@@ -2602,7 +2636,7 @@
2602
2636
  // variants for the same target in single mode.
2603
2637
  candidates.push({
2604
2638
  key: "ancestor descendant bridge",
2605
- value: xpath
2639
+ value: xpath,
2606
2640
  });
2607
2641
  if (candidates.length >= 1) {
2608
2642
  return candidates;
@@ -2630,8 +2664,8 @@
2630
2664
  // Single-mode axis anchors may use a stable prefix before a numeric suffix.
2631
2665
  const valueBeforeNumber = attributeValue.split(/\d/)[0] || "";
2632
2666
  const stableValue = valueBeforeNumber.replace(/[-_:\s.]+$/g, "").trim();
2633
- return stableValue.length > 2 && /[a-zA-Z]/.test(stableValue)
2634
- ? stableValue
2667
+ return stableValue.length > 2 && /[a-zA-Z]/.test(stableValue) ?
2668
+ stableValue
2635
2669
  : "";
2636
2670
  };
2637
2671
  const canUseStableAxisAttribute = (attribute) => {
@@ -2686,7 +2720,7 @@
2686
2720
  if (isExactUniqueXpath(xpath, element, docmt)) {
2687
2721
  candidates.push({
2688
2722
  key: "child chain",
2689
- value: xpath
2723
+ value: xpath,
2690
2724
  });
2691
2725
  if (candidates.length >= 2) {
2692
2726
  return candidates;
@@ -2726,48 +2760,52 @@
2726
2760
  {
2727
2761
  nodes: limitNodes(nextSiblings),
2728
2762
  axis: "preceding-sibling",
2729
- key: "preceding-sibling"
2763
+ key: "preceding-sibling",
2730
2764
  },
2731
2765
  {
2732
2766
  nodes: limitNodes(previousSiblings),
2733
2767
  axis: "following-sibling",
2734
- key: "following-sibling"
2735
- }
2768
+ key: "following-sibling",
2769
+ },
2736
2770
  ],
2737
2771
  [
2738
2772
  { nodes: limitNodes(ancestors), axis: "child", key: "child" },
2739
2773
  {
2740
2774
  nodes: limitNodes(descendants, 15),
2741
2775
  axis: "parent",
2742
- key: "parent"
2743
- }
2776
+ key: "parent",
2777
+ },
2744
2778
  ],
2745
2779
  [
2746
2780
  { nodes: limitNodes(ancestors), axis: "descendant", key: "descendant" },
2747
2781
  {
2748
2782
  nodes: limitNodes(ancestors),
2749
2783
  axis: "descendant-or-self",
2750
- key: "descendant-or-self"
2784
+ key: "descendant-or-self",
2751
2785
  },
2752
2786
  {
2753
2787
  nodes: limitNodes(descendants, 10),
2754
2788
  axis: "ancestor",
2755
- key: "ancestor"
2789
+ key: "ancestor",
2756
2790
  },
2757
2791
  {
2758
2792
  nodes: limitNodes(descendants, 10),
2759
2793
  axis: "ancestor-or-self",
2760
- key: "ancestor-or-self"
2761
- }
2794
+ key: "ancestor-or-self",
2795
+ },
2762
2796
  ],
2763
2797
  [
2764
2798
  {
2765
2799
  nodes: limitNodes(precedingSeeds),
2766
2800
  axis: "following",
2767
- key: "following"
2801
+ key: "following",
2802
+ },
2803
+ {
2804
+ nodes: limitNodes(followingSeeds),
2805
+ axis: "preceding",
2806
+ key: "preceding",
2768
2807
  },
2769
- { nodes: limitNodes(followingSeeds), axis: "preceding", key: "preceding" }
2770
- ]
2808
+ ],
2771
2809
  ];
2772
2810
  // timeLog("nodes sorting", t);
2773
2811
  const childChainResults = buildAnchoredChildChainAxesXpaths(element, docmt, isTarget, isIndex);
@@ -2803,14 +2841,14 @@
2803
2841
  // global must always exist)
2804
2842
  results.push({
2805
2843
  key: "xpath by index",
2806
- value: `(//${tag})[${idx}]`
2844
+ value: `(//${tag})[${idx}]`,
2807
2845
  });
2808
2846
  // Add at most one scoped/indexed XPath anchored by the nearest stable ancestor.
2809
2847
  let current = element.parentElement;
2810
2848
  while (current && current !== docmt.body) {
2811
2849
  if (current.id || current.className) {
2812
- const scopeXpaths = current.id
2813
- ? getXpathStrings(current, "id", current.id)
2850
+ const scopeXpaths = current.id ?
2851
+ getXpathStrings(current, "id", current.id)
2814
2852
  : getXpathStrings(current, "class", current.className.toString());
2815
2853
  // Scoped index anchors should also try every stable class token before
2816
2854
  // walking to a higher ancestor.
@@ -2820,7 +2858,7 @@
2820
2858
  if (scopedIdx > 0) {
2821
2859
  results.push({
2822
2860
  key: "xpath by index",
2823
- value: `(${scopeXpath}//${tag})[${scopedIdx}]`
2861
+ value: `(${scopeXpath}//${tag})[${scopedIdx}]`,
2824
2862
  });
2825
2863
  break;
2826
2864
  }
@@ -2852,7 +2890,7 @@
2852
2890
  }
2853
2891
  if (strategyName === "tag") {
2854
2892
  return withStrategyKey(strategy, [
2855
- { key: "xpath till tag", value: getTagOnlyXPath(element, docmt) }
2893
+ { key: "xpath till tag", value: getTagOnlyXPath(element, docmt) },
2856
2894
  ]);
2857
2895
  }
2858
2896
  return [];
@@ -2864,14 +2902,14 @@
2864
2902
  xpaths = [
2865
2903
  // `${nodeXpath1}/descendant::${nodeXpath2}`,
2866
2904
  `${nodeXpath1}/descendant-or-self::${nodeXpath2}`,
2867
- `${nodeXpath1}/following::${nodeXpath2}`
2905
+ `${nodeXpath1}/following::${nodeXpath2}`,
2868
2906
  ];
2869
2907
  }
2870
2908
  else {
2871
2909
  xpaths = [
2872
2910
  // `${nodeXpath1}/descendant::${nodeXpath2}`,
2873
2911
  `${nodeXpath1}/ancestor-or-self::${nodeXpath2}`,
2874
- `${nodeXpath1}/preceding::${nodeXpath2}`
2912
+ `${nodeXpath1}/preceding::${nodeXpath2}`,
2875
2913
  ];
2876
2914
  }
2877
2915
  // Iterate through XPath patterns
@@ -2888,7 +2926,7 @@
2888
2926
  xpathDataWithIndex$1.push({
2889
2927
  key: `relative xpath by unique parent ${isIndex ? "index" : ""}`,
2890
2928
  value: xpath,
2891
- count
2929
+ count,
2892
2930
  });
2893
2931
  }
2894
2932
  }
@@ -2896,7 +2934,7 @@
2896
2934
  xpathDataWithIndex$1.push({
2897
2935
  key: `relative xpath by unique parent ${isIndex ? "index" : ""}`,
2898
2936
  value: xpath,
2899
- count
2937
+ count,
2900
2938
  });
2901
2939
  }
2902
2940
  }
@@ -2908,7 +2946,7 @@
2908
2946
  getCountOfXPath(indexedXpath, targetElemt, docmt) === 1) {
2909
2947
  xpathData$1.push({
2910
2948
  key: `relative xpath by unique parent ${isIndex ? "index" : ""}`,
2911
- value: indexedXpath
2949
+ value: indexedXpath,
2912
2950
  });
2913
2951
  }
2914
2952
  }
@@ -2922,7 +2960,7 @@
2922
2960
  isTarget ? "target" : "reference",
2923
2961
  isIndex ? "index" : "no-index",
2924
2962
  nodeXpath,
2925
- getElementSignature(node)
2963
+ getElementSignature(node),
2926
2964
  ].join("::");
2927
2965
  let nodeCache = parentXpathCache.get(domNode);
2928
2966
  if (!nodeCache) {
@@ -2962,14 +3000,16 @@
2962
3000
  (isTarget && !isNumberExist(currentNode.textContent))) {
2963
3001
  let xpathe;
2964
3002
  if (!reWhiteSpace$1.test(currentNode.textContent)) {
2965
- xpathe = isSvg(currentNode)
2966
- ? `//*[local-name()='${currentNode.tagName}' and normalize-space(.)=${escapeCharacters(getFilteredText(currentNode))}]`
2967
- : `//${currentNode.tagName || "*"}[normalize-space(.)=${escapeCharacters(getFilteredText(currentNode))}]`;
3003
+ xpathe =
3004
+ isSvg(currentNode) ?
3005
+ `//*[local-name()='${currentNode.tagName}' and normalize-space(.)=${escapeCharacters(getFilteredText(currentNode))}]`
3006
+ : `//${currentNode.tagName || "*"}[normalize-space(.)=${escapeCharacters(getFilteredText(currentNode))}]`;
2968
3007
  }
2969
3008
  else {
2970
- xpathe = isSvg(currentNode)
2971
- ? `//*[local-name()='${currentNode.tagName}' and .=${escapeCharacters(getFilteredText(currentNode))}]`
2972
- : `//${currentNode.tagName || "*"}[.=${escapeCharacters(getFilteredText(currentNode))}]`;
3009
+ xpathe =
3010
+ isSvg(currentNode) ?
3011
+ `//*[local-name()='${currentNode.tagName}' and .=${escapeCharacters(getFilteredText(currentNode))}]`
3012
+ : `//${currentNode.tagName || "*"}[.=${escapeCharacters(getFilteredText(currentNode))}]`;
2973
3013
  }
2974
3014
  const othersWithAttr = checkRelativeXpathRelation(xpathe, nodeXpath, node, docmt, isIndex, "parent");
2975
3015
  if (othersWithAttr) {
@@ -2979,7 +3019,7 @@
2979
3019
  else {
2980
3020
  const combinePattern = [];
2981
3021
  const contentRes = [
2982
- ...new Set(getFilteredText(currentNode).match(/([^0-9]+)/g))
3022
+ ...new Set(getFilteredText(currentNode).match(/([^0-9]+)/g)),
2983
3023
  ];
2984
3024
  const reWhiteSpace = new RegExp(/^[\S]+( [\S]+)*$/gi);
2985
3025
  if (contentRes?.length) {
@@ -2995,8 +3035,8 @@
2995
3035
  }
2996
3036
  }
2997
3037
  if (combinePattern?.length) {
2998
- const xpathe = isSvg(currentNode)
2999
- ? `//*[local-name()='${currentNode.tagName}' and ${combinePattern.join(" and ")}]`
3038
+ const xpathe = isSvg(currentNode) ?
3039
+ `//*[local-name()='${currentNode.tagName}' and ${combinePattern.join(" and ")}]`
3000
3040
  : `//${currentNode.tagName || "*"}[${combinePattern.join(" and ")}]`;
3001
3041
  const othersWithAttr = checkRelativeXpathRelation(xpathe, nodeXpath, node, docmt, isIndex, "parent");
3002
3042
  if (othersWithAttr) {
@@ -3007,8 +3047,8 @@
3007
3047
  }
3008
3048
  // Construct the XPath based on the tag name
3009
3049
  if (!hasUniqueAttr) {
3010
- const xpathe = isSvg(currentNode)
3011
- ? `/*[local-name()='${currentNode.tagName}']`
3050
+ const xpathe = isSvg(currentNode) ?
3051
+ `/*[local-name()='${currentNode.tagName}']`
3012
3052
  : `/${currentNode.tagName}`;
3013
3053
  xpathParts.unshift(xpathe);
3014
3054
  }
@@ -3078,8 +3118,8 @@
3078
3118
  }
3079
3119
  if (!uniqueAttrFound) {
3080
3120
  // Construct the XPath based on the tag name
3081
- const xpathe = isSvg(currentNode)
3082
- ? `/*[local-name()='${currentNode.tagName}']`
3121
+ const xpathe = isSvg(currentNode) ?
3122
+ `/*[local-name()='${currentNode.tagName}']`
3083
3123
  : `/${currentNode.tagName}`;
3084
3124
  // Prepend the current XPath part to the array
3085
3125
  xpathParts.unshift(xpathe);
@@ -3164,8 +3204,8 @@
3164
3204
  }
3165
3205
  if (!uniqueAttrFound) {
3166
3206
  // Construct the XPath based on the tag name
3167
- const xpathe = isSvg(currentNode)
3168
- ? `/*[local-name()='${currentNode.tagName}']`
3207
+ const xpathe = isSvg(currentNode) ?
3208
+ `/*[local-name()='${currentNode.tagName}']`
3169
3209
  : `/${currentNode.tagName}`;
3170
3210
  // Prepend the current XPath part to the array
3171
3211
  xpathParts.push(xpathe);
@@ -3214,7 +3254,7 @@
3214
3254
  for (const attr of Array.from(sibling.attributes)) {
3215
3255
  const xpaths = intermediateXpathSteps(sibling, {
3216
3256
  name: attr.name,
3217
- value: attr.value
3257
+ value: attr.value,
3218
3258
  }, isTarget);
3219
3259
  for (let xpathe of xpaths) {
3220
3260
  xpathe += `/${axis}::${nodeXpath}`;
@@ -3222,7 +3262,7 @@
3222
3262
  if (count === 1) {
3223
3263
  xpathData$1.push({
3224
3264
  key: `xpath by ${axis}`,
3225
- value: xpathe
3265
+ value: xpathe,
3226
3266
  });
3227
3267
  return;
3228
3268
  }
@@ -3233,7 +3273,7 @@
3233
3273
  xpathDataWithIndex$1.push({
3234
3274
  key: `relative xpath by ${axis}`,
3235
3275
  value: xpathe,
3236
- count
3276
+ count,
3237
3277
  });
3238
3278
  }
3239
3279
  }
@@ -3241,7 +3281,7 @@
3241
3281
  xpathDataWithIndex$1.push({
3242
3282
  key: `relative xpath by ${axis}`,
3243
3283
  value: xpathe,
3244
- count
3284
+ count,
3245
3285
  });
3246
3286
  }
3247
3287
  }
@@ -3252,14 +3292,14 @@
3252
3292
  let xpathe;
3253
3293
  xpathe = intermediateXpathStep(sibling, {
3254
3294
  name: "text",
3255
- value: sibling.textContent
3295
+ value: sibling.textContent,
3256
3296
  }, isTarget);
3257
3297
  if (xpathe) {
3258
3298
  const count = getCountOfXPath(xpathe, sibling, docmt);
3259
3299
  if (count === 1) {
3260
3300
  xpathData$1.push({
3261
3301
  key: `xpath by ${axis}`,
3262
- value: xpathe
3302
+ value: xpathe,
3263
3303
  });
3264
3304
  return;
3265
3305
  }
@@ -3270,7 +3310,7 @@
3270
3310
  xpathDataWithIndex$1.push({
3271
3311
  key: `relative xpath by ${axis}`,
3272
3312
  value: xpathe,
3273
- count
3313
+ count,
3274
3314
  });
3275
3315
  }
3276
3316
  }
@@ -3278,7 +3318,7 @@
3278
3318
  xpathDataWithIndex$1.push({
3279
3319
  key: `relative xpath by ${axis}`,
3280
3320
  value: xpathe,
3281
- count
3321
+ count,
3282
3322
  });
3283
3323
  }
3284
3324
  }
@@ -3300,20 +3340,17 @@
3300
3340
  if (hasNumericAttributeValue(normalizedTextContent) && !stableTargetText) {
3301
3341
  return;
3302
3342
  }
3303
- const textForPredicate = hasNumericAttributeValue(normalizedTextContent)
3304
- ? stableTargetText
3305
- : normalizedTextContent;
3343
+ const textForPredicate = hasNumericAttributeValue(normalizedTextContent) ? stableTargetText : (normalizedTextContent);
3306
3344
  const containerTextCondition = getContainerTextCondition(targetElemt, textForPredicate);
3307
3345
  for (const attrName of attributes) {
3308
3346
  if (checkBlockedAttributes(attrName, targetElemt, isTarget)) {
3309
3347
  let attrValue = sanitizeAttributeValue(attrName.name, attrName.nodeValue);
3310
3348
  const elementName = attrName.name;
3311
- const textCondition = containerTextCondition
3312
- ? containerTextCondition
3313
- : hasNumericAttributeValue(normalizedTextContent)
3314
- ? `contains(normalize-space(.),${escapeCharacters(textForPredicate)})`
3315
- : /\s/.test(textContent)
3316
- ? `normalize-space(.)=${escapeCharacters(textForPredicate)}`
3349
+ const textCondition = containerTextCondition ? containerTextCondition
3350
+ : hasNumericAttributeValue(normalizedTextContent) ?
3351
+ `contains(normalize-space(.),${escapeCharacters(textForPredicate)})`
3352
+ : /\s/.test(textContent) ?
3353
+ `normalize-space(.)=${escapeCharacters(textForPredicate)}`
3317
3354
  : `text()=${escapeCharacters(textForPredicate)}`;
3318
3355
  const xpath = `//${tagName}[@${elementName}='${attrValue}' and ${textCondition}]`;
3319
3356
  if (xpath) {
@@ -3335,7 +3372,7 @@
3335
3372
  for (const attrName of attribute) {
3336
3373
  const expressions = intermediateXpathSteps(targetElemt, {
3337
3374
  name: attrName.name,
3338
- value: attrName.value
3375
+ value: attrName.value,
3339
3376
  }, isTarget);
3340
3377
  for (const expression of expressions) {
3341
3378
  if (expression) {
@@ -3347,7 +3384,7 @@
3347
3384
  if (targetElemt.textContent) {
3348
3385
  let expression = intermediateXpathStep(targetElemt, {
3349
3386
  name: "text",
3350
- value: targetElemt.textContent
3387
+ value: targetElemt.textContent,
3351
3388
  }, isTarget);
3352
3389
  if (expression) {
3353
3390
  nodeXpath.push(expression);
@@ -3372,7 +3409,7 @@
3372
3409
  if (count === 1) {
3373
3410
  xpathData$1.push({
3374
3411
  key: "relative xpath by relative parent",
3375
- value: fullRelativeXpath
3412
+ value: fullRelativeXpath,
3376
3413
  });
3377
3414
  }
3378
3415
  else if (count > 1 && isIndex) {
@@ -3381,7 +3418,7 @@
3381
3418
  getCountOfXPath(relativeXpathIndex, targetElemt, docmt) === 1) {
3382
3419
  xpathData$1.push({
3383
3420
  key: `relative xpath by relative parent ${isIndex ? "index" : ""}`,
3384
- value: relativeXpathIndex
3421
+ value: relativeXpathIndex,
3385
3422
  });
3386
3423
  }
3387
3424
  }
@@ -3402,7 +3439,7 @@
3402
3439
  if (count === 1) {
3403
3440
  xpathData$1.push({
3404
3441
  key: "relative xpath by relative child",
3405
- value: fullRelativeXpath
3442
+ value: fullRelativeXpath,
3406
3443
  });
3407
3444
  }
3408
3445
  else if (count > 1 && isIndex) {
@@ -3411,7 +3448,7 @@
3411
3448
  getCountOfXPath(relativeXpathIndex, targetElemt, docmt) === 1) {
3412
3449
  xpathData$1.push({
3413
3450
  key: `relative xpath by relative parent ${isIndex ? "index" : ""}`,
3414
- value: relativeXpathIndex
3451
+ value: relativeXpathIndex,
3415
3452
  });
3416
3453
  }
3417
3454
  }
@@ -3425,7 +3462,7 @@
3425
3462
  if (txtXpath) {
3426
3463
  xpathData$1.unshift({
3427
3464
  key: `xpath by text${isIndex ? "index" : ""}`,
3428
- value: txtXpath
3465
+ value: txtXpath,
3429
3466
  });
3430
3467
  }
3431
3468
  }
@@ -3435,7 +3472,7 @@
3435
3472
  if (tempRelativeXpath) {
3436
3473
  xpathData$1.push({
3437
3474
  key: "xpath by unique parent",
3438
- value: tempRelativeXpath
3475
+ value: tempRelativeXpath,
3439
3476
  });
3440
3477
  }
3441
3478
  }
@@ -3492,7 +3529,7 @@
3492
3529
  if (xpth) {
3493
3530
  xpathData$1.push({
3494
3531
  key: `xpath by ${attr.name}${isIndex ? " index" : ""}`,
3495
- value: xpth
3532
+ value: xpth,
3496
3533
  });
3497
3534
  }
3498
3535
  }
@@ -3502,7 +3539,7 @@
3502
3539
  if (txtXpath) {
3503
3540
  xpathData$1.push({
3504
3541
  key: `xpath by text${isIndex ? " index" : ""}`,
3505
- value: txtXpath
3542
+ value: txtXpath,
3506
3543
  });
3507
3544
  }
3508
3545
  if (attributesArray.find((element) => ["className", "class"].includes(element.name)) &&
@@ -3511,16 +3548,16 @@
3511
3548
  if (xpath) {
3512
3549
  xpathData$1.push({
3513
3550
  key: "xpath by class",
3514
- value: xpath
3551
+ value: xpath,
3515
3552
  });
3516
3553
  }
3517
3554
  }
3518
- if (!xpathData$1.length && attributesArray.length > 1) {
3555
+ if (attributesArray.length > 1) {
3519
3556
  const combinationXpath = getAttributeCombinationXpath(targetElemt, docmt, attributesArray, isTarget);
3520
3557
  if (combinationXpath)
3521
3558
  xpathData$1.push({
3522
3559
  key: "xpath by combination",
3523
- value: combinationXpath
3560
+ value: combinationXpath,
3524
3561
  });
3525
3562
  }
3526
3563
  if (!xpathData$1.length) {
@@ -3528,7 +3565,7 @@
3528
3565
  if (textAttribute)
3529
3566
  xpathData$1.push({
3530
3567
  key: "xpath by textAttribute",
3531
- value: textAttribute
3568
+ value: textAttribute,
3532
3569
  });
3533
3570
  }
3534
3571
  }
@@ -3566,7 +3603,7 @@
3566
3603
  if (count === 1) {
3567
3604
  xpathData$1.push({
3568
3605
  key: `${xpathData$1[i].key} regex`,
3569
- value: xpth
3606
+ value: xpth,
3570
3607
  });
3571
3608
  }
3572
3609
  }
@@ -3590,7 +3627,7 @@
3590
3627
  getParentRelativeXpath,
3591
3628
  getUniqueParentXpath,
3592
3629
  checkRelativeXpathRelation,
3593
- buildAxesStrategyXpaths
3630
+ buildAxesStrategyXpaths,
3594
3631
  };
3595
3632
 
3596
3633
  let xpathDataWithIndex = [];
@@ -3744,7 +3781,9 @@
3744
3781
  // Share target candidate work across all relation attempts in this run.
3745
3782
  const targetCandidateContext = createTargetCandidateContext(element2, docmt, xpaths2);
3746
3783
  let finalXpaths = [];
3747
- const relations = allowedRelations || [];
3784
+ const relations = allowedRelations?.length ?
3785
+ allowedRelations
3786
+ : getSmartRelations(element1, element2);
3748
3787
  if (!isIndex) {
3749
3788
  // Fresh non-index runs should start from a clean candidate list for this
3750
3789
  // reference/target pair; only candidates found in this pass are reusable.
@@ -5205,6 +5244,19 @@
5205
5244
  const trimmed = value.trim();
5206
5245
  return Boolean(trimmed) && trimmed.length <= 120 && !/^\d+$/.test(trimmed);
5207
5246
  };
5247
+ const isStableSemanticId = (el, idAttribute) => {
5248
+ const value = idAttribute.value.trim();
5249
+ if (!isStableAttributeValue(value))
5250
+ return false;
5251
+ const wasModified = modifiedElementAttributes.some((modifiedAttribute) => modifiedAttribute.doc === el.ownerDocument &&
5252
+ modifiedAttribute.element === el &&
5253
+ modifiedAttribute.attributeName === "id");
5254
+ if (wasModified)
5255
+ return false;
5256
+ // Keep semantic levels/versions such as "level2-shadow-host", but reject
5257
+ // IDs containing long numeric runs, which are more likely generated.
5258
+ return !/\d{4,}/.test(value);
5259
+ };
5208
5260
  const isCssSelectorUnique = (selector, el, root) => {
5209
5261
  try {
5210
5262
  const matches = root.querySelectorAll(selector);
@@ -5262,8 +5314,8 @@
5262
5314
  return;
5263
5315
  const idAttribute = el.getAttributeNode("id");
5264
5316
  if (!idAttribute ||
5265
- !isStableAttributeValue(idAttribute.value) ||
5266
- !checkBlockedAttributes(idAttribute, el, true)) {
5317
+ (!checkBlockedAttributes(idAttribute, el, true) &&
5318
+ !isStableSemanticId(el, idAttribute))) {
5267
5319
  return;
5268
5320
  }
5269
5321
  return `#${escapeCssIdentifier(el.id, el)}`;
@@ -5383,6 +5435,59 @@
5383
5435
  }
5384
5436
  return;
5385
5437
  };
5438
+ const getNthOfType = (el) => {
5439
+ let index = 1;
5440
+ let sibling = el.previousElementSibling;
5441
+ while (sibling) {
5442
+ if (sibling.tagName === el.tagName)
5443
+ index++;
5444
+ sibling = sibling.previousElementSibling;
5445
+ }
5446
+ return index;
5447
+ };
5448
+ const getRelativeCssSegment = (el) => {
5449
+ const parent = el.parentElement;
5450
+ const tag = el.tagName.toLowerCase();
5451
+ if (parent) {
5452
+ const stableSelectors = getDirectStableSelectors(el, {
5453
+ includeTag: true,
5454
+ preferredAttributesOnly: true
5455
+ })
5456
+ .filter((selector) => isCssSelectorUnique(selector, el, parent))
5457
+ .sort((a, b) => a.length - b.length);
5458
+ if (stableSelectors.length)
5459
+ return stableSelectors[0];
5460
+ const sameTagSiblings = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
5461
+ if (sameTagSiblings.length > 1) {
5462
+ return `${tag}:nth-of-type(${getNthOfType(el)})`;
5463
+ }
5464
+ }
5465
+ return tag;
5466
+ };
5467
+ const getShortestPositionalCssPath = (el) => {
5468
+ if (!isElementNode(el) || isIgnoredTag(el))
5469
+ return;
5470
+ const root = el.getRootNode();
5471
+ const descendantSegments = [];
5472
+ let current = el;
5473
+ while (current && isElementNode(current) && !isIgnoredTag(current)) {
5474
+ const anchorSelectors = getDirectStableSelectors(current, {
5475
+ includeTag: true,
5476
+ preferredAttributesOnly: true
5477
+ });
5478
+ for (const anchorSelector of anchorSelectors) {
5479
+ if (!isCssSelectorUnique(anchorSelector, current, root))
5480
+ continue;
5481
+ const candidate = [anchorSelector, ...descendantSegments].join(" > ");
5482
+ if (isCssSelectorUnique(candidate, el, root)) {
5483
+ return candidate;
5484
+ }
5485
+ }
5486
+ descendantSegments.unshift(getRelativeCssSegment(current));
5487
+ current = current.parentElement;
5488
+ }
5489
+ return getAbsoluteCssPath(el);
5490
+ };
5386
5491
  const getAbsoluteCssPath = (el) => {
5387
5492
  const view = el.ownerDocument?.defaultView;
5388
5493
  if (!view || !(el instanceof view.Element))
@@ -5410,6 +5515,7 @@
5410
5515
  getIdCssPath,
5411
5516
  getAttributeCssPath,
5412
5517
  getClassCssPath,
5518
+ getShortestPositionalCssPath,
5413
5519
  getAbsoluteCssPath
5414
5520
  };
5415
5521