Added basic HTML generation and response handling.
[jrw.git] / src / jrw / sp / Formatter.java
1 package jrw.sp;
2
3 import jrw.util.*;
4 import java.util.*;
5
6 public class Formatter extends LazyPChannel {
7     private final Element root;
8     private final String header;
9     private final List<Frame> stack = new ArrayList<>();
10     private final Map<Namespace, String> ns = new IdentityHashMap<>();
11     private boolean headed = false;
12
13     class Frame {
14         Element el;
15         Iterator<Map.Entry<Name, String>> ai;
16         Iterator<Node> ci;
17         boolean sh;
18         boolean h, e, t;
19
20         Frame(Element el) {
21             this.el = el;
22             this.ai = el.attribs.entrySet().iterator();
23             this.ci = el.children.iterator();
24             this.sh = shorten(el);
25         }
26     }
27
28     private void countns(Map<Namespace, Integer> freq, Set<Namespace> attrs, Element el) {
29         for(Name anm : el.attribs.keySet()) {
30             if(anm.ns != null) {
31                 attrs.add(anm.ns);
32                 Integer f = freq.get(anm.ns);
33                 freq.put(anm.ns, ((f == null) ? 0 : f) + 1);
34             }
35         }
36         Integer f = freq.get(el.name.ns);
37         freq.put(el.name.ns, ((f == null) ? 0 : f) + 1);
38         for(Node ch : el.children) {
39             if(ch instanceof Element)
40                 countns(freq, attrs, (Element)ch);
41         }
42     }
43
44     private void calcnsnames() {
45         Map<Namespace, Integer> freq = new IdentityHashMap<>();
46         Set<Namespace> attrs = new HashSet<>();
47         countns(freq, attrs, root);
48         if(freq.get(null) != null) {
49             ns.put(null, null);
50             freq.remove(null);
51         } else if(!attrs.contains(root.name.ns)) {
52             ns.put(root.name.ns, null);
53             freq.remove(root.name.ns);
54         }
55         List<Namespace> order = new ArrayList<>(freq.keySet());
56         Collection<String> ass = new HashSet<>();
57         ass.add(null);
58         Collections.sort(order, (x, y) -> (freq.get(y) - freq.get(x)));
59         for(Namespace ns : order) {
60             String p = ns.prefabb;
61             if((p != null) && !ass.contains(p)) {
62                 this.ns.put(ns, p);
63                 ass.add(p);
64             } else {
65                 int i;
66                 if(p == null) {
67                     p = "ns";
68                     i = 1;
69                 } else {
70                     i = 2;
71                 }
72                 while(ass.contains(p + i))
73                     i++;
74                 this.ns.put(ns, p + i);
75                 ass.add(p + i);
76             }
77         }
78     }
79
80     public Formatter(DocType doctype, Element root) {
81         this.header = "<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n" + doctype.format() + "\n";
82         this.root = root;
83         calcnsnames();
84         Frame rf = new Frame(root);
85         Map<Name, String> ra = new HashMap<>(root.attribs);
86         for(Map.Entry<Namespace, String> ent : this.ns.entrySet()) {
87             Namespace ns = ent.getKey();
88             String abb = ent.getValue();
89             if(ns == null)
90                 continue;
91             ra.put(new Name((abb == null) ? "xmlns" : ("xmlns:" + abb)), ns.uri);
92         }
93         rf.ai = ra.entrySet().iterator();
94         stack.add(rf);
95     }
96
97     private String fmtname(Name nm) {
98         String abb = ns.get(nm.ns);
99         return((abb == null) ? nm.local : (abb + ":" + nm.local));
100     }
101
102     private String head(Element el) {
103         return(String.format("<%s", fmtname(el.name)));
104     }
105
106     private String tail(Element el) {
107         return(String.format("</%s>", fmtname(el.name)));
108     }
109
110     private String attrquote(String val) {
111         char qc;
112         if(val.indexOf('"') >= 0) {
113             qc = '\'';
114             val = val.replace("'", "&apos;");
115         } else {
116             qc = '"';
117             val = val.replace("\"", "&quot;");
118         }
119         val = val.replace("&", "&amp;");
120         val = val.replace("<", "&lt;");
121         val = val.replace(">", "&gt;");
122         return(qc + val + qc);
123     }
124
125     private String attr(Name nm, String value) {
126         String anm = (nm.ns == null) ? nm.local : fmtname(nm);
127         return(String.format(" %s=%s", anm, attrquote(value)));
128     }
129
130     private String quote(String text) {
131         text = text.replace("&", "&amp;");
132         text = text.replace("<", "&lt;");
133         text = text.replace(">", "&gt;");
134         return(text);
135     }
136
137     protected boolean shorten(Element el) {
138         return(el.children.isEmpty());
139     }
140
141     protected boolean produce() {
142         if(!headed) {
143             headed = true;
144             if(write(header))
145                 return(false);
146         }
147         if(stack.isEmpty())
148             return(true);
149         Frame f = stack.get(stack.size() - 1);
150         if(!f.h && (f.h = true) && write(head(f.el)))
151             return(false);
152         while(f.ai.hasNext()) {
153             Map.Entry<Name, String> ent = f.ai.next();
154             if(write(attr(ent.getKey(), ent.getValue())))
155                 return(false);
156         }
157         if(!f.sh) {
158             if(!f.e && (f.e = true) && write(">"))
159                 return(false);
160             if(f.ci.hasNext()) {
161                 Node ch = f.ci.next();
162                 if(ch instanceof Text) {
163                     write(quote(((Text)ch).text));
164                 } else if(ch instanceof Raw) {
165                     write(((Raw)ch).text);
166                 } else {
167                     stack.add(new Frame((Element)ch));
168                 }
169                 return(false);
170             }
171             if(!f.t && (f.t = true) && write(tail(f.el)))
172                 return(false);
173         } else {
174             if(!f.e && (f.e = true) && write(" />"))
175                 return(false);
176         }
177         stack.remove(stack.size() - 1);
178         return(false);
179     }
180 }