summaryrefslogtreecommitdiffstats
path: root/utils_xml.py
blob: 9e27b65a2e9930de4db1599e6a1aa34460c73bf9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# -*- coding: UTF-8 -*-
# Copyright 2014 Red Hat, Inc.
# Part of clufter project
# Licensed under GPLv2+ (a copy included | http://gnu.org/licenses/gpl-2.0.txt)
"""XML helpers"""
__author__ = "Jan Pokorný <jpokorny @at@ Red Hat .dot. com>"

from copy import deepcopy
from lxml import etree

from .error import ClufterPlainError
from .utils import selfaware


NAMESPACES = {
    'clufter': 'http://people.redhat.com/jpokorny/ns/clufter',
    'rng':     'http://relaxng.org/ns/structure/1.0',
    'xsl':     'http://www.w3.org/1999/XSL/Transform',
}

xslt_identity = '''\
    <xsl:template match="{0}@*|{0}node()"
                  xmlns:xsl="''' + NAMESPACES['xsl'] + '''">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
       </xsl:copy>
    </xsl:template>'''


class UtilsXmlError(ClufterPlainError):
    pass


def squote(s):
    """Simple quote"""
    return "'" + s + "'"


def namespaced(ns, ident):
    """Return `ident` in Clark's notation denoting `ns` namespace"""
    ret = "{{{0}}}{1}".format(NAMESPACES.get(ns, ns), ident)
    return ret


def nselem(ns, tag, **kwargs):
    return etree.Element(namespaced(ns, tag), **kwargs)

rng_get_start = etree.ETXPath("/{0}/{1}"
                              .format(namespaced('rng', 'grammar'),
                                      namespaced('rng', 'start')))
xmltag_get_localname = lambda tag: etree.QName(tag).localname
xmltag_get_namespace = lambda tag: etree.QName(tag).namespace

RNG_ELEMENT = ("/{0}//{1}".format(namespaced('rng', 'grammar'),
                                  namespaced('rng', 'element'))
               .replace('{', '{{').replace('}', '}}')
               + "[@name = '{0}']")


@selfaware
def rng_pivot(me, et, tag):
    """Given Relax NG grammar etree as `et`, change start tag (in situ!)

    Use copy.deepcopy or so to (somewhat) preserve the original.

    Returns the live reference to the target element, i.e.,

        at_start = rng_pivot(et, tag)

    is equivalent to

        rng_pivot(et, tag)
        at_start = rng_get_start(et)[0]
    """
    start = rng_get_start(et)
    localname = xmltag_get_localname(tag)
    if len(start) != 1:
        raise UtilsXmlError("Cannot change start if grammar's `start' is"
                            " not contained exactly once ({0} times)"
                            .format(len(start)))
    target = etree.ETXPath(RNG_ELEMENT.format(tag))(et)
    if len(target) != 1:
        raise UtilsXmlError("Cannot change start if the start element `{0}'"
                            " is not contained exactly once ({1} times)"
                            .format(localname, len(target)))
    start, target = start[0], target[0]
    parent_start, parent_target = start.getparent(), target.getparent()
    index_target = parent_target.index(target)
    label = me.__name__ + '_' + localname

    # target's content place directly under /grammar wrapped with new define...
    new_define = nselem('rng', 'define', name=label)
    new_define.append(target)
    parent_start.append(new_define)

    # ... while the original occurrence substituted in-situ with the reference
    new_ref = nselem('rng', 'ref', name=label)
    parent_target.insert(index_target, new_ref)

    # ... and finally /grammar/start pointed anew to refer to the new label
    start_ref = nselem('rng', 'ref', name=label)
    start.clear()
    start.append(start_ref)

    return target