tested django-newsletter
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
from surlex.grammar import Parser, RegexScribe, get_all_nodes, MacroTagNode
|
||||
from surlex.macros import MacroRegistry, DefaultMacroRegistry
|
||||
import re
|
||||
|
||||
class Surlex(object):
|
||||
def __init__(self, surlex, macro_registry=DefaultMacroRegistry()):
|
||||
self.translated = False
|
||||
self.surlex = surlex
|
||||
self.macro_registry = macro_registry
|
||||
|
||||
def translate(self):
|
||||
self.parser = Parser(self.surlex)
|
||||
self.node_list = self.parser.get_node_list()
|
||||
self.scribe = RegexScribe(
|
||||
self.node_list,
|
||||
self.macro_registry,
|
||||
)
|
||||
self.regex = self.scribe.translate()
|
||||
return self.regex
|
||||
|
||||
@property
|
||||
def groupmacros(self):
|
||||
macros = {}
|
||||
if not self.translated:
|
||||
self.translate()
|
||||
for node in get_all_nodes(self.node_list):
|
||||
if isinstance(node, MacroTagNode):
|
||||
macros[node.name] = node.macro
|
||||
return macros
|
||||
|
||||
@property
|
||||
def to_regex(self):
|
||||
if not self.translated:
|
||||
self.translate()
|
||||
return self.regex
|
||||
|
||||
def match(self, subject):
|
||||
m = re.match(self.to_regex, subject)
|
||||
if m:
|
||||
return m.groupdict()
|
||||
|
||||
# This allows "surlex.register_macro" to register to the default registry
|
||||
register_macro = DefaultMacroRegistry.register
|
||||
|
||||
def surlex_to_regex(surlex):
|
||||
parser = Parser(surlex)
|
||||
scribe = RegexScribe(parser.get_node_list())
|
||||
return scribe.translate()
|
||||
|
||||
def parsed_surlex_object(surlex):
|
||||
object = Surlex(surlex)
|
||||
object.translate()
|
||||
return object
|
||||
|
||||
def match(surlex, subject):
|
||||
return Surlex(surlex).match(subject)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.conf.urls import url
|
||||
from surlex import Surlex
|
||||
|
||||
def surl(surlex, *args, **kwargs):
|
||||
return url(Surlex(surlex).translate(), *args, **kwargs)
|
||||
@@ -0,0 +1,19 @@
|
||||
class SurlexException(Exception):
|
||||
"""
|
||||
a generic surlex exception
|
||||
"""
|
||||
pass
|
||||
|
||||
class MalformedSurlex(SurlexException):
|
||||
"""
|
||||
surlex parser error -- when read_until does not find
|
||||
the expected character it'll throw this
|
||||
"""
|
||||
pass
|
||||
|
||||
class MacroDoesNotExist(SurlexException):
|
||||
"""
|
||||
surlex parser error -- when a macro cannot be resolved
|
||||
this will be thrown
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,185 @@
|
||||
import re
|
||||
from surlex.exceptions import MalformedSurlex
|
||||
from surlex.macros import MacroRegistry, DefaultMacroRegistry
|
||||
|
||||
# Define the next function for python 2 and 3 compatibility
|
||||
try:
|
||||
if next:
|
||||
pass
|
||||
except NameError:
|
||||
def next(iterable):
|
||||
return iterable.next()
|
||||
|
||||
class Node(object):
|
||||
pass
|
||||
|
||||
class TextNode(Node):
|
||||
def __init__(self, token):
|
||||
self.token = token
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.token == other.token)
|
||||
|
||||
def __repr__(self):
|
||||
return '<TextNode "%s">' % self.token
|
||||
|
||||
class WildcardNode(Node):
|
||||
def __init__(self):
|
||||
pass
|
||||
def __eq__(self, other):
|
||||
return self.__class__ == other.__class__
|
||||
|
||||
def __repr__(self):
|
||||
return '<WildcardNode>'
|
||||
|
||||
class BlockNode(Node):
|
||||
def __init__(self, node_list):
|
||||
self.node_list = node_list
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.node_list == other.node_list)
|
||||
|
||||
class OptionalNode(BlockNode):
|
||||
def __repr__(self):
|
||||
return '<OptionalNode: %s>' % self.node_list
|
||||
|
||||
class TagNode(Node):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.name == other.name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<TagNode: %s>' % self.name
|
||||
|
||||
class RegexTagNode(TagNode):
|
||||
def __init__(self, name, regex):
|
||||
self.name = name
|
||||
self.regex = regex
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.name == other.name and
|
||||
self.regex == other.regex)
|
||||
|
||||
def __repr__(self):
|
||||
return '<RegexTagNode %s: %s>' % (self.name, self.regex)
|
||||
|
||||
class MacroTagNode(TagNode):
|
||||
def __init__(self, name, macro):
|
||||
self.name = name
|
||||
self.macro = macro
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.name == other.name and
|
||||
self.macro == other.macro)
|
||||
|
||||
def __repr__(self):
|
||||
return '<MacroTagNode %s: %s>' % (self.name, self.macro)
|
||||
|
||||
class Parser(object):
|
||||
def __init__(self, surlex):
|
||||
self.surlex = surlex
|
||||
self.chars = iter(surlex)
|
||||
|
||||
def get_node_list(self):
|
||||
return list(self.parse(self.chars))
|
||||
|
||||
def read_until(self, chars, char):
|
||||
try:
|
||||
next_char = next(chars)
|
||||
except StopIteration:
|
||||
raise MalformedSurlex('Malformed surlex. Expected %s.' % char)
|
||||
if next_char == char:
|
||||
return ''
|
||||
if next_char == '\\':
|
||||
# only escape what we are looking for
|
||||
escaped_char = next(chars)
|
||||
if escaped_char == char:
|
||||
return escaped_char + self.read_until(chars, char)
|
||||
else:
|
||||
return '\\' + escaped_char + self.read_until(chars, char)
|
||||
else:
|
||||
return next_char + self.read_until(chars, char)
|
||||
|
||||
def parse(self, chars):
|
||||
token = ''
|
||||
for char in chars:
|
||||
if char in '<*(':
|
||||
if token:
|
||||
yield TextNode(token)
|
||||
token = ''
|
||||
if char == '\\':
|
||||
# escape with backslash
|
||||
token += next(chars)
|
||||
elif char == '<':
|
||||
tag_content = self.read_until(chars, '>')
|
||||
name = ''
|
||||
regex = None
|
||||
macro = None
|
||||
for char in tag_content:
|
||||
if char == '=':
|
||||
name, regex = tag_content.split('=', 1)
|
||||
break
|
||||
if char == ':':
|
||||
name, macro = tag_content.split(':', 1)
|
||||
break
|
||||
if regex:
|
||||
yield RegexTagNode(name, regex)
|
||||
elif macro:
|
||||
yield MacroTagNode(name, macro)
|
||||
else:
|
||||
yield TagNode(tag_content)
|
||||
elif char == '*':
|
||||
# wildcard
|
||||
yield WildcardNode()
|
||||
elif char == '(':
|
||||
yield OptionalNode(list(self.parse(chars)))
|
||||
elif char == ')':
|
||||
# end of node list, stop parsing
|
||||
break
|
||||
else:
|
||||
# literal output
|
||||
token += char
|
||||
if token:
|
||||
yield TextNode(token)
|
||||
|
||||
class RegexScribe(object):
|
||||
def __init__(self, node_list, macro_registry=DefaultMacroRegistry()):
|
||||
self.node_list = node_list
|
||||
self.macro_registry = macro_registry
|
||||
|
||||
def translate(self):
|
||||
output = ''
|
||||
for node in self.node_list:
|
||||
if isinstance(node, TextNode):
|
||||
output += node.token.replace('.', '\.')
|
||||
elif isinstance(node, WildcardNode):
|
||||
output += '.*'
|
||||
elif isinstance(node, OptionalNode):
|
||||
output += '(' + RegexScribe(node.node_list).translate() + ')?'
|
||||
elif isinstance(node, TagNode):
|
||||
if isinstance(node, MacroTagNode):
|
||||
regex = self.macro_registry.get(node.macro)
|
||||
elif isinstance(node, RegexTagNode):
|
||||
regex = node.regex
|
||||
else:
|
||||
regex = '.+'
|
||||
if node.name:
|
||||
output += '(?P<%s>%s)' % (node.name, regex)
|
||||
else:
|
||||
output += regex
|
||||
return output
|
||||
|
||||
def get_all_nodes(node_list):
|
||||
for node in node_list:
|
||||
if isinstance(node, BlockNode):
|
||||
for node in get_all_nodes(node.node_list):
|
||||
yield node
|
||||
else:
|
||||
yield node
|
||||
@@ -0,0 +1,47 @@
|
||||
from surlex.exceptions import MacroDoesNotExist
|
||||
|
||||
class MacroRegistry(object):
|
||||
macros = {}
|
||||
def __init__(self, macros={}):
|
||||
all_macros = {}
|
||||
all_macros.update(self.macros)
|
||||
all_macros.update(macros)
|
||||
self.macros = all_macros
|
||||
|
||||
def get(self, macro_name):
|
||||
try:
|
||||
return self.macros[macro_name]
|
||||
except KeyError:
|
||||
raise MacroDoesNotExist('Macro "%s" not defined' % macro_name)
|
||||
|
||||
def set(self, macro_name, regex):
|
||||
self.macros[macro_name] = regex
|
||||
|
||||
class DefaultMacroRegistry(MacroRegistry):
|
||||
global_macros = {}
|
||||
|
||||
def __init__(self):
|
||||
super(DefaultMacroRegistry, self).__init__({
|
||||
'Y': r'\d{4}', # year, including century
|
||||
'y': r'\d{2}', # year, not including century
|
||||
'M': r'(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)', # month, abbreviated
|
||||
'm': r'(0?([1-9])|10|11|12)', # month, 1 or 2 digit
|
||||
'd': r'((0|1|2)?([1-9])|[1-3]0|31)', # day, 1 or 2 digit
|
||||
'#': r'\d+', # number, any length
|
||||
's': r'[\w-]+', # slug
|
||||
'u': r'[a-fA-F0-9]{8}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{4}-?[a-fA-F0-9]{12}', # uuid
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def register(cls, macro, regex):
|
||||
cls.global_macros[macro] = regex
|
||||
|
||||
def get(self, macro_name):
|
||||
try:
|
||||
return super(DefaultMacroRegistry, self).get(macro_name)
|
||||
except MacroDoesNotExist:
|
||||
try:
|
||||
return self.__class__.global_macros[macro_name]
|
||||
except KeyError:
|
||||
raise MacroDoesNotExist('Macro "%s" not defined' % macro_name)
|
||||
|
||||
Reference in New Issue
Block a user