<reponame>orest-d/liquer<filename>liquer/parser.py
from urllib.parse import quote, unquote
import re
from pyparsing import (
    Literal,
    Word,
    alphas,
    alphanums,
    nums,
    delimitedList,
    Regex,
    OneOrMore,
    Group,
    Combine,
    ZeroOrMore,
    Optional,
    Forward,
    lineno,
    col,
    FollowedBy,
)

"""
This module provides functions to decode and encode query.
A query is a sequence of commands, each command being a list of strings starting with the command name as a first element.
Commands are separated by a COMMAND_SEPARATOR, parts of the command (tokens) by PARAMETER_SEPARATOR.
Having special charachers (like COMMAND_SEPARATOR or PARAMETER_SEPARATOR) inside the commands is possible
by escaping with ESCAPE character and using escape sequences.
Query is URL-safe and URL escaping is applied to other special characters.
Empty commands are ignored (e.g. two consecutive COMMAND_SEPARATORs are equivalent to a single COMMAND_SEPARATOR).

Query can be decoded from string to a list of lists of strings by decode (or encoded by encode).
Encode and decode use encode_token and decode_token that are acting on a token level.
"""

COMMAND_SEPARATOR = "/"
PARAMETER_SEPARATOR = "-"
ESCAPE = "~"
ESCAPE_SEQUENCES = [
    (ESCAPE, ESCAPE + ESCAPE),
    ("https://", ESCAPE + "H"),
    ("http://", ESCAPE + "h"),
    ("file://", ESCAPE + "f"),
    ("://", ESCAPE + "P"),
    (COMMAND_SEPARATOR, ESCAPE + "I"),
    (PARAMETER_SEPARATOR, ESCAPE + "_"),
    (" ", ESCAPE + "."),
]


def decode(query: str):
    """Decode query string into a list of lists of strings."""
    ql = [
        [decode_token(etoken) for etoken in eqv.split(PARAMETER_SEPARATOR)]
        for eqv in query.split(COMMAND_SEPARATOR)
    ]
    return [qcommand for qcommand in ql if len(qcommand) and len(qcommand[0])]


def encode_token(token: str):
    "Encode single token by escaping special characters and character sequences"
    for sequence, encoding in ESCAPE_SEQUENCES:
        token = token.replace(sequence, encoding)
    return quote(token).replace("%7E", "~").replace("%7e", "~")


def decode_token(token: str):
    "Decode single token by un-escaping special characters and character sequences"
    encoding = {e: s for s, e in ESCAPE_SEQUENCES}
    if token == "":
        return ""
    try:
        index = token.index(ESCAPE)
        head = token[:index]
        mid = token[index : index + 2]
        tail = token[index + 2 :]
        return unquote(head + encoding.get(mid, mid)) + decode_token(tail)
    except ValueError:
        return unquote(token)


def encode(ql: list):
    """Decode query list (list of lists of strings) into an properly escaped query string."""
    return COMMAND_SEPARATOR.join(
        PARAMETER_SEPARATOR.join(encode_token(token) for token in qv) for qv in ql
    )


def all_splits(query):
    """Make all splits of the query into two parts (biginning and remainder)
    by progressively increasing the remainder.
    query == beginning + COMMAND_SEPARATOR + remainder
    """
    ql = decode(query)
    for i in range(len(ql), -1, -1):
        yield encode(ql[:i]), encode(ql[i:])


def indent(text, prefix="  "):
    return "\n".join(prefix + x for x in text.split("\n"))


def list_indent(lst, prefix="  "):
    if len(lst):
        return "[\n" + ",\n".join(indent(repr(x), prefix) for x in lst) + "\n]"
    else:
        return "[]"


class Position:
    def __init__(self, offset=0, line=0, column=0):
        self.offset = offset
        self.line = line
        self.column = column

    @classmethod
    def from_loc(cls, loc, string):
        return cls(loc, line=lineno(loc, string), column=col(loc, string))

    @classmethod
    def from_dict(cls, d):
        if d is None:
            return cls()
        else:
            return cls(offset=d["offset"], line=d["line"], column=d["column"])

    def to_dict(self):
        return dict(offset=self.offset, line=self.line, column=self.column)

    def __str__(self):
        if self.line == 0:
            return "(unknown position)"
        elif self.line > 1:
            return f"line {self.line}, position {self.column}"
        else:
            return f"position {self.column}"

    def __repr__(self):
        if self.line == 0:
            return "Position()"
        return f"Position(offset={self.offset}, line={self.line}, column={self.column})"


class QueryException(Exception):
    def __init__(self, message, position=None, query=None):
        self.original_message = message
        if position is not None:
            message += f" at {position}"
            if query is not None:
                message += f":\n  query: {query[:position.offset]}\n     {' '*position.offset}--> {query[position.offset:]}"
        else:
            if query is not None:
                message += f":\n  query: {query}"

        super().__init__(message)
        self.position = position
        self.query = query


class ActionParameter(object):
    def __init__(self, position=None):
        self.position = position or Position()

    def __repr__(self):
        return f"{self.__class__.__name__}({repr(self.position)})"

    def __str__(self):
        return f"{self.__class__.__name__} at {self.position}"


class LinkActionParameter(ActionParameter):
    def __init__(self, link, position=None):
        super().__init__(position)
        self.link = link

    def encode(self):
        return "~X~" + self.link.encode() + "~E"

    def __repr__(self):
        return f"""LinkActionParameter(
{indent(repr(self.link))},
  {repr(self.position)})"""

    def __str__(self):
        return self.encode()


class ExpandedActionParameter(ActionParameter):
    def __init__(self, value, link, position=None):
        super().__init__(position)
        self.value = value
        self.link = link

    def encode(self):
        return "~X~" + self.link.encode() + "~E"

    def __repr__(self):
        return f"""ExpandedActionParameter(
{indent(repr(self.value))}
{indent(repr(self.link))},
  {repr(self.position)})"""

    def __str__(self):
        return str(self.value)


class StringActionParameter(ActionParameter):
    def __init__(self, string: str, position=None):
        super().__init__(position)
        self.string = string

    def encode(self):
        #        encoded = self.string.replace("~","~~").replace("/","~/").replace("://","~P")
        encoded = encode_token(self.string)
        return encoded

    def __repr__(self):
        return f"StringActionParameter({repr(self.string)}, {repr(self.position)})"

    def __str__(self):
        return self.string


class ResourceName(ActionParameter):
    def __init__(self, name: str, position=None):
        self.position = position or Position()
        self.name = name

    def encode(self):
        return self.name

    def __repr__(self):
        return f"ResourceName({repr(self.name)}, {repr(self.position)})"

    def __str__(self):
        return self.encode()


class ActionRequest(object):
    def __init__(self, name: str, parameters=None, position=None):
        self.name = name
        self.parameters = [] if parameters is None else parameters
        self.position = position or Position()

    @classmethod
    def from_arguments(cls, name: str, *parameters):
        assert type(name) == str
        typedparam = []
        for p in parameters:
            if isinstance(p, ActionParameter):
                typedparam.append(p)
            else:
                assert type(p) in (str, int, float, bool)
                typedparam.append(StringActionParameter(str(p)))

        return cls(name, typedparam)

    def to_list(self):
        lst = [self.name]
        for x in self.parameters:
            if isinstance(x, StringActionParameter):
                lst.append(x.string)
            elif isinstance(x, LinkActionParameter):
                lst.append(x.encode())
            else:
                raise Exception(
                    f"Unsupported action parameter type: {type(x)} ({repr(x)})"
                )
        return lst

    @classmethod
    def from_list(cls, lst):
        return cls.from_arguments(lst[0], *lst[1:])

    def encode(self):
        if len(self.parameters):
            p = "-".join(x.encode() for x in self.parameters)
            return f"{self.name}-{p}"
        else:
            return self.name

    def __repr__(self):
        return f"""ActionRequest(
  {repr(self.name)},
{indent(list_indent(self.parameters))},
  {repr(self.position)}
)"""

    def __str__(self):
        return self.encode()


class SegmentHeader(object):
    def __init__(
        self,
        name: str = "",
        level: int = 1,
        parameters: list = None,
        resource: bool = False,
        position=None,
    ):
        self.name = name
        self.level = level
        self.parameters = parameters or []
        self.resource = resource
        self.position = position or Position()

    def encode(self):
        assert self.level >= 1
        encoded = "-" * self.level
        if self.resource:
            encoded += "R"
        encoded += self.name
        if len(self.parameters):
            assert len(self.name) > 0 or self.resource
            for parameter in self.parameters:
                encoded += "-"
                encoded += parameter.encode()
        return encoded

    def __repr__(self):
        return f"""SegmentHeader(
  name       ={repr(self.name)},
  level      ={self.level},
  parameters ={indent(list_indent(self.parameters))},
  resource   ={self.resource},
  position   ={repr(self.position)}
)"""

    def __str__(self):
        return self.encode()


class TransformQuerySegment(object):
    def __init__(self, header=None, query=None, filename=None):
        "header can be SegmentHeader, query is a list of ActionRequest objects"
        self.header = header
        self.query = query or []
        self.filename = filename

    def predecessor(self):
        if self.filename is None:
            if len(self.query):
                p = TransformQuerySegment(
                    header=self.header, query=self.query[:-1], filename=None
                )
                r = TransformQuerySegment(
                    header=self.header, query=[self.query[-1]], filename=None
                )
                return p, r
            else:
                return None, None
        else:
            p = TransformQuerySegment(
                header=self.header, query=self.query, filename=None
            )
            r = TransformQuerySegment(
                header=self.header, query=[], filename=self.filename
            )
            return p, r

    def is_empty(self):
        return len(self.query) == 0 and self.filename is None

    def is_filename(self):
        return len(self.query) == 0 and self.filename is not None

    def is_action_request(self):
        return len(self.query) == 1 and self.filename is None

    def action(self):
        if self.is_action_request():
            return self.query[0]
        else:
            return None

    def encode(self):
        query = "/".join(x.encode() for x in self.query)
        if self.filename is not None:
            query = f"{query}/{self.filename}" if len(query) else self.filename

        if self.header is None:
            return query
        else:
            if len(query):
                return f"{self.header.encode()}/{query}"
            else:
                return self.header.encode()

    def append(self, q):
        if q is None:
            return self
        if isinstance(q, TransformQuerySegment):
            self.query.extend(q.query)
            self.filename = q.filename
            return self
        if isinstance(q, Query):
            if q.is_transform_query():
                return self.append(q.segments[0])
            else:
                raise Exception(
                    f"Appending general query {q} to transform query {self.encode()} not supported"
                )
        if isinstance(q, ActionRequest):
            self.query.append(q)
            return self
        if isinstance(q, str):
            return self.append(parse(q))

        raise Exception(
            f"Transform query {self.encode()} can't append object {repr(q)}"
        )

    def __add__(self, q):
        if q is None:
            return self
        if isinstance(q, TransformQuerySegment):
            return TransformQuerySegment(
                header=self.header, query=self.query + q.query, filename=q.filename
            )

        raise Exception(f"Unsupported operation (add): {self.encode()} + {repr(q)}")

    def __radd__(self, q):
        if q is None:
            return self
        if isinstance(q, TransformQuerySegment):
            return q + self
        raise Exception(f"Unsupported operation (radd): {repr(q)} + {self.encode()}")

    def __repr__(self):
        return f"""TransformQuerySegment(
  header   = {indent(repr(self.header))},
  query    = {indent(list_indent(self.query))},
  filename = {repr(self.filename)}
)"""

    def __str__(self):
        return self.encode()


class ResourceQuerySegment(object):
    def __init__(self, header=None, query=None):
        "header can be SegmentHeader, query is a list of ActionRequest objects"
        self.header = header
        self.query = query or []

    @property
    def position(self):
        if self.header is not None:
            return self.header.position
        else:
            if len(self.query):
                return self.query[0].position

    def path(self):
        return "/".join(x.encode() for x in self.query)

    def encode(self):
        query = self.path()
        if self.header is None:
            rqs = ""
        else:
            rqs = self.header.encode()
        if len(rqs):
            rqs += "/"
        if len(query):
            return f"{rqs}{query}"
        else:
            return rqs

    def __repr__(self):
        return f"""ResourceQuerySegment(
  header = {indent(repr(self.header))},
  query  = {indent(list_indent(self.query))}
)"""

    def __str__(self):
        return self.encode()


class Query(object):
    def __init__(self, segments: list = None, absolute=False):
        self.segments = segments or []
        self.absolute = absolute

    def filename(self):
        if len(self.segments):
            segment = self.segments[-1]
            if (
                isinstance(segment, TransformQuerySegment)
                and segment.filename is not None
            ):
                return str(segment.filename)
            if (
                isinstance(segment, ResourceQuerySegment)
                and segment.query is not None
                and len(segment.query)
            ):
                return str(segment.query[-1])
        return None

    def is_empty(self):
        return len(self.segments) == 0

    def is_transform_query(self):
        return len(self.segments) == 1 and isinstance(
            self.segments[0], TransformQuerySegment
        )

    def transform_query(self):
        if self.is_transform_query():
            return self.segments[0]
        else:
            return None

    def is_resource_query(self):
        return len(self.segments) == 1 and isinstance(
            self.segments[0], ResourceQuerySegment
        )

    def resource_query(self):
        if self.is_resource_query():
            return self.segments[0]
        else:
            return None

    def is_action_request(self):
        return self.is_transform_query() and self.segments[0].is_action_request()

    def action(self):
        if self.is_action_request():
            return self.segments[0].action()
        else:
            return None

    def predecessor(self):
        if len(self.segments):
            if isinstance(self.segments[-1], TransformQuerySegment):
                p, r = self.segments[-1].predecessor()
                if p is None or p.is_empty():
                    qp = Query(self.segments[:-1], absolute=self.absolute)
                    qr = r
                    return qp, qr
                else:
                    qp = Query(self.segments[:-1] + [p], absolute=self.absolute)
                    qr = r
                    return qp, qr
            else:
                return None, None
        else:
            return None, None

    def short(self):
        _, r = self.predecessor()
        if r is None:
            q = str(self)
            if len(self) > 30:
                q = "..." + q[-30:]
            return q
        else:
            return str(r)

    def all_predecessors(self):
        qp, qr = self, None
        while qp is not None:
            yield qp, qr
            qp, r = qp.predecessor()
            qr = r + qr

    def create_segment(self, name: str = "", level=1):
        qs = TransformQuerySegment(SegmentHeader(name, level=level))
        self.segments.append(qs)
        return qs

    def last_transform_query_segment(self):
        if not self.is_empty():
            if isinstance(self.segments[-1], TransformQuerySegment):
                return self.segments[-1]
        return self.create_segment()

    def with_action(self, name: str, *parameters):
        self.last_transform_query_segment().append(
            ActionRequest.from_arguments(name, *parameters)
        )
        return self

    def __add__(self, tq):
        assert isinstance(tq, TransformQuerySegment)
        return Query(self.segments + [tq], absolute=self.absolute)

    def encode(self):
        return ("/" if self.absolute else "") + "/".join(
            x.encode() for x in self.segments
        )

    def __repr__(self):
        return f"""Query(
{indent(list_indent(self.segments))},
  absolute = {self.absolute}
)"""

    def __str__(self):
        return self.encode()


identifier = Regex("[a-z_][a-zA-Z0-9_]*").setName("identifier")
filename = Regex(r"[a-zA-Z0-9_]*\.[a-zA-Z0-9._-]*").setName("filename")


def _resource_name_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    name = unquote("".join(toks))
    return ResourceName(name, position=position)


resource_name = (
    Regex(r"[a-zA-Z0-9._][a-zA-Z0-9._-]*")
    .setParseAction(_resource_name_parse_action)
    .setName("resource_name")
)
parameter_text = Regex("[a-zA-Z0-9_+.]+").setName("parameter_text")
percent_encoding = Regex("%[0-9a-fA-F][0-9a-fA-F]").setName("percent_encoding")
parse_query = Forward()

tilde_entity = (
    Literal("~~").setParseAction(lambda s, loc, toks: ["~"]).setName("tilde_entity")
)
minus_entity = (
    Literal("~_").setParseAction(lambda s, loc, toks: ["-"]).setName("minus_entity")
)
islash_entity = (
    Literal("~I").setParseAction(lambda s, loc, toks: ["/"]).setName("islash_entity")
)
slash_entity = (
    Literal("~/").setParseAction(lambda s, loc, toks: ["/"]).setName("slash_entity")
)

https_entity = (
    Literal("~H")
    .setParseAction(lambda s, loc, toks: ["https://"])
    .setName("https_entity")
)
http_entity = (
    Literal("~h")
    .setParseAction(lambda s, loc, toks: ["http://"])
    .setName("http_entity")
)
file_entity = (
    Literal("~f")
    .setParseAction(lambda s, loc, toks: ["file://"])
    .setName("file_entity")
)
protocol_entity = (
    Literal("~P")
    .setParseAction(lambda s, loc, toks: ["://"])
    .setName("protocol_entity")
)

negative_number_entity = (
    Regex("~[0-9]")
    .setParseAction(lambda s, loc, toks: ["-" + toks[0][1:]])
    .setName("negative_number_entity")
)
space_entity = (
    Literal("~.").setParseAction(lambda s, loc, toks: [" "]).setName("space_entity")
)
end_entity = Literal("~E")


def _expand_entity_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    return LinkActionParameter(toks[0], position=position)


expand_entity = (
    (Literal("~X~").suppress() + parse_query + end_entity.suppress())
    .setParseAction(_expand_entity_parse_action)
    .setName("expand_entity")
)

entities = (
    tilde_entity
    | minus_entity
    | negative_number_entity
    | space_entity
    | islash_entity
    | slash_entity
    | http_entity
    | https_entity
    | file_entity
    | protocol_entity
)


def _parameter_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    par = unquote("".join(toks))
    return StringActionParameter(par, position=position)


parameter = expand_entity | (
    ZeroOrMore(parameter_text | entities | percent_encoding)
    .setParseAction(_parameter_parse_action)
    .setName("parameter")
)


def _action_request_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    name = toks[0]
    parameters = list(toks[1:])
    return ActionRequest(name=name, parameters=parameters, position=position)


action_request = (
    (identifier + ZeroOrMore(Literal("-").suppress() + parameter))
    .setParseAction(_action_request_parse_action)
    .setName("action_request")
)


def _action_path_parse_action(s, loc, toks):
    ap = list(toks)
    if type(ap[-1]) is str:
        return (ap[:-1], ap[-1])
    else:
        return (ap, None)


# segment_indicator = (
#    OneOrMore(Literal("-"))
#    .setParseAction(lambda s, loc, toks: len(toks))
#    .setName("segment_indicator")
# )


def _segment_identifier_action(s, loc, toks):
    m = re.match("(-+)([a-zA-Z0-9_]*)", toks[0])
    assert m is not None
    return len(m.group(1)), m.group(2)


segment_identifier = (
    (Regex("-+[a-z][a-zA-Z0-9_]*") | (Regex("-+") + FollowedBy("/")))
    .setParseAction(_segment_identifier_action)
    .setName("segment_identifier")
)


def _resource_identifier_action(s, loc, toks):
    m = re.match("(-+)R([a-zA-Z0-9_]*)", toks[0])
    assert m is not None
    return len(m.group(1)), m.group(2)


resource_identifier = (
    (Regex("-+R[a-zA-Z0-9_]*"))
    .setParseAction(_resource_identifier_action)
    .setName("resource_identifier")
)

action_path_nonempty = (
    (
        (
            ZeroOrMore(
                action_request
                + (
                    Literal("/") + ~(resource_identifier | segment_identifier)
                ).suppress()
            )
            + (filename | action_request)
        )
    )
    .setParseAction(_action_path_parse_action)
    .setName("action_path_nonempty")
)


def _resource_path_parse_action(s, loc, toks):
    return list(toks)


resource_path = (
    delimitedList(resource_name, delim="/")
    .setParseAction(_resource_path_parse_action)
    .setName("resource_path")
)


def _segment_header_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    level, name = toks[0]
    if len(toks) > 1:
        parameters = list(toks[1:])
        return SegmentHeader(
            name=name, level=level, parameters=parameters, position=position
        )
    else:
        return SegmentHeader(level=level, name=name)


segment_header = (
    (segment_identifier + ZeroOrMore(Literal("-").suppress() + parameter))
    .setParseAction(_segment_header_parse_action)
    .setName("segment_header")
)


def _resource_segment_with_header_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    level, name = toks[0]
    header = SegmentHeader(
        name=name,
        level=level,
        parameters=list(toks[1]),
        resource=True,
        position=position,
    )
    if len(toks) == 3:
        return ResourceQuerySegment(header=header, query=list(toks[2]))
    else:
        return ResourceQuerySegment(header=header, query=[])


resource_segment_with_header = (
    (
        resource_identifier
        + Group(ZeroOrMore(Word("-").suppress() + parameter))
        + Literal("/").suppress()
        + Optional(Group(resource_path))
    )
    .setParseAction(_resource_segment_with_header_parse_action)
    .setName("resource_segment_with_header")
)


def _segment_with_header_parse_action(s, loc, toks):
    if len(toks) == 1:
        return TransformQuerySegment(header=toks[0])
    else:
        assert len(toks) == 3
        return TransformQuerySegment(
            header=toks[0], query=toks[2][0][0], filename=toks[2][0][1]
        )


segment_with_header = (
    (segment_header + Optional(Literal("/") + Group(action_path_nonempty)))
    .setParseAction(_segment_with_header_parse_action)
    .setName("segment_with_header")
)


def _segment_without_header_parse_action(s, loc, toks):
    return TransformQuerySegment(query=list(toks[0][0][0]), filename=toks[0][0][1])


segment_without_header = (
    Group(action_path_nonempty)
    .setParseAction(_segment_without_header_parse_action)
    .setName("segment_without_header")
)

query_segment = (
    segment_with_header | segment_without_header | resource_segment_with_header
).setName("query_segment")


def _parse_query_parse_action(s, loc, toks):
    if toks[0] == "/":
        return Query(segments=list(toks[1:]), absolute=True)
    else:
        return Query(segments=list(toks), absolute=False)


parse_query << (
    (Optional(Literal("/")) + delimitedList(query_segment, "/"))
    .setParseAction(_parse_query_parse_action)
    .setName("parse_query")
)


def _resource_transform_query_action(s, loc, toks):
    if toks[0] == "/":
        assert len(toks) == 3
        resource = ResourceQuerySegment(query=list(toks[1]))
        transform = toks[2]
        return Query(segments=[resource, transform], absolute=True)
    else:
        assert len(toks) == 2
        resource = ResourceQuerySegment(query=list(toks[0]))
        transform = toks[1]
        return Query(segments=[resource, transform], absolute=False)


resource_transform_query = (
    (
        Optional(Literal("/"))
        + Group(resource_path)
        + Literal("/").suppress()
        + segment_with_header
    )
    .setParseAction(_resource_transform_query_action)
    .setName("resource_transform_query")
)


def parse(query):
    try:
        return resource_transform_query.parseString(query, True)[0]
    except:
        return parse_query.parseString(query, True)[0]


if __name__ == "__main__":
    #    print(action_path_nonempty.parseString("abc/def/file.txt", True))
    #    print(query_segment.parseString("abc/def/file.txt", True))
    # print(resource_path.parseString("abc/def/file.txt", True))
    #    print(query_segment.parseString("-qs/abc/def/file.txt", True))
    #    print(parse_query.parseString("abc/def/file.txt", True))
    #    print(parse_query.parseString("-/abc/def/file.txt", True))
    #    print(parse_query.parseString("-qs/abc/def/file.txt", True))
    #    print(parse_query.parseString("-/xxx", True))
    #    print(parse_query.parseString("abc/def/-/xxx", True))
    #    print(parse_query.parseString("abc/def/-/xxx/file.txt", True))
    #    print(repr(parse("abc/def/-/xxx/file.txt")))
    #    print(repr(parse("abc/def")))

    #    print(parse("abc-def/-/x-y/--xxx-y/aaa"))
    #    print(repr(parse("/abc-def/-/x-y/--xxx-y/aaa")))
    #    print(resource_segment_with_header.parseString("-R/abc/def"))
    # print(resource_segment_with_header.parseString("-R-1-2/"))
    print(repr(parse("-R/abc/def/-/ghi")))
    print((parse("-R/abc/def/-/ghi")))
    print()
    for p, r in parse("-R/abc/def/-/ghi/jkl/file.txt").all_predecessors():
        print(p.encode(), "   -   ", "NONE" if r is None else r.encode())
    #    print(parse("abc/xxx/-/def"))
    #    print(repr(parse("-/def")))
    #    print(repr(parse("def")))
    #    print(repr(action_path_nonempty.parseString("abc", True)))
    #    print(repr(expand_entity.parseString("~X~abc~E", True)))
    #    print(repr(parse("-/def-~X~abc~E")))
    print(parse("abc/def/-/xxx/-q/qqq"))
    print(parse("abc/def/-/xxx/-q/qqq-abc-~X~xxx/yyy~E-def"))
    print(repr(parse("abc/def/-/xxx/-q/qqq-abc-~X~xxx/yyy~E-def")))

from urllib.parse import quote, unquote
import re
from pyparsing import (
    Literal,
    Word,
    alphas,
    alphanums,
    nums,
    delimitedList,
    Regex,
    OneOrMore,
    Group,
    Combine,
    ZeroOrMore,
    Optional,
    Forward,
    lineno,
    col,
    FollowedBy,
)

"""
This module provides functions to decode and encode query.
A query is a sequence of commands, each command being a list of strings starting with the command name as a first element.
Commands are separated by a COMMAND_SEPARATOR, parts of the command (tokens) by PARAMETER_SEPARATOR.
Having special charachers (like COMMAND_SEPARATOR or PARAMETER_SEPARATOR) inside the commands is possible
by escaping with ESCAPE character and using escape sequences.
Query is URL-safe and URL escaping is applied to other special characters.
Empty commands are ignored (e.g. two consecutive COMMAND_SEPARATORs are equivalent to a single COMMAND_SEPARATOR).

Query can be decoded from string to a list of lists of strings by decode (or encoded by encode).
Encode and decode use encode_token and decode_token that are acting on a token level.
"""

COMMAND_SEPARATOR = "/"
PARAMETER_SEPARATOR = "-"
ESCAPE = "~"
ESCAPE_SEQUENCES = [
    (ESCAPE, ESCAPE + ESCAPE),
    ("https://", ESCAPE + "H"),
    ("http://", ESCAPE + "h"),
    ("file://", ESCAPE + "f"),
    ("://", ESCAPE + "P"),
    (COMMAND_SEPARATOR, ESCAPE + "I"),
    (PARAMETER_SEPARATOR, ESCAPE + "_"),
    (" ", ESCAPE + "."),
]


def decode(query: str):
    """Decode query string into a list of lists of strings."""
    ql = [
        [decode_token(etoken) for etoken in eqv.split(PARAMETER_SEPARATOR)]
        for eqv in query.split(COMMAND_SEPARATOR)
    ]
    return [qcommand for qcommand in ql if len(qcommand) and len(qcommand[0])]


def encode_token(token: str):
    "Encode single token by escaping special characters and character sequences"
    for sequence, encoding in ESCAPE_SEQUENCES:
        token = token.replace(sequence, encoding)
    return quote(token).replace("%7E", "~").replace("%7e", "~")


def decode_token(token: str):
    "Decode single token by un-escaping special characters and character sequences"
    encoding = {e: s for s, e in ESCAPE_SEQUENCES}
    if token == "":
        return ""
    try:
        index = token.index(ESCAPE)
        head = token[:index]
        mid = token[index : index + 2]
        tail = token[index + 2 :]
        return unquote(head + encoding.get(mid, mid)) + decode_token(tail)
    except ValueError:
        return unquote(token)


def encode(ql: list):
    """Decode query list (list of lists of strings) into an properly escaped query string."""
    return COMMAND_SEPARATOR.join(
        PARAMETER_SEPARATOR.join(encode_token(token) for token in qv) for qv in ql
    )


def all_splits(query):
    """Make all splits of the query into two parts (biginning and remainder)
    by progressively increasing the remainder.
    query == beginning + COMMAND_SEPARATOR + remainder
    """
    ql = decode(query)
    for i in range(len(ql), -1, -1):
        yield encode(ql[:i]), encode(ql[i:])


def indent(text, prefix="  "):
    return "\n".join(prefix + x for x in text.split("\n"))


def list_indent(lst, prefix="  "):
    if len(lst):
        return "[\n" + ",\n".join(indent(repr(x), prefix) for x in lst) + "\n]"
    else:
        return "[]"


class Position:
    def __init__(self, offset=0, line=0, column=0):
        self.offset = offset
        self.line = line
        self.column = column

    @classmethod
    def from_loc(cls, loc, string):
        return cls(loc, line=lineno(loc, string), column=col(loc, string))

    @classmethod
    def from_dict(cls, d):
        if d is None:
            return cls()
        else:
            return cls(offset=d["offset"], line=d["line"], column=d["column"])

    def to_dict(self):
        return dict(offset=self.offset, line=self.line, column=self.column)

    def __str__(self):
        if self.line == 0:
            return "(unknown position)"
        elif self.line > 1:
            return f"line {self.line}, position {self.column}"
        else:
            return f"position {self.column}"

    def __repr__(self):
        if self.line == 0:
            return "Position()"
        return f"Position(offset={self.offset}, line={self.line}, column={self.column})"


class QueryException(Exception):
    def __init__(self, message, position=None, query=None):
        self.original_message = message
        if position is not None:
            message += f" at {position}"
            if query is not None:
                message += f":\n  query: {query[:position.offset]}\n     {' '*position.offset}--> {query[position.offset:]}"
        else:
            if query is not None:
                message += f":\n  query: {query}"

        super().__init__(message)
        self.position = position
        self.query = query


class ActionParameter(object):
    def __init__(self, position=None):
        self.position = position or Position()

    def __repr__(self):
        return f"{self.__class__.__name__}({repr(self.position)})"

    def __str__(self):
        return f"{self.__class__.__name__} at {self.position}"


class LinkActionParameter(ActionParameter):
    def __init__(self, link, position=None):
        super().__init__(position)
        self.link = link

    def encode(self):
        return "~X~" + self.link.encode() + "~E"

    def __repr__(self):
        return f"""LinkActionParameter(
{indent(repr(self.link))},
  {repr(self.position)})"""

    def __str__(self):
        return self.encode()


class ExpandedActionParameter(ActionParameter):
    def __init__(self, value, link, position=None):
        super().__init__(position)
        self.value = value
        self.link = link

    def encode(self):
        return "~X~" + self.link.encode() + "~E"

    def __repr__(self):
        return f"""ExpandedActionParameter(
{indent(repr(self.value))}
{indent(repr(self.link))},
  {repr(self.position)})"""

    def __str__(self):
        return str(self.value)


class StringActionParameter(ActionParameter):
    def __init__(self, string: str, position=None):
        super().__init__(position)
        self.string = string

    def encode(self):
        #        encoded = self.string.replace("~","~~").replace("/","~/").replace("://","~P")
        encoded = encode_token(self.string)
        return encoded

    def __repr__(self):
        return f"StringActionParameter({repr(self.string)}, {repr(self.position)})"

    def __str__(self):
        return self.string


class ResourceName(ActionParameter):
    def __init__(self, name: str, position=None):
        self.position = position or Position()
        self.name = name

    def encode(self):
        return self.name

    def __repr__(self):
        return f"ResourceName({repr(self.name)}, {repr(self.position)})"

    def __str__(self):
        return self.encode()


class ActionRequest(object):
    def __init__(self, name: str, parameters=None, position=None):
        self.name = name
        self.parameters = [] if parameters is None else parameters
        self.position = position or Position()

    @classmethod
    def from_arguments(cls, name: str, *parameters):
        assert type(name) == str
        typedparam = []
        for p in parameters:
            if isinstance(p, ActionParameter):
                typedparam.append(p)
            else:
                assert type(p) in (str, int, float, bool)
                typedparam.append(StringActionParameter(str(p)))

        return cls(name, typedparam)

    def to_list(self):
        lst = [self.name]
        for x in self.parameters:
            if isinstance(x, StringActionParameter):
                lst.append(x.string)
            elif isinstance(x, LinkActionParameter):
                lst.append(x.encode())
            else:
                raise Exception(
                    f"Unsupported action parameter type: {type(x)} ({repr(x)})"
                )
        return lst

    @classmethod
    def from_list(cls, lst):
        return cls.from_arguments(lst[0], *lst[1:])

    def encode(self):
        if len(self.parameters):
            p = "-".join(x.encode() for x in self.parameters)
            return f"{self.name}-{p}"
        else:
            return self.name

    def __repr__(self):
        return f"""ActionRequest(
  {repr(self.name)},
{indent(list_indent(self.parameters))},
  {repr(self.position)}
)"""

    def __str__(self):
        return self.encode()


class SegmentHeader(object):
    def __init__(
        self,
        name: str = "",
        level: int = 1,
        parameters: list = None,
        resource: bool = False,
        position=None,
    ):
        self.name = name
        self.level = level
        self.parameters = parameters or []
        self.resource = resource
        self.position = position or Position()

    def encode(self):
        assert self.level >= 1
        encoded = "-" * self.level
        if self.resource:
            encoded += "R"
        encoded += self.name
        if len(self.parameters):
            assert len(self.name) > 0 or self.resource
            for parameter in self.parameters:
                encoded += "-"
                encoded += parameter.encode()
        return encoded

    def __repr__(self):
        return f"""SegmentHeader(
  name       ={repr(self.name)},
  level      ={self.level},
  parameters ={indent(list_indent(self.parameters))},
  resource   ={self.resource},
  position   ={repr(self.position)}
)"""

    def __str__(self):
        return self.encode()


class TransformQuerySegment(object):
    def __init__(self, header=None, query=None, filename=None):
        "header can be SegmentHeader, query is a list of ActionRequest objects"
        self.header = header
        self.query = query or []
        self.filename = filename

    def predecessor(self):
        if self.filename is None:
            if len(self.query):
                p = TransformQuerySegment(
                    header=self.header, query=self.query[:-1], filename=None
                )
                r = TransformQuerySegment(
                    header=self.header, query=[self.query[-1]], filename=None
                )
                return p, r
            else:
                return None, None
        else:
            p = TransformQuerySegment(
                header=self.header, query=self.query, filename=None
            )
            r = TransformQuerySegment(
                header=self.header, query=[], filename=self.filename
            )
            return p, r

    def is_empty(self):
        return len(self.query) == 0 and self.filename is None

    def is_filename(self):
        return len(self.query) == 0 and self.filename is not None

    def is_action_request(self):
        return len(self.query) == 1 and self.filename is None

    def action(self):
        if self.is_action_request():
            return self.query[0]
        else:
            return None

    def encode(self):
        query = "/".join(x.encode() for x in self.query)
        if self.filename is not None:
            query = f"{query}/{self.filename}" if len(query) else self.filename

        if self.header is None:
            return query
        else:
            if len(query):
                return f"{self.header.encode()}/{query}"
            else:
                return self.header.encode()

    def append(self, q):
        if q is None:
            return self
        if isinstance(q, TransformQuerySegment):
            self.query.extend(q.query)
            self.filename = q.filename
            return self
        if isinstance(q, Query):
            if q.is_transform_query():
                return self.append(q.segments[0])
            else:
                raise Exception(
                    f"Appending general query {q} to transform query {self.encode()} not supported"
                )
        if isinstance(q, ActionRequest):
            self.query.append(q)
            return self
        if isinstance(q, str):
            return self.append(parse(q))

        raise Exception(
            f"Transform query {self.encode()} can't append object {repr(q)}"
        )

    def __add__(self, q):
        if q is None:
            return self
        if isinstance(q, TransformQuerySegment):
            return TransformQuerySegment(
                header=self.header, query=self.query + q.query, filename=q.filename
            )

        raise Exception(f"Unsupported operation (add): {self.encode()} + {repr(q)}")

    def __radd__(self, q):
        if q is None:
            return self
        if isinstance(q, TransformQuerySegment):
            return q + self
        raise Exception(f"Unsupported operation (radd): {repr(q)} + {self.encode()}")

    def __repr__(self):
        return f"""TransformQuerySegment(
  header   = {indent(repr(self.header))},
  query    = {indent(list_indent(self.query))},
  filename = {repr(self.filename)}
)"""

    def __str__(self):
        return self.encode()


class ResourceQuerySegment(object):
    def __init__(self, header=None, query=None):
        "header can be SegmentHeader, query is a list of ActionRequest objects"
        self.header = header
        self.query = query or []

    @property
    def position(self):
        if self.header is not None:
            return self.header.position
        else:
            if len(self.query):
                return self.query[0].position

    def path(self):
        return "/".join(x.encode() for x in self.query)

    def encode(self):
        query = self.path()
        if self.header is None:
            rqs = ""
        else:
            rqs = self.header.encode()
        if len(rqs):
            rqs += "/"
        if len(query):
            return f"{rqs}{query}"
        else:
            return rqs

    def __repr__(self):
        return f"""ResourceQuerySegment(
  header = {indent(repr(self.header))},
  query  = {indent(list_indent(self.query))}
)"""

    def __str__(self):
        return self.encode()


class Query(object):
    def __init__(self, segments: list = None, absolute=False):
        self.segments = segments or []
        self.absolute = absolute

    def filename(self):
        if len(self.segments):
            segment = self.segments[-1]
            if (
                isinstance(segment, TransformQuerySegment)
                and segment.filename is not None
            ):
                return str(segment.filename)
            if (
                isinstance(segment, ResourceQuerySegment)
                and segment.query is not None
                and len(segment.query)
            ):
                return str(segment.query[-1])
        return None

    def is_empty(self):
        return len(self.segments) == 0

    def is_transform_query(self):
        return len(self.segments) == 1 and isinstance(
            self.segments[0], TransformQuerySegment
        )

    def transform_query(self):
        if self.is_transform_query():
            return self.segments[0]
        else:
            return None

    def is_resource_query(self):
        return len(self.segments) == 1 and isinstance(
            self.segments[0], ResourceQuerySegment
        )

    def resource_query(self):
        if self.is_resource_query():
            return self.segments[0]
        else:
            return None

    def is_action_request(self):
        return self.is_transform_query() and self.segments[0].is_action_request()

    def action(self):
        if self.is_action_request():
            return self.segments[0].action()
        else:
            return None

    def predecessor(self):
        if len(self.segments):
            if isinstance(self.segments[-1], TransformQuerySegment):
                p, r = self.segments[-1].predecessor()
                if p is None or p.is_empty():
                    qp = Query(self.segments[:-1], absolute=self.absolute)
                    qr = r
                    return qp, qr
                else:
                    qp = Query(self.segments[:-1] + [p], absolute=self.absolute)
                    qr = r
                    return qp, qr
            else:
                return None, None
        else:
            return None, None

    def short(self):
        _, r = self.predecessor()
        if r is None:
            q = str(self)
            if len(self) > 30:
                q = "..." + q[-30:]
            return q
        else:
            return str(r)

    def all_predecessors(self):
        qp, qr = self, None
        while qp is not None:
            yield qp, qr
            qp, r = qp.predecessor()
            qr = r + qr

    def create_segment(self, name: str = "", level=1):
        qs = TransformQuerySegment(SegmentHeader(name, level=level))
        self.segments.append(qs)
        return qs

    def last_transform_query_segment(self):
        if not self.is_empty():
            if isinstance(self.segments[-1], TransformQuerySegment):
                return self.segments[-1]
        return self.create_segment()

    def with_action(self, name: str, *parameters):
        self.last_transform_query_segment().append(
            ActionRequest.from_arguments(name, *parameters)
        )
        return self

    def __add__(self, tq):
        assert isinstance(tq, TransformQuerySegment)
        return Query(self.segments + [tq], absolute=self.absolute)

    def encode(self):
        return ("/" if self.absolute else "") + "/".join(
            x.encode() for x in self.segments
        )

    def __repr__(self):
        return f"""Query(
{indent(list_indent(self.segments))},
  absolute = {self.absolute}
)"""

    def __str__(self):
        return self.encode()


identifier = Regex("[a-z_][a-zA-Z0-9_]*").setName("identifier")
filename = Regex(r"[a-zA-Z0-9_]*\.[a-zA-Z0-9._-]*").setName("filename")


def _resource_name_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    name = unquote("".join(toks))
    return ResourceName(name, position=position)


resource_name = (
    Regex(r"[a-zA-Z0-9._][a-zA-Z0-9._-]*")
    .setParseAction(_resource_name_parse_action)
    .setName("resource_name")
)
parameter_text = Regex("[a-zA-Z0-9_+.]+").setName("parameter_text")
percent_encoding = Regex("%[0-9a-fA-F][0-9a-fA-F]").setName("percent_encoding")
parse_query = Forward()

tilde_entity = (
    Literal("~~").setParseAction(lambda s, loc, toks: ["~"]).setName("tilde_entity")
)
minus_entity = (
    Literal("~_").setParseAction(lambda s, loc, toks: ["-"]).setName("minus_entity")
)
islash_entity = (
    Literal("~I").setParseAction(lambda s, loc, toks: ["/"]).setName("islash_entity")
)
slash_entity = (
    Literal("~/").setParseAction(lambda s, loc, toks: ["/"]).setName("slash_entity")
)

https_entity = (
    Literal("~H")
    .setParseAction(lambda s, loc, toks: ["https://"])
    .setName("https_entity")
)
http_entity = (
    Literal("~h")
    .setParseAction(lambda s, loc, toks: ["http://"])
    .setName("http_entity")
)
file_entity = (
    Literal("~f")
    .setParseAction(lambda s, loc, toks: ["file://"])
    .setName("file_entity")
)
protocol_entity = (
    Literal("~P")
    .setParseAction(lambda s, loc, toks: ["://"])
    .setName("protocol_entity")
)

negative_number_entity = (
    Regex("~[0-9]")
    .setParseAction(lambda s, loc, toks: ["-" + toks[0][1:]])
    .setName("negative_number_entity")
)
space_entity = (
    Literal("~.").setParseAction(lambda s, loc, toks: [" "]).setName("space_entity")
)
end_entity = Literal("~E")


def _expand_entity_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    return LinkActionParameter(toks[0], position=position)


expand_entity = (
    (Literal("~X~").suppress() + parse_query + end_entity.suppress())
    .setParseAction(_expand_entity_parse_action)
    .setName("expand_entity")
)

entities = (
    tilde_entity
    | minus_entity
    | negative_number_entity
    | space_entity
    | islash_entity
    | slash_entity
    | http_entity
    | https_entity
    | file_entity
    | protocol_entity
)


def _parameter_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    par = unquote("".join(toks))
    return StringActionParameter(par, position=position)


parameter = expand_entity | (
    ZeroOrMore(parameter_text | entities | percent_encoding)
    .setParseAction(_parameter_parse_action)
    .setName("parameter")
)


def _action_request_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    name = toks[0]
    parameters = list(toks[1:])
    return ActionRequest(name=name, parameters=parameters, position=position)


action_request = (
    (identifier + ZeroOrMore(Literal("-").suppress() + parameter))
    .setParseAction(_action_request_parse_action)
    .setName("action_request")
)


def _action_path_parse_action(s, loc, toks):
    ap = list(toks)
    if type(ap[-1]) is str:
        return (ap[:-1], ap[-1])
    else:
        return (ap, None)


# segment_indicator = (
#    OneOrMore(Literal("-"))
#    .setParseAction(lambda s, loc, toks: len(toks))
#    .setName("segment_indicator")
# )


def _segment_identifier_action(s, loc, toks):
    m = re.match("(-+)([a-zA-Z0-9_]*)", toks[0])
    assert m is not None
    return len(m.group(1)), m.group(2)


segment_identifier = (
    (Regex("-+[a-z][a-zA-Z0-9_]*") | (Regex("-+") + FollowedBy("/")))
    .setParseAction(_segment_identifier_action)
    .setName("segment_identifier")
)


def _resource_identifier_action(s, loc, toks):
    m = re.match("(-+)R([a-zA-Z0-9_]*)", toks[0])
    assert m is not None
    return len(m.group(1)), m.group(2)


resource_identifier = (
    (Regex("-+R[a-zA-Z0-9_]*"))
    .setParseAction(_resource_identifier_action)
    .setName("resource_identifier")
)

action_path_nonempty = (
    (
        (
            ZeroOrMore(
                action_request
                + (
                    Literal("/") + ~(resource_identifier | segment_identifier)
                ).suppress()
            )
            + (filename | action_request)
        )
    )
    .setParseAction(_action_path_parse_action)
    .setName("action_path_nonempty")
)


def _resource_path_parse_action(s, loc, toks):
    return list(toks)


resource_path = (
    delimitedList(resource_name, delim="/")
    .setParseAction(_resource_path_parse_action)
    .setName("resource_path")
)


def _segment_header_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    level, name = toks[0]
    if len(toks) > 1:
        parameters = list(toks[1:])
        return SegmentHeader(
            name=name, level=level, parameters=parameters, position=position
        )
    else:
        return SegmentHeader(level=level, name=name)


segment_header = (
    (segment_identifier + ZeroOrMore(Literal("-").suppress() + parameter))
    .setParseAction(_segment_header_parse_action)
    .setName("segment_header")
)


def _resource_segment_with_header_parse_action(s, loc, toks):
    position = Position.from_loc(loc, s)
    level, name = toks[0]
    header = SegmentHeader(
        name=name,
        level=level,
        parameters=list(toks[1]),
        resource=True,
        position=position,
    )
    if len(toks) == 3:
        return ResourceQuerySegment(header=header, query=list(toks[2]))
    else:
        return ResourceQuerySegment(header=header, query=[])


resource_segment_with_header = (
    (
        resource_identifier
        + Group(ZeroOrMore(Word("-").suppress() + parameter))
        + Literal("/").suppress()
        + Optional(Group(resource_path))
    )
    .setParseAction(_resource_segment_with_header_parse_action)
    .setName("resource_segment_with_header")
)


def _segment_with_header_parse_action(s, loc, toks):
    if len(toks) == 1:
        return TransformQuerySegment(header=toks[0])
    else:
        assert len(toks) == 3
        return TransformQuerySegment(
            header=toks[0], query=toks[2][0][0], filename=toks[2][0][1]
        )


segment_with_header = (
    (segment_header + Optional(Literal("/") + Group(action_path_nonempty)))
    .setParseAction(_segment_with_header_parse_action)
    .setName("segment_with_header")
)


def _segment_without_header_parse_action(s, loc, toks):
    return TransformQuerySegment(query=list(toks[0][0][0]), filename=toks[0][0][1])


segment_without_header = (
    Group(action_path_nonempty)
    .setParseAction(_segment_without_header_parse_action)
    .setName("segment_without_header")
)

query_segment = (
    segment_with_header | segment_without_header | resource_segment_with_header
).setName("query_segment")


def _parse_query_parse_action(s, loc, toks):
    if toks[0] == "/":
        return Query(segments=list(toks[1:]), absolute=True)
    else:
        return Query(segments=list(toks), absolute=False)


parse_query << (
    (Optional(Literal("/")) + delimitedList(query_segment, "/"))
    .setParseAction(_parse_query_parse_action)
    .setName("parse_query")
)


def _resource_transform_query_action(s, loc, toks):
    if toks[0] == "/":
        assert len(toks) == 3
        resource = ResourceQuerySegment(query=list(toks[1]))
        transform = toks[2]
        return Query(segments=[resource, transform], absolute=True)
    else:
        assert len(toks) == 2
        resource = ResourceQuerySegment(query=list(toks[0]))
        transform = toks[1]
        return Query(segments=[resource, transform], absolute=False)


resource_transform_query = (
    (
        Optional(Literal("/"))
        + Group(resource_path)
        + Literal("/").suppress()
        + segment_with_header
    )
    .setParseAction(_resource_transform_query_action)
    .setName("resource_transform_query")
)


def parse(query):
    try:
        return resource_transform_query.parseString(query, True)[0]
    except:
        return parse_query.parseString(query, True)[0]


if __name__ == "__main__":
    #    print(action_path_nonempty.parseString("abc/def/file.txt", True))
    #    print(query_segment.parseString("abc/def/file.txt", True))
    # print(resource_path.parseString("abc/def/file.txt", True))
    #    print(query_segment.parseString("-qs/abc/def/file.txt", True))
    #    print(parse_query.parseString("abc/def/file.txt", True))
    #    print(parse_query.parseString("-/abc/def/file.txt", True))
    #    print(parse_query.parseString("-qs/abc/def/file.txt", True))
    #    print(parse_query.parseString("-/xxx", True))
    #    print(parse_query.parseString("abc/def/-/xxx", True))
    #    print(parse_query.parseString("abc/def/-/xxx/file.txt", True))
    #    print(repr(parse("abc/def/-/xxx/file.txt")))
    #    print(repr(parse("abc/def")))

    #    print(parse("abc-def/-/x-y/--xxx-y/aaa"))
    #    print(repr(parse("/abc-def/-/x-y/--xxx-y/aaa")))
    #    print(resource_segment_with_header.parseString("-R/abc/def"))
    # print(resource_segment_with_header.parseString("-R-1-2/"))
    print(repr(parse("-R/abc/def/-/ghi")))
    print((parse("-R/abc/def/-/ghi")))
    print()
    for p, r in parse("-R/abc/def/-/ghi/jkl/file.txt").all_predecessors():
        print(p.encode(), "   -   ", "NONE" if r is None else r.encode())
    #    print(parse("abc/xxx/-/def"))
    #    print(repr(parse("-/def")))
    #    print(repr(parse("def")))
    #    print(repr(action_path_nonempty.parseString("abc", True)))
    #    print(repr(expand_entity.parseString("~X~abc~E", True)))
    #    print(repr(parse("-/def-~X~abc~E")))
    print(parse("abc/def/-/xxx/-q/qqq"))
    print(parse("abc/def/-/xxx/-q/qqq-abc-~X~xxx/yyy~E-def"))
    print(repr(parse("abc/def/-/xxx/-q/qqq-abc-~X~xxx/yyy~E-def")))
This module provides functions to decode and encode query. A query is a sequence of commands, each command being a list of strings starting with the command name as a first element. Commands are separated by a COMMAND_SEPARATOR, parts of the command (tokens) by PARAMETER_SEPARATOR. Having special charachers (like COMMAND_SEPARATOR or PARAMETER_SEPARATOR) inside the commands is possible by escaping with ESCAPE character and using escape sequences. Query is URL-safe and URL escaping is applied to other special characters. Empty commands are ignored (e.g. two consecutive COMMAND_SEPARATORs are equivalent to a single COMMAND_SEPARATOR).  Query can be decoded from string to a list of lists of strings by decode (or encoded by encode). Encode and decode use encode_token and decode_token that are acting on a token level. Decode query string into a list of lists of strings. Decode query list (list of lists of strings) into an properly escaped query string. Make all splits of the query into two parts (biginning and remainder)     by progressively increasing the remainder.     query == beginning + COMMAND_SEPARATOR + remainder LinkActionParameter( {indent(repr(self.link))},   {repr(self.position)}) ExpandedActionParameter( {indent(repr(self.value))} {indent(repr(self.link))},   {repr(self.position)}) #        encoded = self.string.replace("~","~~").replace("/","~/").replace("://","~P") ActionRequest(   {repr(self.name)}, {indent(list_indent(self.parameters))},   {repr(self.position)} ) SegmentHeader(   name       ={repr(self.name)},   level      ={self.level},   parameters ={indent(list_indent(self.parameters))},   resource   ={self.resource},   position   ={repr(self.position)} ) TransformQuerySegment(   header   = {indent(repr(self.header))},   query    = {indent(list_indent(self.query))},   filename = {repr(self.filename)} ) ResourceQuerySegment(   header = {indent(repr(self.header))},   query  = {indent(list_indent(self.query))} ) Query( {indent(list_indent(self.segments))},   absolute = {self.absolute} ) # segment_indicator = ( #    OneOrMore(Literal("-")) #    .setParseAction(lambda s, loc, toks: len(toks)) #    .setName("segment_indicator") # ) #    print(action_path_nonempty.parseString("abc/def/file.txt", True)) #    print(query_segment.parseString("abc/def/file.txt", True)) # print(resource_path.parseString("abc/def/file.txt", True)) #    print(query_segment.parseString("-qs/abc/def/file.txt", True)) #    print(parse_query.parseString("abc/def/file.txt", True)) #    print(parse_query.parseString("-/abc/def/file.txt", True)) #    print(parse_query.parseString("-qs/abc/def/file.txt", True)) #    print(parse_query.parseString("-/xxx", True)) #    print(parse_query.parseString("abc/def/-/xxx", True)) #    print(parse_query.parseString("abc/def/-/xxx/file.txt", True)) #    print(repr(parse("abc/def/-/xxx/file.txt"))) #    print(repr(parse("abc/def"))) #    print(parse("abc-def/-/x-y/--xxx-y/aaa")) #    print(repr(parse("/abc-def/-/x-y/--xxx-y/aaa"))) #    print(resource_segment_with_header.parseString("-R/abc/def")) # print(resource_segment_with_header.parseString("-R-1-2/")) #    print(parse("abc/xxx/-/def")) #    print(repr(parse("-/def"))) #    print(repr(parse("def"))) #    print(repr(action_path_nonempty.parseString("abc", True))) #    print(repr(expand_entity.parseString("~X~abc~E", True))) #    print(repr(parse("-/def-~X~abc~E")))