This repository has been archived by the owner on Apr 9, 2023. It is now read-only.
/
filter.py
344 lines (279 loc) · 12 KB
/
filter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""
$Id: $
"""
from Products.Archetypes import public as atapi
from Products.Archetypes.debug import log as atlog
from Products.Archetypes.config import REFERENCE_CATALOG
from Products.filter import config as config
from Products.CMFCore import CMFCorePermissions
from Products.CMFCore.utils import getToolByName, _getViewFor
from ZODB.POSException import ConflictError
from Products.filter.utils import macro_render, createContext, ijoin
from Products.PageTemplates.Expressions import PathExpr, getEngine
from zope.interface import implements
from interfaces import IFieldFilter
import re
TALESEngine = getEngine()
class Filter(object):
"""abstract base
"""
implements(IFieldFilter)
name = None # required
pattern = None
def __init__(self, context):
self.context = context
def filter(self, text, **kwargs):
# Simple text replacement via co-op with the modules
chunks = self.pattern.split(text)
if len(chunks) == 1: # fastpath
return text
subs = []
dynamic = chunks[1::2] # my ben, aren't you tricky..
for d in dynamic:
subs.append(self._filterCore(d, **kwargs))
# Now join the two lists (knowing that len(text) == subs+1)
return ''.join(ijoin(chunks[::2], subs))
__call__ = filter
def _filterCore(self, chunk, **kwargs):
"""Subclasses override this to provide specific impls"""
return ''
class MacroSubstitutionFilter(Filter):
"""filter content with simple runtime subst
This looks for $$key$$ in the text and replaces it
"""
name = "Macro Substitution Filter"
pattern = re.compile('\$\$(\w+)\$\$')
def _macro_renderer(self, macro, template=None, **kw):
"""render approved macros"""
try:
if not template:
view = _getViewFor(self.context)
macro = view.macros[macro]
else:
template = self.context.restrictedTraverse(path=template)
macro = template.macros[macro]
except ConflictError:
raise
except:
import traceback
traceback.print_exc()
return ''
econtext = createContext(self.context, **kw)
return macro_render(macro, self.context, econtext, **kw)
def _filterCore(self, chunk, **kwargs):
"""Subclasses override this to provide specific impls"""
return self._macro_renderer(chunk, template=kwargs.get('template'))
class ReferenceLinkFilter(Filter):
"""designed to be used in HTML, implements a simple strategy for
doing TALES like expressions on references of a given relationship
example:
<a href="${reference/id/URL}">${reference/id/Title}</a>
we mangle the expression (to make it simpler for the user)
in the following way
id in these examples becomes "reference" in the evaluation context
Title and URL are resolved and available
here is the tricky part, to make this work, the
"${reference/foo/xxx}" really becomes ${xxx}
so that /Title referes to the Title of the reference
automatically.
If you need the reference object directly you can say
${reference/id/getTargetObject/xxx}
"""
name = "Reference Link"
pattern = re.compile('\${reference/([^}]+?)}')
relationship = config.LINK_RELATIONSHIP
def _filterCore(self, chunk, **kwargs):
# Obtain the id of the reference from the expression
parts = chunk.split('/', 1)
targetId = parts[:1]
expr = parts[1:]
if targetId:
targetId = targetId[0]
# resolve references for this id and relationship
reference_tool = getToolByName(self.context,
REFERENCE_CATALOG)
# We employ two strategies here.
# look for the targetId as a UID (this is the most
# flexible form as it allow even the object to be renamed)
brains = reference_tool._queryFor(sid=self.context.UID(),
tid=targetId,
relationship=self.relationship)
if not brains:
# look for targetId as an Id ( this is more common on
# smaller sites with hand coded HTML)
brains = reference_tool._queryFor(sid=self.context.UID(),
targetId=targetId,
relationship=self.relationship)
if not brains:
# if there were no results we can't do anything
atlog('''Link Resolution Problem: %s references
missing object with id (%s)''' % (
self.context.getId(), targetId))
return chunk
elif len(brains) > 1:
# there should only be one, however, its possible that
# referenced objects could share an id (not a UUID). In
# this unlikely event we issue a warning and use the first
atlog('''Link Resolution Problem: %s references
more than one object with the same id (%s)''' % (
self.context.getId(), targetId))
brains = (brains[0],)
# Generate a TALES Expression from chunk
if expr:
expr = expr[0]
else:
expr = "reference/getTargetObject"
expression = PathExpr('reference', expr, TALESEngine)
# brains is still in context, we can use it to generate the
# default context, remember, this points to the "reference
# object", not the targetObject itself.
brain = brains[0]
refobj = brain.getObject()
# some of this information is not in the default referernce
# object. To get this to appear and be used here we need to
# update the ref catalog and use a new relationship that
# inlcudes this information as the referenceClass.
# Extensions/Install/configureReferenceCatalog shows this
econtext = createContext(self.context,
reference=refobj,
Title=brain.targetTitle,
URL=brain.targetURL,
)
# and evaluate the expression
result = expression(econtext)
if callable(result):
result = result()
return result
class PaginatingFilter(Filter):
"""
Pagination,
'its not more professional, but you can show more ads'(tm)
designed to be used in HTML
parses text breaking it into pages. this example is more complex
that the last in that it needs both information from the request
(what page are we looking for) and to provide information to the
template for the purpose for rendering prev/next page links.
The parser itself is not that sophisticated. Pagination to a fixed
sized page when including such things as table layout and images
becomes extremely complex. luckly we are not doing real
typesetting here and are only showing the concept. :)
"""
name = "HTML Paginator"
SIZE_LIMIT = 4096
SIGNIFICANT_TAGS = ['P', 'DIV', 'TABLE', ]
BREAK_BEFORE = ['<h1', '<h2', '<h3', '<h4', '<h6', '<h6', '<hr']
END_TAGS = ['</%s>' % i for i in SIGNIFICANT_TAGS]
END_RE = re.compile('(?i)%s' %( '|'.join(END_TAGS + BREAK_BEFORE)))
UNBREAKABLE_RE = re.compile('(?i)<table|<ul|<ol|<dl')
UNBREAKABLE_CLOSE_RE = re.compile('(?i)</table>|</ul>|</ol>|</dl>')
def chunkpage(self, text, limit=SIZE_LIMIT):
pages = []
para = self.findHTMLChunks(text)
para = list(para)
current = []
for p in para:
cpage = ''.join(current)
clen = len(cpage)
if clen > limit:
pages.append(cpage)
current = []
current.append(p)
return pages
def findHTMLChunks(self, text):
"""
yield a stream of chunks of devisible text
the page breaker can use these
we can not count on \n and the like to mean much in HTML, we need
close tags
"""
start = 0
end = len(text)
end_re = self.END_RE
unbreakable_re = self.UNBREAKABLE_RE
unbreakable_close_re = self.UNBREAKABLE_CLOSE_RE
while 1:
match = end_re.search(text, start)
if match:
# the only real sin would be to break a table or a list
# we should scan for open tags within this
# chunk, if we find one, we have to expand the search
chunk = text[start:match.end()]
tm = unbreakable_re.search(chunk)
if tm:
tm = unbreakable_close_re.search(chunk)
if not tm:
tm = unbreakable_close_re.search(text, start)
if tm:
match = tm
chunk = text[start:match.end()]
# else we just keep the old match, it was
# broken HTML anyway
yield chunk
start = match.end()
else:
yield text[start:]
break
def filter(self, text, **kwargs):
page = kwargs.get('page')
if not page:
page = self.context.REQUEST.get('page', 1)
page = int(page)
if page == 0: page = 1 # non-geek counting
pages = self.chunkpage(text, limit=int(kwargs.get('limit', self.SIZE_LIMIT)))
# if we couldn't parse it or they indicated they want everything
if not pages or page == -1:
# we couldn't do anything?
return text
page -= 1
if page > len(pages) or page < 0:
page = 0
p = pages[page]
# we should now have the relevant page text, but we want to
# include some additional information in the output that can
# be used to page among these things
if kwargs.get('template'):
data = {
'pages' : len(pages),
'current' : page + 1,
'prev' : max(page, 1),
'next' : min(page + 2, len(pages)),
}
econtext = createContext(self.context,
**data)
# reuse some of the macro code to render the "pages" macro
# and insert a pager into the resultant text
template = self.context.restrictedTraverse(path=kwargs.get('template'))
if template:
macro = template.macros['pages']
text = macro_render(macro, self.context, econtext)
p = p + text
return p
__call__ = filter
class WeakWikiFilter(Filter):
## This just showns another type of Wiki-like dynamic filtering
## transforms BumpyWords into links of something with that title
## exists in the portal_catalog
name = "Weak Wiki Filter"
pattern = re.compile('([A-Z][a-z]+[A-Z]\w+)')
def _filterCore(self, chunk, **kwargs):
pc = self.context.portal_catalog
brains = pc(Title=chunk)
# lets only handle unique matches
if brains and len(brains) == 1:
url = brains[0].getURL()
# build a context and render a macro for the link
# (again, to keep things flexible)
data = {
'url' : url,
'anchor' : chunk,
}
econtext = createContext(self.context,
**data)
# reuse some of the macro code to render the "wikilink" macro
# and insert a stylized link into the output
template = self.context.restrictedTraverse(path=kwargs.get('template'))
if template:
macro = template.macros['wikilink']
return macro_render(macro, self.context, econtext)
return chunk
__all__=('Filter', 'WeakWikiFilter', 'PaginatingFilter', 'ReferenceLinkFilter', 'MacroSubstitutionFilter')