Added basic HTML generation and response handling.
[jrw.git] / src / jrw / sp / Formatter.java
diff --git a/src/jrw/sp/Formatter.java b/src/jrw/sp/Formatter.java
new file mode 100644 (file)
index 0000000..0791614
--- /dev/null
@@ -0,0 +1,180 @@
+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("'", "&apos;");
+       } else {
+           qc = '"';
+           val = val.replace("\"", "&quot;");
+       }
+       val = val.replace("&", "&amp;");
+       val = val.replace("<", "&lt;");
+       val = val.replace(">", "&gt;");
+       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("&", "&amp;");
+       text = text.replace("<", "&lt;");
+       text = text.replace(">", "&gt;");
+       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);
+    }
+}