com.google.gwt.dom.client.LocalDom.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dom.client.LocalDom.java

Source

package com.google.gwt.dom.client;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

import com.google.common.base.Preconditions;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.ElementRemote.ElementRemoteIndex;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.LocalDomDebug;

import cc.alcina.framework.common.client.logic.domaintransform.lookup.JavascriptKeyableLookup;
import cc.alcina.framework.common.client.logic.domaintransform.lookup.JsUniqueMap;
import cc.alcina.framework.common.client.util.Ax;
import cc.alcina.framework.common.client.util.CommonUtils;
import cc.alcina.framework.common.client.util.TopicPublisher.TopicSupport;
import cc.alcina.framework.gwt.client.browsermod.BrowserMod;

/*
 * Refactoring needs - there's a lot of semi-duplication in the 'link remote to localdom' models - i.e. puts to remoteLookup
 * 
 * Probably need just one true path
 */
public class LocalDom {
    private static LocalDom instance = new LocalDom();

    private static LocalDomCollections collections;

    private static Map<String, String> declarativeCssNames;

    // FIXME - ie9?
    public static boolean fastRemoveAll = true;

    private static boolean useRemoteDom = GWT.isClient();

    private static boolean disableRemoteWrite;

    static boolean ie9;

    static boolean emitCommentPisAsText;

    private static final String TOPIC_EXCEPTION = LocalDom.class.getName() + "." + "TOPIC_EXCEPTION";

    static final String TOPIC_UNABLE_TO_PARSE = LocalDom.class.getName() + ".TOPIC_UNABLE_TO_PARSE";

    public static LocalDomMutations mutations;

    public static void debug(ElementRemote elementRemote) {
        get().debug0(elementRemote);
    }

    public static void detach(Node node) {
        // see "removereference"
        // if (node.wasResolved()) {
        // LocalDom instance = get();
        // node.local().walk(nl -> {
        // if (nl.node.linkedToRemote()) {
        // instance.removeReference(nl.node.typedRemote());
        // }
        // });
        // }
    }

    public static void ensureRemote(Node node) {
        get().ensureRemote0(node);
    }

    public static void ensureRemoteDocument() {
        nodeFor(Document.get().typedRemote().getDocumentElement0());
    }

    public static NodeRemote ensureRemoteNodeMaybePendingResolution(Node node) {
        return get().ensureRemoteNodeMaybePendingResolution0(node);
    }

    public static void eventMod(NativeEvent evt, String eventName) {
        get().eventMod0(evt, eventName);
    }

    public static void flush() {
        get().resolve0();
    }

    public static boolean isDisableRemoteWrite() {
        return disableRemoteWrite;
    }

    public static boolean isPending(NodeRemote nodeRemote) {
        return get().isPending0(nodeRemote);
    }

    public static boolean isStopPropogation(NativeEvent evt) {
        return get().isStopPropogation0(evt);
    }

    public static boolean isUseRemoteDom() {
        return useRemoteDom;
    }

    public static void log(LocalDomDebug channel, String message, Object... args) {
        get().debugImpl.log(channel, message, args);
    }

    public static <T extends Node> T nodeFor(JavaScriptObject jso) {
        return nodeFor((NodeRemote) jso);
    }

    public static <T extends Node> T nodeFor(NodeRemote remote) {
        return (T) get().nodeFor0(remote);
    }

    public static void register(Document doc) {
        if (useRemoteDom) {
            get().linkRemote(doc.typedRemote(), doc);
            get().nodeFor0(doc.typedRemote().getDocumentElement0());
            mutations.startObservingIfNotInEventCycle();
        }
    }

    public static Node resolveExternal(NodeRemote nodeRemote) {
        return get().resolveExternal0(nodeRemote);
    }

    public static void setDisableRemoteWrite(boolean disableRemoteWrite) {
        LocalDom.disableRemoteWrite = disableRemoteWrite;
    }

    public static void setUseRemoteDom(boolean useRemoteDom) {
        LocalDom.useRemoteDom = useRemoteDom;
    }

    public static void syncToRemote(Element element) {
        get().parseAndMarkResolved(element.typedRemote(), element.typedRemote().getOuterHtml(), element);
    }

    public static TopicSupport<Exception> topicException() {
        return new TopicSupport<>(TOPIC_EXCEPTION);
    }

    public static TopicSupport<String> unableToParseTopic() {
        return new TopicSupport<>(LocalDom.TOPIC_UNABLE_TO_PARSE);
    }

    public static String validateHtml(String html) {
        return get().validateHtml0(html);
    }

    private static LocalDom get() {
        return instance;
    }

    static LocalDomCollections collections() {
        return collections;
    }

    static void consoleLog(String message) {
        if (LocalDomDebugImpl.debugAll) {
            consoleLog0(message);
        }
    }

    static void consoleLog(Supplier<String> messageSupplier) {
        if (LocalDomDebugImpl.debugAll) {
            consoleLog0(messageSupplier.get());
        }
    }

    native static void consoleLog0(String message) /*-{
                                                   console.log(message);
                                                       
                                                   }-*/;

    static Element createElement(String tagName) {
        return get().createElement0(tagName);
    }

    synchronized static String declarativeCssName(String key) {
        return declarativeCssNames.computeIfAbsent(key, k -> {
            String lcKey = k.toLowerCase();
            if (!lcKey.equals(k)) {
                StringBuilder sb = new StringBuilder();
                for (int idx = 0; idx < k.length(); idx++) {
                    char c = k.charAt(idx);
                    if (c >= 'A' && c <= 'Z') {
                        sb.append("-");
                        sb.append(String.valueOf(c).toLowerCase());
                    } else {
                        sb.append(c);
                    }
                }
                return sb.toString();
            } else {
                return k;
            }
        });
    }

    static boolean hasNode(JavaScriptObject remote) {
        return get().remoteLookup.containsKey(remote);
    }

    static Node nodeForNoResolve(NodeRemote nodeRemote) {
        return get().remoteLookup.get(nodeRemote);
    }

    static void putRemote(Element element, ElementRemote remote) {
        get().putRemote0(element, remote);
    }

    static String safeParseByBrowser(String html) {
        ElementRemote remote = Document.get().typedRemote().generateFromOuterHtml(html);
        return remote.buildOuterHtml();
    }

    static void wasResolved(Element elem) {
        get().wasResolved0(elem);
    }

    LocalDomDebugImpl debugImpl = new LocalDomDebugImpl();

    private Map<NodeRemote, Node> remoteLookup;

    private Map<String, Supplier<Element>> elementCreators;

    Map<NativeEvent, List<String>> eventMods = new LinkedHashMap<>();

    List<Node> pendingResolution = new ArrayList<>();

    ScheduledCommand resolveCommand = null;

    private int resolutionEventId = 1;

    private boolean resolutionEventIdDirty;

    private boolean resolving;

    private boolean useBuiltHtmlValidation;

    public LocalDom() {
        if (GWT.isScript() && JsUniqueMap.supportsJsWeakMap()) {
            remoteLookup = JsUniqueMap.createWeakMap();
        } else {
            remoteLookup = new LinkedHashMap<>();
        }
        ie9 = GWT.isClient() ? BrowserMod.isIE9() : false;
        LocalDom.mutations = GWT.isClient() ? new LocalDomMutations() : null;
        useBuiltHtmlValidation = GWT.isClient() ? BrowserMod.isInternetExplorer() : false;
        emitCommentPisAsText = true;
        if (collections == null) {
            initStatics();
        }
    }

    public void ensurePendingResolved(Node node) {
        Preconditions.checkState(node.linkedToRemote());
        Element element = (Element) node;
        if (element.isPendingResolution()) {
            ElementRemote remote = (ElementRemote) node.remote();
            DomElement local = node.local();
            localToRemote(element, remote, local);
        }
    }

    public Node resolveExternal0(NodeRemote nodeRemote) {
        switch (nodeRemote.getNodeType()) {
        case Node.ELEMENT_NODE:
            ElementRemote elementRemote = (ElementRemote) nodeRemote;
            Element element = Document.get().local().createElement(elementRemote.getTagNameRemote());
            element.putRemote(nodeRemote, false);
            syncToRemote(element);
            linkRemote(elementRemote, element);
            return element;
        case Node.TEXT_NODE:
            Text textNode = Document.get().createTextNode(nodeRemote.getNodeValue());
            textNode.putRemote(nodeRemote, true);
            return textNode;
        default:
            throw new UnsupportedOperationException();
        }
    }

    private Element createElement0(String tagName) {
        Supplier<Element> creator = elementCreators.get(tagName.toLowerCase());
        if (creator == null) {
            return new Element();
        } else {
            return creator.get();
        }
    }

    private void debug0(ElementRemote elementRemote) {
        int debug = 3;
    }

    private void ensureFlush() {
        if (resolveCommand == null) {
            resolveCommand = () -> flush();
            Scheduler.get().scheduleFinally(resolveCommand);
        }
    }

    private void ensureRemote0(Node node) {
        resolve0(true);
        List<Node> ancestors = new ArrayList<>();
        Node cursor = node;
        Node withRemote = null;
        while (cursor != null) {
            if (cursor.linkedToRemote()) {
                withRemote = cursor;
                break;
            } else {
                ancestors.add(cursor);
                cursor = cursor.getParentElement();
            }
        }
        Collections.reverse(ancestors);
        if (withRemote == null) {
            // attaching child with-remote to without-remote (say, a popup)
            Node root = ancestors.get(0);
            ensureRemoteNodeMaybePendingResolution(root);
            ensureRemote0(node);
            return;
        }
        for (Node needsRemote : ancestors) {
            int idx = needsRemote.indexInParentChildren();
            if (needsRemote instanceof Element && withRemote instanceof Element) {
                debugImpl.debugPutRemote((Element) needsRemote, idx, (Element) withRemote);
            }
            NodeRemote remote = withRemote.typedRemote().getChildNodes0().getItem0(idx);
            linkRemote(remote, needsRemote);
            needsRemote.putRemote(remote, true);
            withRemote = needsRemote;
        }
    }

    private NodeRemote ensureRemoteNodeMaybePendingResolution0(Node node) {
        if (node.linkedToRemote()) {
            return node.remote();
        }
        ensureFlush();
        NodeRemote remote = null;
        int nodeType = node.getNodeType();
        switch (nodeType) {
        case Node.ELEMENT_NODE:
            Element element = (Element) node;
            remote = ((DomDispatchRemote) DOMImpl.impl.remote).createElement(element.getTagName());
            element.pendingResolution();
            pendingResolution.add(node);
            log(LocalDomDebug.CREATED_PENDING_RESOLUTION,
                    "created pending resolution node:" + element.getTagName());
            break;
        case Node.TEXT_NODE:
            remote = Document.get().typedRemote().createTextNode0(((Text) node).getData());
            break;
        // case Node.DOCUMENT_NODE:
        // nodeDom = doc.domImpl();
        // break;
        default:
            throw new UnsupportedOperationException();
        }
        linkRemote(remote, node);
        node.putRemote(remote, false);
        return remote;
    }

    private void eventMod0(NativeEvent evt, String eventName) {
        log(LocalDomDebug.EVENT_MOD, Ax.format("eventMod - %s %s", evt, eventName));
        if (!eventMods.keySet().contains(evt)) {
            eventMods.clear();
            eventMods.put(evt, new ArrayList<>());
        }
        eventMods.get(evt).add(eventName);
    }

    private void initElementCreators() {
        elementCreators.put(DivElement.TAG, () -> new DivElement());
        elementCreators.put(SpanElement.TAG, () -> new SpanElement());
        elementCreators.put(BodyElement.TAG, () -> new BodyElement());
        elementCreators.put(ButtonElement.TAG, () -> new ButtonElement());
        elementCreators.put(StyleElement.TAG, () -> new StyleElement());
        elementCreators.put(TableElement.TAG, () -> new TableElement());
        elementCreators.put(HeadElement.TAG, () -> new HeadElement());
        elementCreators.put(TableSectionElement.TAG_TBODY, () -> new TableSectionElement());
        elementCreators.put(TableSectionElement.TAG_TFOOT, () -> new TableSectionElement());
        elementCreators.put(TableSectionElement.TAG_THEAD, () -> new TableSectionElement());
        elementCreators.put(TableCaptionElement.TAG, () -> new HeadElement());
        elementCreators.put(TableCellElement.TAG_TD, () -> new TableCellElement());
        elementCreators.put(TableCellElement.TAG_TH, () -> new TableCellElement());
        elementCreators.put(TableColElement.TAG_COL, () -> new TableColElement());
        elementCreators.put(TableColElement.TAG_COLGROUP, () -> new TableColElement());
        elementCreators.put(TableRowElement.TAG, () -> new TableRowElement());
        elementCreators.put(InputElement.TAG, () -> new InputElement());
        elementCreators.put(TextAreaElement.TAG, () -> new TextAreaElement());
        elementCreators.put(HeadingElement.TAG_H1, () -> new HeadingElement());
        elementCreators.put(HeadingElement.TAG_H2, () -> new HeadingElement());
        elementCreators.put(HeadingElement.TAG_H3, () -> new HeadingElement());
        elementCreators.put(HeadingElement.TAG_H4, () -> new HeadingElement());
        elementCreators.put(HeadingElement.TAG_H5, () -> new HeadingElement());
        elementCreators.put(HeadingElement.TAG_H6, () -> new HeadingElement());
        elementCreators.put(AnchorElement.TAG, () -> new AnchorElement());
        elementCreators.put(ImageElement.TAG, () -> new ImageElement());
        elementCreators.put(LabelElement.TAG, () -> new LabelElement());
        elementCreators.put(ScriptElement.TAG, () -> new ScriptElement());
        elementCreators.put(SelectElement.TAG, () -> new SelectElement());
        elementCreators.put(OptionElement.TAG, () -> new OptionElement());
        elementCreators.put(IFrameElement.TAG, () -> new IFrameElement());
        elementCreators.put(UListElement.TAG, () -> new UListElement());
        elementCreators.put(OListElement.TAG, () -> new OListElement());
        elementCreators.put(LIElement.TAG, () -> new LIElement());
        elementCreators.put(PreElement.TAG, () -> new PreElement());
        elementCreators.put(ParagraphElement.TAG, () -> new ParagraphElement());
        elementCreators.put(BRElement.TAG, () -> new BRElement());
        elementCreators.put(HRElement.TAG, () -> new HRElement());
        elementCreators.put(FormElement.TAG, () -> new FormElement());
        // svg
        elementCreators.put(MapElement.TAG, () -> new MapElement());
        elementCreators.put(ParamElement.TAG, () -> new ParamElement());
        elementCreators.put(OptGroupElement.TAG, () -> new OptGroupElement());
        elementCreators.put(QuoteElement.TAG_BLOCKQUOTE, () -> new QuoteElement());
        elementCreators.put(QuoteElement.TAG_Q, () -> new QuoteElement());
        elementCreators.put(TableCaptionElement.TAG, () -> new TableCaptionElement());
        elementCreators.put(DListElement.TAG, () -> new DListElement());
        elementCreators.put(TitleElement.TAG, () -> new TitleElement());
        elementCreators.put(FieldSetElement.TAG, () -> new FieldSetElement());
        elementCreators.put(FrameSetElement.TAG, () -> new FrameSetElement());
        elementCreators.put(MetaElement.TAG, () -> new MetaElement());
        elementCreators.put(SourceElement.TAG, () -> new SourceElement());
        elementCreators.put(LinkElement.TAG, () -> new LinkElement());
        elementCreators.put(ObjectElement.TAG, () -> new ObjectElement());
        elementCreators.put(ModElement.TAG_INS, () -> new ModElement());
        elementCreators.put(ModElement.TAG_DEL, () -> new ModElement());
        elementCreators.put(BaseElement.TAG, () -> new BaseElement());
        elementCreators.put(FrameElement.TAG, () -> new FrameElement());
        elementCreators.put(AreaElement.TAG, () -> new AreaElement());
        elementCreators.put(LegendElement.TAG, () -> new LegendElement());
    }

    private void initStatics() {
        if (GWT.isScript()) {
            JavascriptKeyableLookup.initJs();
            collections = new LocalDomCollections_Script();
        } else {
            collections = new LocalDomCollections();
        }
        declarativeCssNames = collections.createStringMap();
        elementCreators = collections.createIdentityEqualsMap(String.class);
        initElementCreators();
    }

    private boolean isPending0(NodeRemote nodeRemote) {
        return pendingResolution.size() > 0 && pendingResolution.stream().anyMatch(n -> n.remote() == nodeRemote);
    }

    private boolean isStopPropogation0(NativeEvent evt) {
        List<String> list = eventMods.get(evt);
        return list != null && (list.contains("eventStopPropagation") || list.contains("eventCancelBubble"));
    }

    private void linkRemote(NodeRemote remote, Node node) {
        Preconditions.checkState(!remoteLookup.containsKey(remote));
        remoteLookup.put(remote, node);
    }

    private void localToRemote(Element element, ElementRemote remote, DomElement local) {
        String innerHTML = local.getInnerHTML();
        if (ie9) {
            switch (element.getTagName()) {
            case "table":
                remote = writeIe9Table((TableElement) element, remote);
                break;
            case "tr":
                remote = writeIe9Tr((TableRowElement) element, remote);
                break;
            default:
                remote.setInnerHTML(innerHTML);
            }
        } else {
            remote.setInnerHTML(innerHTML);
        }
        log(LocalDomDebug.RESOLVE, "%s - uiobj: %s - \n%s", element.getTagName(),
                Optional.ofNullable(element.uiObject).map(ui -> ui.getClass().getSimpleName()).orElse("(null)"),
                CommonUtils.trimToWsChars(innerHTML, 1000));
        ElementRemote f_remote = remote;
        // doesn't include style
        local.getAttributes().entrySet().forEach(e -> {
            switch (e.getKey()) {
            case "text":
                f_remote.setPropertyString(e.getKey(), e.getValue());
                break;
            default:
                f_remote.setAttribute(e.getKey(), e.getValue());
                break;
            }
        });
        local.getStyle().getProperties().entrySet().forEach(e -> {
            StyleRemote remoteStyle = f_remote.getStyle0();
            remoteStyle.setProperty(e.getKey(), e.getValue());
        });
        int bits = ((ElementLocal) local).orSunkEventsOfAllChildren(0);
        bits |= DOM.getEventsSunk(element);
        DOM.sinkEvents(element, bits);
        pendingResolution.remove(element);
        element.resolvePending();
        wasResolved0(element);
    }

    private <T extends Node> T nodeFor0(NodeRemote remote) {
        return nodeFor0(remote, false);
    }

    private <T extends Node> T nodeFor0(NodeRemote remote, boolean postReparse) {
        try {
            return nodeFor1(remote, postReparse);
        } catch (RuntimeException re) {
            topicException().publish(re);
            throw re;
        }
    }

    private <T extends Node> T nodeFor1(NodeRemote remote, boolean postReparse) {
        if (remote == null) {
            return null;
        }
        T node = (T) remoteLookup.get(remote);
        if (node != null) {
            return node;
        }
        if (remote.provideIsText()) {
            // FIXME - non-performant, but rare (exception for selectionish)
            ElementRemote parentRemote = (ElementRemote) remote.getParentNodeRemote();
            Node parent = nodeFor0(parentRemote);
            int index = remote.indexInParentChildren();
            if (parent.getChildCount() == parentRemote.getChildCount()) {
                Node childNode = parent.getChild(index);
                linkRemote(remote, childNode);
                childNode.putRemote(remote, true);
                return (T) childNode;
            } else {
                if (postReparse) {
                    unableToParseTopic().publish(Ax.format("Text node reparse - remote:\n%s\n\nlocal:\n%s\n",
                            parentRemote.getOuterHtml(), ((Element) parent).getOuterHtml()));
                    throw new RuntimeException("Text node reparse");
                }
                ElementRemoteIndex remoteIndex = parentRemote.provideRemoteIndex(false);
                ElementRemote hasNodeRemote = remoteIndex.hasNode();
                reparseFromRemote(hasNodeRemote, (Element) parent, remoteIndex);
                return nodeFor0(remote, true);
            }
        }
        if (!remote.provideIsElement()) {
            return null;// say, shadowroot...
        }
        ElementRemote elem = (ElementRemote) remote;
        // if (elem.getTagNameRemote().equalsIgnoreCase("iframe")) {
        // return null;// don't handle iframes
        // }
        ElementRemoteIndex remoteIndex = elem.provideRemoteIndex(false);
        ElementRemote hasNodeRemote = remoteIndex.hasNode();
        boolean hadNode = hasNodeRemote != null;
        if (hasNodeRemote == null) {
            ElementRemote root = remoteIndex.root();
            Element hasNode = parseAndMarkResolved(root, root.getOuterHtml(), null);
            linkRemote(root, hasNode);
            hasNode.putRemote(root, true);
            hasNodeRemote = root;
        }
        Element hasNode = (Element) remoteLookup.get(hasNodeRemote);
        // if this returns true, we knew the remote element has DOM manipulated
        // outside GWT - parse the tree
        if (hasNode.resolveRemoteDefined()) {
            return nodeFor0(remote);
        }
        // htmlparser will sometimes fail to parse dodgy DOM - reparse from
        // browser DOM
        if (shouldTryReparseFromRemote(elem, hasNode, remoteIndex) && !postReparse) {
            reparseFromRemote(hasNodeRemote, hasNode, remoteIndex);
            return nodeFor0(remote, true);
        }
        List<Integer> indicies = remoteIndex.indicies();
        List<Boolean> remoteDefined = remoteIndex.remoteDefined();
        JsArray ancestors = remoteIndex.ancestors();
        debugImpl.debugNodeFor(elem, hasNode, remoteIndex, true);
        Element cursor = hasNode;
        for (int idx = indicies.size() - 1; idx >= 0; idx--) {
            int nodeIndex = indicies.get(idx);
            cursor.resolveRemoteDefined();
            Element child = (Element) cursor.getChild(nodeIndex);
            NodeRemote childRemote = (NodeRemote) ancestors.get(idx);
            linkRemote(childRemote, child);
            child.putRemote(childRemote, true);
            cursor = child;
        }
        debugImpl.debugNodeFor(elem, hasNode, remoteIndex, false);
        return (T) remoteLookup.get(remote);
    }

    private Element parseAndMarkResolved(ElementRemote root, String outerHtml, Element replaceContents) {
        Element parsed = null;
        try {
            parsed = new HtmlParser().parse(outerHtml, replaceContents,
                    root == Document.get().typedRemote().getDocumentElement0());
        } catch (Exception e) {
            // TODO - possibly log. But maybe not - full support of dodgy dom wd
            // be truly hard
            parsed = new HtmlParser().parse(safeParseByBrowser(outerHtml), replaceContents,
                    root == Document.get().typedRemote().getDocumentElement0());
        }
        if (parsed != null) {
            wasResolved0(parsed);
        } else {
            unableToParseTopic().publish(outerHtml);
        }
        return parsed;
    }

    private void putRemote0(Element element, ElementRemote remote) {
        flush();
        resolutionEventId++;
        wasResolved(element);
        remoteLookup.put(remote, element);
        element.putRemote(remote, true);
    }

    @SuppressWarnings("unused")
    private void removeReference(NodeRemote typedRemote) {
        // unfortunately doesn't work - if node is later reattached
        // FIXME - do remove, but add a unique 'reattach id' field
        // that way we at least lose the remote node ref
        // also, have a say 30 sec deallocate loop. this is the urkiest of
        // localdom
        // note that weakmaps would save'us'ere
        // Node node = get().remoteLookup.get(typedRemote);
        // if (node != null && node.provideIsElement()) {
        // Element element = (Element) node;
        // pendingResolution.remove(element);
        // }
        // get().remoteLookup.remove(typedRemote);
        //
        // ended up using weakmaps...
    }

    private void reparseFromRemote(ElementRemote elem, Element hasNode, ElementRemoteIndex remoteIndex) {
        List<Integer> sizes = remoteIndex.sizes();
        List<Integer> indicies = remoteIndex.indicies();
        boolean sizesMatch = true;
        Element cursor = hasNode;
        ElementRemote remoteCursor = elem;
        for (int idx = sizes.size() - 1; idx >= 0; idx--) {
            int size = sizes.get(idx);
            boolean invalid = cursor.getChildCount() != size;
            Node node = null;
            NodeRemote remoteNode = null;
            if (!invalid) {
                int nodeIndex = indicies.get(idx);
                node = cursor.getChild(nodeIndex);
                remoteNode = remoteCursor.getChildNodes0().getItem0(nodeIndex);
                invalid = node.getNodeType() != remoteNode.getNodeType();
            }
            if (invalid) {
                // FIXME - check we have no widgets in the tree - if we do,
                // we're...not..good. Also remove and remote refs below (albeit
                // unlikely)
                int localIndex = (cursor.getParentElement() == null ? cursor : cursor.getParentElement())
                        .getChildIndexLocal(cursor);
                cursor.local().clearChildrenAndAttributes0();
                String builtOuterHtml = remoteCursor.buildOuterHtml();
                String remoteOuterHtml = remoteCursor.getOuterHtml();
                parseAndMarkResolved(remoteCursor, builtOuterHtml, cursor);
                invalid = cursor.getChildCount() != size;
                if (!invalid) {
                    int nodeIndex = indicies.get(idx);
                    node = cursor.getChild(nodeIndex);
                    remoteNode = remoteCursor.getChildNodes0().getItem0(nodeIndex);
                    invalid = node.getNodeType() != remoteNode.getNodeType();
                }
                if (invalid) {
                    String preface = Ax.format(
                            "sizes: %s\nsizeIdx:%s\nlocalIndex: %s\n"
                                    + "(local) cursor.childCount: %s\nremoteSize:%s\n",
                            sizes, idx, localIndex, cursor.getChildCount(), size);
                    if (cursor.getChildCount() != size) {
                        preface += "size mismatch\n";
                    } else {
                        int nodeIndex = indicies.get(idx);
                        node = cursor.getChild(nodeIndex);
                        remoteNode = remoteCursor.getChildNodes0().getItem0(nodeIndex);
                        preface += Ax.format("local node:%s\nremote node:%s\n", node, remoteNode);
                    }
                    try {
                        preface += Ax.format("Remote index:\n%s\n", remoteIndex.getString());
                    } catch (Exception e) {
                        preface += Ax.format("Exception getting remoteIndex:\n%s\n", e.toString());
                    }
                    preface += Ax.format("Local dom tree:\n%s\n", hasNode.local().provideLocalDomTree());
                    preface += Ax.format("(Local outer html):\n%s\n", hasNode.local().getOuterHtml());
                    String message = Ax.format("%s\n(Built outer html):\n%s\n\n(Remote outer html):\n%s", preface,
                            builtOuterHtml, remoteOuterHtml);
                    unableToParseTopic().publish(message);
                    Ax.out("Reparse unsuccessful");
                    return;
                }
            }
            cursor = (Element) node;
            remoteCursor = (ElementRemote) remoteNode;
        }
        Ax.out("Reparse successful");
    }

    private boolean shouldTryReparseFromRemote(ElementRemote elem, Element hasNode,
            ElementRemoteIndex remoteIndex) {
        if (remoteIndex.hasRemoteDefined()) {
            return false;
        }
        List<Integer> sizes = remoteIndex.sizes();
        List<Integer> indicies = remoteIndex.indicies();
        boolean sizesMatch = true;
        Element cursor = hasNode;
        for (int idx = sizes.size() - 1; idx >= 0; idx--) {
            int size = sizes.get(idx);
            if (cursor.getChildCount() != size) {
                sizesMatch = false;
                break;
            }
            int nodeIndex = indicies.get(idx);
            Node node = cursor.getChild(nodeIndex);
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                return true;
            }
            cursor = (Element) node;
        }
        if (sizesMatch) {
            return false;
        }
        return true;
    }

    private String validateHtml0(String html) {
        DivElement div = Document.get().createDivElement();
        ensureRemote(div);
        ElementRemote typedRemote = div.implAccess().typedRemote();
        typedRemote.setInnerHTML(html);
        try {
            if (useBuiltHtmlValidation) {
                String outerHtml = typedRemote.buildOuterHtml();
                RegExp regexp = RegExp.compile("^<div>(.+)</div>$", "i");
                MatchResult exec = regexp.exec(outerHtml);
                return exec.getGroup(1);
            } else {
                return typedRemote.getInnerHTML0();
            }
        } catch (Exception e) {
            topicException().publish(e);
            return html;
        }
    }

    private void wasResolved0(Element elem) {
        elem.local().walk(nl -> nl.node.resolved(resolutionEventId));
        resolutionEventIdDirty = true;
    }

    private ElementRemote writeIe9Table(TableElement elem, ElementRemote remote) {
        String outer = elem.local().getOuterHtml();
        remoteLookup.remove(remote);
        remote = Document.get().typedRemote().generateFromOuterHtml(outer);
        elem.replaceRemote(remote);
        return remote;
    }

    private ElementRemote writeIe9Tr(TableRowElement elem, ElementRemote remote) {
        Preconditions.checkArgument(elem.getChildNodes().stream().allMatch(n -> n.getNodeName().equals("td")));
        int idx = 0;
        for (Node node : elem.getChildNodes()) {
            Element child = (Element) node;
            ElementRemote remoteCell = elem.insertCellRemote(idx);
            localToRemote(child, remoteCell, child.local());
            idx++;
        }
        return remote;
    }

    void resolve0() {
        resolve0(false);
    }

    void resolve0(boolean force) {
        if (resolving) {
            return;
        }
        if (resolveCommand == null && !force) {
            return;
        }
        resolveCommand = null;
        log(LocalDomDebug.RESOLVE, "**resolve**");
        try {
            resolving = true;
            if (resolutionEventIdDirty) {
                resolutionEventId++;
                resolutionEventIdDirty = false;
            }
            new ArrayList<>(pendingResolution).stream().forEach(this::ensurePendingResolved);
            if (resolutionEventIdDirty) {
                resolutionEventId++;
            }
        } catch (RuntimeException re) {
            topicException().publish(re);
            throw re;
        } finally {
            resolutionEventIdDirty = false;
            resolving = false;
        }
    }

    public static class LocalDomCollections {
        public <K, V> Map<K, V> createIdentityEqualsMap(Class<K> keyClass) {
            return new LinkedHashMap<>();
        }

        public Map<String, String> createStringMap() {
            return createIdentityEqualsMap(String.class);
        }
    }

    public static class LocalDomCollections_Script extends LocalDomCollections {
        @Override
        public <K, V> Map<K, V> createIdentityEqualsMap(Class<K> keyClass) {
            if (JsUniqueMap.supportsJsMap()) {
                // element.attributes will need entryset, not yet supported in
                // nativemap
                return JsUniqueMap.create(keyClass, keyClass != String.class);
            } else {
                return super.createIdentityEqualsMap(keyClass);
            }
        }
    }
}