Commit | Line | Data |
---|---|---|
6e0043cc FT |
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 | } |