1*0e16a31dSJim Jagielski#************************************************************** 2*0e16a31dSJim Jagielski# 3*0e16a31dSJim Jagielski# Licensed to the Apache Software Foundation (ASF) under one 4*0e16a31dSJim Jagielski# or more contributor license agreements. See the NOTICE file 5*0e16a31dSJim Jagielski# distributed with this work for additional information 6*0e16a31dSJim Jagielski# regarding copyright ownership. The ASF licenses this file 7*0e16a31dSJim Jagielski# to you under the Apache License, Version 2.0 (the 8*0e16a31dSJim Jagielski# "License"); you may not use this file except in compliance 9*0e16a31dSJim Jagielski# with the License. You may obtain a copy of the License at 10*0e16a31dSJim Jagielski# 11*0e16a31dSJim Jagielski# http://www.apache.org/licenses/LICENSE-2.0 12*0e16a31dSJim Jagielski# 13*0e16a31dSJim Jagielski# Unless required by applicable law or agreed to in writing, 14*0e16a31dSJim Jagielski# software distributed under the License is distributed on an 15*0e16a31dSJim Jagielski# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16*0e16a31dSJim Jagielski# KIND, either express or implied. See the License for the 17*0e16a31dSJim Jagielski# specific language governing permissions and limitations 18*0e16a31dSJim Jagielski# under the License. 19*0e16a31dSJim Jagielski# 20*0e16a31dSJim Jagielski#************************************************************** 21*0e16a31dSJim Jagielski 22*0e16a31dSJim Jagielski# Removes foreign elements and attributes (ODF 1.2 Part 1 3.17) so that the rest can be validated as an 23*0e16a31dSJim Jagielski# extended document (2.2.2 B.1). Loaded by validate.py and selftest.py. 24*0e16a31dSJim Jagielski# foreign attribute: namespace outside tables 1-4 of 1.5 (xml: included), removed. 25*0e16a31dSJim Jagielski# foreign element: namespace outside tables 1-4, outside any element of tables 1-4 (their wildcard 26*0e16a31dSJim Jagielski# content is the schema's), removed with its content, except that its tags alone are removed when 27*0e16a31dSJim Jagielski# it sits in a text:p or text:h and every ancestor up to it permits character data in the schema. 28*0e16a31dSJim Jagielski# office:process-content (deprecated in 1.2) is not consulted. Table 4 names that the schema does not 29*0e16a31dSJim Jagielski# define are not removed, so they stay visible as errors. 30*0e16a31dSJim Jagielskiimport re 31*0e16a31dSJim Jagielskifrom xml.etree import ElementTree as ET 32*0e16a31dSJim Jagielskifrom xml.parsers import expat 33*0e16a31dSJim Jagielskifrom xml.sax.saxutils import escape, quoteattr 34*0e16a31dSJim Jagielski 35*0e16a31dSJim JagielskiODF = "urn:oasis:names:tc:opendocument:xmlns:%s:1.0" 36*0e16a31dSJim JagielskiT123 = {ODF % x for x in "animation chart config database dr3d drawing form manifest meta datastyle office presentation script table text style " 37*0e16a31dSJim Jagielski "xsl-fo-compatible svg-compatible smil-compatible".split()} | {"http://docs.oasis-open.org/ns/office/1.2/meta/odf#"} 38*0e16a31dSJim JagielskiT4 = {"http://purl.org/dc/elements/1.1/", "http://www.w3.org/1998/Math/MathML", "http://www.w3.org/2002/xforms", "http://www.w3.org/1999/xlink", 39*0e16a31dSJim Jagielski "http://www.w3.org/1999/xhtml", "http://www.w3.org/2003/g/data-view#", "http://www.w3.org/XML/1998/namespace"} 40*0e16a31dSJim JagielskiRNG = "{http://relaxng.org/ns/structure/1.0}" 41*0e16a31dSJim JagielskiTEXT_P, TEXT_H = "{%s}p" % (ODF % "text"), "{%s}h" % (ODF % "text") 42*0e16a31dSJim Jagielski 43*0e16a31dSJim Jagielski 44*0e16a31dSJim Jagielskidef mixed_elements(rng_path): 45*0e16a31dSJim Jagielski """Expanded names of the schema's elements whose content model permits character data.""" 46*0e16a31dSJim Jagielski prefixes = dict(re.findall(r'xmlns:(\w+)="([^"]+)"', open(rng_path, encoding="utf-8").read(4000))) 47*0e16a31dSJim Jagielski defines = {} 48*0e16a31dSJim Jagielski for d in ET.parse(rng_path).getroot().iter(RNG + "define"): 49*0e16a31dSJim Jagielski defines.setdefault(d.get("name"), []).append(d) 50*0e16a31dSJim Jagielski memo = {} 51*0e16a31dSJim Jagielski 52*0e16a31dSJim Jagielski def has_text(node, seen): 53*0e16a31dSJim Jagielski tag = node.tag 54*0e16a31dSJim Jagielski if tag in (RNG + "text", RNG + "mixed"): 55*0e16a31dSJim Jagielski return True 56*0e16a31dSJim Jagielski if tag in (RNG + "element", RNG + "attribute", RNG + "data", RNG + "value"): 57*0e16a31dSJim Jagielski return False 58*0e16a31dSJim Jagielski if tag == RNG + "ref": 59*0e16a31dSJim Jagielski n = node.get("name") 60*0e16a31dSJim Jagielski if n in seen: 61*0e16a31dSJim Jagielski return False 62*0e16a31dSJim Jagielski return any(has_text(c, seen | {n}) for d in defines.get(n, []) for c in d) 63*0e16a31dSJim Jagielski return any(has_text(c, seen) for c in node) 64*0e16a31dSJim Jagielski out = set() 65*0e16a31dSJim Jagielski for el in ET.parse(rng_path).getroot().iter(RNG + "element"): 66*0e16a31dSJim Jagielski name = el.get("name") 67*0e16a31dSJim Jagielski if name and ":" in name: 68*0e16a31dSJim Jagielski pre, local = name.split(":", 1) 69*0e16a31dSJim Jagielski if any(has_text(c, frozenset()) for c in el): 70*0e16a31dSJim Jagielski out.add("{%s}%s" % (prefixes[pre], local)) 71*0e16a31dSJim Jagielski return out 72*0e16a31dSJim Jagielski 73*0e16a31dSJim Jagielski 74*0e16a31dSJim Jagielskidef strip_foreign(text, mixed): 75*0e16a31dSJim Jagielski out, scopes, frames, state = [], [{"xml": "http://www.w3.org/XML/1998/namespace"}], [], {"drop": 0, "pending": {}} 76*0e16a31dSJim Jagielski p = expat.ParserCreate() 77*0e16a31dSJim Jagielski p.ordered_attributes = True 78*0e16a31dSJim Jagielski 79*0e16a31dSJim Jagielski def expand(name, scope, is_attr): 80*0e16a31dSJim Jagielski """Expanded name, or None when the prefix is unbound (kept, so that the validator reports it).""" 81*0e16a31dSJim Jagielski if ":" in name: 82*0e16a31dSJim Jagielski pre, local = name.split(":", 1) 83*0e16a31dSJim Jagielski return "{%s}%s" % (scope[pre], local) if pre in scope else None 84*0e16a31dSJim Jagielski uri = "" if is_attr else scope.get("", "") 85*0e16a31dSJim Jagielski return "{%s}%s" % (uri, name) if uri else name 86*0e16a31dSJim Jagielski 87*0e16a31dSJim Jagielski def ns_of(expanded): 88*0e16a31dSJim Jagielski return expanded[1:].split("}", 1)[0] if expanded and expanded.startswith("{") else "" 89*0e16a31dSJim Jagielski 90*0e16a31dSJim Jagielski def start(name, attrs): 91*0e16a31dSJim Jagielski if state["drop"]: 92*0e16a31dSJim Jagielski state["drop"] += 1 93*0e16a31dSJim Jagielski return 94*0e16a31dSJim Jagielski decl = {} 95*0e16a31dSJim Jagielski for i in range(0, len(attrs), 2): 96*0e16a31dSJim Jagielski if attrs[i] == "xmlns" or attrs[i].startswith("xmlns:"): 97*0e16a31dSJim Jagielski decl[attrs[i][6:]] = attrs[i + 1] 98*0e16a31dSJim Jagielski scope = dict(scopes[-1]) 99*0e16a31dSJim Jagielski scope.update(decl) 100*0e16a31dSJim Jagielski exp = expand(name, scope, False) 101*0e16a31dSJim Jagielski in_t4 = any(ns_of(f[0]) in T4 for f in frames if f[1]) 102*0e16a31dSJim Jagielski if exp is not None and ns_of(exp) not in T123 | T4 and not in_t4: 103*0e16a31dSJim Jagielski if unwrap(frames): 104*0e16a31dSJim Jagielski scopes.append(scope) 105*0e16a31dSJim Jagielski frames.append((exp, False)) 106*0e16a31dSJim Jagielski state["pending"].update(decl) 107*0e16a31dSJim Jagielski else: 108*0e16a31dSJim Jagielski state["drop"] = 1 109*0e16a31dSJim Jagielski return 110*0e16a31dSJim Jagielski kept = [] 111*0e16a31dSJim Jagielski for i in range(0, len(attrs), 2): 112*0e16a31dSJim Jagielski k, v = attrs[i], attrs[i + 1] 113*0e16a31dSJim Jagielski ke = expand(k, scope, True) 114*0e16a31dSJim Jagielski if k == "xmlns" or k.startswith("xmlns:") or ":" not in k or ke is None or ns_of(ke) in T123 | T4: 115*0e16a31dSJim Jagielski kept.append((k, v)) 116*0e16a31dSJim Jagielski have = {k for k, _ in kept} 117*0e16a31dSJim Jagielski for pre, uri in state["pending"].items(): 118*0e16a31dSJim Jagielski k = "xmlns:" + pre if pre else "xmlns" 119*0e16a31dSJim Jagielski if k not in have: 120*0e16a31dSJim Jagielski kept.insert(0, (k, uri)) 121*0e16a31dSJim Jagielski state["pending"] = {} 122*0e16a31dSJim Jagielski out.append("<" + name + "".join(" %s=%s" % (k, quoteattr(v, {"\n": " ", "\t": "	", "\r": " "})) for k, v in kept) + ">") 123*0e16a31dSJim Jagielski scopes.append(scope) 124*0e16a31dSJim Jagielski frames.append((exp, True)) 125*0e16a31dSJim Jagielski 126*0e16a31dSJim Jagielski def end(name): 127*0e16a31dSJim Jagielski if state["drop"]: 128*0e16a31dSJim Jagielski state["drop"] -= 1 129*0e16a31dSJim Jagielski return 130*0e16a31dSJim Jagielski scopes.pop() 131*0e16a31dSJim Jagielski if frames.pop()[1]: 132*0e16a31dSJim Jagielski out.append("</" + name + ">") 133*0e16a31dSJim Jagielski 134*0e16a31dSJim Jagielski def data(s): 135*0e16a31dSJim Jagielski if not state["drop"]: 136*0e16a31dSJim Jagielski out.append(escape(s)) 137*0e16a31dSJim Jagielski 138*0e16a31dSJim Jagielski def unwrap(fr): 139*0e16a31dSJim Jagielski """Foreign element in a paragraph, every ancestor up to it permitting character data?""" 140*0e16a31dSJim Jagielski for exp, kept in reversed(fr): 141*0e16a31dSJim Jagielski if not kept: 142*0e16a31dSJim Jagielski continue 143*0e16a31dSJim Jagielski if exp in (TEXT_P, TEXT_H): 144*0e16a31dSJim Jagielski return True 145*0e16a31dSJim Jagielski if exp not in mixed: 146*0e16a31dSJim Jagielski return False 147*0e16a31dSJim Jagielski return False 148*0e16a31dSJim Jagielski p.StartElementHandler, p.EndElementHandler, p.CharacterDataHandler = start, end, data 149*0e16a31dSJim Jagielski p.Parse(text.encode("utf-8") if isinstance(text, str) else text, True) 150*0e16a31dSJim Jagielski return '<?xml version="1.0" encoding="UTF-8"?>\n' + "".join(out) + "\n" 151