| 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("'", "'"); |
| 115 | } else { |
| 116 | qc = '"'; |
| 117 | val = val.replace("\"", """); |
| 118 | } |
| 119 | val = val.replace("&", "&"); |
| 120 | val = val.replace("<", "<"); |
| 121 | val = val.replace(">", ">"); |
| 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("&", "&"); |
| 132 | text = text.replace("<", "<"); |
| 133 | text = text.replace(">", ">"); |
| 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 | } |