Index: quintagroup.transmogrifier/trunk/README.txt
===================================================================
--- quintagroup.transmogrifier/trunk/README.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/README.txt	(revision 275)
@@ -0,0 +1,4 @@
+Introduction
+============
+
+
Index: quintagroup.transmogrifier/trunk/docs/HISTORY.txt
===================================================================
--- quintagroup.transmogrifier/trunk/docs/HISTORY.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/docs/HISTORY.txt	(revision 275)
@@ -0,0 +1,8 @@
+Changelog
+=========
+
+0.1 - Unreleased
+----------------
+
+* Initial release
+
Index: quintagroup.transmogrifier/trunk/quintagroup/__init__.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/__init__.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/__init__.py	(revision 275)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/README.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/README.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/README.txt	(revision 275)
@@ -0,0 +1,14 @@
+quintagroup.transmogrifier Package Readme
+=========================
+
+Overview
+--------
+
+Plone blueprints for collective.transmogrifier pipelines.
+
+
+Your tests here
+---------------
+
+    >>> 1 + 1
+    3
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/__init__.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/__init__.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/__init__.py	(revision 275)
@@ -0,0 +1,11 @@
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
+
+# import monkey pathes for GS TarballExportContext
+import patches
+
+def initialize(context):
+    """Initializer called when used as a Zope 2 product."""
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/__init__.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/__init__.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/__init__.py	(revision 275)
@@ -0,0 +1,1 @@
+# i'm a package
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/configure.zcml
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/configure.zcml	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/configure.zcml	(revision 275)
@@ -0,0 +1,28 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five">
+
+    <!-- 'marshall' adapters -->
+    <adapter
+        factory=".marshall.ReferenceExporter"
+        name="marshall"
+        />
+
+    <adapter
+        for="Products.ATContentTypes.interface.IATFile"
+        factory=".marshall.FileExporter"
+        name="marshall"
+        />
+
+    <adapter
+        for="Products.ATContentTypes.interface.IATImage"
+        factory=".marshall.FileExporter"
+        name="marshall"
+        />
+
+    <adapter
+        factory=".marshall.CriterionExporter"
+        name="marshall"
+        />
+
+</configure>
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/marshall.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/marshall.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/adapters/marshall.py	(revision 275)
@@ -0,0 +1,150 @@
+from xml.dom import minidom
+
+from zope.interface import implements
+from zope.component import adapts
+
+from OFS.Image import File
+
+from Products.Archetypes import atapi
+from Products.Archetypes.interfaces import IBaseObject
+from Products.ATContentTypes.interface import IATTopicCriterion
+from Products.Marshall.registry import getComponent
+from Products.Archetypes import config as atcfg
+
+from quintagroup.transmogrifier.interfaces import IExportDataCorrector
+
+class ReferenceExporter(object):
+    """ Add reference fields to XML, generated by Marshall product.
+    """
+    implements(IExportDataCorrector)
+    adapts(IBaseObject)
+
+    def __init__(self, context):
+        self.context = context
+
+    def __call__(self, data):
+        data['data'] = self.exportReferences(data['data'])
+        return data
+
+    def exportReferences(self, xml):
+        """ Marshall 1.0.0 doesn't export references, do it manually.
+        """
+        doc = minidom.parseString(xml)
+        root = doc.documentElement
+        for fname in self.context.Schema().keys():
+            if not isinstance(self.context.Schema()[fname], atapi.ReferenceField):
+                continue
+            elem = doc.createElement("field")
+            attr = doc.createAttribute("name")
+            attr.value = fname
+            elem.setAttributeNode(attr)
+            for value in self.context[fname]:
+                ref = doc.createElement('reference')
+                uid = doc.createElement('uid')
+                value = doc.createTextNode(str(value))
+                uid.appendChild(value)
+                ref.appendChild(uid)
+                elem.appendChild(ref)
+            root.appendChild(elem)
+        return doc.toxml('utf-8')
+
+class FileExporter(ReferenceExporter):
+    """ Add file fields to XML, generated by Marshall product.
+    """
+    implements(IExportDataCorrector)
+
+    def __call__(self, data):
+        xml = data['data']
+        xml = self.normalizeBinaryField(xml)
+        data['data'] = self.exportReferences(xml)
+        return data
+
+    def normalizeBinaryField(self, data):
+        """ Rewrite 'image' or 'file' xml element data with binary data.
+
+            'data' - xml text, formed by Marshall. It's basestring and can have non-ascii characters,
+            but minidom.parseString(data) returns unicode string and we need to explicitly set
+            encoding when returning modified xml data (if not and there non-ascii characters in
+            data, UnicodeDecodeError is raised when calling doc.toxml()).
+        """
+        data, tagName = self.repairXMLData(data)
+        if tagName is None:
+            # return unchanged data 
+            return data
+        doc = minidom.parseString(data)
+        root = doc.documentElement
+        # there is only one 'field' element with name attribute that is equal to 'file' or 'image'
+        elem = [field for field in root.getElementsByTagName('field') 
+                     if field.getAttribute('name') == tagName][0]
+
+        field = self.context[tagName]
+        # create 'filename' attribute (decode file name to unicode using utf-8 encoding)
+        attr = doc.createAttribute('filename')
+        attr.value = field.getFilename() #.decode('utf-8')
+        elem.setAttributeNode(attr)
+        # create 'content_type' attribute
+        attr = doc.createAttribute('content_type')
+        attr.value = field.getContentType()
+        elem.setAttributeNode(attr)
+        # create 'transfer_encoding' attribute
+        attr = doc.createAttribute('transfer_encoding')
+        attr.value = 'base64'
+        elem.setAttributeNode(attr)
+
+        # get file data and encode it in CDATA section
+        value = field.data
+        if isinstance(value, File):
+            value = getattr(value, 'data', value)
+        value = str(value).encode('base64')
+        value = doc.createCDATASection(value)
+        # remove old childs and add new
+        if elem.hasChildNodes():
+            for i in elem.childNodes:
+                elem.removeChild(i)
+        elem.appendChild(value)
+        elem.normalize()
+        return doc.toxml('utf-8')
+
+    def repairXMLData(self, data):
+        """ Find element with binary data, remove invalid data and remove element's name.
+            This element is 'file' or 'image'.
+        """
+        tagName = None
+        if data.find('<field name="file">') != -1:
+            tagName = 'file'
+        elif data.find('<field name="image">') != -1:
+            tagName = 'image'
+        else:
+            return data, tagName
+        openTag = '<field name="%s">' % tagName
+        closeTag = '</field>'
+        index1 = data.find(openTag) + len(openTag)
+        index2 = data.find(closeTag, index1)
+        out = data[:index1] + data[index2:]
+        return out, tagName
+
+class CriterionExporter(ReferenceExporter):
+    """ Special marshalling adapter for topic criterias.
+    """
+
+    implements(IExportDataCorrector)
+    adapts(IATTopicCriterion)
+
+    def __init__(self, context):
+        self.context = context
+        self.marshaller = getComponent('atxml')
+
+    def __call__(self, data):
+        if data['data'] is not None:
+            return super(CriterionExporter, self).__call__(data)
+        # Marshall fails when object has UID that is equal to None
+        # fix it here by setting it to empty string and after marshalling setting back to old value
+        old_uid_attr = getattr(self.context, atcfg.UUID_ATTR)
+        setattr(self.context, atcfg.UUID_ATTR, "")
+
+        ct, length, xml = self.marshaller.marshall(self.context)
+        xml = self.exportReferences(xml)
+        data['data'] = xml
+
+        setattr(self.context, atcfg.UUID_ATTR, old_uid_attr)
+        return data
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/configure.zcml
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/configure.zcml	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/configure.zcml	(revision 275)
@@ -0,0 +1,130 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier"
+    xmlns:gs="http://namespaces.zope.org/genericsetup"
+    i18n_domain="quintagroup.transmogrifier">
+
+    <!--<five:registerPackage package="." initialize=".initialize" />-->
+
+    <include package="collective.transmogrifier" file="meta.zcml" />
+    <include package="collective.transmogrifier" />
+    <include package="plone.app.transmogrifier" />
+    <!--<include package="." file="meta.zcml" />-->
+
+    <!-- register our blueprints -->
+    <utility
+        component=".sitewalker.SiteWalkerSection"
+        name="quintagroup.transmogrifier.sitewalker"
+        />
+
+    <utility
+        component=".manifest.ManifestSection"
+        name="quintagroup.transmogrifier.manifest"
+        />
+
+    <utility
+        component=".marshall.MarshallSection"
+        name="quintagroup.transmogrifier.marshall"
+        />
+
+    <utility
+        component=".propertymanager.PropertyManagerSection"
+        name="quintagroup.transmogrifier.propertymanager"
+        />
+
+    <utility
+        component=".discussioncontainer.DiscussionContainerSection"
+        name="quintagroup.transmogrifier.discussioncontainer"
+        />
+
+    <utility
+        component=".exportadapters.ExportAdaptersSection"
+        name="quintagroup.transmogrifier.exportadapters"
+        />
+
+    <utility
+        component=".writer.WriterSection"
+        name="quintagroup.transmogrifier.writer"
+        />
+
+    <utility
+        component=".reader.ReaderSection"
+        name="quintagroup.transmogrifier.reader"
+        />
+
+    <utility
+        component=".manifest.ManifestImportSection"
+        name="quintagroup.transmogrifier.manifestimport"
+        />
+
+    <utility
+        component=".substitution.SubstitutionSection"
+        name="quintagroup.transmogrifier.substitution"
+        />
+
+    <utility
+        component=".xslt.XSLTSection"
+        name="quintagroup.transmogrifier.xslt"
+        />
+
+    <!-- include registration of adapters for "exportadapters" blueprint -->
+    <include package=".adapters" />
+
+    <transmogrifier:registerConfig
+        name="export"
+        title="Export pipeline configuration"
+        description="This is a Plone content export pipeline configuration."
+        configuration="export.cfg"
+        />
+
+    <!-- We registry this export step in zcml and it will override Plone's standard 
+         Content export step. Than we need to go in ZMI to Manage tab of portal_setup
+         tool and delete old step. (I also detected that steps registered in zcml
+         overriede those registered in profile without deleting them on manage tab)
+    -->
+    <gs:exportStep
+        name="content"
+        title="Content (transmogrifier)"
+        description="Export the site's structure and content."
+        handler=".exportimport.exportSiteStructure"
+        />
+
+    <transmogrifier:registerConfig
+        name="import"
+        title="Import pipeline configuration"
+        description="This is a Plone content import pipeline configuration."
+        configuration="import.cfg"
+        />
+
+    <!-- With import step we can't do the same. Plone's portal_setup tool currently
+         works such that if we delete standard Content import step on Manage tab
+         it will be recreated when rendering Import tab. (also this step is overriden
+         when we import all steps, but old handler is used if we select only content
+         step).
+
+         <gs:importStep
+            name="content"
+            title="Content (transmogrifier)"
+            description="Import the site's structure and content."
+            handler=".exportimport.importSiteStructure"
+            />
+
+         That's why we can go another way and register adapter which is looked up in
+         standard handler.
+
+         <adapter
+            for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+            provides="Products.GenericSetup.interfaces.IFilesystemImporter"
+            factory=".exportimport.PloneSiteImporter"
+            />
+    -->
+
+    <gs:importStep
+        name="content"
+        title="Content (transmogrifier)"
+        description="Import the site's structure and content."
+        handler=".exportimport.importSiteStructure"
+        />
+
+</configure>
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/discussioncontainer.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/discussioncontainer.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/discussioncontainer.py	(revision 275)
@@ -0,0 +1,90 @@
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+
+from Acquisition import aq_base
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+class DiscussionContainerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+
+        self.doc = minidom.Document()
+
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            # check if object has comments
+            discussion_container = getattr(aq_base(obj), 'talkback', None)
+            if discussion_container is not None:
+                data = self.extractComments(discussion_container)
+                if data:
+                    files = item.setdefault('_files', {})
+                    item['_files']['discussioncontainer'] = {
+                        'name': '.comments.xml',
+                        'data': data,
+                    }
+
+            yield item
+
+    def extractComments(self, container):
+        doc = self.doc
+
+        items = container.objectItems()
+        if not items:
+            return None
+
+        root = doc.createElement('discussion')
+        doc.appendChild(root)
+        for item_id, item in items:
+            hdrlist = item.getMetadataHeaders()
+            # get creator (it is displayed in "Posted by")
+            hdrlist.append(('Creator', item.Creator()))
+            # get modification date (also is displayed)
+            hdrlist.append(('Modification_date', item.ModificationDate()))
+            # get relation
+            hdrlist.append(('In_reply_to', str(item.in_reply_to)))
+            # get comment text
+            hdrlist.append(('Text', item.text))
+
+            item_elem = doc.createElement('item')
+            attr = doc.createAttribute('id')
+            attr.value = item_id
+            item_elem.setAttributeNode(attr)
+
+            for k, v in hdrlist:
+                field = doc.createElement('field')
+                attr = doc.createAttribute('name')
+                attr.value = k
+                field.setAttributeNode(attr)
+                text = doc.createTextNode(v)
+                field.appendChild(text)
+                item_elem.appendChild(field)
+
+            root.appendChild(item_elem)
+
+        # all comments are strings encoded in 'utf-8' and they will properly
+        # saved in xml file, but if we explicitly give 'utf-8' encoding
+        # UnicodeDecodeError will be raised when they have non-ascii chars
+        data = self.doc.toprettyxml(indent='  ') #, encoding='utf-8')
+
+        self.doc.unlink()
+        return data
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/discussioncontainer.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/discussioncontainer.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/discussioncontainer.txt	(revision 275)
@@ -0,0 +1,92 @@
+Discussion container section
+============================
+
+The discussion container section blueprint name is
+``quintagroup.transmogrifier.discussioncontainer``.
+
+Discussion container needs at least 1 piece of information: the path to the
+object to update. To determine the path, the marshall section inspects each
+item and looks for one key, as described below. Any item missing this piece of
+information will be skipped. Similarly, items with a path that doesn't exist
+or haven't ``talkback`` attribute will be skipped as well.
+
+Paths to objects are always interpreted as relative to the context.
+
+>>> discussioncontainer = """
+... [transmogrifier]
+... pipeline =
+...     discussioncontainersource
+...     discussioncontainer
+...     printer
+...     dataprinter
+...     
+... [discussioncontainersource]
+... blueprint = quintagroup.transmogrifier.tests.discussioncontainersource
+... 
+... [discussioncontainer]
+... blueprint = quintagroup.transmogrifier.discussioncontainer
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     discussioncontainer
+...     data
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.discussioncontainer', discussioncontainer)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.discussioncontainer') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_files': {'discussioncontainer': {'data': ...
+                                    'name': '.comments.xml'}},
+ '_path': 'spam/eggs/foo'}
+<?xml version="1.0" ?>
+<discussion>
+  <item id="1">
+    <field name="Creator">
+      creator
+    </field>
+    <field name="Modification_date">
+      date
+    </field>
+    <field name="In_reply_to">
+      None
+    </field>
+    <field name="Text">
+      comment to content
+    </field>
+  </item>
+  <item id="2">
+    <field name="Creator">
+      creator
+    </field>
+    <field name="Modification_date">
+      date
+    </field>
+    <field name="In_reply_to">
+      1
+    </field>
+    <field name="Text">
+      reply to first comment
+    </field>
+  </item>
+  <item id="3">
+    <field name="Creator">
+      creator
+    </field>
+    <field name="Modification_date">
+      date
+    </field>
+    <field name="In_reply_to">
+      None
+    </field>
+    <field name="Text">
+      other comment to content
+    </field>
+  </item>
+</discussion>
+<BLANKLINE>
+{'_path': 'not/existing/bar'}
+{}
+{'_path': 'spam/eggs/notdiscussable'}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/export.cfg
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/export.cfg	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/export.cfg	(revision 275)
@@ -0,0 +1,33 @@
+[transmogrifier]
+pipeline =
+    sitewalker
+    manifest
+    marshall
+    propertymanager
+    discussioncontainer
+    exportadapters
+    writer
+
+[sitewalker]
+blueprint = quintagroup.transmogrifier.sitewalker
+
+[manifest]
+blueprint = quintagroup.transmogrifier.manifest
+
+[marshall]
+blueprint = quintagroup.transmogrifier.marshall
+
+[propertymanager]
+blueprint = quintagroup.transmogrifier.propertymanager
+exclude = title
+
+[discussioncontainer]
+blueprint = quintagroup.transmogrifier.discussioncontainer
+
+[exportadapters]
+blueprint = quintagroup.transmogrifier.exportadapters
+data-key = _files
+
+[writer]
+blueprint = quintagroup.transmogrifier.writer
+prefix = structure
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportadapters.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportadapters.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportadapters.py	(revision 275)
@@ -0,0 +1,44 @@
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+from zope.component import queryAdapter
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from quintagroup.transmogrifier.interfaces import IExportDataCorrector
+
+class ExportAdaptersSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.datakey = defaultMatcher(options, 'data-key', name, 'data')
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            datakey = self.datakey(*item.keys())[0]
+
+            if not (pathkey and datakey):
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            data_store = item[datakey]
+            if not data_store:
+                yield item; continue
+
+            for name, data in data_store.items():
+                adapter = queryAdapter(obj, IExportDataCorrector, name)
+                if adapter:
+                    data_store[name] = adapter(data)
+
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportadapters.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportadapters.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportadapters.txt	(revision 275)
@@ -0,0 +1,42 @@
+Export adapters section
+=======================
+
+The export adapters section blueprint name is
+``quintagroup.transmogrifier.exportadapters``.
+
+Export adapters needs at least 2 piece of information: the path to the object
+to update and data dictionary which determine, what adapters will be queried and
+what data they will consume. To determine the path, the marshall section inspects
+each item and looks for one key, as described below. Any item missing this piece of
+information will be skipped. Similarly, items with a path that doesn't exist
+or without data dictionary will be skipped too.
+
+Paths to objects are always interpreted as relative to the context.
+
+>>> exportadapters = """
+... [transmogrifier]
+... pipeline =
+...     exportadapterssource
+...     exportadapters
+...     printer
+...     
+... [exportadapterssource]
+... blueprint = quintagroup.transmogrifier.tests.exportadapterssource
+... 
+... [exportadapters]
+... blueprint = quintagroup.transmogrifier.exportadapters
+... data-key = _files
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.exportadapters', exportadapters)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.exportadapters') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_files': {'unchanged': 'this must be unchanged', 'mock': 'modified data'},
+ '_path': 'spam/eggs/foo'}
+{'_path': 'not/existing/bar'}
+{}
+{'_files': {'mock': "item hasn't path"}}
+{'_files': {'mock': "object isn't adaptable"},
+ '_path': 'spam/eggs/notadaptable'}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportimport.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportimport.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/exportimport.py	(revision 275)
@@ -0,0 +1,102 @@
+from zope.interface import implements
+
+from collective.transmogrifier.interfaces import ITransmogrifier
+from collective.transmogrifier.transmogrifier import _load_config, constructPipeline
+
+from Products.GenericSetup.context import TarballExportContext, TarballImportContext
+from Products.GenericSetup.interfaces import IFilesystemImporter
+
+from quintagroup.transmogrifier.writer import WriterSection
+from quintagroup.transmogrifier.reader import ReaderSection
+
+EXPORT_CONFIG = 'export'
+IMPORT_CONFIG = 'import'
+
+def exportSiteStructure(context):
+    transmogrifier = ITransmogrifier(context.getSite())
+
+    # we don't use transmogrifer's __call__ method, because we need to do
+    # some modification in pipeline sections
+
+    transmogrifier._raw = _load_config(EXPORT_CONFIG)
+    transmogrifier._data = {}
+
+    options = transmogrifier._raw['transmogrifier']
+    sections = options['pipeline'].splitlines()
+    pipeline = constructPipeline(transmogrifier, sections)
+
+    last_section = pipeline.gi_frame.f_locals['self']
+
+    # if 'quintagroup.transmogrifier.writer' section's export context is
+    # tarball replace it with given function argument
+    while hasattr(last_section, 'previous'):
+        if isinstance(last_section, WriterSection) and \
+            isinstance(last_section.export_context, TarballExportContext):
+            last_section.export_context = context
+        last_section = last_section.previous
+        # end cycle if we get empty starter section
+        if type(last_section) == type(iter(())):
+            break
+        last_section = last_section.gi_frame.f_locals['self']
+
+    # Pipeline execution
+    for item in pipeline:
+        pass # discard once processed
+
+def importSiteStructure(context):
+    # this function is also called when adding Plone site, so call standard handler
+    if not context.readDataFile('.objects.xml', subdir='structure'):
+        IFilesystemImporter(context.getSite()).import_(context, 'structure', True)
+        return
+
+    transmogrifier = ITransmogrifier(context.getSite())
+
+    # we don't use transmogrifer's __call__ method, because we need to do
+    # some modification in pipeline sections
+
+    transmogrifier._raw = _load_config(IMPORT_CONFIG)
+    transmogrifier._data = {}
+
+    options = transmogrifier._raw['transmogrifier']
+    sections = options['pipeline'].splitlines()
+    pipeline = constructPipeline(transmogrifier, sections)
+
+    last_section = pipeline.gi_frame.f_locals['self']
+
+    # if 'quintagroup.transmogrifier.writer' section's export context is
+    # tarball replace it with given function argument
+    while hasattr(last_section, 'previous'):
+        if isinstance(last_section, ReaderSection) and \
+            isinstance(last_section.import_context, TarballImportContext):
+            last_section.import_context = context
+        last_section = last_section.previous
+        # end cycle if we get empty starter section
+        if type(last_section) == type(iter(())):
+            break
+        last_section = last_section.gi_frame.f_locals['self']
+
+    # Pipeline execution
+    for item in pipeline:
+        pass # discard once processed
+
+
+class PloneSiteImporter(object):
+    """ Importer of plone site.
+    """
+    implements(IFilesystemImporter)
+
+    def __init__(self, context):
+        self.context = context
+
+    def import_(self, import_context, subdir="structure", root=False):
+        # When performing import steps we need to use standart importing adapter,
+        # if 'object.xml' file is absent in 'structure' directory of the profile.
+        # This may be because it is the base plone profile or extension profile, that has
+        # structure part in other format.
+
+        objects_xml = import_context.readDataFile('.objects.xml', subdir)
+        if objects_xml is not None:
+            importSiteStructure(import_context)
+        else:
+            from Products.CMFCore.exportimport.content import StructureFolderWalkingAdapter
+            StructureFolderWalkingAdapter(self.context).import_(import_context, "structure", True)
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/import.cfg
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/import.cfg	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/import.cfg	(revision 275)
@@ -0,0 +1,33 @@
+[transmogrifier]
+pipeline =
+    reader
+    manifestimport
+#    substitution
+    constructor
+#    xslt
+
+[reader]
+blueprint = quintagroup.transmogrifier.reader
+prefix = structure
+
+[manifestimport]
+blueprint = quintagroup.transmogrifier.manifestimport
+
+# standart import pipiline configuration doesn't need this section
+# this would go to more specific package
+[substitution]
+blueprint = collective.transmogrifier.substitution
+key = _type
+Blog = Weblog
+BlogFolder = Folder
+BlogEntry = WeblogEntry
+PloneFormMailer = FormFolder
+
+[constructor]
+blueprint = collective.transmogrifier.sections.constructor
+
+# this too
+[xslt]
+blueprint = quintagroup.transmogrifier.xslt
+from-key = _old_type
+to-key = _type
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/interfaces.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/interfaces.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/interfaces.py	(revision 275)
@@ -0,0 +1,9 @@
+from zope.interface import Interface
+
+class IExportDataCorrector(Interface):
+    """ Inteface for components that do some correction on exported data.
+    """
+
+    def __call__(data):
+        """ Correct data given in 'data' argument and return it.
+        """
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest.py	(revision 275)
@@ -0,0 +1,95 @@
+import os.path
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+class ManifestSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.entrieskey = defaultMatcher(options, 'entries-key', name, 'entries')
+
+    def __iter__(self):
+        for item in self.previous:
+            entrieskey = self.entrieskey(*item.keys())[0]
+            if not entrieskey:
+                yield item; continue
+
+            manifest = self.createManifest(item[entrieskey])
+
+            if manifest:
+                files = item.setdefault('_files', {})
+                item['_files']['manifest'] = {
+                    'name': '.objects.xml',
+                    'data': manifest,
+                }
+
+            yield item
+
+    def createManifest(self, data):
+        if not data:
+            return None
+        manifest = '<?xml version="1.0" ?>\n<manifest>\n'
+        for obj_id, obj_type in data:
+            manifest += '  <record type="%s">%s</record>\n' % (obj_type, obj_id)
+        manifest += "</manifest>\n"
+        return manifest
+
+class ManifestImportSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        # we need this dictionary to store manifest data, because reader section
+        # uses recursion when walking through content folders
+        self.manifests = {}
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            # skip items without path
+            if not pathkey: continue
+
+            path  = item[pathkey]
+
+            if path != '':
+                parent, item_id = os.path.split(path)
+                manifest = self.manifests.get(parent, {})
+
+                # skip that are not listed in their parent's manifest
+                if item_id not in manifest: continue
+
+                item['_type'] = manifest.pop(item_id)
+                # remove empty manifest dict
+                if not manifest:
+                    del self.manifests[parent]
+
+            # this item is folderish - parse manifest
+            if fileskey and 'manifest' in item[fileskey]:
+                self.extractManifest(path, item[fileskey]['manifest']['data'])
+
+            yield item
+
+    def extractManifest(self, path, data):
+        doc = minidom.parseString(data)
+        objects = {}
+        for record in doc.getElementsByTagName('record'):
+            type_ = str(record.getAttribute('type'))
+            object_id = str(record.firstChild.nodeValue.strip())
+            objects[object_id] = type_
+        self.manifests[path] = objects
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest.txt	(revision 275)
@@ -0,0 +1,36 @@
+Manifest section
+================
+
+>>> manifest = """
+... [transmogrifier]
+... pipeline =
+...     manifestsource
+...     manifest
+...     printer
+... 
+... [manifestsource]
+... blueprint = quintagroup.transmogrifier.tests.manifestsource
+... 
+... [manifest]
+... blueprint = quintagroup.transmogrifier.manifest
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.manifest',
+...                manifest)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.manifest') # doctest: +ELLIPSIS
+{'_entries': (('document1', 'Document'),
+              ('folder1', 'Folder'),
+              ('document3', 'Document')),
+ '_files': {'manifest': {'data': ...
+                         'name': '.objects.xml'}}}
+{}
+>>> print data
+<?xml version="1.0" ?>
+<manifest>
+  <record type="Document">document1</record>
+  <record type="Folder">folder1</record>
+  <record type="Document">document3</record>
+</manifest>
+<BLANKLINE>
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest_import.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest_import.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/manifest_import.txt	(revision 275)
@@ -0,0 +1,31 @@
+Manifest import section
+=======================
+
+>>> manifest = """
+... [transmogrifier]
+... pipeline =
+...     manifestsource
+...     manifest
+...     printer
+... 
+... [manifestsource]
+... blueprint = quintagroup.transmogrifier.tests.manifestsource
+... 
+... [manifest]
+... blueprint = quintagroup.transmogrifier.manifestimport
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.manifest',
+...                manifest)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.manifest') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_files': {'manifest': {'data': ...
+                         'name': '.objects.xml'}},
+ '_path': ''}
+{'_type': 'Document', '_path': 'document1'}
+{'_files': {'manifest': {'data': ...
+                         'name': '.objects.xml'}},
+ '_path': 'folder1',
+ '_type': 'Folder'}
+{'_type': 'Document', '_path': 'folder1/document2'}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/marshall.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/marshall.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/marshall.py	(revision 275)
@@ -0,0 +1,56 @@
+from zope.interface import classProvides, implements
+from ZODB.POSException import ConflictError
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.Marshall import registry
+from Products.Archetypes.interfaces import IBaseObject
+
+class MarshallSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_fields')
+
+        self.marshaller = registry.getComponent("atxml")
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IBaseObject.providedBy(obj):
+                excludekey = self.excludekey(*item.keys())[0]
+                atns_exclude = ()
+                if excludekey:
+                    atns_exclude = item[excludekey]
+                    
+                try:
+                    content_type, length, data = self.marshaller.marshall(obj, atns_exclude=atns_exclude)
+                except ConflictError:
+                    raise
+                except:
+                    data = None
+                    
+                if data or data is None:
+                    # None value has special meaning for IExportDataCorrector adapter for topic criterias
+                    files = item.setdefault('_files', {})
+                    item['_files']['marshall'] = {
+                        'name': '.marshall.xml',
+                        'data': data,
+                    }
+
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/marshall.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/marshall.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/marshall.txt	(revision 275)
@@ -0,0 +1,53 @@
+Marshall section
+================
+
+The marshall section blueprint name is
+``quintagroup.transmogrifier.marshall``.
+
+Marshall needs at least 1 piece of information: the path to the object
+to update. To determine the path, the marshall section inspects each
+item and looks for one key, as described below. Any item missing this piece of
+information will be skipped. Similarly, items with a path that doesn't exist
+or are not Archetypes objects will be skipped as well.
+
+For the object path, it'll look (in order) for
+``_plone.app.transmogrifier.atschemaupdater_[sectionname]_path``,
+``_plone.app.transmogrifier.atschemaupdater_path``, ``_[sectionname]_path``
+and ``_path``, where ``[sectionname]`` is replaced with the name given to the
+current section. This allows you to target the right section precisely if
+needed. Alternatively, you can specify what key to use for the path by
+specifying the ``path-key`` option, which should be a list of keys to try (one
+key per line, use a ``re:`` or ``regexp:`` prefix to specify regular
+expressions).
+
+Paths to objects are always interpreted as relative to the context.
+
+>>> import pprint
+>>> marshall = """
+... [transmogrifier]
+... pipeline =
+...     marshallsource
+...     marshall
+...     printer
+...     
+... [marshallsource]
+... blueprint = quintagroup.transmogrifier.tests.marshallsource
+... 
+... [marshall]
+... blueprint = quintagroup.transmogrifier.marshall
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.marshall', marshall)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.marshall')  # doctest: +REPORT_NDIFF
+{'_excluded_fields': ('fieldone', 'fieldtwo'),
+ '_files': {'marshall': {'data': 'marshalled', 'name': '.marshall.xml'}},
+ '_path': 'spam/eggs/foo'}
+{'_files': {'marshall': {'data': None, 'name': '.marshall.xml'}},
+ '_path': 'topic/criterion'}
+{'_path': 'not/existing/bar'}
+{}
+{'_path': 'spam/eggs/notatcontent'}
+>>> pprint.pprint(plone.exported)
+(('spam/eggs/foo', ('fieldone', 'fieldtwo')), ('topic/criterion', ()))
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/meta.zcml
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/meta.zcml	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/meta.zcml	(revision 275)
@@ -0,0 +1,12 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta">
+
+    <meta:directive
+        namespace="http://namespaces.plone.org/transmogrifier"
+        name="stylesheet"
+        schema=".xslt.IStylesheetDirective"
+        handler=".xslt.stylesheet"
+        />
+
+</configure>
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/patches.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/patches.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/patches.py	(revision 275)
@@ -0,0 +1,79 @@
+import os
+import time
+from tarfile import TarInfo, DIRTYPE
+from StringIO import StringIO
+
+# TarballExportContext don't write dirs in tarball and we need to fix this
+
+#security.declareProtected( ManagePortal, 'writeDataFile' )
+def writeDataFile( self, filename, text, content_type, subdir=None ):
+
+    """ See IExportContext.
+    """
+    mod_time = time.time()
+
+    if subdir is not None:
+        elements = subdir.split('/')
+        parents = filter(None, elements)
+        while parents:
+            dirname = os.path.join(*parents)
+            try:
+                self._archive.getmember(dirname+'/')
+            except KeyError:
+                info = TarInfo(dirname)
+                info.size = 0
+                info.mtime = mod_time
+                info.type = DIRTYPE
+                self._archive.addfile(info, StringIO())
+            parents = parents[:-1]
+
+        filename = '/'.join( ( subdir, filename ) )
+
+    stream = StringIO( text )
+    info = TarInfo( filename )
+    info.size = len( text )
+    info.mtime = mod_time
+    self._archive.addfile( info, stream )
+
+from Products.GenericSetup.context import TarballExportContext
+TarballExportContext.writeDataFile = writeDataFile
+
+from Products.GenericSetup.context import SKIPPED_FILES, SKIPPED_SUFFIXES
+
+def listDirectory(self, path, skip=SKIPPED_FILES,
+                    skip_suffixes=SKIPPED_SUFFIXES):
+
+    """ See IImportContext.
+    """
+    if path is None:  # root is special case:  no leading '/'
+        path = ''
+    elif path:
+        if not self.isDirectory(path):
+            return None
+
+        if not path.endswith('/'):
+            path = path + '/'
+
+    pfx_len = len(path)
+
+    names = []
+    for name in self._archive.getnames():
+        if name == path or not name.startswith(path):
+            continue
+        name = name[pfx_len:]
+        if name.count('/') > 1:
+            continue
+        if '/' in name and not name.endswith('/'):
+            continue
+        if name in skip:
+            continue
+        if [s for s in skip_suffixes if name.endswith(s)]:
+            continue
+        # directories have trailing '/' character and we need to remove it
+        names.rstrip('/')
+        names.append(name)
+
+    return names
+
+from Products.GenericSetup.context import TarballImportContext
+TarballImportContext.listDirectory = listDirectory
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/propertymanager.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/propertymanager.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/propertymanager.py	(revision 275)
@@ -0,0 +1,70 @@
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from OFS.interfaces import IPropertyManager
+from Products.GenericSetup.utils import PropertyManagerHelpers
+
+class PropertyManagerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_properties')
+        if 'exclude' in options:
+            self.exclude = [i.strip() for i in options['exclude'].splitlines() if i.strip()]
+        else:
+            self.exclude = []
+
+        self.helper = PropertyManagerHelpers()
+        self.doc = minidom.Document()
+        self.helper._doc = self.doc
+
+
+    def __iter__(self):
+        helper = self.helper
+        doc = self.doc
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IPropertyManager.providedBy(obj):
+                data = None
+                excludekey = self.excludekey(*item.keys())[0]
+                excluded_props = self.exclude
+                if excludekey:
+                    excluded_props = tuple(set(item[excludekey]) | set(excluded_props))
+
+                helper.context = obj
+                node = doc.createElement('properties')
+                for elem in helper._extractProperties().childNodes:
+                    if elem.getAttribute('name') not in excluded_props:
+                        node.appendChild(elem)
+                if node.hasChildNodes():
+                    doc.appendChild(node)
+                    data = doc.toprettyxml(indent='  ', encoding='utf-8')
+                    doc.unlink()
+
+                if data:
+                    files = item.setdefault('_files', {})
+                    item['_files']['propertymanager'] = {
+                        'name': '.properties.xml',
+                        'data': data,
+                    }
+
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/propertymanager.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/propertymanager.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/propertymanager.txt	(revision 275)
@@ -0,0 +1,61 @@
+Property manager section
+========================
+
+The marshall section blueprint name is
+``quintagroup.transmogrifier.propertymanager``.
+
+Property manager needs at least 1 piece of information: the path to the object
+to update. To determine the path, the marshall section inspects each
+item and looks for one key, as described below. Any item missing this piece of
+information will be skipped. Similarly, items with a path that doesn't exist
+or are not objects that provide OFS.interfaces.IPropertyManager will be skipped as well.
+
+Paths to objects are always interpreted as relative to the context.
+
+>>> import pprint
+>>> propertymanager = """
+... [transmogrifier]
+... pipeline =
+...     propertymanagersource
+...     propertymanager
+...     printer
+...     dataprinter
+...     
+... [propertymanagersource]
+... blueprint = quintagroup.transmogrifier.tests.propertymanagersource
+... 
+... [propertymanager]
+... blueprint = quintagroup.transmogrifier.propertymanager
+... exclude = 
+...     propone
+...     proptwo
+...     notexistentprop
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     propertymanager
+...     data
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.propertymanager', propertymanager)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.propertymanager') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_excluded_properties': ('proptwo', 'propthree'),
+ '_files': {'propertymanager': {'data': ...
+                                'name': '.properties.xml'}},
+ '_path': 'spam/eggs/foo'}
+<?xml version="1.0" encoding="utf-8"?>
+<properties>
+  <property name="propfour">
+    value
+  </property>
+</properties>
+<BLANKLINE>
+{'_path': 'not/existing/bar'}
+{}
+{'_path': 'spam/eggs/notatcontent'}
+>>> pprint.pprint(plone.updated)
+('spam/eggs/foo',)
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/reader.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/reader.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/reader.py	(revision 275)
@@ -0,0 +1,87 @@
+import os.path
+
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.GenericSetup import context
+from Products.CMFCore import utils
+
+FILENAME_TO_SECTION = {
+    '.objects.xml': 'manifest',
+    '.marshall.xml': 'marshall',
+    '.properties.xml': 'propertymanager',
+    '.comments.xml': 'discussioncontainer'
+}
+
+class ReaderSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = options.get('path-key', '_path').strip()
+        self.fileskey = options.get('files-key', '_files').strip()
+        self.contextkey = options.get('context-key', '_import_context').strip()
+
+        if 'prefix' in options:
+            self.prefix = options['prefix'].strip()
+            self.prefix = self.prefix.strip('/')
+        else:
+            self.prefix = ''
+
+        context_type = options.get('context', 'tarball').strip()
+        if context_type not in ['directory', 'tarball', 'snapshot']:
+            context_type = 'tarball'
+
+        path = options.get('path', '').strip()
+
+        setup_tool = utils.getToolByName(self.context, 'portal_setup')
+        if context_type == 'directory':
+            self.import_context = context.DirectoryImportContext(setup_tool, path)
+        elif context_type == 'tarball':
+            if os.path.exists(path):
+                archive = file(path, 'rb')
+                archive_bits = archive.read()
+                archive.close()
+            else:
+                archive_bits = ''
+            self.import_context = context.TarballImportContext(setup_tool, archive_bits)
+        elif context_type == 'snapshot':
+            self.import_context = context.SnapshotImportContext(setup_tool, path)
+
+    def walk(self, top):
+        names = self.import_context.listDirectory(top)
+        yield self.readFiles(top, names)
+        for name in names:
+            name = os.path.join(top, name)
+            if self.import_context.isDirectory(name):
+                for i in self.walk(name):
+                    yield i
+
+    def readFiles(self, top, names):
+        path = top[len(self.prefix):]
+        path = path.lstrip('/')
+        item = {self.pathkey: path}
+        for name in names:
+            full_name = os.path.join(top, name)
+            if self.import_context.isDirectory(full_name): continue
+            section = FILENAME_TO_SECTION.get(name, name)
+            files = item.setdefault(self.fileskey, {})
+            files[section] = {
+                'name': name,
+                'data': self.import_context.readDataFile(name, top)
+            }
+        return item
+
+    def __iter__(self):
+        for item in self.previous:
+            yield item
+
+        for item in self.walk(self.prefix):
+            # add import context to item (some next section may use it)
+            item[self.contextkey] = self.import_context
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/reader.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/reader.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/reader.txt	(revision 275)
@@ -0,0 +1,92 @@
+Reader section
+==============
+
+A reader source pipeline section walks through different GenericSetup
+import contexts and yields items for every folder. Item contains it's
+path stored on a key given in ``path-key`` option and dictionary of
+data of files, contained in directory to which this item corresponds,
+stored on a key given in ``files-key`` option. The reader section blueprint
+name is ``quintagroup.transmogrifier.reader``.
+
+Which import context to use is controlled by ``context`` option. Possible
+values are: directory, tarball (default if option is absent or it's illegal)
+and snapshot. The only required option in this section is ``path`` which
+is a filesystem path of directory or archive for first two contexts, or
+snapshot's id for last context. Other usefull option is ``prefix`` which
+is additional path that must be added to ``path`` (it was designed to
+be used for tarball context, where content lives in 'structure' subdirectory,
+but may be used for other two too). Also note that ``prefix`` isn't added
+to generated item's path.
+
+>>> reader = """
+... [transmogrifier]
+... pipeline =
+...     reader
+...     printer
+... 
+... [reader]
+... blueprint = quintagroup.transmogrifier.reader
+... prefix = structure
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.reader', reader)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.reader') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_files': {'other.file': {'data': 'some data', 'name': 'other.file'},
+            'propertymanager': {'data': 'some data',
+                                'name': '.properties.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': ''}
+{'_files': {'manifest': {'data': 'some data', 'name': '.objects.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': 'news'}
+{'_import_context': <Tarball ...>,
+ '_path': 'news/recent'}
+{'_files': {'manifest': {'data': 'some data', 'name': '.objects.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': 'pages'}
+{'_files': {'discussioncontainer': {'data': 'some data',
+                                    'name': '.comments.xml'},
+            'marshall': {'data': 'some data', 'name': '.marshall.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': 'pages/front-page'}
+
+Now we test if reader context is properly controlled by ``context`` option.
+
+>>> dirreader = """
+... [transmogrifier]
+... pipeline =
+...     reader
+...     printer
+... 
+... [reader]
+... blueprint = quintagroup.transmogrifier.reader
+... context = directory
+... path = /path/to/directory
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.dirreader', dirreader)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.dirreader') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_import_context': <Directory args=(..., '/path/to/directory')>,
+ '_path': ''}
+>>> dbreader = """
+... [transmogrifier]
+... pipeline =
+...     reader
+...     printer
+... 
+... [reader]
+... blueprint = quintagroup.transmogrifier.reader
+... context = snapshot
+... path = snapshot-20081008163000
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.dbreader', dbreader)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.dbreader') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_import_context': <Snapshot args=(..., 'snapshot-20081008163000')>,
+ '_path': ''}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/sitewalker.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/sitewalker.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/sitewalker.py	(revision 275)
@@ -0,0 +1,38 @@
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+
+from Products.CMFCore.interfaces import IFolderish
+from Products.Archetypes.interfaces import IBaseFolder
+
+class SiteWalkerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+    def walk(self, obj):
+        if IFolderish.providedBy(obj) or IBaseFolder.providedBy(obj):
+            contained = [(k, v.getPortalTypeName()) for k, v in obj.contentItems()]
+            yield obj, tuple(contained)
+            for v in obj.contentValues():
+                for x in self.walk(v):
+                    yield x
+        else:
+            yield obj, ()
+
+    def __iter__(self):
+        for item in self.previous:
+            yield item
+
+        for obj, contained in self.walk(self.context):
+            item = {
+                '_path': '/'.join(obj.getPhysicalPath()[2:]),
+                '_type': obj.getPortalTypeName(),
+            }
+            if contained:
+                item['_entries'] = contained
+            yield item
+
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/sitewalker.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/sitewalker.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/sitewalker.txt	(revision 275)
@@ -0,0 +1,39 @@
+Site walker source section
+==========================
+
+A Plone site walker source pipeline section lets you create pipeline items from
+contents of Plone site folders. The site walker source section blueprint name is
+``quitagroup.transmogrifier.sitewalker``.
+
+A site walker source section will recurse through site's content tree, starting 
+form folder, which is the context of transmogrifier, and will yield an item for
+each content object. For folder it will also add a tuple of (id, portal_type)
+pairs of contained items.
+
+>>> sitewalker = """
+... [transmogrifier]
+... pipeline =
+...     sitewalker
+...     printer
+...
+... [sitewalker]
+... blueprint = quintagroup.transmogrifier.sitewalker
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.sitewalker',
+...                sitewalker)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.sitewalker')
+{'_entries': (('document1', 'Document'),
+              ('folder1', 'Folder'),
+              ('document3', 'Document')),
+ '_path': '',
+ '_type': 'MockPortal'}
+{'_type': 'Document', '_path': 'document1'}
+{'_entries': (('folder2', 'Folder'), ('document2', 'Document')),
+ '_path': 'folder1',
+ '_type': 'Folder'}
+{'_type': 'Folder', '_path': 'folder1/folder2'}
+{'_type': 'Document', '_path': 'folder1/document2'}
+{'_type': 'Document', '_path': 'document3'}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/substitution.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/substitution.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/substitution.py	(revision 275)
@@ -0,0 +1,24 @@
+from zope.interface import classProvides, implements
+from collective.transmogrifier.interfaces import ISectionBlueprint
+from collective.transmogrifier.interfaces import ISection
+
+class SubstitutionSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.key = options['key'].strip()
+        self.oldkey = "_old" + self.key
+        self.options = options
+        self.previous = previous
+
+    def __iter__(self):
+        key = self.key
+        for item in self.previous:
+            if key in item:
+                old = item[key]
+                new = self.options.get(item[key])
+                if new is not None and old != new:
+                    item[key] = new.strip()
+                    item[self.oldkey] = old
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/substitution.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/substitution.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/substitution.txt	(revision 275)
@@ -0,0 +1,37 @@
+Substitution section
+====================
+
+A substitution pipeline section lets you substitute item's value stored
+on some key with value given in options. The substitution section blueprint
+name is ``quintagroup.transmogrifier.substitution``.
+
+Substition is performed in this way: if section has option with name that
+is equal to value stored on this key, than value of this option is used
+as new value for this key. Old value is stored on ``_old${key}`` key.
+Which key to use is determined by ``key`` option.
+
+>>> substitution = """
+... [transmogrifier]
+... pipeline =
+...     source
+...     substitution
+...     printer
+... 
+... [source]
+... blueprint = quintagroup.transmogrifier.tests.substitutionsource
+... 
+... [substitution]
+... blueprint = quintagroup.transmogrifier.substitution
+... key = _type
+... Blog = Weblog
+... PloneFormMailer = FormFolder
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.substitution', substitution)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.substitution') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_old_type': 'Blog', '_type': 'Weblog'}
+{'_old_type': 'PloneFormMailer', '_type': 'FormFolder'}
+{'_type': 'Document'}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/tests.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/tests.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/tests.py	(revision 275)
@@ -0,0 +1,708 @@
+import unittest
+import pprint
+import os
+
+from zope.testing import doctest, cleanup
+from zope.component import provideUtility, provideAdapter, adapts
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISectionBlueprint, ISection
+from collective.transmogrifier.tests import tearDown
+from collective.transmogrifier.sections.tests import sectionsSetUp
+from collective.transmogrifier.sections.tests import SampleSource
+
+from Products.Five import zcml
+
+import quintagroup.transmogrifier
+from quintagroup.transmogrifier.xslt import stylesheet_registry
+
+# Doctest support
+
+class DataPrinter(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.printkey = [i.strip() for i in options['print'].splitlines() if i.strip()]
+        if 'prettyprint' in options:
+            self.pprint = pprint.PrettyPrinter().pprint
+
+    def __iter__(self):
+        for item in self.previous:
+            if self.printkey:
+                data = item
+                for i in self.printkey:
+                    if i in data:
+                        data = data[i]
+                    else:
+                        data = None
+                        break
+                if data is not None:
+                    if hasattr(self, 'pprint'):
+                        self.pprint(data)
+                    else:
+                        print data
+            yield item
+
+ctSectionsSetup = sectionsSetUp
+def sectionsSetUp(test):
+    ctSectionsSetup(test)
+    # load meta.zcml of directives that are used in out package config
+    import Products.GenericSetup
+    zcml.load_config('meta.zcml', Products.GenericSetup)
+    zcml.load_config('configure.zcml', quintagroup.transmogrifier)
+
+    provideUtility(DataPrinter,
+        name=u'quintagroup.transmogrifier.tests.dataprinter')
+
+def siteWalkerSetUp(test):
+    sectionsSetUp(test)
+
+    from Products.CMFCore.interfaces import IFolderish
+    from Products.Archetypes.interfaces import IBaseFolder
+
+    class MockContent(object):
+        path = ()
+
+        def getPhysicalPath(self):
+            return self.path
+
+        def getPortalTypeName(self):
+            return self.__class__.__name__
+
+    class Document(MockContent):
+        pass
+
+    class Folder(MockContent, dict):
+        implements(IBaseFolder)
+
+        contentItems = dict.items
+        contentValues = dict.values
+
+    class MockPortal(MockContent, dict):
+        implements(IFolderish)
+
+        contentItems = dict.items
+        contentValues = dict.values
+
+    portal = MockPortal()
+
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    portal.path = ('', 'plone')
+    portal['document1'] = Document()
+    portal['document1'].path = ('', 'plone', 'document1')
+    portal['folder1'] = Folder()
+    portal['folder1'].path = ('', 'plone', 'folder1')
+    portal['folder1']['document2'] = Document()
+    portal['folder1']['document2'].path = ('', 'plone', 'folder1', 'document2')
+    portal['folder1']['folder2'] = Folder()
+    portal['folder1']['folder2'].path = ('', 'plone', 'folder1', 'folder2')
+    portal['document3'] = Document()
+    portal['document3'].path = ('', 'plone', 'document3')
+
+def manifestSetUp(test):
+    sectionsSetUp(test)
+
+    item = {'_entries' : (
+        ('document1', 'Document'),
+        ('folder1', 'Folder'),
+        ('document3', 'Document'),)
+    }
+
+    class ManifestSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(ManifestSource, self).__init__(*args, **kw)
+            self.sample = (item, dict())
+
+    provideUtility(ManifestSource,
+        name=u'quintagroup.transmogrifier.tests.manifestsource')
+
+    from quintagroup.transmogrifier.manifest import ManifestSection
+    section = ManifestSection(test.globs['transmogrifier'], 
+        'manifest', {'blueprint': ''}, iter(()))
+    data = section.createManifest(item['_entries'])
+    test.globs['data'] = data
+
+def marshallSetUp(test):
+    sectionsSetUp(test)
+
+    from Products.Archetypes.interfaces import IBaseObject
+
+    class MockCriterion(object):
+        implements(IBaseObject)
+
+    class MockPortal(object):
+        implements(IBaseObject)
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path == 'topic/criterion':
+                self._last_path = path
+                return MockCriterion()
+            if path.endswith('/notatcontent'):
+                return object()
+            self._last_path = path
+            return self
+
+        def getId(self):
+            return "plone"
+
+        exported = ()
+        def marshall(self, instance, **kwargs):
+            self.exported += ((self._last_path, kwargs.get('atns_exclude')),)
+            # Marshall often fails to export topic criteria
+            if isinstance(instance, MockCriterion):
+                return None, None, None
+            else:
+                return None, None, "marshalled"
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    from Products.Marshall import registry
+    def getComponent(name):
+        return portal
+    registry.getComponent = getComponent
+
+    class MarshallSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(MarshallSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='spam/eggs/foo', _excluded_fields=('fieldone','fieldtwo')),
+                dict(_path='topic/criterion'),
+                dict(_path='not/existing/bar'),
+                dict(),
+                dict(_path='spam/eggs/notatcontent'),
+            )
+    provideUtility(MarshallSource,
+        name=u'quintagroup.transmogrifier.tests.marshallsource')
+
+def propertyManagerSetUp(test):
+    sectionsSetUp(test)
+
+    from OFS.interfaces import IPropertyManager
+
+    class MockPortal(object):
+        implements(IPropertyManager)
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notatcontent'):
+                return object()
+            self._last_path = path
+            return self
+
+        updated = ()
+        def _propertyMap(self):
+            self.updated += (self._last_path,)
+            return ({'id':'propone', 'type': 'string', 'mode': 'w'},
+                    {'id':'proptwo', 'type': 'string', 'mode': 'w'},
+                    {'id':'propthree', 'type': 'string', 'mode': 'w'},
+                    {'id':'propfour', 'type': 'string', 'mode': 'w'})
+
+        def getProperty(id, d=None):
+            return 'value'
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class PropertyManagerSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(PropertyManagerSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='spam/eggs/foo', _excluded_properties=('proptwo','propthree')),
+                dict(_path='not/existing/bar'),
+                dict(),
+                dict(_path='spam/eggs/notatcontent'),
+            )
+
+    provideUtility(PropertyManagerSource,
+        name=u'quintagroup.transmogrifier.tests.propertymanagersource')
+
+def discussionContainerSetUp(test):
+    sectionsSetUp(test)
+
+    class MockDiscussionItem(object):
+        def __init__(self, reply, text):
+            self.in_reply_to = reply
+            self.text = text
+
+        def Creator(self):
+            return "creator"
+
+        def ModificationDate(self):
+            return "date"
+
+        def getMetadataHeaders(self):
+            return []
+
+    class MockPortal(object):
+        discussion = {
+            '1': MockDiscussionItem(None, 'comment to content'),
+            '2': MockDiscussionItem('1', 'reply to first comment'),
+            '3': MockDiscussionItem(None, 'other comment to content')
+        }
+
+        @property
+        def talkback(self):
+            return self
+
+        def objectItems(self):
+            l = self.discussion.items()
+            l.sort(key=lambda x: int(x[0]))
+            return l
+
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notdiscussable'):
+                return object()
+            return self
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    import Acquisition
+    def aq_base(obj):
+        return obj
+    Acquisition.aq_base = aq_base
+
+    class DiscussionContainerSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(DiscussionContainerSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='spam/eggs/foo'),
+                dict(_path='not/existing/bar'),
+                dict(),
+                dict(_path='spam/eggs/notdiscussable'),
+            )
+
+    provideUtility(DiscussionContainerSource,
+        name=u'quintagroup.transmogrifier.tests.discussioncontainersource')
+
+
+def exportAdaptersSetUp(test):
+    sectionsSetUp(test)
+
+    class MockPortal(object):
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notadaptable'):
+                return object()
+            return self
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    from quintagroup.transmogrifier.exportadapters import IExportDataCorrector
+
+    class MockAdapter(object):
+        implements(IExportDataCorrector)
+        adapts(MockPortal)
+        def __init__(self, context):
+            self.context = context
+
+        def __call__(self, data):
+            return "modified data"
+
+    provideAdapter(MockAdapter, name="mock")
+
+    class ExportAdaptersSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(ExportAdaptersSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='spam/eggs/foo',
+                     _files=dict(mock='some data', unchanged='this must be unchanged')),
+                dict(_path='not/existing/bar'),
+                dict(),
+                dict(_files=dict(mock="item hasn't path")),
+                dict(_path='spam/eggs/notadaptable', _files=dict(mock="object isn't adaptable")),
+            )
+
+    provideUtility(ExportAdaptersSource,
+        name=u'quintagroup.transmogrifier.tests.exportadapterssource')
+
+def writerSetUp(test):
+    sectionsSetUp(test)
+
+    class MockExportContext(object):
+        def __init__( self, *args, **kwargs):
+            self.args = args
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+            self._wrote = []
+
+        def __getitem__(self, name):
+            return getattr(self, name, None)
+
+        def __contains__(self, name):
+            return hasattr(self, name)
+
+        def writeDataFile(self, filename, text, content_type, subdir=None):
+            filename = '%s/%s' % (subdir, filename)
+            self._wrote.append((filename, text, content_type))
+
+        def __repr__(self):
+            s = " ".join(["%s=%s" % (k,v) for k,v in self.__dict__.items()])
+            return "<%s %s>" % (self.__class__.__name__, s)
+
+
+    from Products.GenericSetup import context
+
+    context.DirectoryExportContext = type('Directory', (MockExportContext,), {})
+    context.TarballExportContext = type('Tarball', (MockExportContext,), {})
+    context.SnapshotExportContext = type('Snapshot', (MockExportContext,), {})
+
+    from Products.CMFCore import utils
+    def getToolByName(context, tool_id):
+        return context
+    utils.getToolByName = getToolByName
+
+    class WriterSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(WriterSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='spam/eggs/foo'),
+                dict(_files=dict(mock=dict(name='.first.xml', data='some data'))),
+                dict(_path='spam/eggs/foo',
+                     _files=dict(mock=dict(name='.first.xml', data='some data'),
+                                 other=dict(name='.second.xml', data='other data'))),
+                dict(_path='other/path',
+                     _files=dict(mock=dict(name='.third.xml', data='some data')))
+            )
+
+    provideUtility(WriterSource,
+        name=u'quintagroup.transmogrifier.tests.writersource')
+
+    class SingleItemSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(SingleItemSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='', _files={}),
+            )
+
+    provideUtility(SingleItemSource,
+        name=u"quintagroup.transmogrifier.tests.singleitemsource")
+
+def readerSetUp(test):
+    sectionsSetUp(test)
+
+    class MockImportContext(object):
+
+        _dirs = [
+            'structure',
+            'structure/news', 'structure/news/recent',
+            'structure/pages', 'structure/pages/front-page',
+        ]
+        _files = [
+            'structure/.properties.xml',
+            'structure/other.file',
+            'structure/news/.objects.xml',
+            'structure/pages/.objects.xml',
+            'structure/pages/front-page/.marshall.xml',
+            'structure/pages/front-page/.comments.xml',
+        ]
+
+        def __init__( self, *args, **kwargs):
+            self.args = args
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+        def __repr__(self):
+            s = " ".join(["%s=%s" % (k,v) for k,v in self.__dict__.items()])
+            return "<%s %s>" % (self.__class__.__name__, s)
+
+        def readDataFile(self, filename, subdir=None):
+            return 'some data'
+
+        def isDirectory(self, path):
+            return path == '' or path in self._dirs
+
+        def listDirectory(self, path):
+            all_names = self._dirs + self._files
+            if path:
+                pfx_len = len(path)+1
+            else:
+                pfx_len = 0
+            names = []
+            for name in all_names:
+                if name == path:
+                    continue
+                if not name.startswith(path):
+                    continue
+                name = name[pfx_len:]
+                if '/' in name:
+                    continue
+                names.append(name)
+            return names
+
+    from Products.GenericSetup import context
+
+    context.DirectoryImportContext = type('Directory', (MockImportContext,),
+        {'listDirectory': lambda self, path: []})
+    context.TarballImportContext = type('Tarball', (MockImportContext,), {})
+    context.SnapshotImportContext = type('Snapshot', (MockImportContext,),
+        {'listDirectory': lambda self, path: []})
+
+    from Products.CMFCore import utils
+    def getToolByName(context, tool_id):
+        return context
+    utils.getToolByName = getToolByName
+
+def manifestImportSetUp(test):
+    sectionsSetUp(test)
+
+    man1 = """<?xml version="1.0" ?>
+<manifest>
+  <record type="Document">document1</record>
+  <record type="Folder">folder1</record>
+</manifest>
+"""
+
+    man2 = """<?xml version="1.0" ?>
+<manifest>
+  <record type="Document">document2</record>
+  <record type="Document">document3</record>
+</manifest>
+"""
+
+    item1 = dict(
+        _path='',
+        _files=dict(
+            manifest=dict(
+                name='.objects.xml',
+                data=man1
+            )
+        )
+    )
+
+    item2 = dict(
+        _path='document1',
+    )
+
+    item3 = dict(
+        _path='folder1',
+        _files=dict(
+            manifest=dict(
+                name='.objects.xml',
+                data=man2
+            )
+        )
+    )
+
+    item4 = dict(
+        _path='folder1/document2',
+    )
+
+    item5 = dict(
+        _path='document4',
+    )
+
+    class ManifestSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(ManifestSource, self).__init__(*args, **kw)
+            self.sample = (item1, dict(), item2, item3, item4, item5)
+
+    provideUtility(ManifestSource,
+        name=u'quintagroup.transmogrifier.tests.manifestsource')
+
+def substitutionSetUp(test):
+    sectionsSetUp(test)
+
+    class SubstitutionSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(SubstitutionSource, self).__init__(*args, **kw)
+            self.sample = (
+                {},
+                {'_type': 'Blog'},
+                {'_type': 'PloneFormMailer'},
+                {'_type': 'Document'},
+            )
+
+    provideUtility(SubstitutionSource,
+        name=u'quintagroup.transmogrifier.tests.substitutionsource')
+
+class MetaDirectivesTests(unittest.TestCase):
+    def setUp(self):
+        zcml.load_config('meta.zcml', quintagroup.transmogrifier)
+
+    def tearDown(self):
+        stylesheet_registry.clear()
+        cleanup.cleanUp()
+
+    def testEmptyZCML(self):
+        zcml.load_string('''\
+<configure xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier">
+</configure>''')
+        self.assertEqual(stylesheet_registry.listStylesheetNames(), ())
+
+    def testConfigZCML(self):
+        zcml.load_string('''\
+<configure
+    xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier">
+<transmogrifier:stylesheet
+    source="marshall"
+    from="Blog"
+    to="Weblog"
+    file="blog.xsl"
+    />
+</configure>''')
+        self.assertEqual(stylesheet_registry.listStylesheetNames(),
+                         (u'marshall:Blog:Weblog',))
+        path = os.path.split(quintagroup.transmogrifier.__file__)[0]
+        self.assertEqual(
+            stylesheet_registry.getStylesheet('marshall', 'Blog', 'Weblog'),
+            dict(from_=u'Blog',
+                 to=u'Weblog',
+                 file=os.path.join(path, 'blog.xsl'))
+        )
+
+    def testMultipleZCML(self):
+        zcml.load_string('''\
+<configure
+    xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier">
+<transmogrifier:stylesheet
+    source="marshall"
+    from="Blog"
+    to="Weblog"
+    file="blog.xsl"
+    />
+<transmogrifier:stylesheet
+    source="propertymanager"
+    from="BlogEntry"
+    to="WeblogEntry"
+    file="blogentry.xsl"
+    />
+</configure>''')
+        self.assertEqual(stylesheet_registry.listStylesheetNames(),
+                         (u'marshall:Blog:Weblog', u'propertymanager:BlogEntry:WeblogEntry'))
+
+def xsltSetUp(test):
+    sectionsSetUp(test)
+
+    class XSLTSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(XSLTSource, self).__init__(*args, **kw)
+            self.sample = (
+                {},
+                {'_type': 'Weblog'},
+                {'_old_type': 'Blog'},
+                {'_old_type': 'Blog',
+                 '_type': 'Weblog',
+                 '_files': {'manifest': {'data': 'xml', 'name': 'marshall.xml'}}},
+                {'_old_type': 'Blog',
+                 '_type': 'Weblog',
+                 '_files': {'marshall': {'data': 'xml', 'name': 'marshall.xml'}}},
+            )
+
+    provideUtility(XSLTSource,
+        name=u'quintagroup.transmogrifier.tests.xsltsource')
+
+    import tempfile
+    from quintagroup.transmogrifier.xslt import XSLTSection, stylesheet_registry
+
+    XSLTSection.applyTransformations = lambda xml, xslt: 'transformed xml'
+
+    tmp = tempfile.NamedTemporaryFile('w+', suffix='.xsl')
+    tmp.write("stylesheet")
+    tmp.flush()
+
+    stylesheet_registry.registerStylesheet('marshall', 'Blog', 'Weblog', tmp.name)
+
+def test_suite():
+    import sys
+    suite = unittest.findTestCases(sys.modules[__name__])
+    suite.addTests((
+        doctest.DocFileSuite(
+            'sitewalker.txt',
+            setUp=siteWalkerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'manifest.txt',
+            setUp=manifestSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'marshall.txt',
+            setUp=marshallSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'propertymanager.txt',
+            setUp=propertyManagerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'discussioncontainer.txt',
+            setUp=discussionContainerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'exportadapters.txt',
+            setUp=exportAdaptersSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'writer.txt',
+            setUp=writerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'reader.txt',
+            setUp=readerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'manifest_import.txt',
+            setUp=manifestImportSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'substitution.txt',
+            setUp=substitutionSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'xslt.txt',
+            setUp=xsltSetUp, tearDown=tearDown),
+    ))
+    return suite
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/version.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/version.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/version.txt	(revision 275)
@@ -0,0 +1,1 @@
+0.1
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/writer.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/writer.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/writer.py	(revision 275)
@@ -0,0 +1,61 @@
+import os.path
+import time
+
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.GenericSetup import context
+from Products.CMFCore import utils
+
+class WriterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        if 'prefix' in options:
+            self.prefix = options['prefix'].strip()
+        else:
+            self.prefix = ''
+
+        context_type = options.get('context', 'tarball').strip()
+
+        setup_tool = utils.getToolByName(self.context, 'portal_setup')
+        if context_type == 'directory':
+            profile_path = options.get('path', '')
+            self.export_context = context.DirectoryExportContext(setup_tool, profile_path)
+        elif context_type == 'tarball':
+            self.export_context = context.TarballExportContext(setup_tool)
+        elif context_type == 'snapshot':
+            items = ('snapshot',) + time.gmtime()[:6]
+            snapshot_id = '%s-%4d%02d%02d%02d%02d%02d' % items
+            self.export_context = context.SnapshotExportContext(setup_tool, snapshot_id)
+        else:
+            self.export_context = context.TarballExportContext(setup_tool)
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey): # path doesn't exist or no data to write
+                yield item; continue
+
+            item['_export_context'] = self.export_context
+
+            path = item[pathkey]
+
+            item_path = os.path.join(self.prefix, path)
+            item_path.rstrip('/')
+
+            for k, v in item[fileskey].items():
+                self.export_context.writeDataFile(v['name'], v['data'], 'text/xml', subdir=item_path)
+
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/writer.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/writer.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/writer.txt	(revision 275)
@@ -0,0 +1,96 @@
+Writer section
+==============
+
+A writer pipeline section is used to store files in different GenericSetup
+export contexts. The writer section blueprint name is 
+``quintagroup.transmogrifier.writer``.
+
+>>> writer = """
+... [transmogrifier]
+... pipeline =
+...     writersource
+...     writer
+...     dataprinter
+...     
+... [writersource]
+... blueprint = quintagroup.transmogrifier.tests.writersource
+... 
+... [writer]
+... blueprint = quintagroup.transmogrifier.writer
+... prefix = structure
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... prettyprint = true
+... print = 
+...     _export_context
+...     _wrote
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writer', writer)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writer') # doctest: +ELLIPSIS, +REPORT_NDIFF
+[('structure/spam/eggs/foo/.second.xml', 'other data', 'text/xml'),
+ ('structure/spam/eggs/foo/.first.xml', 'some data', 'text/xml')]
+[('structure/spam/eggs/foo/.second.xml', 'other data', 'text/xml'),
+ ('structure/spam/eggs/foo/.first.xml', 'some data', 'text/xml'),
+ ('structure/other/path/.third.xml', 'some data', 'text/xml')]
+
+Now we test if writer context is properly controlled by ``context`` option.
+
+>>> writerdefault = """
+... [transmogrifier]
+... pipeline =
+...     source
+...     writer
+...     printer
+...     
+... [source]
+... blueprint = quintagroup.transmogrifier.tests.singleitemsource
+... 
+... [writer]
+... blueprint = quintagroup.transmogrifier.writer
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writerdefault', writerdefault)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writerdefault') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Tarball args=(...) _wrote=[]>,
+ '_files': {},
+ '_path': ''}
+>>> writerdir = """
+... [transmogrifier]
+... include = quintagroup.transmogrifier.tests.writerdefault
+... 
+... [writer]
+... context = directory
+... path = content
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writerdir', writerdir)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writerdir') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Directory args=(..., 'content') _wrote=[]>,
+ '_files': {},
+ '_path': ''}
+>>> writertar = """
+... [transmogrifier]
+... include = quintagroup.transmogrifier.tests.writerdefault
+...
+... [writer]
+... context = tarball
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writertar', writertar)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writertar') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Tarball args=(...) _wrote=[]>,
+ '_files': {},
+ '_path': ''}
+>>> writerdb = """
+... [transmogrifier]
+... include = quintagroup.transmogrifier.tests.writerdefault
+...
+... [writer]
+... context = snapshot
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writerdb', writerdb)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writerdb') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Snapshot args=(..., 'snapshot-...') _wrote=[]>,
+ '_files': {},
+ '_path': ''}
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/xslt.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/xslt.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/xslt.py	(revision 275)
@@ -0,0 +1,154 @@
+try:
+    import libxml2
+    import libxslt
+except ImportError:
+    HAS_LIBS = False
+else:
+    HAS_LIBS = True
+
+from zope.interface import classProvides, implements, Interface
+from zope.configuration.fields import Path
+from zope.schema import TextLine
+
+from collective.transmogrifier.interfaces import ISectionBlueprint
+from collective.transmogrifier.interfaces import ISection
+from collective.transmogrifier.utils import defaultMatcher
+
+class StylesheetRegistry(object):
+    def __init__(self):
+        self.clear()
+
+    def clear(self):
+        self._stylesheet_info = {}
+
+    def registerStylesheet(self, source, from_, to, file):
+        name = "%s:%s" % (from_, to)
+        if source in self._stylesheet_info:
+            if name in self._stylesheet_info[source]:
+                raise KeyError('Duplicate stylesheet registration: %s %s %s' % 
+                    (source, from_, to))
+        source = self._stylesheet_info.setdefault(source, {})
+        source[name] = {
+            'from_': from_,
+            'to': to,
+            'file': file
+        }
+
+    def getStylesheet(self, source, from_, to):
+        name = "%s:%s" % (from_, to)
+        try:
+            return self._stylesheet_info[source][name]
+        except KeyError:
+            return None
+
+    def listStylesheetNames(self):
+        names = []
+        for k, v in self._stylesheet_info.items():
+            for name in v.keys():
+                names.append("%s:%s" % (k, name))
+        return tuple(names)
+
+stylesheet_registry = StylesheetRegistry()
+
+# Test cleanup support
+from zope.testing.cleanup import addCleanUp
+addCleanUp(stylesheet_registry.clear)
+del addCleanUp
+
+class IStylesheetDirective(Interface):
+    """ Register XSLT file with the global registry.
+    """
+
+    source = TextLine(
+        title=u"Source",
+        description=u"The source of XML data.",
+        required=True
+        )
+
+    from_ = TextLine(
+        title=u"From",
+        description=u"Value which describes XML data before transformation.",
+        required=True
+        )
+
+    to = TextLine(
+        title=u"To",
+        description=u"Value which describes XML data after transformation.",
+        required=True
+        )
+
+    file = Path(
+        title=u"XSLT file",
+        description=u"The XSLT file to register.",
+        required=True
+        )
+
+
+def stylesheet(_context, source, from_, to, file):
+    """Add a new stylesheet to the registry"""
+
+    _context.action(
+        discriminator=('stylesheet', source, from_, to),
+        callable=stylesheet_registry.registerStylesheet,
+        args=(source, from_, to, file))
+
+
+class XSLTSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+        self.source = options.get('source', 'marshall').strip()
+        self.fromkey = defaultMatcher(options, 'from-key', name, 'from')
+        self.tokey = defaultMatcher(options, 'to-key', name, 'to')
+
+        self.previous = previous
+
+    def __iter__(self):
+        source = self.source
+        for item in self.previous:
+            fileskey = self.fileskey(*item.keys())[0]
+            fromkey = self.fromkey(*item.keys())[0]
+            tokey = self.tokey(*item.keys())[0]
+
+            if not (fileskey and fromkey and tokey):
+                yield item; continue
+
+            if not (source in item[fileskey] and item[fileskey][source]):
+                yield item; continue
+
+            from_ = item[fromkey]
+            to = item[tokey]
+            stylesheet_info = stylesheet_registry.getStylesheet(source, from_, to)
+
+            if stylesheet_info is None:
+                yield item; continue
+
+            fp = file(stylesheet_info['file'])
+            stylesheet = fp.read()
+            fp.close()
+
+            source_dict = item[filekey][source]
+            source_dict['data'] = self.applyTransformations(source_dict['data'], stylesheet)
+
+            yield item
+
+    def applyTransformations(self, xml, xslt):
+        if not HAS_LIBS:
+            return xml
+        # parse document
+        doc = libxml2.parseDoc(xml)
+        # parse stylesheet
+        styledoc = libxml2.parseDoc(xslt)
+        # make style object
+        style = libxslt.parseStylesheetDoc(styledoc)
+        # apply style to document
+        result = style.applyStylesheet(doc, None)
+        # write result to a file
+        transformed = style.saveResultToString(result)
+        # free all documents
+        style.freeStylesheet()
+        doc.freeDoc()
+        result.freeDoc()
+        return transformed
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/xslt.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/xslt.txt	(revision 275)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/xslt.txt	(revision 275)
@@ -0,0 +1,57 @@
+XSLT section
+============
+
+A XSLT pipeline section lets you apply stylesheet to some XML data stored on
+item. The XSLT section blueprint name is ``quintagroup.transmogrifier.xslt``.
+
+That data need to be transformed is determined by ``source`` option. It gives
+the key in dictionay where data is stored. Dictionary is standard structure
+for storing files data in other our sections. It's stored on item on ``_files``
+key, but this can be changed with ``files-key`` option.
+
+Usually stylesheets are used to transform data returned by ``marshall`` section.
+We register those stylesheets in ZCML with special directive, for example:
+
+    <stylesheet xmlns="http://namespaces.zope.org/transmogrifier"
+        from="OldPortalType"
+        to="NewPortalType"
+        source="marshall"
+        file="path/to/stylesheet.xsl"
+        />
+
+``source`` atribute in this directive is similar to that in section option.
+
+Which stylesheet to use is determined for every pipeline item by inspecting
+it's values stored on keys specified in ``from-key`` and ``to-key`` section
+options. If item don't has those keys, it will be skepped.
+
+>>> xslt = """
+... [transmogrifier]
+... pipeline =
+...     xsltsource
+...     xslt
+...     printer
+... 
+... [xsltsource]
+... blueprint = quintagroup.transmogrifier.tests.xsltsource
+... 
+... [xslt]
+... blueprint = quintagroup.transmogrifier.xslt
+... source = marshall
+... from-key = _old_type
+... to-key = _type
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.xslt', xslt)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.xslt') # doctest: +ELLIPSIS
+{}
+{'_type': 'Weblog'}
+{'_old_type': 'Blog'}
+{'_files': {'manifest': {'data': 'xml', 'name': 'manifest.xml'}},
+ '_old_type': 'Blog',
+ '_type': 'Weblog'}
+{'_files': {'marshall': {'data': 'transformed xml', 'name': 'marshall.xml'}},
+ '_old_type': 'Blog',
+ '_type': 'Weblog'}
Index: quintagroup.transmogrifier/trunk/setup.cfg
===================================================================
--- quintagroup.transmogrifier/trunk/setup.cfg	(revision 275)
+++ quintagroup.transmogrifier/trunk/setup.cfg	(revision 275)
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
Index: quintagroup.transmogrifier/trunk/setup.py
===================================================================
--- quintagroup.transmogrifier/trunk/setup.py	(revision 275)
+++ quintagroup.transmogrifier/trunk/setup.py	(revision 275)
@@ -0,0 +1,34 @@
+from setuptools import setup, find_packages
+import os
+
+version = '0.1'
+
+setup(name='quintagroup.transmogrifier',
+      version=version,
+      description="Plone blueprints for collective.transmogrifier pipelines.",
+      long_description=open("README.txt").read() + "\n" +
+                       open(os.path.join("docs", "HISTORY.txt")).read(),
+      # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      classifiers=[
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='',
+      author='Quintagroup',
+      author_email='info@quintagroup.com',
+      url='',
+      license='GPL',
+      packages=find_packages(exclude=['ez_setup']),
+      namespace_packages=['quintagroup'],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          'setuptools',
+          'collective.transmogrifier',
+          'plone.app.transmogrifier'
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )
