docs: public XStreamDOM expandMacro(VariableResolver<String> vars) {
          String[] newAttributes = new String[attributes.length];
          for (int i=0; i<attributes.length; i+=2) {
              //noinspection PointlessArithmeticExpression
              newAttributes[i+0] = attributes[i]; // name
              newAttributes[i+1] = Util.replaceMacro(attributes[i+1],vars);
          }
  
          List<XStreamDOM> newChildren = null;
          if (children!=null) {
              newChildren = new ArrayList<>(children.size());
              for (XStreamDOM d : children)
                  newChildren.add(d.expandMacro(vars));
          }
  
          return new XStreamDOM(tagName,newAttributes,newChildren,Util.replaceMacro(value,vars));
      }
  
      public String getAttribute(String name) {
          for (int i=0; i<attributes.length; i+=2)
              if (attributes[i].equals(name))
                  return attributes[i+1];
          return null;
      }
  
      public int getAttributeCount() {
          return attributes.length / 2;
      }
  
      String getAttributeName(int index) {
          return attributes[index*2];
      }
  
      public String getAttribute(int index) {
          return attributes[index*2+1];
      }
  
      public String getValue() {
          return value;
      }
  
      public List<XStreamDOM> getChildren() {
          return children;
      }
  
      /**
       * Returns a new {@link HierarchicalStreamReader} that reads a sub-tree rooted at this node.
       */
      public HierarchicalStreamReader newReader() {
          return new ReaderImpl(this);
      }
  
      /**
       * Returns a new {@link HierarchicalStreamWriter} for marshalling objects into {@link XStreamDOM}.
       * After the writer receives the calls, call {@link WriterImpl#getOutput()} to obtain the populated tree.
       */
      public static WriterImpl newWriter() {
          return new WriterImpl();
      }
  
      /**
       * Writes this {@link XStreamDOM} into {@link OutputStream}.
       */
      public void writeTo(OutputStream os) {
          writeTo(XStream2.getDefaultDriver().createWriter(os));
      }
  
      public void writeTo(Writer w) {
          writeTo(XStream2.getDefaultDriver().createWriter(w));
      }
  
      public void writeTo(HierarchicalStreamWriter w) {
          new ConverterImpl().marshal(this,w,null);
      }
  
      /**
       * Marshals the given object with the given XStream into {@link XStreamDOM} and return it.
       */
      public static XStreamDOM from(XStream xs, Object obj) {
          WriterImpl w = newWriter();
          xs.marshal(obj, w);
          return w.getOutput();
      }
  
      public static XStreamDOM from(InputStream in) {
          return from(XStream2.getDefaultDriver().createReader(in));
      }
  
      public static XStreamDOM from(Reader in) {
          return from(XStream2.getDefaultDriver().createReader(in));
      }
  
      public static XStreamDOM from(HierarchicalStreamReader in) {
          return new ConverterImpl().unmarshalElement(in, null);
      }
  
      public Map<String, String> getAttributeMap() {
          Map<String,String> r = new HashMap<>();
          for (int i=0; i<attributes.length; i+=2)
              r.put(attributes[i],attributes[i+1]);
          return r;
      }
  
      private static class ReaderImpl extends AbstractXmlReader implements DocumentReader {
          private static class Pointer {
              final XStreamDOM node;
              int pos;
  
              private Pointer(XStreamDOM node) {
                  this.node = node;
              }
  
              public String peekNextChild() {
                  if (hasMoreChildren())
                      return node.children.get(pos).tagName;
                  return null;
              }
  
              public boolean hasMoreChildren() {
                  return node.children!=null && pos<node.children.size();
              }
  
              public String xpath() {
                  XStreamDOM child = node.children.get(pos - 1);
                  int count =0;
                  for (int i=0; i<pos-1; i++)
                      if (node.children.get(i).tagName.equals(child.tagName))
                          count++;
                  boolean more = false;
                  for (int i=pos; !more && i<node.children.size(); i++)
                      if (node.children.get(i).tagName.equals(child.tagName))
                          more = true;
  
                  if (count==0 && !more)  return child.tagName;   // sole child
                  return child.tagName+'['+count+']';
              }
          }
  
          private final Stack<Pointer> pointers = new Stack<>();
  
  
          public ReaderImpl(XStreamDOM current) {
              super(new XmlFriendlyReplacer());
              pointers.push(new Pointer(current));
          }
  
          private Pointer current() {
              return pointers.peek();
          }
  
          public Object getCurrent() {
              return current().node;
          }
  
          public boolean hasMoreChildren() {
              return current().hasMoreChildren();
          }
  
          public HierarchicalStreamReader underlyingReader() {
              return this;
          }
  
          public void moveDown() {
              Pointer p = current();
              pointers.push(new Pointer(p.node.children.get(p.pos++)));
          }
  
          public void moveUp() {
              pointers.pop();
          }
  
          public Iterator getAttributeNames() {
              return new AttributeNameIterator(this);
          }
  
          public void appendErrors(ErrorWriter errorWriter) {
              StringBuilder buf = new StringBuilder();
              Pointer parent = null;
              for (Pointer cur : pointers) {
                  if (parent!=null) {
                      buf.append('/').append(parent.xpath());
                  } else {
                      buf.append(cur.node.tagName);
                  }
                  parent = cur;
              }
              errorWriter.add("xpath", buf.toString());
          }
  
          public void close() {
          }
  
          public String peekNextChild() {
              return current().peekNextChild();
          }
  
          public String getNodeName() {
              return unescapeXmlName(current().node.tagName);
          }
  
          public String getValue() {
              return Util.fixNull(current().node.value);
          }
  
          public String getAttribute(String name) {
              return current().node.getAttribute(name);
          }
  
          public String getAttribute(int index) {
              return current().node.getAttribute(index);
          }
  
          public int getAttributeCount() {
              return current().node.getAttributeCount();
          }
  
          public String getAttributeName(int index) {
              return unescapeXmlName(current().node.getAttributeName(index));
          }
      }
  
      public static class WriterImpl extends AbstractXmlWriter {
          private static class Pending {
              final String tagName;
              List<XStreamDOM> children;
              List<String> attributes = new ArrayList<String>();
              String value;
  
              private Pending(String tagName) {
                  this.tagName = tagName;
              }
  
              void addChild(XStreamDOM dom) {
                  if (children==null)
                      children = new ArrayList<XStreamDOM>();
                  children.add(dom);
              }
  
              XStreamDOM toDOM() {
                  return new XStreamDOM(tagName,attributes.toArray(new String[attributes.size()]),children,value);
              }
          }
  
          private final Stack<Pending> pendings = new Stack<Pending>();
  
          public WriterImpl() {
              pendings.push(new Pending(null));   // to get the final result
          }
  
          public void startNode(String name) {
              pendings.push(new Pending(escapeXmlName(name)));
          }
  
  
  
          public void endNode() {
              XStreamDOM dom = pendings.pop().toDOM();
              pendings.peek().addChild(dom);
          }
  
          public void addAttribute(String name, String value) {
              List<String> atts = pendings.peek().attributes;
              atts.add(escapeXmlName(name));
              atts.add(value);
          }
  
          public void setValue(String text) {
              pendings.peek().value = text;
          }
  
          public void flush() {
          }
  
          public void close() {
          }
  
          public HierarchicalStreamWriter underlyingWriter() {
              return this;
          }
  
          public XStreamDOM getOutput() {
              if (pendings.size()!=1)     throw new IllegalStateException();
              return pendings.peek().children.get(0);
          }
      }
  
      public static class ConverterImpl implements Converter {
          public boolean canConvert(Class type) {
              return type==XStreamDOM.class;
          }
  
          /**
           * {@link XStreamDOM} holds infoset (which is 'escaped' from XStream's PoV),
           * whereas {@link HierarchicalStreamWriter} expects unescaped names,
           * so we need to unescape it first before calling into {@link HierarchicalStreamWriter}.
           */
          // TODO: ideally we'd like to use the contextual HierarchicalStreamWriter to unescape,
          // but this object isn't exposed to us
          private String unescape(String s) {
              return REPLACER.unescapeName(s);
          }
  
          private String escape(String s) {
              return REPLACER.escapeName(s);
          }
  
          public void marshal(Object source, HierarchicalStreamWriter w, MarshallingContext context) {
              XStreamDOM dom = (XStreamDOM)source;
              w.startNode(unescape(dom.tagName));
              for (int i=0; i<dom.attributes.length; i+=2)
                  w.addAttribute(unescape(dom.attributes[i]),dom.attributes[i+1]);
              if (dom.value!=null)
                  w.setValue(dom.value);
              else {
                  for (XStreamDOM c : Util.fixNull(dom.children)) {
                      marshal(c, w, context);
                  }
              }
              w.endNode();
          }
  
          /**
           * Unmarshals a single child element.
           */
          public XStreamDOM unmarshal(HierarchicalStreamReader r, UnmarshallingContext context) {
              r.moveDown();
              XStreamDOM dom = unmarshalElement(r,context);
              r.moveUp();
              return dom;
          }
  
          public XStreamDOM unmarshalElement(HierarchicalStreamReader r, UnmarshallingContext context) {
              String name = escape(r.getNodeName());
  
              int c = r.getAttributeCount();
              String[] attributes = new String[c*2];
              for (int i=0; i<c; i++) {
                  attributes[i*2]   = escape(r.getAttributeName(i));
                  attributes[i*2+1] = r.getAttribute(i);
              }
  
              List<XStreamDOM> children = null;
              String value = null;
              if (r.hasMoreChildren()) {
                  children = new ArrayList<XStreamDOM>();
                  while (r.hasMoreChildren()) {
                      children.add(unmarshal(r, context));
                  }
              } else {
                  value = r.getValue();
              }
  
              return new XStreamDOM(name,attributes,children,value);
          }
      }
  
      public static XmlFriendlyReplacer REPLACER = new XmlFriendlyReplacer();
  }