add proxy for the first part and try to develop the second part

This commit is contained in:
Pierre Geeraert
2025-12-03 12:08:56 +00:00
parent 6c34d510e5
commit 460f18d85a
1095 changed files with 358799 additions and 5 deletions
@@ -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)