add proxy for the first part and try to develop the second part
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
@@grammar :: contentline
|
||||
@@whitespace :: None
|
||||
@@left_recursion :: False
|
||||
|
||||
# generate with tatsu --generate-parser --outfile ics/grammar/contentline.py ics/grammar/contentline.ebnf
|
||||
|
||||
start = @:contentline $;
|
||||
full = @+:contentline { /(\r?\n)+/ @+:contentline }+ /(\r?\n)*/ $;
|
||||
|
||||
contentline = name:ALPHADIGIT_MINUS_PLUS { ";" params+:param }* ":" value:VALUE_CHAR_STAR ;
|
||||
param = name:ALPHADIGIT_MINUS_PLUS "=" values+:param_value { "," values+:param_value }* ;
|
||||
param_value = DQUOTE ~ value:QSAFE_CHAR_STAR DQUOTE quoted:`true` | value:SAFE_CHAR_STAR quoted:`false` ;
|
||||
|
||||
ALPHADIGIT_MINUS_PLUS = /[a-zA-Z0-9-]+/;
|
||||
QSAFE_CHAR_STAR = /[^\x00-\x08\x0A-\x1F\x22\x7F]*/;
|
||||
SAFE_CHAR_STAR = /[^\x00-\x08\x0A-\x1F\x22\x2C\x3A\x3B\x7F]*/;
|
||||
VALUE_CHAR_STAR = /[^\x00-\x08\x0A-\x1F\x7F]*/;
|
||||
DQUOTE = '"';
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# CAVEAT UTILITOR
|
||||
#
|
||||
# This file was automatically generated by TatSu.
|
||||
#
|
||||
# https://pypi.python.org/pypi/tatsu/
|
||||
#
|
||||
# Any changes you make to it will be overwritten the next time
|
||||
# the file is generated.
|
||||
|
||||
|
||||
from __future__ import print_function, division, absolute_import, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from tatsu.buffering import Buffer
|
||||
from tatsu.parsing import Parser
|
||||
from tatsu.parsing import tatsumasu, leftrec, nomemo
|
||||
from tatsu.parsing import leftrec, nomemo # noqa
|
||||
from tatsu.util import re, generic_main # noqa
|
||||
|
||||
|
||||
KEYWORDS = {} # type: ignore
|
||||
|
||||
|
||||
class contentlineBuffer(Buffer):
|
||||
def __init__(
|
||||
self,
|
||||
text,
|
||||
whitespace=None,
|
||||
nameguard=None,
|
||||
comments_re=None,
|
||||
eol_comments_re=None,
|
||||
ignorecase=None,
|
||||
namechars='',
|
||||
**kwargs
|
||||
):
|
||||
super(contentlineBuffer, self).__init__(
|
||||
text,
|
||||
whitespace=whitespace,
|
||||
nameguard=nameguard,
|
||||
comments_re=comments_re,
|
||||
eol_comments_re=eol_comments_re,
|
||||
ignorecase=ignorecase,
|
||||
namechars=namechars,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class contentlineParser(Parser):
|
||||
def __init__(
|
||||
self,
|
||||
whitespace=None,
|
||||
nameguard=None,
|
||||
comments_re=None,
|
||||
eol_comments_re=None,
|
||||
ignorecase=None,
|
||||
left_recursion=False,
|
||||
parseinfo=True,
|
||||
keywords=None,
|
||||
namechars='',
|
||||
buffer_class=contentlineBuffer,
|
||||
**kwargs
|
||||
):
|
||||
if keywords is None:
|
||||
keywords = KEYWORDS
|
||||
super(contentlineParser, self).__init__(
|
||||
whitespace=whitespace,
|
||||
nameguard=nameguard,
|
||||
comments_re=comments_re,
|
||||
eol_comments_re=eol_comments_re,
|
||||
ignorecase=ignorecase,
|
||||
left_recursion=left_recursion,
|
||||
parseinfo=parseinfo,
|
||||
keywords=keywords,
|
||||
namechars=namechars,
|
||||
buffer_class=buffer_class,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@tatsumasu()
|
||||
def _start_(self): # noqa
|
||||
self._contentline_()
|
||||
self.name_last_node('@')
|
||||
self._check_eof()
|
||||
|
||||
@tatsumasu()
|
||||
def _full_(self): # noqa
|
||||
self._contentline_()
|
||||
self.add_last_node_to_name('@')
|
||||
|
||||
def block1():
|
||||
self._pattern('(\\r?\\n)+')
|
||||
self._contentline_()
|
||||
self.add_last_node_to_name('@')
|
||||
self._positive_closure(block1)
|
||||
self._pattern('(\\r?\\n)*')
|
||||
self._check_eof()
|
||||
|
||||
@tatsumasu()
|
||||
def _contentline_(self): # noqa
|
||||
self._ALPHADIGIT_MINUS_PLUS_()
|
||||
self.name_last_node('name')
|
||||
|
||||
def block1():
|
||||
self._token(';')
|
||||
self._param_()
|
||||
self.add_last_node_to_name('params')
|
||||
self._closure(block1)
|
||||
self._token(':')
|
||||
self._VALUE_CHAR_STAR_()
|
||||
self.name_last_node('value')
|
||||
self.ast._define(
|
||||
['name', 'value'],
|
||||
['params']
|
||||
)
|
||||
|
||||
@tatsumasu()
|
||||
def _param_(self): # noqa
|
||||
self._ALPHADIGIT_MINUS_PLUS_()
|
||||
self.name_last_node('name')
|
||||
self._token('=')
|
||||
self._param_value_()
|
||||
self.add_last_node_to_name('values')
|
||||
|
||||
def block2():
|
||||
self._token(',')
|
||||
self._param_value_()
|
||||
self.add_last_node_to_name('values')
|
||||
self._closure(block2)
|
||||
self.ast._define(
|
||||
['name'],
|
||||
['values']
|
||||
)
|
||||
|
||||
@tatsumasu()
|
||||
def _param_value_(self): # noqa
|
||||
with self._choice():
|
||||
with self._option():
|
||||
self._DQUOTE_()
|
||||
self._cut()
|
||||
self._QSAFE_CHAR_STAR_()
|
||||
self.name_last_node('value')
|
||||
self._DQUOTE_()
|
||||
self._constant('true')
|
||||
self.name_last_node('quoted')
|
||||
with self._option():
|
||||
self._SAFE_CHAR_STAR_()
|
||||
self.name_last_node('value')
|
||||
self._constant('false')
|
||||
self.name_last_node('quoted')
|
||||
self._error('no available options')
|
||||
self.ast._define(
|
||||
['quoted', 'value'],
|
||||
[]
|
||||
)
|
||||
|
||||
@tatsumasu()
|
||||
def _ALPHADIGIT_MINUS_PLUS_(self): # noqa
|
||||
self._pattern('[a-zA-Z0-9-]+')
|
||||
|
||||
@tatsumasu()
|
||||
def _QSAFE_CHAR_STAR_(self): # noqa
|
||||
self._pattern('[^\\x00-\\x08\\x0A-\\x1F\\x22\\x7F]*')
|
||||
|
||||
@tatsumasu()
|
||||
def _SAFE_CHAR_STAR_(self): # noqa
|
||||
self._pattern('[^\\x00-\\x08\\x0A-\\x1F\\x22\\x2C\\x3A\\x3B\\x7F]*')
|
||||
|
||||
@tatsumasu()
|
||||
def _VALUE_CHAR_STAR_(self): # noqa
|
||||
self._pattern('[^\\x00-\\x08\\x0A-\\x1F\\x7F]*')
|
||||
|
||||
@tatsumasu()
|
||||
def _DQUOTE_(self): # noqa
|
||||
self._token('"')
|
||||
|
||||
|
||||
class contentlineSemantics(object):
|
||||
def start(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def full(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def contentline(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def param(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def param_value(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def ALPHADIGIT_MINUS_PLUS(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def QSAFE_CHAR_STAR(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def SAFE_CHAR_STAR(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def VALUE_CHAR_STAR(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
def DQUOTE(self, ast): # noqa
|
||||
return ast
|
||||
|
||||
|
||||
def main(filename, start=None, **kwargs):
|
||||
if start is None:
|
||||
start = 'start'
|
||||
if not filename or filename == '-':
|
||||
text = sys.stdin.read()
|
||||
else:
|
||||
with open(filename) as f:
|
||||
text = f.read()
|
||||
parser = contentlineParser()
|
||||
return parser.parse(text, rule_name=start, filename=filename, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import json
|
||||
from tatsu.util import asjson
|
||||
|
||||
ast = generic_main(main, contentlineParser, name='contentline')
|
||||
print('AST:')
|
||||
print(ast)
|
||||
print()
|
||||
print('JSON:')
|
||||
print(json.dumps(asjson(ast), indent=2))
|
||||
print()
|
||||
@@ -0,0 +1,227 @@
|
||||
import collections
|
||||
import re
|
||||
from typing import Dict, List
|
||||
|
||||
import attr
|
||||
import tatsu
|
||||
|
||||
from ics.grammar.contentline import contentlineParser
|
||||
|
||||
# grammar_path = Path(__file__).parent.joinpath('contentline.ebnf')
|
||||
#
|
||||
# with open(grammar_path) as fd:
|
||||
# GRAMMAR = tatsu.compile(fd.read())
|
||||
|
||||
GRAMMAR = contentlineParser()
|
||||
|
||||
|
||||
@attr.s(auto_exc=True)
|
||||
class ParseError(Exception):
|
||||
line: str = attr.ib(default="")
|
||||
nr: int = attr.ib(default=-1)
|
||||
|
||||
|
||||
class ContentLine:
|
||||
"""
|
||||
Represents one property line.
|
||||
|
||||
For example:
|
||||
|
||||
``FOO;BAR=1:YOLO`` is represented by
|
||||
|
||||
``ContentLine('FOO', {'BAR': ['1']}, 'YOLO'))``
|
||||
|
||||
Args:
|
||||
|
||||
name: The name of the property (uppercased for consistency and
|
||||
easier comparison)
|
||||
params: A map name:list of values
|
||||
value: The value of the property
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
ret = (self.name == other.name and
|
||||
self.params == other.params and
|
||||
self.value == other.value)
|
||||
return ret
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __init__(self, name: str, params: Dict[str, List[str]] = {}, value: str = ''):
|
||||
self.name = name.upper()
|
||||
self.params = params
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
params_str = ''
|
||||
for pname in self.params:
|
||||
params_str += ';{}={}'.format(pname, ','.join(self.params[pname]))
|
||||
return "{}{}:{}".format(self.name, params_str, self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "<ContentLine '{}' with {} parameter{}. Value='{}'>".format(
|
||||
self.name,
|
||||
len(self.params),
|
||||
"s" if len(self.params) > 1 else "",
|
||||
self.value,
|
||||
)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.params[item]
|
||||
|
||||
def __setitem__(self, item, *values):
|
||||
self.params[item] = [val for val in values]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, line, nr=-1):
|
||||
"""Parse a single iCalendar-formatted line into a ContentLine"""
|
||||
if "\n" in line or "\r" in line:
|
||||
raise ValueError("ContentLine can only contain escaped newlines")
|
||||
try:
|
||||
ast = GRAMMAR.parse(line)
|
||||
except tatsu.exceptions.FailedToken:
|
||||
raise ParseError(line, nr)
|
||||
|
||||
name = ''.join(ast['name'])
|
||||
value = ''.join(ast['value'])
|
||||
params = {}
|
||||
for param_ast in ast.get('params', []):
|
||||
param_name = ''.join(param_ast["name"])
|
||||
param_values = [''.join(x["value"]) for x in param_ast["values"]]
|
||||
params[param_name] = param_values
|
||||
return cls(name, params, value)
|
||||
|
||||
def clone(self):
|
||||
"""Makes a copy of itself"""
|
||||
# dict(self.params) -> Make a copy of the dict
|
||||
return self.__class__(self.name, dict(self.params), self.value)
|
||||
|
||||
|
||||
class Container(list):
|
||||
"""Represents an iCalendar object.
|
||||
Contains a list of ContentLines or Containers.
|
||||
|
||||
Args:
|
||||
|
||||
name: the name of the object (VCALENDAR, VEVENT etc.)
|
||||
items: Containers or ContentLines
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, *items: List):
|
||||
super(Container, self).__init__(items)
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
name = self.name
|
||||
ret = ['BEGIN:' + name]
|
||||
for line in self:
|
||||
ret.append(str(line))
|
||||
ret.append('END:' + name)
|
||||
return "\r\n".join(ret)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Container '{}' with {} element{}>" \
|
||||
.format(self.name, len(self), "s" if len(self) > 1 else "")
|
||||
|
||||
@classmethod
|
||||
def parse(cls, name, tokenized_lines):
|
||||
items = []
|
||||
for line in tokenized_lines:
|
||||
if line.name == 'BEGIN':
|
||||
items.append(Container.parse(line.value, tokenized_lines))
|
||||
elif line.name == 'END':
|
||||
if line.value != name:
|
||||
raise ParseError(
|
||||
"Expected END:{}, got END:{}".format(name, line.value))
|
||||
break
|
||||
else:
|
||||
items.append(line)
|
||||
return cls(name, *items)
|
||||
|
||||
def clone(self):
|
||||
"""Makes a copy of itself"""
|
||||
c = self.__class__(self.name)
|
||||
for elem in self:
|
||||
c.append(elem.clone())
|
||||
return c
|
||||
|
||||
|
||||
def unfold_lines(physical_lines, with_linenr=False):
|
||||
if not isinstance(physical_lines, collections.abc.Iterable):
|
||||
raise ParseError('Parameter `physical_lines` must be an iterable')
|
||||
current_nr = -1
|
||||
current_line = ''
|
||||
for nr, line in enumerate(physical_lines):
|
||||
if len(line.strip()) == 0:
|
||||
continue
|
||||
elif not current_line:
|
||||
current_nr = nr
|
||||
current_line = line.strip('\r')
|
||||
elif line[0] in (' ', '\t'):
|
||||
current_line += line[1:].strip('\r')
|
||||
else:
|
||||
if with_linenr:
|
||||
yield current_nr, current_line
|
||||
else:
|
||||
yield current_line
|
||||
current_nr = nr
|
||||
current_line = line.strip('\r')
|
||||
if current_line:
|
||||
if with_linenr:
|
||||
yield current_nr, current_line
|
||||
else:
|
||||
yield current_line
|
||||
|
||||
|
||||
def tokenize_line(unfolded_lines):
|
||||
for line in unfolded_lines:
|
||||
if isinstance(line, tuple):
|
||||
yield ContentLine.parse(line[1], line[0])
|
||||
else:
|
||||
yield ContentLine.parse(line)
|
||||
|
||||
|
||||
def parse(tokenized_lines, block_name=None):
|
||||
res = []
|
||||
for line in tokenized_lines:
|
||||
if line.name == 'BEGIN':
|
||||
res.append(Container.parse(line.value, tokenized_lines))
|
||||
else:
|
||||
res.append(line)
|
||||
return res
|
||||
|
||||
|
||||
def lines_to_container(lines, linewise=True):
|
||||
if linewise:
|
||||
return parse(tokenize_line(unfold_lines(lines, with_linenr=True))) # linewise
|
||||
else:
|
||||
return string_to_container("\r\n".join(lines), linewise) # full-string
|
||||
|
||||
|
||||
def string_to_container(txt, linewise=True):
|
||||
if linewise:
|
||||
return lines_to_container(txt.split("\n"), linewise) # linewise
|
||||
else:
|
||||
return parse(string_to_content_lines(txt)) # full-string
|
||||
|
||||
|
||||
def string_to_content_lines(txt):
|
||||
txt = re.sub("\r?\n[ \t]", "", txt)
|
||||
ast = GRAMMAR.parse(txt, rule_name='full')
|
||||
for line in ast:
|
||||
line = line[0]
|
||||
name = ''.join(line['name'])
|
||||
value = ''.join(line['value'])
|
||||
params = {}
|
||||
for param_ast in line.get('params', []):
|
||||
param_name = ''.join(param_ast["name"])
|
||||
param_values = [''.join(x["value"]) for x in param_ast["values"]]
|
||||
params[param_name] = param_values
|
||||
yield ContentLine(name, params, value)
|
||||
|
||||
|
||||
def calendar_string_to_containers(string):
|
||||
if not isinstance(string, str):
|
||||
raise TypeError("Expecting a string")
|
||||
return string_to_container(string)
|
||||
Reference in New Issue
Block a user