--- /dev/null
+package jrw.sp;
+
+import jrw.util.*;
+import java.util.*;
+
+public class Formatter extends LazyPChannel {
+ private final Element root;
+ private final String header;
+ private final List<Frame> stack = new ArrayList<>();
+ private final Map<Namespace, String> ns = new IdentityHashMap<>();
+ private boolean headed = false;
+
+ class Frame {
+ Element el;
+ Iterator<Map.Entry<Name, String>> ai;
+ Iterator<Node> ci;
+ boolean sh;
+ boolean h, e, t;
+
+ Frame(Element el) {
+ this.el = el;
+ this.ai = el.attribs.entrySet().iterator();
+ this.ci = el.children.iterator();
+ this.sh = shorten(el);
+ }
+ }
+
+ private void countns(Map<Namespace, Integer> freq, Set<Namespace> attrs, Element el) {
+ for(Name anm : el.attribs.keySet()) {
+ if(anm.ns != null) {
+ attrs.add(anm.ns);
+ Integer f = freq.get(anm.ns);
+ freq.put(anm.ns, ((f == null) ? 0 : f) + 1);
+ }
+ }
+ Integer f = freq.get(el.name.ns);
+ freq.put(el.name.ns, ((f == null) ? 0 : f) + 1);
+ for(Node ch : el.children) {
+ if(ch instanceof Element)
+ countns(freq, attrs, (Element)ch);
+ }
+ }
+
+ private void calcnsnames() {
+ Map<Namespace, Integer> freq = new IdentityHashMap<>();
+ Set<Namespace> attrs = new HashSet<>();
+ countns(freq, attrs, root);
+ if(freq.get(null) != null) {
+ ns.put(null, null);
+ freq.remove(null);
+ } else if(!attrs.contains(root.name.ns)) {
+ ns.put(root.name.ns, null);
+ freq.remove(root.name.ns);
+ }
+ List<Namespace> order = new ArrayList<>(freq.keySet());
+ Collection<String> ass = new HashSet<>();
+ ass.add(null);
+ Collections.sort(order, (x, y) -> (freq.get(y) - freq.get(x)));
+ for(Namespace ns : order) {
+ String p = ns.prefabb;
+ if((p != null) && !ass.contains(p)) {
+ this.ns.put(ns, p);
+ ass.add(p);
+ } else {
+ int i;
+ if(p == null) {
+ p = "ns";
+ i = 1;
+ } else {
+ i = 2;
+ }
+ while(ass.contains(p + i))
+ i++;
+ this.ns.put(ns, p + i);
+ ass.add(p + i);
+ }
+ }
+ }
+
+ public Formatter(DocType doctype, Element root) {
+ this.header = "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n" + doctype.format() + "\n";
+ this.root = root;
+ calcnsnames();
+ Frame rf = new Frame(root);
+ Map<Name, String> ra = new HashMap<>(root.attribs);
+ for(Map.Entry<Namespace, String> ent : this.ns.entrySet()) {
+ Namespace ns = ent.getKey();
+ String abb = ent.getValue();
+ if(ns == null)
+ continue;
+ ra.put(new Name((abb == null) ? "xmlns" : ("xmlns:" + abb)), ns.uri);
+ }
+ rf.ai = ra.entrySet().iterator();
+ stack.add(rf);
+ }
+
+ private String fmtname(Name nm) {
+ String abb = ns.get(nm.ns);
+ return((abb == null) ? nm.local : (abb + ":" + nm.local));
+ }
+
+ private String head(Element el) {
+ return(String.format("<%s", fmtname(el.name)));
+ }
+
+ private String tail(Element el) {
+ return(String.format("</%s>", fmtname(el.name)));
+ }
+
+ private String attrquote(String val) {
+ char qc;
+ if(val.indexOf('"') >= 0) {
+ qc = '\'';
+ val = val.replace("'", "'");
+ } else {
+ qc = '"';
+ val = val.replace("\"", """);
+ }
+ val = val.replace("&", "&");
+ val = val.replace("<", "<");
+ val = val.replace(">", ">");
+ return(qc + val + qc);
+ }
+
+ private String attr(Name nm, String value) {
+ String anm = (nm.ns == null) ? nm.local : fmtname(nm);
+ return(String.format(" %s=%s", anm, attrquote(value)));
+ }
+
+ private String quote(String text) {
+ text = text.replace("&", "&");
+ text = text.replace("<", "<");
+ text = text.replace(">", ">");
+ return(text);
+ }
+
+ protected boolean shorten(Element el) {
+ return(el.children.isEmpty());
+ }
+
+ protected boolean produce() {
+ if(!headed) {
+ headed = true;
+ if(write(header))
+ return(false);
+ }
+ if(stack.isEmpty())
+ return(true);
+ Frame f = stack.get(stack.size() - 1);
+ if(!f.h && (f.h = true) && write(head(f.el)))
+ return(false);
+ while(f.ai.hasNext()) {
+ Map.Entry<Name, String> ent = f.ai.next();
+ if(write(attr(ent.getKey(), ent.getValue())))
+ return(false);
+ }
+ if(!f.sh) {
+ if(!f.e && (f.e = true) && write(">"))
+ return(false);
+ if(f.ci.hasNext()) {
+ Node ch = f.ci.next();
+ if(ch instanceof Text) {
+ write(quote(((Text)ch).text));
+ } else if(ch instanceof Raw) {
+ write(((Raw)ch).text);
+ } else {
+ stack.add(new Frame((Element)ch));
+ }
+ return(false);
+ }
+ if(!f.t && (f.t = true) && write(tail(f.el)))
+ return(false);
+ } else {
+ if(!f.e && (f.e = true) && write(" />"))
+ return(false);
+ }
+ stack.remove(stack.size() - 1);
+ return(false);
+ }
+}