Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

start the plone.outputfilters package, refs #9938

svn path=/plone.outputfilters/trunk/; revision=38866
  • Loading branch information...
commit 6e24af5729f9941d33d3bd41b5dd4f45fbecdaa9 0 parents
@davisagli davisagli authored
@@ -0,0 +1,7 @@
+1.0dev (unreleased)
+- Initial implementation.
339 docs/LICENSE.GPL
@@ -0,0 +1,339 @@
+ Version 2, June 1991
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+ Preamble
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+ The precise terms and conditions for copying, distribution and
+modification follow.
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+ How to Apply These Terms to Your New Programs
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+Also add information on how to contact you by electronic and paper mail.
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
16 docs/LICENSE.txt
@@ -0,0 +1,16 @@
+ plone.outputfilters is copyright
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ GNU General Public License for more details.
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+ MA 02111-1307 USA.
6 plone/
@@ -0,0 +1,6 @@
+# See
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
128 plone/outputfilters/README.txt
@@ -0,0 +1,128 @@
+``plone.outputfilters`` provides a framework for registering filters that
+get applied to text as it is rendered.
+By default, these filters are wired up to occur when text is transformed from
+the text/html mimetype to the text/x-html-safe mimetype via the
+PortalTransforms machinery.
+With both Archetypes TextFields and the RichText field of
+````, this transform is typically applied when the field
+value is first accessed. The result of the transform is then cached until the
+value is replaced.
+Included Filters
+A default filter is included which provides the following features:
+* Resolving UID-based links
+* Adding captions to images
+(These are implemented as one filter to avoid the overhead of parsing the HTML
+Resolving UID-based links
+Internal links may be inserted with a UID reference rather than the real path
+of the item being linked. For example, a link might look like this::
+ <a href="resolveuid/6992f1f6-ae36-11df-9adf-001ec2a8cdf1">
+Such URLs can be resolved by the resolveuid view, but this requires an extra
+redirect to reach the actual object. The resolveuid filter will avoid that by
+replacing such URLs with the object's actual full absolute URL.
+UIDs are resolved using ````, with a fallback to
+the Archetypes UID catalog for backwards compatibility. LingaPlone translations
+are supported when LinguaPlone is present.
+The resolveuid filter is enabled when XXX
+Image captioning
+Image tags with the "captioned" class and a ``src`` attribute that resolves to
+an image object within the site will be wrapped in a definition list (DL) tag
+which includes a caption based on the value of the image's ``description``
+field, if any.
+For example, this image tag::
+ <img src="path/to/image" class="captioned"/>
+might be transformed into::
+ <dl class="captioned">
+ <dt><img src="path/to/image"/></dt>
+ <dd class="image-caption">Caption text</dd>
+ </dl>
+assuming the image found at "path/to/image" has the description "Caption text".
+The image captioning filter is enabled when XXX
+Adding a custom filter
+As an example, the following filter replaces all doubled hyphens ("--") with em
+dashes ("—"). (Don't use the example verbatim, because it doesn't parse HTML to apply itself only to text nodes, so will mangle HTML comments.)
+A filter is a callable which accepts a UTF-8-encoded HTML string as input, and
+returns a modified UTF-8-encoded HTML string. A return value of ``None`` may be
+used to indicate that the input should not be modified.
+ .. include:: filters/
+ :literal:
+The ``order`` attribute may be used to affect the order in which filters are
+applied (higher values run later). The is_enabled method should return a boolean
+indicating whether the filter should be applied.
+Filters are registered in ZCML as a named multi-adapter of the context and
+request to IFilter.
+ >>> from Products.Five.zcml import load_string
+ >>> load_string("""
+ ... <configure
+ ... xmlns="">
+ ...
+ ... <adapter
+ ... name="em_dash_adder"
+ ... provides="plone.outputfilters.interfaces.IFilter"
+ ... for="* *"
+ ... factory="plone.outputfilters.filters.example.EmDashAdder"
+ ... />
+ ...
+ ... </configure>
+ ... """)
+Now when text is transformed from text/html to text/x-html-safe, the filter will
+be applied.
+ >>> str(self.portal.portal_transforms.convertTo('text/x-html-safe',
+ ... 'test--test', mimetype='text/html'))
+ 'test\xe2\x80\x94test'
+How it works
+``plone.outputfilters`` hooks into the PortalTransforms machinery by installing:
+1. a new mimetype ("text/x-plone-outputfilters-html")
+2. a transform from text/html to text/x-plone-outputfilters-html
+3. a null transform from text/x-plone-outputfilters-html back to text/html
+4. a "transform policy" for the text/x-html-safe mimetype, which says that text
+ being transformed to text/x-html-safe must first be transformed to
+ text/x-plone-outputfilters-html
+The filter adapters are looked up and applied during the execution of the
+transform from step #2.
2  plone/outputfilters/
@@ -0,0 +1,2 @@
+def initialize(context):
+ """Initializer called when used as a Zope 2 product."""
24 plone/outputfilters/configure.zcml
@@ -0,0 +1,24 @@
+ xmlns=""
+ xmlns:gs=""
+ i18n_domain="plone.outputfilters">
+ <include package=".filters"/>
+ <gs:registerProfile
+ name="default"
+ title="HTML Output Filters"
+ directory="profiles/default"
+ description="Framework for applying filters to HTML as it is rendered."
+ provides="Products.GenericSetup.interfaces.EXTENSION"
+ />
+ <gs:importStep
+ name="plone_outputfilters_various"
+ title="HTML Output Filters installation"
+ description="Import various plone.outputfilters"
+ handler="plone.outputfilters.setuphandlers.importVarious">
+ <depends name="componentregistry"/>
+ </gs:importStep>
0  plone/outputfilters/filters/
No changes.
11 plone/outputfilters/filters/configure.zcml
@@ -0,0 +1,11 @@
+ xmlns="">
+ <adapter
+ provides="..interfaces.IFilter"
+ name="resolveuid_and_caption"
+ for="* *"
+ factory=".resolveuid_and_caption.ResolveUIDAndCaptionFilter"
+ />
17 plone/outputfilters/filters/
@@ -0,0 +1,17 @@
+import re
+from zope.interface import implements
+from plone.outputfilters.interfaces import IFilter
+class EmDashAdder(object):
+ implements(IFilter)
+ order = 1000
+ def __init__(self, context, request):
+ pass
+ def is_enabled(self):
+ return True
+ pattern = re.compile(r'--')
+ def __call__(self, data):
+ return self.pattern.sub('\xe2\x80\x94', data)
227 plone/outputfilters/filters/
@@ -0,0 +1,227 @@
+from zope.interface import implements
+from Products.CMFCore.utils import getToolByName
+from sgmllib import SGMLParser, SGMLParseError
+from urlparse import urlsplit, urljoin
+from urllib import unquote
+ from Products.LinguaPlone.utils import translated_references
+except ImportError:
+from plone.outputfilters.interfaces import IFilter
+singleton_tags = ["img", "br", "hr", "input", "meta", "param", "col"]
+class ResolveUIDAndCaptionFilter(SGMLParser):
+ """ Parser to convert UUID links and captioned images """
+ implements(IFilter)
+ def __init__(self, context=None, request=None):
+ SGMLParser.__init__(self)
+ self.current_status = None
+ self.context = context
+ self.request = request
+ self.pieces = []
+ # IFilter implementation
+ order = 800
+ @property
+ def captioned_images(self):
+ # XXX
+ return False
+ @property
+ def resolve_uids(self):
+ # XXX
+ return True
+ def is_enabled(self):
+ if self.context is None:
+ return False
+ return self.captioned_images or self.resolve_uids
+ def __call__(self, data):
+ self.feed(data)
+ self.close()
+ return self.getResult()
+ # SGMLParser implementation
+ def append_data(self, data, add_eol=0):
+ """Append data unmodified to, add_eol adds a newline character"""
+ if add_eol:
+ data += '\n'
+ self.pieces.append(data)
+ def handle_charref(self, ref):
+ """ Handle characters, just add them again """
+ self.append_data("&#%s;" % ref)
+ def handle_entityref(self, ref):
+ """ Handle html entities, put them back as we get them """
+ self.append_data("&%s;" % ref)
+ def handle_data(self, text):
+ """ Add data unmodified """
+ self.append_data(text)
+ def handle_comment(self, text):
+ """ Handle comments unmodified """
+ self.append_data("<!--%s-->" % text)
+ def handle_pi(self, text):
+ """ Handle processing instructions unmodified"""
+ self.append_data("<?%s>" % text)
+ def handle_decl(self, text):
+ """Handle declarations unmodified """
+ self.append_data("<!%s>" % text)
+ def lookup_uid(self, uid):
+ context = self.context
+ # If we have LinguaPlone installed, add support for language-aware
+ # references
+ uids = translated_references(context, context.Language(), uid)
+ if len(uids) > 0:
+ uid = uids[0]
+ reference_tool = getToolByName(context, 'reference_catalog')
+ return reference_tool.lookupObject(uid)
+ def unknown_starttag(self, tag, attrs):
+ """Here we've got the actual conversion of links and images. Convert UUID's to absolute url's, and process captioned images to HTML"""
+ if tag in ['a', 'img']:
+ # Only do something if tag is a link or images
+ attributes = {}
+ for (key, value) in attrs:
+ attributes[key] = value
+ if tag == 'a':
+ if attributes.has_key('href'):
+ href = attributes['href']
+ if 'resolveuid' in href:
+ # We should check if "Link using UIDs" is enabled in
+ # the TinyMCE tool, but then the kupu resolveuid is
+ # used, so let's always transform here
+ parts = href.split("/")
+ # Get the actual UUID
+ uid = parts[1]
+ appendix = ""
+ if len(parts) > 2:
+ # There is more than just the UUID, save it in
+ # appendix
+ appendix = "/".join(parts[2:])
+ # move name of links to anchors to appendix
+ # (resolveuid/12fc34#anchor)
+ if '#' in uid:
+ uid, anchor = uid.split('#')
+ appendix = '#%s' % anchor #anchor + appendix won't happen
+ ref_obj = self.lookup_uid(uid)
+ if ref_obj:
+ href = ref_obj.absolute_url() + appendix
+ attributes['href'] = href
+ attrs = attributes.iteritems()
+ elif tag == 'img':
+ # First collect some attributes
+ src = ""
+ description = ""
+ if attributes.has_key("src"):
+ src = attributes["src"]
+ # if we set an description within tinymce we want to keep that one
+ if attributes.has_key("alt") and attributes["alt"]:
+ description = attributes["alt"]
+ if 'resolveuid' in src:
+ # We need to convert the UUID to a relative path here
+ parts = src.split("/")
+ uid = parts[1]
+ appendix = ""
+ if len(parts) > 2:
+ # There is more than just the UUID, save it in appendix (query parameters for example)
+ appendix = "/" + "/".join(parts[2:])
+ image_obj = self.lookup_uid(uid)
+ if image_obj:
+ # Only do something when the image is actually found in the reference_catalog
+ src = image_obj.absolute_url() + appendix
+ attributes["src"] = src
+ if hasattr(image_obj, "Description") and not description:
+ description = image_obj.Description()
+ else:
+ # It's a relative path, let's see if we can get the description from the portal catalog
+ full_path = urljoin(self.context.absolute_url(), src)
+ #remove any encoded characters
+ full_path = unquote(full_path)
+ scheme, netloc, path, query, fragment = urlsplit(full_path)
+ portal_catalog = getToolByName(self.context, "portal_catalog")
+ # Check if we can find this in the portal catalog
+ brains = portal_catalog({'path' : {'query':path}, 'type' : 'Image'})
+ if len(brains) == 0:
+ # Maybe something like 'image_preview' is in the path, let's chop it
+ query= {'path' : {'query' : "/".join(path.split('/')[:-1])}, 'type' : 'Image'}
+ brains = portal_catalog(query)
+ if len(brains) > 0 and not description:
+ description = brains[0].Description
+ # Check if the image is a captioned image
+ classes = ""
+ if attributes.has_key("class"):
+ classes = attributes["class"]
+ if self.captioned_images and classes.find('captioned') != -1:
+ # We have captioned images, and we need to convert them, so let's do so
+ width_style = ""
+ if attributes.has_key("width"):
+ width_style="style=\"width:%spx;\" " % attributes["width"]
+ image_attributes = ""
+ image_attributes = image_attributes.join(["%s %s=\"%s\"" % (image_attributes, key, value) for (key, value) in attributes.items() if not key in ["class", "src"]])
+ captioned_html = """<dl %sclass="%s">
+ <dt %s>
+ <img %s src="%s"/>
+ </dt>
+ <dd class="image-caption">%s</dd>
+ </dl>""" % (width_style, classes, width_style, image_attributes, src ,description)
+ self.append_data(captioned_html)
+ return True
+ else:
+ # Nothing happens with the image, so add it normally
+ attrs = attributes.iteritems()
+ # Add the tag to the result
+ strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
+ if tag in singleton_tags:
+ self.append_data("<%s%s />" % (tag, strattrs))
+ else:
+ self.append_data("<%s%s>" % (tag, strattrs))
+ def unknown_endtag(self, tag):
+ """Add the endtag unmodified"""
+ self.append_data("</%s>" % tag)
+ def parse_declaration(self, i):
+ """Fix handling of CDATA sections. Code borrowed from BeautifulSoup.
+ """
+ j = None
+ if self.rawdata[i:i+9] == '<![CDATA[':
+ k = self.rawdata.find(']]>', i)
+ if k == -1:
+ k = len(self.rawdata)
+ data = self.rawdata[i+9:k]
+ j = k+3
+ self.append_data("<![CDATA[%s]]>" % data)
+ else:
+ try:
+ j = SGMLParser.parse_declaration(self, i)
+ except SGMLParseError:
+ toHandle = self.rawdata[i:]
+ self.handle_data(toHandle)
+ j = i + len(toHandle)
+ return j
+ def getResult(self):
+ """Return the parsed result and flush it"""
+ result = "".join(self.pieces)
+ self.pieces = None
+ return result
25 plone/outputfilters/
@@ -0,0 +1,25 @@
+from zope.interface import Interface
+from zope import schema
+class IFilter(Interface):
+ """A filter that accepts raw HTML and returns a filtered version.
+ Register a named multi-adapter from (context, request) to
+ this interface to install a new filter.
+ To control the order of filters, use the 'order' attribute. It may be
+ positive or negative."""
+ order = schema.Int(title=u"Order")
+ def is_enabled():
+ """Returns a boolean indicating whether the filter should be applied."""
+ def __call__(data):
+ """Apply the filter.
+ ``data`` is a UTF-8-encoded string.
+ Return a UTF-8-encoded string, or ``None`` to indicate that the data
+ should remain unmodified.
+ """
7 plone/outputfilters/
@@ -0,0 +1,7 @@
+from Products.MimetypesRegistry.MimeTypeItem import MimeTypeItem
+class text_plone_outputfilters_html(MimeTypeItem):
+ __name__ = "Plone Output Filters HTML"
+ mimetypes = ('text/x-plone-outputfilters-html',)
+ binary = 0
1  plone/outputfilters/profiles/default/plone.outputfilters.txt
@@ -0,0 +1 @@
57 plone/outputfilters/
@@ -0,0 +1,57 @@
+from zope.component import getUtility
+from Products.PortalTransforms.interfaces import IPortalTransformsTool
+from Products.MimetypesRegistry.interfaces import IMimetypesRegistryTool
+from plone.outputfilters.mimetype import text_plone_outputfilters_html
+from plone.outputfilters.transforms import plone_outputfilters_html_to_html, \
+ html_to_plone_outputfilters_html
+def register_mimetype(context, mimetype):
+ mimetypes_registry = getUtility(IMimetypesRegistryTool)
+ mimetypes_registry.register(mimetype())
+def unregister_mimetype(context, mimetype):
+ mimetypes_registry = getUtility(IMimetypesRegistryTool)
+ mimetypes_registry.unregister(mimetype())
+def register_transform(context, transform):
+ transform_tool = getUtility(IPortalTransformsTool)
+ transform = transform()
+ transform_tool.registerTransform(transform)
+def unregister_transform(context, transform):
+ transform_tool = getUtility(IPortalTransformsTool)
+ if hasattr(transform_tool, transform):
+ transform_tool.unregisterTransform(transform)
+def register_transform_policy(context, output_mimetype, required_transform):
+ transform_tool = getUtility(IPortalTransformsTool)
+ unregister_transform_policy(context, output_mimetype)
+ transform_tool.manage_addPolicy(output_mimetype, [required_transform])
+def unregister_transform_policy(context, output_mimetype):
+ transform_tool = getUtility(IPortalTransformsTool)
+ policies = [mimetype for (mimetype, required) in transform_tool.listPolicies() if mimetype == output_mimetype]
+ if policies:
+ # There is a policy, remove it!
+ transform_tool.manage_delPolicies([output_mimetype])
+def install_mimetype_and_transforms(context):
+ """ register mimetype and transformations for captioned images """
+ register_mimetype(context, text_plone_outputfilters_html)
+ register_transform(context, plone_outputfilters_html_to_html)
+ register_transform(context, html_to_plone_outputfilters_html)
+ register_transform_policy(context, "text/x-html-safe", "html_to_plone_outputfilters_html")
+def uninstall_mimetype_and_transforms(context):
+ """ unregister mimetype and transformations for captioned images """
+ unregister_transform(context, "plone_outputfilters_html_to_html")
+ unregister_transform(context, "html_to_plone_outputfilters_html")
+ unregister_mimetype(context, text_plone_outputfilters_html)
+ unregister_transform_policy(context, "text/x-html-safe")
+def importVarious(context):
+ if context.readDataFile('plone.outputfilters.txt') is None:
+ return
+ site = context.getSite()
+ install_mimetype_and_transforms(site)
0  plone/outputfilters/tests/
No changes.
22 plone/outputfilters/tests/
@@ -0,0 +1,22 @@
+from Testing import ZopeTestCase as ztc
+from Products.Five import fiveconfigure
+from Products.PloneTestCase import PloneTestCase as ptc
+from Products.PloneTestCase.layer import PloneSite
+class OutputFiltersTestCase(ptc.PloneTestCase):
+ class layer(PloneSite):
+ @classmethod
+ def setUp(cls):
+ fiveconfigure.debug_mode = True
+ import plone.outputfilters
+ ztc.installPackage(plone.outputfilters)
+ fiveconfigure.debug_mode = False
+ @classmethod
+ def tearDown(cls):
+ pass
12 plone/outputfilters/tests/
@@ -0,0 +1,12 @@
+import unittest
+from Testing import ZopeTestCase as ztc
+from plone.outputfilters.tests.base import OutputFiltersTestCase
+def test_suite():
+ return unittest.TestSuite([
+ ztc.ZopeDocFileSuite(
+ 'README.txt', package='plone.outputfilters',
+ test_class=OutputFiltersTestCase),
+ ])
78 plone/outputfilters/tests/
@@ -0,0 +1,78 @@
+from unittest import TestCase
+from plone.outputfilters.tests.base import OutputFiltersTestCase
+from plone.outputfilters.filters.resolveuid_and_caption import \
+ ResolveUIDAndCaptionFilter
+class ResolveUIDAndCaptionFilterUnitTestCase(TestCase):
+ def test_parsing_preserves_newlines(self):
+ # Test if it preserves newlines which should not be filtered out
+ text = """<pre>This is line 1
+This is line 2</pre>"""
+ parser = ResolveUIDAndCaptionFilter()
+ res = parser(text)
+ self.assertTrue('\n' in str(res))
+ def test_parsing_preserves_CDATA(self):
+ # Test if it preserves CDATA sections, such as those TinyMCE puts into
+ # script tags. The standard SGMLParser has a bug that will remove
+ # these.
+ text = """<p>hello</p>
+<script type="text/javsacript">// <![CDATA[
+// ]]></script>
+ parser = ResolveUIDAndCaptionFilter()
+ res = parser(text)
+ self.assertEqual(str(res), text)
+class ResolveUIDAndCaptionFilterIntegrationTestCase(OutputFiltersTestCase):
+ def afterSetUp(self):
+ # create an image and record its UID
+ self.setRoles(['Manager'])
+ from Products.CMFPlone.tests import dummy
+ self.portal.invokeFactory('Image', id='image', title='Image', file=dummy.Image())
+ image = getattr(self.portal, 'image')
+ image.reindexObject()
+ self.UID = image.UID()
+ def test_resolve_uids_in_images(self):
+ text = """<html>
+ <head></head>
+ <body>
+ <img src="resolveuid/%s/image_thumb" class="image-left captioned" width="200" alt="My alt text" />
+ <p><img src="/plone/image.jpg" class="image-right captioned" width="200" style="border-width:1px" /></p>
+ </body>
+</html>""" % self.UID
+ parser = ResolveUIDAndCaptionFilter(context=self.portal)
+ res = parser(text)
+ # The UID reference should be converted to an absolute url
+ self.failUnless('<img src="http://nohost/plone/image/image_thumb" alt="My alt text" class="image-left captioned" width="200" />' in str(res))
+ def test_resolve_uids_in_links(self):
+ text = """<html>
+ <head></head>
+ <body>
+ <a class="internal-link" href="resolveuid/%s">Some link</a>
+ <a class="internal-link" href="resolveuid/%s#named-anchor">Some anchored link</a>
+ </body>
+</html>""" % (self.UID, self.UID)
+ parser = ResolveUIDAndCaptionFilter(context=self.portal)
+ res = parser(text)
+ self.assertTrue('href="http://nohost/plone/image"' in str(res))
+ self.assertTrue('href="http://nohost/plone/image#named-anchor"' in str(res))
+ def test_resolve_uids_non_AT_content(self):
+ pass
+ def test_image_captioning(self):
+ pass
+def test_suite():
+ from unittest import TestSuite, makeSuite
+ suite = TestSuite()
+ suite.addTest(makeSuite(ResolveUIDAndCaptionFilterUnitTestCase))
+ suite.addTest(makeSuite(ResolveUIDAndCaptionFilterIntegrationTestCase))
+ return suite
51 plone/outputfilters/tests/
@@ -0,0 +1,51 @@
+from unittest import TestCase
+from plone.outputfilters.transforms import apply_filters
+class DummyFilter(object):
+ order = 500
+ def is_enabled(self):
+ return True
+ called = []
+ def __call__(self, data):
+ self.__class__.called.append(self)
+ return data
+class FilterTestCase(TestCase):
+ def setUp(self):
+ DummyFilter.called = []
+ def test_apply_filters(self):
+ filters = [DummyFilter()]
+ apply_filters(filters, '')
+ self.assertEqual([filters[0]], DummyFilter.called)
+ def test_apply_filters_ordering(self):
+ filter1 = DummyFilter()
+ filter2 = DummyFilter()
+ filter2.order = 100
+ filters = [filter1, filter2]
+ apply_filters(filters, '')
+ self.assertEqual([filters[1], filters[0]], DummyFilter.called)
+ def test_apply_filters_checks_is_enabled(self):
+ filter = DummyFilter()
+ filter.is_enabled = lambda: False
+ filters = [filter]
+ apply_filters(filters, '')
+ self.assertEqual([], DummyFilter.called)
+ def test_apply_filters_handles_return_none(self):
+ class DummyFilterReturningNone(DummyFilter):
+ def __call__(self, data):
+ return None
+ filter = DummyFilterReturningNone()
+ res = apply_filters([filter], '')
+ self.assertEqual('', res)
78 plone/outputfilters/
@@ -0,0 +1,78 @@
+from zope.component import getAdapters
+from zope.interface import implements
+from import getSite
+ try:
+ from Products.PortalTransforms.interfaces import ITransform
+ except ImportError:
+ from Products.PortalTransforms.z3.interfaces import ITransform
+except ImportError:
+ ITransform = None
+from Products.PortalTransforms.interfaces import itransform
+from plone.outputfilters.interfaces import IFilter
+def apply_filters(filters, data):
+ by_order = lambda x: x.order
+ filters = sorted(filters, key=by_order)
+ for filter in filters:
+ if filter.is_enabled():
+ res = filter(data)
+ if res is not None:
+ data = res
+ return data
+class html_to_plone_outputfilters_html:
+ """ transform which applies output filters"""
+ if ITransform is not None:
+ implements(ITransform)
+ __implements__ = itransform
+ __name__ = "html_to_plone_outputfilters_html"
+ inputs = ('text/html',)
+ output = "text/x-plone-outputfilters-html"
+ def __init__(self, name=None):
+ self.config_metadata = {
+ 'inputs' : ('list', 'Inputs', 'Input(s) MIME type. Change with care.'),
+ }
+ if name:
+ self.__name__ = name
+ def name(self):
+ return self.__name__
+ def convert(self, orig, data, **kwargs):
+ context = kwargs.get('context')
+ request = getattr(getSite(), 'REQUEST', None)
+ filters = [f for _, f in getAdapters((context, request), IFilter)]
+ res = apply_filters(filters, orig)
+ data.setData(res)
+ return data
+class plone_outputfilters_html_to_html:
+ if ITransform is not None:
+ implements(ITransform)
+ __implements__ = itransform
+ __name__ = "plone_outputfilters_html_to_html"
+ inputs = ('text/x-plone-outputfilters-html',)
+ output = "text/html"
+ def __init__(self, name=None):
+ self.config_metadata = {
+ 'inputs' : ('list', 'Inputs', 'Input(s) MIME type. Change with care.'),
+ }
+ if name:
+ self.__name__ = name
+ def name(self):
+ return self.__name__
+ def convert(self, orig, data, **kwargs):
+ # we actually don't do anything in this transform, it is needed to get back in
+ # the transformation policy chain
+ text = orig
+ data.setData(text)
+ return data
@@ -0,0 +1,41 @@
+from setuptools import setup, find_packages
+import os
+version = '1.0dev'
+ version=version,
+ description="Transformations applied to HTML in Plone text fields as they are rendered",
+ long_description=open(os.path.join('plone', 'outputfilters', "README.txt")).read() + "\n" +
+ open("CHANGES.txt").read(),
+ # Get more strings from
+ #
+ classifiers=[
+ "Framework :: Plone",
+ "Programming Language :: Python",
+ ],
+ keywords='',
+ author='',
+ author_email='',
+ url='',
+ license='GPL',
+ packages=find_packages(exclude=['ez_setup']),
+ namespace_packages=['plone'],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=[
+ 'setuptools',
+ 'Products.GenericSetup',
+ 'Products.MimetypesRegistry',
+ 'Products.PortalTransforms',
+ # -*- Extra requirements: -*-
+ ],
+ entry_points="""
+ # -*- Entry points: -*-
+ [z3c.autoinclude.plugin]
+ target = plone
+ """,
+ setup_requires=["PasteScript"],
+ paster_plugins=["ZopeSkel"],
+ )
Please sign in to comment.
Something went wrong with that request. Please try again.