onSetOuterHTML method

void onSetOuterHTML(
  1. int? id,
  2. Map<String, dynamic> params
)

https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-setOuterHTML Sets outer HTML for an element. Simplified parser that supports tag, attributes, and plain text content. Nested markup in content will be treated as text.

Implementation

void onSetOuterHTML(int? id, Map<String, dynamic> params) {
  final ctx = dbgContext;
  if (ctx == null) {
    sendToFrontend(id, null);
    return;
  }
  final int? nodeId = params['nodeId'];
  final String? outer = params['outerHTML'];
  if (nodeId == null || outer == null) {
    sendToFrontend(id, null);
    return;
  }

  final targetId = ctx.getTargetIdByNodeId(nodeId);
  if (targetId == null || targetId == 0) {
    sendToFrontend(id, null);
    return;
  }
  final Node? baseNode = ctx.getBindingObject(Pointer.fromAddress(targetId)) as Node?;
  if (baseNode is! Element) {
    sendToFrontend(id, null);
    return;
  }

  String? tag;
  String attrs = '';
  String content = '';
  final String s = outer.trim();
  if (s.startsWith('<')) {
    int i = 1;
    while (i < s.length && isAsciiWhitespaceCodeUnit(s.codeUnitAt(i))) i++;
    final int tagStart = i;
    while (i < s.length) {
      final int cu = s.codeUnitAt(i);
      final bool isNameChar = (cu >= 0x30 && cu <= 0x39) ||
          (cu >= 0x41 && cu <= 0x5A) ||
          (cu >= 0x61 && cu <= 0x7A) ||
          cu == 0x2D;
      if (!isNameChar) break;
      i++;
    }
    if (i > tagStart) {
      tag = s.substring(tagStart, i);
      // Scan to end of start tag, respecting quotes.
      String? quote;
      bool escape = false;
      int startTagEnd = -1;
      for (int j = i; j < s.length; j++) {
        final String ch = s[j];
        if (quote != null) {
          if (escape) {
            escape = false;
          } else if (ch == '\\') {
            escape = true;
          } else if (ch == quote) {
            quote = null;
          }
          continue;
        }
        if (ch == '"' || ch == '\'') {
          quote = ch;
          continue;
        }
        if (ch == '>') {
          startTagEnd = j;
          break;
        }
      }
      if (startTagEnd != -1) {
        // Determine self-closing.
        int k = startTagEnd - 1;
        while (k >= 0 && isAsciiWhitespaceCodeUnit(s.codeUnitAt(k))) k--;
        final bool isSelfClosing = k >= 0 && s.codeUnitAt(k) == 0x2F /* / */;
        final int attrsEnd = isSelfClosing ? k : startTagEnd;
        attrs = s.substring(i, attrsEnd).trim();

        if (isSelfClosing) {
          content = '';
        } else {
          // Find the closing tag at the end.
          int closeStart = s.lastIndexOf('</');
          if (closeStart != -1 && closeStart > startTagEnd) {
            int p = closeStart + 2;
            while (p < s.length && isAsciiWhitespaceCodeUnit(s.codeUnitAt(p))) p++;
            final int closeNameStart = p;
            while (p < s.length) {
              final int cu = s.codeUnitAt(p);
              final bool isNameChar = (cu >= 0x30 && cu <= 0x39) ||
                  (cu >= 0x41 && cu <= 0x5A) ||
                  (cu >= 0x61 && cu <= 0x7A) ||
                  cu == 0x2D;
              if (!isNameChar) break;
              p++;
            }
            final String closeName = (p > closeNameStart) ? s.substring(closeNameStart, p) : '';
            if (closeName.isNotEmpty && closeName.toLowerCase() == tag.toLowerCase()) {
              content = s.substring(startTagEnd + 1, closeStart);
            }
          }
        }
      }
    }
  }

  if (tag == null || tag.isEmpty) {
    // Fallback: ignore invalid markup
    sendToFrontend(id, null);
    return;
  }

  final controller = ctx.getController() ?? devtoolsService.controller;
  if (controller == null) {
    sendToFrontend(id, null);
    return;
  }

  // Prepare working element reference; rename if needed and update nodeId
  Element workingEl = baseNode;
  int workingNodeId = nodeId;
  if (baseNode.tagName.toLowerCase() != tag.toLowerCase()) {
    final newPtr = allocateNewBindingObject();
    try {
      controller.view.createElement(newPtr, tag);
      // Copy attributes/inline style
      controller.view.cloneNode(baseNode.pointer!, newPtr);
      final Element? newEl = ctx.getBindingObject(newPtr) as Element?;
      if (newEl != null) {
        // Move children from old to new
        while (baseNode.firstChild != null) {
          newEl.appendChild(baseNode.firstChild!);
        }
        // Insert new next to old and remove old
        controller.view.insertAdjacentNode(baseNode.pointer!, 'afterend', newPtr);
        controller.view.removeNode(baseNode.pointer!);
        workingEl = newEl;
        workingNodeId = ctx.forDevtoolsNodeId(newEl);
      }
    } catch (_) {
      // fall back: keep baseNode
    }
  }

  // Apply attributes
  if (attrs.isNotEmpty) {
    onSetAttributesAsText(null, {'nodeId': workingNodeId, 'text': attrs});
  } else {
    // Clear attributes (preserve id if present in original) – keep it simple: leave as-is when empty
  }

  // Replace children with plain text content
  try {
    // Remove all existing children via view bridge to emit events
    while (workingEl.firstChild != null) {
      final child = workingEl.firstChild!;
      controller.view.removeNode(child.pointer!);
    }
  } catch (_) {
    // Fallback: direct removal
    while (workingEl.firstChild != null) {
      workingEl.removeChild(workingEl.firstChild!);
    }
  }

  final trimmed = content.trim();
  if (trimmed.isNotEmpty) {
    // If content contains markup, treat as text
    final textPtr = allocateNewBindingObject();
    controller.view.createTextNode(textPtr, trimmed);
    try {
      controller.view.insertAdjacentNode(workingEl.pointer!, 'beforeend', textPtr);
    } catch (_) {
      final tnode = ctx.getBindingObject(textPtr) as Node?;
      if (tnode != null) workingEl.appendChild(tnode);
    }
  }

  sendToFrontend(id, null);
}