1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20#************************************************************** 21 22import os, sys 23import xml.etree.ElementTree as ET 24 25HERE = os.path.dirname(os.path.abspath(__file__)) 26SCHEMAS = [("", "OpenDocument-v1.2-os-schema.rng"), 27 ("manifest", "OpenDocument-v1.2-os-manifest-schema.rng")] 28RNG = "{http://relaxng.org/ns/structure/1.0}" 29XML_NS = "http://www.w3.org/XML/1998/namespace" 30WILD = ("*", "*") 31 32 33def tag(e): 34 return e.tag[len(RNG):] 35 36 37class Schema: 38 def __init__(self, path): 39 self.prefixes = {"xml": XML_NS} 40 for ev, v in ET.iterparse(path, events=("start-ns", "end")): 41 if ev == "start-ns": 42 p, uri = v 43 if self.prefixes.setdefault(p, uri) != uri: 44 sys.exit("prefix %r bound twice in %s" % (p, path)) 45 self.root = ET.parse(path).getroot() 46 self.defs = {} 47 for d in self.root.iter(RNG + "define"): 48 if d.get("name") in self.defs: 49 sys.exit("duplicate define %s" % d.get("name")) 50 self.defs[d.get("name")] = d 51 self.cache = {} 52 self.stack = set() 53 54 def qname(self, text, is_attr): 55 text = text.strip() 56 if ":" in text: 57 p, local = text.split(":", 1) 58 return self.prefixes[p], local 59 if not is_attr: 60 sys.exit("unprefixed element name %r" % text) 61 return "", text 62 63 def names(self, nc, is_attr): 64 t = tag(nc) 65 if t == "name": 66 if nc.get("ns") is not None: 67 sys.exit("name with ns attribute") 68 return [self.qname(nc.text, is_attr)] 69 if t == "choice": 70 parts = [self.names(c, is_attr) for c in nc] 71 return None if any(p is None for p in parts) else [n for p in parts for n in p] 72 return None 73 74 def merge_group(self, parts): 75 out = {} 76 for part in parts: 77 for k, (req, vals) in part.items(): 78 if k in out: 79 r0, v0 = out[k] 80 vals = None if v0 is None or vals is None else v0 | vals 81 req = r0 or req 82 out[k] = (req, vals) 83 return out 84 85 def merge_choice(self, parts): 86 out = {} 87 for part in parts: 88 for k, (req, vals) in part.items(): 89 if k in out: 90 r0, v0 = out[k] 91 vals = None if v0 is None or vals is None else v0 | vals 92 req = r0 and req 93 else: 94 req = req and all(k in p for p in parts) 95 out[k] = (req, vals) 96 return out 97 98 def values(self, children): 99 parts = [self.value_of(c) for c in children] 100 return None if len(parts) != 1 else parts[0] 101 102 def value_of(self, n): 103 t = tag(n) 104 if t == "value": 105 if n.get("type", "token") != "token" or n.get("datatypeLibrary"): 106 return None 107 return frozenset([n.text or ""]) 108 if t == "ref": 109 return self.values(list(self.defs[n.get("name")])) 110 if t == "choice": 111 parts = [self.value_of(c) for c in n] 112 return None if any(p is None for p in parts) else frozenset().union(*parts) 113 return None 114 115 def attrs(self, n): 116 t = tag(n) 117 if t == "attribute": 118 kids = list(n) 119 if "name" in n.attrib: 120 names, body = [self.qname(n.get("name"), True)], kids 121 else: 122 names, body = self.names(kids[0], True), kids[1:] 123 if names is None: 124 return {WILD: (False, None)} 125 vals = self.values(body) if body else None 126 req = len(names) == 1 127 return {k: (req, vals) for k in names} 128 if t == "ref": 129 return self.attrs_define(n.get("name")) 130 if t in ("element", "value", "data", "text", "empty", "list", "notAllowed"): 131 return {} 132 parts = [self.attrs(c) for c in n] 133 if t == "choice": 134 return self.merge_choice(parts) 135 out = self.merge_group(parts) 136 if t in ("optional", "zeroOrMore"): 137 out = {k: (False, v) for k, (r, v) in out.items()} 138 return out 139 140 def attrs_define(self, name): 141 if name in self.cache: 142 return self.cache[name] 143 if name in self.stack: 144 sys.exit("pattern recursion without element: %s" % name) 145 self.stack.add(name) 146 res = self.merge_group([self.attrs(c) for c in self.defs[name]]) 147 self.stack.discard(name) 148 self.cache[name] = res 149 return res 150 151 def element_defs(self): 152 for e in self.root.iter(RNG + "element"): 153 kids = list(e) 154 if "name" in e.attrib: 155 names, body = [self.qname(e.get("name"), False)], kids 156 else: 157 names, body = self.names(kids[0], False), kids[1:] 158 yield names, self.merge_group([self.attrs(c) for c in body]) 159 160 161def extract(path): 162 s = Schema(path) 163 elements, per_elem = set(), {} 164 stats = {"element_nodes": 0, "wild_elements": 0, "wild_attr_elements": 0} 165 for names, attrs in s.element_defs(): 166 stats["element_nodes"] += 1 167 if names is None: 168 stats["wild_elements"] += 1 169 continue 170 if WILD in attrs: 171 stats["wild_attr_elements"] += len(names) 172 del attrs[WILD] 173 for nm in names: 174 elements.add(nm) 175 per_elem.setdefault(nm, []).append(attrs) 176 pairs = {} 177 for nm, defs in per_elem.items(): 178 keys = set().union(*defs) 179 for k in keys: 180 reqs = [d[k][0] if k in d else False for d in defs] 181 vals = [d[k][1] if k in d else frozenset() for d in defs] 182 vals = None if any(v is None for v in vals) else frozenset().union(*vals) 183 pairs[(nm, k)] = (all(reqs), vals) 184 return elements, pairs, stats 185 186 187def write(outdir, elements, pairs): 188 os.makedirs(outdir, exist_ok=True) 189 with open(os.path.join(outdir, "elements.tsv"), "w") as f: 190 for ns, local in sorted(elements): 191 f.write("%s\t%s\n" % (ns, local)) 192 with open(os.path.join(outdir, "attributes.tsv"), "w") as f, \ 193 open(os.path.join(outdir, "values.tsv"), "w") as g: 194 for (en, an), (req, vals) in sorted(pairs.items()): 195 f.write("%s\t%s\t%s\t%s\t%s\n" % (en + an + ("required" if req else "optional",))) 196 for v in sorted(vals or ()): 197 g.write("%s\t%s\t%s\t%s\t%s\n" % (en + an + (v,))) 198 199 200def main(): 201 if len(sys.argv) != 2: 202 sys.exit("usage: python3 -I cov_schema.py <out-dir>") 203 out = sys.argv[1] 204 for sub, name in SCHEMAS: 205 elements, pairs, stats = extract(os.path.join(HERE, "schema", name)) 206 write(os.path.join(out, sub), elements, pairs) 207 enum = [p for p, (r, v) in pairs.items() if v] 208 sys.stderr.write( 209 "%s: elements=%d (element nodes=%d) attributes=%d pairs=%d required=%d " 210 "enumerated pairs=%d values=%d\n" 211 " skipped wildcards: %d anyName/nsName element nodes, " 212 "%d elements with a wildcard attribute\n" 213 % (name, len(elements), stats["element_nodes"], 214 len({k for (_, k) in pairs}), len(pairs), 215 sum(1 for r, v in pairs.values() if r), len(enum), 216 sum(len(pairs[p][1]) for p in enum), 217 stats["wild_elements"], stats["wild_attr_elements"])) 218 219 220main() 221