assign static method

StableIdAssignment assign(
  1. NormalizedTree tree, {
  2. required String screen,
})

Walk tree and produce a stable ID + structural path for every node.

Sibling slugs are unique per parent. A slug shared by several siblings is disambiguated by CONTENT when the colliding nodes' texts carry data tokens the slug dropped (15 Oct ₹500 paid / 16 Oct ₹200 paid → oct_paid_15_500 / oct_paid_16_200), so the suffix follows the row, not its position in the current window of a lazy list. Every member of such a group takes its content suffix — including the first — so which row happens to be first on screen never decides who owns the bare slug. Remaining collisions (identical texts, role slugs) get _2, _3, … from a per-slug counter: O(n) in total, never a rescan from 2. Nodes whose slug is unique keep exactly the ID they always had.

Implementation

static StableIdAssignment assign(
  NormalizedTree tree, {
  required String screen,
}) {
  final n = tree.nodes;
  final stableIds = List<String>.filled(n.length, '');
  final paths = List<String>.filled(n.length, '');
  final screenSlug = slugify(screen);

  // Pass 1: base slug per node, and how many siblings share it. Sibling
  // namespaces: one per parent, plus the shared overlay namespace.
  final baseSlugs = List<String>.filled(n.length, '');
  final groups = List<int>.filled(n.length, 0);
  final roleCounts = <int, Map<String, int>>{};
  final slugCounts = <int, Map<String, int>>{};
  for (var i = 0; i < n.length; i++) {
    final node = n[i];
    final startsOverlay = node.overlayId > 0 &&
        (node.parentIndex == -1 ||
            n[node.parentIndex].overlayId != node.overlayId);
    final group = startsOverlay ? -2 : node.parentIndex;
    groups[i] = group;
    final roles = roleCounts.putIfAbsent(group, () => <String, int>{});
    final occurrence = roles[node.role] ?? 0;
    roles[node.role] = occurrence + 1;
    final slug = generateStableId(
      parentPath: '',
      role: node.role,
      siblingPosition: occurrence,
      identifier: node.identifier,
      text: node.text,
    );
    baseSlugs[i] = slug;
    final counts = slugCounts.putIfAbsent(group, () => <String, int>{});
    counts[slug] = (counts[slug] ?? 0) + 1;
  }

  // Pass 2: ids, pre-order (a parent's id exists before its children's).
  final usedByParent = <String, Set<String>>{};
  final nextSuffix = <String, Map<String, int>>{};
  for (var i = 0; i < n.length; i++) {
    final node = n[i];
    final parentSibPath =
        node.parentIndex == -1 ? '' : paths[node.parentIndex];
    paths[i] = parentSibPath.isEmpty
        ? '${node.siblingIndex}'
        : '$parentSibPath.${node.siblingIndex}';

    final parentStable = groups[i] == -2
        ? '$screenSlug.overlay'
        : node.parentIndex == -1
            ? screenSlug
            : stableIds[node.parentIndex];
    final used = usedByParent.putIfAbsent(parentStable, () => <String>{});

    var slug = baseSlugs[i];
    if (node.identifier.isEmpty &&
        node.text.isNotEmpty &&
        (slugCounts[groups[i]]![slug] ?? 0) > 1) {
      final d = contentDisambiguator(node.text, node.normalizedText);
      if (d.isNotEmpty) slug = '${slug}_$d';
    }
    if (used.contains(slug)) {
      final counters =
          nextSuffix.putIfAbsent(parentStable, () => <String, int>{});
      var k = counters[slug] ?? 2;
      while (used.contains('${slug}_$k')) {
        k++;
      }
      counters[slug] = k + 1;
      slug = '${slug}_$k';
    }
    used.add(slug);
    stableIds[i] = '$parentStable.$slug';
  }

  return StableIdAssignment(stableIds: stableIds, paths: paths);
}